swift-bridge 0.1.37 supports passing `Box<dyn FnOnce(A, B) -> C>` from Rust to Swift

43 points chinedufn

As of the latest release of swift-bridge is is now possible to pass boxed functions from Rust -> Swift. The boxed function shows up on the Swift side as Swift's native function type, and gets de-allocated after it is called or dropped on the Swift side.

https://github.com/chinedufn/swift-bridge/pull/110


Boxed function support is useful for people writing applications on Apple hardware that need their Rust code to be able to interface with callback-based asynchronous Swift code.

One example is if you're building a Rust application on iOS that needs to interact with the Stripe Terminal SDK's Swift APIs.

Before swift-bridge 0.1.37 you'd need to define opaque Rust types such as CollectCardPaymentCallback(Box<dyn FnOnce(Result<PaymentIntent, String>)>) in order to send over a callback that the Swift side that could be called.

This was cumbersome since you needed to bridge a type for every callback that your application needed.

Now, instead, you can do something like:

#[swift_bridge::bridge]
mod ffi {
    extern "Swift" {
        type StripeTerminalBridgedSingleton;

        #[swift_bridge(init)]
        fn new() -> StripeTerminalBridgedSingleton;

        fn retrieve_payment_intent(
            &self,
            client_secret: &str,
            callback: Box<dyn FnOnce(PaymentIntentWrapper, String)>,
        );

        fn collect_payment_method(
            &self,
            payment_intent: PaymentIntentWrapper,
            callback: Box<dyn FnOnce(PaymentIntentWrapper, String)>,
        );

        fn process_payment(
            &self,
            payment_intent: PaymentIntentWrapper,
            callback: Box<dyn FnOnce(PaymentIntentWrapper, ProcessPaymentError)>,
        );
    }

    // ...
}

In general, when bridging Rust types to Swift we try to preserve as many of Rusty invariants as we can.

For example, if you pass a &SomeRustType to Swift it is not possible for Swift to call any of SomeRustType's mutable methods. This is enforced at compile time by taking advantage of Swift's class inheritence https://chinedufn.github.io/swift-bridge/bridge-module/opaque-types/index.html#owned-ref-and-refmut.

In the case of bridging FnOnce functions we wanted to guarantee that Swift could only call the function 0 or 1 times.

This is currently enforced at runtime by having Swift raise a fatal error if you call the callback more than once.

Let me know if you know of a good way to enforce this at compile time.


If you're curious about how the generated code looks you can see examples here https://github.com/chinedufn/swift-bridge/blob/2ad1757cc3858f03adf52aef7c8734157900bb3c/crates/swift-bridge-ir/src/codegen/codegen_tests/boxed_fnonce_tests.rs#L22


If you're interested in running Rust on iOS or other Apple hardware there are plenty of ways that you could contribute to swift-bridge's code generation and ergonomics.
I'd be more than happy to create some good first issues for you to hack on!


Cheers!