Formalizing @cdecl

I don’t like truncated names (like func), but I think a more important consideration is how extern works syntactically in C: extern is attached to a declaration, and then controls either exporting or importing of a symbol depending on whether the symbol is defined. In today’s Swift, @_silgen_name kind of parallels this, since it can be used on a bodyless declaration or a full function definition, while @_cname can only be used on a full definition.

Since prototypes are Not A Thing in Swift, I don’t think the C design (or the @_silgen_name design) are particularly appropriate, and separate attributes for importing and exporting symbols would be desirable.

2 Likes

Nothing, it's more a point of flexibility that can be enabled. Consider PythonKit in the compiler. Im not arguing that we should do that, merely that we do not design something which precludes it.

@extern(c) would indicate that the function should be setup for C linkage - it covers name decoration and calling convention. As to the calling convention for C, I don't think that we need that to be something which can be overridden. I think that the target triple is sufficient for that purpose as breaking that breaks nearly everything implicitly.

IMO, yes. The reason is that there is precedence and associated meaning with this for those working with the ABI level considerations.

When you are working with dynamically linked libraries that are loaded at runtime, you can isolate the address space sufficiently to enable multiple ABIs for C++ to coexist (dangerous, but possible). Yes, I am aware of the complications of the C++ standard wrt doing this.

All that said, I am simply pointing out the various things which come up when dealing with interoperability. I think that starting with @extern(c++) @convention(thiscall) (member functions and pointer to member functions) and @extern(c++) (global variables and free functions) is a great starting point. Extending the support for @abi is something which can be done as a follow up.

To give another option here, picking up on how you've been referring to it: @linkage rather than @extern seems a more descriptive attribute.

Sorry for dropping the post and then disappearing for a week! I forgot how busy a first week of work can be. ;-) But of course what I've come back to (and snooped on during the week) is a bunch of really good points. I'll break up my responses based on topic; this one's about spelling out what we need to support on the C side:

This is a really good point, and I think I agree it means that it'd be better to have something parameterized. After all, there are only three numbers in computer science, and "C and C++" is already more than one even if @objc remains special.

For now, I think the rule is "everything you can see gets generated", since @objc is still illegal without -enable-objc-interop. I don't know how that scales to a module that wants to support two-way or three-way interop, but we don't have to support that right out of the gate.

This is also a good point, but I'm not sure how much it matters if the goal is "expose a Swift decl so it can be called from C". We don't need to support all the Windows calling conventions; we just need to pick one that everyone else supports calling. Still, it's an another con for a plain @cdecl.

Ditto: I don't think we need this level of control. We should pick the default ABI for your target triple and make sure the generated header has it annotated.

The counterargument to this is of course the "plug-in entry point" use case, but until we see one I think we might be able to get away without it.

I don't the idea of doing this at the type level, but it does seem like something we could allow on declarations (in some form). I don't think we need to plan it out now, though, while bool vs. BOOL is immediately relevant.

Ditto (even in simplified form).

This is about C-style plug-ins that dlopen a library and dlsym its entry point. In this case, the entry point's signature is defined in terms of the host's API; an example is libFuzzer:

The first step in using libFuzzer on a library is to implement a fuzz target – a function that accepts an array of bytes and does something interesting with these bytes using the API under test. Like this:

// fuzz_target.cc
extern "C" int LLVMFuzzerTestOneInput(const uint8_t *Data, size_t Size) {
  DoSomethingInterestingWithMyAPI(Data, Size);
  return 0;  // Non-zero return values are reserved for future use.
}

By default, libFuzzer expects your library to export a symbol with exactly the name 'LLVMFuzzerTestOneInput'. People have already been using libFuzzer from Swift via @_cdecl (and @_silgen_name, but that's technically incorrect). There's no custom types involved, but if there were they'd be structs defined in libFuzzer's headers, not on the plug-in side.

2 Likes

@_silgen_name is the elephant in the room. Part of that is right there in the name of the attribute: "SILGen name". @_silgen_name doesn't just change the symbol name of a function; it changes how the compiler itself sees the function, well before it's emitted.* It also does not change the calling convention, but the Swift calling convention is mostly compatible with C's on most platforms and those "mosts" should freak you out.

The other thing @_silgen_name does is let you declare functions that exist without importing a module or header file. That's inherently unsafe, but it's also how Rust calls C functions**, so I don't want to say it's forever off the table. For now, though, it'd be a big shift in how what Swift allows, so I want to set this aside. I really want to set this aside.

I don't think this was ever a thing; please go see if you can remove @_silgen_name!

* For what it's worth, I believe @_cdecl is currently implemented the same way, and I'm not sure we should keep that. I'd rather preserve the Swift name until we get down to LLVM, which is where we've resolved all names and no longer need to look in imported modules.

** Though often these extern blocks are generated by a tool like bindgen rather than written by hand, precisely because it's easy to get it wrong.


Okay, so what about the other aspect: the separation of name control from calling convention? The main thing I'm thinking about here is the goal: call a Swift-defined function from C.

Jens's stub here is clever, and mostly works because we've kept our mangling scheme based on the C identifier character set. I had originally dismissed the idea because I didn't think of a way to do it without language extensions. I would still like a way to control the symbol name too, but maybe that really is separable.

Anyway, the conclusion I'm coming to is that we should have an attribute like @available where there's one required parameter—the language—and then several keyword arguments. That leaves room for expansion in the future.

@extern(c)
@extern(c, name: fookit_setup)
@extern(c++, abi: itanium)

If we follow the convention of @objc, then @extern(c) would generate a C function but use the Swift symbol name, and @extern(c, name: fookit_setup) would actually change the symbol name. That's fairly subtle, though, so maybe we'd want to do something else? (As has been pointed out above, you can always write a wrapper function that has the right C name if you also care about the name in Swift.)

Does the name really need a parameter label, since it's the most common thing? It does if we want to support Objective-C selectors without quotes, and I think quotes are bad here because the compiler does need to validate it a little. Okay, technically you can distinguish them because selectors with arguments always end in a colon, but still.

I'm not the first in the thread to come up with the argument-based attribute, of course; I'm just throwing my weight behind it. Y'all have convinced me. :-)

One thing that's missing here is the @objc @cdecl thing. How do you distinguish between a C function that allows ObjC types from a C function that doesn't? Maybe something like @extern(c, allow: objc)? That does scale to @extern(c, allow: c++), which is a thing that happens sometimes… But if anyone has better suggestions, I'm all ears.

*** You can't do this with Objective-C methods because you need to be able to implement a protocol method in both Objective-C and Swift and have it work from the other language. And it wouldn't work at all with classes.


Finally, the name. I don't love the name extern because I feel like it's more strongly associated with it's "declare but not define" meaning than its "share with other files/libraries/languages" meaning, but it does have massive precedent. My thoughts on the other suggestions in the thread:

  • @linkage isn't a term most people would know, I think. (It means more than just C vs. C++ in C-land, too.)

  • @decl feels very generic (though I appreciate that Swift is just another choice there)

  • @export really sounds like it's related to imports to me.

  • @callableBy feels wordy and doesn't work on things other than functions.

  • @convention(c) doesn't feel like it'll scale to all our uses, and @extern @convention(c) seems needlessly verbose even if @extern also supports a symbol name.


I was hoping I'd come to conclusions at the end of these update posts, but there aren't too many to be had. I'm glad we're having the discussion, though!

P.S. I don't like anyone's names for the alternate -emit-objc-header either. Bleah!

6 Likes

If it starts with @extern(c, ...) I'd expect the function to be visible in a C header. If the thing is only exported to an Objective-C header, I'd rather see it start with @extern(objc, ...). Then add something to say it takes the form of a C function: @extern(objc, format: c)

@exposed(c): starts with "ex" like extern, but without suggesting it was defined externally.

3 Likes

@expose[d] does match the language in documentation about “exposing methods to Objective-C”. It feels a little long to me but it may be the best trade-off we get.

I don’t love @expose(objc) making a C function, but I guess we could say (in the future) “methods on structs and enums become functions, methods on a class become methods”, which would be 99% of the use cases anyway.

1 Like

I would like to express my excitement for this feature. The ability to create not only functions, but types (including function pointers) exposable to C would be great to have.

Would it be possible to create a better support for C macro system? Fox example the ability to wrap C macro with arguments as a functions.

3 Likes

I'm not very familiar with ObjC interop in Swift: to me it has always just been a confusing addition that is kind if implicitly there – as you mentioned in a post above, @_cdecl on Darwin platforms appears to always imply @objc anyway.

In my opinion we should remove that confusion and have @extern(c) only do one consistent thing across all platforms (i.e. only set linkage and calling convention). I think the way you wrote this originally (leaving the two attributes separate) is the right thing to do. @objc @extern(c) seems like the right mix of keeping things as they were while keeping attributes orthogonal.

For the record I am fine with the idea of not demangling symbol names by default in @extern(c) so long as we still can define them ourselves (via @extern(c, name: "...") would be fine).

One thing this discussion is missing is forward declaration of external symbols. That is possible via @_silgen_name but AFAIK there's no way to specify a forward declaration made in that way as needing, for example, the C calling convention – which is something we need very often.

2 Likes

Having the ability to expose structs and static instances of C structs would enable Swift libs to expose CPython modules directly.

2 Likes

As mentioned that’s very deliberate: besides being something that’s easy to separate from the “defining” side, it’s something that I expect to be a lot more controversial.

That’s about the other direction, C-into-Swift, and it’s a hard problem because most macros give no indication at all what types it expects. I don’t know if there’s ever going to be a better answer than “write a wrapper function in C”.

4 Likes

Fair enough. For now we will continue using @_silgen_name and hoping the calling convention stays "compatible enough".

I am also aware we could create and import a C module that exposes these, so it's not really a blocker for us. I guess the real issue I have here is that creating and importing a C module currently is not as straightforward as it could be. If that was easier at a tooling level I personally could drop the requirement for forward declarations of C functions altogether.

1 Like

You should never use @_silgen_name. The calling convention is already fundamentally incompatible on some targets (wasm, and likely arm64e on Apple platforms when it eventually stabilizes). You can forward declare C entry points by importing a header that contains the C declaration.

3 Likes

I have been using this annotation for a couple of years.

@_cdecl("LIBHelloWorld")
func libHelloWorld() {
  World.hello()
}

private class World {
  static func hello() {
    print("Hello World!")
  }
}

Passing parameters also works well.

Following this forum thread, it's not clear to me the status of this proposal, or if I can help out.
In my daily workflow, this code pattern is seen again and again.

I am worried that @_cdecl could be removed from the language without warning. Is that possible, or would someone reply here before doing that?

I mean, that’s what the underscore name means, right?

2 Likes

Yes, the underscore means the annotation is unofficial.

I will rephrase the question.

What would give me a heads up that @_cdecl is being removed from the language? I assume that subscribing to this thread is the best thing, but maybe there is something else.

Hi Joe, a couple of times in this thread you speak about "importing a header". Is there a way to do this that doesn't involve creating a clang modulemap which imports the header? Because that works, but it always feels like jumping through hoops – creating multiple files, targets, build rules, to expose just one function, whose body should be defined in Swift anyway.

We are experiencing sporadic crashes calling functions exposed to the JNI via @_cdecl and we're looking for a "more official" way of doing it.

If you were to expose a function to C today in a non-broken way, how would you do it?

  1. Create thing.h which declares (in C syntax) that MyFunction exists
  2. Create a module map that imports thing.h
  3. Somehow import that module (SwiftPM, Xcode Build Settings, etc.)
  4. In the Swift code, something something (???) – how do we declare the function in Swift, and if we do, what was the point of importing the header?

I don't mean to derail the conversation with this. I am genuinely interested in finding out whether a use-case that I still somehow consider essential to this thread's (pre-)proposal can be achieved otherwise with reasonable effort.

1 Like

If you're defining the functions in Swift, then @_cdecl should be fine. If it's crashing, then there's either a bug in the compiler, or a calling convention mismatch between what Swift and LLVM are doing and what the JNI expects. You would use headers and modulemaps to declare external C functions to be imported into Swift.

3 Likes