C#从C/RUST DLL接口获取数组/字符串数据

上一篇文章《如何用C#调用RUST的DLL》介绍了如何在c#中调用rust的dll,但是那篇文章只演示了传递基本类型,和使用c#传入数组到dll接口。如果我们需要从dll中读取数组或字符串数据,应该如何操作呢?本篇文章就将描述具体的实现方法

dll端的接口

这里我们用c来当作类比,另外,这里的代码将忽略引用的包(includeuse

//返回test字符串,5字节
char* get_str()
{
    char* s = malloc(5);
    memcpy(s,"test",5);
    return s;
}
//返回一个长度为len的字节数组,长度不能超过255
char* get_vec(unsigned char len)
{
    unsigned char* s = malloc(len);
    unsigned char i = 0;
    for(i = 0;i<len;i++)
        s[i] = i;
    return s;
}
//释放指针
void free_s(char* s)
{
    free(s);
}

可以看到,我们如果想给c#提供接口,首先需要双方已知的一个数组长度(字符串则要保证以\0结尾),并在对用后,使用dll接口提供的free接口释放掉这个malloc出来的空间

下面是功能一致的rust代码,具体解释请参考代码中的注释

use std::{convert::TryInto, ffi::CString, os::raw::c_char};

//返回test字符串,5字节
#[no_mangle]
pub extern "C" fn get_str() -> *mut c_char {
    CString::from(CString::new("test").unwrap()).into_raw()
}

//返回一个长度为len的字节数组,长度不能超过255
#[no_mangle]
pub extern "C" fn get_vec(len: u8) -> *mut c_char {
    //存储结果的数组
    let mut v: Vec<u8> = Vec::with_capacity(len.try_into().unwrap());
    for i in 0..len {
        v.push(i);
    }
    //直接将这个数组转成c字符串(字节数组)并返回指针
    unsafe { CString::from_vec_with_nul_unchecked(v).into_raw() }
}

//释放指针
#[no_mangle]
pub extern "C" fn free_s(s: *mut c_char) {
    unsafe {
        if s.is_null() {
            return;
        }
        //将指针重新转换为字符串
        CString::from_raw(s)
        //出作用域后将被rust自动释放
    };
}

可以看到,和c的逻辑大体相同,理论上可以返回任意字节数组

C#端的使用

因为有一个需要free的操作,所以在C#中需要务必确保获取后释放。当然,手写这个流程十分容易出bug,好在.net库中提供了一个自动处理的类,叫做SafeHandle

我们先将每个接口引用到c#中,这里我们新建一个类,起名为TestDLL

internal class TestDLL
{
    [DllImport("csharpdll.dll")]
    internal static extern void free_s(IntPtr str);
    [DllImport("csharpdll.dll")]
    internal static extern BytesHandle get_str();
    [DllImport("csharpdll.dll")]
    internal static extern BytesHandle get_vec(byte len);
}

这里的BytesHandle类就是我们接下来需要声明的类,它将接管我们返回的指针,并在变量被释放时自动调用dll中的free接口,具体代码如下:

internal class BytesHandle : SafeHandle
{
    public BytesHandle() : base(IntPtr.Zero, true) { }
    public override bool IsInvalid
    {
        get { return false; }
    }

    /// <summary>
    /// 将返回的结果当作字符串来读取
    /// </summary>
    /// <returns>读到的字节数组</returns>
    public string AsString()
    {
        //当前的指针位置
        var ptr = handle;
        //读到的数据,缓存
        List<byte> temp = new List<byte>();
        while(true)
        {
            //读一字节数据
            var c = Marshal.ReadByte(ptr);
            //不为0则表示字符串还没结束
            if (c != 0)
                temp.Add(c);
            else//说明字符串结束了
                break;
            ptr+=1;//指针向后挪1字节
        }
        return Encoding.Default.GetString(temp.ToArray());
    }

    /// <summary>
    /// 将返回的结果当作byte数组来读取
    /// </summary>
    /// <param name="len">读取的长度</param>
    /// <returns>读到的字节数组</returns>
    public byte[] AsBytes(byte len)
    {
        byte[] buffer = new byte[len];
        Marshal.Copy(handle, buffer, 0, buffer.Length);
        return buffer;
    }

    /// <summary>
    /// 调用dll中的free接口,释放资源
    /// </summary>
    protected override bool ReleaseHandle()
    {
        TestDLL.free_s(handle);
        return true;
    }
}

可以看到,返回的接口中的handle就是我们获取到的指针,通过自己编写的AsStringAsBytes接口来获取到我们需要的数据,并且在对象被释放时,会自动调用ReleaseHandle以释放dll返回出来的指针。这样省时省力,可以像下面这样来方便地调用

using(var s = TestDLL.get_str())
{
    var str = s.AsString();
    Console.WriteLine($"got string! {str}");
}
using (var b = TestDLL.get_vec(20))
{
    var bytes = b.AsBytes(20);
    Console.WriteLine($"got bytes! ");
    foreach(var item in bytes)
        Console.Write(item+" ");
}

输出如下:

got string! test
got bytes!
0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19

输出结果符合预期

更多用法

可以使用Marshal里的方法来获取其他类型的数组数据,甚至是结构体指针中的数据,这一点就不再赘述,留给大家自行探索了。

1 Comment

发表回复

您的电子邮箱地址不会被公开。 必填项已用 * 标注