Async with C interop

So here's my use-case. For an cross-platform mobile app I want to use Rust for some shared logic, as well as I/O. For this I would like to be able to do some async stuff and then Notify the Swift code, that data is available.

Let's assume I have such a method called by Swift:

pub extern fn get_data(callback: fn(*mut c_char) -> ()) -> ()

The method should call an async method and call then callback with the data. How can this be achieved.
I tried to manually create an Tokio runtime and preventing the runtime to be dropped by haven't succeeded so far:

let mut rt = Runtime::new()?;

let handle = rt.spawn(run(callback));

unsafe {
    let unsafeRt = Box::new(rt);
    Box::into_raw(unsafeRt);

    Box::into_raw(Box::new(handle));
}

Does anyone have an idea?

1 Like

Can you give a few more details? Who calls who? I'm guessing swift calls the callback when the data has been received? Do you want to make a future that completes once the callback is called by swift, or do you want to notify swift when data loaded by Rust is ready? What's in that char pointer?

Swift would call the Rust function get_data. Since get_data takes long, it should not block the thread and instead do the work in the background and call a callback with the result when it's done.
For simplicity let's imagine the result is a String. To pass a String to Swift you need to pass a * c_char.

I managed to implement a callback function like this:

pub extern fn get_data(callback: fn(*mut c_char) -> ()) -> () {
     let value = CString::new(String::from("my value")).unwrap().into_raw()
     (callback)(value);
}

And the string is perfectly passed to Swift.

However when adding asynchronity, it looks like the async task is not executed.

This is my async function:

async fn run(callback: fn(*mut c_char) -> ()) -> Result<(), reqwest::Error> {
    let body = reqwest::get("https://www.rust-lang.org")
         .await?
         .text()
         .await?;

    let result = CString::new(body).unwrap().into_raw();
    (callback)(result);

    Ok(())
}

And this my attempt to call it:

pub extern fn get_data(callback: fn(*mut c_char) -> ()) -> () {
    let mut rt = Runtime::new()?;

    let handle = rt.spawn(run(callback));

    unsafe {
        let unsafeRt = Box::new(rt);
        Box::into_raw(unsafeRt);
        Box::into_raw(Box::new(handle));
    }
    Ok(())

But now the callback is never called.

1 Like

Your specific problem here is that the runtime gets dropped when get_data returns. That will cancel all tasks. You can around that by having a global runtime which you initialize earlier and reference inside the get_data call.

However the overall problem is hard, and there are several other things that need to be taken into account:

  • Memory management and thread safety. Rust will call back the provided callback at any time and from any kind of thread. It's not from any Swift [UI] thread, in case you need that. So you need to be prepared to handle this.
  • If Rust/tokio calls back into your Swift code, make sure you don't call back into Rust from this. This has the chance to produce reentrancy and mutabiltiy issues which Rust is not able to check - since it doesn't know what you are doing inside the callback.

@Matthias247 has correctly pointed out several issues here. I'll add a concrete suggestion that might help you make progress. A common pattern in C libraries is something like this:

something_new() -> pointer  // init a resource
something_foo(Something*)  // use the resource
something_free(Something*)  // free the resource

You are creating a resource, probably on the heap (i.e. in a Box), using it, and freeing it. The user of the library is the effective owner of this memory, responsible for making sure these things are called at the right times in the right order.

You could use this pattern to create your Runtime and any other long-lasting data in advance. Then you can pass a pointer to your Box<Runtime> or Box<ContextStructContainingRuntime> as a parameter to your get_data function.

Until recently you would accomplish this with Box::into_raw and Box::from_raw but it looks like this pattern is much simpler in Rust v1.41. (I haven't tried this yet.)

3 Likes

@Matthias247 @thombles thank you for your advices. It turned out that I already prevented the Runtime to be dropped (by producing an memory leak tough).
However the problem was that on the I didn't use the rt-threaded feature flag. My current understanding is, that without rt-threaded you are using the basic scheduler. And it seems that the basic scheduler only executes it's tasks when someone calls block_on.

2 Likes

This topic was automatically closed 90 days after the last reply. New replies are no longer allowed.