Rust FFI如何传递trait对象作为上下文,在回调函数中调用它们

6

好的,我将为您翻译以下内容:

  1. C 调用 Rust
  2. Rust 回调到 C 并在用户定义的 trait 对象上注册回调函数
  3. C 通过上下文调用 Rust
  4. Rust 在上下文(trait 对象)上调用回调函数

我已经进行了一些尝试。虽然我已经做得很多了,但还没有完全成功。

C 部分:

#include <dlfcn.h>
#include <stdio.h>

void *global_ctx;

void c_function(void* ctx) {
    printf("Called c_function\n");
    global_ctx = ctx;
}

int main(void) {
    void *thing = dlopen("thing/target/debug/libthing.dylib", RTLD_NOW | RTLD_GLOBAL);
    if (!thing) {
        printf("error: %s\n", dlerror());
        return 1;
    }
    void (*rust_function)(void) = dlsym(thing, "rust_function");
    void (*rust_cb)(void*) = dlsym(thing, "rust_cb");
    printf("rust_function = %p\n", rust_function);
    rust_function();

    rust_cb(global_ctx);
}

锈蚀位:
extern crate libc;


pub trait Foo {
    fn callback(&self);
}

extern {
    fn c_function(context: *mut libc::c_void);
}

pub struct MyFoo;
impl Foo for MyFoo {
    fn callback(&self) {
        println!("callback on trait");
    }
}

#[no_mangle]
pub extern fn rust_cb(context: *mut Foo) {
    unsafe {
        let cb:Box<Foo> = Box::from_raw(context);
        cb.callback();
    }
}

#[no_mangle]
pub extern fn rust_function() {
    println!("Called rust_function");
    let tmp = Box::new(MyFoo);
    unsafe {
        c_function(Box::into_raw(tmp) as *const Foo as *mut libc::c_void);
    }
}

问题:
我的程序在尝试在“rust_cb”中对特质对象调用“callback”时崩溃。
一个解决方案: - 将“rust_cb”的函数签名更改为
pub extern fn rust_cb(context: *mut MyFoo)

但这不是我想要的,因为我试图创建一个安全的包装器,只知道侦听器的特征。

希望能得到任何帮助。

PS:我的假设是它会导致段错误,因为编译器不知道 trait Foo 上回调函数的偏移量,它需要实际对象来确定其位置。但我不知道如何解决这个问题。

2个回答

6

因此,如果您需要将Foo表示为void *,可以使用以下代码:

extern crate libc;

pub trait Foo {
    fn callback(&self);
}

extern {
    fn c_function(context: *mut libc::c_void);
}

pub struct MyFoo;
impl Foo for MyFoo {
    fn callback(&self) {
        println!("callback on trait");
    }
}

#[no_mangle]
pub extern fn rust_cb(context: *mut Box<Foo>) {
    unsafe {
        let cb: Box<Box<Foo>> = Box::from_raw(context);
        cb.callback();
    }
}

#[no_mangle]
pub extern fn rust_function() {
    println!("Called rust_function");
    let tmp: Box<Box<Foo>> = Box::new(Box::new(MyFoo));
    unsafe {
        c_function(Box::into_raw(tmp) as *mut Box<Foo> as *mut libc::c_void);
    }
}

我认为您对trait对象的理解可能有误。Trait对象是一个大小为两个指针的类型(因此,在64位系统上为128位)。在这个例子中,Foo不是trait对象,它是动态大小的类型(即具有可变大小的类型,例如str)。Box<Foo>是trait对象。 Box<Box<Foo>>既不是trait对象也不是动态大小的类型,而是与指针大小相同的类型,这就是为什么我们需要在这里使用它,因为我们想将其转换为 void *
我称它为“泄漏”,因为当您调用Box::into_raw时,您正在泄漏盒子里面的任何内容的内存,这意味着您有责任确保析构函数(Drop实现)被调用。

太棒了,谢谢!我现在想我明白了。不知道为什么,但是我发现从C语言转过来的所有这些东西都很难理解。 - codemedian
rust_function 不一定需要是 pub extern,对吗? - Benni

4

Rust的trait对象(例如Box<Foo>)的大小是普通指针的两倍,因此您不能使用void *来表示它们。有关更多信息,请参见std::raw::TraitObject。以下是您代码的可行版本:

program.c:

#include <dlfcn.h>
#include <stdio.h>

struct rs_trait_obj {
    void *data;
    void *vtable;
};

struct rs_trait_obj global_ctx;

void c_function(struct rs_trait_obj ctx) {
    printf("Called c_function\n");
    global_ctx = ctx;
}

int main(void) {
    void *thing = dlopen("thing/target/debug/libthing.dylib", RTLD_NOW | RTLD_GLOBAL);
    if (!thing) {
        printf("error: %s\n", dlerror());
        return 1;
    }
    void (*rust_function)(void) = dlsym(thing, "rust_function");
    void (*rust_cb)(struct rs_trait_obj) = dlsym(thing, "rust_cb");
    printf("rust_function = %p\n", rust_function);
    rust_function();

  rust_cb(global_ctx);
}

lib.rs:

#![feature(raw)]

extern crate libc;

use std::raw::TraitObject;
use std::mem;

pub trait Foo {
    fn callback(&self);
}

extern {
    fn c_function(context: TraitObject);
}

pub struct MyFoo;
impl Foo for MyFoo {
    fn callback(&self) {
        println!("callback on trait");
    }
}

#[no_mangle]
pub extern fn rust_cb(context: TraitObject) {
    unsafe {
        let cb: Box<Foo> = mem::transmute(context);
        cb.callback();
    }
}

#[no_mangle]
pub extern fn rust_function() {
    println!("Called rust_function");
    let tmp: Box<Foo> = Box::new(MyFoo);
    unsafe {
        c_function(mem::transmute(tmp));
    }
}

这只能在nightly版本的rustc上工作(因为需要#![feature(raw)]),并且会因为TraitObject不是FFI安全而给出警告。如果你想要一个可以在稳定版本上工作的东西,你可以定义一些适当大小的结构体,并使用它代替TraitObject
#[repr(C)]
struct FFITraitObject {
    data: usize,
    vtable: usize,
}

当然,另一个选择就是直接使用Box<Foo>代替TraitObject,但这样仍会收到警告:

extern crate libc;

pub trait Foo {
    fn callback(&self);
}

extern {
    fn c_function(context: Box<Foo>);
}

pub struct MyFoo;
impl Foo for MyFoo {
    fn callback(&self) {
        println!("callback on trait");
    }
}

#[no_mangle]
pub extern fn rust_cb(context: Box<Foo>) {
    context.callback();
}

#[no_mangle]
pub extern fn rust_function() {
    println!("Called rust_function");
    let tmp: Box<Foo> = Box::new(MyFoo);
    unsafe {
        c_function(tmp);
    }
}

如果你真的想使用一个 void *,你可以考虑泄漏 TraitObjectMyFoo,并使用两个间接级别。

谢谢!我不太明白如何使用c_void使其工作,以及如何“泄漏”TraitObject。 我真的/不想改变C端 - 并且希望尽可能少地使用开销/间接。也许我想要的并不容易实现。(PS:我可以将额外的参数传递到真正的C函数中,所以也许我可以将类型传递给Rust?) - codemedian

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接