C reverse interoperabiity

I’m working on a project that is going to make heavy use of C interoperability (that is, using C functions from swift) to pull in libraries and functions already in use by the first version of the code base. I believe I’ve got a fairly good handle on how this works, but if anyone has any resources that give clear information on how to go about this let me know; more information is always helpful.

However, I also know that I will eventually need the reverse; that is, the ability to call into a swift library from C. Is there any facility / faculty in swift to do so? I’m happy with any solution, even if it requires building the calling code with LLVM for compatibility / a wrapper / special environment, etc. I’m planning to use SPM for building the project so solutions that keep that in mind are preferred.

Thanks in advance for any help!

2 Likes

There isn't a finalized design for this, but the compiler does implement an attribute @_cdecl("name"), which can be applied to global function definitions to export them as C functions with the given symbol name:

@_cdecl("my_add") // export to C as intptr_t my_add(intptr_t, intptr_t);
public func add(x: Int, y: Int) -> Int {
  return x + y
}
4 Likes

How did you determine that signatur? In Project-Swift.h is appears as

 NSInteger my_add(NSInteger x, NSInteger y) SWIFT_WARN_UNUSED_RESULT;

which is the same since “intptr_t == NSInteger” on all platforms, unless I am mistaken. I am just curious how you came up with intptr_t.

1 Like

It maps Swift types to their corresponding C/ObjC types by the same mechanism as ObjC bridging. Since Swift's Int is always pointer-sized, it uses a C typedef that is in turn always pointer-sized. I guess that's NSInteger on Apple platforms, but it ought to use something more portable for other platforms where Foundation isn't always available.

Is there some way to determine the C signature of the exported function? In Xcode I can “Jump to Generated Interface” but what about Linux?

You can pass swiftc the -emit-objc-header-path path/to/Project-Swift.h flag to have it generate the header file for definitions exported to C/ObjC, like Xcode would.

1 Like

@Joe_Groff We can pass Swift's String to a UnsafePointer<UInt8> of an imported C function. Do you mean a Swift function with an String argument will be imported in C as char*?

The bridging mapping for C is currently the same as ObjC, so a Swift function that takes or returns String will be presented to C as one that takes and returns NSString * (requiring Foundation in the process). If we were going to make @_cdecl a proper language feature, that seems like behavior that'd be worthy for debate, but for now, if you want a Swift function to present in C as taking a char*, you'll need to define it in terms of UnsafePointer<CChar> in Swift.

2 Likes

Does @_cdecl work in Linux? @Joe_Groff thanks for the -emit-objc-header-path flag, that's very helpful and from what I can tell not documented anywhere in particular (or I'm not looking in the right places). How did you know that / figure that out in case its the latter :wink: ?

I cheated and checked the compiler source. @_cdecl should work on Linux, though note that its bridging behavior with Swift containers is still Foundation-centric, so things like Array and String will try to bridge to NSArray and NSString instead of to pointers as you'd probably expect in vanilla C. You can use scalar numeric and pointer types as you'd expect in C though.

3 Likes

I do this via function pointers, taking advantage of two unrelated features that combine nicely:

  • Swift has a fully-supported mechanism for defining a Swift function that uses C calling conventions. This is commonly used for supplying Swift callbacks to C functions that take a function pointer.

  • In C, you can call a global variable function pointer using the same syntax as a function.

So, for example, in my C code I declare this:

int (*Add)(int, int);

In my Swift code I set it up like this:

Add = { return $0 + $1 }

Back in my C code I call it like this:

int result = Add(1, 2);

Share and Enjoy

Quinn “The Eskimo!”
Apple Developer Relations, Developer Technical Support, Core OS/Hardware
let myEmail = "eskimo" + "1" + "@apple.com"

7 Likes

Hello Quinn. Are you able to expand a bit on this please and provide a concrete example? Particularly on the Swift part, as it doesn't compile and I assume you wrote it as pseudo-code for those more familiar with interoperability.

Thanks,
Sergiu

as it doesn't compile

Hmmm, which bit doesn’t compile?

I testing this this here in my office (Xcode 13.1 on macOS 11.6) and it works a treat:

  1. I created a new project from the macOS > Command Line Tool target, choosing Swift as the language.

  2. In that target, I created a new Temp Objective-C class, just to set up the bridging header.

  3. I then deleted Hack.{h,m} from the project because we’re doing C, not Objective-C.

  4. I created a new Adder.c file, with an associated header.

  5. I set the header to this:

    #ifndef Adder_h
    #define Adder_h
    
    #include <stddef.h>
    
    extern int (*Add)(int, int);
    
    extern int AddSomeNumbers(const int * numbers, size_t numberCount);
    
    #endif /* Adder_h */
    
  6. I set the implementation to this:

    #include "Adder.h"
    
    #include <assert.h>
    
    int (*Add)(int, int);
    
    extern int AddSomeNumbers(const int * numbers, size_t numberCount) {
        assert(numbers != NULL);
        assert(Add != NULL);
        int result = 0;
        for (size_t i = 0; i < numberCount; i++) {
            result = Add(result, numbers[i]);
        }
        return result;
    }
    
  7. I set the bridging header to this:

    #include "Adder.h"
    
  8. I set main.swift to this:

    import Foundation
    
    func main() {
        Add = { return $0 + $1 }
        let numbers: [CInt] = [1, 2, 3, 4, 5]
        let result = AddSomeNumbers(numbers, numbers.count)
        print(result)   // prints: 15
    }
    
    main()
    
  9. I ran the project. It printed 15, as you’d expect.

Share and Enjoy

Quinn “The Eskimo!” @ DTS @ Apple

1 Like

I guess you made it implicit in your previous answer that you have to add the C header into the Bridging header to expose it to Swift, which my mind couldn't deduce))

Thanks Quinn. Cheers.