[Pitch] @objc on global functions: ABI clarification for SE-0495
SE-0495 specifies that @objc on a global function should behave like @c but with Objective-C types allowed in the signature. I have an implementation up at swiftlang/swift#90089 with all CI checks passing. Before this merges I want to surface an ABI detail that wasn't fully specified in the proposal.
The issue
SE-0495 states:
The compiler emits a single symbol for
@cand@objcfunctions.
and:
Changing between
@cand@objcis ABI stable.
This works for @c because @c restricts the signature to C-representable types. The function body operates on C types directly, so one C symbol is sufficient.
@objc allows Swift types that bridge to Objective-C types: String, Array, Dictionary, Set, @objc classes, @objc protocols, etc. These types need bridging code emitted somewhere. A function declared as:
@objc func greet(_ name: String) -> String { ... }
must accept NSString * from Objective-C callers and return NSString *, but Swift callers should pass and receive String natively.
What the implementation does
The implementation follows the existing @_cdecl code path, which emits two symbols:
- A Swift-native symbol (mangled) for the function body, operating on Swift types.
- A C-named thunk (native-to-foreign thunk) that accepts Objective-C types, bridges them to Swift types, calls the body, and bridges the result back.
This is the same mechanism @_cdecl has used since its introduction. The @c single-symbol model was introduced later by @xymus in 4be74c714fc (October 2025), which added hasOnlyCEntryPoint to distinguish @c (single C body) from @_cdecl (Swift body + C thunk).
The @objc global function uses getCDeclKind() == ForeignLanguage::ObjectiveC to follow the @_cdecl path rather than the @c path. The thunk is what enables the type bridging cleanly and in one place.
Why two symbols
A single C-body symbol would require the function to operate on Objective-C types in the implementation. Every Swift call site would then need to emit inline bridging code -- convert arguments before the call, convert results after. This means:
- Bridging scattered across call sites instead of centralized in one thunk.
- SIL type mismatch -- the function's SIL type would use bridged ObjC types rather than native Swift types, possibly losing type information at the Swift level if they can't be represented by ObjC's lightweight generics.
- No code reuse -- the existing
emitNativeToForeignThunkinfrastructure handles all the bridging correctly today. The single-symbol approach would need new code to emit bridging at call sites, or do bridging in the prologue and epilogue of the C symbol to handle native Swift types in the implementation.
The two-symbol approach keeps all bridging in one place (the thunk), lets Swift callers work with native types without bridging, and reuses the @_cdecl infrastructure with zero new code paths for type conversion.
ABI implications
From the C caller's perspective, the ABI is identical regardless of implementation strategy: a C function with the declared name, C calling convention, and Objective-C parameter/return types. The C-side interface is the same.
The difference is on the Swift side. @c emits a single symbol that IS the C entry point. @objc emits a Swift symbol plus a C thunk. This means:
@c→@objcon a function with only C-compatible types (no bridging needed): the C symbol is preserved and callable, but the Swift symbol changes from a C-convention symbol to a native Swift symbol + thunk. This is ABI-breaking on the Swift side.@_cdecl→@objc: the ABI is identical. Both emit a mangled Swift symbol + a C thunk. This is the migration path the proposal describes ("Existing adopters of@_cdeclcan replace the attribute with@objc").
I think the proposal's claim that @c ↔ @objc is ABI stable was written under the assumption that both would use the single-symbol model. Since @objc needs type bridging, that assumption doesn't hold cleanly. I'd suggest the documented ABI guidance should be:
@_cdecl→@objc: ABI stable (same two-symbol model)@c↔@objc: ABI-breaking (different symbol models), C-side stable if the type signature is C-compatible in both cases
Type checking
The implementation accepts the types SE-0495 specifies for @objc global functions:
- Everything
@caccepts (C-representable types) @objcclasses, enums, and protocols- Imported Objective-C types
- Swift types with implicit ObjC bridging (
String,Array,Dictionary,Set, etc.)
Protocol existentials are rejected for both @c and @objc, as the proposal requires. Note that @objc protocol types are accepted, but bare protocol existentials (any P) are not -- this is consistent with how @objc methods on classes already work.
Optional refinement
We could also do a type-checking pass on @objc global functions that could be lowered to @c global functions (if the type signature is entirely expressible in C types) to either do that symbol simplifying during lowering or emit a diagnostic to replace the attribute with @c.
If going the diagnostic route, there's no surprise at the ABI boundary, but if the signature of a two-symbol @objc function is already C-compatible and doesn't emit any bridging code, the thunk will merely forward the call to the swift symbol and can be optimized away.
Summary
The implementation at swiftlang/swift#90089 reuses the existing @_cdecl two-symbol infrastructure to support @objc on global functions with full type bridging. This diverges from the "single symbol" statement in SE-0495, but I believe the two-symbol approach is the correct one for @objc given the type bridging requirements. I'd like feedback on the ABI implications before merging.