如何使用cbindgen返回并释放一个Box<Vec<_>>?

3

我从Rust返回一个结构体给C代码。我不知道这是否是一种好的做法,但它确实可以重新构建结构体并在没有泄漏的情况下释放内存。

#[repr(C)]
pub struct s {
    // ...
}

#[repr(C)]
#[allow(clippy::box_vec)]
pub struct s_arr {
    arr: *const s,
    n: i8,
    vec: Box<Vec<s>>,
}

/// Frees memory that was returned to C code
pub unsafe extern "C" fn free_s_arr(a: *mut s_arr) {
    Box::from_raw(s_arr);
}

/// Generates an array for the C code
pub unsafe extern "C" fn gen_s_arr() -> *mut s_arr {
    let many_s: Vec<s> = Vec::new();
    // ... logic here

    Box::into_raw(Box::new(s_arr {
        arr: many_s.as_mut_ptr(),
        n: many_s.len() as i8,
        vec: many_s,
    }))
}

目前C头文件是手写的,但我想尝试使用cbindgen。 s_arr 的手动C定义如下:

struct s_arr {
    struct s *arr;
    int8_t n;
    void *_;
};

cbindgen 为 s_arr 生成以下内容:

typedef struct Box_Vec_s Box_Vec_s;

typedef struct s_arr {
        const s *arr;
        int8_t n;
        Box_Vec_s vec;
} s_arr;

这不起作用,因为struct Box_Vec_s未定义。理想情况下,我只想覆盖为vec生成的cbindgen类型,使其成为void *,因为它不需要任何代码更改,也没有额外的测试,但我可以接受其他建议。
我已经查看了cbindgen文档,尽管没有查看示例,但仍然找不到任何内容。
1个回答

3

你的问题不太清楚,但我认为如果我理解得没错,你正在混淆两件事情,并因此走上了一条黑暗的小路。

C语言中,动态大小的数组通常由以下两个元素来识别:

  1. 它的起始位置,作为一个指针
  2. 它的长度

Rust遵循相同的约定 - 在幕后,一个 Vec<_> 共享相同的结构(嗯,几乎是一样的。它也有一个容量,但这是无关紧要的)。

在指针之上传递装箱的向量不仅过度设计,而且极其不明智。FFI绑定可以很聪明,但大多数时候他们处理不了装箱的复杂类型。

为了解决这个问题,我们将简化你的绑定。我添加了一个单一元素在 struct S 中来向你展示如何做。我还清理了你的FFI边界:

#[repr(C)]
#[no_mangle]
pub struct S {
    foo: u8
}

#[repr(C)]
pub struct s_arr {
    arr: *mut S,
    n: usize,
    cap: usize
}

// Retrieve the vector back
pub unsafe extern "C" fn recombine_s_arr(ptr: *mut S, n: usize, cap: usize) -> Vec<S> {
    Vec::from_raw_parts(ptr, n, cap)
}

#[no_mangle]
pub unsafe extern "C" fn gen_s_arr() -> s_arr {
    let mut many_s: Vec<S> = Vec::new();

    let output = s_arr {
        arr: many_s.as_mut_ptr(),
        n: many_s.len(),
        cap: many_s.capacity()
    };
    std::mem::forget(many_s);
    output
}

通过这个方法,cbindgen 返回预期的头文件定义:

typedef struct {
  uint8_t foo;
} so58311426S;

typedef struct {
  so58311426S *arr;
  uintptr_t n;
  uintptr_t cap;
} so58311426s_arr;

so58311426s_arr gen_s_arr(void);

这使我们能够从C或Rust中调用gen_s_arr()并检索一个可在FFI边界的两个部分(so58311426s_arr)中使用的结构体。该结构体包含我们需要修改的所有S数组(根据cbindgen是so58311426S)。
通过FFI传递时,您需要确保几件简单的事情:
  • 您不能传递原始盒子或非原始类型;你几乎总是需要向下转换为一组指针或更改定义以适应(正如我在这里所做的那样)
  • 绝对不要传递原始向量。 最多,你传递一个片段,因为那是一种原始类型(参见上面的一点)。
  • 确保std::mem::forget()任何您不想释放的内容,并确保记住在其他地方释放它或重组它。
我将在一个小时内编辑此问题; 我有班机要赶。 如果需要澄清任何内容,请告诉我,我到了正确的国家后会处理它们 :-)

谢谢!我知道我做了一些愚蠢的事情,但是几个月前我写原始代码时找不到好的解决方案。对于std::mem::forget(many_s);,这保证不会泄漏吗?我期望Vec中唯一分配在堆上的结构成员是动态数组,但也许将来还有另一个Vec成员在堆上。我只是相信stdlib维护者会保持Vec这种方式吗?这更多是一个学术问题而不是实际问题 - 您的解决方案完全符合我的需求。 - MrTheFoolish
@MrTheFoolish 我已经有时间准备了很多东西给你。其中大部分将回答你的问题,可能还会引发更多的问题 :-) 我也可能把所有这些信息转化成文章发布在某个地方,因为关于ffi的信息实在太少了。 - Sébastien Renauld

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