Implementing global @objc functions

[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 @c and @objc functions.

and:

Changing between @c and @objc is 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:

  1. A Swift-native symbol (mangled) for the function body, operating on Swift types.
  2. 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 emitNativeToForeignThunk infrastructure 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@objc on 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 @_cdecl can 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 @c accepts (C-representable types)
  • @objc classes, 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.

What would be drawback of handling specifically @objc-global-functions-with-only-types-expressible-in-C according to the single-symbol model (besides implementation complexity)? It seems that would preserve the ABI guarantees made in SE-0495, no?

1 Like

What's the benefit of two attributes over just using the existing @c global func if we're restricting the same type-checking?. The purpose of an @objc attribute would be to accept objc types in the signature, which from the user's perspective bridging is implied like in class @objc methods.

From the SE:

It offers the same behavior as @c while allowing the signature to reference types representable in Objective-C. The signature of a @objc function is printed in the compatibility header using corresponding Objective-C types.

Currently we can already use @_cdecl but:

  • the feature doesn't emit the (obj)C symbol in the generated header
  • the underscore implies an unstable imlementation

Conversely, how much would it take to emit two symbols for @c, with one being an alias for the other? That would also remove the inconsistency and allow ABI stability as long as you don't go back to an earlier compiler version. I'm not sure this is a good idea, but it is simple (and could be limited to library evolution mode anyway).

Two symbols was called out repeatedly in the review as undesirable for @c which makes a lot of sense.

The only caveat that is important here is that going from @objc → @c func would be ABI breaking on the Swift-visible symbol's calling convention. The reverse is not true as the same-named C symbol exists in both (obviously renaming the symbol would be breaking).

But moreover I think this is not that important because the only reason you'd be going between the two would be to represent types that you couldn't in an @c func which is necessarily API and ABI breaking (API breaking but not ABI breaking if you're replacing a void* with an NSObject*) but that's almost trivial to point out.

As mentioned above if we did a typecheck of an @objc func and reject it if it is expressible in @c, then going between the two would have to be API breaking (and we would expect ABI breaking as well). That would simplify the ABI expectation but at the cost of an additional type-checking pass, which I don't think is desirable.

1 Like

That's not quite what I'm asking. The model I'm asking about would be: as promised in the proposal, @objc would transparently accept Obj-C types and @c would not. But if the signature of an @objc function has only types representable only in C, then the compiler uses the single-symbol model as-though it were written @c.

If the signature is later changed to include an Obj-C type, that's an error with @c but is accepted with @objc, and in that case we switch to a two-symbol model; changing the signature is obviously ABI-breaking already, so changing from a single-symbol to two-symbol model at that juncture is immaterial.

What if we required you to annotate the function with @swift to get the second entry point…:

@c @swift
func foo() -> CInt

@objc @swift
func foo(_: String)

Yes I called this out above under optional refinement. That way the ABI change would only be triggered by an API change (is this desirable?) and in my previous comment that would come at the cost of two type-checking passes for objc-compatible types in the signature.

Right, I'm aware (I quoted your callout): my question is—what's the drawback (besides implementation complexity) to making that not optional but the implementation of record, so that we have no need to alter the proposal's promises and no ABI break? Phrased another way: doesn't what you wrote as a refinement answer the problem you pose for discussion? Or phrased a third way: doesn't the proposal's promises specifically make non-optional what you call the optional refinement? Or phrased a fourth way: since what you call the optional refinement (to my understanding) would satisfy the proposal and its single-symbol model, why does "diverg[ing] from [...] SE-0495 [with a] two-symbol approach" the "correct approach" instead of the refinement? Hope I'm making sense...

Thanks. Doesn't the same rationale about @c apply to @objc, though? "I had an ObjC library with some global functions that took ObjC types, and I want to port it to Swift with no change in the ABI." Someone is going to be disappointed.

I saw this called out and unanswered in the review and I'll give my take: I don't think the @swift token is semantically meaningful to the programmer. Yes it simplifies the expectation about entrypoints, but someone coding against this symbol shouldn't have to think about those anyway, what they're really interested in is exposing that function to clang.

Say you omit the @swift symbol, does that then make the symbol not callable by Swift? Does it require the frontend to emit bridging code at the call-site from Swift? If that's the case you'd have:

swift caller > emmitted bridging code (to objc) > c symbol with objc types > reverse bridging code (to swift types) > Swift implementation > return type bridging code (to objc types) > emitted bridging code (back to swift types) > swift caller resumes

If swift can call the symbol natively, we don't need that song and dance, and I think removing that complexity is key.

The ABI is still stable when calling from ObjC in either direction. The ABI break is going from @objc to @c from a swift caller)

From the post you linked:

All of this rationale applies to "global @objc func" too.

Gotcha.

Yes, at the cost of additional type-checking.

I think if we want to go down this route (different ABI entrypoints based on types in the signature) then I would prefer to just reject the @objc attribute at type-checking if the symbol would be lowered to a C symbol and be semantically equivalent to @c. Then you would only have an ABI change when you have an API change (introduce objc types).

I see. Yeah, I think on that point I'd disagree. A user who cares only that they want to have Obj-C interop shouldn't have to care about what to them would be irrelevant trivia that their signature happens to include C-representable types only. They should be able to use @objc everywhere including on globals without thinking about that: the compiler can (and therefore should) just handle this. We can take this off their plate without weakening @c-to-@objc ABI compatibility for representable-in-C signatures.

1 Like

Yes, if we're considering equally representable types on both sides of the language boundary. If we want type bridging at the call-site (and I certainly do) then see my comment above about the bridging code required. If the optimizer could remove that bridging code by emitting a native Swift cc func to avoid the indirection in both directions (call into the C symbols implementation without it's signature's types) that would get us back to two symbol emission, but that is I think more difficult than what we have already.

This would come at the cost of the "surprise" of emitting multiple symbols and the ABI breaking from Swift's perspective, which I'm not sure @Douglas_Gregor would agree to. But again the ABI would only break if the API does, so that's at least expected. If that's the trade-off that we're making I'm not terribly concerned.

The API and ABI would inevitably break if the signature gets changed, right? It's simply not the same function at that point.

1 Like

Exactly

Should we also add a fix-it for a @c func with types in its signature that can be representable in @objc but not in C? Maybe if Foundation is imported? Or just reject it as we do today?

Fix-it sounds nice to me :) I think that’s definitely an optional refinement but it seems nice.

2 Likes