The Complete Guide to Swift Interoperability With C

Swift was designed to be a practical language for Apple platforms, but it did not arrive in a vacuum. The system frameworks underneath iOS, macOS, watchOS, and tvOS are still largely C and Objective-C. When you call DispatchQueue, read a stat structure, or talk to a POSIX socket, you are crossing a language boundary. Swift interoperability with C is the set of rules, annotations, and conventions that make those crossings safe and predictable. It matters because production apps routinely depend on C libraries for cryptography, audio, networking, and hardware access. If you understand the boundary, you can use those libraries without leaking memory or corrupting data. If you do not, the compiler will happily let you write code that crashes six months later on a customer’s device.

This guide covers the mechanics: how Swift imports C headers, how types map, how pointers behave, how to call C from Swift and Swift from C, and where the sharp edges are. It assumes you are comfortable with Swift and have seen a C header or two. It does not assume you have memorized the C standard.

How Swift Sees C: The Clang Importer

Swift does not parse C headers itself. It uses the Clang importer, the same front end that powers Objective-C interoperability. When you add a C header to a bridging header or a module map, Clang reads the declarations and Swift translates them into Swift declarations. This translation is mostly mechanical, but the details matter.

For a simple function like int add(int a, int b);, Swift imports func add(_ a: Int32, _ b: Int32) -> Int32. The C int becomes Int32, not Int. That distinction is not academic. On a 64-bit Apple platform, Int is 64 bits and Int32 is 32 bits. Passing an Int where an Int32 is expected requires an explicit conversion, and the compiler will not do it for you.

Structures, enums, and typedefs are imported as Swift types with the same names. A C struct becomes a Swift struct with a memberwise initializer. A C enum becomes a Swift enum or a set of constants, depending on how it is declared. If the enum is marked with NS_ENUM or CF_ENUM, Swift imports it as a proper Swift enum with cases. Plain C enums are imported as raw integer constants, which is less pleasant but still usable.

One subtlety: C macros are not imported. The Clang importer handles simple constant macros in some cases, but function-like macros and complex expressions are invisible to Swift. If a C library exposes functionality only through macros, you will need a small C shim to wrap it in a real function.

Setting Up the Bridge

There are two common ways to expose C code to Swift: a bridging header for app targets, and a module map for frameworks and Swift packages.

Bridging Header

In an Xcode app target, you add a bridging header file and set the SWIFT_OBJC_BRIDGING_HEADER build setting to its path. The header typically contains #import or #include statements for the C headers you want to use. Everything visible in those headers becomes visible to Swift in that target.

Bridging headers are convenient but blunt. They expose everything, including private symbols if the header declares them. For a small app, that is fine. For a large codebase, it becomes a maintenance problem.

Module Map

A module map is a file named module.modulemap that tells Clang how to group headers into a module. Swift can then import that module by name. This is the approach used by Swift Package Manager and by frameworks that ship C code.

A minimal module map looks like this:

module MyCLibrary {
    header "include/MyCLibrary.h"
    export *
}

With that in place, Swift code can write import MyCLibrary and use the declarations. Module maps are more precise than bridging headers because you control exactly which headers are part of the module. They also support submodules, which let you expose a smaller public API and keep implementation details private.

If you are building a Swift package that wraps a C library, Swift Package Manager expects a directory named include with the public headers and a module map. The package manifest declares the target as a .target with sources in a C directory, and Swift can depend on it directly.

Type Mapping: What Becomes What

The Clang importer maps C types to Swift types according to a fixed table. Knowing the table prevents a class of bugs that are hard to debug.

  • char becomes CChar, which is a typealias for Int8.
  • unsigned char becomes CUnsignedChar, a typealias for UInt8.
  • short becomes CShort (Int16), int becomes CInt (Int32), and long becomes CLong (64-bit on Apple platforms).
  • float becomes Float, double becomes Double.
  • bool from stdbool.h becomes Bool.
  • size_t becomes Int on 64-bit platforms, because Swift imports it as a signed integer of the same width.
  • void * becomes UnsafeMutableRawPointer?.
  • const void * becomes UnsafeRawPointer?.

The pointer mappings are where most confusion lives. Swift does not have implicit pointer conversion. You cannot pass an Int where a void * is expected, and you cannot pass a Swift array where a C pointer is expected without an explicit bridge.

Pointers Without Fear

Swift’s pointer types are verbose but consistent. The main ones are UnsafePointer<T>, UnsafeMutablePointer<T>, UnsafeRawPointer, and UnsafeMutableRawPointer. The names are honest: they are unsafe, and the compiler will not protect you from mistakes.

When a C function takes a pointer to a single value, you pass it with & if the parameter is inout in Swift. For example, a C function void get_value(int *out); imports as func get_value(_ out: UnsafeMutablePointer<Int32>!). You call it like this:

var value: Int32 = 0
get_value(&value)

When a C function takes an array pointer, you have options. If the function does not store the pointer beyond the call, you can use withUnsafeBufferPointer or withUnsafeMutableBufferPointer on a Swift array. If the function stores the pointer, you must allocate memory manually with UnsafeMutablePointer<T>.allocate(capacity:) and free it with deallocate().

String handling is a common source of leaks. A C function that returns char * may return a pointer that you own and must free, or a pointer to static memory that you must not free. The header documentation is the only reliable guide. Swift imports char * as UnsafeMutablePointer<CChar>!, and you can convert it to a Swift String with String(cString:). That initializer copies the bytes, so the original pointer can be freed afterward if you own it.

For passing a Swift string to C, use withCString or String.withCString. The closure receives a pointer valid only for the duration of the closure. Do not store it.

Calling C From Swift: A Practical Example

Suppose you have a C library that computes a checksum. The header declares:

uint32_t checksum(const uint8_t *data, size_t length);

Swift imports this as func checksum(_ data: UnsafePointer<UInt8>!, _ length: Int) -> UInt32. To call it with a Swift Data value:

let data = Data([0x01, 0x02, 0x03])
let result = data.withUnsafeBytes { buffer in
    checksum(buffer.bindMemory(to: UInt8.self).baseAddress, data.count)
}

The withUnsafeBytes closure gives you a raw buffer. Binding it to UInt8 and taking the base address produces the pointer the C function expects. The pointer is valid only inside the closure. If the C function stores it, you have a bug.

This pattern is the workhorse of C interoperability. It is safe as long as the C function treats the pointer as borrowed. If the function takes ownership, you need a different approach.

Calling Swift From C

The reverse direction is less common but sometimes necessary. You might have a C callback that needs to call back into Swift, or a C library that expects a function pointer.

Swift functions can be passed to C as function pointers if they are not capturing closures. A global function or a closure with no captured context can be converted to a C function pointer using @convention(c). For example:

let callback: @convention(c) (Int32) -> Void = { value in
    print(value)
}

You can then pass callback to a C function that expects void (*)(int). The catch is that the closure cannot capture any context. If you need context, you must pass it through a void * user data parameter and cast it back inside the callback. That cast is unsafe and requires careful lifetime management.

For exposing Swift code to C more broadly, you can use @_cdecl to give a Swift function a C-compatible symbol name. This attribute is underscored, meaning it is not officially stable API, but it is widely used in production. A function like this:

@_cdecl("swift_add")
public func swiftAdd(_ a: Int32, _ b: Int32) -> Int32 {
    return a + b
}

can be declared in a C header as int32_t swift_add(int32_t a, int32_t b); and called from C. The types must match exactly. Swift’s Int32 maps to C’s int32_t, and the calling convention is the platform’s C convention.

Memory Management Across the Boundary

Swift uses automatic reference counting for its own objects. C uses manual memory management. At the boundary, you must decide who owns what.

When Swift passes a pointer to C, the pointer is borrowed unless you explicitly transfer ownership. When C returns a pointer to Swift, the ownership depends on the C API’s contract. Functions named create or copy typically return owned pointers that you must free. Functions named get typically return borrowed pointers.

If you allocate memory in Swift with UnsafeMutablePointer.allocate, you must deallocate it. If you allocate with malloc, you must free it with free. Mixing allocators is undefined behavior. This is not a theoretical concern; it is a common source of crashes in code that wraps C libraries.

For C structures that contain pointers, Swift imports them as structs with pointer fields. You can read and write those fields, but you are responsible for the memory they point to. Swift will not manage it.

Common Pitfalls and How to Avoid Them

Integer Width Mismatches

Swift’s Int is 64 bits on Apple platforms. C’s int is 32 bits. If you pass an Int to a C function expecting int, the compiler will complain, but if you force a conversion with Int32(someInt), you may truncate the value. Always check the range or use the exact type.

String Encoding

C strings are null-terminated byte sequences. Swift strings are Unicode. Converting between them can lose information if the string contains characters that cannot be represented in the C string’s encoding. String(cString:) assumes UTF-8 by default, which is usually correct on Apple platforms, but not always.

Pointer Lifetime

The pointer you get from withUnsafeBytes or withCString is valid only inside the closure. If you pass it to a C function that stores it, you have a dangling pointer. This is the single most common bug in C interoperability.

Thread Safety

C libraries often have their own threading assumptions. Swift’s concurrency model does not automatically make a C library thread-safe. If you call a C function from multiple threads, you need to know whether it is reentrant. The header documentation is the only source of truth.

Tools and Workflow

Xcode’s Clang importer is the primary tool. When a C declaration does not import as expected, you can use swiftc -dump-ast or Xcode’s generated interface to see how Swift sees it. The generated interface is available in the assistant editor under “Generated Interface.”

For Swift packages, swift build will report module map errors. A common issue is a missing export * or a header that includes another header outside the module. Keeping the module map minimal and the headers self-contained avoids most problems.

The Swift forums and the Clang documentation are useful references. The Clang module documentation explains module maps in detail. Apple’s documentation on imported C APIs covers the type mapping rules. For a deeper look at Swift’s memory model, the Swift documentation is the canonical source.

When to Use C Interoperability

Not every project needs C. If a pure Swift library exists, use it. C interoperability adds complexity and risk. But when you need a specific C library, or when you are working with system APIs that are only exposed in C, the techniques in this guide are the difference between a working integration and a crash report.

The key is to isolate the boundary. Wrap C calls in a small Swift layer that handles pointer conversion, memory management, and error checking. Keep the unsafe code in one place, and test it thoroughly. The rest of your app can then use a safe Swift API.

FAQ

Can I use C++ from Swift?

Swift does not import C++ directly. You can write a C or Objective-C wrapper around the C++ code and import that. The wrapper exposes a C-compatible API, and Swift calls it. This is the standard approach for using C++ libraries in Swift projects.

Why does Swift import size_t as Int?

On 64-bit Apple platforms, size_t is 64 bits wide. Swift imports it as Int because Int is also 64 bits and signed, which matches the platform’s pointer width. This is a deliberate choice to avoid unsigned arithmetic surprises in Swift code.

How do I pass a Swift array to a C function that takes a pointer?

Use withUnsafeBufferPointer for a read-only pointer or withUnsafeMutableBufferPointer for a mutable one. The pointer is valid only inside the closure. If the C function stores the pointer, allocate memory manually instead.

What is @_cdecl and is it safe to use?

@_cdecl is an underscored attribute that gives a Swift function a C-compatible symbol name. It is not officially stable, but it is widely used and has been available for years. If you need to call Swift from C, it is the practical option. Be aware that the underscore signals that the Swift team reserves the right to change it.

How do I debug a crash in C code called from Swift?

Use the same tools you would use for any C crash: lldb, Address Sanitizer, and the crash log. The stack trace will show the C frames and the Swift frames. If the crash is in C code, the bug is likely in how you passed pointers or managed memory. Check the pointer lifetime and the ownership contract first.