Constrain type to "representible in C" for use with @convention(c) and generics?

The following code:

func test<each Input, Output>(_ closure: @convention(c) (repeat each Input) -> Output) {}

Fails with the following error:

error: '(repeat each Input) -> Output' is not representable in Objective-C, so it cannot be used with '@convention(c)'

Which is expected because there is no guarantee that each Input and Output are representible in C. The message mentions Objective-C, which in my case is wrong because I'm running Swift on Windows but it doesn't matter, I imagine it's a leftover error message from macOS.

Is there a way to make this work? Is there any protocol akin to AnyObject but for types representible in C, that I could use to constrain my generic parameters? Or another workaround?

I'm already using unsafeBitCast in my actual code here so I don't mind any unsafe solution or workaround.

1 Like

Since your test() function can be called with packs of arbitrary size and elements at run time, there's no really no way to represent what you're trying to do, short of JIT compilation that looks at the types stored inside the pack at runtime and then generates a @convention(c) stub to wrap your closure.

The reason is that on modern architectures, a C function like int(int, int) or int(float, int, char*) or whatever is fundamentally not the same as a varargs function int(...); fixed parameters are passed in registers, while varargs are passed on the stack, so you'd have the same difficulty if you were working in C, and tried to come up with a way to call a completely arbitrary function pointer with a given list of dynamic values.

Even without packs, if we wanted to allow something like this,

func test<T: RepresentableInC>(fn: @convention(c) (T) -> ()) {}

then T can be an integer, or a floating point value, or a SIMD vector, or a struct passed in registers, or a larger struct passed by indirect value, etc, so the calling convention of (T) -> () would need to depend dynamically on the underlying C type of T.

Do you actually need to dynamically form arbitrary C function pointer types, or would it suffice to pre-generate a handful of cases?

1 Like

Thanks for your answer!

To give a little bit of context, I'm toying with AngelScript to bridge it to Swift. I've tried using Cxx interop but since it relies on virtual methods I wasn't able to go anywhere with Cxx for now, so I use the C interface.

I am trying to pass a @convention(c) function pointer to AngelScript, which expects a asFUNCTION_t declared as such: typedef void (*asFUNCTION_t)().

Since everything is dynamic, when registering a function you have to specify :

  • the calling convention (cdecl in my case)
  • the function parameters and their types
  • the function return type
  • the function pointer (as asFUNCTION_t)

The AS VM then does everything dynamically, depending on the calling convention specified when the function is registered. I have no idea how it handles passing parameters in registers or in the stack as you mentioned, but the "cdecl" calling convention seems well documented so I trust that @convention(c) from Swift is the same as "cdecl" from AngelScript.

I am able to use unsafeBitCast(closure, to: asFUNCTION_t.self) to convert from a @convention(c) closure to the "erased" function pointer that AS expects, and everything works.

However I would like to make it a little bit safer on the Swift side, because right now the closure signature does not match anything I give to AngelScript (it's unsafe by definition). For example I can forget the @convention(c) of the closure I give it, or give AS the wrong parameter or return types. Using parameter packs would have been a good starting point.

I guess I could use a macro to wrap the Swift closure inside a struct containing the C closure, as well as corresponding AngelScript metadata, generated at compile time?

And to answer your question, no, I can't generate a handful of cases because I won't know in advance what I will be registering to and from AngelScript.