With Swift 6.3 or later, mark a global function with @c, generate its compatibility header, compile the caller as C, and link with swiftc. This walkthrough builds a small native Mac command-line program that demonstrates each boundary.

The reader task is specific: call a Swift calculation from an ordinary .c file without compiling that file as Objective-C or C++. Swift’s official release documentation identifies @c as the supported C interoperability feature introduced in Swift 6.3. Check your installed compiler before following older advice about experimental attributes. Swift 6.3 Released

What makes a Swift function callable from C?

Three pieces must agree: the declaration the C compiler reads, the calling convention used by the implementation, and the object files supplied to the linker. A header addresses the first piece; it cannot supply a missing implementation or change an existing function’s ABI.

The @c attribute gives a global Swift function the C calling convention and restricts its signature to C-representable types. An optional attribute argument selects its C name. Swift emits the corresponding declaration in the C-compatible portion of its generated compatibility header. These behaviors are specified in the implemented Swift Evolution proposal. C compatible functions and enums

For this example, the interface accepts and returns a C integer. The synthetic calculation clamps a display percentage to the inclusive range from zero through 100. There are no buffers, allocated results, or object lifetimes to coordinate. That keeps the first build focused on compilation and linking.

Check the Mac toolchain first

You need a macOS development environment with Clang, a macOS SDK, and Swift 6.3 or later. Apple supplies command-line tools with Xcode and also offers a separate Command Line Tools package. If you already have Xcode, Apple says a separate tools installation is unnecessary. Installing the command-line tools

Run these checks in Terminal:

xcode-select --print-path
xcrun --sdk macosx swiftc --version
xcrun --sdk macosx clang --version

Inspect the Swift compiler version itself. An older compiler does not gain the @c feature merely because a newer SDK is installed. If several Xcode versions are present, Apple documents selecting the active version in Xcode > Settings > Locations > Command Line Tools, or overriding it for a shell session with DEVELOPER_DIR. Configuring command-line tools settings

The commands below use xcrun to invoke the selected development tools. Apple documents this tool-discovery role in its command-line build reference. Building from the Command Line with Xcode FAQ

Create an empty working directory and save both source files below inside it. Run the build commands from that directory. This is a native build for the local Mac; it does not create a universal binary or configure another platform.

Write the Swift implementation and the C caller

Define the exported Swift function

Save this complete Swift source file as Percent.swift:

@c(demo_clamp_percent)
public func clampPercent(_ value: CInt) -> CInt {
    if value < 0 {
        return 0
    }
    if value > 100 {
        return 100
    }
    return value
}

The Swift name is clampPercent; the C-facing name is demo_clamp_percent. CInt maps to C’s int. Using that explicit pairing avoids assuming Swift Int means C int: the documented mapping for Swift Int is ptrdiff_t. C compatible functions and enums

The function compares values and returns either an input or a constant. Its calculation introduces no arithmetic overflow, including for inputs outside the percentage range. The prefix on the exported name is an example convention for distinguishing this interface from other functions in a larger program.

Call it from a complete C program

Save this file as main.c. The build will create the included header:

#include <stddef.h>
#include <stdio.h>
#include "Percent-Swift.h"

int main(void) {
    const int inputs[] = {-12, 42, 140};
    const int expected[] = {0, 42, 100};
    const size_t count = sizeof inputs / sizeof inputs[0];

    for (size_t i = 0; i < count; ++i) {
        const int actual = demo_clamp_percent(inputs[i]);
        printf("%d -> %d\n", inputs[i], actual);
        if (actual != expected[i]) {
            fprintf(stderr, "Unexpected result at index %zu\n", i);
            return 1;
        }
    }
    return 0;
}

The three inputs exercise the lower bound, an unchanged value, and the upper bound. The program checks results explicitly and returns a nonzero status on a mismatch, so its output is more useful than a demonstration that merely prints a number.

Generate the header, compile C, and link the program

Run this sequence in a shell from the directory containing the two files. Each build step stops the sequence if it fails:

(
    set -eu
    SDK_PATH="$(xcrun --sdk macosx --show-sdk-path)"

    xcrun --sdk macosx swiftc \
        -sdk "$SDK_PATH" \
        -parse-as-library \
        -module-name Percent \
        -emit-object Percent.swift \
        -emit-objc-header-path Percent-Swift.h \
        -o Percent.o

    xcrun --sdk macosx clang \
        -isysroot "$SDK_PATH" \
        -x c -std=c11 \
        -Wall -Wextra -Werror \
        -c main.c -o main.o

    xcrun --sdk macosx swiftc \
        -sdk "$SDK_PATH" \
        main.o Percent.o \
        -o percent-demo

    ./percent-demo
)

The first command produces the Swift object and compatibility header. Swift’s driver defines -emit-object as object-file output and -parse-as-library as parsing library source rather than a script. This leaves the executable entry point to the C file. Swift compiler driver options

Despite its historical name, -emit-objc-header-path also requests the compatibility header’s C section for @c declarations. The implemented proposal explicitly preserves that flag for this purpose. Do not manually replace the generated header with an assumed prototype. C compatible functions and enums

The second command is the decisive source-language check. Clang documents -x as selecting the input language and -c as producing an object file without linking. Here, -x c -std=c11 makes the caller compile as C11 while reading the actual generated header. Clang command guide

The final compiler invocation links both object files using the Swift driver. Swift’s own compiler guide recommends asking swiftc to link files rather than manually assembling a lower-level link command. Debugging the Compiler

Expected output, derived from the example’s branches, is:

-12 -> 0
42 -> 42
140 -> 100

These commands were inspected against the retrieved documentation but were not executed here: the research environment is Linux and has neither swiftc nor Clang available. The output above is an expected result, not a reported Mac test run.

Choose the interface that matches your existing code

For a new, small interface on Swift 6.3 or later, the generated-header approach above keeps the declaration tied directly to the implementation. The release documentation also supports a second direction: @c @implementation implements a function already declared in an imported C header, with Swift checking that the signatures match. That is useful when an existing C API must remain the interface contract. Swift 6.3 Released

If the surrounding project already calls Swift through C++, Swift’s generated C++ bindings offer another route. Public Swift functions are represented by inline C++ functions inside a module namespace. A C caller would need a separate adapter compiled as C++, exposing a small C-facing declaration. The C translation unit would include that adapter’s C header. Mixing Swift and C++

In such an adapter, extern "C" specifies C language linkage on the C++ declaration or definition. It does not convert the contents of a C++ header into C syntax. The C++ working draft defines linkage specifications and distinguishes language linkage from ordinary declarations. C++ working draft: Linkage specifications

For this tutorial, upgrading an older compiler to one supporting @c is the direct supported path. The proposal describes @_cdecl as its experimental predecessor; copying that older spelling into a new example would hide a meaningful version boundary. C compatible functions and enums

Diagnose failures at the boundary where they occur

Use the first failing build step to narrow the investigation. A header-generation failure, C parser error, and unresolved symbol require different corrections.

First failure Next check
Swift rejects @c Recheck the selected Swift compiler version; this example requires Swift 6.3 or later.
C cannot find Percent-Swift.h Confirm the Swift command succeeded and both files are being built from the intended directory.
C cannot find demo_clamp_percent Inspect the newly generated header and check the attribute spelling; rebuild after source changes.
C reports C++ or Objective-C syntax Verify which header it included and which toolchain generated it. Preserve explicit C compilation while investigating.
Linking cannot resolve the exported function Confirm Percent.o is included in the final command and was rebuilt from the current Swift source.
The executable reports unexpected results Read the printed input/result pair and compare it with the Swift branches.

This sequence follows the compiler stages documented by Clang: parsing establishes whether the source is valid, object generation produces compiled code, and linking combines the compiled inputs. A successful C compilation alone therefore establishes only part of the task. Clang command guide

When adapting the example, keep the C-facing function small and give every new parameter an explicit contract. For text, decide encoding and length handling; for buffers, decide capacity and ownership before writing the wrapper. Those are interface design decisions beyond this integer-only demonstration.

The finished local check has three observable results: the generated header is accepted during explicit C compilation, both implementations link into one executable, and the caller verifies the chosen inputs. Preserve those checks when replacing the synthetic calculation with your own Swift function.

Sources