Use clang API notes to Annotate C Header Files for Swifty Module Imports
I learned threee new things when I read Doug Gregor’s “Improving the usability of C libraries in Swift” blog post that may also affect your daily life importing C libraries:
- The clang compiler support YAML module annotations so you don’t have to own and modify
.hfiles; - The
SWIFT_NAMEannotation can be used to make free C functions act like methods on objects; - You can tell the compiler to generate object types for automatic reference counting instead of dealing with opaque pointers.
Until today, I was under the impression that you can make C functions sound a little bit nicer and add argument labels with SWIFT_NAME mostly, but this is something on a different level.
One Example to turn a free function into a method:[#20260123webgpu][]
WGPU_EXPORT void wgpuQueueWriteBuffer(
WGPUQueue queue, WGPUBuffer buffer, uint64_t bufferOffset, void const * data, size_t size)
WGPU_FUNCTION_ATTRIBUTE SWIFT_NAME("WGPUQueueImpl.writeBuffer(self:buffer:bufferOffset:data:size:)");
That’s what got me interested: object-oriented conventions from C, expressed in Swift’s notation! Where does the type come from? Usually, you just get opaque pointers.
The WGPUQueueImpl typealias is generated as well with a different annotation – that’s now becoming a reference-counting wrapper around WGPUQueue instead of an opaque pointer. The heavy lifting of this is done by a parameterized annotation, SWIFT_SHARED_REFERENCE(increment, decrement), to which you pass function names to increment and decrement the refcount.
The annotation, if you would apply it manually in the header, would look like this:
// Original typedef is:
// typedef struct WGPUQueueImpl* WGPUQueue WGPU_OBJECT_ATTRIBUTE;
typedef struct
SWIFT_SHARED_REFERENCE(wgpuGroupAddRef, wgpuGroupRelease)
WGPUQueueImpl* WGPUQueue WGPU_OBJECT_ATTRIBUTE;
… making the generated Swift interface:
// Original generated typealias would've been:
// public typealias WGPUGroup = OpaquePointer
public class WGPUGroupImpl { }
public typealias WGPUGroup = WGPUGroupImpl
Now the secret ingredient to make this bearable is to not copy the header files, but to use a YAML “sidecar” file to instruct clang to generate a different API. Combining the generated type declaration and the method example from above:
- Name: WGPUGroupImpl
SwiftImportAs: reference
SwiftReleaseOp: wgpuGroupRelease
SwiftRetainOp: wgpuGroupAddRef
- Name: wgpuQueueWriteBuffer
SwiftName: WGPUQueueImpl.writeBuffer(self:buffer:bufferOffset:data:size:)
clang supports API notes in a YAML file to annotate .h files without having to change the .h files themselves for module imports. That’s so neat: with that you can wrap C libraries in Swift without having to maintain a copy of the headers.
- API notes are found under the name “Foo.apinotes” for a module named “Foo”.
- Private module map API notes are picked up as well: “Foo_private.apinotes”
- Here’s an example file from the clang docs.
Clang will search for API notes files next to module maps only when passed the
-fapinotes-modulesoption.
Next to methods, properties also work with SWIFT_NAME annotations:
- Name: wgpuQuerySetGetCount
SwiftName: getter:WGPUQuerySetImpl.count(self:)
- Name: wgpuQuerySetGetType
SwiftName: getter:WGPUQuerySetImpl.type(self:)
Depending on the C API, this can be a game-changer in terms of Swift language ergonomics for you and your team. No more manual refcounting (which, if limited to e.g. a function scope, isn’t that bad thanks to defer, but can get hairy when you need to escape and keep references alive for longer), proper Swift types instead of opaque pointers, and methods and properties on these types instead of namespaced free functions. Lovely.