Passing std::function to C++ from Swift

I'm trying to call a function that expects a std::function in C++, and I'd like to provide it a Swift closure.

Autocomplete seems to recognize what I'm doing and offers to cover the std::function argument into a trailing closure. But I can't find any configuration that actually compiles.

In the trailing closure case, I get:

Trailing closure passed to parameter of type 'XXX' (aka 'std.__1.function<((XXX, XXX) -> XXX)>') that does not accept a closure

I've also noticed despite the autocomplete working, in the C++/Swift interop guide std::function is listed as not supported.

Is that doc up to date? Is there a way to provide a closure to std::function?

3 Likes

I'm wondering the same thing. I expected an initializer that accepts a Swift closure or something similar. The only completion suggestion in Xcode is an initializer without parameters, which compiles but doesn't really help.

Since Swift lambdas and C++ std::functions are fundamentally different, the best way I have found so far to bridge between is to use C-style functions that carry their closure (context) as void*.

So;

  1. Create a class in Swift that wraps the closure. Give it a call(..) func which internally calls the closure
  2. Create a new instance of that Swift class with your desired Swift closure
  3. Convert that instance to an Unmanaged & toOpaque() (with a retain)
  4. Create a static C-style Swift function that takes an opaque pointer as a first arg, and optionally any other args as well - call it callClosure or something
  5. Create a second C-style Swift function that takes an opaque pointer and unwraps it using Unmanaged with a retained take and release. Call it destroyClosure or something
  6. Finally, create a C++ function that takes three args: void* context, void(*callClosure)(void* /* context */, and void(*destroy)(void* /* context */. This will create a shared_ptr of the context so it's ref-counted (and destroy() is the custom deleter), and then return an std::function in which you call the callClosure() func with context as it's first argument.

See my implementation in Nitro: nitro/packages/react-native-nitro-modules/ios/utils/SwiftClosure.hpp at main · mrousavy/nitro · GitHub

2 Likes

I did face two issues with my above approach though:

  1. All C++ types passed to the C-style Swift function (callClosure(...)) will be double-freed - so their destructor is called twice. For primitives like double this is not a problem, but anything that has a complex destructor (eg std::shared_ptr), this will cause memory corruption issues. I filed a but report here: Passing a `std::shared_ptr<...>` from C++ to a C-style Swift function crashes: `malloc: Heap corruption detected ***` · Issue #78292 · swiftlang/swift · GitHub
  2. Because it's a C-style function pointer, you can't seem to use borrowing or inout as far as I can see. So everything is copied. This is also annoying if you do shared_ptr.pointee as this copies the pointee everytime.

I ended up adding a simple C++ function that accepts a Clang block and captures and evaluates the block from within the std::function. I'm not sure how well it works with more complex types, but it seems to do the job in my case.

2 Likes

Clang already has a conversion from blocks to std::function (they are “callable” in the C++ sense, after all), so you may be able to remove a level of wrapping here. But the thread’s larger point about providing nice interop should stand.

2 Likes
  1. C/Objective-C blocks won't support C++ types as arguments or return values afaik. You can only use C or Objective-C types. (didn't double check this tho!)
  2. A C/Objective-C block cannot be automatically converted to an std::function in my tests. I always need to capture it and call it. But I might have done something wrong (it was a top-level inline function that did this for a void() block)
  1. It works for me, at least with std::vector types.
  2. This actually seems to work for me, too. So all I need now is a very thin function that takes a block and forwards it to the original function that takes a std::function. Not too bad.
1 Like

Interesting - I'll give this another go then...