在Rust中分配的C# byte[]如何释放?

4

我有一个 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#中调用它而不崩溃。(取而代之的是崩溃,我假设这是正确的,但不能百分之百确定)。

[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”打印)。那么我的错误在于如何回收要释放的 Vec<u8>,在Rust接受的类型上(例如*mut u8*mut *mut u8),在我的 C#DllImport中,还是其他地方?


3
您的实际代码是否在从数组中复制后立即返回给Rust?如果是这样,将C#字节数组传递给Rust并让Rust对其进行复制,然后双方都不必概念上放弃各自缓冲区的所有权,这样做会更容易些。 - loganfsmyth
这句话应该是 Vec::from_raw_parts(*bytes, len as usize, len as usize),对吧? - Jmb
@loganfsmyth - 假设这种方法是可行的,我面临的挑战是我不知道要预先分配多少字节。我可能可以调用一次来计算大小,再调用一次来填充byte[],但这并不理想。 - user655321
1个回答

7

主要问题

byte*/*mut u8byte[]是不同种类的对象。后者必须指向由.NET GC管理的内存。因此,尽管在钉住时可以将byte[]视为byte*,但不能将任意byte*视为byte[]

我不完全确定你的情况下marshaller正在做什么,但它可能是这样做的:

  • 分配一个指针大小的空间,初始化为null指针。
  • 使用该空间的指针作为第二个参数调用rust方法。
  • 将该空间的更新内容解释为指向C风格字节数组的指针。
  • 将此数组的内容复制到新分配的托管数组中。
  • 将该托管数组放置在C#本地bytes中。

正如您所看到的,在bytes中获得的数组是一个全新的托管数组,与Rust写入*bytes的指针没有持久关系。因此,尝试在bytes上调用free_bytes会失败,因为它将被marshalled为指向由.NET GC管理的内存的指针,而不是Rust。

次要问题

如果您打算通过P/Invoke释放内存,那么没有办法避免传递容量到C#并将其保留。这是因为Vec :: shrink_to_fit不能保证将capacity减少到len,如文档所示。而且,您必须拥有正确的容量才能调用 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。
  • 使用 (byte*)ptr.ToPointer() 将 IntPtr 转换为 byte* 并直接使用原始的 byte*。这可能比上面的方法稍微简洁一些,但同样容易出错,而且许多有用的 API 不接受原始指针。
  • 将数据从 IntPtr 复制到托管的 byte[] 中。这可能有点低效,但您将拥有真正托管数组的所有优势,并且在调用 free_bytes 后仍然可以安全地使用数组。但是,如果您想修改数组并使这些修改对 Rust 可见,您将需要执行另一个复制操作。对于此解决方案,请将注释替换为:
byte[] bytes = new byte[len];
Marshal.Copy(ptr, bytes, 0, len);
  • 如果您正在使用C# 7.2或更高版本,您可以使用新的Span<T>类型避免复制内存,它可以表示托管或非托管内存的范围。根据您计划对bytes执行什么操作,Span<byte>可能已经足够了,因为许多API在最近的C#版本中已更新以接受spans。由于span直接引用Rust分配的内存,对其进行的任何变化都将反映在Rust侧,并且在调用free_bytes后不能尝试使用它释放该内存。对于此解决方案,请将注释替换为:
Span<byte> bytes = new Span<byte>(ptr.ToPointer(), len);

关于安全性的说明

请注意,Rust函数get_bytes被标记为unsafe。这是因为使用as操作符将vec的长度和容量强制转换为i32。如果它们不在i32范围内,这将导致panic。据我所知,在跨越P / Invoke引入的FFI边界上引发panic仍然是未定义的行为。在生产代码中,get_bytes可以修改为以其他方式处理此类错误,例如通过返回空指针,C#需要检测到此情况并作出相应反应。


太棒了,谢谢!Marshal.Copy似乎可以做到。使用Span<byte>方法是否需要MarshalAs(UnmanagedType.LPArray...)来告知.NET关于span的大小? - user655321
我列出的方法都基于上面给出的相同P/Invoke签名。据我所知,您无法自动编排Spans,因此由于Span不会(也不能)出现在P/Invoke签名中,因此不需要任何“MarshalAs”属性。相反,通过Span<byte>构造函数手动通知.NET跨度的长度。 - AlphaModder
糟糕,我才意识到我没有通知你我的回复。@user655321 - AlphaModder

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