在 DllImport 中使用 Unicode 字符串和用 Rust 编写的 DLL
Using Unicode strings in DllImport with a DLL written in Rust
我正在尝试从 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,这就是为什么它 returns 仅字符串的第一个字符。
知道如何将 Unicode 字符串作为参数传递给 Rust DLL 吗?是否可以像 print2
函数那样使事情变得更简单?
如果您检查 the documentation on CharSet
,您会看到 CharSet.Unicode
告诉 .NET 将字符串编组为 UTF-16( 即 每个代码两个字节观点)。因此,.NET 试图传递 printc
应该是 *const u16
, 而不是 的 *const libc::c_char
。当 CStr
开始计算字符串的长度时,它 看到的 如下:
b"y[=10=]e[=10=]y[=10=]e[=10=]y[=10=]e[=10=]"
也就是看到了一个个代码单元,然后是一个空字节,所以就停止了;因此为什么它说长度是“1”。
Rust 没有对 UTF-16 字符串的标准支持,但是 如果 你正在处理 Windows,有一些转换方法:在文档中搜索 OsStrExt
和 OsStringExt
。请注意,您必须使用随编译器安装的文档;网上的不包括。
遗憾的是,没有任何方法可以直接处理以 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 + "[=11=]");
}
然后像这样声明 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 安全类型,永远。如果你在这里的目的是将一个 "owned" 字符串传递给 Rust...我不知道它是否甚至 可能 与 .NET 一致。
旁白:Rust 中的 "FFI-safe" 本质上可以归结为:是内置的固定大小类型(即 不是 usize
/isize
),或者是附加了 #[repr(C)]
的用户定义类型。遗憾的是,文档中没有包含类型的 "FFI-safe"-ness。
我正在尝试从 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,这就是为什么它 returns 仅字符串的第一个字符。
知道如何将 Unicode 字符串作为参数传递给 Rust DLL 吗?是否可以像 print2
函数那样使事情变得更简单?
如果您检查 the documentation on CharSet
,您会看到 CharSet.Unicode
告诉 .NET 将字符串编组为 UTF-16( 即 每个代码两个字节观点)。因此,.NET 试图传递 printc
应该是 *const u16
, 而不是 的 *const libc::c_char
。当 CStr
开始计算字符串的长度时,它 看到的 如下:
b"y[=10=]e[=10=]y[=10=]e[=10=]y[=10=]e[=10=]"
也就是看到了一个个代码单元,然后是一个空字节,所以就停止了;因此为什么它说长度是“1”。
Rust 没有对 UTF-16 字符串的标准支持,但是 如果 你正在处理 Windows,有一些转换方法:在文档中搜索 OsStrExt
和 OsStringExt
。请注意,您必须使用随编译器安装的文档;网上的不包括。
遗憾的是,没有任何方法可以直接处理以 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 + "[=11=]");
}
然后像这样声明 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 安全类型,永远。如果你在这里的目的是将一个 "owned" 字符串传递给 Rust...我不知道它是否甚至 可能 与 .NET 一致。
旁白:Rust 中的 "FFI-safe" 本质上可以归结为:是内置的固定大小类型(即 不是 usize
/isize
),或者是附加了 #[repr(C)]
的用户定义类型。遗憾的是,文档中没有包含类型的 "FFI-safe"-ness。