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

Arbeiten mit Zeichenketten in der windows-Kiste

Es gibt mehrere Zeichenkettentypen in den Windows-APIs, darunter

  • PSTR/PCSTR: Ein Zeiger auf eine nullterminierte Zeichenkette, die aus Zeichen (u8) besteht. Zeichenketten sollten mit der Codepage des aktuellen Threads codiert werden. Ein 'C' steht für eine "konstante" (nur lesbare) Zeichenkette.
  • PWSTR/PCWSTR: Ein Zeiger auf eine nullterminierte Zeichenkette, die aus 'Wide-Zeichen' (u16) besteht und mit UTF-16 codiert ist.
  • BSTR: Eine binäre Zeichenkette, die üblicherweise in COM/OLE-Funktionen verwendet wird. Sie besteht aus u16-Zeichen, gefolgt von einem Nullterminator. Die Zeichenketten haben ihre Länge vor dem Zeiger präfixiert, und einige Funktionen verwenden sie, um beliebige Binärdaten (einschließlich Daten mit Nullzeichen) zu übergeben, wobei sie sich auf das Präfix und nicht auf den Terminator verlassen. Sie können jedoch normalerweise als normale, nullterminierte Wide-Zeichenketten verwendet werden.
  • HSTRING: Ein Handle zu einer Windows Runtime-Zeichenkette. HSTRINGS sind UTF-16 und unveränderlich.

Beachten Sie, dass Sie BSTR oder HSTRING an Funktionen übergeben können, die einen PCWSTR erwarten.

Leider entspricht keiner dieser Typen eins zu eins den Rust-Typen. Wir können jedoch die windows-strings-Kiste verwenden, um uns zu helfen.

Typen von API-Funktionen (schmal oder breit)

Die Win32-API unterteilt Zeichenkettenfunktionen in ihre "schmale" Version (endet auf 'A', wie MessageBoxA) und ihre "breite" Version (endet auf 'W', wie MessageBoxW). Schmale Versionen der API erwarten u8-Byte-Zeichenketten, die mit der Codepage des aktuellen Threads codiert sind, während breite Versionen UTF-16 erwarten.

Als allgemeine Empfehlung sollten Sie breite Versionen bevorzugen; es ist viel einfacher, zwischen Rusts UTF-8-Zeichenketten und Windows' UTF-16 zu konvertieren.

Aufrufen von APIs, die Zeichenketten verbrauchen

Sehen wir uns ein Beispiel mit der einfachen MessageBox-Funktion an, die eine Pop-up-Dialogbox anzeigt. Wir werden für dieses Beispiel die breite Version (MessageBoxW) verwenden.

Wenn Sie eine Windows-API mit einem Zeichenkettenliteral aufrufen möchten, enthält die windows-strings-Kiste Makros, um Windows-Zeichenketten aus Zeichenkettenliteralen zu generieren

  • h! generiert ein HSTRING aus einem Zeichenkettenliteral, fügt einen Nullterminator hinzu und konvertiert es in UTF-16.
  • w! tut dasselbe, generiert aber anstelle eines HSTRING einen PCWSTR.
  • s! generiert einen PCSTR mit einem Nullterminator. Vorsicht: Dies führt keine Konvertierungen durch, sondern fügt lediglich einen Nullterminator hinzu.

Wenn wir die Nachrichtenbox aufrufen wollten, könnten wir die windows-Kiste mit dem Win32_UI_WindowsAndMessaging-Feature verwenden und aufrufen

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

Das funktioniert, aber was, wenn wir dieselbe Funktion mit einer Rust-Zeichenkette aufrufen wollten? Das wird etwas komplizierter. Wir könnten manuell in eine UTF-16-Byte-Sequenz konvertieren und den Nullterminator selbst hinzufügen, wie hier

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

Das ist jedoch umständlich – wir können die Komfortfunktionen in der windows-strings-Kiste verwenden, um dies viel einfacher zu gestalten, indem wir die Rust-Zeichenketten in HSTRING konvertieren.

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

Das ist viel ergonomischer – es kümmert sich um die Nullterminierung und die UTF-16-Konvertierung für Sie.

Aufrufen von APIs, die Zeichenketten generieren

Windows-APIs, die Zeichenketten generieren, erfordern normalerweise einen zweistufigen Aufruf. Beim ersten Aufruf der API übergeben Sie einen NULL-Zeiger für den Zeichenkettenpuffer und rufen die Länge der zu generierenden Zeichenkette ab.

Dadurch können Sie den Puffer entsprechend zuweisen und die Funktion dann erneut mit einem entsprechend großen Puffer aufrufen.

Für dieses Beispiel verwenden wir die GetComputerNameW-Funktion. Dies erfordert das Win32_System_WindowsProgramming-Feature aus der windows-Kiste.

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

Es lohnt sich, die Funktionsweise des Längenparameters zu erläutern. Für GetComputerNameW

  • Bei der Eingabe repräsentiert es die Größe des Puffers einschließlich des Nullterminators in wchar.
  • Wenn die Funktion einen Pufferüberlauf zurückgibt, ist die zurückgegebene Länge, wie groß ein Puffer benötigt wird einschließlich des Nullterminators in wchars.
  • Wenn die Funktion erfolgreich in den Puffer geschrieben hat, ist die Länge die Anzahl der geschriebenen wchars ohne den Nullterminator.

Dieses Verhalten ist in der Dokumentation der Funktion beschrieben – seien Sie bei der Verwendung der Windows-API vorsichtig und prüfen Sie, was die Funktion in Bezug auf Nullterminatoren erwartet.

Unabhängig davon funktioniert das, aber wir können es besser machen. Computernamen können nur bis zu MAX_COMPUTERNAME_LENGTH lang sein, was magere 16 Zeichen sind. Wir können hier eine Heap-Allokation vermeiden und einfach Arrays verwenden, da wir unsere Pufferlänge zur Kompilierzeit kennen.

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

Wenn uns die Heap-Allokation (und ein paar zusätzliche Systemaufrufe) jedoch nichts ausmachen, gibt es eine ergonomischere Option. Die windows-strings-Kiste enthält HStringBuilder, den wir anstelle des Arrays verwenden können. Dies ermöglicht uns einfachere Konvertierungen.

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

Wenn Sie direkt mit UTF-16-Zeichenketten arbeiten müssen, sollten Sie die widestring-Kiste in Betracht ziehen, die UTF-16-fähig ist. Dies ermöglicht es Ihnen, Elemente zu pushen/poppen/anzuhängen, ohne die Zeichenkette in eine native Rust-UTF-8-Zeichenkette konvertieren zu müssen. Der Vollständigkeit halber hier ein Beispiel für die Rückgabe einer widestring und das Anhängen einiger Ausrufezeichen.

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