Juli 2015

Band 30, Nummer 7

C++ - Verwendung von STL Strings an den Begrenzungen der Win32-API

Von Giovanni Dicanio | Juli 2015

Die Win32-API stellt mehrere Features, die über eine reine C-Schnittstelle. Dies bedeutet, dass es gibt keine C++-String-Klassen nativ für den Austausch von Text an den Grenzen der Win32-API. Stattdessen werden roh C-Format-Zeichen-Zeiger verwendet. Beispielsweise hat die SetWindowText Win32-Funktion den folgenden Prototyp (aus der zugehörigen MSDN-Dokumentation unter bit.ly/1Fkb5lw):

BOOL WINAPI SetWindowText(
  HWND hWnd,
  LPCTSTR lpString
);

Der Zeichenfolgenparameter ist ausgedrückt in Form von LPCTSTR, was gleichbedeutend mit const TCHAR *. In Unicode-Builds (die seit Visual Studio 2005 Standard und sollte im modernen Windows C++-Anwendungen verwendet werden), TCHAR Typedef entspricht Wchar_t, sodass der Prototyp des SetWindowText als liest:

BOOL WINAPI SetWindowText(
  HWND hWnd,
  const wchar_t* lpString
);

Also, im Grunde die Eingabezeichenfolge wird als Konstante übergeben (d. h. schreibgeschützt) Wchar_t Zeichenzeiger, unter der Annahme, die auf die Zeichenfolge verweisen ist NUL endende, in einem klassischen, reinen C-Stil. Dies ist ein typisches Muster für Eingabezeichenfolge-Parameter, die an den Grenzen der Win32-API übergeben.

Auf der anderen Seite sind Ausgabezeichenfolgen an den Grenzen der Win32-API in der Regel vertreten mit ein paar Informationen: Ein Zeiger auf einen Zielpuffer, vergeben durch den Anrufer und eine Size-Parameter, die Gesamtgröße der Aufrufer bereitgestellte Puffer darstellt. Ein Beispiel ist die Funktion GetWindowText (bit.ly/1bAMkpA):

int WINAPI GetWindowText(
  HWND hWnd,
  LPTSTR lpString,
  int nMaxCount
);

In diesem Fall werden die Informationen mit Bezug zu den Zielpuffer Zeichenfolge (der "Ausgabe"-String-Parameter) in den letzten beiden Parametern gespeichert: LpString und nMaxCount. Ersteres ist ein Zeiger auf den Zielpuffer Zeichenfolge (mit Typedef LPTSTR Win32, die auf TCHAR * übersetzt, dargestellt oder Wchar_t * in Unicode baut). Die letzteren nMaxCount, repräsentiert die gesamte Größe des Zielpuffers Zeichenfolge, in Wchar_ts; Beachten Sie, dass dieser Wert das abschließende NUL-Zeichen schließt (nicht vergessen, dass die C-Format-Strings NUL endende Arrays).

Natürlich ist reines C C++ statt eine äußerst produktive Option für Entwicklung-Benutzermodus-Code in Windows und Windows-Anwendungen insbesondere. In der Tat, im Allgemeinen die Verwendung von C++ löst die semantische Ebene des Codes und steigert die Produktivität der Programmierer, ohne negative Auswirkungen auf die Performance. Insbesondere mit bequemen C++ Zeichenfolge Klassen ist viel besser (einfacher, produktiver, weniger anfällig für Fehler) als Umgang mit Roh-, C-ähnlichen, NUL-terminierte Zeichen-Arrays.

So wird die Frage ist nun: Welche Art von C++ String-Klassen für die Interaktion mit der Win32-API-Schicht, die nativ eine pure-C-Schnittstelle verfügbar macht verwendet werden kann?

Active Template Library (ATL) / Microsoft Foundation Class (MFC) Bibliothek CString nun, die ATL/MFC CString-Klasse ist eine Option. CString ist sehr gut integriert, mit C++ Windows-Frameworks wie ATL, MFC und Windows Template Library (WTL), die Win32-Programmierung mit C++. Es macht also Sinn CString verwenden, um Zeichenfolgen auf der Win32-API Plattform-spezifischen Schicht von C++ Windows-Anwendungen darstellen, wenn Sie diese Frameworks verwenden. Darüber hinaus bietet CString bequeme Windows Plattform-spezifischen Funktionen wie könnend Zeichenfolgen von Ressourcen und So weiter zu laden; plattformabhängige Features, die eine Cross-Plattform-standard-Bibliothek wie die Standard Template Library (STL), per definitionem bieten einfach nicht möglich sind. Also, beispielsweise ob entwerfen und implementieren eine neue C++-Klasse, die von einigen vorhandenen ATL oder MFC-Klasse abgeleitet werden müssen auf jeden Fall überlegen Sie mit einem CString Zeichenfolgen dargestellt.

STL-Standardzeichenfolgen allerdings gibt es Fälle, wo es besser ist, eine normaler String-Klasse an der Schnittstelle von selbsterstellten C++-Klassen verwenden, die Windows-Anwendungen bilden. Beispielsweise sollten Sie zu abstrahieren die Win32-API-Ebene so bald wie möglich in C++-Code, bevorzugen die Verwendung einer STL-String-Klasse anstelle von Windows-spezifische Klassen wie CString an der öffentlichen Schnittstelle speziell C++-Klassen. Also, betrachten Sie die Groß-/Kleinschreibung in STL-Zeichenfolge-Klassen gespeichert. Zu diesem Zeitpunkt müssen Sie die STL-Strings über Win32 API hinweg übergeben (die eine pure-C-Schnittstelle verfügbar machen, wie am Anfang dieses Artikels beschrieben). Mit WTL, ATL und MFC das Framework implementiert "Klebstoff" Code zwischen die Win32 C Schnittstellenschicht und CString, unter der Haube verstecken, aber diese Bequemlichkeit nicht mit STL Strings zur Verfügung.

Im Sinne dieses Artikels angenommen Zeichenfolgen werden in Unicode UTF-16-Format gespeichert, die den Unicode für Windows-APIs Standardcodierung ist. In der Tat, wenn diese Zeichenfolgen ein anderes Format (z. B. Unicode UTF-8) verwendet, konnte die in UTF-16 an der Grenze der Win32-API, erfüllen die oben genannten Anforderungen dieses Artikels konvertiert werden. Für diese Konvertierungen, die Win32-MultiByteToWide­Char und WideCharToMultiByte Funktion verwendet werden könnten: Erstere können aufgerufen werden, um eine Unicode UTF-8-codierte ("Multi-Byte") String in eine Unicode UTF-16 ("Breite") Zeichenfolge konvertieren; Letzteres kann für die umgekehrte Konvertierung verwendet werden.

In Visual C++ ist der Std:: wstring Typ gut geeignet, um eine Zeichenfolge von Unicode UTF-16 darstellen, weil der zugrunde liegende Charakter-Typ Wchar_t, ist die eine Größe von 16 Bits in Visual C++, die genaue Größe der eine Codeeinheit UTF-16 hat. Beachten Sie, dass auf anderen Plattformen, wie Linux GCC, ein Wchar_t 32 Bits, also ein Std:: wstring auf diesen Plattformen eignet sich ideal zur Darstellung von Unicode UTF-32-codierte Text wäre. Um diese Mehrdeutigkeit zu entfernen, wurde ein neue Norm String-Datentyp in C ++ 11 eingeführt: Std::u16string. Dies ist eine Spezialisierung der Klasse std::basic_string mit Elementen des Typs char16_t, d. h. 16-Bit-Zeicheneinheiten.

Der Eingabezeichenfolge-Fall

Wenn eine Win32-API eine PCWSTR (oder LPCWSTR in älteren Terminologie), d. h. erwartet, eine const Wchar_t * eingegebenen NUL endende C-Format string-Parameter, einfach die Std::wstring::c_str-Methode aufgerufen werden prima. In der Tat, gibt diese Methode einen Zeiger auf eine nur-Lese NUL endende C-Zeichenfolge.

Legen Sie den Text der Titelleiste eines Fensters oder den Text eines Steuerelements, indem Sie den Inhalt in einem Std:: wstring gespeichert kann z. B. der Win32-API SetWindowText wie folgt aufgerufen werden:

std::wstring text;
::SetWindowText(hWnd, text.c_str());

Beachten Sie, dass während der ATL/MFC CString eine implizite Konvertierung in einen rohen Charakter const Zeiger bietet (const TCHAR *, const Wchar_t * in modernen Unicode entspricht baut), STL-Zeichenfolgen nicht solch eine implizite Konvertierung anbieten. Stattdessen stellen Sie einen expliziten Aufruf der STL-String C_str-Methode. Es gibt ein gemeinsames Verständnis in modernen C++, die implizite Konvertierungen sind in der Regel keine gute Sache sein, so dass die Designer des STL-Zeichenfolge-Klassen für eine Methode explizit aufrufbaren C_str entschieden. (Eine ähnliche Diskussion finden Sie auf den Mangel an implizite Konvertierung im modernen STL intelligente Zeiger in der Blog-Post auf bit.ly/1d9AGT4.)

Der Ausgabe-String-Fall

Es werden Dinge ein wenig komplizierter mit Ausgabezeichenfolgen. Das übliche Muster besteht des ersten Aufrufs eine Win32-API, um die Größe des Zielpuffers für die Ausgabezeichenfolge zu erhalten. Dies kann oder sind nicht die abschließende NUL; die Dokumentation des jeweiligen Win32 API muss zu diesem Zweck gelesen werden.

Dann wird ein Puffer der richtigen Größe dynamisch vom Aufrufer reserviert. Die Größe dieses Puffers ist die Größe, die im vorherigen Schritt bestimmt.

Und schließlich ein weiterer Aufruf erfolgt an eine Win32-API, tatsächliche Zeichenfolge Inhalte in den vom Aufrufer reservierten Puffer zu lesen.

Um den Text eines Steuerelements abzurufen, kann beispielsweise die GetWindowTextLength-API aufgerufen werden, um die Länge in Wchar_ts der Zeichenfolge zu erhalten. (Beachten Sie, dass in diesem Fall die zurückgegebene Länge nichtauf die abschließende NUL tut.)

Dann kann ein Zeichenfolgenpuffer mit dieser Länge zugeordnet werden. Hier eine Option könnte ein Std:: Vector < Wchar_t > verwenden Verwalten Sie den Puffer, zum Beispiel:

// Get the length of the text string
// (Note: +1 to consider the terminating NUL)
const int bufferLength = ::GetWindowTextLength(hWnd) + 1;
// Allocate string buffer of proper size
std::vector<wchar_t> buffer(bufferLength);

Beachten Sie, dass dies einfacher ist als mit einem raw "neue Wchar_t [BufferLength]" nennen, weil das würde erfordern, richtig freigeben des Puffers mit einem Aufruf von delete [] (und vergessen zu tun, die verursachen würden ein Memory Leak). Mit Std:: Vector ist eben einfacher, auch wenn mit kleinem Aufwand im Vergleich zu einem rohen neue [] Aufruf hat. In der Tat würde in diesem Fall die Std-Destruktor automatisch den zugewiesenen Puffer löschen.

Dies hilft auch beim Aufbau von Ausnahme-sichere C++-Code: Wenn im Code irgendwo eine Ausnahme ausgelöst wird, würde automatisch der Std-Destruktor aufgerufen werden. Stattdessen würde ein Puffer mit new [], deren Zeiger in einem unformatierten besitzenden Zeiger gespeichert ist, dynamisch reservierten zugespielt werden.

Eine andere Möglichkeit, als Alternative zu Std:: Vector, möglicherweise die Verwendung von std::unique_ptr, in bestimmten, std::unique_ptr < Wchar_t [] >. Diese Option hat die automatische Zerstörung (und die Ausnahme-Sicherheit) Std:: Vector (Dank an Std::unique_ptr der Destruktor), sowie weniger Overhead als Std:: Vector, da std::unique_ptr ein sehr kleine C++-Wrapper um ein unformatierter besitzenden Zeiger ist. Unique_ptr ist grundsätzlich eine besitzende Zeiger innerhalb sicherer Grenzen der RAII geschützt. RAII (bit.ly/1AbSa6k) ist eine sehr häufige C++ Programmierung Ausdrucksweise. Wenn Sie damit nicht vertraut sind, denken Sie nur an RAII als Implementierung Technik, dass automatisch Anrufe [] auf dem umschlossenen Zeiger löschen — z. B. in der Unique_ptr Destruktor — die zugeordneten Ressourcen freigeben und Speicher-Lecks (und Ressourcenverlusten im allgemeinen) zu verhindern.

Mit Unique_ptr könnte der Code wie folgt aussehen:

// Allocate string buffer using std::unique_ptr
std::unique_ptr< wchar_t[] > buffer(new wchar_t[bufferLength]);

Oder mit std::make_unique (verfügbar seit 14 C ++ und implementiert in Visual Studio 2013):

auto buffer = std::make_unique< wchar_t[] >(bufferLength);

Dann, sobald ein Puffer der richtigen Größe reserviert ist und einsatzbereit, GetWindowText-API aufgerufen werden kann, einen Zeiger übergeben in diesem String-Puffer. Um einen Zeiger auf den Anfang des raw-Puffers verwaltet von der Std:: Vector, die Std::vector::data-Methode zu erhalten (bit.ly/1I3ytEA) kann verwendet werden, etwa so:

// Get the text of the specified control
::GetWindowText(hWnd, buffer.data(), bufferLength);

Mit Unique_ptr könnte stattdessen die Get-Methode aufgerufen werden:

// Get the text of the specified control (buffer is a std::unique_ptr)
::GetWindowText(hWnd, buffer.get(), bufferLength);

Und, schließlich, der Text des Steuerelements kann tief in einem Std:: wstring-Instanz aus dem temporären Puffer kopiert:

std::wstring text(buffer.data()); // When buffer is a std::vector<wchar_t>
std::wstring text(buffer.get()); // When buffer is a std::unique_ptr<wchar_t[]>

In vorhergehenden Codeausschnitt habe ich eine Konstruktorüberladung von Wstring, wobei einen Konstante roh Wchar_t-Zeiger auf eine NUL endende Eingabezeichenfolge. Dies funktioniert prima, da die genannte Win32-API einen NUL-Terminator im Zeichenfolgenpuffer Ziel vom Aufrufer bereitgestellte einfügen wird.

Als eine Form der leichten Optimierung wenn die Länge der Zeichenfolge (in Wchar_ts) bekannt ist, ein Wstring Konstruktor überladen, wobei einen Zeiger und ein Zeichenfolgenparameter Charakter Graf könnte stattdessen verwendet werden. In diesem Fall die Länge der Zeichenfolge wird in der aufrufenden Site bereitgestellt und Wstring Konstruktor muss nicht fündig (normalerweise mit eine o (n)-Operation, wie das Aufrufen der Wcslen in einer Visual C++-Implementierung).

Eine Verknüpfung für den Fall der Ausgabe: Arbeiten im Ort mit Std:: wstring

Im Hinblick auf die Technik der Zuteilung einer temporären Zeichenfolge Puffer mit einem Std:: Vector (oder ein std::unique_ptr), und dann tief in einem Std:: wstring kopieren, können Sie eine Verknüpfung zu nehmen.

Im Grunde könnte eine Instanz von Std:: wstring direkt als Zielpuffer übergeben an Win32-APIs verwendet werden.

Std:: wstring hat in der Tat eine Resize-Methode, die zum Erstellen einer Zeichenfolge der richtigen Größe verwendet werden kann. Beachten Sie, dass in diesem Fall Sie nicht über den tatsächlichen ursprünglichen Inhalt der Größe String, Pflege, da es von der aufgerufenen Win32-API überschrieben werden. Abbildung 1 enthält eine Beispiel-Code-Snippet zeigt, wie Zeichenfolgen mit Std:: wstring lesen.

Ich würde gerne ein paar Punkte mit Bezug auf den Anwendungscode zu klären Abbildung 1.

Abbildung 1 Einlesen von Zeichenfolgen in Ort Verwendung Std:: wstring

// Get the length of the text string
// (Note: +1 to consider the terminating NUL)
const int bufferLength = ::GetWindowTextLength(hWnd) + 1;
// Allocate string of proper size
std::wstring text;
text.resize(bufferLength);
// Get the text of the specified control
// Note that the address of the internal string buffer
// can be obtained with the &text[0] syntax
::GetWindowText(hWnd, &text[0], bufferLength);
// Resize down the string to avoid bogus double-NUL-terminated strings
text.resize(bufferLength - 1);

Immer Zugriff auf den internen Puffer schreiben sehen Sie sich zuerst den GetWindowText-Aufruf:

::GetWindowText(hWnd, &text[0], bufferLength);

C++-Programmierer könnte versucht sein, verwenden die Std::wstring::data-Methode auf den internen String-Inhalt zuzugreifen, rufen Sie über einen Zeiger an die GetWindowText übergeben werden. Aber wstring::data gibt einen const Zeiger, der den Inhalt des internen Zeichenpuffers zu modifizierenden nicht ermöglichen würde. Und da GetWindowText Schreibzugriff auf den Inhalt der Wstring erwartet, diese Forderung würde nicht kompilieren. Also eine Alternative Verwendung der & Textsyntax [0], erhalten die Adresse der Anfang von den internen Puffer, als Ausgabe übergeben werden (d. h. änderbaren) Zeichenfolge an die gewünschte Win32-API.

Im Vergleich zu den vorherigen Ansatz, ist diese Technik effizienter, denn es keine temporären Std:: Vector, mit einem Puffer zuerst reserviert gibt, dann tief in einem Std:: wstring kopiert und schließlich verworfen. In der Tat arbeitet in diesem Fall der Code nur Platz in einem Std:: wstring-Instanz.

Vermeidet falsche Double-NUL-Terminated Strings achten Sie auf die letzte Codezeile in Abbildung 1:

// Resize down the string to avoid bogus double-NUL-terminated strings
text.resize(bufferLength - 1);

Mit dem ersten wstring::resize-Aufruf (text.resize(bufferLength); ohne die "-1"-Korrektur), genügend Platz im internen Wstring Puffer der GetWindowText-Win32-API zu kritzeln in seiner NUL Abschlusszeichen ermöglichen zugeordnet ist. Jedoch bietet neben dieser NUL-Terminator geschrieben von GetWindowText, Std:: wstring implizit ein anderes NUL Abschlusszeichen. Also, die resultierende Zeichenfolge endet als Double-NUL -­beendete Zeichenfolge: Geschrieben von GetWindowText NUL-Terminator und die NUL-Terminator automatisch von Wstring hinzugefügt.

Um diese falsche Doppel-NUL-terminierte Zeichenkette zu beheben, kann bis zu hacken Wstring-Instanz geändert werden, NUL Terminator der Win32-API aus, hinzugefügt verlassen nur die Wstring NUL Zeilenabschluss. Dies ist der Zweck des Gesprächs text.resize(bufferLength-1).

Handhabung eine Race Condition

Vor Abschluss dieser Artikel, es lohnt sich diskutieren, wie eine potenzieller Racebedingung zu behandeln, die bei einigen APIs auftreten können. Genommen Sie an, dass Sie Code haben, die einen Zeichenfolgenwert aus der Windows-Registrierung gelesen wird. Nach dem Muster zeigte im vorherigen Abschnitt, ein C++-Programmierer würde zunächst aufrufen, die RegQuery­ValueEx-Funktion, um die Länge der Zeichenfolgenwert ermitteln. Dann würde ein Puffer für die Zeichenfolge zugeordnet werden, und schließlich der RegQueryValueEx würde aufgerufen werden, ein zweites Mal, um den tatsächlichen Zeichenfolgenwert in den Puffer, die im vorherigen Schritt erstellten gelesen.

Race-Bedingung, die entstehen können, ist in diesem Fall einen anderen Prozess Ändern des String-Wert zwischen den beiden RegQueryValueEx-aufrufen. Die Länge der Zeichenfolge, die vom ersten Aufrufs könnte einen bedeutungslosen Wert, unabhängig von den neuen Zeichenfolgenwert, der von den anderen Prozess in die Registrierung geschrieben. Der zweite Aufruf von RegQueryValueEx würde also, die neue Zeichenfolge in einem Puffer mit einer falschen Größe zugewiesen lesen.

Um diesen Fehler zu beheben, können Sie eine Codierungsmuster wie in Abbildung 2.

Abbildung 2 Beispiel Codierung Muster um eine potenzielle Race Condition in immer Strings behandeln

LONG error = ERROR_MORE_DATA;
std::unique_ptr< wchar_t[] > buffer;
DWORD bufferLength = /* Some initial reasonable length for the string buffer */;
while (error == ERROR_MORE_DATA)
{
  // Create a buffer with bufferLength size (measured in wchar_ts)
  buffer = std::make_unique<wchar_t[]>(bufferLength);
  // Call the desired Win32 API
  error = ::SomeApiCall(param1, param2, ..., buffer.get(), &bufferLength);
}
if (error != ERROR_SUCCESS)
{
  // Some error occurred
  // Handle it e.g. throwing an exception
}
// All right!
// Continue processing
// Build a std::wstring with the NUL-terminated text
// previously stored in the buffer
std::wstring text(buffer.get());

Die Verwendung der While-Schleife in Abbildung 2 sorgt dafür, dass die Zeichenfolge in einen Puffer, der richtige Länge gelesen wird, weil jedes Mal ERROR_MORE_DATA zurückgegeben wird, ein neuer Puffer mit dem richtigen BufferLength-Wert zugeordnet wird, bis der API-Aufruf erfolgreich ist (Rückgabe ERROR_SUCCESS) oder einen anderen bieten einen nicht ausreichend großen Puffer Grund fehlschlägt.

Beachten Sie, dass das Code-Snippet in Abbildung 2 ist nur Beispiel Skelett Code; anderen Win32-APIs können unterschiedliche Fehlercodes mit Bezug zu einer unzureichenden Puffer bereitgestellt vom Aufrufer, z. B. den Code ERROR_INSUFFICIENT_BUFFER.

Nachbereitung

Während die Verwendung von CString auf der Win32-API-Grenze – mit Hilfe von Frameworks wie ATL/WTL und MFC — blendet die Mechanik der Interoperation mit Win32 pure-C-Schnittstelle Schicht, wenn mit STL den C++-Programmierer Streicher müssen achten Sie auf bestimmte Details. In diesem Artikel erörtert einige Codierung Muster für die Interoperation von der STL Wstring-Klasse und die Win32-Funktionen für pure-C-Schnittstelle. Bei der Eingabe ist die Wstring der C_str-Methode prima eine Eingabezeichenfolge an der Win32-C-Schnittstelle-Grenze, in Form einer einfachen (schreibgeschützt) NUL endende Konstantenzeichenfolge Zeichenzeiger übergeben. Für Ausgabezeichenfolgen muss vom Aufrufer ein temporären Zeichenfolgenpuffer reserviert werden. Dies kann erreicht werden, entweder die Std-STL-Klasse verwenden, oder mit etwas weniger overhead, STL-std::unique_ptr smart Pointer-Vorlage-Klasse. Eine weitere Option ist die wstring::resize-Methode verwenden, um etwas Platz innerhalb der String-Instanz als Zielpuffer für Win32 API-Funktionen zuweisen. In diesem Fall ist es wichtig, dass genügend Speicherplatz zum ermöglichen der aufgerufenen Win32-API in seine NUL Abschlusszeichen kritzeln und dann der Größe bis zu hacken, die Weg und lassen nur Wstring NUL Abschlusszeichen angegeben. Schließlich ich eine potenzieller Racebedingung bedeckt und präsentiert eine Probe Codierung Muster um diese Racebedingung zu lösen.


Giovanni Dicanio ist ein Computer-Programmierer, C++ und Windows-OS, Autor eines Pluralsight und ein Visual C++-MVP spezialisiert. Neben Programmierung und authoring-Kurs, er genießt, anderen zu helfen, in Foren und Gemeinschaften C++ gewidmet und können kontaktiert werden, um giovanni.dicanio@gmail.com.

Dank der folgenden technischen Experten für die Überprüfung dieses Artikels: David Cravey (GlobalSCAPE) und Stephan T. Lavavej (Microsoft)
David Cravey ist Enterprise Architekt bei GlobalSCAPE, führt mehrere C++ User Groups und war eine Zeit, vier Visual C++MVP.
Stephan T. Lavavej ist ein Senior-Entwickler bei Microsoft. Seit 2007 ist er mit Dinkumware zu Visual C++-Implementierung der C++-Standardbibliothek gearbeitet. Außerdem entwarf er ein paar C ++ 14 Funktionen: Make_unique und die transparente Operator Funktoren.