Open Gankra opened 1 month ago
Dumping some thoughts here in case you or someone else ever gets to this:
As of Swift 5.10, Swift files can't formally declare structs with C-compatible layouts. However, in practice tuples have nearly-C-compatible layouts, and since tuples are part of the stable ABI on macOS and iOS, that's unlikely to change even if general struct layout changes. The "nearly" comes because Swift does not automatically tail-pad structures (unlike C and Rust, but kind of like C++ base classes), instead choosing to make a size/stride distinction. If a struct's size is a multiple of its alignment already, however, this shouldn't be a problem.
Still, the currently-recommended approach for this is to declare a C header and import it. But there's a bunch more scaffolding needed for that than just calling swiftc (two generated input files??). Arguably this is also a different test, but possibly the more useful one since it's the one that's actually supported.
Swift does not have fixed-sized arrays yet. Imitating this with a manually-expanded tuple is equivalent in all cases where size == stride. (It's unclear whether a native Swift array would have tail padding on its last element, because Swift does not have fixed-sized arrays yet.)
There's one other exception to this: unlike Rust, in Swift a zero-sized type always has stride = 1. This includes Void/()
. So ((), (), (), ())
has total size 0 and stride 1, but the buffer inside [(), (), (), ()]
(a heap-allocated array in Swift, closer to Vec) is 4 bytes long.
Swift does not have a way to define unions, or explicitly set alignment, or declare an opaque type. I can't think of good hacks for any of those.
C-like enums are supported, you just have to mark them @objc
and give them an underlying type. There's no direct equivalent to repr(C)
, but in practice the underlying type CInt
ought to be equivalent to repr(C)
as long as the enum values don't exceed the limits of CInt. (This is based on how C++ picks the underlying type for its equivalent feature.)
Swift generic functions are not monomorphized, but generic types effectively are. (Which you could probably figure out for yourself, since otherwise Optional<UnsafeRawPointer>
couldn't be a single pointer.) This does mean you can't get a C-compatible function pointer from a generic function.
The unstable @_cdecl
attribute is how you mark a function as C-compatible in Swift 5.10. However, this also restricts the argument types to what the Swift compiler knows how to express in C, so if you want to do anything with generics or custom structs, you have to go through an opaque UnsafeRawPointer or something.
Swift doesn't have references. Don't try to use the ownership stuff as equivalent to &T
, it doesn't map 1:1 with ABI like that. Just use UnsafePointer<T>
and refer to .pointee
or [0]
everywhere it's used.
In that vein, remember that Swift pointer types aren't nullable. abi-cafe's ptr
should be Unsafe[Mutable]RawPointer?
, with the trailing ?
being syntactic sugar for Optional.
All Swift structs are effectively repr-transparent. I don't think there's an equivalent to C's "wrapping this in a struct causes the ABI to change", but also who ordered that anyway?
As of #39 we've made a pretty significant shift in the """semantics""" of KDLScript to make it more useful for testing non-C-ABIs and layouts. We now take a single type declaration and use it to test both C reprs and Rust reprs, as well as C conventions and Rust conventions. (this is useful for i.e. cranelift which should ideally be ABI-compatible with the llvm-based rustc backend).
Basically at this point it's basically giving you the shape of types and interfaces, and the harness is free to take that shape and apply modifiers like "...with $LANG-style repr" or "...with $LANG calling convention".
So yeah we could have a repr(Swift) and Convention::Swift which reflect what happens when you just Write Some Normal Swift Code and ask it to generate two (swift?)staticlibs that get linked into one (swift?)dylib. And if anyone ever has a wild project to add Swift interop to Rust or whatever then hey we're ready to deal with that!
It's interesting to hear that the header importer is still the only decent way to ask Swift to give things C-style layout, but we could maybe handle that..? I was sure one day we'd need to deal with a staticlib needing multiple source files, but adding that is still gonna be a pain in the ass.
There's one other exception to this: unlike Rust, in Swift a zero-sized type always has stride = 1. This includes Void/(). So ((), (), (), ()) has total size 0 and stride 1, but the buffer inside [(), (), (), ()] (a heap-allocated array in Swift, closer to Vec) is 4 bytes long.
FWIW this kind of thing is actually awesome/great, for abi-cafe to slam face first into without adjustment. The original problem statement of the tool was to demonstrate these kinds of mismatches for decision-makers! It's still an open question as to what level we should mark these kinds of issues as "known bad" on (as we ran into with the new f16/f128 support), but I think it's broadly desirable to be able to ask for them and have them blow up.
I'm not sure how much work this is but it sure would be neat!