Rust FFI pasa el objeto de rasgo como contexto para llamar a las devoluciones de llamada

6 minutos de lectura

Rust FFI pasa el objeto de rasgo como contexto para llamar a las devoluciones de llamada
códigomediano

Bien, estoy tratando de lograr lo siguiente:

  1. C llama al óxido
  2. rust vuelve a llamar a c y registra una devolución de llamada en un objeto de rasgo definido por el usuario
  3. c llama a rust con el contexto
  4. rust llama a la devolución de llamada en el contexto (objeto de rasgo)

He estado jugando un poco con eso. Llegué bastante lejos, pero todavía no del todo.

El bit 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);
}

El pedacito de óxido:

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);
    }
}

La cuestión:

  • Mi programa falla cuando intento llamar a “devolución de llamada” en el objeto de rasgo en “rust_cb”

Una solución: – Cambie la firma de función de “rust_cb” a

pub extern fn rust_cb(context: *mut MyFoo)

pero eso no es lo que quiero, ya que estoy tratando de crear un envoltorio seguro que solo conozca el rasgo del oyente

Cualquier ayuda apreciada

PD: mi suposición es que falla, porque el compilador no conoce el desplazamiento de la devolución de llamada en el rasgo Foo, necesita el objeto real para determinar dónde está. pero entonces no tengo idea de cómo solucionar eso

Entonces, si necesita representar el Foo como un void *puedes usar esto:

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);
    }
}

Creo que puede estar malinterpretando qué es un objeto de rasgo. Un objeto de rasgo es un tipo que tiene el tamaño de dos punteros (es decir, 128 bits en un sistema de 64 bits). En este ejemplo, Foo no es un objeto de rasgo, es un tipo de tamaño dinámico (es decir, un tipo que tiene un tamaño variable, como str). Box<Foo> es un objeto de rasgo. Box<Box<Foo>> no es un objeto de rasgo ni un tipo de tamaño dinámico, es un tipo que tiene el mismo tamaño que un puntero, por lo que necesitamos usarlo aquí, ya que queremos convertirlo en un void *.

Yo lo llamo “fuga” porque cuando llamas Box::into_rawestá perdiendo la memoria de lo que sea que haya en la caja, lo que significa que usted es responsable de asegurarse de que el destructor (el Drop implementación) es llamado.

  • ¡Increíble gracias! Creo que ahora lo entiendo. No sé por qué, pero me resulta bastante difícil entender todas esas cosas que vienen de C 🙁

    – codemedian

    26 de noviembre de 2015 a las 20:51

  • rust_function no tiene que ser pub externo, ¿o sí?

    – Beni

    16 dic 2019 a las 22:50

Rust FFI pasa el objeto de rasgo como contexto para llamar a las devoluciones de llamada
Adrián

Objetos de rasgo de óxido como Box<Foo> tienen el doble del tamaño de un puntero normal, por lo que no puede usar void * para representarlos. Ver std::raw::TraitObject para más información. Aquí hay una versión funcional de su código:

programa.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));
    }
}

Esto solo funcionará en nighty rustc (debido a la #![feature(raw)]) y también dará una advertencia porque TraitObject no es seguro para FFI. Si quiere algo que funcione en estable, puede definir alguna estructura de un tamaño apropiado como esta y usarla en lugar de TraitObject:

#[repr(C)]
struct FFITraitObject {
    data: usize,
    vtable: usize,
}

Otra opción, por supuesto, sería usar Box<Foo> en lugar de TraitObjectpero aún recibiría una advertencia:

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);
    }
}

Si realmente quieres usar un void *podría considerar filtrar el TraitObject así como el MyFoo y utilizar dos niveles de direccionamiento indirecto.

  • ¡Gracias! No entiendo muy bien cómo hacer que funcione con c_void y cómo “filtrar” el TraitObject. Realmente no puedo/no quiero cambiar el lado C, y me gustaría usar la menor sobrecarga/indirección posible. Tal vez lo que quiero no es fácilmente alcanzable. (PD: puedo pasar argumentos adicionales a la función C real, ¿así que tal vez pueda pasar el tipo a rust?)

    – codemedian

    26 de noviembre de 2015 a las 11:04

¿Ha sido útil esta solución?