Simple way to expose cancelable work implemented in rust to Swift/c

I'm new to rust.

I'm trying to wrap existing rust code so that I can call it from Swift. I have this working for simple rust functions using cbindgen. But the rust code that I want to wrap does work on a background thread and sometimes I would like to cancel that work before it completes.

Here's a simplified example of the rust function that I'd like to call:

// Created from copy/paste on internet. Seems to work for me.
// Let me know if there's improvements to be made.
use std::thread;
use std::sync::Arc;
use std::sync::atomic::{AtomicBool, Ordering};

fn schedule_do_work(work_was_done_callback: Box<Fn(i32) + Send>) -> impl Fn() {
    let cancel = Arc::new(AtomicBool::new(false));
    let threads_cancel = cancel.clone();

    thread::spawn(move || {
        let result_of_work = 42;        
        if !threads_cancel.load(Ordering::SeqCst) {
            (work_was_done_callback)(result_of_work);
        }
    });

    move || {
        cancel.store(true, Ordering::SeqCst);
    }
}

The function:

  1. accepts a callback.
  2. does work in background thread, calls the callback with result of work
  3. returns a function that can be used to cancel the work.

The trouble is I can't figure how to expose it through ffi so that I can call it from Swift/c. In particular I don't know how to map the incoming callback closure, or the returned "cancel" closure. Though web searches and documentation I sort of understand what some of the issues are... that passing closures between rust and c requires splitting out the function and captured environment into two values.

But after working on it all afternoon I can't figure out how to make it work.

Any tips on what I need to do next? I'm happy to start over by redesigning my approach in the schedule_do_work if needed. I've also looked at an approached based on Using Rust objects from other languages and that helps with the "cancel" part of the equation. But I'm still left dealing with closures when trying to notify the Swift/c code that work is completed.

// This code is just a sketch of how I'm trying to wrap the function in ffi
// Doesn't compile, probably all wrong! :slight_smile: 
use std::os::raw::c_void;

pub type Callback = fn(size: i32, context: c_void);

pub struct Cancelable {
    callback: Box<Fn()>,
    context: c_void,
}

#[no_mangle]
pub extern fn schedule_do_work_from_c(callback: Callback, context: c_void) -> Cancelable {
  let cancelable = schedule_do_work(Box::new(move |value| {
    callback(value, context);
  }));
  return cancelable;
}

Thanks for any help!

I think you can get away without using closures for cancellation?

Something like this might work:

struct CancelationToken(Arc<AtomicBool>);

extern "C" cancel(token: &CancelationToken) {
    token.0.store(true, SeqCst);
}

Ahh, thanks! I think that will work. Haven't actually run code, but seems to get me closer.

I now have:

use std::os::raw::c_void;

pub type Callback = fn(size: i32, context: *mut c_void);
pub struct CancelationToken(Arc<AtomicBool>);

#[no_mangle]
pub extern fn schedule_do_work_from_c(callback: Callback, context: *mut c_void) -> CancelationToken {
    CancelationToken(schedule_do_work(Box::new(move |value| {
        callback(value, context);
    })))
}

#[no_mangle]
pub extern fn cancel(token: &CancelationToken) {
    token.0.store(true, Ordering::SeqCst);
}

And see error:

It works if I just call callback(value, context); in the outer block, not in the callback. It also works if I make context into some primitive value such as an i32. I'm not sure how to make the callback work from the callback while passing a * but c_void ... of if that's even the type I should be using.

I had this exact same use case when doing GUI stuff at work. We wanted the GUI (not written in Rust) to be able to spawn a Rust job in the background, allowing us to either cancel it midway or retrieve the result at the end.

After several failed attempts, we came up a set of abstractions around a Task trait as well as a macro which will generate all the necessary C bindings. It's published as part of the ffi_helpers crate on crates.io.

1 Like

Thanks for sharing.

Are you able to use it together with cbindgen? By default cbindgen runs before macro expansion. When I enable macro expansion in cbindgen the build process seems to hang. The cbindgen config mentions that clean needs to be set sometimes when expanding macros... I think I'm doing that below, but still it's hanging.

extern crate cbindgen;

use std::env;
use cbindgen::Config;
use cbindgen::ParseConfig;

fn main() {
    let crate_dir = env::var("CARGO_MANIFEST_DIR").unwrap();
    let expand_crate = ["client"];

    let config = Config {
        parse: ParseConfig {
            clean: true,
            ..Default::default()
        },
        ..Default::default()
    };

    cbindgen::Builder::new()
      .with_config(config)
      .with_crate(crate_dir)
      .with_language(cbindgen::Language::C)
      .with_parse_expand(&expand_crate)
      .generate()
      .expect("Unable to generate bindings")
      .write_to_file("client.h");
}