Skip to content

Flat Win32 ([DllImport]) support: generated natural wrappers + Registry (stacks on #65) - #67

Open
Gordon Lam (yeelam-gordon) wants to merge 76 commits into
microsoft:mainfrom
yeelam-gordon:feat/win32-flat-tier2
Open

Flat Win32 ([DllImport]) support: generated natural wrappers + Registry (stacks on #65)#67
Gordon Lam (yeelam-gordon) wants to merge 76 commits into
microsoft:mainfrom
yeelam-gordon:feat/win32-flat-tier2

Conversation

@yeelam-gordon

@yeelam-gordon Gordon Lam (yeelam-gordon) commented Jul 22, 2026

Copy link
Copy Markdown
Contributor

Summary

Adds flat Win32 ([DllImport]) support to dynwinrt: the code generator now discovers plain C exports in Windows.Win32.winmd (static methods on Apis classes) and emits natural TS/JS wrappers, backed by a general LoadLibrary + GetProcAddress + libffi runtime path. Demonstrated end‑to‑end with a generated Registry wrapper that reads real values on live Windows.

⚠️ Stacks on the classic‑COM PR (#65) for shared napi/runtime plumbing (pointer, u64, raw‑pointer dispatch). Review/merge #65 first — until then this PR's diff shows classic + flat combined; afterward it reduces to the flat‑only delta.

What you write (generated API)

// Read a registry value — generated flat-Win32 wrappers (LoadLibrary + GetProcAddress + libffi)
import { regOpenKeyExW, regCloseKey } from './generated/registry/Apis.js';
import { REG_SAM_FLAGS } from './generated/registry/REG_SAM_FLAGS.js';

const HKEY_LOCAL_MACHINE = 0x80000002n;
const { status, phkResult } = regOpenKeyExW(
  HKEY_LOCAL_MACHINE,
  'SOFTWARE\\Microsoft\\Windows NT\\CurrentVersion',
  0,
  REG_SAM_FLAGS.KEY_READ,
);
// status === 0 (ERROR_SUCCESS); phkResult is a bigint HKEY.
// [out] params become return fields; caller-allocated buffers + in/out size are handled for you.
regCloseKey(phkResult);

Motivation

Classic COM is vtable/object‑based; flat Win32 APIs (RegOpenKeyExW, CreateFileW, CredReadW, GetForegroundWindow, …) are plain DLL exports — a fundamentally different invocation path (no object, no vtable). A large share of the Electron native modules people ship today is exactly this cohort (registry, credential, window/input access), so covering it is what makes dynwinrt relevant to those apps.

Architecture

Windows.Win32.winmd ── [DllImport] static methods on `Apis` ──▶ dynwinrt-codegen ──▶ natural .js + .d.ts
                                                                                        │
                                                                                        ▼
                                     LoadLibraryW + GetProcAddress + libffi  (flat_invoke)
  • New flat IR + emitter, separate from the COM/WinRT paths (which are untouched).
  • Marshalling: LPCWSTRstring, [out] pointer params → return values, HANDLE/HKEYbigint, caller‑allocated output buffers + in/out size (LPDWORD), UTF‑16 decode, LSTATUS/Win32 error surfaced (0 = success; non‑zero throws).
  • Fails loud on shapes not yet representable (e.g. I64/F32/F64 returns) — it skips/erroring at generation time rather than silently emitting a wrong‑value wrapper.

Developer experience (sample usage)

High‑level generated wrapper:

import { Registry } from './generated/registry.js';

const name = Registry.getString(
  'HKEY_LOCAL_MACHINE',
  'SOFTWARE\\Microsoft\\Windows NT\\CurrentVersion',
  'ProductName');            // => "Windows 11 Pro"

The underlying generated exports are also emitted (typed, flatInvoke hidden):

import { regOpenKeyExW, regQueryValueExW, regCloseKey } from './generated/Apis.js';
// regOpenKeyExW(hKey, subKey, 0, samDesired) -> { status, phkResult }

Generate — partial, per namespace/class:

dynwinrt-codegen generate --winmd Windows.Win32.winmd \
  --namespace Windows.Win32.System.Registry --class-name Apis --output ./generated

Tests

Layer Coverage
Runtime — cargo test -p dynwinrt flat_invoke scalar/pointer/buffer marshalling; 5 live‑advapi32 registry tests (out‑handle via PHKEY, caller buffer, in/out LPDWORD size, ERROR_MORE_DATA, null‑size query) → 96 passed + regression
Codegen — cargo test -p dynwinrt-codegen flat [DllImport] discovery, natural emit, [out]→return, fail‑loud unsupported returns, deterministic snapshot — TDD (6 unit + 10 integration, red→green)
Live Node E2E flat_registry.mjs reads a real ProductName via the generated wrapper (plus corner cases: missing value/subkey → typed error)

Combined with #65, the full stack was verified together: cargo (96 + regression), codegen, 5 Node E2Es (taskbar/registry/dtm/smtc/flat_registry), WinRT pipeline py 29/29 · ts 28/28. An automated code‑review loop was run to convergence (buffer keep‑alive/lifetime, PWSTR/PSTR handling, fail‑loud, enum dedup).

Scope / follow‑on

The registry vertical proves the mechanism. Broader flat‑API coverage — by‑value structs, arrays, other DLLs, PWSTR‑out → string marshal‑back, callbacks/function pointers, ANSI/W auto‑preference — is follow‑on work; unsupported shapes fail loud today, so nothing is silently wrong.


⚠️ Known limitations (flat codegen coverage)

The flat layer covers the common scalar / string-in / enum / handle / [out]-scalar path cleanly. The harder aggregate/callback cases are not yet covered and fail loud (skip + warning) — never silently wrong:

  • Opaque structs / caller-allocated byte buffers / void* / mutable PWSTR-out — emitted as a raw Buffer | null slot the caller must allocate and pack (e.g. RegQueryValueExW lpData). The truly dynamic-size case is inherent to the Win32 contract.
  • Callbacks / function pointers (EnumWindows, hooks) — opaque Ptr; no JS-function -> native thunk, so these are effectively uncallable today.
  • By-value struct param/return — skipped (fail-loud); this drives the skip counts below.

Measured skip rates on real Windows.Win32.winmd (--class-name Apis):

Namespace Generated Skipped Notes
System.Registry 83 0
Security.Credentials 127 67 100% of skips are Smart Card SCard*; every Credential Manager fn (credReadW/WriteW/EnumerateW/DeleteW/Free) generates
Graphics.Dwm 31 5
UI.Shell 693 145 struct-heavy niche calls
UI.WindowsAndMessaging 424 85

Skips concentrate in struct-heavy niche sub-families, not mainstream functions; nothing is silently wrong (kept = correct, skipped = warned).

Handle ergonomics (#4): handle-value params (HWND / HANDLE / HKEY) accept bigint | number and work today, but an Electron getNativeWindowHandle() Buffer must be unwrapped to a value first (e.g. .readBigUInt64LE(0)). A codegen improvement that accepts the Buffer directly — reading its little-endian bytes as the handle VALUE — is prepared and held pending the foundational rework in #65 (to avoid churn in the same projection layer).
Foundational classic-COM ABI issues (caller-owned buffers, namespace-mode vtable slots, ISize/USize width, pointer-ownership double-adopt, typed-array detachment, BSTR/enum, packaging) are tracked in #65 (see leileizhang (@lei9444)'s review) and apply to the classic-COM half this PR stacks on.

Gordon Lam (yeelam-gordon) and others added 21 commits July 22, 2026 15:12
…shared napi plumbing

Reorganizes the Win32/COM work into a self-contained classic-vertical
that pairs the classic-COM runtime (call.rs RawPtr, classic_com.rs,
signature::define_from_iunknown), classic-COM/interop codegen
(codegen::com, main.rs --class-name COM path), and the shared napi
plumbing (coCreateInstance, registerInterfaceUnknown, pointer,
iidPointer, asPointerBigint, u64 Either fix, createTestHwnd) needed by
the ITaskbarList3 / DTM / SMTC E2Es.

Flat-Win32 pieces (flat_call.rs, codegen::flat, flatInvoke napi,
Apis/DllImport meta) are intentionally absent from this branch and are
layered back on top in reorg/flat-vertical.

The classic E2Es (taskbarlist.mjs, dtm.mjs, smtc.mjs) acquire a
process-owned HWND through a new tiny napi helper createTestHwnd()
(delegates to CreateWindowExW via windows-rs) instead of the
flatInvoke-based path used in the fully-integrated reference. A small
hwnd.mjs helper module encapsulates the call.

Gauntlet (green):
  - cargo test -p dynwinrt: 83 passed + 1 winrt_regression
  - cargo test -p dynwinrt-codegen: all suites green
  - napi build (release): OK
  - Node E2Es: taskbarlist.mjs / dtm.mjs / smtc.mjs PASS
  - tests\e2e_test.ps1 -SkipBuild: py 29/29, ts 28/28

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
… enum`

Mirror the WinRT enum generator (render_enum_dts in
codegen/javascript/render/declarations.rs) so classic-COM enum .d.ts stays
consumable under TS isolatedModules and matches the JS Object.freeze runtime
shape.

Snapshot updated (tests/snapshots/itaskbarlist3/TBPFLAG.d.ts); the .js output
is unchanged so the taskbarlist E2E still works via TBPFLAG.TBPF_NORMAL member
access.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…t-param projection

- meta::parse_com_interface_from_index now tracks explicit termination at
  IUnknown or IInspectable and returns None if the base-chain walk exits
  without reaching either, instead of silently defaulting to IInspectable
  (base offset 6). This prevents wrong absolute vtable slots when a winmd
  has an unexpected inheritance shape or missing interface_impls.

- unwrap_return_js: for opaque Win32 handle out-params (HWND, PWSTR, ...)
  emit `.asPointerBigint()` instead of `.toI64()`. The runtime may
  produce WinRTValue::Object/RawPtr/Null when the handle's inner `Value`
  field is a void*-shaped type, and `.toI64()` panics on those variants
  (its fallback `.toNumber()` panics for non-numeric variants).
  `.asPointerBigint()` cleanly handles all three pointer representations.

All snapshots and E2Es unaffected: current interfaces have no [out] handle
params, and the two rooted tests (ITaskbarList3 → IUnknown, SMTC interop
→ IInspectable) still pass.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…om_index

Add an early guard that returns None unless the resolved TypeDef has
`TypeAttributes::Interface` set. Prevents WinRT runtime classes,
structs, enums, and delegates from being mis-parsed by walking their
`interface_impls()` and flattening a bogus method list — which could
have quietly routed `--class-name *Interop` runtime classes through
the classic-COM code path in `main.rs`.

Callers see `None` and fall through to the correct WinRT path.
All existing tests + node/py/ts E2Es unaffected (real interfaces still
have the Interface attribute set).

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…path

- napi::create_test_hwnd() now caches the HWND in an AtomicUsize so
  repeated calls in a long-lived Node process (tests, REPL, Electron)
  don't accumulate window handles.

- codegen(com): handle typedef comments now say "Opaque Win32 handle or
  pointer newtype (e.g. HWND, PWSTR)" instead of just "Opaque Win32
  handle" — the aliases cover both handles and pointer newtypes like
  PWSTR/PCWSTR. Snapshots updated.

- meta::find_runtime_class_default_iid now collects all runtime-class
  matches for a simple name and refuses to pick when they resolve to
  distinct default-interface IIDs (cross-namespace collisions). Emits an
  explicit warning listing the candidates and returns None so callers
  fall through instead of silently generating interop wrappers with the
  wrong IID.

- DynWinRTValue.u64 number branch now takes f64 (not i64) so we can
  reject NaN, +/-Infinity, and fractional values explicitly. napi's
  previous i64 coercion silently truncated fractions and mis-handled
  non-finite inputs. Bigint path and MAX_SAFE_INTEGER bound unchanged.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…parsed

parse_com_interface_from_index previously logged a warning and continued
when parse_interface_with_offset returned None for a base interface.
That left slot_cursor unadvanced and produced a truncated flattened
method list, so the leaf interface's absolute vtable indices would be
off by however many base methods were missing. The debug_assert_eq!
below caught this in debug builds, but in release it was silently
compiled out — so codegen would emit wrappers that dispatch to the
wrong COM methods.

Now return None on any base-parse failure (with a warning naming both
the missing base and the leaf we're refusing to emit), so callers see
a clean skip rather than misgeneration.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
- MetadataTable::register_interface and register_interface_iunknown now
  route through create_interface_method_table*(iid, base_slot) BEFORE
  checking the name cache. That call is already assertive on base_slot
  mismatch (arena.rs:131). Previously a first-time
  register_interface(name, iid) with base_slot=6 would let a later
  register_interface_iunknown(name, iid) — expecting base_slot=3 —
  silently reuse the WinRT-shaped vtable and dispatch to the wrong
  absolute slots. Now the mismatch panics loudly.

- meta::find_runtime_class_default_iid: replaced the `?` on the
  default-interface TypeDef lookup with a `let-else { continue }`.
  A missing/unreadable interface TypeDef for one candidate no longer
  aborts the whole search — other matching runtime classes (or other
  DefaultAttribute impls on the same class) can still resolve.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Stacks flat-Win32 support on top of reorg/classic-vertical, restoring
the full functionality of the reference tree at feat/win32-com-codegen:
- crates/dynwinrt/src/flat_call.rs — flat runtime (LoadLibrary +
  GetProcAddress + libffi + last-error).
- tools/dynwinrt-codegen/src/codegen/flat.rs + parse_flat_apis /
  FlatApisMeta in meta.rs — flat codegen from Apis/DllImport metadata.
- napi flat_invoke / flat_last_error exports.
- main.rs Apis/flat-codegen dispatch path.
- bindings/js/e2e/registry.{js,mjs} + flat_registry.mjs E2Es.
- tools/dynwinrt-codegen/tests/win32_flat_test.rs +
  tests/snapshots/registry_apis/* snapshots.

Also restores the reference behaviour of the shared classic-COM E2Es
(taskbarlist.mjs / dtm.mjs / smtc.mjs) to use flatInvoke for HWND
acquisition, and drops the classic-only hwnd.mjs helper + napi
createTestHwnd() shim (no longer needed because flat is present again).

Gauntlet (green):
  - cargo test -p dynwinrt: 96 passed + 1 winrt_regression
  - cargo test -p dynwinrt-codegen: 15 win32_flat_test tests + all suites green
  - napi build (release): OK
  - Node E2Es: taskbarlist / registry / dtm / smtc / flat_registry PASS
  - tests\e2e_test.ps1 -SkipBuild: py 29/29, ts 28/28
  - git diff HEAD feat/win32-com-codegen: empty (tree identical to reference)

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…safety

Round-1 review fixes on top of the rebased flat-Win32 vertical:

1. PSTR/LPCSTR params were marshalled with _wideStringBuffer (UTF-16LE),
   which passes wrong bytes to ANSI/UTF-8 Win32 A-suffixed exports (e.g.
   RegOpenKeyExA) and can smash the callee's stack. Split into a new
   _narrowStringBuffer (UTF-8, NUL-terminated) and route FlatAbiType::PStr
   through it. Rejects embedded NUL for the same truncation-safety reason
   as the wide-string helper.

2. FlatAbiType::Unknown was typed as "unknown" in .d.ts but marshalled as
   DynWinRtValue.pointer(var) at runtime -- the .d.ts didn't match the
   runtime contract and silently accepted arbitrary JS values that would
   then crash inside DynWinRtValue.pointer(...) with a type error. Type
   it as (bigint | Buffer | null), matching Ptr/PtrTo(_).

3. BSTR (SysAllocString-owned, length-prefixed COM string) was mapped to
   FlatAbiType::PWStr -- this drops the 4-byte length prefix and can
   crash callees using SysStringLen. Map to FlatAbiType::Unknown so it
   surfaces as an opaque pointer parameter instead of silently
   mis-marshalling.

Snapshot updated (57 insertions/34 deletions in Apis.js) -- all
A-suffixed Registry exports now go through _narrowStringBuffer.

Gauntlet (green):
  - cargo test -p dynwinrt: 96 passed + 1 winrt_regression
  - cargo test -p dynwinrt-codegen: 15 win32_flat + all other suites
  - Node E2Es: taskbarlist, registry, dtm, smtc, flat_registry -> PASS
  - repo e2e: py 29/29, ts 28/28

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Round-2 review fix: DynWinRtValue.pointer(Buffer) extracts the Buffer's
as_ptr() but does NOT retain the Buffer itself. When a string wrapper
was called inline as `DynWinRtValue.pointer(_wideStringBuffer(x))` the
temporary Buffer became unreachable the moment `pointer(...)` returned,
so GC could reclaim it before `flatInvoke` reached the flat Win32 export
-- passing a dangling pointer to (e.g.) RegOpenKeyExW.

Fix: for every PWStr/PStr input parameter, emit a named local before
the flatInvoke call --
    const _<jname>Buf = _wideStringBuffer(<jname>);   // or _narrowStringBuffer
    const _ret = DynWinRtValue.flatInvoke(..., [
        ..., DynWinRtValue.pointer(_<jname>Buf), ...
    ]);
-- and reference the local in the args array. The local's identifier is
reachable through the enclosing scope until the function returns, so JS
engines must keep the Buffer alive across the flat call. Same shape as
the out/in-out `_*Slot` Buffers, which have always been named locals.

Snapshot: Registry Apis.js now hoists every string Buffer to
`_<jname>Buf` before its flatInvoke call. No other test surface changes.

Gauntlet (green):
  - cargo test -p dynwinrt-codegen: 15 win32_flat + all other suites
  - Node E2Es: taskbarlist, registry, dtm, smtc, flat_registry -> PASS
  - repo e2e: py 29/29, ts 28/28

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…uffer keep-alive

Round-2 review fixes.

1. Flat enum .d.ts was emitting `export declare const enum Foo { ... }`,
   which diverges from the codebase convention (const object + companion
   type) used by the WinRT/classic-COM emitter in
   `tools/dynwinrt-codegen/src/codegen/javascript/render/declarations.rs`
   and breaks TypeScript `isolatedModules` builds (Vite/esbuild/Next.js).
   PR #1's earlier round 197a84f explicitly moved WinRT enums off
   `const enum` for this reason; flat codegen must follow the same
   convention. Now emits:

       export type Foo = (typeof Foo)[keyof typeof Foo];
       export declare const Foo: {
           readonly A: 1;
           readonly B: 2;
       };

   which mirrors the `Object.freeze({...})` runtime shape and is
   `isolatedModules`-safe.

2. Documented the Buffer keep-alive contract on `DynWinRtValue.flatInvoke`
   in `bindings/js/src/lib.rs`. The docstring now spells out that
   `pointer(Buffer|Uint8Array)` stores only the raw pointer, that inlining
   `pointer(Buffer.alloc(...))` or `pointer(_wideStringBuffer(x))` risks
   passing a dangling pointer to the native call, and shows the "hoist
   the buffer to a named const" pattern the codegen already follows.

Snapshot: all 9 flat enum .d.ts files updated to the new shape;
Apis.d.ts unchanged. No runtime changes.

Gauntlet (green):
  - cargo test -p dynwinrt: 96 passed + regression
  - cargo test -p dynwinrt-codegen: 15 win32_flat + all other suites
  - Node E2Es: taskbarlist, registry, dtm, smtc, flat_registry -> PASS
  - repo e2e: py 29/29, ts 28/28

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…urns as .result

Two Copilot-flagged flat-Win32 codegen bugs.

1) meta.rs: parse_flat_apis_from_index silently truncated a method's
   argument list when winmd param_defs.len() != sig.types.len(). That
   produced a wrapper with the wrong arity — flatInvoke would then
   corrupt the callee's stack. Fail loud instead: emit a stderr warning
   and skip the whole method. The generated surface then simply lacks
   this export, which is far safer than a wrapper that misinvokes.

2) flat.rs: is_status_return treated EVERY I32/U32 return as a Win32
   status code. That mis-projected APIs like GetCurrentProcessId ->
   u32 (PID) and MulDiv -> i32 (result) as { status: number }.
   Now the classification is done at parse time from the RAW winmd
   Type (HRESULT / NTSTATUS / LSTATUS) OR from the mapped enum name
   (WIN32_ERROR-family, *STATUS-suffixed) — stored on FlatMethodMeta
   as 
eturn_is_status. Only true status-typedef returns project as
   { status }; plain-integer returns now project as { result }.

Registry snapshot regenerated: RegConnectRegistryExA/W flip from
{ status } to { result } because the win32metadata authors type
their return as raw i32, not LSTATUS. The winmd is the source of
truth for the codegen; the vast majority of Reg* functions (RegCloseKey,
RegOpenKeyExW, etc.) are typed as LSTATUS and still project as
{ status }.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
The @returns { ... } JSDoc line for flat exports listed OUT-param
fields using the RAW winmd param name (e.g. `lpType`) — but the
generated `return { ... }` statement uses the SANITIZED JS
identifier (Hungarian-stripped `type`) that `js_param_names_for_method`
produces (and which is required to avoid `lpXxx` in the public
surface, plus deal with collisions/reserved words).

Result: docs said `{ status, lpType }` but callers accessed
`r.type`. Fixed by passing the `jnames` list into
`describe_return_shape` and indexing by param position — the
documented shape now matches the actual return object exactly.

Registry snapshot updated: 4 methods (RegEnumValueA/W,
RegQueryValueExA/W) flipped the doc from `lpType: <out>` to
`type: <out>` — matching what the emitter has always emitted.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
- flat.rs scalar_slot_write: Handle in-out slots now accept both
  bigint and Buffer inputs (the .d.ts advertises bigint|Buffer for
  handles, and BigInt(Buffer) throws). Branch on runtime type:
  read the u64 out of the buffer when Buffer.isBuffer, otherwise
  coerce via BigInt. U64 kept as-is because its input surface is
  bigint|number, both safe for BigInt(). Registry snapshot did not
  change (no [in,out] pointer-to-Handle params in that namespace).

- win32_flat_test.rs: fix misleading doc comment on
  no_arg_and_void_returns_are_emitted — it tests RegCloseKey(HKEY)
  which is one [in] param + LSTATUS return, not a void/no-arg export.
  Renamed comment to match reality.

- lib.rs: add DLL loading (SECURITY) section to flat_invoke docstring
  mirroring the Rust-layer warning. Explains LoadLibraryW search
  order, DLL preloading/hijacking risk, and the safe patterns
  (system-DLL name or absolute path).

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…] path

The E2E preamble said flat-Win32 [DllImport] codegen was `out of
scope for this branch`, but this PR ships that codegen (see the
new `flat_registry.mjs` E2E that consumes generated wrappers
directly). Rewrite the comment to describe the actual relationship:
this file is intentionally hand-written for the natural JS surface
(`Registry.getString(hive, subKey, valueName)`); the lower-level
`[DllImport]` wrappers on `Windows.Win32.System.Registry.Apis`
are now generated by `dynwinrt-codegen --lang js --class-name Apis`.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot review caught that the flat-Win32 handle typedef advertised
`bigint | Buffer` but the emitter marshals handles via
`DynWinRtValue.pointer(hKey)` — and `pointer(Buffer)` uses the
buffer's own base address, NOT the pointer bits inside it. So a
caller passing a Buffer of pointer bits would end up with the
Buffer's address being interpreted as the HANDLE (i.e., an
address of a stack/heap slot, not the intended kernel handle).

Fix by narrowing the handle type to `bigint | number`:
  - `bigint` is the safe path for full 64-bit kernel handles
  - `number` is ergonomic for handles that fit in a JS safe int
    (small HWND window IDs, etc.)
  - `Buffer` is intentionally removed from the surface

Also:
  - `wrap_arg_js(Handle)` now emits `pointer(BigInt(hKey))`.
    Idempotent for bigint, coerces number, and avoids the
    JS Number.MAX_SAFE_INTEGER ambiguity when the caller happens
    to hand-write a numeric constant.
  - `scalar_slot_write(Handle)` reverts the Buffer branch added
    in round 6 — that branch was based on the (now-corrected)
    misconception that Buffer was a valid Handle input shape.
    Reverted to `BigInt({value_var})` (same as U64), which
    handles bigint (identity) and number (coerce) safely.
  - Handle typedef doc string explains the ban on Buffer and why.

Registry snapshot regenerated: HKEY/HANDLE/PSECURITY_DESCRIPTOR
types flip from `bigint | Buffer` to `bigint | number` in the
.d.ts (with expanded doc); every `pointer(hKey)` becomes
`pointer(BigInt(hKey))` in the .js. Node E2Es all pass
unchanged because they were already passing bigint HKEY constants.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot review round 9 flagged the doc comments as out of sync with
the actual emitter contract:

- PWStr/PStr said just "PWSTR/LPCWSTR" / "PSTR/LPCSTR" and "natural
  surface is string | null". Clarify that the string-input projection
  is correct for the CONST forms (PCWSTR / PCSTR / LPCWSTR / LPCSTR)
  which are read-only inputs, and note that the MUTABLE PWSTR/PSTR
  output-buffer forms flow through the pointer(Buffer) marshalling
  path in flat.rs — they are NOT string-marshalled.

- Handle said "natural surface is bigint | Buffer" — but round 8
  fixed the emitter to type handles as bigint | number and explicitly
  ban Buffer (because pointer(Buffer) uses the buffer's own address,
  not the pointer bits inside it). Update the comment to match.

Doc-only change; no code, no snapshot delta.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
`is_status_return_type` (added in round 4) treats HRESULT, NTSTATUS,
and LSTATUS as Win32 status typedefs — but `resolve_named_flat_type`
only had explicit shortcuts for HRESULT and NTSTATUS. Currently harmless
because the win32 metadata models LSTATUS as a plain Int32 typedef that
resolves cleanly through the type-def path, but if a future metadata
revision ever exposed LSTATUS as a `struct { Value: Int32 }` (the
same shape the Handle typedefs use), the TypeDef fallback below would
classify it as `FlatAbiType::Handle` — which routes returns through
`retKind = 'Ptr'` and would mis-marshal the status code as a
pointer address. Fail-loud shortcut: map LSTATUS to I32 up front so
this stays consistent with is_status_return_type.

No snapshot delta (the current metadata already routes LSTATUS
correctly); pure defensive fix.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
… return

Round 11 review fixes for the flat-Win32 codegen:

1. classify(): LPWSTR/LPSTR params marked [out] or [in,out] are
   caller-allocated output buffers (RegEnumKeyW, RegEnumValueW,
   RegLoadMUIStringW, RegQueryValueW, RegQueryInfoKeyW, ...), not
   read-only string inputs. Previously classified as Input, which
   fed them through _wideStringBuffer/_narrowStringBuffer — the
   flat exports would write into a fresh throwaway buffer that the
   caller could never observe. Now routed through OpaquePointer
   so the caller supplies (and reads back from) their own Buffer.

2. Argument builder: added an explicit ParamSurface::OpaquePointer
   branch that emits DynWinRtValue.pointer(jname) — bypasses
   wrap_arg_js, which would still try to allocate a wide/narrow
   string buffer if it saw a PWStr/PStr abi. The .d.ts side already
   surfaces these as `bigint | Buffer | null`.

3. flat_ret_kind_literal: downgrade the Void => "I32" fallback
   to a debug_assert! (matching the existing I64/U64 arm). Void
   returns are already filtered out by partition_supported_methods
   via unsupported_return_reason (round 6), so the arm is
   unreachable; the assert catches a missing upstream filter in
   tests instead of silently emitting an I32 wrapper for a void
   export.

Snapshot regenerated: 10 Registry APIs (RegEnumKeyA/W, RegEnumKeyExA/W,
RegEnumValueA/W, RegLoadMUIStringA/W, RegQueryInfoKeyA/W,
RegQueryMultipleValuesA/W, RegQueryValueA/W) now expose their [out]
LPWSTR/LPSTR params as `bigint | Buffer | null` instead of
`string | null`. E2E fixture regenerated in lockstep.

Full gauntlet green:
  cargo test -p dynwinrt         96 passed + 1 regression
  cargo test -p dynwinrt-codegen 15/15 flat + classic + interop + snapshots
  5 Node E2Es                    taskbarlist / registry / dtm / smtc /
                                 flat_registry all PASS
  tests/e2e_test.ps1 -SkipBuild  py 29/29, ts 28/28

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Round 12 review fix: `render_method_dts` was using `dts_type_of` (which
is the *input* param type) for the `result` field of pointer-family
return types. At runtime, however, any `retKind === "Ptr"` — which
`flat_ret_kind_literal` routes for `Ptr`, `PtrTo(_)`, `PWStr`, `PStr`,
and `Handle{..}` — is unconditionally converted through
`_ret.asPointerBigint()`, which returns a plain `bigint` (`0n` for
null). Typing the `.d.ts` `result` as `bigint | Buffer | null` or
`string | null` (as `dts_type_of` did) misdescribed the runtime and
forced callers into wrong-branch narrowing.

Introduces `dts_return_type_of` that maps the pointer family to a
plain `bigint` and delegates everything else to `dts_type_of`. Both
the "no projected out-scalars" and "with projected out-scalars"
branches of the return type synthesis now use it for the `result`
field. Input params and projected out-scalar fields keep using
`dts_type_of` (they still accept caller-supplied Buffers / string
inputs at the boundary).

Registry snapshot regenerated: no delta (Registry APIs return LSTATUS
or void, no pointer-returning exports). Added a synthesized-metadata
unit test `flat_dts_return_types_match_js_runtime` that covers Ptr,
PtrTo, PWStr, PStr, and Handle returns — all must project as
`result: bigint` and the raw `asPointerBigint()` call must appear in
the generated `.js`.

Full gauntlet green:
  cargo test -p dynwinrt         96 passed + 1 regression
  cargo test -p dynwinrt-codegen 16/16 flat + classic + interop + snapshots
  5 Node E2Es                    taskbarlist / registry / dtm / smtc /
                                 flat_registry all PASS
  tests/e2e_test.ps1 -SkipBuild  py 29/29, ts 28/28

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…ollisions

Round 13 review fix: `collect_enum` was keying its dedup `HashSet` on
the enum's *simple* name only, even though `TypeMeta::Enum` carries
both `namespace` and `name`. If an `Apis` class ever referenced two
distinct enums that shared a simple name across different namespaces
(e.g. `Ns.A::Status` vs `Ns.B::Status`), the second one was silently
dropped at parse time — the emitted `Apis.d.ts` would then reference
the wrong enum type, and the sibling `Status.js`/`Status.d.ts` files
would carry only the first variant's members.

Two-part fix:

1. `parse_flat_apis_from_index`: change the dedup key from
   `HashSet<String>` (simple name) to `HashSet<(String, String)>`
   (namespace, name). Both distinct enums now reach codegen.

2. `generate_flat_apis_files`: enum sibling files (`Foo.js`,
   `Foo.d.ts`) still use the simple name for the file name and
   `Apis.d.ts` imports, so a genuine collision would corrupt the
   emitted module. Added a fail-loud check that panics with a clear
   diagnostic (`multiple distinct enums named X referenced by C from
   namespaces [...]`) instead of silently emitting a wrong-shape
   module. When we need to support this shape, the fix is to add
   namespace-qualified aliasing in the emitter — the panic points
   directly at that decision.

Registry snapshot regenerated: no delta (Registry references
WIN32_ERROR, REG_ROUTINE_FLAGS, REG_KEY_ACCESS_RIGHTS, REG_SAVE_FORMAT,
REG_VALUE_TYPE, and their close relatives — all uniquely named).
Node E2E fixture regenerated in lockstep.

Added regression test `flat_fails_loud_on_simple_name_enum_collision`
that constructs a `FlatApisMeta` with two `Status` enums in different
namespaces and asserts the emitter panics.

Full gauntlet green:
  cargo test -p dynwinrt         96 passed + 1 regression
  cargo test -p dynwinrt-codegen 17/17 flat + classic + interop + snapshots
  5 Node E2Es                    taskbarlist / registry / dtm / smtc /
                                 flat_registry all PASS
  tests/e2e_test.ps1 -SkipBuild  py 29/29, ts 28/28

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…+ COM pointer adoption

Fixes 4 gaps surfaced by a systematic Windows.Win32 winmd exploration sweep:
- C4: add napi DynWinRtType.u16Type() (+ i16Type/u8Type/f32Type/f64Type aliases).
- C1: [in] HRESULT params project as number / i32Type() / DynWinRtValue.i32(hr).
- C2: caller-owned [out] PWSTR + cch string buffers (IShellLinkW.GetPath/
      GetDescription) generate a real wrapper via ParamDirection::OutStringBuffer;
      narrow detector (direct PWSTR/PSTR + adjacent char-count; PWSTR* not matched;
      cb byte-counts excluded for PWSTR; PSTR fails loud). Previously crashed.
- C3: adoptComPointer(ptr, iid?) adopts an AddRef-owned returned COM pointer via
      IUnknown::from_raw (no extra AddRef) + optional QI-validate; codegen wraps
      directly-named TypeMeta::Interface out-params as typed wrappers.

Tests: TDD unit tests each; refcount-correct native adoption test;
e2e/shelllink-buffer.mjs proves SetPath/GetPath + SetDescription/GetDescription
round-trips on live classic COM.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…64/f32/f64/pointer)

Fixes flat return-value gaps surfaced by the Windows.Win32 winmd exploration sweep,
which previously skipped ~945 exports and truncated pointer returns:
- F4: function-pointer returns (FARPROC/PROC/NEARPROC + Delegate typedefs) now
      classify as pointer-width and decode as BigInt via asPointerBigint() instead
      of truncating through I32 (GetProcAddress returned a 32-bit-truncated value).
      Unclassified returns now FAIL LOUD (skipped) rather than silently I32.
- void returns: FlatReturnKind::Void via libffi Type::void(); generated JS returns
      undefined (or the projected-outs object) — unblocks ~899 exports incl.
      GetNativeSystemInfo/GetSystemInfo out-struct fills.
- i64/u64 returns: libffi Type::i64()/u64(); decode via new toI64BigInt()/
      toU64BigInt() (BigInt, no JS-number truncation) — unblocks GetTickCount64 etc.
- f32/f64 returns AND args: libffi Type::f32()/f64() (Win64 XMM0 ABI); toF64 decode.

Every FlatReturnKind pairs the matching libffi Type with the corresponding
cif.call::<T>(); the retKind strings, JS decoders, and .d.ts types line up at every
site (both match arms are exhaustive — a new kind fails to compile, not truncates).

Tests: rewrote the former skip-tests to assert emitted retKinds/decoders; added an
unknown-return fail-loud test and a libffi-level Rust unit test for the f64/u64/void
return paths (authoritative, export-independent). e2e/flat_returns.mjs proves live:
un-truncated GetProcAddress pointer, GetTickCount64 (u64), GetNativeSystemInfo (void
out-struct), and D2D1Tan (f32 + float arg).

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…untime read

Copilot review (PR #2): projected out/in-out slot return fields used dts_type_of on
the pointee, so a handle out-slot (e.g. phkResult: HKEY) was typed as the handle
alias (bigint | number). But the generated .js reads handle slots with
readBigUInt64LE, always producing a bigint. Use dts_return_type_of for the returned
field so the .d.ts matches the runtime contract (handles/pointers -> bigint; scalars
-> number; enums -> alias). Input param types are unchanged. Snapshot updated.
… opaque pointer)

After adding param/return validation, bare Unknown by-value params/returns are
skipped fail-loud rather than emitted as opaque pointers; only PtrTo(Unknown) is kept
as an opaque pointer param. Update the doc comment to match.
…o flat-Win32

Integrates Leilei Zhang's "isolate classic COM from WinRT" refactor (com module +
napi DynCom/DynComMethodSig + com_metadata codegen) with the flat-Win32 vertical.

Conflict resolution:
- bindings/js/src/lib.rs: keep the shared native-ABI + flat methods on DynWinRTValue
  (pointer, asPointerBigint, toI64BigInt, toU64BigInt, flatInvoke, flatLastError,
  value ctors). The truly classic-COM methods (coCreateInstance, adoptComPointer,
  registerInterfaceUnknown, createTestHwnd) now live on DynCom (com.rs).
- meta.rs: keep flat metadata (FlatAbiType, parse_flat_apis, type mapping); take the
  classic-metadata removal (moved to com_metadata).
- main.rs: keep both generation paths — com_metadata (classic) and flat Apis.
- crates/dynwinrt/src/lib.rs: keep both `pub mod com;` and `pub mod flat_call;`.
- Classic e2e wrappers + interop tests: take Leilei's regenerated DynCom versions.

Verified: cargo build + dynwinrt (116) + codegen suites green; napi rebuilt; all 7
Win32 e2e (taskbarlist, dtm, smtc, shelllink-buffer, flat_registry, registry,
flat_returns) pass. Independently re-verified (no lost flat method, no orphaned
classic call, no leftover conflict markers).

Co-authored-by: Leilei Zhang <leilzh@microsoft.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…ange, enum signedness)

An independent multi-agent self-review (beyond the Copilot loop) found issues the
passing E2E sweep did not exercise:

- CRITICAL (memory safety): DynWinRtValue.pointer(existingObject) returned an UNOWNED
  raw pointer; if adopted via DynCom.adoptComPointer it would take ownership and
  double-free/UAF an object the original JS wrapper still holds. pointer() now rejects
  Object/DynWinRtValue inputs (raw BigInt/number/Buffer/null only). No committed caller
  passed an object (verified). Adds pointer-u64-safety.mjs.
- HIGH (correctness): DynWinRtValue.u64(value: i64) could not represent u64 values above
  i64::MAX (lossy/rejected). Now takes a lossless BigInt validated as a full u64; test
  covers 0xFFFFFFFFFFFFFFFF.
- MEDIUM: flat enum underlying was always emitted I32, so real unsigned Win32 enums lost
  their signedness and the unsigned-enum >>>0 u32-boundary coercion was dead code. Now
  preserves the enum backing (value__) signedness; snapshot updated; test added.
- TEST rigor: flat_returns.mjs now reports a visible SKIP for the float live check when
  Direct2D is unavailable instead of implying the float path was proven.

Verified: cargo dynwinrt 116, codegen 140; napi rebuilt; all 8 Win32 e2e pass (classic
via DynCom, flat via DynWinRtValue) with no regression.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…es plain numbers)

The prior self-review hardening changed DynWinRtValue.u64 to require a BigInt, which
broke the WinRT javascript codegen path — it emits DynWinRtValue.u64(<number>) without
a BigInt wrapper (e.g. async_memory_stream_roundtrip stream sizes), so a plain number
was rejected (CI e2e ts 27/28). Accept Either<BigInt, i64>: a JS number converts via
the i64 branch (validated non-negative), a JS bigint via the BigInt branch (lossless
full unsigned-64 range). Restores WinRT compatibility while keeping the > i64::MAX
support from the u64 fix.

Verified: WinRT pipeline py 34/34 + ts 28/28 (async_memory_stream_roundtrip passes),
all 8 Win32 e2e pass, u64 full-range safety test passes.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…m params/returns

Copilot review (follow-up to preserving enum underlying signedness): now that enum
underlying width/signedness is captured (incl I64/U64/F32/F64 via value__), the rest
of the enum model still can't faithfully represent 64-bit/float enums — EnumMember.value
is i32-backed and the TS surface projects enums as number-based unions. Emitting such a
wrapper would produce truncated/wrong member constants and an ABI-mismatched calling
convention.

Add a shared enum_underlying_unrepresentable() helper and skip (fail-loud, with a
warning) any method whose return OR param (by value, or PtrTo out-param) is an enum with
a non-32-bit-integer underlying. Representable I8/U8/I16/U16/I32/U32 enums are unaffected.
Adds a regression test (U64-underlying enum param skipped; U32 kept). Registry snapshot
unchanged (no registry export uses a 64-bit enum).

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…nsferManager substring

The interop return-type assertion matched the bare substring "DataTransferManager",
which is always present as part of the interop class name `IDataTransferManagerInterop`.
That made it a false positive: it passed regardless of the real return type and its
message ("must project the return type as DataTransferManager") contradicted the actual
design, which returns the explicit `DynWinRtValue` bridge (no synthesized WinRT
runtime-class projection). Assert the real contract instead:
`getForWindow(appWindow: HWND): DynWinRtValue;`.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@yeelam-gordon
Gordon Lam (yeelam-gordon) marked this pull request as ready for review July 23, 2026 23:33
The fallback error hardcoded "CoTaskMem-allocated", but take_raw_pointer serves
multiple pointer kinds (description = "COM interface", "wide-string",
"ANSI-string", "CoTaskMem allocation"). The claim was inaccurate for 3 of 4
callers. The `description` parameter already conveys the kind, so drop the
misleading qualifier: "Expected a {description} raw pointer".

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
… assertions

1. Address review feedback: WIN32_WINMD was hardcoded to a developer-local path
   (C:\s\win32metadata\Windows.Win32.winmd), so the whole classic-COM test suite
   silently self-skipped on CI and other machines. Replace the const with a
   win32_winmd() helper that honors the DYNWINRT_WIN32_WINMD environment variable
   and falls back to the local path. Applied to win32_com_test.rs and
   win32_com_interop_test.rs.

2. Because those tests never ran in CI, two assertions went stale after the
   "isolate classic COM from WinRT" refactor and were failing locally:
     - shellitem_getdisplayname_...: expected DynWinRtMethodSig/DynWinRtType.i32Type()
     - u16_input_param_...: expected DynWinRtValue.u16(wHotkey)
   Classic-COM codegen now emits DynComMethodSig / DynCom.i32Type() / DynCom.u16(...).
   Update the expectations (intent unchanged: callee-allocated addOut(pointer), and
   u16 wrapped via the existing ctor). Full dynwinrt-codegen suite now green.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Gordon Lam (yeelam-gordon) and others added 19 commits July 24, 2026 14:30
…-back iid_pointer

Two classic-COM memory fixes surfaced by the pointer-lifecycle audit:

#2 (Medium, double-release hazard): DynCom.pointer() accepted an existing
DynWinRtValue and, for Object values, returned a borrowed raw COM pointer
owner-backed by a clone. That raw pointer is indistinguishable from an owned
+1 pointer to adoptComPointer(), enabling a double-release. Align tier1 with
tier2: reject all DynWinRtValue inputs; callers pass raw pointer bits,
Buffer/Uint8Array, or null. Generated COM code only ever passes HWND/buffer/
PIDL values to pointer(), never objects, so nothing breaks. Adds
e2e/pointer-reject-object.mjs regression.

#4 (Low, unbounded leak): iid_pointer boxed one GUID per distinct GUID into a
static HashMap and never freed it. Replace with an owner-backed
NativePointerOwner::Guid(Box<GUID>) that frees on drop/GC. The REFIID is only
read during the synchronous COM call and the JS temporary outlives it.
Classic-COM gauntlet (taskbarlist, dtm, smtc, shelllink, hwnd) all pass.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Brings the latest classic-COM fixes into the combined branch:
- #2 pointer() rejects DynWinRtValue inputs (double-release hazard)
- #4 iid_pointer owner-backed GUID (no static leak)
- env-overridable DYNWINRT_WIN32_WINMD test path + stale-assertion fixes
- take_raw_pointer ownership-neutral message; interop return-type assertion

Conflict resolved in win32_com_test.rs (comment wording only; both sides
already assert the DynCom.u16(...) contract).
…ngling (#1)

The flat-Win32 path did LoadLibraryW + FreeLibrary per call (LoadedLibrary RAII).
A flat export returning a pointer/string/function address INTO the module
(FlatReturnKind::Ptr, PWSTR/PSTR/Handle) would dangle once FreeLibrary ran
before the caller used it. Masked today only because kernel32/advapi32 are
always resident.

Replace the per-call load/free with a process-lifetime module cache (load once,
never FreeLibrary), matching .NET [DllImport] behavior and removing per-call
load/unload overhead. Adds a regression test proving the cached handle is
stable and a Ptr-returning export (GetCommandLineW) stays valid after the call.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…er passing

Addresses two Copilot review threads on the module-cache change:

- flatInvoke JSDoc still described per-call FreeLibrary and warned that 'Ptr'
  returns may dangle. That is now false: modules are cached process-lifetime and
  never unloaded, so Ptr returns into a module stay valid. Rewrote the doc.

- Handle arguments were wrapped as DynWinRtValue.pointer(BigInt(x)). For a JS
  number above Number.MAX_SAFE_INTEGER the bits are already lost before BigInt
  sees them, and the wrap bypassed pointer()'s safe-integer validation. Pass the
  value straight through: pointer() accepts bigint|number, carries a bigint
  losslessly, and rejects unsafe numbers instead of silently truncating.
  Regenerated the registry Apis.js golden snapshot to match.

(The third thread — "unused use napi::JsValue" — is a false positive: the trait
provides the .value() method used on Unknown; removing it fails to compile.)

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…ison recovery

Second review round on the flat/combined branch:

- DynWinRtValue.u64() number branch took the JS number as i64 then cast to u64,
  silently rounding/truncating fractional or out-of-safe-range numbers into a
  wrong value. Switch the numeric arm to f64 and validate a finite, non-negative
  safe integer (reject otherwise; callers use a bigint for values above 2^53-1).
  The bigint path is unchanged (full lossless u64 range). WinRT py/ts pipeline
  (34/34, 28/28) still green.

- pointer() doc claimed classic-COM only; it is also the primary way to pass
  pointers/buffers into flatInvoke. Doc updated.

- flat module cache used lock().unwrap(), which would abort the host process if
  the mutex was ever poisoned by an unrelated panic. Recover the map from a
  poisoned mutex instead (append-only name->HMODULE map, safe to reuse).

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…; reserve `result`

- get_cached_module held the module-cache mutex across LoadLibraryW. LoadLibraryW
  runs loader work / the DLL's DllMain, which can re-enter flat_invoke and lock
  the same (non-reentrant) mutex → deadlock, and it serialized all concurrent
  flat calls during a load. Restructured to a double-checked pattern: probe the
  cache under a short lock, release it, LoadLibraryW without the lock, then
  re-acquire to insert (first writer wins).

- The flat reserved-name guard covered `status` but not `result`; both are
  return-object field names, so a parameter/out-field stripping to `result`
  would create a duplicate `result:` key and overwrite the return value. Reserve
  `result` too. Adds a js_param_name unit test covering status/result/keywords.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
The flat-Win32 test suite hard-coded C:\s\win32metadata\Windows.Win32.winmd, so
it silently self-skipped on CI/other machines even when win32metadata was present
elsewhere. Mirror the COM test suites: resolve the path via a win32_winmd() helper
that honors the DYNWINRT_WIN32_WINMD environment variable and falls back to the
local checkout path.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
… helper)

scalar_slot_write emitted `writeBigUInt64LE(BigInt(x))` for handle in/out slots.
Handles are typed `bigint | number`, so a JS number above 2^53-1 has already
lost bits before BigInt sees it, silently writing a wrong handle — the same
lossy-number class fixed on the handle ARG path. Route handle slot writes
through a new `_handleU64` helper that carries a bigint losslessly and rejects a
number that isn't a non-negative safe integer. The helper is emitted only when a
generated file actually writes a handle slot. (I64/U64 slots are `bigint`-typed,
so they keep the direct BigInt() coercion.) Adds a unit test.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Backfill dedicated regression tests for fixes that shipped without one:

- handle_arg_passes_through_without_lossy_bigint_wrap (16d293f): asserts a
  handle ARG emits `DynWinRtValue.pointer(x)`, not `pointer(BigInt(x))`.
- e2e/u64-validation.mjs (123d172): asserts u64() rejects fractional/negative/
  unsafe-integer/NaN/Infinity numbers and overflow bigints, and accepts full
  unsigned-64 bigints — the number branch previously truncated silently.
- module_cache_recovers_from_poisoned_mutex (123d172): poisons the cache mutex
  and asserts get_cached_module still succeeds (old lock().unwrap() panicked).
- concurrent_first_load_does_not_deadlock (98b3f99): best-effort concurrency
  smoke test for the lock-not-held-during-LoadLibraryW change (the exact DllMain
  re-entrancy deadlock isn't deterministically reproducible in a unit test).

The first three fail against the pre-fix code; the last is a concurrency smoke.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Classic COM handle-value newtypes like HWND were projected as bigint | Buffer, but DynCom.pointer(Buffer) passes the Buffer's address instead of the handle bits it contains. Emit handle values as bigint | number while keeping NUL-terminated string pointer aliases such as PWSTR as bigint | Buffer.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Backfills the missing test for the #4 memory fix (commit 50193b4): asserts
iid_pointer returns an owner-backed DynWinRtValue (NativePointerOwner::Guid, so
the boxed GUID frees on drop) holding the correct GUID bytes, and that two
concurrently-live calls for the same GUID allocate distinct boxes (no shared
static cache). Verified this FAILS against the pre-fix static-cache-leak impl
(the owner-backed assertion fails) and passes after.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Windows DLL resolution is case-insensitive, but get_cached_module used the raw
`dll` string as the cache key, so `ADVAPI32.dll` and `advapi32.dll` created two
cache entries and two LoadLibraryW references for the same module. Normalize the
key with to_ascii_lowercase(). Adds module_cache_key_is_case_insensitive, which
fails against the raw-string key (the case variant added a second entry).

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Opaque pointer params were typed `bigint | Buffer | null`, but the runtime
DynWinRtValue.pointer() also accepts a Uint8Array (uses its data pointer). The
narrower type made a valid Uint8Array argument a spurious TypeScript error.
Widen to `bigint | Buffer | Uint8Array | null`. Adds
opaque_pointer_param_dts_accepts_uint8array (fails against the old narrower
type) and regenerates the registry Apis.d.ts golden.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…list

The pointer() doc comment's "Accepts:" list omitted `number` and `Uint8Array`
even though both are accepted (and advertised in ts_arg_type). Doc-only.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Keep the current main WinRT and Classic COM architecture, port the flat Win32 delta, and move flat E2E coverage into the unified test layout.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

Copilot-Session: 41a826d4-b04a-4932-b1b8-31173fc5ab40
Integrate mixed COM packaging and the latest Python and WinUI codegen updates while retaining the flat Win32 routing for subsequent review repairs.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

Copilot-Session: 41a826d4-b04a-4932-b1b8-31173fc5ab40
Bring in Python WinUI E2E and DispatcherQueue GIL integration from PR microsoft#79 before continuing the uncommitted flat Win32 review repairs.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

Copilot-Session: 41a826d4-b04a-4932-b1b8-31173fc5ab40
Add a dedicated /win32 runtime and codegen domain, preserve native metadata contracts, fail closed on unsafe ABI shapes, secure system DLL loading, and integrate namespace-safe packaging and E2E coverage.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

Copilot-Session: 41a826d4-b04a-4932-b1b8-31173fc5ab40
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants