Creating a C-accessible shared library in Swift?

Hello everyone,
I'm working on a fairly large project that is implemented as a shared library - it's going to be used by lots of different consumers, each written in different languages.

The lowest common denominator here is C. I'd prefer to write as much of the code as possible in Swift, but at the end of the day, it needs to be compiled down to a shared library that any code written in C can call.

A good way of thinking about what I'm trying to accomplish: I am creating a data processing library that will eventually need to be callable from Python, C, Lua, etc, and I would like to write said library in Swift. Is such a thing possible?

(Please note this is not the same thing as calling C from Swift, nor is it the same thing as passing a Swift function to a C function that requires a callback.)

(This would also be used on not just Apple platforms, but Linux and Windows as well.)

Thank you!

It's possible, but it isn't straightforward. @_cdecl will let you create functions that have C symbols, and you can manually write header files that correspond with those definitions. However, controlling symbol visibility isn't necessarily easy.

Thanks for the feedback.

I've looked into _cdecl, but it seems to suffer from two major problems: it's not standardized or even officially supported, and it doesn't allow passing of Swift objects back into C to be held there. For example, if I create a handle in Swift, I have to pass it to the caller, who holds on to it. It doesn't seem possible to pass any sort of reference back to C that way.

Perhaps I'm wrong, though. Thanks again.

You will need to define any types you pass across this interface in a .h file and import them into swift from that header. So any reference like you describe will have to be packaged in a way that C can ingest.

You can absolutely do that, but all your types will have to be opaque to C: that is, they will need to be allocated by Swift and passed by pointer. This is because C cannot unpack fields in types declared in Swift.

As an example, you could have mylib.h:

typedef struct mytype mytype_t;

mytype_t *mytype_create(void);
void mytype_destroy(mytype_t *);
int mytype_get_count(mytype_t *);

In Swift, you would then define:

final class MyType {
    var count: Int = 55
}

@_cdecl("mytype_create")
public func mytype_create() -> OpaquePointer {
    let type = MyType()
    let retained = Unmanaged.passRetained(type).toOpaque()
    return OpaquePointer(retained)
}

@_cdecl("mytype_get_count")
public func mytype_create(_ type: OpaquePointer) -> CInt {
    let type = Unmanaged<MyType>.fromOpaque(UnsafeRawPointer(type)).takeUnretainedValue()
    return CInt(type.count)
}

@_cdecl("mytype_destroy")
public func mytype_destroy(_ type: OpaquePointer) {
    _ = Unmanaged<MyType>.fromOpaque(UnsafeRawPointer(type)).takeRetainedValue()
}

You can then use those functions from C accordingly. This is the only way C can safely hold the Swift types, as C is unaware of how Swift lays things out. Fortunately, this is also largely the recommended programming model for ABI-stable C libraries anyway, so most C programmers will be reasonably familiar with the approach in question.

6 Likes

That's very informative, thank you!

Oh, I should add: this is going to be tricky to consistently get right. Unlike writing the equivalent library in C, nothing here will tell you when the ABI you've declared in your header file doesn't match the ABI you've declared in Swift, and scary nastiness can and will ensue. Ideally you'd use a tool that is capable of generating these interface functions from your header files, or alternatively that can generate the header files from the Swift functions, to reduce the risk that you'll have truly awkward to debug problems down the road.

2 Likes

Swift's header generation supports @_cdecl entry points so should be able to do this for you.

3 Likes

Huh, I didn’t know Swift could do this. How do you ask swiftc to generate the header file?

1 Like

It looks like you pass -emit-objc-header-path header.h to swiftc. Xcode normally does this automatically in mixed Swift/ObjC projects, so swiftpm might have a way to do it as well.

1 Like

Oh neat, I didn't know this existed, very handy! It's not flawless, but it seems like it's very useful.