键盘快捷键

在章节间导航

S/ 在书中搜索

? 显示此帮助

Esc 隐藏此帮助

实现传统的 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 字符串的指针是否为 null,在这种情况下返回 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 模式编译器中的错误消息以帮助调试。最后,如果句柄指针不为 null,我们可以继续打包编译后的表示并将其作为“句柄”返回。

现在让我们继续讨论 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。您可以在此处找到完整的示例