Pitch: A vision for COM Interoperability in Swift

I think there is an alternative we should consider for supporting COM that would reduce the scope of language changes needed.

Today, COM is predominantly used for consuming Windows APIs, while using custom COM interfaces for arbitrary cross-language interop sees very little usage. While C# has excellent COM interop capabilities, .NET assemblies are the standard for modern Windows development. There is no real modern ecosystem for third-party COM components that would be useful to Swift, so supporting this in the language would be trying to start adoption of this use case. I think there are two problems with this:

  1. Memory safety is a strict prerequisite for modern cross-language tooling, and COM is not capable of expressing these guarantees.

  2. C# and C++ are the current established languages for Windows app development, and there are better solutions if we want Swift to interoperate with them. For C#, we could make a bindings generator similar to Swift-Java. For C++, we can use Swift's C++ interoperability. This would be more idiomatic and safe for both sides of the language boundary and have better performance.

I think the cross-platform story for COM also has very low motivation:

  • Mozilla deprecated and removed support for addons based on XPCOM and XUL with the release of Firefox Quantum in 2017. Firefox extensions now use the standardised WebExtensions API. Today, XPCOM is strictly an internal implementation detail
    of the Gecko engine which Mozilla has been working to move away from, in an effort known as "DeCOMtamination" (10/10 pun).

  • CFPlugIn on Apple platforms is largely considered legacy. Out-of-process app extensions are the modern standard and IOKit has been superseded by user-space frameworks like DriverKit and IOUSBHost.

  • MiniCOM is a highly niche project and I do not think it justifies Swift Evolution.

For these reasons, I think Swift does not need as deep interoperability for COM as it does for Objective-C, and there is a simpler way to support the primary use case of consuming Windows APIs.

Microsoft's solution for idiomatic bindings to Windows APIs in C#, C++ and Rust is 'projection' libraries generated from Windows Metadata (which is provided for not only WinRT APIs, but also C and COM APIs via the win32metadata project). For those unfamiliar, I have been working on such a projection for Swift called Windows Kit. My plan to support COM is to use Swift's C++ interop since on Windows it is based on the MSVC C++ ABI, and I've made a demo called COMTest which demonstrates this is possible. This approach supports most of the goals of language-level COM interoperability, including:

  • Calls to AddRef and Release being generated by the compiler via foreign reference counting
  • Safe QueryInterface and CoClass construction with interface type parameters instead of interface IDs
  • Bridging HRESULT to throws(COMError)
  • Renaming APIs to match Swift conventions

The COMTest demo is handwritten to illustrate what code generated for a projection could look like supporting these features. The fact that it was practical to handwrite shows the code generation requirements are fairly simple.

The main limitation currently is there is no way to implement a COM interface in Swift, which is required for using some Windows APIs. This could be solved by supporting inheritance of C++ classes in Swift, which would also be useful for C++ interop in general on every platform. C++ classes only containing pure virtual methods (which is how COM interfaces are defined in both the Windows headers and my headers) could be imported as protocols (either by default or with a Clang attribute) to allow implementation of multiple on the same type.

If that were solved, there would be another limitation: a COM interface in C++ can only use features that are representable in C++ and Clang attributes, which prevents some API refinements that are possible with the information provided by win32metadata. This limitation also applies to language-level COM interop, as the design notes say COM interface definitions in Swift reject requirements that do not have a stable COM ABI. One solution is to use an extension to implement the raw requirements from the user's implementation of the idiomatic requirements, but this risks infinite mutual recursion because an extension would also be needed to implement the idiomatic members from the raw members implemented by COM CoClasses from Windows APIs. Another option would be to wrap the C++ interface types in a Swift wrapper struct for Windows CoClasses instead of an extension, though this means we would have two types for each interface. I have a few alternative ideas to avoid this that require language changes:

  • The language could have some way of bidirectional refinement of C++ types for both consumption and implementation of virtual methods without the risk of mutual recursion, which might be useful for general C++ interop as well.
  • The language could add a C++ equivalent to @_cdecl so that projection generators do not need to generate C++ headers and the language boundary is smaller. Support for foreign reference counting and bidirectional refinement would be placed there.
  • Foreign reference counting could be exposed to the Swift side so a macro can insert COM ABI details into user classes, which is how implementation of COM interfaces for Rust structs works in windows-rs. There would be a separate IExampleImpl protocol for implementing IExample in Swift to prevent mutual recursion.

The common thread in these ideas is providing primitives for projection generators to express the COM ABI rather than supporting COM directly in the compiler, and that is primarily what I am pitching here. Having C++ interop be the substrate also makes any required language changes generalisable and highly relevant across platforms. And there may be more of the solution space I have not considered.

A projection generator based on Windows Metadata is required for achieving idiomatic Swift bindings because headers and MIDL (when available) are missing a lot of important information that cannot be inferred (including [PreserveSig]), as I talked about in another thread. Metadata handling in the compiler has been raised as a possibility, but that would be a large responsibility to add to the compiler. It would also expand the scope to include projection of C and WinRT APIs like Windows Kit does, or else they could not match the privilege of COM in compiler-space. So I think delegating as much as possible to user-space libraries while achieving our goals would be ideal. It would align with how Microsoft handled interop for C#, C++ and Rust.

There are some COM APIs not covered by Windows Metadata in the Windows App SDK, which we cannot achieve automatic, idiomatic bindings for. But once a Swift class can inherit from a C++ class, it should be possible to scrape the headers and apply foreign reference counting to achieve a "best effort" projection.

Finally, there are a few more limitations but I think they are manageable:

  • QueryInterface is a method with signature queryInterface<T>(...) throws(COMError) -> T instead of being mapped to the as? operator. As I mentioned in my previous message here, I think this is better because it preserves the HRESULT and avoids overloading a dynamic type check operator with calls to a method with an arbitrary implementation for a user-defined type. Even if most of the time try? is used to get an optional binding, it is still good to preserve that information.

  • Because my headers redefine Windows API types, they are nominally incompatible with those from the WinSDK module. This could be addressed with an attribute like @_originallyDefinedIn, or just ignored if it is not important. Importing both also fails the one definition rule for the C++ translation unit, causing a compiler error. This can be fixed with unique name prefixes (hidden from Swift with the SWIFT_NAME Clang attribute) or C++ namespaces, but ideally my C++ module would be treated separately from the WinSDK module by the build system. I don't think API redefinition is avoidable given the API refinements and organisation into modules that I want to do. The completely flat global namespace and macro abuse of the original Windows headers makes them hostile to modularity which API Notes cannot fix.

Whether we go with this idea or the original pitch or something in between, I hope this serves as a useful exploration of exactly where the limitations of current language features are and the needs of projection generators.

5 Likes