如何解决我应该如何释放在 Rust 中分配的 C# byte[]? 次要问题
我有一个将字节数组传递给 C# 的 Rust 函数:
#[no_mangle]
pub extern "C" fn get_bytes(len: &mut i32,bytes: *mut *mut u8) {
let mut buf : Vec<u8> = get_data();
buf.shrink_to_fit();
// Set the output values
*len = buf.len() as i32;
unsafe {
*bytes = buf.as_mut_ptr();
}
std::mem::forget(buf);
}
从 C# 中,我可以调用它而不会崩溃。 (代替崩溃,我假设这是正确的,但我不是 100% 确定):
[DllImport("my_lib")] static extern void get_bytes(ref int len,[MarshalAs(UnmanagedType.LPArray,SizeParamIndex = 0)] ref byte[] bytes);
void test()
{
int len = 0;
byte[] bytes = null;
get_bytes(ref len,ref bytes);
}
然后我使用了 bytes
,但我知道这个内存需要被 Rust 释放。所以我有另一个 Rust 函数来释放它:
#[no_mangle]
pub extern "C" fn free_bytes(len: i32,bytes: *mut *mut u8) {
// also tried with: -------------- bytes: *mut u8
assert!(len > 0);
// Rebuild the vec
let v = unsafe { Vec::from_raw_parts(bytes,len as usize,len as usize) };
//println!("bytes to free: {:?}",v);
drop(v); // or it could be implicitly dropped
}
以及相应的C#。拨打电话使我的应用程序崩溃:
[DllImport("my_lib")] extern void free_bytes(int len,ref byte[] bytes);
void test()
{
int len = 0;
byte[] bytes = null;
get_bytes(ref len,ref bytes);
// copy bytes to managed memory
bytes[] copy = new byte[len];
bytes.CopyTo(copy,0);
// free the unmanaged memory
free_bytes(len,ref bytes); // crash occurs when executing this function
}
我看到 Vec::from_parts_raw
“非常不安全”。因为“capacity
需要是分配指针的容量。”,我还尝试在 Rust 和 C# 之间传递容量而不使用 shrink_to_fit
以保留长度和容量。那也崩溃了。
我假设 from_parts_raw
恢复了堆上的现有内存,但我注意到 C# 中的字节内容(在 Visual Studio 中显示)与 Rust 中的内容不匹配(通过 'bytes to free' println )。我在如何恢复要释放的 Vec<u8>
方面的错误也是如此,在 Rust 接受的类型中(例如,*mut u8
vs *mut *mut u8
),在我的 C# DllImport
,其他地方?
解决方法
主要问题
byte*
/*mut u8
和 byte[]
是不同种类的对象。后者必须指向由 .NET GC 管理的内存。因此,虽然可以将 byte[]
视为 byte*
(固定时),但您不能将任意 byte*
视为 byte[]
。
我不完全确定编组员在您的情况下正在做什么,但可能是这样的:
- 分配一个指针大小的空间,初始化为空指针。
- 使用指向该空间的指针作为第二个参数调用 rust 方法。
- 将该空间的更新内容解释为指向 C 样式字节数组的指针。
- 将此数组的内容复制到新分配的托管数组。
- 将该托管数组放在 C# 本地
bytes
中。
如您所见,您在 bytes
中获得的数组是一个新的托管数组,与 Rust 写入 *bytes
的指针没有持久关系。因此,当然尝试在 free_bytes
上调用 bytes
会失败,因为它将被编组为指向由 .NET GC 而不是 Rust 管理的内存的指针。
次要问题
如果您打算通过 P/Invoke 释放内存,则无法绕过将容量传递给 C# 并保留它。这是因为 Vec::shrink_to_fit
不能保证将 capacity
减少到 len
,如 the documentation 所示。并且您必须具有正确的容量才能调用 Vec::from_raw_parts
。
解决方案
将 Vec
的所有权传递给其他代码的唯一合理方法是在 Rust 端使用此类函数。
#[no_mangle]
pub unsafe extern "C" fn get_bytes(len: *mut i32,capacity: *mut i32) -> *mut u8 {
let mut buf: Vec<u8> = get_data();
*len = buf.len() as i32;
*capacity = buf.capacity() as i32;
let bytes = buf.as_mut_ptr();
std::mem::forget(buf);
return bytes;
}
#[no_mangle]
pub unsafe extern "C" fn free_bytes(data: *mut u8,len: i32,capacity: i32) {
let v = Vec::from_raw_parts(bytes,len as usize,capacity as usize);
drop(v); // or it could be implicitly dropped
}
而在 C# 方面,您将拥有如下内容:
[DllImport("my_lib")]
static extern IntPtr get_bytes(out int len,out int capacity);
[DllImport("my_lib")]
static extern void free_bytes(IntPtr bytes,int len,int capacity);
void test()
{
int len,capacity;
IntPtr ptr = get_bytes(out len,out capacity);
// TODO: use the data in ptr somehow
free_bytes(ptr,len,capacity);
}
您有几种不同的选择来代替 TODO。
- 按原样使用
IntPtr
,使用Marshal.ReadIntPtr
等方法从数组中读取数据。我不建议这样做,因为它冗长且容易出错,并且会阻止使用大多数面向数组的 API。 - 使用
IntPtr
将byte*
转换为(byte*)ptr.ToPointer()
并直接使用原始byte*
。这可能比上面的稍微简洁一些,但同样容易出错,而且许多有用的 API 不接受原始指针。 - 将
IntPtr
中的数据复制到托管byte[]
中。这有点低效,但是您将拥有真正托管数组的所有优点,并且即使在原始内存上调用free_bytes
之后使用该数组也是安全的。但是,如果您想修改数组并使这些修改对 Rust 可见,则必须执行另一个复制。对于此解决方案,请将注释替换为:
byte[] bytes = new byte[len];
Marshal.Copy(ptr,bytes,len);
- 如果您使用的是 C# 7.2 或更高版本,则可以避免使用新的
Span<T>
类型复制内存,该类型可以表示托管或非托管内存的范围。根据您计划使用bytes
做什么,Span<byte>
可能就足够了,因为许多 API 已更新为接受最新版本的 C# 中的跨度。由于 span 直接指向 Rust 分配的内存,因此对它的任何更改都会反映在 Rust 端,并且在通过调用free_bytes
释放该内存后,您不得尝试使用它。对于此解决方案,请将注释替换为:
Span<byte> bytes = new Span<byte>(ptr.ToPointer(),len);
安全注意事项
注意 Rust 函数 get_bytes
被标记为 unsafe
。这是因为 as
运算符用于将 vec 的长度和容量转换为 i32
。如果它们不适合 i32
的范围,这将导致恐慌,据我所知,在 P/Invoke 引入的 FFI 边界上恐慌仍然是未定义的行为。在生产代码中,可以修改 get_bytes
以通过其他方式处理此类错误,例如返回空指针,C# 需要检测这种情况并采取相应措施。
版权声明:本文内容由互联网用户自发贡献,该文观点与技术仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 dio@foxmail.com 举报,一经查实,本站将立刻删除。