Classic COM JavaScript Usage Guide

September 15, 2026 · View on GitHub

This guide explains how to use dynwinrt to call Classic COM from Node.js/Electron and how to correctly handle GUIDs, IIDs, CLSIDs, and REFIIDs.

Status: preview, under active development. Generated bindings are available for interfaces whose complete ABI, layout, ownership, and cleanup contracts are validated. Unsupported native methods fail closed. The explicitly bounded borrowed-copy facades below expose only their documented transactions and are not counted as complete interface support. See Classic COM support for the current coverage baseline and known gaps.

Prefer the safe bindings generated by dynwinrt-codegen. Use @microsoft/dynwinrt/com/unsafe only when the metadata cannot fully describe the ABI and you have verified every calling convention against the native headers and Microsoft documentation.

See the Classic COM API support plan for current coverage, semantic and raw unsafe layers, blocked API families, and the roadmap. The raw unsafe architecture is documented in Classic COM raw unsafe.

1. Responsibilities of the four entry points

Entry pointPurpose
@microsoft/dynwinrtPublic WinRT API; does not include Classic COM
@microsoft/dynwinrt/comCOM initialization, GUIDs, safe projectAs interface views, and managed Automation/POD values
@microsoft/dynwinrt/com/unsafeManual interface registration, explicit ABI declarations, vtable invocation, and native pointer handling
@microsoft/dynwinrt/com/unsafe/rawOwned/bounded external memory, raw aggregate layouts, pointer slots, and raw outbound ABI calls

Applications that consume standard generated code usually need only:

import { initializeCom } from "@microsoft/dynwinrt/com";
import { TaskbarList, TBPFLAG } from "./generated/com/index.js";

Do not import COM APIs from the package root:

// Incorrect: The root entry point remains WinRT-only.
import { DynCom } from "@microsoft/dynwinrt";

2. Installation

npm install @microsoft/dynwinrt
npm install --save-dev @microsoft/dynwinrt-codegen

Classic COM code generation requires Windows.Win32.winmd. It is typically provided by the Microsoft.Windows.SDK.Win32Metadata NuGet package, for example:

Q:\.tools\.nuget\packages\microsoft.windows.sdk.win32metadata\
  71.0.14-preview\Windows.Win32.winmd

3. Initialize the COM apartment

Call initializeCom() once on each thread before that thread first interacts with any COM object:

import { initializeCom } from "@microsoft/dynwinrt/com";

initializeCom(1); // MTA, the default
// initializeCom(0); // STA

Parameter values:

ValueApartment
0STA, for COM APIs that require a single-threaded apartment
1MTA, for most background, service, and data-processing APIs

Important:

  • An apartment belongs to a thread, not to the entire process.
  • Initialize the Node.js main thread and each Worker thread separately, once per thread and before COM object interaction on that thread.
  • Do not initialize a thread as STA and then attempt to change it to MTA.
  • initializeCom() does not load the Windows App SDK and is not equivalent to initWinappsdk().

4. Generate safe bindings with codegen

4.1 Run a dry run first

$winmd = "Q:\.tools\.nuget\packages\microsoft.windows.sdk.win32metadata\71.0.14-preview\Windows.Win32.winmd"

npx dynwinrt-codegen generate `
  --winmd $winmd `
  --namespace Windows.Win32.UI.Shell `
  --class-name TaskbarList `
  --output .\generated `
  --dry-run

--dry-run performs metadata, dependency, ABI, ownership, and layout validation without writing files. If validation fails, the complete interface still contains an unmodeled native contract. The generator fails closed instead of emitting code that appears usable but might corrupt memory.

To reproduce the complete-interface coverage census:

npx dynwinrt-codegen com-census --winmd $winmd --json

The command prints stable JSON containing eligible, complete, and incomplete interface counts plus the exact percentage.

4.2 Generate files

npx dynwinrt-codegen generate `
  --winmd $winmd `
  --namespace Windows.Win32.UI.Shell `
  --class-name TaskbarList `
  --output .\generated

You can also generate multiple fully qualified names at once:

npx dynwinrt-codegen generate `
  --winmd $winmd `
  --class-name "Windows.Win32.UI.Shell.TaskbarList,Windows.Win32.System.Com.ISequentialStream" `
  --output .\generated

Generated output is placed in generated/com/. Internally, the .js files import @microsoft/dynwinrt/com/unsafe to execute the validated ABI plan. The public .d.ts files reference only the safe value types from @microsoft/dynwinrt/com.

The generator records files per top-level COM root in generated/com/.dynwinrt-com-manifest.json, currently version 5. Within the same manifest version, regenerating a root removes files it no longer owns, while files shared with or generated by other roots remain intact.

Upgrade requirement: PR1 introduced version 3 for registered projectAs and typed endpoint activation. Borrowed-copy support introduced version 4 for audio initialization/GetService context effects. The plan-driven copy recipes now require manifest version 5 and borrowed-descriptor version 2; public APIs and supported interfaces are unchanged. Delete the existing generated bindings directory and completely regenerate all selected roots with the matching updated runtime and codegen. There is no in-place migration: do not edit the manifest version, regenerate only one old root, or mix old and new generated classes.

4.3 Automatic generated unsafe companions

The ordinary command has no unsafe opt-in flag. If complete safe projection fails but one or more methods are raw_metadata_complete, codegen writes an isolated namespace-qualified companion such as generated/com/unsafe/windows/win32/ai/machine-learning/win-ml/IWinMLEvaluationContextUnsafe.js and records every method in support.json. Safe-complete interfaces use matching canonical namespace directories under generated/com/.

npx dynwinrt-codegen generate `
  --winmd $winmd `
  --namespace Windows.Win32.AI.MachineLearning.WinML `
  --class-name IWinMLEvaluationContext `
  --output .\generated

IWinMLEvaluationContextUnsafe hides IID registration, inheritance, absolute slots, and signature wiring while keeping unknown descriptor layout and ownership explicit:

import { DynComRawMemory } from "@microsoft/dynwinrt/com/unsafe/raw";
import {
  IWinMLEvaluationContextUnsafe,
  UnsafePointee,
} from "./generated/com/unsafe/index.js";

const context = IWinMLEvaluationContextUnsafe.from(existingContextValue);
const descriptor = DynComRawMemory.allocate(descriptorSize, descriptorAlignment);
try {
  context.bindValue(UnsafePointee.required(descriptor));
} finally {
  descriptor.release();
  context.release();
}

The short barrel export is available only while IWinMLEvaluationContextUnsafe is globally unique. If two namespaces contain IFoo, import their deep modules:

import { IFooUnsafe as FooA } from "./generated/com/unsafe/contoso/a/IFooUnsafe.js";
import { IFooUnsafe as FooB } from "./generated/com/unsafe/contoso/b/IFooUnsafe.js";

The ambiguous IFooUnsafe short export is omitted until one root is removed.

This is still unsafe. Verify acquisition, apartment, optional-pointer, storage, and cleanup rules against the native contract. A failing HRESULT may throw after native code has mutated caller-owned slots; inspect and reconcile such storage according to the API contract. See the generated unsafe strategy reference for explicit pointer, buffer, and output-ownership strategies.

Stage 2 also emits raw_manual_contract methods with required strategies from generated/com/unsafe/runtime.js. A pointer output with unknown ownership remains caller-selected and explicit:

import {
  IWinMLEvaluationContextUnsafe,
  UnsafePointerOutput,
} from "./generated/com/unsafe/index.mjs";

const context = IWinMLEvaluationContextUnsafe.from(existingContextValue);
const output = context.getValueByName(
  nameStorage,
  UnsafePointerOutput.rawResponsibility(),
);
try {
  // Reconcile and clean up `output` only according to the application's
  // authoritative contract. The unsafe wrapper does not guess an allocator.
} finally {
  context.release();
}

Audio format negotiation is now on the safe surface. The audio endpoint example uses projectAs, IMMDevice.activate(IAudioClient), and DynComAudioFormat.pcm() without an unsafe pointer strategy. An already acquired managed audio value can also be wrapped with projectAs(existingAudioClientValue, IAudioClient).

For IMDSPDeviceControl and IWMDMDeviceControl, record(null) selects the device's default recording format after the required capability and seek setup. record(pcm) supplies an explicit format. This nullable contract does not apply to IAudioClient.initialize or isFormatSupported, whose format inputs remain required.

IDXGIObjectUnsafe.getPrivateData keeps its exact caller-storage API because metadata proves the ABI but not whether the payload is bytes or an AddRef'd interface. Pass ordinary bounded memory for bytes. For an interface payload, pass pointer-sized storage and reconcile the returned pointer with DynComRawOwnedComPointer.assumeTransferred only after the caller has verified the private-data contract.

IHostMallocUnsafe.alloc requires an output strategy. Its allocator is the interface itself, so no standard cleanup is guessed:

const allocation = hostMalloc.alloc(
  size,
  criticality,
  UnsafePointerOutput.rawResponsibility(),
);
// Later, use the generated Free method with explicit pointee storage.
hostMalloc.free(UnsafePointee.required(allocation));

Handle ownership is also explicit:

const handle = device.getMyDeviceHandle(
  UnsafeHandleOutput.closeHandle(),
  UnsafeRawCall.acknowledge(),
);
handle?.release();

When metadata lacks a count relationship, generated code preserves every native count/storage parameter. Only parameters carrying that exact metadata reason require UnsafeCountedBuffer; unrelated pointers keep their ordinary raw storage type. A count reason that cannot be assigned to one parameter instead requires UnsafeRawCall.acknowledge() rather than infecting every pointer:

stillImage.getSTILaunchInformation(
  UnsafeCountedBuffer.required(deviceNameStorage),
  eventCodeStorage,
  UnsafeCountedBuffer.required(eventNameStorage),
);

Manual methods validate strategy types before dispatch. Output strategies are one-shot, clean dirty failure output according to the selected contract, and never infer an allocator.

Strategies are frozen branded objects with module-private state; do not wrap them in proxies or alter prototypes. Required strategies reject literal/raw null, zero-address views, released storage, and misaligned output slots before dispatch. Generated calls also reject duplicate or partially overlapping writable strategy spans. COM-owned outputs and interface replacements require an actual native WinGuid; plain objects and proxies are rejected before slot mutation or native dispatch.

An input-only UnsafePointee may use bounded memory or a raw pointer. An Out/InOut pointee must use live bounded DynComRawMemory: generation records its direction and known per-target layout, and runtime preparation checks size/alignment before dispatch. When layout is unknown, the complete bounded memory range is the writable span. These spans are checked against other pointees, output/replacement slots, counted buffers, and ordinary writable count/actual storage.

Interface replacement is generated only for missing_interface_replacement_contract on an exact InOut IFoo**. A consumes-old transfer is rolled back if validation fails before the core native-dispatch boundary, so the same strategy remains retryable. IFoo*, typed Out parameters, IFoo***, and interface parameters on a method manual for an unrelated reason do not acquire a hidden pointer slot.

IWbemServices now uses exact Windows SDK evidence through the safe generated surface. The mode selects exactly one optional native output, pCtx is native null, and the selected result is an owned managed COM value:

import { projectAs } from "@microsoft/dynwinrt/com";
import { IWbemServices } from "./generated/com/index.js";

const services = projectAs(existingServiceValue, IWbemServices);
const workingNamespace = services.openNamespace("child", { mode: "sync" });
try {
  // The semisynchronous form uses { mode: "semisync" }.
} finally {
  workingNamespace.release();
  services.release();
}

Generated code maps sync to exact flags 0 and semisync to WBEM_FLAG_RETURN_IMMEDIATELY (16). Other values are unrepresentable in the safe declaration. Dirty output references written before a failing HRESULT are released by the runtime.

Never reuse one DynComRawOwnedComPointer object for multiple replacement arguments, including preserve/unchanged combinations. Create an independent +1 owner for each native slot:

const first = DynComRawOwnedComPointer.addRef(value.nativeValue);
const second = DynComRawOwnedComPointer.addRef(value.nativeValue);
object.replace(
  UnsafeInterfaceReplacement.preservesOld(first, iid),
  UnsafeInterfaceReplacement.unchanged(second, iid),
);

For raw-responsibility failure output, catch and reconcile the pointer exactly once:

const output = UnsafePointerOutput.rawResponsibility();
try {
  object.method(output);
} catch (error) {
  const pointer = output.takeFailurePointer();
  // error.unsafeOutputs contains the same typed raw pointer.
  // Apply the externally documented allocator/ownership contract.
  throw error;
}

Owned cleanup results have idempotent release() and a finalizer fallback. Explicit cleanup failure leaves the owner retryable. A finalizer cleanup failure cannot be thrown and is conservatively leaked.

Native pointer-width scalar parameters and direct returns are always bigint; the generated wrapper enforces I32/U32 on i686 and I64/U64 on 64-bit targets. It does not accept a lossy JavaScript number.

If an interface has no executable method, codegen still commits generated/com/unsafe/support.json with every reason, emits no companion class, and exits nonzero. --dry-run prints the same structured counts/reasons but writes nothing. The report fingerprints the complete loaded metadata set, not only the first --winmd path. Every non-dry command targeting the same output uses one OS-backed lock, including WinRT-only, COM-only, mixed, report-only, and Python commands. The complete output root is staged and committed as one transaction, so root barrels/package files and the com subtree cannot expose different generations. Python and JavaScript remain separate package formats and should normally use different output roots.

Publication of the complete staged root is the commit point. If later backup cleanup fails, codegen keeps the new output, reports a warning, and retries the orphan cleanup on the next locked run. It never restores a potentially partial backup over a complete final root.

Ordinary unmanaged file symlinks, directory symlinks, and Windows junctions inside an existing generated package are retained without traversing their targets. Codegen moves each link entry through backup and stage during commit; rollback moves it back. Cleanup removes only link entries, so an external target such as node_modules/@microsoft/dynwinrt is never copied, rewritten, or recursively deleted. Unsupported reparse tags and links colliding with generated or manifest-owned paths fail closed.

Generated and retained unsafe paths are validated with Windows case-insensitive ASCII identity rules. Case-only namespace/type collisions, traversal, rooted paths, trailing dots/spaces, and reserved device names fail generation. A path already shared with another manifest root can be reused only when the staged file exists and its bytes match exactly.

4.4 Customize the runtime path

For development repositories or packaging scenarios, specify:

npx dynwinrt-codegen generate `
  --winmd $winmd `
  --namespace Windows.Win32.UI.Shell `
  --class-name TaskbarList `
  --import-name ../runtime/com-unsafe.js `
  --output .\generated

The default package name, @microsoft/dynwinrt, maps to @microsoft/dynwinrt/com/unsafe in JavaScript and @microsoft/dynwinrt/com in declaration files. The exact name @microsoft/dynwinrt/com also maps to /com/unsafe. When a custom path has the exact basename com.js or com-unsafe.js, codegen converts between the two names as appropriate. It does not guess or rewrite any other custom module name.

5. Call generated COM classes

5.1 Coclass activation

import { initializeCom } from "@microsoft/dynwinrt/com";
import { TaskbarList, TBPFLAG } from "./generated/com/index.js";

initializeCom(1);

const taskbar = new TaskbarList();
try {
  taskbar.hrInit();
  taskbar.setProgressState(hwnd, TBPFLAG.TBPF_NORMAL);
  taskbar.setProgressValue(hwnd, 30n, 100n);
} finally {
  taskbar.release();
}

Public constructors are generated only for coclasses whose primary interface can be determined conservatively from metadata. Interface classes cannot be instantiated directly with new.

5.2 QueryInterface views

import { projectAs } from "@microsoft/dynwinrt/com";
import { TaskbarList, ITaskbarList3 } from "./generated/com/index.js";

const taskbar = new TaskbarList();
try {
  const v3 = projectAs(taskbar, ITaskbarList3);
  try {
    // v3 owns an independent +1 reference returned by the actual QueryInterface call.
  } finally {
    v3.release();
  }

  const optional = taskbar.tryAs(ITaskbarList3);
  optional?.release();

  console.log(taskbar.supports(ITaskbarList3));
} finally {
  taskbar.release();
}

projectAs(value, InterfaceClass) is exported by the runtime @microsoft/dynwinrt/com entrypoint. It accepts a managed native COM value or a generated wrapper, borrows the source, and performs a real QueryInterface, returning an independently owned wrapper with its TypeScript type inferred from InterfaceClass. Release the result independently; the source is not consumed, even when projecting to the same interface.

The private registry rejects unregistered or generated *Unsafe target classes. Numeric addresses, Buffers, native RawPtr values, and raw unsafe pointer owners are not accepted as sources. Generated safe modules register their descriptors through private /com/unsafe helpers; generated IMMDevice.activate(InterfaceClass) obtains its target descriptor from that same registry. Applications use projectAs, not descriptor registration or arbitrary IID/signature pairs. Existing generated .as(...) paths are unchanged.

Differences:

MethodBehavior
projectAs(value, InterfaceClass)Queries a borrowed managed value or wrapper into a separately owned safe wrapper; throws if unsupported
as(InterfaceClass)Throws if the IID is not supported
tryAs(InterfaceClass)Returns null for E_NOINTERFACE
supports(InterfaceClass)Queries and immediately releases the temporary reference, then returns a Boolean

5.3 WinRT interop interfaces

Some Classic COM interfaces provide access to WinRT objects, such as IDataTransferManagerInterop. The generator creates a safe create() method:

import { initializeCom } from "@microsoft/dynwinrt/com";
import { IDataTransferManagerInterop } from "./generated/com/windows/win32/ui/shell/IDataTransferManagerInterop.js";

initializeCom(1);

const interop = IDataTransferManagerInterop.create();
try {
  const manager = interop.getForWindow(hwnd);
  try {
    // The return value is a managed DynWinRtValue. Pass it to APIs that accept
    // this WinRT/COM interface, or cast it by using the exact IID.
    console.log(manager.isNull()); // false
  } finally {
    manager.release();
  }
} finally {
  interop.release();
}

5.4 Implement generated COM event sinks

Codegen emits static implement() only when an interface is IUnknown-rooted and every method in its complete inherited vtable maps to the validated callback ABI subset. For example, IFileDialogEvents can be implemented by synchronous JavaScript handlers:

import { initializeCom } from "@microsoft/dynwinrt/com";
import {
  FDE_OVERWRITE_RESPONSE,
  FileOpenDialog,
  IFileDialogEvents,
  FDE_SHAREVIOLATION_RESPONSE,
} from "./generated/com/index.js";

initializeCom(0); // File dialogs and their event sink use this STA thread.

const dialog = new FileOpenDialog();
const events = IFileDialogEvents.implement({
  onFileOk(fileDialog) {
    console.log("File accepted", fileDialog.isNull());
    return 0; // S_OK; another successful HRESULT such as S_FALSE is also allowed.
  },
  onFolderChanging() {},
  onFolderChange() {},
  onSelectionChange() {},
  onShareViolation(_fileDialog, _item) {
    return FDE_SHAREVIOLATION_RESPONSE.FDESVR_REFUSE;
    // To return an explicit HRESULT too: return [hresult, response].
  },
  onTypeChange() {},
  onOverwrite() {
    return FDE_OVERWRITE_RESPONSE.FDEOR_DEFAULT;
  },
});

const cookie = dialog.advise(events.nativeValue);
try {
  dialog.show(hwnd);
} finally {
  dialog.unadvise(cookie);
  events.release();
  dialog.release();
}

nativeValue is a borrowed bridge for generated COM input parameters. Do not release it separately; release the generated sink wrapper after every source has been unadvised.

Multiple generated interfaces can share one COM identity. Use implementation() for every additional interface and call as() to obtain a QueryInterface view:

const object = IPrimary.implement(
  primaryHandlers,
  ISecondary.implementation(secondaryHandlers),
);
const secondary = object.as(ISecondary);

secondary.release();
object.release();

The generated implementation boundary is:

  • one or more generated IUnknown-rooted interfaces, including validated single-inheritance/base-IID chains;
  • every generated handler must be provided; dynwinrt never invents semantic callback defaults;
  • HRESULT, semantic HRESULT, void, and direct scalar returns;
  • scalar, enum, GUID/REFGUID, handle, COM-interface In/Out, BSTR/HSTRING, borrowed NUL-terminated strings, POD value/pointer, basic InOut, and plain counted-buffer contracts with known capacity/count/allocator semantics;
  • static thunks for common signatures and dynamic libffi closures for the remaining validated signatures;
  • synchronous handlers on the creating thread only; Promises are unsupported;
  • a wrong-thread HRESULT method returns RPC_E_WRONG_THREAD without entering JS; direct-return methods return a zero value and void methods do nothing.

If any method contains an unmodeled Automation/union/ownership/allocator contract, codegen omits implement() for the entire interface. Cross-apartment JavaScript dispatch, connection-point helpers, COM aggregation, custom marshaling, and COM server registration remain unsupported. The native-only one-shot completion in section 5.6 does not relax these JavaScript callback restrictions.

5.5 Audio endpoint activation

Generate the endpoint coclass, device interface, and requested target:

npx dynwinrt-codegen generate `
  --winmd $winmd `
  --namespace Windows.Win32.Media.Audio `
  --class-name "MMDeviceEnumerator,IMMDevice,IAudioClient" `
  --output .\generated
import { DynComAudioFormat, initializeCom, projectAs } from "@microsoft/dynwinrt/com";
import { IAudioClient, IMMDevice, MMDeviceEnumerator } from "./generated/com/index.js";

initializeCom(1);
const enumerator = new MMDeviceEnumerator();
let endpointValue, device, audio;
try {
  endpointValue = enumerator.getDefaultAudioEndpoint(0, 0); // eRender, eConsole
  device = projectAs(endpointValue, IMMDevice);
  audio = device.activate(IAudioClient); // inferred IAudioClient, owned independently
  const pcm = DynComAudioFormat.pcm(2, 48_000, 16);
  const [status, closest] = audio.isFormatSupported(0, pcm); // shared mode
  console.log(device.getId(), status, closest);
} finally {
  audio?.release();
  device?.release();
  endpointValue?.release();
  enumerator.release();
}

This requires running Windows audio services and a default render endpoint. IMMDevice is safe-complete, but its generated activate(InterfaceClass) deliberately fixes CLSCTX_INPROC_SERVER = 1 and passes native NULL for activation parameters. The target must be a registered generated safe class for exactly one of IAudioClient, IAudioEndpointVolume, IAudioMeterInformation, IAudioSessionManager, or IAudioSessionManager2. Generate and import whichever target the application needs. Other/custom targets and loopback or other parameterized activation remain outside this synchronous subset. The separate bounded async entry point is described below.

The five targets are named, cited records in tools/dynwinrt-codegen/src/com_activation_registry.rs, each carrying the in-process/native-NULL/owned-interface conditions of this method contract. This is the current projection policy, not an exhaustive Windows activation list. Registering activation evidence does not establish a target wrapper's complete safe support or guarantee that a device implements the requested IID. This registry migration preserves the existing five targets and generated API.

getId() returns a natural string and frees the native CoTaskMem allocation. The existing safe IAudioClient/IAudioClient2/IAudioClient3 format methods, including getMixFormat(), and the exact nullable device-control record() contracts remain unchanged. This path uses validated generated contracts and shared runtime primitives, not per-interface native adapters; the WinRT root entrypoint is unchanged.

5.6 Native one-shot audio activation

Generate the DLL-export consumer, which also validates and emits both supported result interface classes:

npx dynwinrt-codegen generate `
  --winmd $winmd `
  --namespace Windows.Win32.Media.Audio `
  --class-name ActivateAudioInterfaceAsync `
  --output .\generated `
  --dry-run
# Remove --dry-run to write the bindings.
import { initializeCom } from "@microsoft/dynwinrt/com";
import {
  activateAudioInterfaceAsync,
  IAudioEndpointVolume,
} from "./generated/com/index.js";

initializeCom(0); // Choose the caller's apartment before starting activation.
const volume = await activateAudioInterfaceAsync(
  renderDeviceInterfacePath, // e.g. an audio-render ID from MediaDevice
  IAudioEndpointVolume,
);
try {
  console.log(volume.getChannelCount()); // No recording or volume changes.
} finally {
  volume.release();
}

activateAudioInterfaceAsync(path, GeneratedInterfaceClass): Promise<T> is available only in the generated COM modules, not the WinRT npm root. The target must be the registered safe IAudioClient or IAudioEndpointVolume class. A successful result has independent owned lifetime and uses the same projectAs registry as synchronous COM acquisition.

The start runs immediately on the calling/owner apartment; it is not moved to an MTA worker to evade Windows consent or UI-thread requirements. Microsoft documents UI-thread consent requirements and explicitly safe render-device activations. initializeCom(0) alone does not provide an application UI thread or a consent window. Applications must select the correct thread and device path.

This first subset always passes native NULL for activationParams. Extra arguments, custom PROPVARIANT/loopback blobs, unregistered or unsupported target classes, and embedded NUL in the device path are rejected. There is no generic DLL-export, callback scheduling, aggregation, or cancellation API. Dropping a Promise is not OS cancellation. At most 256 native completions may be pending per Node environment; pending work keeps its dispatcher alive until completion or environment teardown.

Windows invokes a private native-only completion signal, not a JavaScript handler. The original operation and result stay on the owner thread. Ordinary generated implement()/implementation() callbacks still reject foreign-thread calls with RPC_E_WRONG_THREAD.

Errors distinguish stage: "start" (export HRESULT), "result" (outer GetActivateResult HRESULT or a NULL-success contract violation), "activation" (inner activation HRESULT), and "projection" (result QI). Native HRESULT failures preserve the signed numeric hresult property and hexadecimal value in the message. Written owned outputs are released on both HRESULT failure paths. Two successful HRESULTs with a NULL interface reject instead of resolving an empty wrapper.

Run deterministic fake-export/FTM/apartment/lifecycle coverage with npm run test:native-completion in bindings\js, with DYNWINRT_WIN32_WINMD set. It does not access an audio device or microphone. An optional live smoke is enabled by DYNWINRT_TEST_AUDIO_RENDER=1. It requires a default speaker endpoint and only activates IAudioEndpointVolume and reads its channel count; it does not record audio, access a microphone, or modify endpoint settings.

5.7 Borrowed native storage: use owned copies

Generate the Audio client and copy receivers together. After upgrading to COM manifest 5, borrowed-descriptor version 2, and unsafe support schema 12, delete the old generated output and regenerate it in full; do not combine old audio initialization wrappers with new copy facades.

npx dynwinrt-codegen generate --winmd $winmd `
  --class-name Windows.Win32.Media.Audio.IAudioClient,Windows.Win32.Media.Audio.IAudioRenderClient,Windows.Win32.Media.Audio.IAudioCaptureClient,Windows.Win32.Graphics.Imaging.IWICBitmap,Windows.Win32.Media.MediaFoundation.IMFMediaBuffer `
  --output .\generated

Audio requires the format from an observed successful initialization. For a managed IAudioClient obtained through endpoint or one-shot activation:

import { projectAs } from "@microsoft/dynwinrt/com";
import { IAudioRenderClient } from "./generated/com/index.js";

const format = client.getMixFormat();
// GetMixFormat alone is not proof. This actual successful call records the
// exact format passed to Initialize, including when conversion flags are used.
client.initialize(0, 0, 200000n, 0n, format, null);

const nativeService = client.getService(IAudioRenderClient.IID.toString());
const render = projectAs(nativeService, IAudioRenderClient);
nativeService.release();
try {
  render.writeSilence(client.getBufferSize()); // e.g. prefill before Start
  // render.writeFramesCopy(ownedPcmBytes); // whole initialized-format frames
} finally {
  render.release();
}

Copies need no caller block alignment or lifetime assertion. Services retain their correct originating client/context even after its wrapper is released. QI/projectAs aliases share the gate and failure state. An externally initialized client without an observed initialization is rejected; there is no assumeInitialized or attach-format escape hatch.

ReceiverAPIOwned result / restriction
IAudioRenderClientwriteFramesCopy(Buffer | Uint8Array)Derives whole frames from bytes; native capacity is authoritative.
IAudioRenderClientwriteSilence(frames)Uses native SILENT semantics; zero requests do not acquire. Exclusive event-driven render is unsupported.
IAudioCaptureClientreadPacketCopy()null for empty, or a packet with kind, frames, flags, timestampsValid, optional timestamps and owned data. Silent packets have no data Buffer.
IWICBitmapreadLockedBgra8Copy({x,y,width,height})STA-only, native 32bpp BGRA, packed owned data plus native width/height. Padding is not copied.
IMFMediaBufferreadCopy() / replaceCopy(data)Linear storage only; use native current/max lengths and always Unlock.

WIC and MF accept already acquired managed interface values through projectAs; this feature does not add arbitrary DLL exports or new acquisition factories. WIC's existing owned-lock acquisition remains available, but an arbitrary lock does not acquire byte-access or writable rights through the copy API.

These operations are synchronous, with a 64 MiB per-operation safety cap. No JS callback runs within a transaction, and no native address/backing store is returned. SharedArrayBuffer and detached inputs are rejected before acquisition. Cleanup failure throws, discards copy results and permanently poisons the shared context; it is not retried by a finalizer. Writes may have taken effect before a later error, so do not assume transactional rollback.

A busy or poisoned object also cannot be passed to another COM object's method. The same rule applies to aliases and supported interface-bearing containers: an array built before a cleanup failure is checked again when used, and releasing the original wrapper does not clear state retained by that array. Release managed holders normally after a failure; do not try to reuse the object through IMFSample.addBuffer() or another input path.

Render/capture and MF are intentionally copy-only facades, not complete native interfaces. General IMF2DBuffer/IMF2DBuffer2 pitch/plane access and writable external WIC locks are not supported. See the exact lifecycle/evidence contract for supported formats, HRESULT handling, and validation.

5.8 Explicit overload names

Existing overloads that codegen can distinguish by JavaScript arity/shape keep their current names and dispatch unchanged. PR4 also admits previously rejected groups of otherwise fully validated normal COM methods whose JavaScript signatures collide or use projected buffers. Every member receives <camelName>AtSlot<absoluteVtableSlot>; there is no ambiguous unsuffixed method and no extra slot argument to pass.

For example, generate Windows.Win32.Graphics.Direct2D.ID2D1Device1 and inspect its .d.ts. Its two inherited CreateDeviceContext slots become distinct createDeviceContextAtSlot... methods instead of createDeviceContext. Schematically, where N and M mean the actual absolute metadata slots:

createDeviceContextAtSlotN(options)
createDeviceContextAtSlotM(options)

Use the exact name and typed signature from the generated declaration for the native overload you need. These deterministic names preserve the native slot, arguments, result conversion, and lifetime contract; changing JavaScript values is not a way to select an absent unsuffixed method.

Alias collisions with actual projected member names fail closed. Synthesized, dynamic-IID, and other non-normal groups still reject, as do incomplete ABI or ownership contracts. This is not a universal overload parser. PR4 preserves already-supported generated APIs byte-for-byte and requires no new manifest version; the manifest 4 / support schema 12 upgrade requirements for older borrowed-copy output still apply.

6. JavaScript projections of common native types

Native semanticsJavaScript/TypeScript
BOOL / VARIANT_BOOLboolean; Automation true uses the native value -1
8-, 16-, and 32-bit integersnumber
64-bit integersbigint
float / doublenumber
By-value GUIDUsually projected as a GUID string; the raw ABI uses WinGuid values
IID/CLSID constantsWinGuid or a CLSID string; see the GUID section
BSTRstring; embedded NUL characters are supported
NUL-terminated PWSTR / PCWSTRstring; correctly encoded byte storage can also be used
Borrowed handles such as HWNDbigint | number
COM interface outputGenerated interface wrapper or managed DynWinRtValue
Multiple output parametersTuple, such as [string, number]
POD structLayout-branded DynComNativeStruct
POD struct arrayLayout-branded DynComNativeStructArray
VARIANTDynComVariant
SAFEARRAYDynComSafeArray
PROPVARIANTDynComPropVariant

Do not treat every pointer as a Buffer:

  • The contents of a handle Buffer represent the handle bits.
  • A data-pointer Buffer passes the address of its backing store.
  • A string Buffer must satisfy the encoding, alignment, and terminator requirements.
  • A COM interface must be a managed reference and cannot be replaced by a Buffer.
  • Safe generated data and string pointers reject arbitrary numeric addresses. Numeric addresses are available only through the explicit /com/unsafe pointer APIs. Numeric handle values remain supported because the value is the handle itself rather than a dereferenced address.

7. Lifetime and ownership

7.1 COM interfaces

The following sources return a managed +1 reference:

  • CoCreateInstance;
  • CoGetClassObject;
  • CoGetMalloc;
  • CreateErrorInfo and a successful GetErrorInfo;
  • QueryInterface;
  • projectAs(value, InterfaceClass) and generated IMMDevice.activate(InterfaceClass);
  • a validated interface out parameter;
  • the QI result returned by DynComUnsafe.borrowComPointer().

Wrapper objects eventually call Release automatically, but deterministic release at a known boundary is recommended:

const object = new TaskbarList();
try {
  // Use object.
} finally {
  object.release();
}

You can call release() more than once, but do not call any other member after releasing the object.

7.2 String and Automation ownership

The runtime uses the matching cleanup function specified by the validated contract:

Allocation/ownershipCleanup
COM +1Release
BSTRSysFreeString
HSTRINGWindowsDeleteString
CoTaskMemCoTaskMemFree
VARIANTVariantClear
SAFEARRAYSafeArrayDestroy
PROPVARIANTPropVariantClear

Safe codegen rejects the entire interface when the allocator is ambiguous.

8. Complete GUID type reference

8.1 Relationship between GUID, IID, CLSID, and REFIID

All four use an underlying 128-bit GUID, but they have different semantics:

NameMeaning
GUIDGeneral-purpose 128-bit identifier
IIDIdentity of a COM interface
CLSIDIdentity of a COM coclass
REFIIDNative ABI pointer to a read-only IID: const GUID*

Do not interchange these concepts merely because they have the same binary layout. For example, a CLSID cannot replace the IID required by QueryInterface.

8.2 WinGuid and WinGUID

import { WinGuid, type WinGUID } from "@microsoft/dynwinrt/com";

const iid: WinGUID = WinGuid.parse("00000000-0000-0000-c000-000000000046");

console.log(iid.toString());

WinGUID is only a TypeScript alias for WinGuid:

export type WinGUID = WinGuid;

API:

APIDescription
WinGuid.parse(text)Validates and creates a GUID
guid.toString()Returns the canonical hyphenated GUID string

Always use the canonical format:

xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx

GUID comparison is case-insensitive for hexadecimal digits under COM semantics. To compare GUIDs, compare their canonical strings:

const equal = left.toString().toLowerCase() === right.toString().toLowerCase();

Invalid input throws immediately:

WinGuid.parse("not-a-guid"); // Error: Invalid GUID

8.3 Generated IID constants

Each generated interface exports a strongly typed IID:

import { IID_ITaskbarList3, ITaskbarList3 } from "./generated/com/index.js";

// IID_ITaskbarList3: WinGuid
console.log(IID_ITaskbarList3.toString());

// The IID on the class descriptor identifies the same interface.
console.log(ITaskbarList3.IID.toString());

Generated code uses this IID internally to:

  • register the interface vtable;
  • call QueryInterface;
  • validate interface out parameters;
  • process dynamic-IID output;
  • generate as() / tryAs() / supports().

8.4 CLSID

A generated coclass usually exports its CLSID as a string:

import { CLSID_TaskbarList, TaskbarList } from "./generated/com/index.js";

console.log(CLSID_TaskbarList);
const value = new TaskbarList(); // The safe wrapper calls CoCreateInstance internally.
value.release();

For a handwritten unsafe call, coCreateInstance() accepts a CLSID string and an IID:

import { DynComUnsafe, WinGuid } from "@microsoft/dynwinrt/com/unsafe";

const CLSID_FileOperation = "3ad05575-8857-4850-9277-11b85bdb8e09";
const IID_IFileOperation = WinGuid.parse(
  "947aab5f-0a5c-4c13-b4d6-4bf7836fc9f8",
);

const object = DynComUnsafe.coCreateInstance(
  CLSID_FileOperation,
  IID_IFileOperation,
);
object.release();

DynCom.coGetClassObject(clsid, iid), DynCom.coGetMalloc(), and DynCom.createErrorInfo() expose the corresponding stock COM acquisition paths to generated unsafe wrappers. DynCom.getErrorInfo() returns null when the current logical thread has no error object and consumes a stored object when one exists.

IMalloc returns opaque DynComAllocation values only under the exact IMalloc IID/slot/signature contract. Each value retains its originating allocator and rejects forged or stale addresses. free(), realloc(), and getSize() require that allocator identity; didAlloc() only borrows a live allocation for inspection, as COM permits probing memory from another allocator. free() and successful realloc() consume the old value; release() provides deterministic cleanup.

8.5 A by-value GUID is not a REFIID

A by-value GUID:

const value = DynCom.guid(iid);
const type = DynCom.guidType();

A REFIID is a const GUID*:

const riid = DynComUnsafe.iidPointer(iid);

Select the representation that matches the native signature. Treating a by-value GUID as a REFIID, or a REFIID as a by-value GUID, changes the ABI argument layout. Safe codegen distinguishes them from metadata. For a handwritten unsafe ABI, the caller is responsible for preserving this distinction.

9. Automation values

These value types are available from the safe /com entry point:

import {
  DynComVariant,
  DynComSafeArray,
  DynComPropVariant,
  WinGuid,
} from "@microsoft/dynwinrt/com";

const variant = DynComVariant.bstr("hello");
const array = DynComSafeArray.i32([1, 2, 3]);
const property = DynComPropVariant.guid(
  WinGuid.parse("00000000-0000-0000-c000-000000000046"),
);

try {
  // Pass these values to generated methods.
} finally {
  variant.release();
  array.release();
  property.release();
}

A SAFEARRAY can preserve nonzero and negative lower bounds:

const matrix = DynComSafeArray.i32(
  [1, 2, 3, 4],
  [
    { lowerBound: -2, length: 2 },
    { lowerBound: 5, length: 2 },
  ],
);

10. Handwritten ABI with com/unsafe

10.1 When to use it

Use this API only when all the following conditions are satisfied:

  1. Safe codegen has failed closed.
  2. You have the exact native interface IID.
  3. You have verified the complete inheritance chain and absolute vtable slot.
  4. You have verified every parameter's type, pointer depth, direction, and nullability.
  5. You have verified every array's count/capacity/actual-length relationship.
  6. You have verified the return convention.
  7. You have verified the ownership and cleanup contract for every output.

10.2 Complete example

The following example manually declares IFileOperation::SetOperationFlags. It demonstrates how to write an unsafe ABI declaration. Production applications should prefer the generated FileOperation.

import { initializeCom } from "@microsoft/dynwinrt/com";
import {
  DynCom,
  DynComMethodSig,
  DynComUnsafe,
  WinGuid,
} from "@microsoft/dynwinrt/com/unsafe";

initializeCom(1);

const CLSID_FileOperation = "3ad05575-8857-4850-9277-11b85bdb8e09";
const IID_IFileOperation = WinGuid.parse(
  "947aab5f-0a5c-4c13-b4d6-4bf7836fc9f8",
);

const IFileOperation = DynComUnsafe.registerIUnknownInterface(
  "Windows.Win32.UI.Shell.IFileOperation",
  IID_IFileOperation,
)
  // IUnknown occupies slots 0–2; the absolute slot for IFileOperation::SetOperationFlags is 5.
  .addMethodAt(
    5,
    "SetOperationFlags",
    new DynComMethodSig().addIn(DynCom.u32Type()),
  );

const object = DynComUnsafe.coCreateInstance(
  CLSID_FileOperation,
  IID_IFileOperation,
);

try {
  const FOF_NO_UI = 0x0614;
  IFileOperation.method(5).invoke(object, [DynCom.u32(FOF_NO_UI)]);
} finally {
  object.release();
}

DynComUnsafeInterface intentionally does not provide addMethod(). You must use addMethodAt() and specify the exact absolute vtable slot.

For calls with no output values, the raw DynComMethodHandle.invoke() currently returns a placeholder DynWinRtValue whose value is 0, rather than JavaScript undefined. Ignore that return value as shown in the preceding example. Higher-level generated bindings naturally project such HRESULT methods as void.

10.3 Parameter direction

const signature = new DynComMethodSig()
  .addIn(inputType)
  .addOut(outputType)
  .addInOut(replacementType);

Available builders:

BuilderNative semantics
addIn(type)[in]
addOut(type)Required [out]
addOptionalOut(type)Nullable [out]
addInOut(type)[in, out] replacement
returns(type)Direct native return value
returnsVoid()Native void return
preserveHresult()Preserves a successful HRESULT as a semantic result
preserveEnumeratorNextHresult()Preserves enumerator S_OK/S_FALSE for the default slot 3
preserveEnumeratorNextHresultAt(slot)Preserves an enumerator HRESULT for the specified absolute slot

By default, a standard HRESULT method throws for a failing HRESULT and returns its projected result on success.

10.4 Counted buffers

new DynComMethodSig().addInputBuffer(
  DynCom.u8Type(),
  countParamIndex,
  actualLengthParamIndex,
  true, // The count is in bytes.
);

Related APIs:

APIPurpose
addInputBufferCaller-provided input array
addCallerOutputBufferCaller allocates capacity; callee writes output
addEnumeratorNextBufferStandard enumerator capacity/fetched relationship
addCoTaskMemOutputBufferCallee allocates with CoTaskMem
addInputStringArrayBorrowed array of string pointers

All parameter indexes refer to the native parameter list, not to JavaScript parameter indexes after hidden parameters have been removed.

A standard enumerator must also declare its HRESULT semantics. For example, IEnumGUID::Next:

const next = new DynComMethodSig()
  .addIn(DynCom.u32Type())
  .addEnumeratorNextBuffer(DynCom.guidType(), 0, 2)
  .addOptionalOut(DynCom.u32Type())
  .preserveEnumeratorNextHresult();

Use preserveEnumeratorNextHresultAt(slot) for an exact interface whose Next method is not in the default slot 3. Without this return convention, the runtime rejects an EnumeratorNext buffer.

10.5 Pointer output and cleanup

Select the type explicitly:

TypeSemantics
unclassifiedPointerType()Returns raw bits only; does not release or automatically adopt
borrowedHandleOutputType()Borrowed HWND/handle bits; no cleanup
ownedComOutputType()Output transfers a COM +1; failure paths call Release
coTaskMemOutputType()Calls CoTaskMemFree
bstrOutputType()Calls SysFreeString

Do not map an unknown allocator arbitrarily to the closest available type.

10.6 External COM pointers

Borrow a pointer and obtain a new managed reference through QI:

const managed = DynComUnsafe.borrowComPointer(pointerBits, iid);
try {
  // managed owns an independent +1 reference.
} finally {
  managed.release();
}

Consume a +1 reference that the caller already owns:

const managed = DynComUnsafe.adoptOwnedComPointer(pointerBits, iid);

Differences:

APIInput ownershipBehavior
borrowComPointerCaller retains ownershipCalls QueryInterface and returns a new +1
adoptOwnedComPointerTransfers one existing +1Consumes the reference whether IID validation succeeds or fails

Both APIs accept only explicit bigint or safe-integer pointer bits. They reject Buffer/Uint8Array backing addresses to prevent ambiguity between the contents of a Buffer and the address of a Buffer.

10.7 Raw memory, bounded external views, and aggregate layouts

Use the raw entry point only after verifying the native header, complete vtable, layout, pointer depth, and lifetime rules. For a native T** or unclassified InOut parameter, allocate the pointer slot yourself and declare the ABI argument as the existing pointer type:

import {
  DynComMethodSig,
  DynComRaw,
  DynComRawCleanup,
  DynComRawMemory,
  DynComRawOwnedComPointer,
  DynComRawPointer,
  DynComUnsafe,
} from "@microsoft/dynwinrt/com/unsafe/raw";

const rawInterface = DynComUnsafe.registerIUnknownInterface(
  name,
  iid,
).addMethodAt(
  absoluteSlot,
  "ReplacePointer",
  new DynComMethodSig().addIn(DynComUnsafe.unclassifiedPointerType()),
);

const pointerSize = DynComRaw.pointerSize();
const slot = DynComRawMemory.allocate(pointerSize, pointerSize);
slot.writePointer(0, DynComRawPointer.null());

try {
  rawInterface.method(absoluteSlot).invoke(object, [slot.pointer().toValue()]);
  const replacement = slot.readPointer(0); // Unowned pointer bits.
  console.log(replacement.address);
  // Apply the exact documented AddRef/free/Release contract yourself.
} finally {
  slot.release(); // Idempotent; retained child pointers now reject new use.
}

DynComRawMemory is zero-initialized, nonzero-sized, explicitly aligned, and thread-bound. Its byte, integer, floating-point, pointer-width, and pointer slot accessors check conversion, overflow, bounds, and release state. Numeric fields use Windows native endianness; 64-bit and pointer-width integer slots use bigint. release() becomes logically visible immediately. If a synchronous native call reenters JavaScript and releases memory already leased by that call, physical deallocation waits only until the call returns; all new access and nested dispatch attempts fail.

DynComRawPointer.fromAddress(bits) creates an unowned external pointer that can be passed to a native call or inspected as an address. The pointer itself cannot be dereferenced or offset. Phase 1B adds the explicit bounded opt-in:

const view = DynComRawMemory.fromUnsafePointer(pointer, byteLength, alignment);
try {
  console.log(view.readU32(0));
} finally {
  view.release(); // Never frees the caller-owned external bytes.
}

fromUnsafeAddress and fromUnsafePointer require a byte length and alignment and apply the ordinary checked memory APIs. The caller still guarantees that the external range is real, writable where used, and live for every access and call. A forged or stale address can terminate the process.

readPointer() returns an unowned pointer, while writePointer() copies only bits and never transfers ownership. A raw pointer's toValue() is borrowed provenance and cannot be adopted as COM, CoTaskMem, or BSTR output.

For completed architecture-specific POD facts, use DynComRawStructLayout.fromDescriptor(json). Its byValueType() works with addIn, addOut, addInOut, and returns; pointerType() describes an already pointer-shaped input. createValue() and readValueBytes() preserve the descriptor's exact qualified identity. A closed raw union descriptor marks each architecture layout complete: true; byValueType() then supports input, Out, InOut, and direct return on x64/i686. Returned bytes have no inferred active member; assertActiveField(value, name) is an explicit caller interpretation. Top-level Win64 3/5/6/7-byte by-value aggregates and ARM64 union runtime calls remain gated as documented in the raw architecture guide.

For managed/raw interface transitions, use a dedicated owner:

const owner = DynComRawOwnedComPointer.queryInterface(managed, iid);
try {
  slot.writePointer(0, owner.pointer()); // Borrowed while owner remains live.
  method.invoke(object, [slot.pointer().toValue()]);
} finally {
  owner.release();
}

addRef(managed) creates an independent raw +1, and owner.retain() creates another independent +1 for the same raw interface pointer. intoManaged(iid?) atomically consumes it, including on IID mismatch. detach() moves the +1 into an RAII detached pointer; dropping an unused detached pointer releases it. adoptTransferred() moves that retained +1 without AddRef.

For interface InOut, record the old address and call owner.transferTo(slot). The slot write is validated before ownership is disarmed. A returned external slot pointer is not directly adoptable: the caller must use the explicitly unsafe assumeTransferred() assertion after verifying that the method returned one owned +1. Old/new reconciliation remains caller-managed. If the callee consumed the old reference, do not release the recorded old address; if it preserved the old reference while replacing the slot, release that old +1 separately; if the slot is unchanged, adopt the single +1 only once. Every owner created with assumeTransferred() must then be explicitly released, converted with intoManaged(), or transferred again.

Standard cleanup is available only through accurately named DynComRawCleanup methods such as coTaskMemFree, localFree, globalFree, sysFreeString, safeArrayDestroy, variantClear, propVariantClear, releaseStgMedium, closeHandle, destroyIcon, and deleteObject. Pointer cleanups take resource values; aggregate cleanups take bounded storage. Failures are thrown, and a successful pointer cleanup consumes that pointer object. Duplicating raw bits can still cause double cleanup.

An incorrect address, layout, vtable slot, signature, allocator, or reference count can corrupt memory or terminate the process. The raw API does not make IDataObject/STGMEDIUM, resource unions, custom cleanup, interface replacement, or cross-thread callbacks safe. Safe generated code never imports or falls back to it; only the explicitly named *Unsafe companions do.

11. Error handling

11.1 HRESULT

A standard failing HRESULT becomes a JavaScript exception:

try {
  object.someMethod();
} catch (error) {
  console.error(error);
}

Safe generated bindings preserve successful HRESULT values such as S_FALSE only when metadata or an exact registry entry establishes that multiple success values are semantically meaningful. A handwritten unsafe ABI can call preserveHresult() explicitly; the caller is then responsible for interpreting successful status codes correctly.

11.2 Generation failures

Common causes include:

  • unknown pointer ownership or allocator;
  • incomplete native struct or union layout;
  • an unsupported callback or sink;
  • an array without a count/actual-length relationship;
  • an ambiguous SAFEARRAY/VARIANT/PROPVARIANT element type;
  • an unresolved interface IID or inheritance chain;
  • unmodeled STGMEDIUM, complex union, or resource lifetime.

These errors define a safety boundary and should not be bypassed with type assertions. Use /com/unsafe only after verifying the complete native ABI.

12. Frequently asked questions

Can I pass a GUID string directly to a runtime API that requires an IID?

No. registerIUnknownInterface(), interfaceType(), and similar APIs require a WinGuid. Call WinGuid.parse() first. Parameters explicitly typed as a CLSID string are the exception.

Why isn't IID_* an ordinary string?

The runtime requires a value that has already been parsed, validated, and stored in native GUID layout. Call .toString() for logging or serialization.

Can I use a random UUID as an IID?

Only when you implement and control the corresponding COM interface. When calling an existing Windows interface, the IID must exactly match the interface definition.

Why doesn't WinGuid expose public fields?

It is an opaque value object for a native GUID. Hiding its fields prevents JavaScript numeric-precision, byte-order, and structure-layout errors.

Why doesn't the default /com entry point export DynComMethodSig?

Handwritten signatures, vtable slots, and pointer cleanup are unsafe ABI operations. Safe generated code uses /com/unsafe internally, while applications see only validated, high-level methods.

Can Classic COM call ordinary Win32 DLL functions?

Not through the COM entrypoint. Flat DLL exports such as CreateFileW, Registry APIs, and GDI APIs are not COM vtables. Generate their Apis containers and use @microsoft/dynwinrt/win32 for supported exports. See the Win32 generation guide and native contract boundary.

13. Additional resources