Skip to content

Instantly share code, notes, and snippets.

@compnerd
Created April 20, 2026 18:00
Show Gist options
  • Select an option

  • Save compnerd/b41c227c1fc1fbac4148e102385adc43 to your computer and use it in GitHub Desktop.

Select an option

Save compnerd/b41c227c1fc1fbac4148e102385adc43 to your computer and use it in GitHub Desktop.
COM Interop Design Notes

COM Interoperability Design

This document describes a design for Component Object Model (COM) interoperability in Swift: the @COM attribute, object layout, ARC bridge, QueryInterface implementation, Clang importer integration, threading model, activation, aggregation, and the COM module contents. A companion document (winrt-projection-design.md) covers the WinRT projection layer built on this foundation.

Introduction

The Component Object Model (COM) is the foundational binary interface standard underlying the Windows platform. Every significant Windows API surface, from Win32 shell extensions to Direct3D, from Office automation to the Windows Runtime (WinRT), is defined in terms of COM interfaces. COM is also used cross-platform: Mozilla's XPCOM and Apple's IOKit both implement COM-compatible binary interfaces, and MiniCOM demonstrates that the binary model is genuinely portable, running on Linux, macOS, Android, iOS, and WebAssembly without any OS COM runtime.

Swift on Windows currently has no first-class story for COM. Developers who need to call COM APIs from Swift must drop to unsafe C pointer manipulation, hand-write vtable structs, and manage reference counts manually alongside Swift ARC. This is a fragile, verbose, and error-prone experience. This document describes how Swift can make COM interoperability as natural as its existing Objective-C interoperability: ergonomic, safe, bidirectional, and with zero overhead on the fast path.

The guiding principle is that COM is the binary interface; Swift is the language. COM defines the binary contract (vtable layout, reference counting, interface identity, memory allocation) and Swift developers should never have to think about those mechanics directly. The Swift compiler and runtime handle the binary interface, just as they handle Objective-C message dispatch or C++ vtable layout today. Developers write idiomatic Swift: protocols, classes, properties, throws, async/await, for/in. The COM binary interface is generated, consumed, and maintained automatically.

The end state is full bidirectional COM interoperability: Swift code can consume existing COM interfaces, implement COM interfaces, declare new COM interfaces, and expose them to other COM-speaking languages, all with the same type fidelity and performance as native C++ COM code.

Background

Component Object Model

COM defines a binary interface standard: any language or compiler that can lay out a vtable of function pointers and follow the calling convention can implement or consume a COM interface. The rules are simple:

  • Every COM interface derives (transitively) from IUnknown, which provides QueryInterface, AddRef, and Release.
  • Interfaces are identified by a 128-bit GUID called an Interface Identifier (IID).
  • Concrete implementations (coclasses) are identified by a Class Identifier (CLSID) and activated via CoCreateInstance.
  • Reference counting is manual: callers call AddRef when they take ownership and Release when they relinquish it.
  • Methods return HRESULT to signal success or failure; rich error information can be attached via IErrorInfo.
  • Objects live in an apartment, a threading context (STA or MTA) that COM uses to serialise or permit concurrent calls.
  • Cross-boundary memory must be allocated with CoTaskMemAlloc and freed with CoTaskMemFree; strings use SysAllocString/SysFreeString.

Cross-Platform COM Variants

COM's binary model has been adopted beyond Windows:

  • XPCOM (Mozilla) — cross-platform COM used in Firefox and Thunderbird. nsISupports is the IUnknown equivalent; nsresult is the HRESULT equivalent (with a different bit layout). Runs on Linux, macOS, and Windows.
  • IOKit (Apple) — a COM-compatible plugin interface used in macOS/iOS kernel extensions and device drivers. Interfaces are expressed as C structs of function pointers following the same QueryInterface/AddRef/Release method pattern, detectable by structural analysis.
  • MiniCOM (forderud/MiniCOM) — a minimal, freestanding COM/ATL subset that enables COM-compatible C++ classes to compile and run on non-Windows platforms without the full Windows COM runtime. The @COM attribute and ARC bridge described in this document are designed to be similarly platform-independent, enabling use with MiniCOM environments.

A well-designed COM interop layer for Swift should accommodate all four families with minimal friction.

Goals

  • Consuming and implementing COM interfaces should feel natural in Swift, not like writing C with extra steps.
  • The ARC bridge is the sole owner of the reference count; manual AddRef/Release calls are unnecessary in ordinary Swift code. Memory allocation domains are respected automatically.
  • Calling a COM interface method through a Swift existential is a single vtable dispatch, no more expensive than calling a virtual C++ method.
  • QueryInterface symmetry, the IUnknown identity rule, apartment affinity, the IErrorInfo error reporting contract, and the COM allocator contract are all preserved.
  • Swift can consume COM interfaces, implement COM interfaces, declare new COM interfaces, and expose them to other COM-speaking languages.
  • Existing C/C++ COM code imported via the Clang importer is automatically elevated to idiomatic Swift without requiring any annotation changes to the original headers.
  • The core @COM attribute and ARC bridge are not Windows-specific; they work wherever Swift runs and COM-like interfaces exist.

Non-Goals

  • Generating COM proxy/stub DLLs for cross-process marshalling is a toolchain concern, not a language proposal.
  • COM structured storage (IStorage/IStream) and OLE document embedding are library concerns, not language concerns.
  • Monikers, the Running Object Table, COM categories, and persistent object patterns (IPersist, IPersistStream, IPersistFile) are library-level patterns.
  • COM security (CoInitializeSecurity, call-level security) is an application-level concern.

Core Design

The Compiler Flag and COM Module

COM interoperability is gated behind the -enable-com-interop compiler flag, mirroring -enable-objc-interop. When the flag is set, the compiler auto-imports a COM standard library module.

When targeting Windows, the driver enables -enable-com-interop by default, just as -enable-objc-interop is enabled by default on Apple platforms. COM is as fundamental to Windows as Objective-C is to macOS and iOS; enabling it by default means Windows developers get COM interoperability without any additional flags. On other platforms, the flag may be set explicitly to support XPCOM, IOKit, or MiniCOM usage.

Module contents

The COM module provides three categories of declarations:

Types and protocols defined by the module:

Declaration Kind Purpose
IUnknown @COM protocol Root COM interface; QueryInterface/AddRef/Release are compiler-synthesised and sealed
ISwiftObject @COM protocol Swift object recovery from COM pointers; compiler-synthesised on every @COM class
GUID<Tag> @frozen generic struct 128-bit identifier parameterised by a phantom tag type for semantic distinction
IID typealias for GUID<IIDTag> Interface identifier
CLSID typealias for GUID<CLSIDTag> Class identifier
IIDTag, CLSIDTag empty enum Phantom types discriminating GUID usage
IErrorInfo @COM protocol Rich error detail interface; defined by the module on non-Windows, imported from SDK on Windows
COMError @frozen struct Error type capturing HRESULT and IErrorInfo fields
COMInterfaceResolver protocol Extension point for QueryInterface (aggregation, tear-offs)
COMAggregatable protocol Opts a @COM class into delegating IUnknown for aggregation
COMThreadingModel enum: Int Threading model declaration (.single, .apartment, .free, .both, .neutral)
COMActivationOptions struct Task-local activation context (CLSCTX + optional COMServerInfo)
COMContext ~Copyable struct Scoped COM initialisation with explicit lifetime
COMSingleThreadedExecutor SerialExecutor STA apartment executor for Swift concurrency
COMMultiThreadedExecutor TaskExecutor MTA executor for Swift concurrency
COMServerInfo struct Remote server info for DCOM activation
CachedTearOff<T> generic struct Library helper for cached tear-off interfaces (not thread-safe)
AtomicCachedTearOff<T> generic struct Thread-safe cached tear-off for free-threaded objects
DisposableTearOff<T> generic struct Library helper for disposable tear-off interfaces

Overlays (Swift-friendly wrappers around SDK functions):

Overlay Wraps
CoCreateInstance<T>(_:) CoCreateInstance with generics, throws, and task-local CLSCTX
CoCreateInstanceEx(_:requesting:) CoCreateInstanceEx with multi-interface tuple return
CoCreateInstance(_:for:) CoCreateInstance for aggregated activation
withCOMContext(_:operation:) Scoped CoInitializeEx/CoUninitialize
withActivationContext(_:server:operation:) Scoped CLSCTX via @TaskLocal
@COMMain Entry-point attribute wrapping CoInitializeEx/CoUninitialize

Extensions on SDK-imported types (Windows only):

SDK type Import mechanism Extensions added by COM module
HRESULT swift_newtype(struct) succeeded, failed, named constants (.ok, .fail, .noInterface, etc.)
CLSCTX APINotes → OptionSet .inproc, .handler, .local, .remote, .all
REFIID Transparent Stripped to IID by value
REFCLSID Transparent Stripped to CLSID by value

GUID<Tag> is defined by the COM module on all platforms as an @frozen generic struct with a phantom type tag. IID and CLSID are typealiases for GUID<IIDTag> and GUID<CLSIDTag>. The binary layout (16 bytes: UInt32 + UInt16 + UInt16 + InlineArray<8, UInt8>) matches the C GUID struct defined by the COM specification. On Windows, the C GUID from the SDK headers and the Swift GUID<Tag> have identical layout, so pointers can be passed directly across the language boundary.

The compiler selects the correct calling convention for synthesised vtable thunks transparently. The convention is STDMETHODCALLTYPE (__stdcall on x86 Windows, the platform C convention on x64/ARM), so Swift authors never need to annotate calling conventions.

The @COM Attribute

The @COM attribute is the single annotation that marks a Swift type as participating in COM's binary interface model. It appears on protocols (to declare COM interfaces) and on classes (to declare COM coclasses).

// IUnknown's requirements (QueryInterface, AddRef, Release) are compiler-synthesised and sealed.
// QueryInterface is expressed as Swift's `as?` operator.
// AddRef and Release are managed transparently by Swift ARC.
@COM(IID: "00000000-0000-0000-C000-000000000046")
public protocol IUnknown {}

// A typical COM interface with properties and methods
@COM(IID: "B196B284-BAB4-101A-B69C-00AA00341D07")
protocol IConnectionPoint: IUnknown {
    var connectionInterface: IID { get throws }        // property — one vtable slot (getter)
    func advise(_ sink: (any IUnknown)?) throws -> UInt32 // method — one vtable slot
    func unadvise(_ cookie: UInt32) throws             // method — one vtable slot
}

// A COM coclass
@COM(CLSID: "F08DF954-8592-11D1-B16A-00C0F0283628")
class SpeechSynthesiser: ISpVoice { ... }

IUnknown is a special case: its three requirements (QueryInterface, AddRef, Release) are compiler-synthesised and sealed. The user does not implement them. QueryInterface is expressed as Swift's as? operator; AddRef and Release are managed by Swift ARC. The protocol body appears empty in Swift source, but the compiler adds three vtable slots.

Attribute shape

@COM may be applied to protocols and classes. It may not be applied to structs or enums, because COM objects are reference-counted through AddRef/Release and must have stable identity for QueryInterface. Value types cannot participate in this model.

On a protocol, @COM requires an IID: identifying the interface:

/// Declares a COM interface with a registered IID.
/// The compiler synthesises a static `IID` property on the protocol.
@COM(IID: uuid-string-literal)

Every COM interface has an IID. This is true across all COM implementations: Windows COM uses GUID, IOKit uses CFUUIDRef, and XPCOM uses nsIID, but all are 128-bit UUIDs. IID: is platform-independent; the COM module provides conversions between these representations.

On a class, @COM accepts additional arguments:

/// Marks the class as a COM object with synthesised QueryInterface,
/// AddRef, Release, and ISwiftObject. The class participates in the
/// COM vtable layout and ARC bridge but is not activatable via
/// CoCreateInstance (no CLSID is registered).
@COM

/// Marks the class as an activatable COM coclass. The CLSID is
/// registered for CoCreateInstance activation; the compiler synthesises
/// an IClassFactory and the appropriate activation entry point. ThreadingModel is
/// optional and defaults to .apartment.
@COM(CLSID: uuid-string-literal)
@COM(CLSID: uuid-string-literal,
     ThreadingModel: COMThreadingModel)    // optional, defaults to .apartment

CLSID: is the coclass identifier used with CoCreateInstance on Windows. Without it, the class is a valid COM object that can be created directly in Swift but is not discoverable by external COM clients through the activation machinery. ThreadingModel: specifies the COM apartment model (see Threading Model) and defaults to .apartment. It requires CLSID: because the threading model is a property of an activatable coclass registered with the COM runtime; a bare @COM class without a CLSID: is not registered and the COM runtime does not manage its apartment affinity. Thread safety for bare @COM classes is expressed through Swift's own concurrency model (actors, Sendable).

GUID<Tag> is a generic @frozen struct parameterised by a phantom tag type. IID is a typealias for GUID<IIDTag> and CLSID is a typealias for GUID<CLSIDTag>, where IIDTag and CLSIDTag are empty enums. The phantom type prevents accidental interchange between interface identifiers and class identifiers at the type level, with zero runtime cost (the tag is not stored). On Windows, the layout matches the SDK's GUID struct; on non-Windows platforms, the COM module defines it directly. CLSID: and ThreadingModel: are specific to Windows COM. COM implementations that do not use class activation or apartments (IOKit, XPCOM, MiniCOM) simply omit them.

Synthesised IID and CLSID properties

The compiler synthesises static read-only properties for the identifiers declared in @COM:

  • For a @COM(IID:) protocol, the compiler synthesises static var IID: IID { get } on the protocol.
  • For a @COM(CLSID:) class, the compiler synthesises static var CLSID: CLSID { get } on the class.

Both properties are @inlinable and @_alwaysEmitIntoClient. The property body is serialised into the .swiftmodule so that cross-module callers inline the UUID constant directly into QueryInterface comparisons, existential construction, and activation call sites without a function call. This enables the compiler to fold the constant at the use site, which is critical for QueryInterface performance (every call compares IIDs).

@COM(IID: "B196B284-BAB4-101A-B69C-00AA00341D07")
protocol IConnectionPoint: IUnknown { ... }

// Synthesised (always emit into client):
extension IConnectionPoint {
    @inlinable @_alwaysEmitIntoClient
    static var IID: IID {
        IID(data1: 0xB196B284, data2: 0xBAB4, data3: 0x101A,
            data4: (0xB6, 0x9C, 0x00, 0xAA, 0x00, 0x34, 0x1D, 0x07))
    }
}

@COM(CLSID: "F08DF954-8592-11D1-B16A-00C0F0283628")
final class SpeechSynthesiser: ISpVoice { ... }

// Synthesised (always emit into client):
extension SpeechSynthesiser {
    @inlinable @_alwaysEmitIntoClient
    static var CLSID: CLSID {
        CLSID(data1: 0xF08DF954, data2: 0x8592, data3: 0x11D1,
              data4: (0xB1, 0x6A, 0x00, 0xC0, 0xF0, 0x28, 0x36, 0x28))
    }
}

This gives each type a natural spelling for its identifier:

IConnectionPoint.IID   // the interface's IID
SpeechSynthesiser.CLSID // the coclass's CLSID

// Used at call sites:
let cp: any IConnectionPoint = try CoCreateInstance(SpeechSynthesiser.CLSID)
if interface is any IConnectionPoint { ... }  // QI compares IConnectionPoint.IID

Inlining the UUID constants is important for performance: QueryInterface compares IIDs on every call, and CoCreateInstance passes the CLSID to the COM runtime. Inlining avoids an indirect call to read a constant that is known at compile time.

Pointer model

COM interface pointers appear at two layers in the design:

At the user layer, COM objects are held as Swift existentials (any IInterface, (any IUnknown)?). The existential stores the Swift object pointer P (the standard class reference), not the COM interface pointer. Users pass, store, and return existentials. The COM pointer is not visible.

At the thunk layer, COM pointers are UnsafeMutableRawPointer. The shared QueryInterface, AddRef, and Release functions in the COM module receive and return raw pointers because the COM ABI uses void*. The COMInterfaceResolver.resolve(_:) protocol method returns UnsafeMutableRawPointer? for the same reason: the resolved pointer is written to ppvObject, a void** out-parameter.

There is no typed COMPointer<T> wrapper between these two layers. The existential is the typed reference from the user's perspective, and the thunk layer intentionally operates on raw pointers because type erasure is inherent in COM's QueryInterface contract (it returns void**).

To bridge between the two layers, the COM module provides an UnsafeMutableRawPointer initialiser that computes the raw COM interface pointer from any IUnknown-conforming value:

extension UnsafeMutableRawPointer {
    /// Computes the raw COM interface pointer for a `@COM` object.
    ///
    /// Recovers the Swift object pointer `P` from the value, reads the
    /// COM vtable pointer offset from `pwt[-1]` (the protocol witness table's
    /// pre-slot), and returns `P - offset`.
    ///
    /// The returned pointer is borrowed and valid for the lifetime of the object.
    /// No `AddRef` is performed.
    @_transparent
    public init<T: IUnknown>(unsafeCOMPointer value: borrowing T) {
        self = UnsafeMutableRawPointer(Builtin.bridgeToCOMPointer(value))
    }
}

The offset from P to the COM interface pointer is stored at pwt[-1], the slot immediately before the protocol witness table for the T: IUnknown conformance. This mirrors vtable[-1] (which stores the reverse adjustment, from COM pointer to P) and uses the same pre-table slot pattern:

Pre-slot Stored before Contains Direction
vtable[-1] COM vtable Byte adjustment from COM pointer to P COM → Swift
pwt[-1] Protocol witness table Byte offset from P to COM pointer Swift → COM

The implementation uses Builtin.bridgeToCOMPointer, a new compiler builtin following the pattern of Builtin.bridgeToRawPointer and Builtin.bridgeFromRawPointer. The compiler recovers P, reads pwt[-1] from the witness table for the T: IUnknown conformance, and computes P - offset. For specialised calls, the witness table is a compile-time constant and the offset can be folded. For unspecialised calls, pwt[-1] is read at runtime.

It is used by COMInterfaceResolver implementations, tear-off helpers, and advanced interop scenarios that need the raw COM pointer.

COM Protocols

A @COM protocol declares a COM interface, specifically an [object] interface in MIDL terms: one that derives from IUnknown, uses vtable dispatch, and returns HRESULT. The @COM attribute always implies [object] semantics; raw DCE RPC interfaces (which have no IUnknown, no reference counting, and use explicit binding handles) are a distinct calling model and are not addressed by this design.

The protocol's requirements define the binary vtable layout, that is, the fixed array of function pointers that COM clients index into by position. Because this layout is a binary contract, the compiler must determine exactly which requirements become vtable slots, and in what order.

Which requirements become vtable slots

A COM vtable contains one slot per callable entry point visible to a COM client. In Swift terms:

Instance method requirements : One vtable slot each, whether or not a default implementation exists in a protocol extension. COM has no concept of optional or defaulted vtable slots; every declared method occupies a slot, and every implementation must provide a valid function pointer. If a protocol extension provides a default, it fills the slot for conforming types that don't provide their own override, but the slot is always present. This matches C++ semantics: a virtual method with a default implementation still occupies a vtable slot.

Instance property requirements : One vtable slot per accessor, whether or not a default exists. A read-only { get throws } property emits one slot (the getter). A read-write { get throws set } emits two slots (getter and setter). Because Swift does not currently support set throws, COM properties whose setters can fail are represented as a read-only property paired with a setFoo(_:) throws method until Swift gains throwing-setter support.

Concrete extension methods (not requirements) : No vtable slot. A method added in a protocol extension that is not declared as a requirement in the protocol body is a Swift-side convenience. It is not part of the COM interface and has no vtable slot. COM callers cannot reach it.

Static methods and properties : No vtable slot. COM's vtable model is instance-based; static members have no representation in a COM vtable.

Associated types : No vtable slot. Associated types are a Swift type-system concept with no COM analogue. They are permitted on @COM protocols but do not affect the vtable layout.

Initialisers : No vtable slot. COM object creation goes through class factories (IClassFactory), not through the interface vtable.

VTable slot ordering

Slots are assigned in declaration order within the protocol. This order is ABI-stable: once a @COM protocol is published, adding, removing, or reordering requirements is an ABI break (it changes the vtable layout that existing compiled clients depend on). COM interfaces are immutable once published, so extending an interface requires creating a new one (e.g., IInterface2 extends IInterface), following the standard COM versioning pattern.

When a @COM protocol refines another @COM protocol, the base protocol's slots come first, followed by the derived protocol's own requirements:

@COM(IID: "...")
protocol IInterface: IUnknown {
    // slots 0–2: QueryInterface/AddRef/Release (inherited from IUnknown)
    func render() throws            // slot 3
    var name: String { get throws } // slot 4 (getter)
}

@COM(IID: "...")
protocol IDerived: IInterface {
    func function() throws         // slot 5
    var property: String { get throws } // slot 6 (getter)
}

A COM client holding an IInterface* to a class that implements IDerived sees the same slot layout as if it held an IInterface* to a class that only implements IInterface. The layout is binary compatible.

Properties vs methods

COM interfaces make a semantic distinction between properties and methods. MIDL defines three property accessors: [propget] (getter), [propput] (setter by value), and [propputref] (setter by reference, which assigns an object rather than copying a value). Swift's @COM protocols honour the property/method distinction directly: var declarations map to property vtable slots; func declarations map to method vtable slots.

In Swift, [propput] and [propputref] both map to a { set } accessor. The Clang importer recognises methods with the get_, put_, and putref_ prefixes (e.g., get_Name, put_Name, putref_Font) and promotes matching groups to Swift property syntax; all other methods become func declarations.

When both put_ and putref_ exist on the same property (rare, but used by some OLE controls), the importer maps { set } to putref_ and does not expose put_. The propput/propputref distinction exists to serve IDispatch::Invoke and the VB Set keyword; at the vtable level both setters receive the same pointer type. A vtable caller has no reason to prefer propput over propputref for an interface-typed property — the propput entry is a dispatch-layer artifact with no meaning in a vtable projection. For the same reason, VARIANT-typed properties with both setters also map to propputref; code that needs the propput path is necessarily calling through IDispatch::Invoke, not through the projected property.

Refining non-@COM protocols

A @COM protocol may refine a non-@COM protocol. The @COM protocol's own requirements occupy COM vtable slots as described above. The non-@COM protocol's requirements do not appear in the COM vtable and are invisible to COM callers. They are dispatched through standard Swift protocol witness table mechanisms; the exact layout of the witness table follows Swift's normal rules and is not prescribed by the COM design:

@COM(IID: "...")
protocol IInterface: IUnknown, CustomStringConvertible {
    func render() throws  // COM vtable slot
}

// COM vtable: [QueryInterface, AddRef, Release, render]
// CustomStringConvertible's `description` requirement is dispatched
// through a Swift protocol witness table, not the COM vtable.

A @COM existential (any IInterface) uses Swift's standard class-constrained existential layout. The stored value is the Swift object pointer P. All protocol requirements, both @COM and non-@COM, dispatch through the protocol witness table. For @COM requirements, the witness thunk computes the COM interface pointer from P (a constant subtraction) and dispatches through the COM vtable. For non-@COM requirements, the witness thunk dispatches through the Swift class vtable or metadata as usual.

MIDL attributes that affect protocol semantics

MIDL's [local] attribute indicates an interface is in-process only, with no marshalling code generated. This has no effect on the Swift import because @COM protocols always use direct vtable dispatch, which is inherently in-process. The related [call_as] attribute, which provides a remotable version of a [local] method for cross-process marshalling, is similarly transparent: Swift always sees and uses the [local] form.

Retroactive conformance

The synthesised QueryInterface on a @COM class must return a complete set of interfaces. COM's contract requires that if an object supports an interface, QueryInterface must report it. This creates a constraint on where @COM protocol conformances can be declared.

graph LR
    A["Module A<br/><code>@COM protocol IInterface</code>"]
    B["Module B<br/><code>@COM class CImplementation</code><br/>synthesises QueryInterface here"]
    C["Module C<br/><code>extension CImplementation: IInterface</code><br/>⚠️ invisible to Module B"]

    B -->|imports| A
    C -->|imports| A
    C -->|imports| B
Loading

When Module B is compiled, it synthesises CImplementation's QueryInterface from the conformances it can see. Module C adds a conformance to IInterface, but Module B has already been compiled without knowledge of it. A COM client that queries CImplementation for IInterface gets E_NOINTERFACE even though the conformance exists in Module C. This silently violates COM's identity rule.

To prevent this, a @COM protocol conformance must be declared in the same module as the conforming type. This guarantees the compiler sees all @COM conformances when synthesising QueryInterface. Adding a @COM conformance to a type you don't own (in a third-party module) is a compile-time error.

Layout stability (@frozen)

COM is a binary interface, so every value type that crosses the COM boundary must have a layout known at compile time by both sides. Swift's resilience model (where non-@frozen types can change layout between library versions) is incompatible with this requirement.

Any Swift struct or enum that appears in a @COM protocol requirement (as a parameter type, return type, or property type) must be @frozen. The compiler enforces this at the protocol declaration site:

struct Point { var x: Float; var y: Float }  // non-frozen

@COM(IID: "...")
protocol ICanvas: IUnknown {
    func drawAt(_ point: Point) throws  // error: Point must be @frozen
}

@frozen
struct Point { var x: Float; var y: Float }  // OK — layout is committed

GUID<Tag> is @frozen with a layout matching the C GUID struct (16 bytes: UInt32 + UInt16 + UInt16 + InlineArray<8, UInt8>). IID and CLSID are typealiases and inherit the same layout. HRESULT and CLSCTX have fixed layouts defined by the SDK headers. COMError (defined in the COM module) is @frozen. All imported MIDL structs are imported as @frozen. @COM protocols themselves are implicitly layout-stable: their vtable slot ordering is a fixed binary contract, immutable once published.

For @COM classes, the vtable block layout (negative-offset COM pointers) is ABI-stable, but the Swift stored properties follow normal Swift resilience rules. A @COM class in a resilient library can add stored properties without breaking the COM interface, because COM clients only see the vtable, never the Swift object body.

Existentials

A Swift COM existential uses Swift's standard existential layout with no changes. The stored value is the class reference (P, the Swift object pointer), not the COM interface pointer. This is the same representation as any class-constrained existential, which means modules compiled with and without -enable-com-interop can pass existentials to each other with no ABI mismatch. COM interop is purely additive: the itable prefix (COM vtable pointers at negative offsets from P) is a per-class layout change invisible to the existential representation.

@COM protocol requirements dispatch through protocol witness table thunks. Each thunk receives P, subtracts a compile-time constant offset to compute the COM interface pointer for the target interface, dereferences the vtable, and calls through it. The offset is baked into the thunk at compile time for each class-protocol pair, so there is no runtime conformance table scan on the dispatch path. The cost compared to a direct COM vtable dispatch is one constant subtraction per call.

If the @COM protocol refines non-@COM protocols, the existential carries a witness table as usual, following Swift's standard rules. The witness table contains thunks for both @COM requirements (which compute the COM pointer and dispatch through the vtable) and non-@COM requirements (which dispatch through the Swift class vtable or metadata).

Casting between COM protocols

Casting from one COM protocol existential to another (any IInterfaceany IDerived) maps directly to QueryInterface. This is COM's native mechanism for asking whether an object supports a given interface, and it works identically for Swift-originated and foreign COM objects:

let interface: any IInterface = ...
if let derived = interface as? any IDerived {
    // QueryInterface for IDerived.IID succeeded
    try derived.function()
}
// QueryInterface returned E_NOINTERFACE — cast returned nil

The compiler generates a QueryInterface call with the target protocol's IID. On success, the returned interface pointer (already AddRef'd by QueryInterface) is wrapped in a new existential. On failure, the cast returns nil. The as! operator traps on E_NOINTERFACE.

The is operator also goes through QueryInterface, calling it with the target IID and immediately releasing the result on success. The full QueryInterface path is needed because objects that participate in aggregation or have COMInterfaceResolver may support interfaces that are not in the static conformance table and are not represented in Swift metadata. For foreign COM objects, QueryInterface is the only way to ask about interfaces.

Each as? cast is a QueryInterface call (conformance scan, AddRef on success). Code that repeatedly casts the same object to the same interface should store the result rather than casting each time.

Casting to a concrete Swift type

When casting from a COM existential to a concrete Swift @COM class (any IInterfaceCImplementation), the runtime issues a QueryInterface for ISwiftObject. The ISwiftObject IID hits the fast-path comparison in the synthesised QueryInterface (checked before the table scan), so the probe is a single IID comparison for Swift-originated objects and a quick E_NOINTERFACE for foreign objects.

If the probe succeeds, ISwiftObject.object recovers the Swift heap object via the vtable[−1] offset, and the runtime performs a standard Swift dynamic type check. If the probe fails, the cast returns nil. vtable[−1] is never read speculatively; it is only accessed inside ISwiftObject.object, after the probe has confirmed the object is Swift-originated.

This follows the COM convention of "query once, store the result." Code that repeatedly needs to cast the same object should store the typed result in a local variable rather than casting each time.

Cast lowering decision tree

The compiler selects the appropriate cast lowering based on whether the source and target types are @COM:

flowchart TD
    Start["as? / is cast"] --> SrcCOM{"Source is\n@COM existential?"}

    SrcCOM -->|Yes| TgtCOM{"Target is\n@COM protocol?"}
    TgtCOM -->|Yes| QI_IID["QueryInterface\nwith target IID"]

    TgtCOM -->|No| TgtClass{"Target is\nconcrete @COM class?"}
    TgtClass -->|Yes| QI_ISO["QueryInterface\nfor ISwiftObject"]
    QI_ISO --> TypeCheck["ISwiftObject.object\n→ isa type check"]

    TgtClass -->|No| TgtNonCOM{"Target is\nnon-@COM protocol?"}
    TgtNonCOM -->|Yes| QI_ISO2["QueryInterface\nfor ISwiftObject"]
    QI_ISO2 --> RecoverP["recover P\n→ standard conformance check"]

    SrcCOM -->|No| TgtCOM2{"Target is\n@COM protocol?"}
    TgtCOM2 -->|Yes| WrapIUnknown["convert source to\nany IUnknown"]
    WrapIUnknown --> QI_IID2["QueryInterface\nwith target IID"]

    TgtCOM2 -->|No| StdCast["standard Swift\ndynamic cast"]
Loading

When the source is not a @COM existential but the target is a @COM protocol, the compiler first converts the source to any IUnknown (which every @COM class satisfies), then follows the standard @COM-to-@COM path through QueryInterface. This unifies the two entry points into a single QI-based mechanism.

COM Classes

A @COM class participates in COM's binary object model. The @COM attribute implies IUnknown and ISwiftObject conformance automatically. Both are compiler-managed: their methods (QueryInterface, AddRef, Release, and ISwiftObject.object) are synthesised and sealed. The user never implements them and cannot override them. Explicitly writing : IUnknown is allowed but unnecessary; explicitly conforming to ISwiftObject is a compile-time error (see ISwiftObject below).

The compiler synthesises the conformance table (a static IID array) and the vtable for each @COM class. The vtable's IUnknown slots (0–2) reference @_alwaysEmitIntoClient functions defined in the COM module. The compiler does not open-code their bodies; it emits data (the conformance table, the vtable, the vtable[−1] adjustments) and references library symbols. The @_alwaysEmitIntoClient mechanism emits local copies in each client module, and the linker deduplicates them as linkonce_odr. This simplifies the compiler implementation (no thunk bodies to generate) and keeps the logic in the COM module where it can evolve by recompiling clients rather than changing the compiler.

  • QueryInterface — table-driven. The compiler emits a static conformance table for each class: a flat array of IID values mirroring the COM block slot order, terminated by ISwiftObject's IID as an implicit sentinel. Each class's vtable QueryInterface slot points to an @_alwaysEmitIntoClient per-class thunk that loads the class's table and tail-calls the @_alwaysEmitIntoClient scan function:

    @_alwaysEmitIntoClient
    public func QueryInterface(
        _ pUnk: UnsafeMutableRawPointer,
        _ riid: borrowing IID,
        _ ppvObject: UnsafeMutablePointer<UnsafeMutableRawPointer?>,
        conformances table: borrowing Span<IID>
    ) -> HRESULT

    The non-delegating function checks ISwiftObject.IID first (returns P[-1] on match — this is the most common QI in Swift, as every as? to a concrete @COM class goes through it), then IUnknown (returns the primary slot at P[-2]), then walks the conformance table. The AnyObject cast is deferred to the COMInterfaceResolver fallback path; the IUnknown and ISwiftObject fast paths are pure pointer arithmetic with no dynamic checks. The match position is the slot index: entry i maps to P[-(i + 2)]. The ISwiftObject.IID entry in the table serves as a sentinel: when reached during the scan, it terminates the walk (the fast-path comparison already handled the match case). The count is derived from the Span length; no separate count field or per-entry index is stored — the position IS the index. Classes that conform to COMInterfaceResolver (see Aggregation below) get an additional callback after the table scan. For classes that conform to COMAggregatable, the compiler emits AggregatedQueryInterface instead, which forwards the entire call to the controller's IUnknown (see Aggregation below).

    Because both the per-class thunk and the scan function are @_alwaysEmitIntoClient, the optimizer sees the entire QueryInterface path and can inline the scan into the thunk. For final classes with small interface counts, the loop may be fully unrolled into direct IID comparisons. No cross-module hop occurs at runtime.

  • AddRef / Release — two pairs of @_alwaysEmitIntoClient functions in the COM module. The non-delegating pair (AddRef/Release) recovers P via vtable[−1], calls swift_retain/swift_release, and returns the count as UInt32 — no dynamic checks. The delegating pair (AggregatedAddRef/AggregatedRelease) recovers P, reads the COMAggregatable.controller, and forwards to the outer's IUnknown. The compiler selects which pair to emit in the vtable at class compilation time: classes that conform to COMAggregatable get the delegating pair; all other classes get the non-delegating pair. This is a compile-time decision with zero runtime cost on either path. No per-class thunk is needed. Because they are @_alwaysEmitIntoClient, they are emitted locally in each client module with no cross-module call.

  • ISwiftObject.object — recovers the Swift heap object from a COM interface pointer.

  • ISupportErrorInfo — an explicit opt-in conformance, not automatically synthesised. When a @COM class conforms to ISupportErrorInfo, the compiler synthesises the vtable and conformance table entry. The class must also conform to IErrorInfo to provide the error detail fields that ISupportErrorInfo advertises. This pairing ensures that COM callers who check ISupportErrorInfo can reliably retrieve IErrorInfo from the object. Classes that do not need COM-visible error reporting simply omit the conformance.

Because Swift ARC subsumes COM's reference counting, there is no need for a smart-pointer wrapper type (the C++ CComPtr/ComPtr<T> pattern). Every Swift reference to a @COM object is automatically reference-counted through the unified ARC bridge.

final classes (recommended)

@COM classes should be final by default. COM's own type system has no concept of class inheritance; COM clients interact with objects exclusively through QueryInterface and never see the implementation hierarchy. Making a @COM class final reflects this and provides concrete benefits:

  • The conformance table is a compile-time constant. The optimizer may inline the shared QueryInterface function and unroll the table scan into a direct comparison sequence for small interface counts.
  • The COM vtable block size is fully determined at compile time, simplifying allocation and layout.
  • No risk of ABI breaks from adding @COM conformances later (there are no subclasses to invalidate).
@COM(CLSID: "...")
final class SpeechSynthesiser: ISpVoice {
    // ...
}

Non-final classes

When a Swift implementation genuinely benefits from an inheritance hierarchy (for example, a base class providing shared COM boilerplate with specialised subclasses), a @COM class may be non-final. This is an explicit choice with trade-offs the author should understand.

Subclasses inherit the COM layout and synthesised machinery without re-declaring @COM. They may add new @COM protocol conformances and override COM interface methods; the override replaces the function pointer in the subclass's own static vtable. AddRef, Release, and QueryInterface are sealed and cannot be overridden. To extend QueryInterface with additional interfaces (aggregation, tear-offs, conditional support), the class conforms to COMInterfaceResolver (see Aggregation below). A subclass may carry its own @COM(CLSID:) to register a distinct coclass identity.

New vtable pointers are prepended at higher negative offsets in the COM block, so existing vtable pointers remain at the same offset from P. A COM client holding an IInterface* gets the same pointer regardless of whether the object is a base or derived type. The layout is binary compatible.

The costs:

  • The conformance table for a non-final class must include entries from subclasses that the base class cannot see at compile time. Each class in the hierarchy carries its own complete table (its own conformances plus all inherited ones). The shared QueryInterface function and table-driven scan work identically for final and non-final classes; the only difference is that the table cannot be inlined for non-final classes because the dynamic type's table must be used. The table is stored in the class's type metadata and read via the isa pointer at runtime.
  • Each class in the hierarchy carries its own static vtable data and its own conformance table.
  • Adding a new @COM conformance to a non-final public class is ABI-breaking, because it shifts the COM block offsets for all existing subclasses.

An alternative to the flat conformance table is a chained approach: each class stores only its own direct conformances and, on a miss, delegates to the base class's QueryInterface via a static call (since QueryInterface is sealed, the base implementation is known at compile time). This avoids duplicating inherited entries in each subclass's table, at the cost of one static function call per hierarchy level on a miss. For a 3-level hierarchy with 10 total interfaces, the flat table does 10 comparisons with no calls; the chained approach does the same 10 comparisons spread across 3 calls. The flat table has better cache locality and simpler implementation; the chained approach has lower per-class metadata size. For typical COM hierarchies (shallow, 5-15 total interfaces), the metadata savings from chaining are small, so the flat table is the preferred default. The implementation may switch strategies based on hierarchy depth or interface count.

Object Layout

A Swift @COM class uses a compact layout that is ABI-compatible with C++ COM objects. The COM vtable pointer block lives immediately before the Swift object header in the same allocation. ISwiftObject is placed at P[-1], the slot closest to the Swift header, giving the runtime a fixed, class-independent offset for Swift object recovery without any metadata lookup. Every @COM class has an IUnknown vtable at P[-2]: when the class conforms to at least one user @COM protocol, that protocol's vtable (which inherits IUnknown's slots 0–2) serves as the IUnknown identity pointer; when the class has no user @COM conformances, the compiler emits a standalone IUnknown vtable at P[-2]. User-declared interfaces beyond the first grow outward from P[-3]. When a subclass adds new conformances, the new vtable pointers are prepended furthest from P, so existing slots remain at their established offsets.

For the full object layout — including vtables, conformance table, protocol witness tables, and dispatch flows — see the COM Object Memory Layout companion document.

The instance contains one vtable pointer (lpVtbl) per COM interface, including the compiler-synthesised ISwiftObject. ISwiftObject is always at P[-1], regardless of how many interfaces the class implements. User-declared interfaces are at higher negative offsets. Each pointer refers to a static, per-class vtable containing the function pointers for that interface. The vtable[-1] entry in each static vtable records the byte offset from the COM interface pointer back to P. COM clients index from vtable[0] and never observe the negative-offset entry. This pre-vtable placement is analogous to the Itanium C++ ABI's offset_to_top field, which stores the displacement from a subobject's vtable pointer to the top of the most-derived object.

The fixed position of ISwiftObject at P[-1] enables a fast path for Swift object recovery: the synthesised QueryInterface compares ISwiftObject.IID before walking the conformance table and returns P[-1] directly on match, with no metadata lookup. This avoids a per-class conformance table read on every as? cast to a concrete Swift type. The vtable[-1] entry is never read speculatively; it is an implementation detail of the ISwiftObject.object getter, only accessed after QueryInterface confirms the object is Swift-originated.

The primary COM interface pointer (returned by QueryInterface for IUnknown) is at a per-class offset from P, recorded in the class's conformance table. This satisfies the COM identity rule: QueryInterface for IUnknown always returns the same pointer for a given object, though the offset from P depends on the class.

For the common case of a class implementing a single interface, the overhead is exactly one additional pointer (ISwiftObject at P[-1]).

When a subclass adds IAccessible, the layout becomes:

P[-3]: [IAccessible vtable ptr]    ← new, prepended furthest from P
P[-2]: [IInterface vtable ptr]     ← unchanged
P[-1]: [ISwiftObject vtable ptr]   ← unchanged, always at P[-1]
P[ 0]: [isa / type metadata]

Existing slots (IInterface at P[-2], ISwiftObject at P[-1]) remain at their established offsets. New interfaces prepend outward. The layout is binary compatible across the class hierarchy.

COM does not require any particular ordering of interface vtable pointers within an object. COM clients receive individual interface pointers from QueryInterface and never observe the object's internal layout. The layout choice (ISwiftObject closest to P for the recovery fast path, user interfaces growing outward) is an internal implementation detail. The only COM-level constraint is the identity rule, satisfied by a stable primary interface offset per class.

Memory Management

The ARC Bridge

COM and Swift ARC share a single unified reference count. AddRef/Release (called by COM clients) and swift_retain/swift_release (called by Swift ARC) operate on the same counter. The object is deallocated exactly once.

No new runtime entry points are needed. The existing swift_retain and swift_release are modified to return the post-operation reference count alongside their existing behaviour (HeapObject is the Swift runtime's internal representation of a heap-allocated object):

At the LLVM IR level (intptr denotes the pointer-width integer type):

; Previously: ptr @swift_retain(ptr)
; Now returns the object pointer and the post-retain count:
declare { ptr, intptr } @swift_retain(ptr)

; Previously: void @swift_release(ptr)
; Now returns the post-release count (0 on final release):
declare intptr @swift_release(ptr)

At the LLVM IR level, swift_retain returns { ptr, i64 } and swift_release returns i64. On x86-64 System V and ARM64, the two-element struct return from swift_retain occupies two registers (rax+rdx / x0+x1); existing callers that only read the first register (the object pointer) are unaffected. For swift_release, existing callers that expect void simply ignore the return register.

AddRef and Release are @_alwaysEmitIntoClient functions in the COM module, emitted locally in each client module with no cross-module call. The AddRef function reads the adjustment at vtable[−1] to recover the Swift object pointer, calls swift_retain, destructures the { ptr, i64 } return to extract the count, and returns it as a ULONG. Release recovers P the same way and tail-calls swift_release, whose i64 return is the count directly. Both functions are generic across all @COM classes (the adjustment constant is the only per-class variation, read from vtable[−1] at runtime), so every non-aggregated vtable's AddRef and Release slots point to the same two functions. For classes conforming to COMAggregatable, the compiler emits the delegating variants instead (see Aggregation below). No per-class code is generated for either pair.

COM's specification states the returned count is advisory ("intended for test purposes only"), which is consistent with Swift's ARC semantics.

The Swift UUID Namespace

IIDs for Swift-specific COM interfaces are derived deterministically using UUID version 5 (SHA-1, RFC 4122 Section 4.3) from a reserved Swift namespace UUID:

Namespace: {E29CA80E-0000-0000-C000-000000000000}

To derive an IID, the namespace bytes are concatenated with the UTF-8 encoding of the interface name, SHA-1 hashed, and the first 16 bytes are formatted as a UUID v5 (version nibble set to 5, variant bits set to RFC 4122). This produces a stable, deterministic IID for each name within the namespace, with no external registry needed.

ISwiftObject

ISwiftObject is a @COM protocol synthesised on every @COM class for recovering the underlying Swift heap object from any COM interface pointer. Its IID is derived from the Swift namespace with name "ISwiftObject":

// IID derived via UUID v5 (SHA-1) from the Swift namespace
// {E29CA80E-0000-0000-C000-000000000000} with name "ISwiftObject"
@COM(IID: "8E369447-5188-5ADA-B9EC-8FCB732D226B")
public protocol ISwiftObject: IUnknown {
    var object: Unmanaged<AnyObject> { @_lifetime(borrow self) get }
}

The property returns Unmanaged because the COM caller already holds a reference via AddRef; the returned pointer is borrowed, not an additional ownership stake. The @_lifetime(borrow self) annotation makes this explicit: the returned reference is valid only while the COM object is alive.

The ISwiftObject vtable pointer is always at P[-1] (see Object Layout above). Within the synthesised QueryInterface, ISwiftObject.IID is the first comparison (before IUnknown and the conformance table scan), returning P[-1] directly on match with no table walk. This provides a fast path for Swift object recovery. The object getter internally uses the vtable[-1] byte offset to recover the Swift object pointer; this is safe because the getter is only reachable after QueryInterface for ISwiftObject has succeeded, confirming the object is Swift-originated (see Existentials and Cast lowering decision tree above).

Explicitly conforming a @COM class to ISwiftObject is a compile-time error. The conformance is compiler-managed: the vtable pointer position, the vtable contents, the object getter implementation, and the fast-path QueryInterface check are synthesised as a unit. A user-provided object getter would bypass the vtable[−1] recovery mechanism and produce incorrect results.

The COM Allocator

COM requires that cross-boundary memory (arrays, strings, structs returned via [out] parameters) be allocated with the COM task allocator so the caller can free it with the matching deallocator. Mismatched allocators (e.g., malloc/CoTaskMemFree) are undefined behaviour.

COM has distinct allocation domains, each with its own allocate/deallocate pair:

Domain Allocate Deallocate Used for
CoTaskMem CoTaskMemAlloc CoTaskMemFree [out] arrays, LPWSTR strings, structs
BSTR SysAllocString SysFreeString COM BSTR strings

Mismatching domains (e.g., allocating with SysAllocString and freeing with CoTaskMemFree) is undefined behaviour. The synthesised wrappers select the correct domain automatically based on the parameter type, so the user never interacts with COM allocators directly. When consuming a COM method, the wrapper frees returned memory with the matching deallocator. When implementing a COM method, the wrapper allocates output memory with the matching allocator so the COM caller can free it correctly.

Advanced users who need raw access to a specific domain (e.g., implementing a custom COM interface with unusual memory management) can call the underlying C functions (CoTaskMemAlloc/CoTaskMemFree, etc.) directly through the C import.

Parameter Passing and Ownership

COM's MIDL annotations encode both direction and ownership. The Clang importer uses these annotations to determine the ownership semantics for each parameter, enabling the synthesised thunks to elide redundant AddRef/Release pairs internally:

MIDL annotation COM semantics Swift mapping
[in] T (value) Caller passes by value T
[in] IInterface* Caller retains; callee borrows (any IInterface)?
[out] T* Callee writes; caller reads result Part of tuple return
[out] IInterface** Callee transfers ownership to caller Caller adopts (no extra retain)
[in, out] T* Caller passes; callee may modify inout T
[in, out] IInterface** Caller passes owned; callee may replace inout (any IInterface)?
[out, retval] T* Primary return value Return type of the function
[out, size_is] T** + count Callee-allocated array [T] return
[in, size_is] T* + count Caller-provided array [T] parameter

The Swift API surface uses normal owned parameter conventions, without a borrowing keyword on [in] interface pointer parameters. Implementors can freely assign parameters to stored properties without needing copy.

The AddRef/Release elision for [in] parameters happens inside the synthesised COM-to-Swift bridge thunk, which knows from the MIDL annotation that the parameter is borrowed at the COM level. The thunk omits the AddRef on entry and Release on exit, so the Swift developer sees a regular parameter and the overhead is eliminated without any syntactic cost.

When implementing a @COM method in Swift, the synthesised thunk retains [out] interface pointers before returning them to the COM caller. [in, out] maps to inout. Swift code never calls AddRef or Release manually.

Lifetime Dependencies

The synthesised thunks eliminate AddRef/Release overhead for [in] parameters invisibly (see above). Swift's @_lifetime dependencies (SE-0446/SE-0447) extend this further for cases where zero-copy access is needed and a lifetime constraint must be visible in the API.

For [in] arrays, the default [T] parameter requires copying into a CoTaskMemAlloc-allocated buffer. For performance-sensitive paths, an alternative overload takes Span<T>, which borrows the caller's array storage directly with no copy and no allocation:

// Default (ergonomic, copying):
func setItems(_ items: [IID]) throws

// Opt-in zero-copy (Span is inherently non-escapable):
func setItems(_ items: Span<IID>) throws  // pins storage directly

Some COM patterns return data that borrows from the source object. For example, IBuffer::get_Data returns a pointer into the buffer's memory. A lifetime dependency makes this safe and zero-copy:

// COM: HRESULT get_Data([out, retval] BYTE** ppData);
var data: Span<UInt8> { @_lifetime(borrow self) get throws }

The caller can read the data without copying. The Span borrows from the COM object and cannot outlive it.

The @_lifetime(borrow self) annotation on ISwiftObject.object (shown above) uses the same mechanism to ensure that using the returned pointer after the COM object is released is a compile-time error rather than a use-after-free.

The default API surface uses copying ([T], String) for ergonomics. The Span paths are available for performance-sensitive code that can work within the lifetime constraints.

Nullability

COM interface pointers are nullable by default. Swift maps this using optionals, following the same strategy as Objective-C interop: _Nullable/_Nonnull annotations are respected; unannotated pointers default to optional.

C/C++ declaration Swift
[in] IInterface* (unannotated) (any IInterface)?
[in] IInterface* _Nonnull any IInterface
[out, retval] IInterface** (unannotated) (any IInterface)? return
[in] BSTR (unannotated) String?
[in] BSTR _Nonnull String
[in] UINT32 UInt32 (value types are never optional)

The ARC bridge handles nil correctly: AddRef/Release on a nil COM pointer are no-ops.

Error Handling

COM uses HRESULT (a 32-bit integer) as the universal error return type. The Clang importer promotes HRESULT-returning pure virtual methods on COM interfaces to Swift throws:

HRESULT GetCount([out, retval] UINT32 *count);
HRESULT GetAt([in] UINT32 index, [out, retval] IUnknown **item);
HRESULT SetName([in] BSTR name);
HRESULT GetIids([out] ULONG *iidCount, [out, size_is(, *iidCount)] IID **iids);
var count: UInt32 { get throws }
func getAt(_ index: UInt32) throws -> (any IUnknown)?
func setName(_ name: String) throws
func getIids() throws -> [IID]

[out, retval] becomes the return value. HRESULT becomes throws. MIDL array annotations ([size_is], [length_is], [max_is]) are promoted to [T] parameters and returns with correct valid-element trimming. The HRESULT-to-throws promotion can be suppressed per method via APINotes, using a Clang attribute (__attribute__((swift_com_returns_hresult)), exposed as the C macro SWIFT_COM_RETURNS_HRESULT). When applied, the method retains HRESULT as its Swift return type instead of being promoted to throws. This is needed for methods that distinguish S_OK from S_FALSE (e.g., IEnumXxx::Next, IPersistStream::IsDirty): since S_FALSE is a success code (bit 31 clear), the throws promotion does not throw for it, and the caller cannot distinguish it from S_OK without the opt-out.

When a COM method fails, the wrapper captures the thread-local IErrorInfo into a COMError:

public struct COMError: Error {
    public let hresult: HRESULT
    public let description: String?
    public let source: String?
    public let helpFile: String?
    public let helpContext: UInt32
}

When a Swift @COM method throws, the synthesised wrapper populates IErrorInfo from the error before returning the HRESULT. Classes that opt into ISupportErrorInfo and IErrorInfo (see COM Classes above) advertise this capability to COM infrastructure. Non-COMError Swift errors map to E_FAIL with the error's localizedDescription.

String Bridging

COM uses different string types, each with its own allocation domain. All are bridged transparently to Swift String.

BSTR (COM) — a length-prefixed, mutable UTF-16 string using SysAllocString/SysFreeString:

  • [in] BSTRString: wrapper converts to temporary BSTR, passes it, frees after the call.
  • [out, retval] BSTR*String return: wrapper receives BSTR, converts, frees with SysFreeString.

LPWSTR / LPSTR — plain null-terminated C strings allocated with CoTaskMemAlloc:

  • [out] LPWSTR*String return: freed with CoTaskMemFree.
  • [in] LPCWSTRString: wrapper provides a temporary null-terminated buffer for the call duration.

The synthesised wrappers select the correct allocator automatically based on the parameter type; Swift code always works with String.

Threading Model and Executors

COM's apartment model is a correctness requirement. A @COM class declares its threading model:

@COM(CLSID: "...", ThreadingModel: .both)
class CImplementation: IInterface { ... }
public enum COMThreadingModel: Int, Sendable {
    /// Single threading. The object lives on the main STA thread (thread 0)
    /// and all calls are marshalled there. This is the most restrictive model,
    /// used for legacy objects that must run on the process's main thread.
    /// Corresponds to `@MainActor` semantics in Swift.
    case single

    /// Apartment threading. The object lives on a single STA thread, and COM
    /// serialises calls through that thread's message loop. Only one thread
    /// may call the object at a time. This is the default threading model.
    case apartment

    /// Free threading. Any thread may call the object concurrently.
    /// The implementation must provide its own synchronisation.
    case free

    /// Compatible with both apartment and free threading. COM may place the
    /// object in whichever apartment type the caller is in. The implementation
    /// must be thread-safe.
    case both

    /// Neutral Apartment (COM+). Calls execute on the calling thread without
    /// thread switching or message-loop involvement. The implementation must
    /// be thread-safe.
    case neutral

    /// Alias for `.apartment`.
    public static var sta: Self { .apartment }
    /// Alias for `.free`.
    public static var mta: Self { .free }
}

A @COM(CLSID:) class that is also a Swift actor receives ThreadingModel: .apartment implicitly, because actor isolation serialises calls, matching apartment threading semantics. Specifying .free or .both on an actor is a compile-time error. A bare @COM actor (no CLSID:) does not carry a threading model; its concurrency safety comes from actor isolation alone.

Sendable and threading safety

All @COM protocols implicitly conform to Sendable. A COM existential (any IInterface) can be passed freely across concurrency domains. The COMSingleThreadedExecutor ensures that method calls are dispatched to the correct apartment, so holding a reference on one thread and calling from another is safe.

For @COM classes with a CLSID: and ThreadingModel:, Sendable follows from the threading model:

  • A @COM actor is Sendable through actor isolation, as with any Swift actor.
  • A @COM(CLSID:, ThreadingModel: .free), .both, or .neutral class is implicitly @unchecked Sendable. The threading model is the author's contract that the implementation is thread-safe; Swift cannot verify this statically.
  • A @COM(CLSID:, ThreadingModel: .apartment) or .single class is not implicitly Sendable. These objects require serialised access, and without actor isolation Swift has no static proof of that. The recommended path is to use an actor.

Bare @COM classes (no CLSID:) have no threading model declaration and follow Swift's standard Sendable rules: they are not Sendable unless the author marks them @unchecked Sendable or makes them an actor.

IUnknown and ISwiftObject, as @COM protocols, are Sendable.

COM initialisation on the current thread

COM requires every thread that makes COM calls to initialise the COM library first (CoInitializeEx) and to uninitialise it when done (CoUninitialize). Failing to initialise produces CO_E_NOTINITIALIZED; failing to uninitialise leaks resources. In C/C++, this is manual ceremony at the start and end of each thread.

Swift provides three patterns for COM initialisation, suited to different program shapes. In all three, CoUninitialize is called automatically. The user never calls it manually, even on error paths.

For short-lived COM usage within a larger program, withCOMContext scopes the initialisation:

/// Initialise COM on the current thread for the duration of `body`.
/// Calls CoInitializeEx on entry and CoUninitialize on exit.
public func withCOMContext<T>(
    _ model: COMThreadingModel,
    operation body: () throws -> T
) rethrows -> T

// async variant
public func withCOMContext<T>(
    _ model: COMThreadingModel,
    operation body: () async throws -> T
) async rethrows -> T
try withCOMContext(.free) {
    let voice = try SpVoice()
    try voice.speak("Hello")
}
// CoUninitialize called automatically on scope exit

For long-lived programs where COM is used throughout and wrapping the entire body in a closure is impractical, COMContext provides explicit lifetime management:

/// A COM context that initialises COM on the current thread at construction
/// and uninitialises it on deallocation.
public struct COMContext: ~Copyable {
    /// Initialise COM with the specified threading model.
    public init(_ model: COMThreadingModel) throws
    deinit  // calls CoUninitialize
}
func main() throws {
    let com = try COMContext(.apartment)
    // COM is initialised for the lifetime of `com`

    let voice = try SpVoice()
    try voice.speak("Hello")
    processEvents()
    // ...hundreds of lines of COM usage...
}
// CoUninitialize called when `com` goes out of scope

For programs that are fundamentally COM-based, @COMMain eliminates all ceremony:

@COMMain(.apartment)
struct MyApp {
    static func main() throws {
        let voice = try SpVoice()
        try voice.speak("Hello")
    }
}
// CoInitializeEx called before main(); CoUninitialize called after main() returns

@COMMain composes with @main. It wraps the entry point in COM initialisation/uninitialisation, similar to how @UIApplicationMain wraps UIKit setup.

Executors for asynchronous code

COMSingleThreadedExecutor bridges COM apartments to Swift's concurrency model. Each COMSingleThreadedExecutor owns a dedicated thread that is COM-initialised as STA with a message loop:

/// A serial executor that dispatches work to a COM Single-Threaded Apartment.
/// Owns a dedicated thread with CoInitializeEx(COINIT_APARTMENTTHREADED) and
/// a message loop. CoUninitialize is called when the executor is deallocated.
public final class COMSingleThreadedExecutor: SerialExecutor {
    /// Create a new STA on a dedicated thread.
    public init()

    /// The executor for the main thread's STA.
    public static let main: COMSingleThreadedExecutor

    /// The executor for the current thread's STA, if one exists.
    public static var current: COMSingleThreadedExecutor? { get }
}

/// A task executor whose threads are COM MTA-initialised.
/// CoInitializeEx(COINIT_MULTITHREADED) is called on each thread on first use;
/// CoUninitialize is called on thread teardown.
public final class COMMultiThreadedExecutor: TaskExecutor {
    public static let shared: COMMultiThreadedExecutor
}

This solves three problems:

  1. COMSingleThreadedExecutor and COMMultiThreadedExecutor call CoInitializeEx on their threads and CoUninitialize on teardown automatically. No manual initialisation is needed in async code.
  2. Calling an STA COM object from the cooperative thread pool automatically hops to the object's COMSingleThreadedExecutor, preventing the cross-apartment deadlocks that occur when STA objects are called from the wrong thread.
  3. A @COM(ThreadingModel: .apartment) actor uses a COMSingleThreadedExecutor as its executor, so actor isolation and apartment serialisation are the same thing.

Imported STA COM objects capture the COMSingleThreadedExecutor of their creating thread. Method calls from a different executor automatically hop to the correct apartment.

Object Creation and Activation

Consuming COM Objects

In C++, CoCreateInstance takes a CLSID, a requested IID, and returns a void pointer. The raw C function imports with its original signature (positional parameters, void** out-pointer, HRESULT return). The COM module provides overlays, Swift-friendly wrappers that add generics, throws, labeled parameters, and task-local activation context, following the same pattern as the standard library's Darwin/Glibc overlays:

/// The class context flags controlling COM server activation scope.
public struct CLSCTX: OptionSet, Sendable {
    /// In-process DLL server.
    public static let inproc: CLSCTX
    /// In-process OLE handler (lightweight proxy for embedded objects).
    public static let handler: CLSCTX
    /// Out-of-process EXE on the same machine.
    public static let local: CLSCTX
    /// DCOM remote machine.
    public static let remote: CLSCTX
    /// Any available server.
    public static let all: CLSCTX
}

/// Options controlling how COM objects are activated.
public struct COMActivationOptions: Sendable {
    public var context: CLSCTX
    public var server: COMServerInfo?  // for DCOM remote activation
    public static let `default` = COMActivationOptions(context: .all)
}

/// Activate a COM object by CLSID, returning the requested interface.
/// Uses the current activation context (default: .all).
public func CoCreateInstance<T: IUnknown>(_ clsid: CLSID) throws -> T

The activation context is managed via a @TaskLocal value. The default is .all, so the common case requires no ceremony:

let voice = try SpVoice()  // CLSCTX.all, zero ceremony

When a different context is needed, withActivationContext scopes it over a block. All activations inside the block, including through convenience initialisers, nested function calls, and async/await, use the scoped context via task-local propagation:

/// Execute `body` with a specific COM activation context.
public func withActivationContext<T>(
    _ context: CLSCTX,
    server: COMServerInfo? = nil,
    operation body: () throws -> T
) rethrows -> T

// async variant
public func withActivationContext<T>(
    _ context: CLSCTX,
    server: COMServerInfo? = nil,
    operation body: () async throws -> T
) async rethrows -> T
// In-process only — all activations in the block use .inproc:
try withActivationContext(.inproc) {
    let voice = try SpVoice()
    let synth = try SpSynth()   // also .inproc
}

// Remote activation (DCOM):
try await withActivationContext(.remote, server: COMServerInfo(name: "server.example.com")) {
    let engine = try Engine()   // activates on remote machine
}

// Scoping composes naturally:
try withActivationContext(.inproc) {
    let voice = try SpVoice()       // .inproc
    try withActivationContext(.local) {
        let outOfProc = try SpVoice()  // .local
    }
    let synth = try SpSynth()       // back to .inproc
}

The generic CoCreateInstance overlay and all synthesised convenience initialisers read the task-local context internally, so no context: parameter is needed at any call site. This keeps the API surface minimal while allowing full control over the activation scope. The raw C functions remain available for advanced use cases that need direct control.

Advanced Activation Overlays

The COM module provides additional overlays for less common activation patterns:

CoCreateInstanceEx can request multiple interfaces in a single round-trip, avoiding separate QueryInterface calls after creation. This is especially valuable for remote activation where each query would be a network round-trip:

let (voice, events) = try CoCreateInstanceEx(
    SpVoice.CLSID,
    requesting: (any ISpVoice).self, (any ISpEventSource).self
)

For COM aggregation, the outer object passes its IUnknown as the controlling unknown. COM requires that the requested interface be IUnknown when a controlling unknown is provided:

let inner: any IUnknown = try CoCreateInstance(
    InnerClass.CLSID,
    for: self  // outer's IUnknown
)

These overlays are library code in the COM module, not compiler features. They convert HRESULT to throws, derive the IID from the generic type parameter, and read the activation context from the task-local scope.

Implementing COM Servers

For each @COM(CLSID:) class in a Swift module, the compiler synthesises an IClassFactory and the activation entry point appropriate for the output type. The compiler uses the output mode to determine which activation model to generate:

-emit-library (dynamic) : The compiler emits DllGetClassObject as a DLL export, dispatching over all @COM(CLSID:) classes in the module. It also emits DllRegisterServer and DllUnregisterServer for registry management. This is the standard in-process COM server model. Each dynamic library is a self-contained COM server; @COM(CLSID:) classes in a module built as a dynamic library are not merged into another module's activation entry point. A static library's @COM(CLSID:) classes are not exported (see -emit-library -static below).

-emit-executable : The compiler emits a call to CoRegisterClassObject at startup for each @COM(CLSID:) class, registering the class factories with the COM runtime. Out-of-process COM servers require a message loop (or equivalent run loop) to pump COM calls. The synthesised registration is placed in the module's initialisation path; the developer provides the run loop.

-emit-library -static : No activation entry point is generated. A static library is consumed by another DLL or executable and is not directly exposed to the COM runtime. The @COM(CLSID:) classes are available for the consuming binary to register through its own activation mechanism.

The synthesised class factory needs to know which initialiser to call when IClassFactory::CreateInstance is invoked. @COMInit marks that initialiser:

@COM(CLSID: "...", ThreadingModel: .both)
class CImplementation: IInterface {
    @COMInit
    init(controller: (any IUnknown)? = nil, config: Config = .default) { ... }
}

@COMInit is a Swift attribute (not a macro). At most one initialiser per class may carry it. The initialiser must accept an optional controller: (any IUnknown)? parameter for aggregation support; additional parameters must have default values (since IClassFactory::CreateInstance cannot pass them). If no @COMInit is present, the compiler looks for required init() or required init(controller:) and emits a diagnostic if neither exists.

When a non-Swift caller activates a class via CoCreateInstance, the synthesised class factory calls the @COMInit initialiser, queries the requested interface, and returns it. This works identically for in-process (DLL) and out-of-process (EXE) servers; the difference is only in how the class factory is registered with the COM runtime.

COMInterfaceResolver and Aggregation

The compiler synthesises base QueryInterface from the class's declared @COM conformances. For classes that need to respond to additional IIDs (aggregation, tear-offs, conditional interfaces), the COM module provides a protocol:

/// Conform to this protocol to extend `QueryInterface` with additional
/// interfaces beyond the class's declared @COM conformances.
public protocol COMInterfaceResolver {
    /// Called by the synthesised `QueryInterface` after checking declared
    /// conformances. Return a COM interface pointer for the requested IID,
    /// or nil to decline.
    func resolve(_ iid: borrowing IID) -> UnsafeMutableRawPointer?
}

Classes that do not conform to COMInterfaceResolver have no extension overhead; QueryInterface only searches declared conformances. Classes that conform get resolve(_:) called for unrecognised IIDs. This is the standard Swift extension-point pattern: opt-in via protocol conformance.

Tear-off interfaces

Tear-off interfaces are interface implementations created lazily rather than pre-allocated in the vtable block. They come in three variants, all built as library-level helpers that compose with COMInterfaceResolver:

  • CachedTearOff<T> — created on first QueryInterface and stored for the owner's lifetime. Subsequent queries return the same instance. Not thread-safe; suited to apartment-threaded or single-threaded objects where COM serialises calls.
  • AtomicCachedTearOff<T> — same semantics as CachedTearOff, but uses atomic initialization internally. Suited to free-threaded objects (.free, .both, .neutral) where concurrent QueryInterface calls are possible. On the post-initialization fast path, the cost is a single atomic load.
  • DisposableTearOff<T> — created fresh on every QueryInterface. Each caller receives an independently reference-counted instance. Suited to interfaces that hold per-caller state. Inherently thread-safe if the factory closure is.

Each tear-off helper stores a factory closure that creates a @COM class implementing the interface. The resolve(_:) method checks the IID, creates or retrieves the instance, and returns its COM interface pointer. The factory closure is responsible for creating an instance that delegates its IUnknown to the owner via COMAggregatable conformance (see Aggregation below), satisfying the COM identity rule.

Thread safety of COMInterfaceResolver.resolve(_:) follows the object's threading model. For apartment-threaded and single-threaded objects, COM serialises calls, so resolve is never called concurrently. For free-threaded objects, the implementation must be safe for concurrent calls — this is the same thread-safety obligation the threading model already imposes on all methods. Bare @COM classes (no CLSID:) follow Swift's own concurrency rules.

@COM(CLSID: "...")
class CImplementation: IInterface, COMInterfaceResolver {
    // Cached: created once, returned for all subsequent queries
    private var accessibility = CachedTearOff<IAccessible> {
        AccessibilityImpl(owner: self)
    }

    // Disposable: fresh instance per caller
    private let connectionPoint = DisposableTearOff<IConnectionPoint> {
        ConnectionPointImpl()
    }

    func resolve(_ iid: borrowing IID) -> UnsafeMutableRawPointer? {
        accessibility.resolve(iid) ?? connectionPoint.resolve(iid)
    }
}

Conditional interface support and custom QueryInterface logic follow the same pattern: conform to COMInterfaceResolver and implement resolve(_:).

Aggregation

COM aggregation (where an outer object delegates interfaces to an inner object) is built on COMInterfaceResolver, the tear-off helpers, and a delegation protocol. The only compiler awareness needed is checking COMAggregatable conformance at vtable emission time to select delegating vs. non-delegating IUnknown slots; all other aggregation mechanics are library and macro code.

COMAggregatable

The COM module provides a protocol for IUnknown delegation:

public protocol COMAggregatable: AnyObject {
    var controller: (any IUnknown)? { get }
}

The compiler emits delegating variants of all three IUnknown slots (AggregatedQueryInterface, AggregatedAddRef, AggregatedRelease) in the vtable for classes that conform to COMAggregatable, instead of the non-delegating QueryInterface/AddRef/Release. This is a compile-time vtable slot selection — zero runtime cost on either path. The delegating variants recover the controller from the COMAggregatable conformance and forward the call to the outer's IUnknown. The only compiler awareness needed is checking COMAggregatable conformance at vtable emission time.

A @COM class that conforms to COMAggregatable has its QueryInterface, AddRef, and Release automatically delegated. The class's own conformance table is not consulted; the outer object's IUnknown handles all identity and lifetime operations. This is how tear-off instances delegate to their owner: the factory closure creates a @COM class conforming to both the target interface and COMAggregatable.

The @COMAggregation macro

The @COMAggregation macro generates a COMInterfaceResolver conformance on the outer class. When QueryInterface is asked for an aggregated interface, the resolver creates a tear-off whose factory produces an inner @COM class that conforms to the aggregated interface (forwarding methods to the real inner object) and to COMAggregatable (delegating IUnknown to the outer).

@COMAggregation(IMarshal.self, clsid: FreeThreadedMarshaler.CLSID)
@COM(CLSID: "...")
class CImplementation: IInterface { ... }

The macro takes an interface type and a CLSID. The inner is created via CoCreateInstance with the CLSID — the general activation path that works for foreign COM objects, Swift @COM(CLSID:) classes, and well-known system objects alike.

The macro generates:

  • A COMInterfaceResolver conformance with a tear-off per aggregated interface.
  • A forwarding @COM class per aggregated interface that implements the interface methods by delegating to the inner object and conforms to COMAggregatable with the outer as controlling unknown.
  • Storage for the inner object, created lazily via CoCreateInstance with the provided CLSID.
  • A @dynamicMemberLookup subscript on the outer class with key path forwarding to the inner, so the user can call aggregated interface methods directly on the outer without casting through as?.

The inner object is created via CoCreateInstance as a normal COM activation. It does not need aggregation awareness. The macro-generated forwarder wraps it and delegates IUnknown to the outer via COMAggregatable.

// Aggregating the Free-Threaded Marshaller:
@COMAggregation(IMarshal.self, clsid: FreeThreadedMarshaler.CLSID)

// Aggregating a custom accessibility provider:
@COMAggregation(IAccessible.self, clsid: AccessibilityProvider.CLSID)

Clang Importer Integration

The Clang importer applies two layers of transformation to produce idiomatic Swift from C/C++ COM headers.

General Windows SDK Improvements

These improve the imported Swift presentation of all Windows SDK headers, independent of COM:

  • BSTR ↔ String bridging as described above.

  • MIDL array annotations: the full family of MIDL array attributes maps to natural Swift arrays:

    MIDL pattern Swift mapping
    [out, size_is(n)] T* + count n [T] return (count elements)
    [in, size_is(n)] T* + count n [T] parameter
    [out, size_is(cap), length_is(len)] T* [T] return trimmed to len valid elements
    [out, max_is(n)] T* [T] return (capacity n+1, trimmed to valid count)
    [in, string] WCHAR* String (null-terminated, count derived from content)

    The [size_is] + [length_is] combination is the most important beyond bare [size_is], as it appears in every IEnumXxx::Next pattern:

    // IEnumUnknown::Next
    HRESULT Next([in] ULONG celt,
                 [out, size_is(celt), length_is(*pceltFetched)] IUnknown **rgelt,
                 [out] ULONG *pceltFetched);
    func next(count: UInt32) throws -> [any IUnknown]  // .count == actual fetched count

    Both the capacity and valid-length parameters are consumed by the promotion and do not appear in the Swift signature. The returned [T] contains only the valid elements.

  • Multi-[out] parameters without [retval] → named tuple return. Parameters that are metadata for another parameter (counts for [size_is]) are consumed by array promotion and don't appear. Remaining independent [out] parameters become named tuple elements:

    HRESULT GetBounds([out] LONG *pLeft, [out] LONG *pTop,
                      [out] LONG *pWidth, [out] LONG *pHeight);
    func getBounds() throws -> (left: Int32, top: Int32, width: Int32, height: Int32)

    Mixed [in] + [out] methods keep [in] as parameters and return [out] as the tuple.

  • PascalCase → camelCase name translation.

  • Hungarian prefix stripping: pszNamename, dwFlagsflags, cItemsitems.

  • Boolean disambiguation: VARIANT_BOOL (-1 = true), BOOLEAN (1 byte), and BOOL (4 bytes) all bridge to Swift Bool with correct truth-value conversion.

  • Flag constants → OptionSet: related #define groups (CLSCTX_*, STGM_*) import as OptionSet types.

  • [defaultvalue] → Swift default parameters: MIDL [defaultvalue(n)] annotations on [in] parameters map to Swift default parameter values, allowing callers to omit trailing arguments:

    HRESULT CreateWindow([in] BSTR title,
                         [in, defaultvalue(800)] LONG width,
                         [in, defaultvalue(600)] LONG height);
    func createWindow(title: String, width: Int32 = 800, height: Int32 = 600) throws

COM-Specific Promotion

These require -enable-com-interop and the COM module:

  • Structural IUnknown detection: C/C++ types whose first three virtual slots match the QueryInterface/AddRef/Release signature (by parameter types and return types) are recognised as COM interfaces. Where __declspec(uuid(...)) is present, the IID is used directly. Where a separate IID_<TypeName> constant follows the standard COM naming convention, the importer associates it automatically. If neither is available, the importer emits a diagnostic; the user can provide the IID via a Swift extension. False positives from coincidental signature matches are possible but rare in practice.
  • IOKit structural detection: C structs with function-pointer tables matching the COM method pattern are recognised as COM interfaces; their IIDs are derived from the associated CFUUIDRef constants.
  • HRESULT → throws for pure virtual methods on COM interfaces.
  • [out, retval] → return value promotion.
  • [in]/[out] → ownership conventions: [in] parameters use normal Swift conventions at the API surface; the synthesised thunk elides AddRef/Release internally. [out] returns are adopted without extra retain. [in, out] maps to inout.
  • get_/put_/putref_ prefix → property synthesis (e.g., get_Name/put_Namevar name).
  • __declspec(uuid(...)).IID static property synthesis.

COM Collections and Iteration

Classic COM: IEnumXxx

Classic COM enumerators follow the Next/Skip/Reset/Clone pattern. When the importer detects this shape, it synthesises IteratorProtocol and Sequence conformances:

// IEnumUnknown automatically conforms to Sequence where Element == any IUnknown
for item in enumUnknown {
    // item: any IUnknown
}

Exporting Swift COM Interfaces

When a developer authors a @COM protocol and implements it in a @COM class, the resulting binary is immediately usable as a COM object: the vtable layout is correct, the IID is registered, and the appropriate activation entry point (DllGetClassObject for DLLs, CoRegisterClassObject for executables) routes activation. Non-Swift callers that have a matching interface description can call it directly.

Non-Swift callers need an interface description: a C/C++ header or MIDL IDL file. Today these must be authored manually. The @COM attribute retains sufficient information for mechanical generation. A future toolchain tool, analogous to -emit-objc-header, would generate these automatically:

swiftc -emit-com-header MyModule.swiftmodule -o MyModule.h
swiftc -emit-com-idl MyModule.swiftmodule -o MyModule.idl

The name translation rules apply in reverse: Swift camelCase → COM PascalCase; var name: T { get throws }[propget] HRESULT get_Name([out,retval] T*); func advise(...)HRESULT Advise(...).

MIDL Type System Mapping

For full fidelity across the language boundary, the MIDL type system must map completely to Swift. The following table covers the base types:

MIDL type Swift type
BYTE, UCHAR UInt8
WORD, USHORT UInt16
DWORD, ULONG UInt32
ULONGLONG UInt64
CHAR CChar
SHORT Int16
LONG Int32
LONGLONG Int64
FLOAT, DOUBLE Float, Double
BOOL (4-byte) Bool
BOOLEAN (1-byte) Bool
VARIANT_BOOL (-1 = true) Bool (with correct conversion)
BSTR String
LPSTR, LPWSTR String (with appropriate encoding)
HRESULT HRESULT / throws
GUID GUID<Tag> (phantom-typed generic struct)
IID IID (typealias for GUID<IIDTag>)
CLSID CLSID (typealias for GUID<CLSIDTag>)
IUnknown* (any IUnknown)?
MIDL enum Swift enum: Int32 (or UInt32 for WinRT flag enums)
MIDL struct Swift struct (with layout validation)
Flag #define groups OptionSet

Future Directions

Automation

IDispatch and late binding: IDispatch enables late-binding COM access from scripting engines, Office VBA, and .NET COM interop. A @COM class may manually conform to IDispatch today. Future work could synthesise GetIDsOfNames and Invoke from the class's vtable interface, and generate type libraries (.tlb) from module metadata.

VARIANT and SAFEARRAY: VARIANT is COM's dynamically-typed value container, a discriminated union of ~30 types used pervasively in OLE Automation and IDispatch. A Swift mapping (enum with associated values or a bridging wrapper) is needed for automation interop. SAFEARRAY is COM's bounds-checked array type used with VARIANT arrays. MIDL's [optional] parameter annotation (distinct from [defaultvalue]) allows parameters to be omitted entirely in IDispatch calls; the runtime passes VT_ERROR / DISP_E_PARAMNOTFOUND in the VARIANT slot. Mapping [optional] to Swift requires VARIANT bridging and is deferred alongside it.

Dual interfaces: In MIDL, the [dual] attribute marks an interface that inherits from IDispatch and provides both vtable-based dispatch (fast, compile-time-bound) and IDispatch-based dispatch (late-bound, accessible from scripting languages). Nearly all Office COM interfaces and most ActiveX controls use [dual]. When consuming a [dual] interface, Swift uses the fast vtable path; the IDispatch side is irrelevant to a compiled caller. When implementing a [dual] interface, the compiler would need to synthesise both the custom vtable and the IDispatch::Invoke dispatch table from the same set of Swift requirements. This depends on IDispatch synthesis.

[oleautomation] type restriction: MIDL's [oleautomation] attribute restricts an interface to the subset of types that the universal (type-library-driven) marshaller can handle without custom proxy/stub DLLs: primitive types, BSTR, VARIANT, SAFEARRAY, IUnknown*, IDispatch*, and Automation-compatible structs and enums. In Swift, this could map to a compile-time validation flag on @COM protocols. The validation depends on the full Automation type set.

DECIMAL, CURRENCY, DATE: OLE Automation defines DECIMAL (128-bit fixed-point), CURRENCY (scaled 64-bit), and DATE (double, days since 1899-12-30). These need bridging to Swift's numeric and Foundation date types.

MIDL Union Types

MIDL defines encapsulated and non-encapsulated unions used in VARIANT, PROPVARIANT, and RPC interfaces. These need a Swift mapping (likely structs with computed accessors or enums with associated values).

Events and Connectable Objects

Synthesised IConnectionPointContainer and IConnectionPoint implementations would allow @COM classes to be event sources without manual boilerplate.

Server and Registration

Registration-free COM: Manifest-based activation (side-by-side assemblies) avoids registry pollution and is increasingly preferred. A future proposal could generate the necessary manifest XML from @COM metadata.

Out-of-process COM server extensions: the core design generates CoRegisterClassObject calls for -emit-executable targets. Additional features for out-of-process servers (custom REGCLS flags, surrogate activation via DllSurrogate, and APPID registration) could be exposed through separate annotations.

DCOM and Swift's Distributed module: DCOM extends COM with cross-process and cross-machine invocation via proxy/stub pairs. The COM runtime transparently inserts proxies for remote objects; from the caller's perspective, a remote COM object looks identical to a local one.

Swift's Distributed module provides a similar abstraction: distributed actor types can be reached across process and machine boundaries, with the compiler synthesising the serialisation thunks and the DistributedActorSystem protocol providing the pluggable transport layer.

The current design accommodates DCOM as a natural extension. The withActivationContext(.remote, server: ...) pattern already provides remote activation. The COMSingleThreadedExecutor handles local apartment dispatching. A future COMDistributedActorSystem could bridge COM's proxy/stub marshalling infrastructure to Swift's DistributedActorSystem protocol, enabling @COM distributed actor types whose methods are remotely callable through standard COM marshalling:

@COM(CLSID: "...", ThreadingModel: .apartment)
distributed actor MyRemoteService: IMyService {
    typealias ActorSystem = COMDistributedActorSystem

    distributed func compute(_ input: Data) throws -> Result { ... }
}

No changes to the core @COM attribute, vtable layout, or ARC bridge are needed for this. The distributed actor system is a library-level transport layer that operates above the COM binary interface, mapping remote method calls to COM proxy/stub marshalling and ActorID to COM's OXID/OID object identity.

Interface Export Tooling

The -emit-com-header and -emit-com-idl tools described in Exporting Swift COM Interfaces are toolchain investments that complement but do not require the language features.

Interface Versioning Conventions

COM interfaces are immutable once published. The standard IInterface2 pattern could be supported by deprecation annotations and version-negotiation helpers.

Foreign @COM Conformances

The current design requires that a @COM protocol conformance be declared in the same module as the conforming type, so the compiler can see all conformances when synthesising QueryInterface. A future extension could lift this restriction by allowing foreign conformances via runtime metadata tables and COM thunk objects.

A module that adds extension CImplementation: @retroactive IInterface { ... } (where CImplementation is defined in another module) would register a foreign COM conformance record in its metadata. The shared QueryInterface function already has a natural extension point between the static conformance table scan and the COMInterfaceResolver callback. A new step at that point would scan runtime metadata tables (analogous to how swift_conformsToProtocol finds retroactive conformances) for foreign @COM conformances matching the requested IID and the object's dynamic type.

When a foreign conformance is found, the runtime creates a COM thunk object: a small proxy holding a vtable for the foreign interface, its own reference count, and a back-pointer to the original Swift object. The thunk delegates IUnknown to the original object's primary interface pointer (the controlling-unknown pattern), satisfying the COM identity rule. This is implicit aggregation, using the same mechanics that explicit @COMAggregation uses.

The thunk is cached per-instance in a global concurrent side table keyed by (ObjectIdentifier, IID). On first QueryInterface for a foreign IID, the table misses, a thunk is created and inserted, and the pointer is returned. On subsequent queries, the table hits and the cached thunk is returned directly. When the thunk's external reference count reaches zero and it is deallocated, it removes its entry from the side table. The original object is unaware of the table; no modifications to the object layout are needed. This is conceptually similar to how the Objective-C runtime's associated object storage (objc_setAssociatedObject) attaches auxiliary data to an object without modifying its class layout, though the implementation would be a Swift-native concurrent data structure rather than the ObjC runtime's side table.

The performance cost is paid only on the foreign conformance path (two hash lookups after the static table scan misses), and only on the first query per instance per foreign IID (cached thereafter). The static conformance table remains the fast path for same-module conformances.

Throwing Setters

When Swift gains set throws support, @COM protocol properties whose setters can fail could use { get throws set throws } directly, removing the current setFoo(_:) throws workaround.

Custom Error-to-HRESULT Mapping

A COMErrorConvertible protocol would allow custom Swift error types to provide specific HRESULT values and IErrorInfo fields, rather than falling through to E_FAIL:

protocol COMErrorConvertible: Error {
    var hresult: HRESULT { get }
    var comErrorDescription: String? { get }
}

Bidirectional Collection Bridging

The core design bridges imported COM collections to Swift Sequence and Collection. The reverse direction, exposing a Swift Sequence as an IEnumXxx or a Swift Array to COM callers, requires synthesising the COM collection interfaces from Swift protocol conformances.

ABI Stability

There are two distinct ABI stability concerns:

The COM vtable ABI is a contract with non-Swift COM clients. COM clients only see the vtable from slot 0 onwards: they dereference the interface pointer to get a vtable pointer, index to the desired slot, and call. They never observe vtable[−1], the object layout, or the position of ISwiftObject. The vtable slot ordering (QueryInterface at 0, AddRef at 1, Release at 2, interface methods at 3+) is immutable once the interface is published. This is COM's own rule, not a Swift-specific constraint.

The Swift-internal ABI is a contract between the compiler, the COM module's shared thunks, and compiled Swift clients. This includes the object layout (COM block at negative offsets from P), the ISwiftObject position at P[-1], the vtable[-1] adjustment convention, and the conformance table format (sentinel-terminated IID array with positional indexing). Changes to any of these would break compiled Swift modules that depend on the shared QueryInterface, AddRef, and Release functions in the COM module. This internal ABI must be stable across Swift compiler versions and COM module versions from the first release that enables -enable-com-interop.

Summary

Swift's COM interoperability story is a layered system. A small set of compiler and runtime additions (the @COM attribute, object layout, ARC bridge, COM allocator, and executor model) provide the platform-independent foundation. The Clang importer elevates existing C/C++ COM APIs to idiomatic Swift. Windows-specific integration adds threading, activation, and error reporting.

Each layer is independently useful. Developers working with XPCOM, IOKit, or MiniCOM benefit from the core language additions and importer improvements. Windows COM server authors add the platform integration. WinRT developers benefit from a companion design that builds the Windows Runtime type system, async bridging, and collection conformances on top of the COM foundation described here.

COM is the binary interface; Swift is the language. The result is a Swift where consuming a COM interface, implementing a COM server, declaring new COM interfaces, or bridging a COM object into Swift's type system are all first-class, safe, bidirectional, and ergonomic operations, with full type fidelity across the language boundary.

COM Object Memory Layout

Full-featured @COM class exercising: two COM interfaces, ISwiftObject (implicit), ISupportErrorInfo (implicit), a non-COM protocol (CustomStringConvertible), and COMInterfaceResolver.

@COM(CLSID: "...", ThreadingModel: .both)
final class CImplementation: IInterface, IAccessible, CustomStringConvertible, COMInterfaceResolver {
    func render() throws { ... }
    var name: String { get throws { ... } }
    var role: Int32 { get throws { ... } }
    var description: String { ... }
    func resolve(_ iid: borrowing IID) -> UnsafeMutableRawPointer? { ... }
}

Object Instance and VTables

classDiagram
    direction LR

    class Instance["Object Instance (heap)"] {
        P[-3]: vtable ptr → IAccessible
        P[-2]: vtable ptr → IInterface
        P[-1]: vtable ptr → ISwiftObject
        P[ 0]: isa (type metadata)
        P[ 1]: reference count
        P[ 2]: stored properties …
    }

    class IInterface_VT["IInterface vtable (static)"] {
        [-1] adjustment: +2 × sizeof(ptr) bytes
        [ 0] IUnknown::QueryInterface
        [ 1] IUnknown::AddRef
        [ 2] IUnknown::Release
        [ 3] IInterface::render
        [ 4] IInterface::name
    }

    class IAccessible_VT["IAccessible vtable (static)"] {
        [-1] adjustment: +3 × sizeof(ptr) bytes
        [ 0] IUnknown::QueryInterface
        [ 1] IUnknown::AddRef
        [ 2] IUnknown::Release
        [ 3] IAccessible::role
    }

    class ISwiftObject_VT["ISwiftObject vtable (static)"] {
        [-1] adjustment: +1 × sizeof(ptr) bytes
        [ 0] IUnknown::QueryInterface
        [ 1] IUnknown::AddRef
        [ 2] IUnknown::Release
        [ 3] ISwiftObject::object
    }

    Instance --> IInterface_VT : P[-2]
    Instance --> IAccessible_VT : P[-3]
    Instance --> ISwiftObject_VT : P[-1]
Loading

Conformance Table

classDiagram
    class ConformanceTable["Conformance Table (static IID array)"] {
        [0] IInterface.IID     → primary, slot at P[-2]
        [1] IAccessible.IID    → slot at P[-3]
        [2] ISwiftObject.IID   → sentinel, slot at P[-1]
    }
Loading

Protocol Witness Tables

classDiagram
    direction LR

    class IInterface_PWT["PWT: CImplementation → IInterface"] {
        [-1] offset: 2 × sizeof(ptr) bytes (Swift → COM)
        [ 0] render witness thunk
        [ 1] name witness thunk
    }

    class IAccessible_PWT["PWT: CImplementation → IAccessible"] {
        [-1] offset: 3 × sizeof(ptr) bytes (Swift → COM)
        [ 0] role witness thunk
    }

    class CustomString_PWT["PWT: CImplementation → CustomStringConvertible"] {
        (no [-1], non-COM protocol)
        [ 0] description witness
    }

    note for IInterface_PWT "Each witness thunk:\n1. Reads P from existential\n2. Subtracts pwt[-1] words\n3. Gets COM interface pointer\n4. Dispatches through COM vtable"
    note for CustomString_PWT "Standard Swift dispatch\nNo COM vtable involved"
Loading

Dispatch Flows

flowchart LR
    subgraph COM_Dispatch["COM method dispatch: (any IInterface).render()"]
        E1[existential\nstores P] --> PWT1[PWT witness\nfor render]
        PWT1 --> SUB1["P[-pwt[-1]]\n= P[-2]"]
        SUB1 --> VT1["deref COM ptr\n→ vtable"]
        VT1 --> SLOT1["vtable[3]\n→ render()"]
    end

    subgraph QI_Cast["as? cast: any IInterface → any IAccessible"]
        E2[existential] --> QI["QueryInterface\n(IAccessible.IID)"]
        QI --> REC["recover P\nvia vtable[-1]"]
        REC --> SCAN["scan conformance\ntable → entry 1"]
        SCAN --> RET["return P[-3]\nwrap in existential"]
    end

    subgraph Concrete_Cast["as? cast: any IInterface → CImplementation"]
        E3[existential] --> QI2["QueryInterface\n(ISwiftObject.IID)"]
        QI2 --> ISO["ISwiftObject.object\nrecover P via vtable[-1]"]
        ISO --> TC["dynamic type check\non P → isa"]
    end
Loading
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment