如何跨越Rust和 Delphi 之间的FFI边界处理内存管理?

hm2xizp9  于 2023-02-23  发布在  其他
关注(0)|答案(1)|浏览(192)

通过 Delphi 和Rust之间的FFI边界传递分配的内存

我目前正在调查我们公司在 Delphi 服务中使用rust作为动态链接库的可行性,我们的用例(自动填写pdf表单)包括从delphi到rust(由delphi管理)以及从rust到delphi(由rust管理)传递指向堆内存的指针。
我构建了一个简单的例子,在这个例子中, Delphi 服务用一个字节数组调用rust库,Rust读取这个数组并把修改过的内容放到一个Vec<u8>中。
现在,Vec<u8>被转换成Buffer(参见下面的trait impl),并返回给 Delphi 。
我已经验证了数据交换的工作和预期的一样。当 Delphi 调用rust库来释放由rust分配的内存时,issues出现了(参见下面的drop_buffer函数)。delphi调试器(显然可以调试汇编)在一些汇编代码上停止并中止正常的程序流。

防 rust 实施

#[repr(C)]
pub struct Buffer {
    ptr: *const [u8],
    len: usize
}

impl From<Vec<u8>> for Buffer {
    fn from(buffer: Vec<u8>) -> Self {
        let boxed_buffer = buffer.into_boxed_slice();
        let len = boxed_buffer.len();
        let ptr = Box::into_raw(boxed_buffer);
        Self {
            ptr,
            len
        }
    }
}
impl Buffer {
    pub unsafe fn manual_drop(ptr: *const [u8]) {
        let boxed_buffer = Box::from_raw(ptr as *mut [u8]);
        drop(boxed_buffer)
    }
}

为了释放内存, Delphi 调用这个函数:

#[no_mangle]
pub extern "cdecl" fn drop_buffer(buffer: *const [u8]) {
    unsafe {Buffer::manual_drop(buffer);}
}

我不知道这是否相关,但这是 Delphi 调试器停止的代码片段。如果还没有尝试理解这段代码。

如何继续

调用一个rust函数来释放在rust中分配的内存对我来说似乎是正确的想法。这是一个明智的方法吗?如果是,我的问题的原因可能是什么?

2izufjch

2izufjch1#

你的问题中没有足够的信息让我们确切地回答你的问题,但我至少可以指出一些我跳出来的问题。
*const [u8]是一个宽指针(又称胖指针)。它实际上包含一个指向数据的普通指针(*const u8)和一个切片长度(usize)。这种类型在FFI边界传递时不安全,因为它的表示形式不能保证是稳定的。从Rustonomicon

  • DST指针(宽指针)和元组在C中不是一个概念,因此永远不是FFI安全的。

(DST表示动态调整大小的类型。[u8]是DST。)
你还没有展示 Delphi 对Buffer的定义,但我怀疑你只是在那里放了一个普通的指针和长度(希望是NativeUInt,或者等价的UIntPtr)。你很幸运,因为宽指针的长度恰好是你认为的len字段所在的位置。
你应该使用*const u8,你需要单独存储长度,你已经在Buffer中有了长度,但是你还需要把它添加到drop_buffer中(或者只传递一个Buffer值)。使用std::slice::from_raw_partsstd::slice::from_raw_parts_mut从普通指针和长度重构切片。这两个函数返回一个引用,而不是指针,所以在调用Box::from_raw之前,需要将引用强制转换为指针。
drop_buffer应为unsafe fn;它不能保证内存的安全性,因为它接收到了一个任意的指针。一个函数是否被标记为unsafe对生成的代码没有任何影响,但是如果你最终从Rust代码中调用了这个函数(有意或无意),它可以帮助你避免将来的错误。
调试器似乎遇到了int 3指令,该指令引发了一个被解释为断点的中断。根据该指令的地址判断,它似乎位于Windows系统DLL中。例如ntdll.dll或kernel32.dll。根据经验,我知道ntdll.dll(可能还有其他的)有int 3指令,这些指令只有在调试器附加到进程时才被触发,并作为出现严重错误的提示。debug symbols的调用栈会给予我们更多关于程序如何到达那里的信息(我不认为 Delphi 的调试器支持符号服务器;使用WinDbg或Microsoft的其他调试器可能会更幸运。)
我猜测,造成崩溃的原因是 Delphi 代码只传递了一个指向drop_buffer的普通指针,但Rust端需要一个宽指针,因此length组件没有正确初始化。(存储器大小和对齐)要释放的值以及指向该值的指针,不像在C中free只需要指向值的指针。垃圾长度可能会导致Box::from_raw构造一个与分配时使用的布局不匹配的布局,这可能会导致分配器崩溃(如果你幸运的话)。
调用一个rust函数来释放在rust中分配的内存对我来说似乎是正确的想法。这是一个明智的方法吗[...]?
是的,绝对的!即使一个库和使用它的应用程序是用同一种编程语言编写的,如果这个库公开了分配内存的函数,那么最好的做法是这个库也提供一个释放内存的函数。原因是不能保证这个库和应用程序实际上使用的是同一个内存分配器。并且在某些情况下(特别是当混合语言时),在两端使用相同的内存分配器是不可能的。
X1 E4 F1 X是一本涵盖了不安全 rust 的各个方面的书。FFI本质上是不安全的,所以那本书的许多部分都包含了重要的信息,这些信息将帮助您正确地实现FFI。

相关问题