Reputation: 1732
I have a Rust function that passes a byte array to 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);
}
From C#, I can call it without crashing. (In lieu of a crash, I assume this is correct but am not 100% sure):
[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);
}
I then make use of bytes
, but I understand that this memory needs to be freed by Rust. So I have another Rust function to free it:
#[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
}
And the corresponding C#. Making the call crashes my app:
[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
}
I see that Vec::from_parts_raw
"is highly unsafe". Because "capacity
needs to be the capacity that the pointer was allocated with.", I also tried passing the capacity between Rust & C# without shrink_to_fit
to preserve length & capacity. That also crashed.
I assume from_parts_raw
recovers the existing memory on the heap, but I noticed that the byte content in C# (shown in Visual Studio) doesn't match the contents in Rust (via the 'bytes to free' println). So is my error in how I'm recovering the Vec<u8>
to be freed, in the types Rust is accepting (eg, *mut u8
vs *mut *mut u8
), in my C# DllImport
, somewhere else?
Upvotes: 4
Views: 1208
Reputation: 3386
A byte*
/*mut u8
and a byte[]
are different kinds of object. The latter must point to memory managed by the .NET GC. So while it is possible to view a byte[]
as a byte*
(while it is pinned), you cannot view an arbitrary byte*
as a byte[]
.
I'm not entirely sure what the marshaller is doing in your case, but it is probably something like this:
bytes
.As you can see, the array you get in bytes
is a fresh managed array, with no lasting relation to the pointer written to *bytes
by Rust. So of course attempting to call free_bytes
on bytes
will fail, since it will be marshalled as a pointer to memory managed by the .NET GC and not Rust.
If you intend to free the memory via P/Invoke, there is no way to get around passing the capacity to C# and keeping it around. This is because Vec::shrink_to_fit
is not guaranteed to reduce capacity
to len
, as indicated by the documentation. And you must have the correct capacity in order to call Vec::from_raw_parts
.
The only reasonable way to pass ownership of a Vec
to other code is with functions like these on the Rust side.
#[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
}
And on the C# side, you would have something like this:
[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);
}
You have a few different options for what to put in place of the TODO.
IntPtr
as-is, reading data from the array with methods like Marshal.ReadIntPtr
. I don't recommend this because it is verbose and error-prone, and would prevent the use of most APIs targeting arrays.IntPtr
to a byte*
with (byte*)ptr.ToPointer()
and use the raw byte*
directly. This might be marginally less verbose than the above, but it's just as error-prone, and many useful APIs do not accept raw pointers.IntPtr
into a managed byte[]
. This is a little inefficient, but you will have all the advantages of a real managed array, and it will be safe to use the array even after calling free_bytes
on the original memory. However, if you want to modify the array and have these modifications be visible to Rust, you will have to perform another copy. For this solution, replace the comment with:byte[] bytes = new byte[len];
Marshal.Copy(ptr, bytes, 0, len);
Span<T>
type, which can represent a range of managed or unmanaged memory. Depending on what you plan to do with bytes
, a Span<byte>
may be sufficient, as many APIs have been updated to accept spans in recent versions of C#. As the span refers directly to the memory allocated by Rust, any mutations to it will be reflected on the Rust side, and you must not attempt to use it after that memory has been freed by the call to free_bytes
. For this solution, replace the comment with:Span<byte> bytes = new Span<byte>(ptr.ToPointer(), len);
Note that the Rust function get_bytes
is marked as unsafe
. This is because the as
operator is used to cast the length and capacity of the vec to i32
s. This will panic if they do not fit in the range of i32
, and to the best of my knowledge panicking over an FFI boundary such as that introduced by P/Invoke is still undefined behavior. In production code, get_bytes
could be modified to handle such errors some other way, perhaps by returning a null pointer, and C# would need to detect this situation and act accordingly.
Upvotes: 7