Formalizing @cdecl

For a while now (possibly since Swift 1?) we've had an undocumented attribute @_cdecl that exposes a Swift function to C. Several years ago (and while still at Apple), I alluded to the remaining decisions to we had to make to make @cdecl a reality. I was hoping someone else would pick that up, but it hasn't happened yet; meanwhile, though, people are using it. So here it is: what we need to decide, along with my recommendations:

  • The name. I think @cdecl is our best choice, because @c is so short, but either one is fine. People might also want to invent something generic like @convention(c) on functions, so that we can scale to "expose this to Java" some day; I personally think this is overkill and will bog down the actual proposal. (If we do use some more generic syntax, make sure it still has a place to put a C name that's different than the Swift name.)

  • Restrictions: Simply put, I think that @cdecl should only allow the subset of types allowed in C, excluding those allowed by ObjC. This makes it easier to write code on a Mac and not have to worry about your @cdecl accidentally using an Objective-C type.

    If you do want a C function that takes or returns ObjC types, it should use @objc @cdecl—yes, both attributes, like @objc dynamic or @objc optional. This spends a bit of verbosity to make it obvious which attribute is doing the "expose to C" operation, which, again, is about not accidentally writing the wrong attribute while on a Mac and then using types you shouldn't.

    (I'm sure this is a bit controversial, so I'll talk through more of my thought process in a follow-up post.)

  • Closures: Exposed as blocks, not function pointers, as with @objc methods today. We already expose block-based APIs in the C side of Dispatch and SourceKit, and we can guard the declaration of the function in the generated header so it's only usable when blocks are available.

  • Any / AnyObject: I think we should start off not supporting these, but in theory we can have an opaque SwiftObjectRef type where the only thing you can do is swift_unknownRetain and swift_unknownRelease it (and maybe debug-print it as well). What I don't love here is that SwiftObjectRef ought to just be id in ObjC modes, but with eventual C++ interop we'd have it presenting different types in C++ vs. ObjC++. We could resolve that by making it convertible to id in ObjC++.

    Anyway, there might be answers here, but people have done just fine so far with just the usual C types, and the clunky-but-serviceable "bounce through Unmanaged" for passing a Swift object through void *, so I think this should be "Future Directions" rather than part of the Minimum Viable Product.

  • Bool: Should be allowed, but do we follow @objc and make it BOOL, or pick a more C-standard bool? I'm inclined to go with the latter, possibly modified by whether the function is @cdecl or @objc @cdecl. (The same decision should be made about Int being encoded as NSInteger vs. intptr_t.)

  • Enum declarations: @cdecl should be allowed on enums, just like @objc is today, with one adjustment: in non-ObjC, non-C++ contexts, we can't use enum Foo : uint8_t { … } syntax for a fixed-size enum. Fortunately Apple has already figured out a fallback: if fixed-size enums are not available, define typedef uint8_t Foo; enum { … } instead—something the generated header can take care of automatically. It's not great (you lose code completion, for example), but it works at the ABI level.

  • Struct declarations: I think @cdecl structs make sense but I don't want to roll them into this proposal; keep defining your structs in C for now.

  • -emit-objc-header: The compiler currently uses "objc" in the names of the arguments to emit a header for a Swift module; that ought to be changed, but I'm not sure to what. -emit-c-header, even though it will sometimes have ObjC in it? -emit-header feels a bit too generic. -emit-interoperation-header is verbose without solving the generic. Suggestions?

  • SwiftPM support: C-language targets ought to be able to depend on Swift targets, so SwiftPM ought to start generating this header, along with a module map for the Swift target. I don't know how hard this is and I'd appreciate help. I suspect it's not too bad because SwiftPM already supports C-language targets.

  • EDIT: Xcode support: Should be automatic for Xcode targets, because they're already set up to always generate ObjC headers! If there's additional Xcode/SwiftPM integration to do, though, someone from Apple will have to deal with that.

I have not implemented any of this in the compiler yet, but it's designed to be a fairly minimal set of changes from what's already there. I am willing to do that work if this goes to a proper proposal.

51 Likes

Being more explicit about what types are allowed:

  • standard library integers and floating-point types
  • Unsafe[Mutable][Raw]Pointer of C types, and optionals thereof
  • OpaquePointer and optionals thereof
  • SIMD types that the compiler supports importing
  • imported-from-C types that aren't ObjC classes or protocols
  • @cdecl enums
  • closures whose arguments and return types follow these rules (as blocks)
  • Bool (see added section above)
  • Void (for return values only)
3 Likes

Thank you for doing this! Would you anticipate:

@cdecl func myCFunction(callback: @convention(c) () -> Void) { ... }

...would come back as taking a function pointer rather than a block?

1 Like

Yes, anything with an explicit @convention(c) shouldn't change, good point.

3 Likes

Okay, why @objc @cdecl together? When thinking this through, I considered several options:

  • @cdecl and @objc are both valid on top-level functions, but behave differently: @objc allows using ObjC-only types and @cdecl does not.

    • Upside: better checking of accidental reliance on ObjC interop.
    • Downside: easy to use the wrong one, in theory.
    • Downside: we may eventually want to allow @cdecl on member functions as well, the same way we have import-as-member for C functions, but @objc already means something there.
  • @cdecl and @objc are interchangeable on Apple platforms, i.e. you can use ObjC-only types in @cdecl functions.

    • Downside: accidental reliance on ObjC interop
    • Downside: same thing about member functions
  • @cdecl allows using ObjC-only types on Apple platforms and you can't write @objc on a top-level function at all

    • Downside: accidental reliance on ObjC interop
  • @cdecl does not allow ObjC-only types, and you can't write @objc on a top-level function at all

    • Downside: seems like an artificial limitation - what if you want a top-level C-style function that takes an NSString? Or a CF type?
    • This is my second choice, though, and it's easy to expand it to my original proposal later.
  • @cdecl does not allow ObjC-only types, but @objc @cdecl does

    • Downside: verbose and a little weird-looking
    • Upside: better checking of accidental reliance on ObjC interop
    • Easy to have the compiler help you if you need both but only have one
    • This is my preferred option.

I also thought about increasing interop capabilities so that anything that Swift can expose to Objective-C can be exposed to C as well with some sort of opaque struct pointer type—then there are no more types requiring ObjC interop! Sadly, I came to the conclusion that it was just a dream, because you'd still expose them differently on platforms with/without ObjC interop, and it's unclear whether you'd even be able to tell what to expose from Swift when there's no such thing as "ObjC ancestry". (And you can't expose everything because Swift allows naming conflicts that C does not, thanks to modules and nesting. Additionally, exposing more things than @objc does today would change the behavior of the "infer-@objc-for-me" annotation @objcMembers, which makes it a source-breaking change.)

3 Likes

Motivations:

  • Plug-in-style APIs that expect your library to define a certain unmangled symbol name.
  • Writing a library in Swift that exposes a C interface.
    • Porting an existing C-based library to Swift, or making a drop-in replacement for an existing library.
  • Baseline interop with non-C-based compiled languages, because for better or worse everyone can talk to C.
  • Any more to add to the list?
4 Likes

Two flags?

-emit-c-header -- warns/errors if you have ObjC declarations
-emit-objc-header -- happy with any combination of @cdecl or @objc declarations

2 Likes

Count me in on Option 5 — I know folks who have lamented the lack of ability to expose a top level function to ObjC with ObjC types. Would be good to make it possible but not required, and I think these attributes compose neatly and the fix-its would be self-explanatory.

2 Likes

Overall, big support for formalizing the existing @_cdecl functionality.

Since you mention the upcoming C++ interop briefly a couple times, it would be good to consider how that fits into a C-family interop story. If we merely remove the underscore from @_cdecl, then we'll have two distinct attributes for C and Obj-C interop. If we later want to add a way to export some subset of Swift types/functions as C++ types/functions, would we then have to add another attribute, like @cppdecl? Or should we start thinking of a more general attribute that can support all of these? (cc: @gribozavr)

Naturally @objc can't go anywhere since it's so ingrained in the language now, and that's probably fine. If it was decided to go with a more general attribute, @objc could still be treated as a convenience shortcut for a common use case.

But it's probably good to think about this now to avoid potential "attribute soup" in the future. And it may help to shake out some potential rough edges in the design:

  • If a non-member function is being exported that has types compatible with C, then exporting it as "C" vs. "Obj-C" is equivalent; they share the same mangling. However, exporting the same function as "C++" would create a separate function with a mangling distinct from the C/Obj-C version, and both could co-exist in the same binary. Should that be allowed? If so, what would that look like in Swift source?

    • C++ supports overloading, whereas C and Objective-C don't (without using the Clang-specifci __attribute__((overloadable)), which I can't remember if Swift imports correctly).
  • What does header generation look like if we're generating a header for code that uses @objc, something equivalent to @_cdecl, and something for C++ all in the same module? Does the flag decide what to generate, or is there always a single header with appropriate #if guards around each section?

Is this referring to plug-ins implemented in Swift and consumed by a Swift host program? This seems like an anti-pattern since, as you mention, it requires the plug-in and the host to marshal the plug-in instances through either unsafe pointers or at a minimum lose type information using a hypothetical SwiftOpaqueRef type that you described.

Plug-ins seem better served by having the host define a protocol for plug-ins to implement and then having the language provide a type-safe generic API to iterate over the conformances that have been loaded into the process. I think the other motivating use cases for a @cdecl-like thing are strong enough that this one doesn't feel like it lines up.

2 Likes

It seems to me that @_cdecl actually does two distinct things:

  • Constrain the types and calling conventions of a function such that it can be called from C (or other languages with C interop). This overlaps with @convention(c), but I don’t really know how closely.
  • Define the linkage name of a function, which is used for static and dynamic linking and also shows up in backtraces. This overlaps with @_silgen_name.

I think we should strive for a design where these two concepts are separable. If there is an attribute that does both at once, it should be possible to achieve the same effect by composing two (public) attributes; that is, @_silgen_name should be public (probably under a different name) and ideally @_silgen_name(foo) @convention(c) should be equivalent to @cdecl(foo).

Alternatively, if the combined case is expected to dominate (which seems reasonable), it should be possible to get the effect of only @_silgen_name using something like @cdecl(foo) @convention(swift).

Use case

Renaming symbols is one of those things where language/core library developers might think “nobody else needs to do this”. However, I’m currently using @_silgen_name to add context to backtraces, in a manner inspired by Dispatch:

@inline(never)
@_silgen_name("__LIBRARY_IS_CALLING_A_BLAH_CALLBACK__")
private func invokeBlah<Param>(_ blah: Blah<Param>, param: Param) {
    return blah(param)
}

Being able to do this with a condoned attribute and not having to type-erase to an UnsafePointer or something would be nice.

Language orthogonality aside, it’s not entirely obvious that exporting an API to C should default to changing the linkage name. Instead of generating C prototypes for exported Swift functions, an exporter could generate stubs like this:

// Swift
@convention(c) public func glorifyMyInt(_ x: Int64) -> Int64 { ... }

// Generated header
static inline int64_t glorifyMyInt(int64_t x) {
    // Strawman: _SC instead of _$ as sigil for cdecl Swift functions
    // Could use __asm__ instead, but requiring extensions undermines my point :-)
    extern int64_t SCs9MyLibrary07glorifyA3Intys5Int64VADF(int64_t);
    return SCs9MyLibrary07glorifyA3Intys5Int64VADF(x);
}

There are at least two good reasons not to do this:

  • In cases like fulfilling a C-function-based plug-in API, the linkage name is important. This could be handled with an orthogonal attribute, though.
  • For generating bindings for languages other than C, this would be an annoying pain. I think this is the best argument for a public @cdecl doing both things.

The inline stub approach would perhaps be helpful for @allevato’s hypothetical C++ exporter, though, since the Swift compiler wouldn’t have to care about C++ mangling.


On a very tangential note, if we do get @objc top-level functions and perhaps @cdecl structs, I’d also like to be able to use @objc on top-level constants, at least for strings and numbers (e.g. @objc public let MyLibraryIdentifier = "hello" → either extern NSString *const MyLibraryIdentifier; or static NSString * const MyLibraryIdentifier = @"hello";)

2 Likes

It's also important to note that this attribute serves two purposes. 1. To change the linkage name of a Swift function (must include body) 2. To define an external Swift function to be used within the current context (must not include function body).

// This represents number 1, where this function is lowered as
// hello with no mangling whatsoever in the binary.
@_silgen_name("hello")
func goodbye() {
  print("hello")
}

// This represents number 2. We know that in this binary there is
// a function named hello (the one we just defined up top), so define
// a function prototype that calls said method. Super important to note
// that this defines an external SWIFT function, thus it'll call the function
// using the Swift calling convention, not the C calling convention.
//
// This is similar to the following C code (if it were valid):
//
// extern "Swift" void hello();
//
@_silgen_name("hello")
func hello()

// From Swift, we call number 1 as the name in the function, not the linkage
// name.
goodbye() // hello

// This calls the second function we defined with using number 2.
hello() // hello

So I'm of the camp that this attribute needs to separate into two separate attributes, one for each purpose. We can define something like @link_name("") to achieve the desired result you're looking for, and we can define something like @extern for the second purpose. I think it'd also be interesting if we could do something like @extern(c) to define external C functions within Swift so that we don't have to go through a shim header, but that's derailing off topic :slight_smile:

Since I’m advocating for compositionality, why not @extern @convention(c)?

2 Likes

+1 to using @extern @convention(c) so we can reuse the syntax in the future with other interop support.

3 Likes

Hey @jrose,

Id like to throw a bit of paint into the colour of the bikeshed here. I'm not too fond of @cdecl as the attribute. The reason is that it is confusing precisely where it is used - interoperability with C. The spelling of @cdecl is too near the spelling of __cdecl and __attribute__((__cdecl__)) which specify the calling convention. At least for Windows, the following calling convention definitions are common:

  • __cdecl
  • __stdcall
  • __fastcall
  • __thiscall
  • __vectorcall
  • __clrcall (for completeness rather than for current usefulness - but, hey, C#/Swift interop anyone?)

Without having really thought much about it, I like the idea of separating the linkage and the convention aspects of the attribute.

2 Likes

IIRC none of these exist on 64b, except for __vectorcall, right?

To be pedantically precise, __clrcall is available on all of them as well. __vectorcall is available on x64 but not ARMv7 and AArch64. But, yes, the remainder are x86 only.

2 Likes

+1 for a generic spelling of the attribute (such as @decl(c) or @extern(c)). The ability to expand easy interoperability with other languages will be invaluable in Swift's future.

3 Likes

I have a suggestion to unify and simplify all of cdecl _silgen_name convention problems in swift.

Use @decl( langID, [linkage] ) to represent lang calling conventions and linkage names in one attribute.

For example,

@decl(c, cName)
@decl(objc, objCName)
@decl(sil) // the same as  "swiftFunName"
@decl(cpp, cppName) 
func swiftFunName() {...}

Also can use @decl(*, newName) to export and rename linkage name for all supported langIDs.

If NO linkage name provided use the same name of original fun, such as @decl(c) is equivalent to @decl(c, swiftFunName) by default.

Any thoughts ?

11 Likes

I don’t dislike it, although I’d prefer to call it @export. (@import instead of @extern is tempting, but could arguably be confusing)

2 Likes

There are other reasons we may need to be able to encode arbitrary C function types in @convention(c) Swift function types, something like @convention(c, typeName: "BOOL (BOOL)") in Swift. We could maybe use the same idea to support overriding the default bridge mapping with a specific C signature for @cdecl too:

@cdecl("foo") func foo(x: Bool) -> Bool // _Bool foo(_Bool);
@cdecl("foo", typeName: "BOOL (BOOL)") func foo(x: Bool) -> Bool // BOOL foo(BOOL);
3 Likes