Swift
C programming
interoperability
Apple development
bridging headers

How to call C from Swift?

Master System Design with Codemia

Enhance your system design skills with over 120 practice problems, detailed solutions, and hands-on exercises.

Introduction

Calling C from Swift is common when integrating existing native libraries or performance-critical functions. Apple tooling supports this directly, but reliable interop depends on correct header exposure, target membership, and memory-ownership rules. Most issues come from build configuration and pointer lifetime mistakes rather than syntax.

Basic Setup in Xcode

Add C source and header files to your target.

c
1// math_utils.h
2#ifndef MATH_UTILS_H
3#define MATH_UTILS_H
4
5int add_ints(int a, int b);
6
7#endif
c
1// math_utils.c
2#include "math_utils.h"
3
4int add_ints(int a, int b) {
5    return a + b;
6}

Then expose header through bridging header.

c
// MyApp-Bridging-Header.h
#import "math_utils.h"

Set Objective-C Bridging Header build setting to that file path.

Call C Function from Swift

Once configured, Swift can call C symbols directly.

swift
let result = add_ints(3, 4)
print(result)

No wrapper is required for simple scalar parameters.

Passing Strings Safely

C APIs often use const char*. In Swift, use withCString for temporary UTF-8 pointer access.

swift
1let name = "Ana"
2name.withCString { cstr in
3    // call C function expecting const char*
4    // greet_prefix(cstr)
5}

Do not store that pointer beyond closure lifetime unless C API explicitly copies data.

Working with C Structs

C structs are imported and can be used from Swift directly.

c
1typedef struct {
2    int x;
3    int y;
4} Point;
5
6int point_sum(Point p);
swift
var p = Point(x: 2, y: 5)
let s = point_sum(p)
print(s)

Interop is straightforward when layout and types are simple.

External C Libraries and Module Maps

For reusable libraries, module maps can expose headers without relying on app-specific bridging headers.

text
1module CLib {
2  header "clib.h"
3  export *
4}

This is especially useful in Swift Package Manager projects.

Memory Ownership Rules

The most important interop contract is ownership:

  • if C allocates, define who frees
  • if Swift passes pointer, define lifetime expectations
  • avoid hidden ownership assumptions

A good C API provides explicit free functions for allocated buffers.

c
char *make_message(void);
void free_message(char *p);

Document these rules in headers so Swift callers use APIs safely.

Build Troubleshooting Checklist

When Swift cannot see C functions, verify:

  1. C files are in correct target membership
  2. bridging header path is correct
  3. imported header compiles independently
  4. clean build after changing headers

Most unresolved symbol issues come from one of these configuration points.

Swift Package Manager Interop

In package-based codebases, define C target and expose public headers through package structure and module maps. This avoids Xcode-only assumptions and keeps interop portable across CI and command-line builds.

If your team uses both app target and package target, keep one source of truth for C headers to avoid divergence.

Testing Interop Boundaries

Add tests that exercise nullability, buffer lengths, and edge values.

swift
1import XCTest
2
3final class CLibInteropTests: XCTestCase {
4    func testAddInts() {
5        XCTAssertEqual(add_ints(2, 3), 5)
6    }
7}

Boundary tests catch ABI changes early when C signatures evolve.

ABI Stability and Header Discipline

Interop reliability improves when C headers are treated as stable ABI contracts. Avoid frequent signature churn, keep parameter types explicit, and version changes carefully so Swift call sites do not break unexpectedly across releases.

Bridging Header Hygiene

Keep bridging header imports minimal and focused. Large umbrella imports slow build times and can cause avoidable symbol collisions in mixed-language projects.

Common Pitfalls

  • Forgetting to set bridging header path.
  • Adding C files to project but not to build target.
  • Passing Swift string pointers outside valid lifetime scope.
  • Assuming ARC manages C-allocated memory automatically.
  • Changing C signatures without updating Swift call assumptions.

Summary

  • Swift can call C directly with bridging headers or module maps.
  • Correct build configuration is essential for symbol visibility.
  • Handle string and pointer lifetimes carefully.
  • Define explicit memory ownership contracts in C APIs.
  • Protect interop boundaries with focused unit tests.

Course illustration
Course illustration

All Rights Reserved.