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/unsafeonly 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 point | Purpose |
|---|---|
@microsoft/dynwinrt | Public WinRT API; does not include Classic COM |
@microsoft/dynwinrt/com | COM initialization, GUIDs, safe projectAs interface views, and managed Automation/POD values |
@microsoft/dynwinrt/com/unsafe | Manual interface registration, explicit ABI declarations, vtable invocation, and native pointer handling |
@microsoft/dynwinrt/com/unsafe/raw | Owned/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:
| Value | Apartment |
|---|---|
0 | STA, for COM APIs that require a single-threaded apartment |
1 | MTA, 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 toinitWinappsdk().
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:
| Method | Behavior |
|---|---|
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_THREADwithout entering JS; direct-return methods return a zero value andvoidmethods 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.
| Receiver | API | Owned result / restriction |
|---|---|---|
IAudioRenderClient | writeFramesCopy(Buffer | Uint8Array) | Derives whole frames from bytes; native capacity is authoritative. |
IAudioRenderClient | writeSilence(frames) | Uses native SILENT semantics; zero requests do not acquire. Exclusive event-driven render is unsupported. |
IAudioCaptureClient | readPacketCopy() | null for empty, or a packet with kind, frames, flags, timestampsValid, optional timestamps and owned data. Silent packets have no data Buffer. |
IWICBitmap | readLockedBgra8Copy({x,y,width,height}) | STA-only, native 32bpp BGRA, packed owned data plus native width/height. Padding is not copied. |
IMFMediaBuffer | readCopy() / 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 semantics | JavaScript/TypeScript |
|---|---|
BOOL / VARIANT_BOOL | boolean; Automation true uses the native value -1 |
| 8-, 16-, and 32-bit integers | number |
| 64-bit integers | bigint |
float / double | number |
By-value GUID | Usually projected as a GUID string; the raw ABI uses WinGuid values |
| IID/CLSID constants | WinGuid or a CLSID string; see the GUID section |
BSTR | string; embedded NUL characters are supported |
NUL-terminated PWSTR / PCWSTR | string; correctly encoded byte storage can also be used |
Borrowed handles such as HWND | bigint | number |
| COM interface output | Generated interface wrapper or managed DynWinRtValue |
| Multiple output parameters | Tuple, such as [string, number] |
| POD struct | Layout-branded DynComNativeStruct |
| POD struct array | Layout-branded DynComNativeStructArray |
VARIANT | DynComVariant |
SAFEARRAY | DynComSafeArray |
PROPVARIANT | DynComPropVariant |
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/unsafepointer 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;CreateErrorInfoand a successfulGetErrorInfo;QueryInterface;projectAs(value, InterfaceClass)and generatedIMMDevice.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/ownership | Cleanup |
|---|---|
COM +1 | Release |
| BSTR | SysFreeString |
| HSTRING | WindowsDeleteString |
| CoTaskMem | CoTaskMemFree |
| VARIANT | VariantClear |
| SAFEARRAY | SafeArrayDestroy |
| PROPVARIANT | PropVariantClear |
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:
| Name | Meaning |
|---|---|
| GUID | General-purpose 128-bit identifier |
| IID | Identity of a COM interface |
| CLSID | Identity of a COM coclass |
| REFIID | Native 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:
| API | Description |
|---|---|
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:
- Safe codegen has failed closed.
- You have the exact native interface IID.
- You have verified the complete inheritance chain and absolute vtable slot.
- You have verified every parameter's type, pointer depth, direction, and nullability.
- You have verified every array's count/capacity/actual-length relationship.
- You have verified the return convention.
- 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:
| Builder | Native 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:
| API | Purpose |
|---|---|
addInputBuffer | Caller-provided input array |
addCallerOutputBuffer | Caller allocates capacity; callee writes output |
addEnumeratorNextBuffer | Standard enumerator capacity/fetched relationship |
addCoTaskMemOutputBuffer | Callee allocates with CoTaskMem |
addInputStringArray | Borrowed 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:
| Type | Semantics |
|---|---|
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:
| API | Input ownership | Behavior |
|---|---|---|
borrowComPointer | Caller retains ownership | Calls QueryInterface and returns a new +1 |
adoptOwnedComPointer | Transfers one existing +1 | Consumes 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.