键盘快捷键

在章节间导航

S/ 在书中搜索

? 显示此帮助

Esc 隐藏此帮助

在 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!("!!!");
}