Skip to content

Commit d3b7067

Browse files
committed
feat: napi support
1 parent 1fa5971 commit d3b7067

311 files changed

Lines changed: 233027 additions & 427 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.github/workflows/windows-napi.yml

Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,88 @@
1+
name: windows-napi
2+
3+
# NOTE: `paths` filters deliberately do NOT accompany the tag trigger — a tag push can have an
4+
# empty changed-file set, and paths+tags together silently skip the run.
5+
on:
6+
push:
7+
tags:
8+
- 'windows-napi-v*'
9+
pull_request:
10+
paths:
11+
- 'windows-napi/**'
12+
- 'runtime/**'
13+
- 'metadata/**'
14+
- '.github/workflows/windows-napi.yml'
15+
workflow_dispatch:
16+
17+
env:
18+
CARGO_TERM_COLOR: always
19+
20+
jobs:
21+
build:
22+
runs-on: windows-latest
23+
strategy:
24+
fail-fast: false
25+
matrix:
26+
settings:
27+
- target: x86_64-pc-windows-msvc
28+
- target: aarch64-pc-windows-msvc
29+
name: build ${{ matrix.settings.target }}
30+
steps:
31+
- uses: actions/checkout@v4
32+
- uses: actions/setup-node@v4
33+
with:
34+
node-version: 20
35+
- uses: dtolnay/rust-toolchain@stable
36+
with:
37+
targets: ${{ matrix.settings.target }}
38+
- uses: Swatinem/rust-cache@v2
39+
with:
40+
key: ${{ matrix.settings.target }}
41+
- name: Install npm dependencies
42+
working-directory: windows-napi
43+
run: npm ci
44+
- name: Build .node
45+
working-directory: windows-napi
46+
run: npx napi build --platform --release --target ${{ matrix.settings.target }}
47+
- name: Run test suite (x64 only — the runner cannot execute arm64 binaries)
48+
if: matrix.settings.target == 'x86_64-pc-windows-msvc'
49+
working-directory: windows-napi
50+
# Server runner SKUs can lack optional feature packs some suites touch (e.g. Media
51+
# Foundation for the MediaPlayer event test), so failures here warn instead of block;
52+
# the authoritative run is the local one on a client SKU.
53+
continue-on-error: true
54+
run: npm test
55+
- uses: actions/upload-artifact@v4
56+
with:
57+
name: bindings-${{ matrix.settings.target }}
58+
path: windows-napi/windows.*.node
59+
if-no-files-found: error
60+
61+
publish:
62+
name: publish to npm
63+
if: startsWith(github.ref, 'refs/tags/windows-napi-v')
64+
needs: build
65+
runs-on: windows-latest
66+
steps:
67+
- uses: actions/checkout@v4
68+
- uses: actions/setup-node@v4
69+
with:
70+
node-version: 20
71+
registry-url: https://registry.npmjs.org
72+
- name: Install npm dependencies
73+
working-directory: windows-napi
74+
run: npm ci
75+
- name: Download all binding artifacts
76+
uses: actions/download-artifact@v4
77+
with:
78+
path: windows-napi/artifacts
79+
- name: Distribute artifacts into npm/<triple> sub-packages
80+
working-directory: windows-napi
81+
run: npx napi artifacts
82+
- name: Publish
83+
working-directory: windows-napi
84+
# `napi prepublish` (prepublishOnly script) publishes each npm/<triple> sub-package,
85+
# then the root package publishes with them as optionalDependencies.
86+
run: npm publish --access public
87+
env:
88+
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}

.gitignore

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,15 @@
1+
# Rust build artifacts (root + nested workspace/package crates)
12
/target
3+
target/
24
/**/*.rs.bk
35
/Cargo.lock
6+
7+
# Node
8+
node_modules/
9+
10+
# Logs
11+
*.log
12+
413
.DS_Store
514
.vs
615

Cargo.toml

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
[workspace]
22
resolver = "2"
3-
members = ["metadata", "playground", "runtime", "runtime-binding-gen", "sbg", "nativescript", "typings-generator", "integration-tests", "runtime-devtools", "metadata-generator","tools/dotnet-tool"]
3+
members = ["metadata", "playground", "runtime", "runtime-binding-gen", "sbg", "nativescript", "typings-generator", "integration-tests", "runtime-devtools", "metadata-generator","tools/dotnet-tool", "windows-napi"]
4+
# Excluded so their C builds / prebuilt-engine links don't run on normal `cargo` invocations.
5+
exclude = ["packages/common", "packages/demo", "packages/windows-quickjs", "packages/windows-hermes", "packages/windows-jsc", "packages/windows-v8"]
46

57
[workspace.dependencies]
68
windows = "0.62.2"

docs/napi-consumption.md

Lines changed: 114 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,114 @@
1+
# Consuming `@nativescript/windows` from Node, Bun, and Deno
2+
3+
The package is a napi-rs native addon (`windows.<triple>.node`) plus a small JS layer
4+
(`index.js` from napi-rs, and `nswinrt.js` with the WinRT ergonomics: `toPromise`, auto-pump,
5+
`interop.*`). Because it targets the **Node-API** ABI, any runtime that implements Node-API can
6+
load it — the engine underneath differs, but the interop code does not.
7+
8+
## Loading
9+
10+
### Node.js (V8)
11+
```js
12+
const { Windows, toPromise, enableAutoPump } = require('@nativescript/windows/nswinrt.js');
13+
enableAutoPump();
14+
```
15+
Or ESM via `createRequire`. No flags needed. This is the primary, fully-tested path.
16+
17+
### Bun (JavaScriptCore)
18+
Bun implements Node-API and loads `.node` addons through the same `require`/`import`:
19+
```js
20+
import { Windows, toPromise } from '@nativescript/windows/nswinrt.js';
21+
```
22+
This is the **JSC-on-Windows** route — no WebKit embedding needed; Bun provides JSC + Node-API.
23+
24+
### Deno (V8)
25+
Deno implements Node-API behind its Node-compat layer. Import via the `npm:` specifier (or
26+
`createRequire`), and grant the permissions the addon needs:
27+
```
28+
deno run --allow-ffi --allow-read --allow-env --allow-write app.js
29+
```
30+
```js
31+
import { Windows, toPromise } from "npm:@nativescript/windows/nswinrt.js";
32+
```
33+
`--allow-ffi` is required for native addons; `--allow-read` is needed for winmd discovery
34+
(below).
35+
36+
> The native `.node` is identical across all three — only the host engine changes. `nswinrt.js`
37+
> is plain CommonJS and uses only `require('./index.js')` + timers, so it runs unmodified on all
38+
> three runtimes.
39+
40+
## winmd discovery (the important part)
41+
42+
WinRT type metadata comes from two sources:
43+
44+
1. **System / OS types** (`Windows.*`) — resolved by the OS via `RoGetMetaDataFile`. **No files
45+
ship with the app**; these always resolve on any Windows machine, in any runtime. Everything
46+
in the test suite (JsonObject, Uri, Calendar, ThreadPool, CryptographicBuffer, …) is system
47+
metadata and needs zero configuration.
48+
49+
2. **Third-party / app `.winmd`** (WebView2, your own WinRT components) — **not** discoverable by
50+
the OS; they must be registered explicitly.
51+
52+
### How the napi package finds them
53+
54+
The rusty_v8 runtime auto-scans for `.winmd` in `Runtime::new` (exe dir + app root). **The napi
55+
path never constructs a `Runtime`** — it drives interop directly on the host's `napi_env` — so
56+
that scan is reproduced on the napi side:
57+
58+
- **Automatic:** the first `getNamespace(...)` (or any interop call) runs a one-time scan of the
59+
**current working directory** and the **host executable's directory**
60+
(`scan_default_winmd_dirs`). Drop your `.winmd` next to where you launch and it's picked up.
61+
- **Explicit (recommended for apps):** point the runtime at your metadata directly —
62+
```js
63+
const { interop } = require('@nativescript/windows/nswinrt.js');
64+
interop.registerWinmd('C:/app/metadata/MyComponent.winmd'); // one file
65+
interop.scanWinmdDir('C:/app/metadata'); // whole folder → count
66+
```
67+
Call these **before** touching the corresponding namespaces.
68+
69+
### Why auto-scan differs from the standalone runtime
70+
71+
Under the standalone (embedded-V8) runtime, `current_exe` is *your app's* exe, so the exe-dir
72+
scan finds app-bundled winmd automatically. Under Node/Bun/Deno, `current_exe` is
73+
`node.exe`/`bun.exe`/`deno.exe` — so the exe-dir scan covers the runtime's own directory, and the
74+
**cwd scan + explicit `registerWinmd`/`scanWinmdDir`** are how app metadata gets loaded. A CLI
75+
that launches apps should either `chdir` to the metadata location or call `scanWinmdDir` on
76+
startup. (This matches the NativeScript CLI's existing responsibility to deploy the `.winmd`.)
77+
78+
## The message loop / async
79+
80+
WinRT async completions (`IAsyncAction`/`IAsyncOperation`) are delivered on the STA thread's
81+
message queue, so that queue must be pumped for a `Promise` to settle. `nswinrt.js` handles this:
82+
83+
- `enableAutoPump()` — installs a ref-counted pump on the host timer loop; it's ref'd only while
84+
a WinRT `Promise` is outstanding (so the process still exits normally). After this, plain
85+
`await toPromise(op)` just works.
86+
- `awaitWithPump(promise, timeoutMs)` — explicit alternative that pumps inline until settle.
87+
88+
This is the same on all three runtimes (all expose `setInterval`/`setImmediate`).
89+
90+
## Natural-syntax extras
91+
92+
Beyond method/property/event access, the napi backend supports:
93+
94+
- **Keyed maps (`IMap`/`IMapView`/`IPropertySet`)** — classes whose default interface is a map
95+
(`StringMap`, `PropertySet`, `ValueSet`, `ApplicationDataContainerSettings`, …) and
96+
interface-typed map returns get keyed sugar: `m['key']``Lookup`, `m['key'] = v``Insert`,
97+
`'key' in m``HasKey`, plus `m.length``Size`. WinRT member names always win over map
98+
keys (use `m.Lookup('Size')` for a shadowed key). `PropertySet`/`ValueSet` values box on
99+
insert and unbox (`IPropertyValue` primitives) on lookup, so `ps['n'] = 42; ps['n'] === 42`.
100+
Classes that merely *also* implement `IMap` alongside a richer identity (e.g. `JsonObject`)
101+
stay host objects — call `Lookup`/`Insert` explicitly there.
102+
- **Subclassing**`class Sub extends WinRTClass` works on both object models (host objects
103+
and Proxy-path instances): constructor args flow through `super(...)`, overrides can call
104+
`super.Method()`, instances satisfy `instanceof` for both the subclass and the WinRT class,
105+
and subclass instances marshal as WinRT arguments (identity-cached, so the same JS object
106+
comes back out).
107+
- **Composable (non-sealed) constructors** — the composition (null-outer) ABI is wired; note
108+
every activatable non-sealed WinRT class lives in `Windows.UI.Xaml`, whose activation
109+
requires a XAML-initialized thread. Headless those ctors surface the factory's
110+
`RPC_E_WRONG_THREAD` as a normal catchable JS error (identical to the classic runtime).
111+
`Windows.UI.Composition` object trees (non-sealed bases) work headless — the backend creates
112+
a `DispatcherQueue` for the JS thread at init.
113+
- **`setImmediate`/`clearImmediate`** — provided by the standalone engine hosts' event loop
114+
(Node/Bun/Deno already have their own).
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
namespace DotNetBridgeTests;
2+
3+
// Test-only stand-ins for a real WinRT base class with virtuals JS may or may not override —
4+
// mirrors why MeasureOverrideBase exists (the real target, a WinUI FrameworkElement, needs the
5+
// full WinRT runtime the xunit host doesn't have).
6+
public class CallBaseFallbackBase
7+
{
8+
public virtual string Describe()
9+
{
10+
return "base-describe";
11+
}
12+
13+
public virtual string Greet(string name)
14+
{
15+
return "base-greet:" + name;
16+
}
17+
18+
public virtual string Text
19+
{
20+
get { return "base-text"; }
21+
set { LastSetText = value; }
22+
}
23+
24+
public string? LastSetText;
25+
}
26+
27+
// Test-only stand-in for a WinRT interface (e.g. INotifyPropertyChanged) JS implements directly
28+
// without a real .NET base class — proves AddInterfaceImplementation + dispatch mechanically,
29+
// independent of the CsWinRT CCW question (covered separately by real-app validation).
30+
public interface ITestNotify
31+
{
32+
string Notify(string message);
33+
}

0 commit comments

Comments
 (0)