在 Windows crate 中使用字符串
Windows API 中有几种字符串类型,包括
PSTR
/PCSTR
:指向由 char (u8) 组成的以 null 结尾的字符串的指针。字符串应使用当前线程的代码页进行编码。“C”表示“常量”(只读)字符串。PWSTR
/PCWSTR
:指向由“宽字符”(u16) 组成的以 null 结尾的字符串的指针,使用 UTF-16 编码。BSTR
:一种二进制字符串,通常用于 COM/OLE 函数。它由 u16 字符组成,后跟一个 null 终止符。字符串的长度预先置于指针之前,有些函数会使用它们来传递任意二进制数据(包括包含 null 的数据),依赖于前缀而不是终止符。然而,它们*通常*可以像普通的、以 null 结尾的宽字符串一样使用。HSTRING
:Windows Runtime 字符串的句柄。HSTRING 是 UTF-16 且不可变。
请注意,您可以将 BSTR 或 HSTRING 传递给期望 PCWSTR 的函数。
不幸的是,这些类型都没有与 Rust 类型一一对应。然而,我们可以使用 windows-strings
crate 来帮助我们。
API 函数的类型(窄或宽)
Win32 API 将字符串函数分为“窄”版本(以“A”结尾,例如 MessageBoxA
)和“宽”版本(以“W”结尾,例如 MessageBoxW
)。API 的窄版本期望 u8 字节字符串,使用当前线程的代码页进行编码,而宽版本期望 UTF-16。
作为一般建议,您应该优先选择宽版本;在 Rust 的 UTF-8 字符串和 Windows 的 UTF-16 之间进行转换要容易得多。
调用消耗字符串的 API
让我们看一个使用简单 MessageBox
函数的示例,它显示一个弹出对话框。我们将在此示例中使用宽版本(MessageBoxW
)。
如果您想使用字符串字面量调用 Windows API,windows-strings
crate 具有从字符串字面量生成 Windows 字符串的宏
h!
从字符串字面量生成一个 HSTRING,添加一个 null 终止符并转换为 UTF-16。w!
作用相同,但生成一个 PCWSTR 而不是 HSTRING。s!
生成一个带 null 终止符的 PCSTR。*注意:这不执行任何转换,它只是添加一个 null 终止符。*
如果我们要调用消息框,我们可以使用带有 Win32_UI_WindowsAndMessaging
功能的 windows crate,并调用
#![allow(unused)] fn main() { // use string literals when calling a message box. let text = h!("Hello from rust!"); let caption = h!("From Rust"); unsafe { // call the MessageBox function and return MESSAGEBOX_RESULT UI::WindowsAndMessaging::MessageBoxW(None, text, caption, UI::WindowsAndMessaging::MESSAGEBOX_STYLE(0) // message box OK ) } }
这有效,但是如果我们想用 Rust 字符串调用相同的函数呢?这就稍微复杂一些了。我们可以手动转换为 UTF-16 字节序列,然后自己添加 null 终止符,就像这样
#![allow(unused)] fn main() { // this works for any &str, not just literals let text = "I am a message to display!"; let caption = "Message from Rust!"; // convert our text and caption to UTF-16 bytes, // add null terminators using chain, and then collect // the result into a vec let text = text.encode_utf16() .chain(iter::once(0u16)) .collect::<Vec<u16>>(); let caption = caption.encode_utf16() .chain(iter::once(0u16)) .collect::<Vec<u16>>(); // call the API, wrapping our vec pointer in a PCWSTR struct. unsafe { UI::WindowsAndMessaging::MessageBoxW(None, PCWSTR(text.as_ptr()), PCWSTR(caption.as_ptr()), UI::WindowsAndMessaging::MESSAGEBOX_STYLE(0) // message box OK ) } }
然而,这很麻烦——我们可以使用 windows-strings
crate 中的便利功能,通过将 Rust 字符串转换为 HSTRING 来大大简化这一点。
#![allow(unused)] fn main() { let text = "I am a message to display!"; let caption = "Message from Rust!"; // convert our strings into UTF-16 // this incurrs a performance cost because there is a copy + conversion // from the standard rust utf-8 string. // we are using HSTRING, which is an immutable UTF-16 string // in the windows-strings crate. It can be generated from a standard // rust string, and it can be used in place of a PCWSTR anywhere in the // windows API. unsafe { UI::WindowsAndMessaging::MessageBoxW(None, &HSTRING::from(text), &HSTRING::from(caption), UI::WindowsAndMessaging::MESSAGEBOX_STYLE(0) // message box OK ) } }
这更符合人体工程学——它为您处理 null 终止和 UTF-16 转换。
调用生成字符串的 API
生成字符串的 Windows API 通常需要两步调用。第一次调用 API 时,您为字符串缓冲区传递一个 NULL 指针,并检索要生成的字符串的长度。
这允许您相应地分配缓冲区,然后使用适当大小的缓冲区再次调用函数。
在此示例中,我们将使用 GetComputerNameW
函数。这需要 windows crate 中的 Win32_System_WindowsProgramming
功能。
#![allow(unused)] fn main() { let mut buff_len = 0u32; unsafe { // this function will return an error code because it // did not actually write the string. This is normal. let e = GetComputerNameW(None, &mut buff_len).unwrap_err(); debug_assert_eq!(e.code(), HRESULT::from(ERROR_BUFFER_OVERFLOW)); } // buff len now has the length of the string (in UTF-16 characters) // the function would like to write. This *does include* the // null terminator. Let's create a vector buffer and feed that to the function. let mut buffer = Vec::<u16>::with_capacity(buff_len as usize); unsafe { WindowsProgramming::GetComputerNameW( Some(PWSTR(buffer.as_mut_ptr())), &mut buff_len).unwrap(); // set the vector length // buff_len now includes the size, which *does not include* the null terminator. // let's set the length to just before the terminator so we don't have to worry // about it in later conversions. buffer.set_len(buff_len); } // we can now convert this to a valid Rust string // omitting the null terminator String::from_utf16_lossy(&buffer) }
值得一提的是长度参数的工作方式。对于 GetComputerNameW
- 在输入时,它表示缓冲区的大小*(包括 null 终止符)*,以 wchar 为单位。
- 如果函数返回缓冲区溢出,则返回的长度参数是它需要的缓冲区大小*(包括 null 终止符)*,以 wchars 为单位。
- 如果函数成功写入缓冲区,则长度是写入的 wchars 数量*(不包括 null 终止符)*。
此行为在函数的文档中有所记载——使用 Windows API 时,请务必小心并检查函数对 null 终止符的期望。
无论如何,这确实有效,但我们可以做得更好。计算机名称最多只能是 MAX_COMPUTERNAME_LENGTH
,即区区 16 个字符。由于我们在编译时知道缓冲区长度,因此我们可以避免在此处进行堆分配,而只需使用数组。
#![allow(unused)] fn main() { // avoid the heap allocation since we already know how big this // buffer needs to be at compile time. let mut name = [0u16; MAX_COMPUTERNAME_LENGTH as usize + 1]; let mut len = name.len() as u32; // we can also skip the two-step call, since we know our buffer // is already larger than any possible computer name unsafe { GetComputerNameW( Some(PWSTR(name.as_mut_ptr())), &mut len) .unwrap(); } // the function writes to len with the number of // UTF-16 characters in the string. We can use this // to slice the buffer. String::from_utf16_lossy(&name[..len as usize]) }
然而,如果我们不介意堆分配(和一些额外的系统调用),还有一种更符合人体工程学的选项。windows-strings
crate 包含 HStringBuilder
,我们可以用它代替数组。这使我们能够进行更简单的转换。
#![allow(unused)] fn main() { // pre-allocate a HSTRING buffer on the heap // (you do not need to add one to len for the null terminator, // the hstring builder will handle that automatically) let mut buffer = HStringBuilder::new( MAX_COMPUTERNAME_LENGTH as usize); let mut len = buffer.len() as u32 + 1; unsafe { GetComputerNameW( Some(PWSTR(buffer.as_mut_ptr())), &mut len).unwrap(); } // we can now generate a valid HSTRING from the HStringBuilder let buffer = HSTRING::from(buffer); // and we can now return a rust string from the HSTRING: buffer.to_string_lossy() }
如果您需要直接处理 UTF-16 字符串,请考虑使用 widestring
crate,它支持 UTF-16。这将使您能够推入/弹出/追加元素,而无需将字符串转换为本机 Rust UTF-8 字符串。为了完整起见,这里有一个返回最宽字符串并追加一些感叹号的示例。
#![allow(unused)] fn main() { // for this example, we'll just use an array again let mut name = [0u16; MAX_COMPUTERNAME_LENGTH as usize + 1]; let mut len = name.len() as u32; unsafe { GetComputerNameW( Some(PWSTR(name.as_mut_ptr())), &mut len) .unwrap(); } // we can make a UTF16Str slice directly from the buffer, // without needing to do any copy. This will error if the buffer // isn't valid UTF-16. let wstr = Utf16Str::from_slice(&name[..len as usize]) .unwrap(); // this can be displayed as is. println!("Computer name is {}", wstr); // we can also transfer it into owned string, which can // be appended or modified. let mut wstring = Utf16String::from(wstr); // let's append another string. We'll use a macro to avoid // any UTF conversion at runtime. wstring = wstring + utf16str!("!!!"); }