Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How to return a vector of structs from Rust to C#?

How is it possible to write Rust code like the C code below? This is my Rust code so far, without the option to marshal it:

pub struct PackChar {
    id: u32,
    val_str: String,
}

#[no_mangle]
pub extern "C" fn get_packs_char(size: u32) -> Vec<PackChar> {
    let mut out_vec = Vec::new();

    for i in 0..size {
        let int_0 = '0' as u32;
        let last_char_val = int_0 + i % (126 - int_0);
        let last_char = char::from_u32(last_char_val).unwrap();
        let buffer = format!("abcdefgHi{}", last_char);

        let pack_char = PackChar {
            id: i,
            val_str: buffer,
        };

        out_vec.push(pack_char);
    }

    out_vec
}

The code above tries to reproduce the following C code which I am able to interoperate with as is.

void GetPacksChar(int size, PackChar** DpArrPnt)
{
    int TmpStrSize = 10;
    *DpArrPnt = (PackChar*)CoTaskMemAlloc( size * sizeof(PackChar));
    PackChar* CurPackPnt = *DpArrPnt;
    char dummyString[]= "abcdefgHij";
    for (int i = 0; i < size; i++,CurPackPnt++)
    {
        dummyString[TmpStrSize-1] = '0' + i % (126 - '0');
        CurPackPnt->IntVal = i;
        CurPackPnt->buffer = strdup(dummyString);
    }
}

This C code could be accessed via DLL import in C# like this:

[Dllimport("DllPath", CallingConvention = CallingConvention.Cdecl)]
public static extern void GetPacksChar(uint length, PackChar** ArrayStructs)

PackChar* MyPacksChar;
GetPacksChar(10, &MyPacksChar);
PackChar* CurrentPack = MyPacksChar;
var contLst = new List<PackChar>();
for (uint i = 0; i < ArrL; i++, CurrentPack++)
    contlist.Add(new PackChar() {
        IntVal = CurrentPack->IntVal, buffer = contLst->buffer
    });
like image 381
Raj Felix Avatar asked Nov 07 '15 09:11

Raj Felix


1 Answers

Let's break this down into the various requirements that your Rust code needs to meet:

  1. The DLL needs to expose a function with the correct name GetPacksChar. This is because you declare it with the name GetPacksChar from C# and the names must match.
  2. The function needs the correct calling convention, in this case extern "C". This is because you declare the function as CallingConvention = CallingConvention.Cdecl from C#, which matches the extern "C" calling convention in Rust.
  3. The function needs the correct signature, in this case taking the Rust equivalent of a uint and a PackChar** and returning nothing. This matches the function signature fn (u32, *mut *mut PackChar).
  4. The declaration of PackChar needs to match between C# and Rust. I'll go over this below.
  5. The function needs to the replicate the behavior of the original C function. I'll go over this below.

The easiest part will be declaring the function in Rust:

#[no_mangle]
pub extern "C" fn GetPacksChar(length: u32, array_ptr: *mut *mut PackChar) {}

Next we need to address PackChar. Based on how it's used in the C# code, it looks like it should be declared:

#[repr(C)]
pub struct PackChar {
    pub IntVal: i32,
    pub buffer: *mut u8,
}

Breaking this down, #[repr(C)] tells the Rust compiler to arrange PackChar in memory the same way a C compiler would, which is important since you're telling C# that it's calling into C. IntVal and buffer are both used from C# and the original C version. IntVal is declared as an int in the C version, so we use i32 in the Rust version, and buffer is treated as an array of bytes in C, so we use a *mut u8 in Rust.

Note that the definition of PackChar in C# should match the declaration in C/Rust, so:

public struct PackChar {
    public int IntVal;
    public char* buffer;
}

Now all that's left is to reproduce the original behavior of the C function in Rust:

#[no_mangle]
pub extern "C" fn GetPacksChar(len: u32, array_ptr: *const *mut PackChar) {
    static DUMMY_STR: &'static [u8] = b"abcdefgHij\0";

    // Allocate space for an array of `len` `PackChar` objects.
    let bytes_to_alloc = len * mem::size_of::<PackChar>();
    *array_ptr = CoTaskMemAlloc(bytes_to_alloc) as *mut PackChar;

    // Convert the raw array of `PackChar` objects into a Rust slice and
    // initialize each element of the array.
    let mut array = slice::from_raw_parts(len as usize, *array_ptr);
    for (index, pack_char) in array.iter_mut().enumerate() {
        pack_char.IntVal = index;
        pack_char.buffer = strdup(DUMMY_STR as ptr);
        pack_char.buffer[DUMMY_STR.len() - 1] = b'0' + index % (126 - b'0');
    }
}

Important points from the above:

  • We have to manually include the null terminating character (\0) in DUMMY_STR because it's meant to be a C string.
  • We call CoTaskMemAlloc() and strdup(), which are both C functions. strdup() is in the libc crate, and you can probably find in the ole32-sys crate.
  • The function is declared as unsafe because we have to do a number of unsafe things, like calling C functions and doing str::from_raw_parts().

Hope that helps!

like image 114
randomPoison Avatar answered Oct 12 '22 15:10

randomPoison