如何从Rust返回一个结构体向量给C#?

17

如何编写像下面这个C代码一样的Rust代码?这是我到目前为止的Rust代码,没有选项可以将其编组:

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
}

上面的代码试图复制下面的C代码,我能够直接进行交互。

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);
    }
}

这段 C 代码可以通过在 C# 中进行 DLL 导入来访问:

[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
    });

14
我对C#交互不是很了解,但对于与任何外部语言使用FFI的事情,总有一件事是真的:你不应该在extern函数中使用Rust特定类型,比如Vec<T>。这些函数中唯一可用的类型是原始类型,如整数、浮点数和指针,以及由这些类型组成的#[repr(C)]结构体。 - Vladimir Matveev
1个回答

5
让我们将此分解为您的Rust代码需要满足的各种要求:
  1. DLL需要使用正确的名称 GetPacksChar 来公开一个函数。这是因为您在C#中声明了它的名称为 GetPacksChar,名称必须匹配。
  2. 该函数需要正确的调用约定,即 extern "C"。这是因为您在C#中将函数声明为 CallingConvention = CallingConvention.Cdecl,它与Rust中的 extern "C" 调用约定相匹配。
  3. 函数需要正确的签名,本例中采用Rust等效的 uintPackChar ** 并返回无内容。这与函数签名 fn (u32, *mut *mut PackChar) 相匹配。
  4. PackChar 的声明需要在C#和Rust之间匹配。下面将详细介绍。
  5. 该函数需要复制原始C函数的行为。下面将详细介绍。

最简单的部分将是在Rust中声明函数:

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

接下来我们需要处理PackChar。根据C#代码中使用的方式,它似乎应该这样声明:

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

将其分解,#[repr(C)]告诉Rust编译器以与C编译器相同的方式排列PackChar在存储器中,这很重要,因为你正在告诉C#它正在调用C。 IntValbuffer都是从C#和原始C版本中使用的。在C版本中,IntVal声明为int,因此我们在Rust版本中使用i32,而buffer在C中被视为字节数组,因此我们在Rust中使用*mut u8
注意,C#中PackChar的定义应与C/Rust中的声明相匹配,因此:
public struct PackChar {
    public int IntVal;
    public char* buffer;
}

现在,唯一剩下的就是在Rust中重现C函数的原始行为:
#[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');
    }
}

上述内容中的重点如下:
  • 我们必须手动包含空终止字符(\0),因为它是一个C字符串。
  • 我们调用了两个C函数CoTaskMemAlloc()strdup()strdup()libc crate中,而您可能会在ole32-sys crate中找到。
  • 该函数声明为unsafe,因为我们必须执行许多不安全的操作,比如调用C函数和执行str::from_raw_parts()
希望这有所帮助!

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