上一篇文章《如何用C#调用RUST的DLL》介绍了如何在c#中调用rust的dll,但是那篇文章只演示了传递基本类型,和使用c#传入数组到dll接口。如果我们需要从dll中读取数组或字符串数据,应该如何操作呢?本篇文章就将描述具体的实现方法
dll端的接口
这里我们用c来当作类比,另外,这里的代码将忽略引用的包(include
与use
)
//返回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
就是我们获取到的指针,通过自己编写的AsString
与AsBytes
接口来获取到我们需要的数据,并且在对象被释放时,会自动调用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
里的方法来获取其他类型的数组数据,甚至是结构体指针中的数据,这一点就不再赘述,留给大家自行探索了。