如何释放Rust中分配的C#字节[]?

如何释放Rust中分配的C#字节[]?,c#,rust,ffi,C#,Rust,Ffi,我有一个Rust函数,它将字节数组传递给C#: #[无损坏] 发布外部“C”fn获取字节(len:&mut i32,字节:*mut*mut u8){ 让mut buf:Vec=get_data(); buf.收缩到合适的位置(); //设置输出值 *len=buf.len()作为i32; 不安全{ *bytes=buf.as_mut_ptr(); } std::mem::忘记(buf); } 从C#开始,我可以毫不犹豫地调用它。(代替崩溃,我假设这是正确的,但不是100%确定): [DllIm

我有一个Rust函数,它将字节数组传递给C#:

#[无损坏]
发布外部“C”fn获取字节(len:&mut i32,字节:*mut*mut u8){
让mut buf:Vec=get_data();
buf.收缩到合适的位置();
//设置输出值
*len=buf.len()作为i32;
不安全{
*bytes=buf.as_mut_ptr();
}
std::mem::忘记(buf);
}
从C#开始,我可以毫不犹豫地调用它。(代替崩溃,我假设这是正确的,但不是100%确定):

[DllImport(“my_lib”)]静态外部无效获取字节(ref int len,
[Marshallas(UnmanagedType.LPArray,SizeParamIndex=0)]参考字节[]字节;
无效测试()
{
int len=0;
字节[]字节=null;
获取_字节(ref len,ref bytes);
}
然后我使用了
字节
,但我知道这个内存需要通过生锈来释放。所以我有另一个除锈功能:

#[无损坏]
发布外部“C”fn空闲字节(len:i32,字节:*mut*mut u8){
//还尝试了:------------字节:*mut u8
断言!(len>0);
//重建vec
设v=不安全{Vec::from_raw_parts(字节,len as usize,len as usize)};
//println!(“要释放的字节:{:?}”,v);
drop(v);//或者可以隐式删除它
}
和相应的C#。拨打电话会使我的应用程序崩溃:

[DllImport(“my_lib”)]extern void free_字节(int len,ref byte[]字节);
无效测试()
{
int len=0;
字节[]字节=null;
获取_字节(ref len,ref bytes);
//将字节复制到托管内存
字节[]复制=新字节[len];
字节。复制到(复制,0);
//释放非托管内存
free_字节(len,ref字节);//执行此函数时发生崩溃
}
我知道这“是非常不安全的”。因为“
capacity
需要为指针分配的容量”,我还尝试在Rust和C#之间传递容量,而不使用
shorn_to_fit
来保留长度和容量。那也崩溃了

我假设
from_parts_raw
恢复堆上的现有内存,但我注意到C#(在Visual Studio中显示)中的字节内容与Rust中的内容不匹配(通过“字节到空闲”println)。我如何恢复要释放的
Vec
的错误也是如此,在Rust正在接受的类型(例如,
*mut u8
vs
*mut*mut u8
)中,在我的C#
DllImport
中,在其他地方?

是主要问题 一个
字节*
/
*mut u8
和一个
字节[]
是不同类型的对象。后者必须指向由.NET GC管理的内存。因此,虽然可以将
字节[]
视为
字节*
(固定时),但不能将任意
字节*
视为
字节*

我不完全确定马歇尔在你的案件中做了什么,但可能是这样的:

[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);
}
  • 分配一个指针大小的空间,初始化为空指针
  • 使用指向此空间的指针作为第二个参数调用rust方法
  • 将该空间的更新内容解释为指向C样式字节数组的指针
  • 将此数组的内容复制到新分配的托管数组
  • 将该托管数组放入C#local
    字节中
如您所见,您在
字节
中获得的数组是一个新的托管数组,与Rust写入
*字节
的指针没有持久的关系。因此,尝试调用
字节
上的
空闲字节
当然会失败,因为它将被封送为指向.NET GC管理的内存的指针,而不会生锈

次要问题 如果您打算通过p/Invoke释放内存,那么将容量传递给C#并保持它不变是无法避免的。这是因为
Vec::shrink_to_fit
不能保证将
容量
降低到
len
,如所示。您必须具有正确的容量,才能从原始零件调用
Vec::from\u raw\u parts

解决方案 将
Vec
的所有权传递给其他代码的唯一合理方法是在生锈的一侧使用类似的函数

#[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
}
您有几个不同的选项来代替TODO

  • 按原样使用
    IntPtr
    ,使用
    Marshal.ReadIntPtr
    等方法从数组中读取数据。我不建议这样做,因为它冗长且容易出错,并且会阻止使用大多数针对阵列的API
  • 使用
    (byte*)ptr.topenter()
    IntPtr
    转换为
    byte*
    ,并直接使用原始
    byte*
    。这可能比上面的内容稍微简单一些,但它同样容易出错,而且许多有用的API不接受原始指针
  • 将数据从
    IntPtr
    复制到托管的
    字节[]
    。这有点低效,但您将拥有真正受管阵列的所有优点,并且即使在原始内存上调用
    free_bytes
    后也可以安全地使用该阵列。但是,如果要修改阵列并使这些修改对Rust可见,则必须执行另一个复制。对于此解决方案,请将注释替换为:
  • 如果您使用的是C#7.2或更高版本,则可以避免使用新的
    Span
    类型复制内存,该类型可以表示一系列托管或非托管内存。根据您计划如何使用
    字节
    ,一个
    Span
    可能就足够了,因为许多API已经更新,可以在最新版本的C#中接受Span。由于span直接指的是Rust分配的内存,它的任何突变都会反映在Rust端,在调用
    free_bytes
    释放内存后,您不得尝试使用它。对于此解决方案,请将注释替换为:
Span字节=新的Span(ptr.ToPointer(),len);
关于安全的说明 请注意,Rust函数
get_bytes
标记为
byte[] bytes = new byte[len];
Marshal.Copy(ptr, bytes, 0, len);
Span<byte> bytes = new Span<byte>(ptr.ToPointer(), len);