Brendonovich / swift-rs

Call Swift functions from Rust with ease!
Apache License 2.0
246 stars 28 forks source link

Asynchronous functions / callback support #31

Open caoimhebyrne opened 1 year ago

caoimhebyrne commented 1 year ago

It would be amazing if we could call asynchronous Swift functions from Rust.

The only problem is: _@cdecl doesn't allow us to mark our functions as asynchronous:

src-swift/lib.swift

@_cdecl("my_async_fn") // ERROR: _@cdecl global function cannot be asynchronous
func myAsyncFunc() async {
    print("Hello, world!")
}

Since that's not possible, it would be nice if we could have better support for closures:

src-swift/lib.swift

@_cdecl("my_closure_func")
func myClosureFunc(closure: @escaping @convention(c) () -> Void) {
    closure()
}

src/main.rs

use swift_rs::{swift, SRClosure};

swift!(pub(crate) fn my_closure_func(SRClosure<Void>));

fn main() {
    let closure: SRClosure<Void> = || {
        println!("Hello!!");
    }.into();

    my_closure_func(closure);
}

The Rust code above is just a proof-of-concept, and I'm not sure if everything I described there is possible, since we can't just pass a pointer to the callback around (from my understanding at least). We may have to implement something like this C-callbacks example from the Rustonomicon (with some sort of Swift code generator?)

caoimhebyrne commented 1 year ago

For some extra context, I implemented a workaround in one of my own crates for calling methods with closures, but it's messy and not ideal:

@_cdecl("my_func")
func myFunc() -> String {
    let semaphore = DispatchSemaphore(value: 0)
    var returnValue = ""

    // getStringValue is not real, but has a signature something like this:
    // func getStringValue(closure: @escaping (String) -> Void)
    getStringValue() { (value) in   
        returnValue = value
        semaphore.signal()
    }

    semaphore.wait()
    return value
}

Being able to use closures in Rust directly, or even better, do some magic to allow asynchronous functions, would be better than this approach.

Brendonovich commented 1 year ago

since we can't just pass a pointer to the callback around

I think you'd be surprised :wink:

SRClosure is a really smart idea and may actually work, since @lucasfernog already did something similar for Tauri

type PluginMessageCallbackFn = unsafe extern "C" fn(c_int, c_int, *const c_char);
pub struct PluginMessageCallback(pub PluginMessageCallbackFn);

impl<'a> SwiftArg<'a> for PluginMessageCallback {
  type ArgType = PluginMessageCallbackFn;

  unsafe fn as_arg(&'a self) -> Self::ArgType {
    self.0
  }
}

I could even try an SRFuture that gets instantiated with an async block in Swift and converts to a Future in Rust, but I'm not 100% confident about that. In theory it'd just be a matter of handling success + failure closures.

Duplicate of #29, but I'll keep this open since it's more detailed.

caoimhebyrne commented 1 year ago

I could even try an SRFuture that gets instantiated with an async block in Swift and converts to a Future in Rust, but I'm not 100% confident about that. In theory it'd just be a matter of handling success + failure closures.

That would be amazing, I would love to contribute but this level of Rust FFI is out of my league :P