Tastenkombinationen

Drücken Sie oder , um zwischen den Kapiteln zu navigieren

Drücken Sie S oder /, um im Buch zu suchen

Drücken Sie ?, um diese Hilfe anzuzeigen

Drücken Sie Esc, um diese Hilfe auszublenden

Implementierung einer traditionellen Win32-API

Nachdem wir nun wissen, wie man eine DLL in Rust erstellt, wollen wir uns überlegen, was für die Implementierung einer einfachen Win32-API erforderlich ist. Obwohl WinRT im Allgemeinen die bessere Wahl für neue Betriebssystem-APIs ist, bleiben Win32-APIs weiterhin wichtig. Möglicherweise müssen Sie eine bestehende API in Rust neu implementieren oder aus einem anderen Grund eine feinere Kontrolle über den Typsystem- oder Aktivierungsmodell benötigen.

Um die Dinge einfach, aber realistisch zu halten, implementieren wir eine JSON-Validator-API. Die Idee ist, eine Möglichkeit zu bieten, einen gegebenen JSON-String effizient gegen ein bekanntes Schema zu validieren. Effizienz erfordert, dass das Schema vorkompiliert ist, damit wir ein logisches JSON-Validator-Objekt erzeugen können, das separat vom Prozess der Validierung des JSON-Strings erstellt und freigegeben werden kann. Sie können sich eine hypothetische Win32-API wie diese vorstellen

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);

Die Funktion CreateJsonValidator sollte das Schema kompilieren und es über das zurückgegebene handle verfügbar machen.

Das Handle kann dann an die Funktion ValidateJson übergeben werden, um die Validierung durchzuführen. Die Funktion kann optional eine bereinigte Version des JSON-Werts zurückgeben.

Das JSON-Validator-Handle kann später mit der Funktion CloseJsonValidator freigegeben werden, wodurch der vom Validator-"Objekt" belegte Speicher freigegeben wird.

Sowohl die Erstellung als auch die Validierung können fehlschlagen, sodass diese Funktionen ein HRESULT zurückgeben, wobei über die Funktion GetErrorInfo reiche Fehlerinformationen verfügbar sind.

Wir verwenden das windows-Crate für grundlegende Windows-Fehlerbehandlung und Typunterstützung. Das beliebte serde_json-Crate wird zum Parsen von JSON-Strings verwendet. Leider bietet es keine Schema-Validierung. Eine schnelle Online-Suche zeigt, dass das jsonschema-Crate das Haupt- oder einzige Mittel in diesem Bereich zu sein scheint. Es wird für dieses Beispiel ausreichen. Der Fokus liegt hier nicht wirklich auf der spezifischen Implementierung, sondern vielmehr auf dem Prozess des allgemeinen Erstellens einer solchen API in Rust.

Angesichts dieser Abhängigkeiten und dessen, was wir über die Erstellung einer DLL in Rust gelernt haben, sollte die Cargo.toml-Datei des Projekts wie folgt aussehen

[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",
]

Wir können eine use-Deklaration verwenden, um uns die Arbeit ein wenig zu erleichtern

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

Und fangen wir mit der API-Funktion CreateJsonValidator an. So könnte die C++-Deklaration in Rust aussehen

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

Nichts Besonderes hier. Wir verwenden lediglich die Definition von HRESULT aus dem windows-Crate. Die Implementierung ruft eine andere Funktion create_validator für ihre Implementierung auf. Wir tun dies, damit wir die syntaktische Bequemlichkeit des Standard- Result-Typs für die Fehlerweitergabe nutzen können. Die Spezialisierung von Result, die vom windows-Crate bereitgestellt wird, unterstützt weiter die Umwandlung eines Result in ein HRESULT, während ihre reichen Fehlerinformationen an den Aufrufer übergeben werden. Dafür wird das nachfolgende into() verwendet.

Die Funktion create_validator sieht wie folgt aus

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

    Ok(())
}
}

Wie Sie sehen können, hat sie genau dieselben Parameter und tauscht lediglich das HRESULT gegen ein Result aus, das den unit type zurückgibt, oder nichts außer Erfolg- oder Fehlerinformationen.

Zuerst müssen wir das bereitgestellte Schema mit serde_json parsen. Da wir JSON an mehreren Stellen parsen müssen, werden wir dies einfach in eine wiederverwendbare Hilfsfunktion auslagern

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

Die Funktion json_from_raw_parts beginnt damit zu prüfen, ob der Zeiger auf einen UTF-8-String nicht null ist, und gibt in solchen Fällen E_POINTER zurück. Wir können dann den Zeiger und die Länge in einen Rust-Slice und von dort in einen String-Slice umwandeln und sicherstellen, dass es sich tatsächlich um einen gültigen UTF-8-String handelt. Schließlich rufen wir serde_json auf, um den String in einen JSON-Wert für die weitere Verarbeitung umzuwandeln.

Jetzt, wo wir JSON parsen können, ist die Fertigstellung der Funktion create_validator relativ unkompliziert

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

Der JSON-Wert, in diesem Fall das JSON-Schema, wird an JSONSchema::compile übergeben, um die kompilierte Darstellung zu erzeugen. Obwohl bekannt ist, dass der Wert an diesem Punkt JSON ist, ist er möglicherweise nicht tatsächlich ein gültiges JSON-Schema. In solchen Fällen geben wir E_INVALIDARG zurück und schließen die Fehlermeldung vom JSON-Schema-Compiler ein, um die Fehlersuche zu erleichtern. Schließlich können wir, vorausgesetzt, der Handle-Zeiger ist nicht null, die kompilierte Darstellung boxen und sie als "Handle" zurückgeben.

Nun wollen wir uns der Funktion CloseJsonValidator widmen, da sie eng mit dem obigen Boxing-Code zusammenhängt. Boxing bedeutet lediglich, den Wert auf den Heap zu verschieben. Die Funktion CloseJsonValidator muss daher das Objekt "droppen" und diese Heap-Allokation freigeben

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

Wir können eine kleine Absicherung hinzufügen, wenn ein null-Handle übergeben wird. Dies ist eine ziemlich standardmäßige Komfortfunktion, um das generische Programmieren für Aufrufer zu vereinfachen, aber ein Aufrufer kann die Indirektionskosten des Aufrufs von CloseJsonValidator im Allgemeinen vermeiden, wenn er weiß, dass das Handle null ist.

Betrachten wir schließlich die Implementierung der Funktion 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()
}
}

Auch hier leitet die Implementierung der Bequemlichkeit halber an eine Funktion weiter, die ein Result zurückgibt

#![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<()> {
    // ...
}
}

Zuerst müssen wir sicherstellen, dass wir überhaupt ein gültiges Handle haben, bevor wir es in eine JSONSchema-Objekt-Referenz umwandeln

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

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

Das sieht etwas knifflig aus, aber wir wandeln einfach das undurchsichtige Handle in einen JSONSchema-Zeiger um und geben dann eine Referenz zurück, um den Besitz nicht zu übernehmen.

Als Nächstes müssen wir den bereitgestellten JSON-Wert parsen

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

Auch hier verwenden wir die praktische Hilfsfunktion json_from_raw_parts und lassen die Fehlerweitergabe automatisch über den ?-Operator abwickeln.

An diesem Punkt können wir die Schema-Validierung durchführen und optional eine bereinigte Kopie des JSON-Werts zurückgeben

#![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 {
    // ...
}
}

Vorausgesetzt, der JSON-Wert stimmt mit dem kompilierten Schema überein, prüfen wir, ob der Aufrufer Zeiger zum Zurückgeben einer bereinigten Kopie des JSON-Werts bereitgestellt hat. In diesem Fall rufen wir to_string auf, um eine String-Darstellung direkt vom JSON-Parser zurückzugeben, verwenden CoTaskMemAlloc, um einen Puffer zur Rückgabe an den Aufrufer zuzuweisen, und kopieren den resultierenden UTF-8-String in diesen Puffer.

Wenn die Dinge nicht gut laufen, können wir das kompilierte Schema verwenden, um eine nützliche Fehlermeldung zu erzeugen, bevor wir E_INVALIDARG an den Aufrufer zurückgeben

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

Die Methode validate gibt eine Sammlung von Fehlern zurück. Wir geben der Einfachheit halber nur den ersten zurück.

Und das ist es! Ihre erste Win32-API in Rust. Das vollständige Beispiel finden Sie hier.