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

我试图从C#程序调用Rust编写的DLL。 DLL有两个简单的函数,它们以不同的方式进行stings并打印到控制台。

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未设置CharSet.Unicode 。 如果设置了,它将只打印第一个char,因此println!("{:?}", c_str.to_bytes().len()); 将打印1

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

知道如何将Unicode字符串作为参数传递给Rust DLL吗? 是否有可能像print2函数一样简单?

如果查看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 。 请注意,您必须使用随编译器一起安装的文档; 在线的人不会包含它。

遗憾的是,没有什么可以直接处理以null结尾的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)并传递以null结尾的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 ,传递重新编码的字符串加上它的长度; 那么你不需要null终止符,并且可以使用std::slice::from_raw_parts函数重建字符串(但这开始超出原始问题)。

至于print2 ,那个是不可行的。 .NET对Rust的String类型一无所知 ,它与.NET字符串完全不兼容。 更重要的是, String甚至没有保证的布局,因此安全地绑定它或多或少是不可能的。

所有这些都是一种非常冗长的说法:不要在跨语言函数中使用String或任何其他非FFI安全类型。 如果您的意图是将“拥有”字符串传递给Rust …我不知道是否可以与.NET协同工作。

另外 :Rust中的“FFI-safe”基本上归结为:是内置的固定大小类型( 不是 usize / isize ),或者是附加了#[repr(C)]的用户定义类型。 遗憾的是,文档中不包含类型的“FFI安全”。