Swift 6.3’s @c Attribute: Replacing @_cdecl and Making C Interop a First-Class Citizen
For a decade, exposing Swift code to C required an underscore-prefixed, unsupported hack — Swift 6.3 finally makes it official.
Background
Swift has always been able to call C code via bridging headers and module maps. The reverse — calling Swift from C — was technically possible but relied on @_cdecl, an attribute with a leading underscore that explicitly signals “this is not a stable API, use at your own risk.” It had real limitations: no enum support, inconsistent nullability handling, and a nasty class of compiler errors caused by mismatches between Swift types and their C counterparts. These would surface as cryptic “deserialization failure” messages rather than actionable diagnostics.
This mattered most in two scenarios: mixed-language codebases where a legacy C library needed to call into a newer Swift module, and Embedded Swift, where you’re often implementing a C ABI directly (interrupt handlers, hardware init routines, linker-section-placed functions). In both cases, @_cdecl worked well enough to ship, but the lack of official support meant no guarantees between Swift versions.
Swift 6.3 replaces @_cdecl with @c — a stable, officially supported attribute that formalises the C-calling-convention export and adds new capabilities on top.
How It Works
When you annotate a Swift function with @c, the compiler does two things: it generates a C-compatible symbol using the C calling convention (no Swift mangling, no runtime metadata), and it automatically emits a corresponding declaration into the generated C header (-emit-objc-header or the new Swift-generated header). That header can then be #included directly from your C or C++ files — no manual header maintenance required.
The attribute accepts an optional name parameter: @c("MyLib_initialize") lets you namespace the exported symbol without renaming the Swift function itself. This is important for C, where there are no namespaces and symbol collisions are a real risk.
The companion feature is @c @implementation, which flips the direction. If you have an existing C header declaring a function, you can write the implementation in Swift and the compiler will verify that the Swift signature matches the C declaration — catching type mismatches at compile time rather than at link time or, worse, at runtime.
Internally, @c fixes the type consistency issues that plagued @_cdecl. Previously, if a C header declared a function without nullability annotations (char * instead of char * _Nonnull), the Swift compiler would sometimes fail to reconcile its own imported type with the @_cdecl declaration, producing an unhelpful deserialization error. @c normalises this: the compiler is now explicit about the mapping rules and reports real diagnostics when something doesn’t match.
Code
Before (Swift 6.2 and earlier) — exporting Swift to C:
// Unofficial, no compiler validation of C header consistency
@_cdecl("MyLib_initialize")
public func initialize() {
AppState.shared.bootstrap()
}
You’d then have to manually write and maintain the C header:
// MyLib.h — written and maintained by hand
extern void MyLib_initialize(void);
After (Swift 6.3) — @c with auto-generated header:
// Compiler generates the C header declaration automatically
@c("MyLib_initialize")
public func initialize() {
AppState.shared.bootstrap()
}
Implementing an existing C interface in Swift (@c @implementation):
// existing_header.h — the C contract you must satisfy
void MyLib_processBuffer(const uint8_t *buf, size_t len);
// Swift implementation — compiler validates the signature matches
@c @implementation
public func MyLib_processBuffer(_ buf: UnsafePointer<UInt8>, _ len: Int) {
// Replace your C implementation one function at a time
Buffer(buf, count: len).process()
}
If the Swift types don’t match the C declaration, you get a compile-time error with a clear message — not a linker failure or a runtime crash.
Embedded Swift — @section and @used for linker control:
// Place an interrupt handler at a specific linker section
@c @section(".isr_vector")
@used // prevent dead-code elimination
public func HardFault_Handler() {
// handle fault
}
Trade-offs & Limitations
@c only supports C-compatible types — no Swift generics, no existentials, no classes with Swift metadata. If your function signature uses anything that doesn’t have a direct C representation, the compiler will reject it. This is intentional and correct, but it means you still need a thin adapter layer if you want to expose complex Swift APIs. Enums are now supported (a @_cdecl limitation that @c fixes), but only C-compatible enums with a raw integer type — Swift enums with associated values are out. The auto-generated header also depends on your build configuration; in some SPM setups you’ll need to explicitly pass -emit-objc-header or configure the header output path, which isn’t fully streamlined yet.
My Take
@c is exactly the kind of housekeeping Swift needed. The @_cdecl situation was embarrassing for a systems-capable language — you couldn’t officially export a Swift function to C without relying on an undocumented attribute. The new attribute does what it should: compiler-validated signatures, auto-generated headers, and proper linker control for embedded targets. For anyone building mixed Swift/C codebases or working on Embedded Swift firmware, this removes a real category of integration bugs. The @c @implementation pattern in particular is underrated — it lets you migrate a C library to Swift incrementally, one function at a time, with the compiler acting as a contract enforcer at every step.
Tags: Swift, Swift 6.3, C Interoperability, Embedded Swift
