Yeah, I wonder if @c is mashing together two axes to C-interop—exposing to C and laying out like C—that we may be able to get away with in this proposal, but now we're finding that even slightly expanding the model requires talking about it. I might want a struct with Swift layout exposed to C, either as opaque or as its only stored property, or I might want a struct with C layout. That's a different feature, but perhaps then @layout(c) or something like that would be more fitting here for this feature.
It isn't directly mentioned in the proposal, but am I correct to infer that an @c enum's discriminant and its raw value will be one and the same?
I agree here.
I bring this up in many reviews, but I think it would be better for the language in general if, rather than creating separate attributes for every version of a feature, we have one attribute that takes a parameter. In this case it would allow for obvious growth as more language layouts are added in the future, and would allow things like @compatibleLanguage(c, c++, objc, python), as well as aligning with the suggested @layout(c). Doing it this way would also allow supporting multiple language versions, like @compatibleLanguage(c23, c99). In general, such a flexible approach seems like a healthier approach for the language into the future.
In this case there are essentially no other languages with stable ABI, so (other than language versions, which—while something to keep in mind—are a fairly minor consideration), there is very little need to generalize this feature.
@compatibleLanguage(c)* is nonetheless much more readable, more discoverable, more web-searchable, and so on, than the single-letter @c.
*Or @foreign(c), @external(c), or whatever descriptive label is most accurate.
@extern(C) naturally :)
I do like the @extern(c) spelling, and was one proposed in the original discussion with Jordan. It also is more extensible and has prior art in C and C++. It also helps separate two axis:
extern -> decoration
convention -> calling convention
That is you could do @extern(c) @convention(vector) func f(_ v: SIMD4<CInt>).
I think there are some gaps between Swift enums and C enums. When bridging from C to Swift, it seems that C enums were bridged to a Swift struct with constants, which resolved the gap.
For incremental adoption of Swift in existing C code bases, I think bridging Swift enums to C enums may need some customization options.
I don’t think such options are required in this proposal. But they should be recognized as possible future directions for the API.
Here are some possible customization points:
(Sorry. I tried swift-DEVELOPMENT-SNAPSHOT-2025-09-23-a-osx, but I got a compile error "Unknown attribute 'c'". So I could not check the actual output.)
typedef
For example, with this Swift code:
@c
enum CEnum: CInt {
case a
case b
}
I assume it will be converted to C like this (the proposal does not show a concrete example):
typedef enum CEnum {
a,
b
} CEnum;
In C, the tag name and the typedef name are in different scopes.
There could be cases where the typedef name is already used for another enum/struct.
So, maybe an option like @c(omitTypedef) is useful.
Enum member scope in C
In Swift, enum member names are scoped inside the enum type.
In C, enum member names are global identifiers.
We can make Swift case names longer to avoid conflicts, but it is not ideal in Swift code.
It may be useful if the compiler synthesizes the prefixed case names, or allows a custom name like @c(“CEnumA”).
Duplicate enum values in C
In C, it is valid that two members have the same value:
typedef enum CEnum {
a = 1,
b = 2,
c = 1
} CEnum;
Maybe it is also useful to expose a static let like this.
@c
enum CEnum: CInt {
case a
case b
static let c: CEnum = .a
}
Not to be too harsh, but I feel that this proposal is incomplete without showing a concrete example of what the generated C enum will look like. You've pointed out very real concerns about naming, given that Swift enum case naming and C enum case naming are essentially diametrically opposed:
- Swift
enumcases are scoped to they type, so they should never repeat the containingenumtype name, and they should be spelled inlowerCamelCase. - C
enumcases are members of the scope containing theenum, so they must be guaranteed to not clash with other definitions in that same scope (often global scope). Therefore, they are often prefixed with the name of the enum. There are no standard case conventions, thoughUPPER_SNAKE_CASEis common.
I don't think it would be good for the case names from Swift to be used verbatim, because Swift authors should not have to sacrifice clarity/convention on that side to make the enum safely exposed to C. Like if the user wants colorRed as a case name for enum Color, they should absolutely not have to spell it colorRed on the Swift side.
Automatic prefixing the type name would probably be the safest choice, turning this:
@c enum Color { case red, green, blue }
into
enum Color { Color_red, Color_green, Color_green }
But we do need to clarify whether @c can be applied to individual cases as well. If so, then auto-prefixing would be the "safe" default and users could override specific names if they wanted to after that.
We could also add an argument to @c to control the naming scheme (describe the case convention, capitalize the first letter of each case after joining, etc.) but that starts to get hyper-specific and I'm not sure it's worth the complexity. We don't do that for functions.
I'm not convinced that we need an interop attribute that generalizes to languages outside of the C family. C, C++, and Objective-C interop have a special place in the Swift compiler because Clang itself is built into it, and that's what powers interop for those languages. So it makes sense for there to be a compiler-built-in attribute that the compiler interprets. For other languages that the compiler isn't aware of (and that we don't want the compiler aware of), I expect interop to use different approaches to produce bindings, like macros or other forms of code generation (e.g., swift-java).
Having a @compatibleLanguage list means the compiler would need to just accept an arbitrary list of strings there to support whatever languages someone may want to support in the future, and that by itself is meaningless without other tooling elsewhere in the build to interpret the entries in that list and generate the right bindings.
Or perhaps we could specify names explicitly:
@c("CColor") enum Color: Int {
@c("CColorRed") case 🔴
...
}
Yes, I mentioned that above as well, but I think it would be a bit of a burden to force the user to specify an explicit name for every case if there was an automatic prefixing scheme that we could use that would be good enough for many cases.
It might be the better approach if the number of languages with builtin-in interoperability support were arbitrary. I don't think that is the case: the compiler provides C/C++/Objective-C interoperability, and all other language interoperability should be handled by libraries. Those libraries can use macros to define @Java, @Python, etc. that will feel right at home next to @objc (which we cannot remove anyway) or @c, but would feel out of place against @compatibleLanguage(c).
I would rather leave this up to implementers to handle as C evolves, rather than requiring proposals to match each thing the C or C++ languages introduce that might be representable in Swift.
Doug
I'll look to add this to the proposal text but here's the gist of it. We print @c enum cases in the compatibility header using names composed from the enum name and the case name attached. The first letter of the case name is capitalized automatically. For example:
@c enum MyEnum: CInt { // --> MyEnum
case FirstCase // --> MyEnumFirstCase
case lowerCase // --> MyEnumLowerCase
}
This is the same naming format used for @objc enum cases.
At the moment, we do not allow @c on an individual enum case but it's something we should be able to support.
Hello,
In the ABI section, this proposal says that a @c function would produce the same entry points that @_cdecl does:
Updating existing
@_cdeclto@objcor@cis ABI stable.
That's not what we want: @_cdecl actual produces two entry points: one with Swift's calling convention that's called by Swift clients, and a C-compatible "thunk" that's called by C clients. We consider that a mistake: a @c function should emit a single, C-compatible entrypoint and all clients should go through it. That makes the ABI surface area smaller and more predictable, as well as exactly matching what would happen if the function were implemented in C directly. This goes both for @c and @c @implementation.
Doug
Is the proposed @c annotation different from current @convention(c) (besides the fact that it applies not only to functions, but also to data structures)?
I'm just trying to figure out when to use @_cdecl, and when @convention(c) and when the proposed @c.
It would be nice to have one way to declare C compatibility for a function, rather than two or three. I hope that the new @c annotation aims at that.
I believe this proposal is a valuable addition to Swift: Formalising @_cdecl and thereby improving C interoperability outside of ObjectiveC is a great step in the right direction. The following questions came up while reviewing the proposal.
Linkage of @c global functions
What is the linkage behaviour of a global function marked as @c? Does adding @c to a function ensure that a linkable symbol will be emitted. Will that be true also for Embedded Swift, where normally no (linkable) symbol would be emitted? That is, does @c imply (at least) @export(interface) from SE-0497?
Access Modifiers
Is @c usable with every available access modifier? If so, what is the intended behaviour of combining @c with private or fileprivate?
Not to speak for the author, but I would not expect @c to imply exported linkage. It is entirely possible to want to write a C function in Swift that you then use from C code in the same target, for instance.
I am kinda curious why this is considered a mistake. Emitting the C interface as a thunk and then calling the Swift CC function allows Swift functions to call without having to adjust the calling convention.
Of course, that doesn’t speak to the exposed ABI. You could expose only the C interface outside of the module to minimize the exposed surface.