键盘快捷键

在章节间导航

S/ 在书中搜索

? 显示此帮助

Esc 隐藏此帮助

Kenny Kerr 是 Microsoft 的一名软件工程师,他致力于 Windows 操作系统的 C++ 和 Rust 工具与库的开发。他是 C++/WinRTRust for Windows 的创建者。Kenny 还曾为 C/C++ Users Journal、Visual Studio Magazine、MSDN Magazine(最初名为 Microsoft Systems Journal)撰写定期专栏和专题文章。

Rust 入门

Kenny 在 Pluralsight 上的课程

Kenny 在 GitHub 上

Kenny 在 YouTube 上

Kenny 在 LinkedIn 上

Kenny 在 WordPress 上的旧博客

Kenny 在 asp.net 上的旧博客

Rust 入门

windows-rs 项目已经推出一段时间了,虽然我还有大量工作要做,但我觉得我应该开始花一些时间来撰写关于 Rust for Windows 的文章,而不仅仅是构建 Rust for Windows。😊 就像我为 C++/WinRT 所做的那样,我想我会开始写一些简短的“如何做”或“它是如何工作的”文章,以帮助开发人员理解 windows-rs 项目的一些基础知识。

其中一些主题对于 Rust 开发人员来说可能很明显,但对于刚接触 Rust 的 Windows 开发人员来说可能不明显。其他主题可能对 Windows 开发人员来说很明显,但对于刚接触 Windows 的经验丰富的 Rust 开发人员来说则不那么明显。无论哪种情况,我都希望您觉得有用。如果您有任何问题,请随时在仓库中提出问题

选择 windows 和 windows-sys crates

windows crate 提供了 Windows API 的绑定,包括像 CreateThreadpool 这样的 C 风格 API 以及 DirectX 等 COM 和 WinRT API。这个 crate 提供了最全面的 Windows 操作系统 API 覆盖。在可能的情况下,windows crate 还尝试为 Rust 开发人员提供更符合习惯且安全的编程模型。

windows-sys crate 提供了 C 风格 Windows API 的原始绑定。它缺乏对 COM 和 WinRT API 的支持。windows-sys crate 的诞生源于一个认识,即 windows crate 在构建时间方面最昂贵的方面是编译函数体的成本。Rust 编译器在编译函数体上花费了大量精力,因此只包含声明的 windows crate 版本相比之下要小得多,速度也快得多。问题是 COM 风格的虚函数调用在 Rust 中需要额外的代码生成(与 C++ 不同),这反过来又导致编译时间变慢。windows-sys crate 应运而生。

当然,我们将继续努力提高底层 Rust 编译器工具链的性能以及为这些 crate 生成的代码的效率。因此,我们相信编译时间将继续改进。

你需要什么?windowswindows-sys
快速编译时间是您的首要考虑因素之一
您需要 no_std 支持
您需要 COM 或 WinRT 支持
您更喜欢使用符合 Rust 习惯的 API
最低支持的 Rust 版本1.561.56

这些 crate 是如何构建的?

windowswindows-sys crates 是从描述 Windows API 的元数据生成的。最初只有 WinRT API 包含元数据,但现在也为旧的 C 和 COM API 提供了元数据。win32metadata 项目提供了生成元数据的工具,而 windows-metadatawindows-bindgen crates 用于读取元数据并生成 windowswindows-sys crates。绑定是根据各个 crate 的不同目标以不同方式生成的。您可以在这里找到用于生成特定版本 windowswindows-sys crates 的确切元数据文件。

如何查找特定的 API?

首先选择您想使用的 crate。然后搜索所选 crate 的文档

请注意,文档中包含一个说明,指示需要启用哪些功能才能访问特定的 API。

包含哪些 API?

除了少数例外,Windows SDK 提供的所有 Windows API 都包含在内。这些 API 的定义是从元数据收集并转换为 Rust 绑定的。生成 Rust 绑定的过程特意省略了一些 API。只有当 API (1) 不适合 Rust 开发人员,并且 (2) 对 windowswindows-sys crates 的总体大小造成很大影响时,才会排除 API。

Xaml API 被排除在外,因为它在没有 Xaml 团队才能提供的直接语言支持的情况下几乎无法使用。Xaml 也专注于为 C# 应用程序开发量身定制,因此此 API 不适用于 Rust 开发人员。MsHtml API 也被排除在外,因为它仅适用于 Microsoft 较旧的脚本语言,如 JScript 和 VBScript。按代码行数衡量,它也是迄今为止最大的单个模块。除此之外,还排除了一些已弃用且无法使用的 API。您可以确切地看到 windows crate 排除了什么以及 windows-sys crate 排除了什么

除此之外,windows-sys crate 目前排除了所有 COM 和 WinRT API。windows-sys crate 只包含声明,如果没有 windows crate 提供的抽象,COM 和 WinRT 调用会非常繁琐。这里有一些关于在 windows 和 windows-sys crate 之间进行选择的提示。

我最喜欢的 Windows SDK 宏在哪里?

windowswindows-sys crates 是从元数据生成的。此元数据只包含类型定义和函数签名,不包含宏、仅头文件函数或函数体。您可能会在 windows crate 中找到一些常见 C/C++ 辅助宏和函数的等价物,但总的来说,这些宏在 windowswindows-sys crate 中没有直接的等价物。

使用 windows crate 调用您的第一个 API

所以你想尝试调用一个简单的 Windows API。从哪里开始呢?让我们看看一个相对简单的 API,用于向线程池提交回调。您可以在这里阅读更多关于此 API 的信息

第一步是添加对 windows crate 的依赖,并指出您希望访问哪些功能

[dependencies.windows]
version = "0.52"
features = [
    "Win32_Foundation",
    "Win32_System_Threading",
]

为什么是这两个功能?线程池 API 在 Win32::System::Threading 模块中定义,我们还将使用 Win32::Foundation 模块中的少量定义。如果您不确定,任何给定 API 的文档都提供了一个有用的注释,指示需要哪些功能。例如,这是 WaitForThreadpoolWorkCallbacks 的文档,您可以看到它依赖于这两个功能,因为它在 Win32::System::Threading 模块中定义,并且依赖于 Win32::Foundation 模块中定义的 BOOL

Cargo 现在将处理繁重的工作,跟踪依赖项并确保导入库存在,这样我们就可以在 Rust 中简单地调用这些 API,而无需任何进一步的配置。我们可以使用 use 声明使这些 API 更易于访问

#![allow(unused)]
fn main() {
use windows::{core::Result, Win32::System::Threading::*};
}

为了“证明”代码有效但又保持简单,我们只使用线程池将计数器递增若干次。这里我们可以使用读写锁来安全地多线程访问计数器变量

#![allow(unused)]
fn main() {
static COUNTER: std::sync::RwLock<i32> = std::sync::RwLock::new(0);
}

对于这个例子,我将只使用一个简单的 main 函数和一个大的 unsafe 块,因为这里几乎所有东西都是 unsafe。为什么会这样?因为 windows crate 允许你调用外部函数,而这些函数通常被认为是 unsafe 的。

fn main() -> Result<()> {
    unsafe {
        
    }

    Ok(())
}

线程池 API 被建模为一组通过传统 C 风格 API 公开的“对象”。我们首先需要做的是创建一个工作对象

#![allow(unused)]
fn main() {
let work = CreateThreadpoolWork(Some(callback), None, None)?;
}

第一个参数是指向回调函数的指针。其余参数是可选的,您可以在我的 MSDN 线程池系列中阅读更多关于它们的信息。

回调本身必须是有效的 C 风格回调,符合线程池 API 预期的签名。这是一个简单的回调,它将增加计数

#![allow(unused)]
fn main() {
extern "system" fn callback(_: PTP_CALLBACK_INSTANCE, _: *mut std::ffi::c_void, _: PTP_WORK) {
    let mut counter = COUNTER.write().unwrap();
    *counter += 1;
}
}

参数可以安全地忽略,但有时确实很有用。此时,我们有一个有效的工作对象,但什么都没有发生。为了启动一些“工作”,我们需要将工作对象提交给线程池。您可以根据需要多次提交,所以让我们继续做十次

#![allow(unused)]
fn main() {
for _ in 0..10 {
    SubmitThreadpoolWork(work);
}
}

现在您可以预期回调会并发运行,因此上面使用了 RwLock。当然,有了所有的并发性,我们需要某种方式来判断工作何时完成。这就是 WaitForThreadpoolWorkCallbacks 函数的工作

#![allow(unused)]
fn main() {
WaitForThreadpoolWorkCallbacks(work, false);
}

第二个参数指示我们是否要取消任何尚未开始执行的待处理回调。在这里传入 false 表示我们希望等待函数阻塞,直到所有提交的工作都已完成。此时,我们可以安全地关闭工作对象以释放其内存

#![allow(unused)]
fn main() {
CloseThreadpoolWork(work);
}

为了证明它可靠地工作,我们可以打印出计数器的值

#![allow(unused)]
fn main() {
let counter = COUNTER.read().unwrap();
println!("counter: {}", *counter);
}

运行示例应该打印出如下内容

counter: 10

这是完整的示例供参考

使用 windows-sys crate 调用您的第一个 API

所以你想尝试调用一个简单的 Windows API。从哪里开始呢?让我们看看一个相对简单的 API,用于向线程池提交回调。您可以在这里阅读更多关于此 API 的信息

第一步是添加对 windows-sys crate 的依赖,并指出您希望访问哪些功能

[dependencies.windows-sys]
version = "0.52"
features = [
    "Win32_Foundation",
    "Win32_System_Threading",
]

为什么是这两个功能?线程池 API 在 Win32::System::Threading 模块中定义,我们还将使用 Win32::Foundation 模块中的少量定义。如果您不确定,任何给定 API 的文档都提供了一个有用的注释,指示需要哪些功能。例如,这是 WaitForThreadpoolWorkCallbacks 的文档,您可以看到它依赖于这两个功能,因为它在 Win32::System::Threading 模块中定义,并且依赖于 Win32::Foundation 模块中定义的 BOOL

Cargo 现在将处理繁重的工作,跟踪依赖项并确保导入库存在,这样我们就可以在 Rust 中简单地调用这些 API,而无需任何进一步的配置。我们可以使用 use 声明使这些 API 更易于访问

#![allow(unused)]
fn main() {
use windows_sys::{Win32::Foundation::*, Win32::System::Threading::*};
}

为了“证明”代码有效但又保持简单,我们只使用线程池将计数器递增若干次。这里我们可以使用读写锁来安全地多线程访问计数器变量

#![allow(unused)]
fn main() {
static COUNTER: std::sync::RwLock<i32> = std::sync::RwLock::new(0);
}

对于这个例子,我将只使用一个简单的 main 函数和一个大的 unsafe 块,因为这里几乎所有东西都是 unsafe。为什么会这样?因为 windows crate 允许你调用外部函数,而这些函数通常被认为是 unsafe 的。

fn main() {
    unsafe {
        
    }
}

线程池 API 被建模为一组通过传统 C 风格 API 公开的“对象”。我们首先需要做的是创建一个工作对象

#![allow(unused)]
fn main() {
let work = CreateThreadpoolWork(Some(callback), std::ptr::null_mut(), std::ptr::null());
}

第一个参数是指向回调函数的指针。其余参数是可选的,您可以在我的 MSDN 线程池系列中阅读更多关于它们的信息。

由于此函数分配内存,它可能会失败,这通过返回空指针而不是有效的工作对象句柄来指示。我们将检查此条件并调用 GetLastError 函数以显示任何相关的错误代码

#![allow(unused)]
fn main() {
if work == 0 {
    println!("{:?}", GetLastError());
    return;
}
}

回调本身必须是有效的 C 风格回调,符合线程池 API 预期的签名。这是一个简单的回调,它将增加计数

#![allow(unused)]
fn main() {
extern "system" fn callback(_: PTP_CALLBACK_INSTANCE, _: *mut std::ffi::c_void, _: PTP_WORK) {
    let mut counter = COUNTER.write().unwrap();
    *counter += 1;
}
}

参数可以安全地忽略,但有时确实很有用。此时,我们有一个有效的工作对象,但什么都没有发生。为了启动一些“工作”,我们需要将工作对象提交给线程池。您可以根据需要多次提交,所以让我们继续做十次

#![allow(unused)]
fn main() {
for _ in 0..10 {
    SubmitThreadpoolWork(work);
}
}

现在您可以预期回调会并发运行,因此上面使用了 RwLock。当然,有了所有的并发性,我们需要某种方式来判断工作何时完成。这就是 WaitForThreadpoolWorkCallbacks 函数的工作

#![allow(unused)]
fn main() {
WaitForThreadpoolWorkCallbacks(work, 0);
}

第二个参数指示我们是否要取消任何尚未开始执行的待处理回调。在这里传入 0(表示 false)表示我们希望等待函数阻塞,直到所有提交的工作都已完成。此时,我们可以安全地关闭工作对象以释放其内存

#![allow(unused)]
fn main() {
CloseThreadpoolWork(work);
}

为了证明它可靠地工作,我们可以打印出计数器的值

#![allow(unused)]
fn main() {
let counter = COUNTER.read().unwrap();
println!("counter: {}", *counter);
}

运行示例应该打印出如下内容

counter: 10

这是完整的示例供参考

调用您的第一个 COM API

COM API 的独特之处在于它们通过接口公开功能。接口只是一组虚拟函数指针,它们被分组在一个称为 vtable(或虚拟函数表)的结构中。这不是 Rust 直接支持的,像 C++ 那样,但 windows crate 提供了必要的代码生成,使其成为可能且无缝。COM API 通常仍然通过传统的 C 风格函数调用开始其生命周期,以获取 COM 接口。然后,您可以通过该接口调用其他方法。

一些基于 COM 的 API 可能非常复杂,所以让我们从一个非常简单的例子开始。CreateUri 函数在 MSDN 上有官方文档,它返回表示解析给定 URI 结果的 IUri 接口。Rust windows crate 的文档表明它位于 Win32::System::Com 模块中,因此我们可以相应地配置我们的 windows crate 依赖项

[dependencies.windows]
version = "0.52"
features = [
    "Win32_System_Com",
]

我们可以使用 use 声明使这个 API 更易于访问。windows crate 的 core 模块也提供了一些辅助函数,使 COM 接口的使用更容易,所以我们也会包含它

#![allow(unused)]
fn main() {
use windows::{core::*, Win32::System::Com::*};
}

对于这个例子,我将只使用一个简单的 main 函数和一个大的 unsafe 块,因为这里几乎所有东西都是 unsafe。为什么会这样?因为 windows crate 允许你调用外部函数,而这些函数通常被认为是 unsafe 的。

fn main() -> Result<()> {
    unsafe {
        
        Ok(())
    }
}

这里唯一“有趣”的地方是使用了 windows::core 模块中的 Result 类型,它提供了 Windows 错误处理以简化后续的 API 调用。有了它,我们可以按如下方式调用 CreateUri 函数

#![allow(unused)]
fn main() {
let uri = CreateUri(w!("http://kennykerr.ca"), Uri_CREATE_CANONICALIZE, 0)?;
}

这里发生了很多事情。第一个参数实际上是一个 PCWSTR,它表示许多 Windows API 使用的以 null 结尾的宽字符串。windows crate 提供了方便的 w! 宏,用于创建有效的以 null 结尾的宽字符串作为编译时常量。第二个参数只是官方文档指定的默认标志。第三个参数是保留的,因此应为零。

生成的 IUri 对象具有各种方法,我们现在可以使用它们来检查 URI。官方文档描述了各种接口方法,Rust 文档让您可以快速浏览它们的各种签名,以便您可以快速了解如何在 Rust 中调用它们。对于这个例子,我们只调用其中两个来打印出 URI 的域和 HTTP 端口号

#![allow(unused)]
fn main() {
let domain = uri.GetDomain()?;
let port = uri.GetPort()?;

println!("{domain} ({port})");
}

在底层,这些方法将通过 COM 接口调用虚函数,并进入 API 提供的实现。它们还提供了一堆错误和签名转换,使其在 Rust 中使用起来非常自然。就是这样,运行示例应该打印出如下内容

kennykerr.ca (80)

这是完整的示例供参考

调用您的第一个 WinRT API

Windows 8 引入了 Windows Runtime,其核心是 COM,并加入了一些额外的约定,使语言绑定看起来更加无缝。windows crate 已经使调用 COM API 比 C++ 开发人员更无缝,但 WinRT 通过提供对构造函数、事件和类层次结构等建模的一流支持,进一步提升了这一体验。在调用您的第一个 COM API中,我们看到您仍然需要通过 C 风格的 DLL 导出引导 API,然后才能调用 COM 接口方法。WinRT 的工作方式相同,但以通用方式抽象了这一点。

让我们用一个简单的例子来说明。XmlDocument“类”模型了一个可以从各种来源加载的 XML 文档。Rust windows crate 的文档表明此类型位于 Data::Xml::Dom 模块中,因此我们可以按如下方式配置 windows crate 依赖项

[dependencies.windows]
version = "0.52" 
features = [
    "Data_Xml_Dom",
]

我们可以使用 use 声明使这个 API 更易于访问。windows crate 的 core 模块只提供了一些辅助函数,使 Windows API 的使用更容易,所以我们也会包含它

#![allow(unused)]
fn main() {
use windows::{core::*, Data::Xml::Dom::XmlDocument}; 
}

对于这个例子,我将只使用一个简单的 main 函数,其中包含来自 windows::core 模块的 Result 类型,以提供自动错误传播并简化后续的 API 调用

fn main() -> Result<()> {

    Ok(())
}

与之前的 Win32 和 COM 示例不同,您会注意到这个 main 函数不需要 unsafe 块,因为 WinRT 调用由于其更受约束的类型系统而被认为是安全的。

首先,我们可以简单地调用 new 方法来创建一个新的 XmlDocument 对象

#![allow(unused)]
fn main() {
let doc = XmlDocument::new()?;
}

这看起来更像一个惯用的 Rust 类型,而不是典型的 COM API,但在底层,使用了类似的机制通过 DLL 导出实例化 XmlDocument 实现。然后我们可以调用 LoadXml 方法进行测试。还有其他各种选项可以从不同的源加载 XML,您可以在官方文档XmlDocument API 的 Rust 文档中阅读。windows crate 还提供了方便的 h! 宏来创建 HSTRING,这是 WinRT API 使用的字符串类型

#![allow(unused)]
fn main() {
doc.LoadXml(h!("<html>hello world</html>"))?;
}

就这样,我们有了一个完整的 Xml 文档,我们可以对其进行检查。对于这个例子,我们只获取文档元素,然后进行一些基本查询,如下所示

#![allow(unused)]
fn main() {
let root = doc.DocumentElement()?;
assert!(root.NodeName()? == "html");
println!("{}", root.InnerText()?);
}

首先我们断言元素的名称实际上是“html”,然后打印出元素的内部文本。与之前的 COM 示例一样,这些方法都通过 COM 接口调用虚函数,但 windows crate 使直接从 Rust 进行此类调用变得非常简单。就是这样。运行示例应该打印出如下内容

hello world

这是完整的示例供参考

如何查询特定的 COM 接口?

windows crate 中的 COM 和 WinRT 接口实现了 ComInterface 特性。此特性提供了 cast 方法,该方法将在底层使用 QueryInterface 将当前接口转换为对象支持的另一个接口。cast 方法返回一个 Result,以便可以在 Rust 中以自然的方式处理失败。

例如,通常需要获取给定 Direct3D 设备的 IDXGIDevice 接口,以便与其他渲染 API 进行互操作。这就是如何为 Direct3D 设备创建交换链以进行绘制和呈现的方法。让我们设想一个简单的函数,它接受一个 Direct3D 设备并返回底层的 DXGI 工厂

#![allow(unused)]
fn main() {
fn get_dxgi_factory(device: &ID3D11Device) -> Result<IDXGIFactory2> {
}
}

您需要做的第一件事是查询或转换 Direct3D 设备的 DXGI 接口,如下所示

#![allow(unused)]
fn main() {
let device = device.cast::<IDXGIDevice>()?;
}

如果更方便,您也可以利用类型推断,如下所示

#![allow(unused)]
fn main() {
let device: IDXGIDevice = device.cast()?;
}

有了 COM 接口,我们需要一个 unsafe 块来调用其方法

#![allow(unused)]
fn main() {
unsafe {
}
}

unsafe 块中,我们可以检索设备的物理适配器

#![allow(unused)]
fn main() {
let adapter = device.GetAdapter()?;
}

为了好玩(或调试),我们可能会打印出适配器的名称

#![allow(unused)]
fn main() {
if cfg!(debug_assertions) {
    let mut desc = Default::default();
    adapter.GetDesc(&mut desc)?;
    println!("{}", String::from_utf16_lossy(&desc.Description));
}
}

最后,我们可以返回适配器的父级,以及设备的 DXGI 工厂对象

#![allow(unused)]
fn main() {
adapter.GetParent()
}

运行示例后,我得到了以下令人印象深刻的结果

AMD FirePro W4100

这是一个更全面的 DirectX 示例

cast 方法对于 WinRT 类和接口同样有效。它对于与 WinRT API 互操作特别有用。

如何实现现有的 COM 接口?

在某些情况下,您可能需要实现一个现有的 COM 接口,而不是简单地调用操作系统提供的现有实现。这就是 implement 功能和宏派上用场的地方。windows crate 提供了可选的实现支持,隐藏在 implement 功能后面。启用后,implement 宏可用于实现任意数量的 COM 接口。该宏负责实现 IUnknown 本身。

让我们实现一个由 Windows 定义的简单接口来演示。IPersist 接口定义在 Win32::System::Com 模块中,所以我们首先添加对 windows crate 的依赖,并包含 Win32_System_Com 功能

[dependencies.windows]
version = "0.52"
features = [
    "implement",
    "Win32_System_Com",
]

implement 功能解锁了实现支持。

implement 宏由 windows::core 模块包含,所以我们通过如下方式将其全部包含,以保持简单

#![allow(unused)]
fn main() {
use windows::{core::*, Win32::System::Com::*};
}

现在是实现的时候了

#![allow(unused)]
fn main() {
#[implement(IPersist)]
struct Persist(GUID);
}

implement 宏将为 IUnknown 接口的生命周期管理和属性中包含的任何接口的接口发现提供必要的实现。在这种情况下,只实现 IPersist

实现本身由遵循 _Impl 模式的特性定义,我们需要为我们的实现实现它,如下所示

#![allow(unused)]
fn main() {
impl IPersist_Impl for Persist_Impl {
    fn GetClassID(&self) -> Result<GUID> {
        Ok(self.0)
    }
}
}

IPersist 接口,最初在此处记录,只有一个方法返回 GUID,所以我们只需通过返回我们实现中包含的值来实现它。window crate 和 implement 宏将通过提供实际的 COM 虚函数调用和虚函数表布局来完成其余工作,这些是将其转换为堆分配和引用计数的 COM 对象所需的。

剩下要做的就是将实现通过 Into trait 移动或装箱到 implement 宏提供的 COM 实现中

#![allow(unused)]
fn main() {
let guid = GUID::new()?;
let persist: IPersist = Persist(guid).into();
}

此时,我们可以简单地将 persist 视为它是一个 COM 对象

#![allow(unused)]
fn main() {
let guid2 = unsafe { persist.GetClassID()? };
assert_eq!(guid, guid2);
println!("{:?}", guid);
}

这是一个完整的示例

如何为 WinRT 集合接口创建库存集合?

除了自己实现 COM 接口之外,windows crate 还为常见的 WinRT 集合接口提供库存集合实现。实现 WinRT 集合接口可能非常具有挑战性,因此在许多情况下,这应该可以为您节省大量精力。要使用这些库存实现,需要 implement 功能。

让我们考虑几个例子。WinRT 集合接口都定义在 Foundation::Collections 模块中,所以我们首先添加对 windows crate 的依赖,并包含 Foundation_Collections 功能

[dependencies.windows]
version = "0.52"
features = [
    "implement",
    "Foundation_Collections",
]

创建集合就像在现有 VecBTreeMap 上使用 TryFrom trait 一样简单,具体取决于集合的类型

WinRT 接口来自
IIterableVec
IVectorViewVec
IMapViewBTreeMap

因此,如果您需要一个 i32 值的 IIterable 实现,您可以按如下方式创建它

use windows::{core::*, Foundation::Collections::*};

fn main() -> Result<()> {
    let collection = IIterable::<i32>::try_from(vec![1, 2, 3])?;

    for n in collection {
        println!("{n}");
    }

    Ok(())
}

生成的 collection 将实现所有专门的 IIterable 方法。

您注意到上面表格中的 T::Default 了吗?挑战在于,当 WinRT 集合包含可空类型时(与 i32 不同),集合必须必然支持支持表达这一点的后端实现。Default 关联类型只是将 T 替换为 Option 用于此类可空或引用类型。

让我们考虑一个稍微更复杂一些的例子。在这里,我们将创建一个 IMapView,其中键是字符串,值是接口。WinRT 字符串不可空,但接口可空。WinRT 字符串在 windows crate 中由 HSTRING 表示,对于接口,我们将只使用 IStringable 实现

#![allow(unused)]
fn main() {
use windows::Foundation::*;

#[implement(IStringable)]
struct Value(&'static str);

impl IStringable_Impl for Value {
    fn ToString(&self) -> Result<HSTRING> {
        Ok(self.0.into())
    }
}
}

我们现在可以创建一个 std 集合,如下所示

#![allow(unused)]
fn main() {
use std::collections::*;

let map = BTreeMap::from([
    ("hello".into(), Some(Value("HELLO").into())),
    ("hello".into(), Some(Value("WORLD").into())),
]);
}

Rust 编译器自然会推断出确切的类型:BTreeMap>

最后,我们可以使用 TryInto trait 将该 BTreeMap 包装在 WinRT 集合中,如下所示

#![allow(unused)]
fn main() {
let map: IMapView<HSTRING, IStringable> = map.try_into()?;

for pair in map {
    println!("{} - {}", pair.Key()?, pair.Value()?.ToString()?);
}
}

理解 windows-targets crate

windowswindows-sys crate 依赖于 windows-targets crate 提供链接器支持。windows-targets crate 包含导入库,支持语义版本控制,以及对 raw-dylib 的可选支持。它为以下目标提供了显式导入库

  • i686_msvc
  • x86_64_msvc
  • aarch64_msvc
  • i686_gnu
  • x86_64_gnu
  • x86_64_gnullvm
  • aarch64_gnullvm

导入库包含链接器用于解析由 DLL 导出的函数的外部引用的信息。这允许操作系统在加载时识别特定的 DLL 和函数导出。导入库是工具链和架构特定的。换句话说,根据您是使用 MSVC 还是 GNU 工具链编译,以及您是为 x86 还是 ARM64 架构编译,需要不同的 lib 文件。请注意,导入库不包含任何代码,而静态库则包含。

虽然 GNU 和 MSVC 工具链通常提供一些导入库来支持 C++ 开发,但这些 lib 文件通常不完整、缺失或完全错误。这可能导致难以诊断的链接器错误。windows-targets crate 确保 windowswindows-sys crates 定义的所有函数都可以链接,而无需依赖工具链分发的隐式 lib 文件。这确保了可以使用 Cargo 管理依赖项并简化交叉编译。windows-targets crate 还包含特定于版本的 lib 文件名,确保语义版本兼容性。如果没有此功能,链接器将简单地选择第一个匹配的 lib 文件名,并无法解析任何缺失或不匹配的导入。

注意:通常,您根本不需要考虑 windows-targets crate。windowswindows-sys crate 自动依赖于 windows-targets crate。只有在极少数情况下,您才需要直接使用它。

首先将以下内容添加到您的 Cargo.toml 文件中

[dependencies.windows-targets]
version = "0.52"

使用 link 宏定义您希望调用的外部函数

#![allow(unused)]
fn main() {
windows_targets::link!("kernel32.dll" "system" fn SetLastError(code: u32));
windows_targets::link!("kernel32.dll" "system" fn GetLastError() -> u32);
}

根据需要使用任何 Windows API

fn main() {
    unsafe {
        SetLastError(1234);
        assert_eq!(GetLastError(), 1234);
    }
}

默认情况下,link 宏将导致链接器使用捆绑的导入库。使用 windows_raw_dylib Rust 构建标志进行编译将导致 Cargo 完全跳过下载导入库,而是使用 raw-dylib 自动解析导入。然后 Rust 编译器将直接创建导入条目。这无需更改任何代码即可工作。如果没有 windows-targets crate,在链接器和 raw-dylib 导入之间切换需要非常复杂的代码更改。截至本文撰写之时,raw-dylib 功能尚未稳定。

独立代码生成

即使可以选择windows 和 windows-sys crate,有些开发人员可能更喜欢使用完全独立的绑定。windows-bindgen crate 允许您通过单个函数调用为 Windows API 生成完全独立的绑定,您可以在测试中运行该函数调用以自动化绑定生成。这有助于减少您的依赖项,同时继续为将来可能需要的任何 API 要求提供可持续的路径,或者只是不时刷新您的绑定以自动从 Microsoft 获取任何错误修复。

警告:独立代码生成只应作为最苛刻场景的最后手段。使用 windows-sys crate 并让 Cargo 管理此依赖项要简单得多。此 windows-sys crate 提供原始绑定,经过严格测试并广泛使用,并且不应显著影响您的构建时间。

首先将以下内容添加到您的 Cargo.toml 文件中

[dependencies.windows-targets]
version = "0.52"

[dev-dependencies.windows-bindgen]
version = "0.52"

windows-bindgen crate 仅用于生成绑定,因此仅作为开发依赖项。windows-targets crate 是 windowswindows-sys crate 共享的依赖项,只包含受支持目标的导入库。这将确保您可以链接到可能需要的任何 Windows API 函数。

编写一个测试来生成绑定,如下所示

#![allow(unused)]
fn main() {
#[test]
fn bindgen() {
    let args = [
        "--out",
        "src/bindings.rs",
        "--config",
        "flatten",
        "--filter",
        "Windows.Win32.System.SystemInformation.GetTickCount",
    ];

    windows_bindgen::bindgen(args).unwrap();
}
}

根据需要使用任何 Windows API。

mod bindings;

fn main() {
    unsafe {
        println!("{}", bindings::GetTickCount());
    }
}

在 Rust 中创建您的第一个 DLL

作为一种具有与 C 和 C++ 类似链接支持的系统编程语言,在 Rust 中构建 DLL 相当简单。然而,Rust 有自己的库概念,与 C 和 C++ 的概念有很大不同,因此只需找到正确的配置即可生成所需的输出。

与大多数 Rust 项目一样,您可以从 Cargo 开始并获得一个基本模板,但它非常简单,我们将在这里手动构建它以了解所涉及的内容。让我们创建如下目录结构

> hello_world
  Cargo.toml
  > src
    lib.rs

只有两个目录和两个文件。有一个 hello_world 目录,其中包含整个项目。在该目录中,我们有一个 Cargo.toml 文件,其中包含项目或包的元数据,以及编译包所需的信息

[package]
name = "hello_world"
edition = "2021"

[lib]
crate-type = ["cdylib"]

至少,[package] 部分包含您的包编译所用的名称和 Rust 版本。

纯 Rust 库通常不包含 [lib] 部分。当您需要专门控制项目将如何使用和链接时,这是必需的。在这种情况下,我们使用“cdylib”,它表示一个动态系统库,并映射到 Windows 上的 DLL。

src 子目录包含 lib.rs Rust 源文件,我们可以在其中添加希望从 DLL 导出的任何函数。这是一个简单的示例

#[no_mangle]
extern "system" fn HelloWorld() -> i32 {
    123
}

[no_mangle] 属性只是告诉编译器禁用任何名称混淆,并使用函数名作为导出的标识符。extern "system" 函数限定符指示函数预期的 ABI 或调用约定。“system”字符串表示系统特定的调用约定,它通常映射到 Windows 上的“stdcall”。

就是这样!您现在可以构建包,它将生成一个 DLL

> cargo build -p hello_world

Cargo 会将生成的二进制文件放到目标目录中,然后您就可以从任何其他编程语言中使用它们了

> dir /b target\debug\hello_world.*
hello_world.d
hello_world.dll
hello_world.dll.exp
hello_world.dll.lib
hello_world.pdb

这是一个简单的 C++ 示例

#include <stdint.h>
#include <stdio.h>

extern "C" {
    int32_t __stdcall HelloWorld();
}

int main() {
    printf("%d\n", HelloWorld());
}

您可以使用 MSVC 如下构建它

cl hello_world.cpp hello_world.dll.lib

dumpbin 工具可用于进一步检查导入和导出。

> dumpbin /nologo /exports hello_world.dll

Dump of file hello_world.dll

File Type: DLL

  Section contains the following exports for hello_world.dll

    00000000 characteristics
    FFFFFFFF time date stamp
        0.00 version
           1 ordinal base
           1 number of functions
           1 number of names

    ordinal hint RVA      name

          1    0 00001000 HelloWorld = HelloWorld
> dumpbin /nologo /imports hello_world.exe

Dump of file hello_world.exe

File Type: EXECUTABLE IMAGE

  Section contains the following imports:

    hello_world.dll
             140017258 Import Address Table
             140021200 Import Name Table
                     0 time date stamp
                     0 Index of first forwarder reference

                           0 HelloWorld

实现传统的 Win32 风格 API

既然我们知道了如何在 Rust 中创建 DLL,那么让我们考虑一下实现一个简单的 Win32 风格 API 需要什么。虽然 WinRT 通常是新操作系统 API 的更好选择,但 Win32 风格 API 仍然很重要。您可能需要用 Rust 重新实现现有 API,或者由于某种原因需要更精细地控制类型系统或激活模型。

为了保持简单但切合实际,让我们实现一个 JSON 验证器 API。其思想是提供一种方法,可以有效地根据已知模式验证给定的 JSON 字符串。效率要求模式预先编译,因此我们可以生成一个逻辑 JSON 验证器对象,该对象可以与验证 JSON 字符串的过程分开创建和释放。您可以想象一个假设的 Win32 风格 API 如下所示

HRESULT __stdcall CreateJsonValidator(char const* schema, size_t schema_len, uintptr_t* handle);

HRESULT __stdcall ValidateJson(uintptr_t handle, char const* value, size_t value_len, char** sanitized_value, size_t* sanitized_value_len);

void __stdcall CloseJsonValidator(uintptr_t handle);

CreateJsonValidator 函数应该编译模式并通过返回的 handle 提供它。

然后可以将句柄传递给 ValidateJson 函数以执行验证。该函数可以选择返回 JSON 值的清理版本。

JSON 验证器句柄以后可以使用 CloseJsonValidator 函数释放,导致验证器“对象”占用的任何内存被释放。

创建和验证都可能失败,因此这些函数返回 HRESULT,通过 GetErrorInfo 函数可以获得丰富的错误信息。

让我们使用 windows crate 来进行基本的 Windows 错误处理和类型支持。流行的 serde_json crate 将用于解析 JSON 字符串。不幸的是,它不提供模式验证。快速在线搜索显示 jsonschema crate 似乎是唯一或主要的可用选项。对于这个示例来说就足够了。这里的重点不是特定的实现,而是通常在 Rust 中构建此类 API 的过程。

鉴于这些依赖项以及我们了解到的在 Rust 中创建 DLL 的知识,项目的 Cargo.toml 文件应该如下所示

[package]
name = "json_validator"
edition = "2021"

[lib]
crate-type = ["cdylib"]

[dependencies]
jsonschema = "0.17"
serde_json = "1.0"

[dependencies.windows]
version = "0.52"
features = [
    "Win32_Foundation",
    "Win32_System_Com",
]

我们可以使用 use 声明来简化操作

#![allow(unused)]
fn main() {
use jsonschema::JSONSchema;
use windows::{core::*, Win32::Foundation::*, Win32::System::Com::*};
}

让我们从 CreateJsonValidator API 函数开始。以下是 C++ 声明在 Rust 中可能的样子

#![allow(unused)]
fn main() {
#[no_mangle]
unsafe extern "system" fn CreateJsonValidator(
    schema: *const u8,
    schema_len: usize,
    handle: *mut usize,
) -> HRESULT {
    create_validator(schema, schema_len, handle).into()
}
}

这里没什么太令人兴奋的。我们只是使用了 windows crate 中 HRESULT 的定义。该实现调用了一个不同的 create_validator 函数来实现它。我们这样做是为了能够使用标准 Result 类型的语法便利性进行错误传播。windows crate 提供的 Result 特化进一步支持将 Result 转换为 HRESULT,同时将其丰富的错误信息传递给调用者。这就是尾部的 into() 的用途。

create_validator 函数如下所示

#![allow(unused)]
fn main() {
unsafe fn create_validator(schema: *const u8, schema_len: usize, handle: *mut usize) -> Result<()> {
    // ...

    Ok(())
}
}

如您所见,它具有完全相同的参数,只是将 HRESULT 替换为返回单元类型Result,或者除了成功或错误信息之外什么都不返回。

首先,我们需要使用 serde_json 解析提供的模式。由于我们需要在几个地方解析 JSON,我们只需将其放入一个可重用的辅助函数中

#![allow(unused)]
fn main() {
unsafe fn json_from_raw_parts(value: *const u8, value_len: usize) -> Result<serde_json::Value> {
    if value.is_null() {
        return Err(E_POINTER.into());
    }

    let value = std::slice::from_raw_parts(value, value_len);

    let value =
        std::str::from_utf8(value).map_err(|_| Error::from(ERROR_NO_UNICODE_TRANSLATION))?;

    serde_json::from_str(value).map_err(|error| Error::new(E_INVALIDARG, format!("{error}").into()))
}
}

json_from_raw_parts 函数首先检查指向 UTF-8 字符串的指针是否为空,在这种情况下返回 E_POINTER。然后我们可以将指针和长度转换为 Rust 切片,并从那里转换为字符串切片,确保它实际上是有效的 UTF-8 字符串。最后,我们调用 serde_json 将字符串转换为 JSON 值以供进一步处理。

现在我们可以解析 JSON 了,完成 create_validator 函数相对简单

#![allow(unused)]
fn main() {
unsafe fn create_validator(schema: *const u8, schema_len: usize, handle: *mut usize) -> Result<()> {
    let schema = json_from_raw_parts(schema, schema_len)?;

    let compiled = JSONSchema::compile(&schema)
        .map_err(|error| Error::new(E_INVALIDARG, error.to_string().into()))?;

    if handle.is_null() {
        return Err(E_POINTER.into());
    }

    *handle = Box::into_raw(Box::new(compiled)) as usize;

    Ok(())
}
}

JSON 值,在本例中为 JSON 模式,被传递给 JSONSchema::compile 以生成编译后的表示。虽然此时该值已知为 JSON,但它实际上可能不是有效的 JSON 模式。在这种情况下,我们将返回 E_INVALIDARG 并包含来自 JSON 模式编译器的错误消息以帮助调试。最后,如果句柄指针不为空,我们可以继续将编译后的表示装箱并将其作为“句柄”返回。

现在让我们继续讨论 CloseJsonValidator 函数,因为它与上面的装箱代码密切相关。装箱只是意味着将值移动到堆上。因此,CloseJsonValidator 函数需要“丢弃”对象并释放该堆分配

#![allow(unused)]
fn main() {
#[no_mangle]
unsafe extern "system" fn CloseJsonValidator(handle: usize) {
    if handle != 0 {
        _ = Box::from_raw(handle as *mut JSONSchema);
    }
}
}

如果提供了零句柄,我们可以添加一个小的保护措施。这是一个非常标准的便利功能,可以简化调用者的通用编程,但如果调用者知道句柄为零,通常可以避免调用 CloseJsonValidator 的间接成本。

最后,让我们考虑 ValidateJson 函数的实现

#![allow(unused)]
fn main() {
#[no_mangle]
unsafe extern "system" fn ValidateJson(
    handle: usize,
    value: *const u8,
    value_len: usize,
    sanitized_value: *mut *mut u8,
    sanitized_value_len: *mut usize,
) -> HRESULT {
    validate(
        handle,
        value,
        value_len,
        sanitized_value,
        sanitized_value_len,
    )
    .into()
}
}

这里,实现再次转发到一个返回 Result 的函数以方便起见

#![allow(unused)]
fn main() {
unsafe fn validate(
    handle: usize,
    value: *const u8,
    value_len: usize,
    sanitized_value: *mut *mut u8,
    sanitized_value_len: *mut usize,
) -> Result<()> {
    // ...
}
}

首先,我们需要确保我们有一个有效的句柄,然后将其转换为 JSONSchema 对象引用

#![allow(unused)]
fn main() {
if handle == 0 {
    return Err(E_HANDLE.into());
}

let schema = &*(handle as *const JSONSchema);
}

这看起来有点棘手,但我们只是将不透明句柄转换为 JSONSchema 指针,然后返回一个引用以避免获取其所有权。

接下来,我们需要解析提供的 JSON 值

#![allow(unused)]
fn main() {
let value = json_from_raw_parts(value, value_len)?;
}

这里我们再次使用方便的 json_from_raw_parts 辅助函数,并允许通过 ? 运算符自动处理错误传播。

此时我们可以执行模式验证,可选地返回 JSON 值的清理副本

#![allow(unused)]
fn main() {
if schema.is_valid(&value) {
    if !sanitized_value.is_null() && !sanitized_value_len.is_null() {
        let value = value.to_string();

        *sanitized_value = CoTaskMemAlloc(value.len()) as _;

        if (*sanitized_value).is_null() {
            return Err(E_OUTOFMEMORY.into());
        }

        (*sanitized_value).copy_from(value.as_ptr(), value.len());
        *sanitized_value_len = value.len();
    }

    Ok(())
} else {
    // ...
}
}

假设 JSON 值与编译后的模式匹配,我们查看调用者是否提供了指针以返回 JSON 值的清理副本。在这种情况下,我们调用 to_string 以直接从 JSON 解析器返回字符串表示形式,使用 CoTaskMemAlloc 分配缓冲区以返回给调用者,并将生成的 UTF-8 字符串复制到此缓冲区中。

如果事情不顺利,我们可以获取编译后的模式以生成一个有用的错误消息,然后向调用者返回 E_INVALIDARG

#![allow(unused)]
fn main() {
let mut message = String::new();

if let Some(error) = schema.validate(&value).unwrap_err().next() {
    message = error.to_string();
}

Err(Error::new(E_INVALIDARG, message.into()))
}

validate 方法返回一个错误集合。为了简单起见,我们只返回第一个。

就是这样!您在 Rust 中的第一个 Win32 风格 API。您可以在这里找到完整的示例

在 windows crate 中使用字符串

Windows API 中有几种字符串类型,包括

  • PSTR/PCSTR:指向由 char (u8) 组成的空终止字符串的指针。字符串应使用当前线程的代码页进行编码。“C”表示“常量”(只读)字符串。
  • PWSTR/PCWSTR:指向由“宽字符”(u16) 组成的空终止字符串的指针,使用 UTF-16 编码。
  • BSTR:通常用于 COM/OLE 函数的二进制字符串。它由 u16 字符后跟一个空终止符组成。字符串在其指针之前预置了长度,并且一些函数将使用它们来传递任意二进制数据(包括包含 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,添加空终止符并转换为 UTF-16。
  • w! 作用相同,但生成 PCWSTR 而不是 HSTRING。
  • s! 生成带空终止符的 PCSTR。注意:这不进行任何转换,它只是添加一个空终止符。

如果我们想调用消息框,我们可以使用带有 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 字节序列,并自己添加空终止符,如下所示

#![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
    )
    }
}

这更符合人体工程学——它为您处理空终止和 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

  • 输入时,它表示缓冲区的字节大小包括空终止符
  • 如果函数返回缓冲区溢出,则返回的长度参数是它需要的缓冲区大小包括空终止符(以字节为单位)。
  • 如果函数成功写入缓冲区,则长度是写入的字节数不包括空终止符。

此行为在函数的文档中有所记录——使用 Windows API 时,请务必仔细检查函数对空终止符的期望。

无论如何,这确实有效,但我们可以做得更好。计算机名最多只能是 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 字符串即可推入/弹出/追加元素。为了完整起见,这里有一个返回 widestring 并附加一些感叹号的示例。

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