我有一个Rust函数,它将字节数组传递给C#:
#[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中的内容不匹配(通过“释放字节数” println)。那么Vec<u8>
,在Rust正在接受的类型(例如*mut u8
vs *mut *mut u8
),C#DllImport
中的其他地方,我如何恢复要释放的,这是我的错误吗?
阿byte*
/*mut u8
和byte[]
是不同种类的对象。后者必须指向由.NET GC管理的内存。因此,虽然可以将abyte[]
视为byte*
(固定时),但不能将abyte*
视为任意byte[]
。
我不完全确定编组员在您的情况下正在做什么,但是可能是这样的:
bytes
。如您所见,进入的数组bytes
是一个全新的托管数组,与*bytes
Rust写入的指针没有持久的关系。所以当然试图调用free_bytes
上bytes
会失败,因为它会被编组为指向内存的指针由.NET GC和不生锈管理。
如果打算通过P / Invoke释放内存,则无法绕开将容量传递给C#并保持其容量。这是因为如文档所示,Vec::shrink_to_fit
不能保证减小capacity
到。并且您必须具有正确的能力才能拨打电话。len
Vec::from_raw_parts
将a的所有权传递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*
with(byte*)ptr.ToPointer()
并byte*
直接使用raw 。这可能比上面的内容略微冗长,但是它容易出错,并且许多有用的API都不接受原始指针。IntPtr
到托管表中byte[]
。这有点低效,但是您将拥有真正的托管阵列的所有优点,并且即使在调用free_bytes
原始内存之后也可以安全地使用该阵列。但是,如果要修改数组并使这些修改对Rust可见,则必须执行另一个复制。对于此解决方案,将注释替换为:byte[] bytes = new byte[len];
Marshal.Copy(ptr, bytes, 0, len);
Span<T>
类型复制内存,该类型可以代表一系列托管或非托管内存。根据您打算使用的内容bytes
,aSpan<byte>
可能就足够了,因为已经更新了许多API以接受最新版本的C#中的跨度。由于范围直接是指Rust分配的内存,因此对它的任何更改都将反映在Rust端,并且您不能在调用释放了该内存之后尝试使用它free_bytes
。对于此解决方案,将注释替换为:Span<byte> bytes = new Span<byte>(ptr.ToPointer(), len);
请注意,Rust函数get_bytes
标记为unsafe
。这是因为as
运算符用于将vec的长度和容量转换为i32
s。如果它们不位于的范围内,将会恐慌i32
,并且据我所知,在FFI边界(例如P / Invoke引入的边界)上恐慌仍然是未定义的行为。在生产代码中,get_bytes
可以将其修改为以其他方式处理此类错误,也许可以通过返回空指针来实现,而C#将需要检测这种情况并采取相应的措施。
本文收集自互联网,转载请注明来源。
如有侵权,请联系[email protected] 删除。
我来说两句