Swift's interoperability with C is powerful but often results in APIs that feel foreign and unsafe. Using API notes, developers can project a native Swift interface onto existing C libraries without modifying the original code, improving ergonomics and safety.

Swift’s direct interoperability with C is one of its most pragmatic features. It allows developers to tap into decades of existing, battle-tested libraries without rewriting them. However, this interoperability often comes at the cost of ergonomics and safety. The resulting Swift code frequently mirrors the C API’s structure—global functions, prefixed names, pervasive use of unsafe pointers, and manual memory management—making it feel out of place in a Swift codebase.
A recent post on Swift.org by Doug Gregor, a member of the Language Steering Group, demonstrates how to bridge this gap. Using the WebGPU C library as a case study, the post outlines a method to layer a native Swift interface over C code using a suite of annotations. The goal is to transform a C-style API into one that leverages Swift features like argument labels, methods, enums, and automatic reference counting, all without altering the original C implementation.
The Problem: A C Feel in Swift
The initial Swift code for using WebGPU looks very much like C. It involves global function calls with long, prefixed names (e.g., wgpuInstanceCreateSurface), global integer constants (e.g., WGPUStatus_Error), and pervasive use of unsafe pointers. Some of these pointers are managed with explicit reference counting, requiring the developer to manually call AddRef and Release functions. While functional, this approach inherits C's safety problems and lacks the clarity and safety of idiomatic Swift.
The Solution: API Notes and Module Maps
The core of the solution involves two key components: module maps and API notes.
Module Maps: A module map creates a Swift-friendly modular structure on top of C headers. By creating a
module.modulemapfile alongside the C header, you can define a module that Swift can import, providing a clean entry point for the C library.API Notes: This is where the real transformation happens. API notes are YAML files (
.apinotes) that allow you to attach additional semantic information to C headers without modifying them. This information guides Swift's importer, telling it how to project the C API into a more Swifty interface.
Transforming the WebGPU API
The post walks through several concrete examples of how API notes can improve a C library's usability in Swift.
Enums: C enums are often imported as wrappers over their underlying integer type, with enumerators becoming global constants. For enums that represent a distinct set of choices, you can use API notes to specify EnumExtensibility: closed. This results in a proper Swift enum with case statements, enabling the use of switch statements and improving type safety.
Reference-Counted Objects: Many C libraries manage object lifetimes manually. WebGPU uses AddRef and Release functions. By using the SWIFT_SHARED_REFERENCE macro (or its API notes equivalent), you can tell Swift to treat these C types as Swift classes. This enables automatic reference counting, eliminating manual memory management and the associated bugs. Functions that return new objects can be annotated with SWIFT_RETURNS_RETAINED (or SwiftReturnOwnership: retained in API notes) to ensure Swift handles the memory correctly.
Functions and Methods: Global C functions can feel disconnected. API notes allow you to:
- Add Argument Labels: Using
SWIFT_NAME(orSwiftNamein API notes), you can provide descriptive argument labels (e.g.,writeBuffer(buffer:bufferOffset:data:size:)), making function calls self-documenting. - Import as Methods: By naming the first parameter
selfin theSWIFT_NAMEannotation, you can transform a function that operates on an object (e.g.,wgpuQueueWriteBuffer) into a method on that object's Swift class (e.g.,myQueue.writeBuffer(...)). - Import as Properties: For "getter" functions (e.g.,
wgpuQuerySetGetCount), you can use thegetter:prefix in the Swift name to create a read-only computed property (e.g.,myQuerySet.count). - Import as Initializers: Functions that create new instances can be mapped to Swift initializers using
initas the method name in the Swift name annotation.
Other Type Improvements:
- Boolean Types: Custom C
typedeffor booleans (e.g.,WGPUBool) can be wrapped in a SwiftstructusingSwiftWrapper: structin API notes. With a small extension, this type can conform toExpressibleByBooleanLiteral, allowing the use oftrueandfalseliterals. - Option Sets: Flag types (e.g.,
WGPUBufferUsage) can also be wrapped in a struct and made to conform toSwift.OptionSet. This enables the familiar Swift syntax for combining flags:[.mapRead, .mapWrite]. - Nullability: The
WGPU_NULLABLEmacro indicates which pointers can beNULL. Using API notes, you can specify nullability for parameters and return types (Ofor optional,Nfor non-optional). This eliminates implicitly-unwrapped optionals (!) from the Swift interface, making the API safer and clearer.
Automating the Process
Manually creating API notes for a large library like WebGPU (which has over 6,400 lines in its header) is impractical. The post provides a Swift script that parses the webgpu.h header using regular expressions. The script identifies patterns (enums, object types, functions) and automatically generates the corresponding WebGPU.apinotes file. While a regex-based approach is a hack for a general C header, it works well for WebGPU's regular structure. For arbitrary libraries, a more robust approach would involve using libclang to parse the header properly.
Practical Application and Limitations
The techniques described are not limited to WebGPU. They can be applied to any C library to create a safer, more ergonomic Swift interface. The recommended approach is to set up a small Swift package to iterate quickly on example code.
However, the post also notes that some improvements could be made at the source. If C header authors adopt conventions that align with Swift's importer (e.g., placing attributes on the type itself rather than the typedef, or using standard nullability specifiers), the need for external API notes would be reduced, making interoperability even smoother.
By investing a bit of effort into creating API notes, developers can make existing C libraries feel native to Swift, combining the power of established C code with the safety and expressiveness of modern Swift.
Relevant Resources:

Comments
Please log in or register to join the discussion