[Pitch] Formalize @cdecl

Hi everyone! Here's a pitch to formalize @cdecl to identify global functions as callable from C code, with supporting features.

See the latest version of the proposal on github. Please let us know what you think of the currently suggested direction.

Formalize @cdecl

  • Author: Alexis Laferrière
  • Review Manager: TBD
  • Status: Awaiting implementation
  • Experimental Feature Flags: CDecl for @cdecl, and CImplementation for @cdecl @implementation

Introduction

Implementing a C function in Swift eases integration of Swift and C code. This proposal introduces @cdecl to mark functions as callable from C, and enums as representable in C. It provides the same behavior under the @objc attribute for Objective-C compatible global functions. To complete the story this proposal adds to the compatibility header a new section for C clients and extends @implementation support to global functions.

Note: This proposal aims to formalize, clean up and extend the long experimental @_cdecl. While experimental this attribute has been widely in use so we will refer to it as needed for clarity in this document.

Motivation

Swift already offers some integration with C, notably it can import declarations from C headers and call C functions. Swift also already offers a wide integration with Objective-C: import headers, call methods, print the compatibility header, and implementing Objective-C classes in Swift with @implementation.

These language features have proven to be useful for integration with Objective-C. Offering a similar language support for C will ease integrating Swift and C, and encourage incremental adoption of Swift in existing C code bases.

Offering a C compatibility type-checking ensures @cdecl functions only reference types representable in C. This type-checking helps cross-platform development as one can define a @cdecl while working from an Objective-C compatible environment and still see the restrictions from a C only environment.

Printing the C representation of @cdecl functions in a C header will enable a mixed-source software to easily call the functions from C code. The current generated header is limited to Objective-C and C++ content. Adding a section for C compatible clients with extend its usefulness to this language.

Extending @implementation to support global C functions will provide support to developers through type-checking by ensuring the C declaration matches the corresponding definition in Swift.

Proposed solution

Introduce the @cdecl attribute to mark a global function as a C functions implemented in Swift. That function uses the C calling convention and its signature can only reference types representable in C. Its body is implemented in Swift as usual. The signature of that function is printed in the compatibility header using C corresponding types for C clients to import and call it.

@cdecl(nameFromC)
func mirror(value: Int) -> Int { return value }

Allow using the existing @objc attribute on a global function to offer the same behavior as @cdecl except for allowing for the signature to also reference types representable in Objective-C. The signature of a function marked with @objc are printed in the compatibility header using Objective-C corresponding types.

@objc(nameFromObjC)
func objectMirror(value: NSObject) -> NSObject { return value }

Note: The attribute @objc on a global function would be the official version of the current behavior of @_cdecl.

Accept the newly introduced @cdecl on enums to identify C compatible enums. A @cdecl enum must declare an integer raw type compatible with C. An enum marked with @cdecl can be referenced from @cdecl or @objc functions. It is printed in the compatibility header as a C enum.

Note: The attribute @objc is already accepted on enums that will be usable from @objc global function signatures but not from @cdecl functions.

Extend support for the @implementation attribute, introduced in SE-0436, to global functions marked with either @cdecl or @objc. Type-checking ensures the C declaration matches the definition in Swift. Functions marks as @implementation are not be printed in the compatibility header.

Detailed design

This proposal affects the language syntax, type-checking and the compatibility header.

Syntax

Required syntax changes are limited to one new attribute and the reuse of two existing attributes.

Introduce the attribute @cdecl expecting a single parameter for the corresponding C name of the function or enum. The compiler should ensure the C name respects the rules of an identifier in C.

Extend @objc to be accepted on global functions, using its parameter to define the C function name instead of the Objective-C symbol.

Extend @implementation to be accepted on global functions.

Type-checking of global functions signatures

Global functions marked with @cdecl or @objc need type-checking to ensure types used in their signature are representable in the target language.

Type-checking should notably accept these types in the signature of @cdecl global functions:

  • Primitive types defined in the standard library: Int, UInt, Float, Double, UInt8, etc.
  • Opaque pointers defined in the standard library.: OpaquePointer, UnsafeRawPointer, and UnsafeMutableRawPointer.
  • C primitive types defined in the standard library: CChar, CUnsignedInt, CLong, CLongLong, etc.
  • Function references using the C calling convention.
  • Enums marked with @cdecl.
  • Types imported from C headers.

Type-checking should accept more types in the signature of @objc global functions and reject them from @cdecl functions:

  • @objc classes, enums and protocols.
  • Types imported from Objective-C modules.

For both @cdecl and @objc global functions, type-checking should reject:

  • Optional non-pointer types.
  • Non-@objc classes.
  • Swift structs.
  • Non-@cdecl enums.
  • Protocol existentials.

Type-checking of @cdecl enums

Ensure the raw type is defined to an integer value representable in C. This should be the same check as currently applied to @objc enums.

@cdecl @implementation and @objc @implementation

Ensure that a global function marked with @cdecl @implementation or @objc @implementation is matched to its corresponding declaration in imported headers.

Compatibility header printing

Print only one compatibility header for all languages as it's currently done for Objective-C and C++, adding a section specific to C. Printing that header can be requested using the existing compiler flags: -emit-objc-header, -emit-objc-header-path or -emit-clang-header-path.

Print the C block in a way it's parseable by compilers targeting C, Objective-C and C++. To do so ensure that only C types are printed, there's no reliance on non-standard C features, and the syntax is C compatible.

Source compatibility

This proposal preserves all source compatibility.

Existing adopters of @_cdecl can replace it with @objc to preserve the same behavior. Alternatively it can be updated to @cdecl for the more restrictive C compatibility check, but this will change exactly how the corresponding C function is printed in the compatibility header.

ABI compatibility

Marking a global function with @cdecl or @objc makes it use the C calling convention. Adding or removing these attributes on a function is ABI breaking. Updating existing @_cdecl to @objc or @cdecl is ABI stable.

Implications on adoption

The changes proposed here are backwards compatible with older runtimes.

Future directions

A valuable addition would be some kind of support for Swift structs. We could consider exposing them to C as opaque pointers or produce some structs with a C layout.

Alternatives considered

@cdecl attribute

In this proposal we use the @cdecl attribute on functions to identify them as C functions implemented in Swift and on enums to identify them as C compatible. This feature is fundamental enough that introducing a new attribute, a familiar one at that, seems appropriate.

We considered some alternatives:

  • A shorter @c would be enough to reference the C language and look appropriate alongside @objc. However a one letter attribute would make it had for searches and general discoverability.

  • An official @expose(c), from the experimental @_expose(Cxx), would integrate well with the C++ interop work. It would likely need to be extended to accept an explicit C name. This sounds like a viable path forward however it's not yet official and may associate two independent features too much if the attribute provides a different behavior for C vs C++.

@objc attribute on global functions

We use the @objc attribute on global functions to identify them as C functions implemented in Swift that are callable from Objective-C. This was more of a natural choice as @objc is already widely used for interoperability with Objective-C.

We considered using instead @cdecl @objc to make it more explicit that the behavior is similar to @cdecl and extending it to Objective-C is additive. We went against this option as it doesn't add much useful information besides being closer to the compiler implementation.

Compatibility header

We decided to extend the existing compatibility header instead of introducing a new one just for C compatibility. This allows content printed from Objective-C to reference C types printed earlier in the same header. Plus this follows the current behavior of the C++ interop which prints its own block in the same compatibility header.

Since we use the same compatibility header, we also use the same compiler flags to request it being emitted. We considered adding a C specific flag as the main one -emit-objc-header is Objective-C specific. In practice however build systems tend to use the -path variant, in that case we have -emit-clang-header-path that applies well to the C language. We could add a -emit-clang-header flag but the practical use of such a flag would be limited.

22 Likes

Here’s the general roadmap to formalize @cdecl as currently planned:

2 Likes

Not a super interesting review, but just a general +1 from me. I do think a more general @expose(c) would be nicer or even @extern(c) (we already have @_extern(c)). Do we have plans for forward declarations with this attribute or is that intended for a different attribute? It would be nice to have something that is unified (kind of like @_silgen_name but for C).

Implementation notes:
Currently, @_cdecl creates a foreign thunk in addition the original Swift function. This has caused a few problems where folks attempting to rewrite C code in Swift have unintentionally broken Swift clients. The compiler will now want to reference the Swift function instead of the C one which is an issue if an older OS doesn't have an entry point for that Swift one because it was implemented in C originally. I really want this formalized attribute to not expose/export the original Swift function and just become the C one so we can perform these sorts of rewrites in the future. That being said, it would make the following assertion in "ABI Stability" no longer true:

Updating existing @_cdecl to @objc or @cdecl is ABI stable.

5 Likes

I agree with @Alejandro 's comments about the additional thunk. This caused us quite a bit of confusion attempting to use @_cdecl for a compatible re-implementation of Decimal.

Regarding the name, I think decl is quite jargon-y. I know @c is short, but I honestly prefer it because it's quite clear what it does, especially when paired with @objc.

4 Likes

Is it ABI breaking to change a @cdecl function to @objc or vice versa?

What kinds of enums are allowed when using @objc instead of @cdecl? If there’s no difference, could you make it possible to use @objc enums from @cdecl functions so that existing types don’t have to be updated?

1 Like

Just to ensure that I understand the proposal, here are a few questions:

  1. Does adding @cdecl implicitly change the calling convention of the function to C?
  2. Is it possible to control the user label prefix? This is important because user label prefixes might need to change mid-flight (e.g. ARM64X ABI on Windows, which can work with different labels in the same AS).
  3. Is @cdecl possible to disassociate from the decl? I'm worried about cases where the exposed entry point spelling may need to change based on the host that you are compiling for.
  4. How do you handle variants? e.g. __attribute__((__regparm(3)__))

+1 I would really like to see this unified with @_expose(cxx) and @_expose(wasm).

Additionally and unrelated to the above , @_cdecl currently implies a liveness root in the compiler and this feels like a conflation with @_used. I think whatever future formalization of @_cdecl should not imply used and the author should have to opt into this.

5 Likes

I was under the impression that @_expose(lang) was the way forward for these kinds of interoperability needs, and @_cdecl will just be abandoned when @expose(C) was possible.

Even if the C++ version needs to stay in experimental mode, I would like to see this as @expose(C), and, if necessary, leave the existing C++ and wasm versions as @expose(_Cxx) and @expose(_wasm) to indicate its status as experimental.

9 Likes

Even on non-Darwin platforms?

2 Likes

Will SIMD types be supported in @cdecl functions? They're not an explicit line item in the list of supported types, but it could either be an oversight or rolled into "things you got from a C header" (as many SIMD types come from import simd, at least on Apple platforms).

2 Likes

Will this proposal type check method parameters? This has always been an issue of using @_cdecl, and it would be great if any formalization of it fixed that.