[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.

27 Likes

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

4 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.

8 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.

5 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?

2 Likes

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.

8 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.

14 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).

3 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.

Thank you all for the feedback! I’m finally back at working on the proposal after having been through most of the implementation. I updated the proposal text, it should address many of the concerns raised here. You can read it at the same link: [Pitch] Formalize `@cdecl` by xymus · Pull Request #2813 · swiftlang/swift-evolution · GitHub .

I published this pitch here very early in my work on @cdecl. At this point I hope I’m in a better position to answer your questions.

We still have the same behavior as @_cdecl at that level. However, I would expect that replacing the function implementation with a Swift one, keeping the declaration in the header and using @cdecl @implemention would work as desired in such a case.

I’ve given more details on the choice of @cdecl in the updated proposal in the Alternatives considered section but here’s a short version. My only worry for @c is that it’s too short for searching and discoverability. For @_expose, I consider that @cdecl and @_expose(Cxx) already diverge significantly and will likely keep diverging. As such sharing an attribute that accepts different kinds of decls and provides different behaviors has a risk to be more misleading than helpful.

Interchanging @objc and @cdecl is ABI stable but it can be source breaking for C or Objective-C clients as some types in the signature change.

The requirements on both are the same, however differentiating them is important for the compatibility header. The order in which they are printed and the language check gating them is different.

Yes that’s right. In the future, I would consider extending @convention and accepting in on @cdecl functions for more control over the calling convention.

This and the other more advanced use cases that you mention should be possible with @cdecl @implementation with attributes on the C declaration. Alternatively, we could extend the attribute to accept more configuration options with some care.

Nothing added in this proposal is gated on Darwin. However, many Objective-C compatible types require the Objective-C interop which is only available on Darwin. In practice, you can still declare an @objc global function on a different platform and use only C compatible types, but the generated function in the compatibility header will still be restricted to Objective-C.

I believe the float4 style is accepted but not the SIMD4<Float> style, this comes from shared compiler logic. It’s a bit of a special case and I’m not sure it’s official.

AFAIK @_cdecl should already have been checking for the representability of parameters in Objective-C. The new @cdecl is more restrictive as it’s C specific. There were a few issues around Unicode.Scalar and Float16 that I recently fixed. However, if you are aware of more types that are wrongly accepted please let me know or file a bug report and ping me.

4 Likes

The linker happily accepts a @_cdecl implementation in Swift that has wrong arguments, missing arguments, too many arguments, wrong return type, etc, as long as the @cdecl name matches.

Do you mean checking that a @cdecl implementation in Swift matches the declaration in a C header? If so, the @cdecl @implementation syntax is designed to ensure a full match of the two signatures and handle this use case.

1 Like

We should support SIMDn<T> when T is a C basic type (as an ext_vector of n x T, with alignment capped at 16 bytes).

3 Likes

We could weasel a bit and say @cplusplus instead, on the assumption that C++ will someday need a similar feature and will use similar constructs? Although that brings up the question of name mangling which I wouldn't want to try to answer here.

FWIW I'm fine with @c here. It's short, yes, but that's on K&R for picking "C" as their language's name. :man_shrugging:

1 Like

@c is definitely the right spelling for this, for synergy with @objc. Sure, it'll be less searchable in places where you don't have the ability to do a regex-based search like @c(\(|\s|$) but I don't think we should optimize for that by adding noise to the name.

(And in a post-SE-0451 world, the correct spelling of the same attribute for C++ would clearly be @`c++`. :grin: )

5 Likes

No love for @`c⧺`, huh?

4 Likes

To my aging eyes, that looks suspiciously close to @`c#`, and I wouldn't want to cause confusion with any potential .NET/CLR interop work. (Or should that be spelled @`c♯`?)

3 Likes

First off, great that this is getting formalised.

In terms of naming, it would be great if it’s possible to be consistent with the other options like @objc and @expose(Cxx) . I.e., when we want to expose Swift functions/types to other languages, we use the same type of syntax.

1 Like