在DLL中使用Unicode字符串和用Rust编写的DLL

本文关键字:DLL Rust Unicode 字符串 | 更新日期: 2023-09-27 18:14:50

我正在尝试从c#程序调用用Rust编写的DLL。DLL有两个简单的函数,它们接受字符串(以不同的方式)并打印到控制台。

Rust DLL代码

#![crate_type = "lib"]
extern crate libc;
use libc::{c_char};
use std::ffi::CStr;
#[no_mangle]
pub extern fn printc(s: *const c_char){
    let c_str : &CStr = unsafe {
        assert!(!s.is_null());
        CStr::from_ptr(s)
    };
    println!("{:?}", c_str.to_bytes().len()); //prints "1" if unicode
    let r_str = std::str::from_utf8(c_str.to_bytes()).unwrap();
    println!("{:?}", r_str);
}
#[no_mangle]
pub extern fn print2(string: String) {
    println!("{:?}", string)
}

c#控制台程序代码

[DllImport("lib.dll", CharSet = CharSet.Unicode, CallingConvention = CallingConvention.Cdecl)]
static extern void print2(ref string str);
[DllImport("lib.dll", CallingConvention = CallingConvention.Cdecl)]
static extern void printc(string str);
static void Main(string[] args)
{
  try
  {
    var graw = "yeyeye";
    printc(graw);
    print2(ref graw);
  }
  catch (Exception ex)
  {
    Console.WriteLine("calamity!, {0}", ex.Message);
  }
  Console.ReadLine();
}

对于print2函数,它一直在屏幕上打印垃圾,直到它导致AccessViolationException

第二个printc函数打印字符串,但仅当CharSet.Unicode未设置时。如果设置了它,它将只打印第一个字符,因此println!("{:?}", c_str.to_bytes().len());将打印1

我认为Cstr::from_ptr函数不支持Unicode,这就是为什么它只返回字符串的第一个字符。

任何想法如何将Unicode字符串作为参数传递给Rust dll ?是否有可能使事情更简单,就像在print2功能?

在DLL中使用Unicode字符串和用Rust编写的DLL

如果你查看CharSet的文档,你会看到CharSet.Unicode告诉。net将字符串封送为UTF-16 (即。每个代码点两个字节)。因此,. net试图传递printc应该是一个*const u16而不是一个*const libc::c_char。当CStr开始计算字符串的长度时,它看到的如下所示:

b"y'0e'0y'0e'0y'0e'0"

也就是说,它看到一个代码单元,然后是一个空字节,所以它停止;这就是为什么它说长度是"1"。

Rust没有对UTF-16字符串的标准支持,但是如果在Windows上工作,有一些转换方法:在文档中搜索OsStrExtOsStringExt。注意必须使用与编译器一起安装的文档;网上的没有。

遗憾的是,没有什么可以直接处理以空结尾的UTF-16字符串。您需要编写一些不安全的代码来将*const u16转换为可以传递给OsStringExt::from_wide&[u16]

现在,Rust 确实使用Unicode,但是它使用UTF-8。遗憾的是,没有直接的方法让。net将字符串封送为UTF-8。使用任何其他编码似乎都会丢失信息,所以你要么必须在Rust端显式地处理UTF-16,要么在c#端显式地处理UTF-8。

在c#中将字符串重新编码为UTF-8要简单得多。您可以利用。net将数组封送为指向第一个元素的原始指针(就像C一样),并传递一个以空结尾的UTF-8字符串。

首先,一个静态方法,用于获取。net字符串并生成存储在字节数组中的UTF-8字符串:

byte[] NullTerminatedUTF8bytes(string str)
{
    return Encoding.GetBytes(str + "'0");
}

然后这样声明Rust函数的签名:

[DllImport(dllname, CallingConvention = CallingConvention.Cdecl)]
static extern void printc([In] byte[] str);

最后,这样调用它:

printc(NullTerminatedUTF8bytes(str));

对于奖励点,您可以重新制作printc,而不是采取*const u8 u32,传递重新编码的字符串加上它的长度;那么您就不需要空终止符,并且可以使用std::slice::from_raw_parts函数重建字符串(但这开始超出了最初的问题)。

至于print2,那是不可行的,. net对Rust的String类型一无所知,而且它在中不可能与。net字符串兼容。更重要的是,String甚至没有保证的布局,所以安全地绑定到它或多或少是不可能的。 所有这些都是非常冗长的说法:不要在跨语言函数中使用String或任何其他非ffi安全类型,永远不要。如果你的意图是在Rust中传递一个"owned"字符串…我甚至不知道它是否可能与。net协同工作。

Aside: Rust中的"FFI-safe"本质上归结为:要么是内置的固定大小类型(即;不是 usize/isize),或者是附加#[repr(C)]的用户定义类型。遗憾的是,文档中没有包含类型的"FFI-safe"性。