Skip to content

API Notes: Improving Swift Imports Without Touching C Headers

Published: 4 min read
Edit on GitHub

When you import a C library into Swift, the results can be… ugly. Function names like MYLCreatePoint and MYLPrintPointWithLabel don’t feel Swift-y at all. Normally, you’d fix this by adding attributes to the C headers. But what if you can’t modify those headers?

That’s where API Notes come in.

The Problem

Imagine you have a C library with this header:

typedef struct {
    int x;
    int y;
} MYLPoint;

MYLPoint* MYLCreatePoint(int x, int y);
void MYLDestroyPoint(MYLPoint* point);
void MYLPrintPointWithLabel(MYLPoint* point, const char* label);

When Swift imports this, you get:

let point = MYLCreatePoint(10, 20)
MYLPrintPointWithLabel(point, "My Point")
MYLDestroyPoint(point)

It works, but it doesn’t feel like Swift. We want labeled arguments, cleaner names, and proper nullability annotations. The traditional fix is adding attributes like NS_SWIFT_NAME to the header - but you might not own that header, or you want to keep it clean for other consumers.

The Solution: Sidecar Files

API Notes is a Clang feature that lets you provide annotations in a separate YAML file. The compiler picks it up automatically and applies the changes during import.

Create a file named MyCLib.apinotes (matching your module name) next to your module map:

Name: MyCLib

Tags:
- Name: MYLPoint
  SwiftName: Point

Functions:
- Name: MYLCreatePoint
  SwiftName: "createPoint(x:y:)"
  NullabilityOfRet: N

- Name: MYLDestroyPoint
  SwiftName: "destroyPoint(_:)"

- Name: MYLPrintPointWithLabel
  SwiftName: "printPoint(_:label:)"

Now Swift sees:

let point = createPoint(x: 10, y: 20)
printPoint(point, label: "My Point")
destroyPoint(point)

Much better! The C header remains untouched.

How It Works

The magic happens at compile time. When you pass -fapinotes-modules to Clang (via -Xcc -fapinotes-modules in swiftc), the compiler looks for .apinotes files next to your module maps.

File placement:

Here’s the project structure for a bare module:

MyCLib/
├── mylib.h           # Original C header (unchanged)
├── mylib.c           # Implementation
├── module.modulemap  # Required for modular imports
└── MyCLib.apinotes   # Your annotations go here

The module map is simple:

module MyCLib {
    header "mylib.h"
    export *
}

Build with:

clang -c MyCLib/mylib.c -o mylib.o
swiftc main.swift mylib.o -I MyCLib -Xcc -fapinotes-modules -o app

What Can You Annotate?

API Notes support many annotations:

AnnotationPurpose
SwiftNameRename types, functions, methods
NullabilityMark pointers as Nonnull, Optional, etc.
AvailabilityMark APIs unavailable in Swift
SwiftPrivateHide from public API (like NS_REFINED_FOR_SWIFT)
SwiftImportAsControl how C++ classes import
EnumKindMake enums import as NS_ENUM or NS_OPTIONS

For Objective-C classes, you can also annotate methods and properties:

Classes:
- Name: UIViewController
  Methods:
  - Selector: "presentViewController:animated:"
    MethodKind: Instance
    SwiftName: "present(_:animated:)"

Version-Specific Annotations

Need different behavior for different Swift versions? API Notes support versioning:

SwiftVersions:
- Version: 4
  Functions:
  - Name: oldFunction
    SwiftName: "legacyFunction()"

Versioned annotations apply to that version and all earlier versions, letting you maintain backwards compatibility while improving the API for newer Swift.

When to Use API Notes

API Notes shine when:

Limitations

A few things to keep in mind:

Try It Yourself

I’ve put together a minimal working example on GitHub: apinotes-demo

Clone it, build it, and experiment with the .apinotes file to see how changes affect the Swift interface.

Reference

API Notes is one of those features that’s been quietly powering Swift’s excellent Objective-C interop for years. Now you can use it for your own C libraries too.