"C" callback from Swift: "does not live long enough"

I have created a simple API to test networking on Rust and trying to use in the IOS project.
It is working fine in Rust environment.
Extern function:

//Works fine
#[no_mangle]
pub extern "C" fn create_swapi_client() -> *mut SwapiClient {
    Box::into_raw(Box::new(SwapiClient::new()))
}

//Throw error
#[no_mangle]
pub unsafe extern "C" fn load_name_cb(client: *mut SwapiClient, c_callback: fn(*mut c_char)) {
    assert!(!client.is_null());
    let local_client = client.as_ref().unwrap();
    let cb = Callback {
        callback: Box::new(|name| {
            println!("Callback result: {}", name);
            let result = CString::new(name.to_owned()).unwrap().into_raw();
            (c_callback)(result); //borrowed value does not live long enough. How to set proper lifetime?
           //If I remove this call I can compile and see logs. 
        }),
    };
    let callback: Box<Callback> = Box::new(cb);
    local_client.load_name(1, callback);
}

Networking API:

pub struct Callback {
    pub callback: Box<dyn FnMut(String)>,
}

unsafe impl Send for Callback {}

impl OnThreadEvent for Callback {
    fn on_load(&mut self, name: &str) {
        (self.callback)(name.to_string().clone());
    }
}

pub trait OnThreadEvent {
    fn on_load(&mut self, s: &str);
}

pub struct SwapiClient();

impl SwapiClient {

    pub fn new() -> SwapiClient {
        SwapiClient()
    }

    pub fn load_name(&self, id: i32, mut callback: Box<dyn OnThreadEvent + Send>) {
        ///Using Tokio runtime
        (*RUN_TIME).spawn(async move {
            let res = load_swapi_async().await; //reqwest function, works fine
            match res {
                Ok(name) => {
                    let result = format!("ID: {}; Name: {}", id, name);
                    callback.on_load(result.as_str());
                }
                Err(err) => {
                    let error = format!("ID: {}; Failed to load name {}", id, err);
                    callback.on_load(error.as_str())
                }
            }
        });
    }
}

I also tried to use thread directly in the extern function - it works fine:

#[no_mangle]
pub extern "C" fn countdown(callback: fn(c_int)) {
    thread::spawn(move || {
        for x in (0..=15).rev() {
            thread::sleep(Duration::from_secs(1));
            callback(x);
        }
    });
}

Please, help. How can I bind the callback functions lifecycle with Callback struct?

Try adding a move on your closure:

#[no_mangle]
pub unsafe extern "C" fn load_name_cb(client: *mut SwapiClient, c_callback: fn(*mut c_char)) {
    assert!(!client.is_null());
    let local_client = client.as_ref().unwrap();
    let cb = Callback {
-       callback: Box::new(|name| {
+       callback: Box::new(move |name| {
            println!("Callback result: {}", name);
            let result = CString::new(name.to_owned()).unwrap().into_raw();
            (c_callback)(result); //borrowed value does not live long enough. How to set proper lifetime?
           //If I remove this call I can compile and see logs. 
        }),
    };
    let callback: Box<Callback> = Box::new(cb);
    local_client.load_name(1, callback);
}

since otherwise the closure will be capturing &c_callback, a local borrow of the parameter of load_name_cb (which is dropped when that function returns)

  • ((c_callback)(...) can be written as (*(&c_callback))(...), so Rust does that by default).
2 Likes

Hi Yandros,
Thanks for the suggestion, it worked!
But still, I'm not sure I understand how Rust handles pointed with move. Does it create a copy of c_callback ?

Yes, (fn pointers are Copy) although it being copied does not play any role here since you do not use c_callback elsewhere. The important thing is that with move you capture owned variables / you take ownership of them, rather than borrow them.

(In c++ notation, it is as if instead of capturing with [&], you started capturing with[=])

2 Likes

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