在C回调中捕获变量

时间:2016-02-19 14:36:48

标签: rust

我在Rust中有一些状态struct和一个接受并调用extern "C" fn的C库。

fn get_callback(state: State) -> extern "C" fn ... {
    extern "C" fn callback(args: &[Whatever]) -> Something {
        // I need to use that state here
    }

    callback
}

当然,这不起作用,因为callbackget_callback之外定义,就像任何其他C函数一样。

如何在回调中包含某些特定状态?我需要这个来为Rust添加mruby回调,并且为状态使用全局变量是不可取的,因为每个mruby状态都有自己的变量。

1 个答案:

答案 0 :(得分:1)

只有当你的回调接受一些"用户数据时,你才能这样做。由呼叫方注入并在配置该回调时设置的参数,例如,如果您有这样的API:

type Callback = extern fn(*mut c_void);
extern {
    fn register_callback(callback: Callback, user_data: *mut c_void);
}

如果您的C API不适用,那么没有某种全局状态就无法做到这一点!

您可以为此回调提供一个闭包,如下所示:

fn register<F: FnMut() + 'static>(cb: F) {
    extern fn internal_callback(user_data: *mut c_void) {
        let callback = user_data as *mut Box<FnMut()>;
        let callback = unsafe { &mut *callback };
        callback();
    }

    let cb: Box<Box<FnMut()>> = Box::new(Box::new(cb));
    let cb_raw = Box::into_raw(cb) as *mut c_void;

    unsafe {
        register_callback(internal_callback, cb_raw);
    }
}

使用recover()来防止恐慌越过语言边界可能是有意义的,但为了简单起见我省略了它。

上述API存在问题:它允许回调环境泄漏。我认为没有一般解决方案;它在很大程度上取决于您的API。

例如,您可能有一个仅在执行期间仅使用传递的回调的函数,即不需要在任何地方存储回调状态。它可能看起来像这样:

type Callback = extern fn(i32, *mut c_void);
extern {
    fn c_do_something(arg: i32, user_data: *mut c_void, callback: Callback);
}

然后可以像这样使用:

fn do_something<F: FnMut(i32)>(arg: i32, mut cb: F) {
    extern fn internal_callback(arg: i32, user_data: *mut c_void) {
        let callback = user_data as *mut &mut FnMut(i32);
        let callback = unsafe { &mut *user_data };
        callback(arg);
    }
    let cb: &mut &mut FnMut = &mut &mut cb;
    let cb_raw = cb as *mut _ as *mut c_void;

    unsafe {
        c_do_something(arg, cb_raw, internal_callback);
    }
}

在这两种方法中,我们设置了一个代理回调函数,该函数接受用户数据参数并将其解释为指向闭包环境的指针。顺便说一句,也许有可能使internal_callback()通用并避免从闭包中创建特征对象,但这并没有改变整个画面。

如果您的C API更像第一个示例(注册回调),那么您可以将回调存储在某个结构中,只将指向此结构的指针传递给C端。然后你需要更加确定C端在你的结构被删除后不会调用这些回调 - 在你将回调的引用转换为原始指针之后,从回调使用站点到其定义的生命周期链接将被切断,你负责执行终身限制。如果你的回调是由一些在C端有限生命的实体调用的,那么这可能没问题。然后,您可以通过仔细设计API将其生命周期与保持闭包的结构的生命周期联系起来。

最后,在您可能经常更改全局回调的可怕情况下,您唯一的选择是为您的回调分配一个全局状态,每个回调一个,这将存储&#34;当前&#34 ;打回来。然后你需要你的Rust包装器用于注册函数用新的回调替换旧的回调。它看起来像这样:

type Callback = extern fn(*mut c_void);
extern {
    fn register_callback(callback: Callback, user_data: *mut c_void);
}

fn register<F: FnMut() + 'static>(cb: F) {
    extern fn internal_callback(user_data: *mut c_void) {
        let callback = user_data as *mut Mutex<Option<Box<Box<FnMut()>>>>;
        let callback = unsafe { &mut *callback };
        let callback = callback.lock();
        if let Some(callback) = callback.as_mut() {
            callback();
        }
    }
    lazy_static! {
        static ref CURRENT_CALLBACK: Mutex<Option<Box<Box<FnMut() + 'static>>>> = Mutex::new(None);
    }
    let cb: Box<Box<FnMut()>> = Box::new(Box::new(cb));
    // extract the old callback and destroy it, if needed
    mem::replace(&mut *CURRENT_CALLBACK.lock(), Some(cb));
    let cb_raw = &mut *CURRENT_CALLBACK as *mut _;

    unsafe {
        register_callback(internal_callback, cb_raw);
    }
}

这里内包装有点复杂;它负责同步对回调状态的访问,并且只允许在任何时候设置一个回调,在必要时清理旧的回调。但是,当使用新指针调用register_callback()时,C端不能使用旧的回调指针,否则会出现问题。