Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
The table of contents is too big for display.
Diff view
Diff view
  •  
  •  
  •  
88 changes: 88 additions & 0 deletions .github/workflows/windows-napi.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
name: windows-napi

# NOTE: `paths` filters deliberately do NOT accompany the tag trigger — a tag push can have an
# empty changed-file set, and paths+tags together silently skip the run.
on:
push:
tags:
- 'windows-napi-v*'
pull_request:
paths:
- 'windows-napi/**'
- 'runtime/**'
- 'metadata/**'
- '.github/workflows/windows-napi.yml'
workflow_dispatch:

env:
CARGO_TERM_COLOR: always

jobs:
build:
runs-on: windows-latest
strategy:
fail-fast: false
matrix:
settings:
- target: x86_64-pc-windows-msvc
- target: aarch64-pc-windows-msvc
name: build ${{ matrix.settings.target }}
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: 20
- uses: dtolnay/rust-toolchain@stable
with:
targets: ${{ matrix.settings.target }}
- uses: Swatinem/rust-cache@v2
with:
key: ${{ matrix.settings.target }}
- name: Install npm dependencies
working-directory: windows-napi
run: npm ci
- name: Build .node
working-directory: windows-napi
run: npx napi build --platform --release --target ${{ matrix.settings.target }}
- name: Run test suite (x64 only — the runner cannot execute arm64 binaries)
if: matrix.settings.target == 'x86_64-pc-windows-msvc'
working-directory: windows-napi
# Server runner SKUs can lack optional feature packs some suites touch (e.g. Media
# Foundation for the MediaPlayer event test), so failures here warn instead of block;
# the authoritative run is the local one on a client SKU.
continue-on-error: true
run: npm test
- uses: actions/upload-artifact@v4
with:
name: bindings-${{ matrix.settings.target }}
path: windows-napi/windows.*.node
if-no-files-found: error

publish:
name: publish to npm
if: startsWith(github.ref, 'refs/tags/windows-napi-v')
needs: build
runs-on: windows-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: 20
registry-url: https://registry.npmjs.org
- name: Install npm dependencies
working-directory: windows-napi
run: npm ci
- name: Download all binding artifacts
uses: actions/download-artifact@v4
with:
path: windows-napi/artifacts
- name: Distribute artifacts into npm/<triple> sub-packages
working-directory: windows-napi
run: npx napi artifacts
- name: Publish
working-directory: windows-napi
# `napi prepublish` (prepublishOnly script) publishes each npm/<triple> sub-package,
# then the root package publishes with them as optionalDependencies.
run: npm publish --access public
env:
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
9 changes: 9 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,6 +1,15 @@
# Rust build artifacts (root + nested workspace/package crates)
/target
target/
/**/*.rs.bk
/Cargo.lock

# Node
node_modules/

# Logs
*.log

.DS_Store
.vs

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

[workspace.dependencies]
windows = "0.62.2"
Expand Down
114 changes: 114 additions & 0 deletions docs/napi-consumption.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,114 @@
# Consuming `@nativescript/windows` from Node, Bun, and Deno

The package is a napi-rs native addon (`windows.<triple>.node`) plus a small JS layer
(`index.js` from napi-rs, and `nswinrt.js` with the WinRT ergonomics: `toPromise`, auto-pump,
`interop.*`). Because it targets the **Node-API** ABI, any runtime that implements Node-API can
load it — the engine underneath differs, but the interop code does not.

## Loading

### Node.js (V8)
```js
const { Windows, toPromise, enableAutoPump } = require('@nativescript/windows/nswinrt.js');
enableAutoPump();
```
Or ESM via `createRequire`. No flags needed. This is the primary, fully-tested path.

### Bun (JavaScriptCore)
Bun implements Node-API and loads `.node` addons through the same `require`/`import`:
```js
import { Windows, toPromise } from '@nativescript/windows/nswinrt.js';
```
This is the **JSC-on-Windows** route — no WebKit embedding needed; Bun provides JSC + Node-API.

### Deno (V8)
Deno implements Node-API behind its Node-compat layer. Import via the `npm:` specifier (or
`createRequire`), and grant the permissions the addon needs:
```
deno run --allow-ffi --allow-read --allow-env --allow-write app.js
```
```js
import { Windows, toPromise } from "npm:@nativescript/windows/nswinrt.js";
```
`--allow-ffi` is required for native addons; `--allow-read` is needed for winmd discovery
(below).

> The native `.node` is identical across all three — only the host engine changes. `nswinrt.js`
> is plain CommonJS and uses only `require('./index.js')` + timers, so it runs unmodified on all
> three runtimes.

## winmd discovery (the important part)

WinRT type metadata comes from two sources:

1. **System / OS types** (`Windows.*`) — resolved by the OS via `RoGetMetaDataFile`. **No files
ship with the app**; these always resolve on any Windows machine, in any runtime. Everything
in the test suite (JsonObject, Uri, Calendar, ThreadPool, CryptographicBuffer, …) is system
metadata and needs zero configuration.

2. **Third-party / app `.winmd`** (WebView2, your own WinRT components) — **not** discoverable by
the OS; they must be registered explicitly.

### How the napi package finds them

The rusty_v8 runtime auto-scans for `.winmd` in `Runtime::new` (exe dir + app root). **The napi
path never constructs a `Runtime`** — it drives interop directly on the host's `napi_env` — so
that scan is reproduced on the napi side:

- **Automatic:** the first `getNamespace(...)` (or any interop call) runs a one-time scan of the
**current working directory** and the **host executable's directory**
(`scan_default_winmd_dirs`). Drop your `.winmd` next to where you launch and it's picked up.
- **Explicit (recommended for apps):** point the runtime at your metadata directly —
```js
const { interop } = require('@nativescript/windows/nswinrt.js');
interop.registerWinmd('C:/app/metadata/MyComponent.winmd'); // one file
interop.scanWinmdDir('C:/app/metadata'); // whole folder → count
```
Call these **before** touching the corresponding namespaces.

### Why auto-scan differs from the standalone runtime

Under the standalone (embedded-V8) runtime, `current_exe` is *your app's* exe, so the exe-dir
scan finds app-bundled winmd automatically. Under Node/Bun/Deno, `current_exe` is
`node.exe`/`bun.exe`/`deno.exe` — so the exe-dir scan covers the runtime's own directory, and the
**cwd scan + explicit `registerWinmd`/`scanWinmdDir`** are how app metadata gets loaded. A CLI
that launches apps should either `chdir` to the metadata location or call `scanWinmdDir` on
startup. (This matches the NativeScript CLI's existing responsibility to deploy the `.winmd`.)

## The message loop / async

WinRT async completions (`IAsyncAction`/`IAsyncOperation`) are delivered on the STA thread's
message queue, so that queue must be pumped for a `Promise` to settle. `nswinrt.js` handles this:

- `enableAutoPump()` — installs a ref-counted pump on the host timer loop; it's ref'd only while
a WinRT `Promise` is outstanding (so the process still exits normally). After this, plain
`await toPromise(op)` just works.
- `awaitWithPump(promise, timeoutMs)` — explicit alternative that pumps inline until settle.

This is the same on all three runtimes (all expose `setInterval`/`setImmediate`).

## Natural-syntax extras

Beyond method/property/event access, the napi backend supports:

- **Keyed maps (`IMap`/`IMapView`/`IPropertySet`)** — classes whose default interface is a map
(`StringMap`, `PropertySet`, `ValueSet`, `ApplicationDataContainerSettings`, …) and
interface-typed map returns get keyed sugar: `m['key']` → `Lookup`, `m['key'] = v` → `Insert`,
`'key' in m` → `HasKey`, plus `m.length` → `Size`. WinRT member names always win over map
keys (use `m.Lookup('Size')` for a shadowed key). `PropertySet`/`ValueSet` values box on
insert and unbox (`IPropertyValue` primitives) on lookup, so `ps['n'] = 42; ps['n'] === 42`.
Classes that merely *also* implement `IMap` alongside a richer identity (e.g. `JsonObject`)
stay host objects — call `Lookup`/`Insert` explicitly there.
- **Subclassing** — `class Sub extends WinRTClass` works on both object models (host objects
and Proxy-path instances): constructor args flow through `super(...)`, overrides can call
`super.Method()`, instances satisfy `instanceof` for both the subclass and the WinRT class,
and subclass instances marshal as WinRT arguments (identity-cached, so the same JS object
comes back out).
- **Composable (non-sealed) constructors** — the composition (null-outer) ABI is wired; note
every activatable non-sealed WinRT class lives in `Windows.UI.Xaml`, whose activation
requires a XAML-initialized thread. Headless those ctors surface the factory's
`RPC_E_WRONG_THREAD` as a normal catchable JS error (identical to the classic runtime).
`Windows.UI.Composition` object trees (non-sealed bases) work headless — the backend creates
a `DispatcherQueue` for the JS thread at init.
- **`setImmediate`/`clearImmediate`** — provided by the standalone engine hosts' event loop
(Node/Bun/Deno already have their own).
33 changes: 33 additions & 0 deletions dotnet-bridge-tests/ManagedSubclassFixtures.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
namespace DotNetBridgeTests;

// Test-only stand-ins for a real WinRT base class with virtuals JS may or may not override —
// mirrors why MeasureOverrideBase exists (the real target, a WinUI FrameworkElement, needs the
// full WinRT runtime the xunit host doesn't have).
public class CallBaseFallbackBase
{
public virtual string Describe()
{
return "base-describe";
}

public virtual string Greet(string name)
{
return "base-greet:" + name;
}

public virtual string Text
{
get { return "base-text"; }
set { LastSetText = value; }
}

public string? LastSetText;
}

// Test-only stand-in for a WinRT interface (e.g. INotifyPropertyChanged) JS implements directly
// without a real .NET base class — proves AddInterfaceImplementation + dispatch mechanically,
// independent of the CsWinRT CCW question (covered separately by real-app validation).
public interface ITestNotify
{
string Notify(string message);
}
Loading
Loading