Mai 2017

Band 32, Nummer 5

C++: Verwenden von Modern C++ für den Zugriff auf die Windows-Registrierung

Von Giovanni Dicanio

Das Windows-Betriebssystem stellt eine Reihe von C-Schnittstellen-APIs bereit, um Entwicklern Zugriff auf die Registrierung zu ermöglichen. Einige dieser APIs sind einer recht niedrigen Ebene zugeordnet und verlangen von Programmierern die Beachtung zahlreicher Details. Ab Windows Vista wurde der Sammlung eine API auf höherer Ebene hinzugefügt: die RegGetValue-Funktion (bit.ly/2jXtfpJ). Vor der Einführung dieser API musste zum Lesen eines Werts aus der Registrierung der gewünschte Registrierungsschlüssel geöffnet werden, der den Wert enthielt. Dies erfolgte durch den Aufruf von „RegOpenKeyEx“. Anschließend musste die RegQueryValueEx-API aufgerufen werden. Dabei waren zahlreiche komplexe Details zu beachten. Wenn Sie z. B. einen Zeichenfolgenwert mit „RegQueryValueEx“ lesen, wird nicht sichergestellt, dass die zurückgegebene Zeichenfolge ordnungsgemäß NULL-terminiert ist. Dies kann eine Reihe von gefährlichen Sicherheitsbugs in Ihrem Code bewirken. Damit dies nicht geschieht, müssen Sie unbedingt überprüfen, ob ein NULL-Terminator in der zurückgegebenen Zeichenfolge vorhanden ist. Wenn dies nicht der Fall ist, müssen Sie ihn hinzufügen. Außerdem müssen Sie sicherstellen, dass Sie den geöffneten Schlüssel durch Aufrufen von „RegCloseKey“ ordnungsgemäß schließen.

Natürlich kann beim Öffnen des Registrierungsschlüssels ein Fehler auftreten. Auch für diesen Fall müssen Sie Code für die Verarbeitung hinzufügen. Die RegGetValue-API vereinfacht diesen Workflow, weil sie den gewünschten Registrierungsschlüssel automatisch öffnet, nach seiner Verwendung schließt und Zeichenfolgen dann ordnungsgemäß NULL-terminiert, bevor sie an den Aufrufer zurückgegeben werden. Trotz dieser Vereinfachung ist die RegGetValue-Funktion noch immer eine Funktion der C-Schnittstelle auf niedriger Ebene. Außerdem wird ihre Schnittstelle aufgrund ihrer Fähigkeit, mehrere verschiedene Typen von Registrierungswerten (von DWORDs über Zeichenfolgen bis hin zu Binärdaten) zu verarbeiten, kompliziert bezüglich ihrer Programmierung.

Glücklicherweise können Sie Modern C++ zum ordnungsgemäßen Erstellen von Abstraktionen auf höherer Ebene um diese RegGetValue-Win32-API verwenden. Dies führt zu einer vereinfachten Schnittstelle zum Lesen von Werten verschiedener Typen aus der Registrierung.

Darstellen von Fehlern mithilfe von Ausnahmen

Die RegGetValue-API ist eine C-Schnittstellen-API und meldet als solche Fehlerbedingungen mithilfe von Rückgabecodes an den Aufrufer. Diese Funktion gibt insbesondere einen Wert vom Typ LONG zurück: ERROR_SUCCESS (also NULL) im Fall von Erfolg und einen anderen Wert bei Fehlern. Wenn der vom Aufrufer bereitgestellte Ausgabepuffer z. B. nicht groß genug zum Schreiben der Daten durch die API ist, gibt die Funktion ERROR_MORE_DATA zurück. Wenn Sie eine C++-Schnittstelle auf höherer Ebene um diese C-API erstellen möchten, können Sie eine C++-Ausnahmeklasse zum Darstellen von Fehlern definieren. Diese Klasse kann von der Standardklasse „std::runtime_error“ abgeleitet werden, und Sie können den von „RegGetValue“ zurückgegebenen LONG-Fehlercode darin einbetten:

class RegistryError
  : public std::runtime_error
{
public:
  ...
private:
  LONG m_errorCode;
};

Außerdem können weitere Informationsbestandteile in das Ausnahmeobjekt eingebettet werden (z. B. der HKEY und der Name des Unterschlüssels). Dies ist nur eine Basisimplementierung.

Sie können einen Konstruktor hinzufügen, um eine Instanz dieser Ausnahmeklasse mit einer Fehlermeldung und dem Rückgabecode aus dem fehlerhaften RegGetValue-Aufruf erstellen:

RegistryError(const char* message, LONG errorCode)
  : std::runtime_error{message}
  , m_errorCode{errorCode}
{}

Der Fehlercode kann außerdem für Clients mithilfe eines schreibgeschützten Accessors (Getters) bereitgestellt werden:

LONG ErrorCode() const noexcept
{
  return m_errorCode;
}

Da Sie nun diese Ausnahmeklasse erstellt haben, können Sie damit fortfahren, für die RegGetValue-C-API eine C++-Schnittstelle auf höherer Ebene als Wrapper zu nutzen, die einfacher zu verwenden und weniger fehleranfällig ist.

Lesen eines DWORD-Werts aus der Registrierung

Beginnen wir mit einem einfachen Vorgang: dem Verwenden der RegGetValue-API zum Lesen eines DWORD-Werts aus der Registrierung. Das Verwendungsmuster ist in diesem Fall ziemlich einfach. Sehen wir uns aber zuerst an, welche Art von Schnittstelle in C++ zum Verwalten dieses Falls definiert werden kann.

Dies ist der Prototyp der RegGetValue-API:

LONG WINAPI RegGetValue(
  _In_        HKEY    hkey,
  _In_opt_    LPCTSTR lpSubKey,
  _In_opt_    LPCTSTR lpValue,
  _In_opt_    DWORD   dwFlags,
  _Out_opt_   LPDWORD pdwType,
  _Out_opt_   PVOID   pvData,
  _Inout_opt_ LPDWORD pcbData
);

Wie Sie sehen könne, nimmt diese C-Schnittstellenfunktion hochgradig generische Daten an, etwa einen void*-Ausgabepuffer („pvData“) und einen Parameter für die Größe des Eingabe-/Ausgabepuffers („pcbData“). Außerdem sind Zeichenfolgen im C-Stil („lpSubKey“ und „lpValue“) vorhanden, die den Registrierungsschlüssel und den jeweiligen Wertnamen unter diesem Schlüssel identifizieren. Sie können diesen C-Funktionsprototyp ein wenig manipulieren, damit er für C++-Aufrufer einfacher wird.

Erstens: Da Sie Fehlerbedingungen durch Auslösen von C++-Ausnahmen melden, kann der aus der Registrierung gelesene DWORD-Wert einfach vom C++-Wrapper als ein Rückgabewert zurückgegeben werden. Auf diese Weise werden der void*-Rohausgabe-Pufferparameter („pvData“) und der zugehörige Größenparameter („pcbData“) automatisch nicht mehr benötigt.

Da Sie C++ verwenden, ist es außerdem besser, Unicode-Zeichenfolgen (UTF-16) mithilfe der std::wstring-Klasse anstatt mit Rohzeigern im C-Stil darzustellen. Sie können daher die folgende, viel einfachere C++-Funktion zum Lesen eines DWORD-Werts aus der Registrierung verwenden:

DWORD RegGetDword(
  HKEY hKey,
  const std::wstring& subKey,
  const std::wstring& value
)

Wie Sie sehen können, sind keine PVOID- und LPDWORD-Parameter vorhanden. Die Eingabezeichenfolgen werden über const-Verweise an std::wstring-Objekte übergeben, und der aus der Registrierung gelesene Wert wird als ein DWORD von dieser C++-Funktion zurückgegeben. Dies ist definitiv eine viel einfachere Schnittstelle auf höherer Ebene.

Befassen wir uns nun mit der Implementierung. Wie bereits erwähnt, ist das Aufrufmuster für „RegGetValue“ in diesem Fall recht einfach. Sie müssen nur eine DWORD-Variable deklarieren, die den aus der Registrierung gelesenen Wert speichert:

DWORD data{};

Dann benötigen Sie eine weitere DWORD-Variable, die die Größe (in Byte) des Ausgabepuffers darstellt, der von „RegGetValue“ geschrieben wird. Beachten Sie, dass der Ausgabepuffer in diesem einfachen Fall nur die frühere data-Variable ist. Ihre Größe ist konstant die Größe von einem DWORD:

DWORD dataSize = sizeof(data);

Beachten Sie jedoch auch, dass „dataSize“ nicht als „const“ markiert werden kann, weil es sich um einen Eingabe- und einen Ausgabeparameter für „RegGetValue“ handelt.

Nun können Sie die RegGetValue-API aufrufen:

LONG retCode = ::RegGetValue(
  hKey,
  subKey.c_str(),
  value.c_str(),
  RRF_RT_REG_DWORD,
  nullptr,
  &data,
  &dataSize
);

Die wstring-Eingabeobjekte werden in Zeichenfolgenrohzeiger im C-Stil mithilfe der wstring::c_str-Methode konvertiert. Die RRF_RT_REG_DWORD-Kennzeichnung schränkt den Typ des Registrierungswerts auf DWORD ein. Wenn der Registrierungswert, der gelesen werden soll, einen anderen Typ aufweist, schlägt der RegGetValue-Funktionsaufruf auf sichere Weise fehl.

Die letzten beiden Parameter stellen die Adresse des Ausgabepuffers (in diesem Fall die Adresse der data-Variablen) und die Adresse einer Variablen dar, die die Größe des Ausgabepuffers speichert. Bei der Rückgabe meldet „RegGetValue“ tatsächlich die Größe der Daten, die in den Ausgabepuffer geschrieben wurden. In diesem Fall, in dem nur ein einfaches DWORD gelesen werden muss, beträgt die Größe der Daten immer 4 Byte, also „sizeof(DWORD)“. Dieser Größenparameter ist jedoch wichtiger für Werte variabler Größe (z. B. Zeichenfolgen). Ich komme darauf an späterer Stelle in diesem Artikel zurück.

Nach dem Aufruf der RegGetValue-Funktion können Sie den Rückgabecode überprüfen und im Fall eines Fehlers eine Ausnahme auslösen:

if (retCode != ERROR_SUCCESS)
{
  throw RegistryError{"Cannot read DWORD from registry.", retCode};
}

Beachten Sie, dass der von „RegGetValue“ zurückgegebene Fehlercode („retCode“) in das Ausnahmeobjekt eingebettet ist und später durch den Code abgerufen werden kann, der die Ausnahme verarbeitet.

Bei Erfolg kann die DWORD-Datenvariable einfach an den Aufrufer zurückgegeben werden:

return data;

Das war die Funktionsimplementierung.

Der Aufrufer kann diese C++-Wrapperfunktion einfach mit Code aufrufen, der dem folgenden Beispiel ähnelt:

DWORD data = RegGetDword(HKEY_CURRENT_USER, subkey, L"MyDwordValue");

Beachten Sie, wie einfach dieser Code im Vergleich zum ursprünglichen RegGetValue-C-API-Aufruf ist. Sie übergeben einfach nur ein Handle an einen geöffneten Registrierungsschlüssel (in diesem Beispiel an den vordefinierten HKEY_CURRENT_USER-Schlüssel), eine Zeichenfolge mit dem Unterschlüssel und den Wertnamen. Bei Erfolg wird der DWORD-Wert an den Aufrufer zurückgegeben. Bei einem Fehler wird dagegen eine benutzerdefinierte Ausnahme vom Typ „RegistryError“ ausgelöst. Diese Art von Code kann auf einer höheren Ebene verwendet werden und ist viel einfacher als der Aufruf von „RegGetValue“. Die Komplexität von „RegGetValue“ wurde in der Tat in dieser benutzerdefinierten RegGetDword-C++-Wrapperfunktion verborgen.

Sie können das gleiche Muster zum Lesen eines QWORD-Werts (64-Bit-Daten) aus der Registrierung verwenden. In diesem Fall müssen Sie nur den DWORD-Typ für den Registrierungswert durch den 64-Bit-Wert ULONGLONG ersetzen.

Lesen eines Zeichenfolgenwerts aus der Registrierung

Das Lesen eines DWORD-Werts aus der Registrierung ist recht einfach: nur ein Aufruf der RegGetValue-Win32-API reicht aus. Dies liegt hauptsächlich daran, dass ein DWORD-Wert eine feste Größe (vier Byte) aufweist: die Größe eines DWORD. Andererseits ergibt sich durch das Lesen von Zeichenfolgen aus der Registrierung ein anderer Komplexitätsgrad, weil Zeichenfolgen Daten variabler Größe sind. Die Idee besteht in diesem Fall darin, die RegGetValue-API zwei Mal aufzurufen: Im ersten Aufruf fordern Sie von der API die Rückgabe der gewünschten Größe für den Ausgabezeichenfolgen-Puffer an. Im nächsten Schritt weisen Sie dynamisch einen Puffer mit der richtigen Größe zu. Schließlich nehmen Sie einen zweiten Aufruf von „RegGetValue“ vor, um die Zeichenfolgendaten tatsächlich in den zuvor zugewiesenen Puffer zu schreiben. (Dieses Muster habe ich ausführlich in meinem vorherigen Artikel „Verwendung von STL Strings an den Begrenzungen der Win32-API” (Using STL Strings at Win32 API Boundaries) unter msdn.com/magazine/mt238407 behandelt).

Sehen Sie sich zuerst den Prototyp der C++-Wrapperfunktion auf höherer Ebene an:

std::wstring RegGetString(
  HKEY hKey,
  const std::wstring& subKey,
  const std::wstring& value
)

Wie im Fall von DWORD ist dies wesentlich vereinfacht im Vergleich zum ursprünglichen komplexen Prototyp der RegGetValue-C-API. Der Zeichenfolgenwert wird von der Funktion als eine std::wstring-Instanz zurückgegeben. Im Fall eines Fehlers wird stattdessen eine Ausnahme ausgelöst. Der Name des Unterschlüssels und der Wertname werden ebenfalls als konstante wstring-Eingabeverweisparameter übergeben.

Ich behandele nun den Implementierungscode.

Wie bereits gesagt, besteht die Idee darin, die RegGetValue-API zuerst zum Abrufen der Größe des Ausgabepuffers zum Speichern des Zeichenfolgenwerts aufzurufen:

DWORD dataSize{};
LONG retCode = ::RegGetValue(
  hKey,
  subKey.c_str(),
  value.c_str(),
  RRF_RT_REG_SZ,
  nullptr,
  nullptr,
  &dataSize
);

Die Aufrufsyntax ähnelt dem Beispiel für den DWORD-Wert weiter oben. Die wstring-Objekte werden in Zeichenfolgenzeiger im C-Stil durch Aufrufen der wstring::c_str-Methode konvertiert. Die RRF_RT_REG_SZ-Kennzeichnung schränkt in diesem Fall den gültigen Registrierungstyp auf den Zeichenfolgentyp (REG_SZ) ein. Bei Erfolg schreibt die RegGetValue-API die gewünschte Größe des Ausgabepuffers (in Byte) in die dataSize-Variable.

Bei einem Fehler müssen Sie eine Ausnahme der benutzerdefinierten RegistryError-Klasse auslösen:

if (retCode != ERROR_SUCCESS)
{
  throw RegistryError{"Cannot read string from registry", retCode};
}

Da Sie nun die gewünschte Größe des Ausgabepuffers kennen, können Sie ein wstring-Objekt der erforderlichen Größe für die Ausgabezeichenfolge zuweisen:

std::wstring data;
data.resize(dataSize / sizeof(wchar_t));

Beachten Sie, dass der von „RegGetValue“ zurückgegebene dataSize-Wert in Byte ausgedrückt wird. Die wstring::resize-Methode erwartet aber eine in „wchar_t“ ausgedrückte Größenangabe. Sie müssen daher eine Skalierung von Byte auf „wchar_t“ ausführen und den früheren Bytewert durch „sizeof(wchar_t)“ dividieren.

Jetzt liegt Ihnen eine Zeichenfolge vor, der genügend Platz zugewiesen wurde. Sie können nun einen Zeiger auf ihren internen Puffer an die RegGetValue-API übergeben, die dieses Mal die tatsächlichen Daten der Zeichenfolge in den bereitgestellten Puffer schreibt:

retCode = ::RegGetValue(
  hKey,
  subKey.c_str(),
  value.c_str(),
  RRF_RT_REG_SZ,
  nullptr,
  &data[0],
  &dataSize
);

„&data[0]“ ist die Adresse des internen wstring-Puffers, der von der RegGetValue-API geschrieben wird.

Wie immer ist es wichtig, das Ergebnis des API-Aufrufs zu verifizieren und im Fall eines Fehlers eine Ausnahme auszulösen:

if (retCode != ERROR_SUCCESS)
{
  throw RegistryError{"Cannot read string from registry", retCode};
}

Beachten Sie, dass „RegGetValue“ bei Erfolg die tatsächliche Größe der Ergebniszeichenfolge (in Byte) in die dataSize-Variable schreibt. Sie müssen die Größe des wstring-Objekts gemäß dieser Größe anpassen. Da „dataSize“ in Byte ausgedrückt wird, ist es besser, diesen Wert in den entsprechenden wchar_t-Wert zu konvertieren, wenn „wstrings“ verarbeitet werden:

DWORD stringLengthInWchars = dataSize / sizeof(wchar_t);

Außerdem enthält „dataSize“ das terminierende NULL-Zeichen für die Ausgabezeichenfolge. wstring-Objekte sind jedoch bereits NULL-­terminiert. Sie müssen daher darauf achten, eine unberechtigte und falsche doppelte NULL-Terminierung für die gelesene Zeichenfolge zu vermeiden. Sie müssen den von „RegGetValue“ geschriebenen NULL-Terminator abtrennen:

stringLengthInWchars--; // Exclude the NUL written by the Win32 API
data.resize(stringLengthInWchars);

Beachten Sie, dass die RegGetValue-API bei Erfolg selbst dann eine NULL-terminierte Zeichenfolge garantiert, wenn die ursprüngliche in der Registrierung gespeicherte Zeichenfolge nicht NULL-terminiert war. Dieses Verhalten ist wesentlich sicherer als die alte RegQueryValueEx-API, die keine NULL-Terminierung für zurückgegebene Zeichenfolgen garantierte. Der Aufrufer musste daher zusätzlichen Code schreiben, um diesen Fall ordnungsgemäß zu berücksichtigen. Dies erhöht die Codekomplexität und Fehleranfälligkeit insgesamt.

Die Datenvariable enthält jetzt den aus der Registrierung gelesenen Zeichenfolgenwert. Sie können ihn daher beim Beenden der Funktion an den Aufrufer zurückgeben:

return data;

Sobald Sie diesen praktischen RegGetString-C++-Wrapper um die RegGetValue-C-API auf unterer Ebene erstellt haben, können Sie diesen wie folgt aufrufen:

wstring s = RegGetString(HKEY_CURRENT_USER, subkey, L"MyStringValue");

Wie im DWORD-Beispiel haben Sie den Abstraktionsgrad im Vergleich zur RegGetValue-Win32-API erhöht und eine einfach zu verwendende und schwer falsch zu verwendende C++-Wrapperfunktion zum Lesen eines Zeichenfolgenwerts aus der Registrierung bereitgestellt. Alle Details und die gesamte Komplexität der RegGetValue-API sind sicher innerhalb dieser benutzerdefinierten RegGetString-C++-Funktion verborgen.

Lesen von mehrteiligen Zeichenfolgenwerten aus der Registrierung

Ein weiterer Typ von Registrierungswert ist die sogenannte „mehrteilige Zeichenfolge“: Dabei handelt es sich im Prinzip um eine Sammlung von doppelt NULL-terminierten Zeichenfolgen, die in einem einzelnen Registrierungswert zusammengefasst werden. Diese doppelt NULL-terminierte Zeichenfolgendatenstruktur besteht aus einer Reihe von NULL-terminierten Zeichenfolgen im C-Stil, die angrenzende Speicherorte belegen. Das Ende der Sequenz wird durch einen weiteren NULL-Terminator markiert. Die gesamte Struktur wird also durch zwei NULL-Werte terminiert. Weitere Einzelheiten zu dieser Datenstruktur finden Sie im Blogbeitrag „Welches Format weist eine doppelt NULL-terminierte Zeichenfolge ohne Zeichenfolgen auf?“ (bit.ly/2jCqg2u).

Das Verwendungsmuster der RegGetValue-Win32-API ähnelt in diesem Fall stark dem vorherigen Beispiel für einzelne Zeichenfolgen. Dies bedeutet Folgendes: Zuerst wird die RegGetValue-API aufgerufen, um die Größe des gesamten Zielpuffers abzurufen, der die gewünschten Daten enthält (in diesem Fall die gesamte Sequenz angrenzender Zeichenfolgen, die durch eine doppelte NULL terminiert werden). Dann wird ein Puffer dieser Größe dynamisch zugewiesen. Schließlich wird die RegGetValue-Funktion ein zweites Mal aufgerufen. Dabei wird die Adresse des zuvor zugewiesenen Puffers übergeben, damit die API die tatsächlichen Daten der mehrteiligen Zeichenfolge in diesen Puffer schreiben kann.

In diesem Fall müssen Sie die Datenstruktur beachten, die die doppelt NULL-terminierte Zeichenfolge speichert. Ein std::wstring-Objekt kann eingebettete NULL-Werte ordnungsgemäß enthalten und potenziell zum Speichern einer doppelt NULL-terminierten Zeichenfolgenstruktur verwendet werden. Ich ziehe es jedoch vor, den Abstraktionsgrad zu erhöhen und die doppelt NULL-terminierte Zeichenfolge in ein zweckmäßigeres vector<wstring>-Objekt auf höherer Ebene zu analysieren.

Der Prototyp Ihrer C++-Wrapperfunktion zum Lesen mehrteiliger Zeichenfolgenwerte aus der Registrierung kann also folgendermaßen aussehen:

std::vector<std::wstring> RegGetMultiString(
  HKEY hKey,
  const std::wstring& subKey,
  const std::wstring& value
)

Bei Erfolg wird die mehrteilige Zeichenfolge an den Aufrufer als ein vector<wstring>-Objekt zurückgegeben. Bei einem Fehler wird hingegen eine Ausnahme im üblichen RegistryError-Format ausgelöst.

Innerhalb Ihrer C++-Wrapperfunktion rufen Sie zuerst die RegGetValue-API auf, um die Größe des gewünschten Ausgabepuffers zum Speichern der mehrteiligen Zeichenfolge abzurufen:

DWORD dataSize{};
LONG retCode = ::RegGetValue(
  hKey,
  subKey.c_str(),
  value.c_str(),
  RRF_RT_REG_MULTI_SZ,
  nullptr,
  nullptr,
  &dataSize
);

Beachten Sie die Verwendung der RRF_RT_REG_MULTI_SZ-Kennzeichnung, die dieses Mal zum Angeben des Typs des mehrteiligen Zeichenfolgenregistrierungswerts verwendet wird.

Wie üblich wird bei einem Fehler eine Ausnahme ausgelöst. Dabei wird der Fehlercode in das RegistryError-Objekt eingebettet:

if (retCode != ERROR_SUCCESS)
{
  throw RegistryError{"Cannot read multi-string from registry", retCode};
}

Bei Erfolg weisen Sie einen Puffer mit der richtigen Größe zu, um die gesamte mehrteilige Zeichenfolge zu speichern:

std::vector<wchar_t> data;
data.resize(dataSize / sizeof(wchar_t));

Ich halte ein vector<wchar_t>-Objekt zum Darstellen des Rohpuffers der mehrteiligen Zeichenfolge für viel klarer als ein wstring-Objekt. Beachten Sie auch, dass der von der RegGetValue-API zurückgegebene Größenwert in Byte ausgedrückt wird. Er muss daher ordnungsgemäß in einen wchar_t-Wert konvertiert werden, bevor er an die vector::resize-Methode übergeben wird.

Anschließend kann die RegGetValue-API zum zweiten Mal aufgerufen werden, um die tatsächlichen Daten der mehrteiligen Zeichenfolge in den zuvor zugewiesenen Puffer zu schreiben:

retCode = ::RegGetValue(
  hKey,
  subKey.c_str(),
  value.c_str(),
  RRF_RT_REG_MULTI_SZ,
  nullptr,
  &data[0],
  &dataSize
);

Das &data[0]-Argument zeigt auf den Anfang des Ausgabepuffers.

Sie müssen erneut den API-Rückgabecode überprüfen und Fehler durch Auslösen eine C++-Ausnahme melden:

if (retCode != ERROR_SUCCESS)
{
  throw RegistryError{"Cannot read multi-string"
    from registry", retCode};
}

Außerdem empfiehlt es sich, die Größe des Datenpuffers anhand des als Ausgabeparameter von der RegGetValue-API zurückgegebenen dataSize-Werts anzupassen:

data.resize( dataSize / sizeof(wchar_t) );

An diesem Punkt speichert die data-Variable (die ein „vector<wchar_t>“ ist) die doppelt NULL-terminierte Zeichenfolgensequenz. Der letzte Schritt besteht im Analysieren dieser Datenstruktur und Konvertieren in ein zweckmäßigeres vector<wstring>-Objekt auf höherer Ebene:

// Parse the double-NUL-terminated string into a vector<wstring>
std::vector<std::wstring> result;
const wchar_t* currStringPtr = &data[0];
while (*currStringPtr != L'\0')
{
  // Current string is NUL-terminated, so get its length with wcslen
  const size_t currStringLength = wcslen(currStringPtr);
  // Add current string to result vector
  result.push_back(std::wstring{ currStringPtr, currStringLength });
  // Move to the next string
  currStringPtr += currStringLength + 1;
}

Schließlich kann das vector<wstring>-Ergebnisobjekt an den Aufrufer zurückgegeben werden:

return result;

Dieser RegGetMultiString-C++-Wrapper kann einfach wie im folgenden Beispiel gezeigt aufgerufen werden:

vector<wstring> multiString = RegGetMultiString(
  HKEY_CURRENT_USER,
  subkey,
  L"MyMultiSz"
);

Erneut wurde die gesamte Komplexität der Win32-RegGetValue-API hinter einer zweckmäßigen C++-Schnittstelle auf hoher Ebene verborgen.

Aufzählen von Werten unter einem Registrierungsschlüssel

Ein weiterer Vorgang, der häufig mit der Windows-Registrierung ausgeführt wird, ist die Enumeration der Werte unter einem bestimmten Registrierungsschlüssel. Windows stellt zu diesem Zweck die RegEnumValue-API (bit.ly/2jB4kaV) bereit. Ich zeige Ihnen hier, wie diese API zum Abrufen einer Liste von Namen und Typen der Werte verwendet wird, die sich unter einem bestimmten Registrierungsschlüssel befinden. Dabei wird für den Enumerationsvorgang eine zweckmäßige C++-Funktion auf höherer Ebene als Wrapper verwendet. Ihre benutzerdefinierte C++-Funktion kann ein gültiges HKEY-Handle als Eingabe annehmen, das dem Schlüssel zugeordnet ist, den Sie aufzählen möchten. Bei Erfolg gibt diese benutzerdefinierte C++-Wrapperfunktion einen Vektor aus Paaren zurück: Das erste Element des Paars ist ein wstring-Objekt, das den Wertnamen enthält, das zweite Element ein DWORD, das den Werttyp darstellt. Der Prototyp dieser C++-Wrapperfunktion sieht also folgendermaßen aus:

std::vector<std::pair<std::wstring, DWORD>> RegEnumValues(HKEY hKey)

Ich befasse mich jetzt mit den Details des Enumerationsvorgangs. Die Idee: Zuerst wird die RegQueryInfoKey-API (bit.ly/2jraw2H) aufgerufen, um einige nützliche Informationen vor der Enumeration abzurufen, z. B. die Gesamtzahl der Werte und die maximale Länge von Wertnamen unter dem angegebenen Registrierungsschlüssel. Abbildung 1 zeigt dies.

Abbildung 1: Aufrufen der RegQuery­InfoKey-API

DWORD valueCount{};
DWORD maxValueNameLen{};
LONG retCode = ::RegQueryInfoKey(
  hKey,
  nullptr,    // No user-defined class
  nullptr,    // No user-defined class size
  nullptr,    // Reserved
  nullptr,    // No subkey count
  nullptr,    // No subkey max length
  nullptr,    // No subkey class length
  &valueCount,
  &maxValueNameLen,
  nullptr,    // No max value length
  nullptr,    // No security descriptor
  nullptr     // No last write time
);

Beachten Sie, dass ich „nullptr“ für die Informationen übergeben habe, die mich nicht interessieren. Natürlich müssen Sie den Rückgabewert überprüfen und eine Ausnahme auslösen, wenn bei Aufrufen der genannten API ein Fehler aufgetreten ist:

if (retCode != ERROR_SUCCESS)
{
  throw RegistryError{"Cannot query key info from"
    the registry", retCode};
}

Die Seite zur RegQueryInfoKey-Funktion in Windows Dev Center (bit.ly/2lctUDt) besagt, dass die für die maximale Länge von Wertnamen zurückgegebene Größe (die im Code oben in der maxValueNameLen-Variablen gespeichert wird) den terminierenden NULL-Wert nicht einschließt. Passen wir diesen Wert also an, indem wir eins hinzufügen, um den terminierenden NULL-Wert zu berücksichtigen, wenn ein Puffer zum Lesen von Wertnamen zugewiesen wird:

maxValueNameLen++;

Anschließend können Sie einen Puffer mit der richtigen Größe zum Lesen der Wertnamen in jedem Enumerationsschritt hinzufügen. Zu diesem Zweck kann ein effizienter „std::unique_ptr<wchar_t[]>“ mit geringem Mehraufwand verwendet werden:

auto nameBuffer = std::make_unique<wchar_t[]>(maxValueNameLen);

Das Ergebnis der Enumeration, dessen Format Paare aus dem Wertnamen und dem Werttyp sind, kann in einem „std::vector“ gespeichert werden:

std::vector<std::pair<std::wstring, DWORD>> values;

Sie fügen diesem Vektor während des Enumerationsvorgangs progressiv Inhalt hinzu. Dann werden „Werte“ an den Aufrufer zurückgegeben, wenn die Enumeration abgeschlossen ist.

Anschließend können Sie eine for-Schleife verwenden, die RegEnumValue-API wiederholt aufrufen und einen neuen Wert in jedem Iterationsschritt aufzählen:

for (DWORD index = 0; index < valueCount; index++)
{
  // Call RegEnumValue to get data of current value ...
}

Beachten Sie, dass Sie „valueCount“ vom anfänglichen Aufruf von „RegQueryInfoKey“ vor der Enumeration abgerufen haben.

Innerhalb des Codes für die Schleife kann die RegEnumValue-API aufgerufen werden, um die gewünschten Informationen für den aktuellen Wert abzurufen. In diesem Kontext sind Sie am Namen und Typ des Werts interessiert. Der Name des Werts wird im zuvor zugewiesenen „nameBuffer“ gelesen, und der Typ des Werts wird in einem einfachen DWORD gespeichert. Im Code für die Schleife können Sie daher Code wie den folgenden einfügen:

DWORD valueNameLen = maxValueNameLen;
DWORD valueType{};
retCode = ::RegEnumValue(
  hKey,
  index,
  nameBuffer.get(),
  &valueNameLen,
  nullptr,    // Reserved
  &valueType,
  nullptr,    // Not interested in data
  nullptr     // Not interested in data size

Wie immer ist es empfehlenswert, den API-Rückgabewert zu überprüfen und bei einem Fehler eine Ausnahme auszulösen:

if (retCode != ERROR_SUCCESS)
{
  throw RegistryError{"Cannot get value info from the registry", retCode};
}

Bei Erfolg schreibt die RegEnumValue-API den Namen des Werts in den bereitgestellten „nameBuffer“ und den Typ des Werts in die valueType-Variable. Sie können daher aus diesen beiden Informationsteilen ein „pair<wstring, DWORD>“ erstellen und dieses Informationspaar dann dem Enumerationsergebnisvektor hinzufügen:

values.push_back(std::make_pair(
  std::wstring{ nameBuffer.get(), valueNameLen },
  valueType
));

Im Anschluss an die for-Schleife kann der Ergebnis„werte“vektor an den Aufrufer zurückgegeben werden:

return values;

Der Aufrufer kann dann alle Werte unter dem Registrierungsschlüssel aufzählen, indem er einfach die C++-Wrapperfunktion wie folgt aufruft:

auto values = RegEnumValues(hKey);
// For each value
for (const auto& v : values)
{
  // Process v.first (value's name) and v.second (value's type)
  // ...
}

Ein ähnliches Codierungsmuster kann zum Aufzählen der Unterschlüssel unter dem angegebenen Registrierungsschlüssel verwendet werden. In diesem Fall muss die Win32-RegEnumKeyEx-API (bit.ly/2k3VEX8) anstelle der zuvor erwähnten RegEnumValue-API verwendet werden. Der Code einer solchen Enumerationsfunktion für Unterschlüssel wird im Download bereitgestellt, der diesen Artikel begleitet.

Ein sicherer Ressourcen-Manager für HKEY-Rohhandles

Für Registrierungsschlüssel, die durch den HKEY-Win32-Rohhandletyp dargestellt werden, kann sicher und bequem eine C++-Ressourcen-Manager-Klasse als Wrapper verwendet werden. Der Klassendestruktor ruft die RegCloseKey-API für das Rohhandle im Wrapper ordnungsgemäß auf, um das Handle automatisch zu schließen. Außerdem können Vorgänge der Verschiebesemantik wie ein move-Konstruktor und ein move-Zuweisungsoperator definiert werden, um den Besitz des Handles im Wrapper effizient zwischen verschiedenen Instanzen der C++-Ressourcen-Manager-Klasse zu übertragen. Aus Effizienzgründen werden alle Klassenmethoden, die keine Ausnahmen auslösen, als „noexcept“ markiert. Auf diese Weise kann der C++-Compiler besser optimierten Code ausgeben. Diese praktische C++-Klasse des Schlüsselressourcen-Managers namens „RegKey“ wird in der Datei „Registry.hpp“ implementiert, die diesen Artikel begleitet. In dieser wiederverwendbaren Nur-Headerdatei finden Sie auch die Implementierungen einiger Hilfsfunktionen: „RegOpenKey“ bzw. „RegCreateKey“, die als Wrapper für die Win32-APIs „RegOpenKeyEx“ und „RegCreateKeyEx“ dienen und ein HKEY-Handle zurückgeben, das sicher in die oben genannte C++-Ressourcen-Manager-Klasse als Wrapper eingeschlossen ist. Sollten Fehler auftreten, lösen diese C++-Funktionen eine RegistryError-Ausnahme aus und verwenden einen Wrapper für den Fehlercode, der von den Win32-Roh-APIs der C-Schnittstelle zurückgegeben wird.

Zusammenfassung

Die RegGetValue-Win32-API stellt im Vergleich zu APIs auf niedrigerer Ebene wie etwa „RegQueryValueEx“ eine Schnittstelle auf relativ höherer Ebene zum Lesen von Werten aus der Windows-Registrierung bereit. „RegGetValue“ bietet außerdem eine sicherere Schnittstelle, die z. B. garantiert, dass die zurückgegebenen Zeichenfolgen ordnungsgemäß NULL-terminiert sind. Davon abgesehen ist „RegGetValue“ immer noch eine C-Schnittstellen-API auf niedriger Ebene, bei der der Programmierer zahlreiche Details beachten muss. Die Programmierung für diese API kann zu Code führen, der fehleranfällig und komplex ist. In diesem Artikel haben Sie erfahren, wie eine praktische, einfach zu verwendende und schwer falsch zu verwendende Modern C++-Schnittstelle zum Verbergen der Komplexität der RegGetValue-API erstellt werden kann, die den Zugriff auf die Windows-Registrierung vereinfacht. Außerdem wurde für die RegEnumValue-API eine praktische C++-Funktion auf höherer Ebene als Wrapper verwendet, um alle Werte unter einem angegebenen Registrierungsschlüssel aufzuzählen. Sie finden den Quellcode, der die Implementierung der in diesem Artikel behandelten Funktionen und Klassen enthält, in einem wiederverwendbaren Nur-Headerformat (in der Datei „Registry.hpp“) im Download zu diesem Artikel.


Giovanni Dicanio ist Programmierer mit den Spezialgebieten C++ und Windows-Betriebssysteme, Pluralsight-Autor (bit.ly/GioDPS) und Visual C++-MVP. Er liebt nicht nur das Programmieren und Verfassen von Kursen, sondern unterstützt auch gerne andere Benutzer in Foren und Communitys mit dem Schwerpunkt C++. Sie können ihn per E-Mail unter giovanni.dicanio@gmail.com erreichen. Er bloggt außerdem unter msmvps.com/gdicanio.

Unser Dank gilt den folgenden technischen Experten für die Durchsicht dieses Artikels: David Cravey und Marc Gregoire
David Cravey ist Unternehmensarchitekt bei GlobalSCAPE, führt verschiedene C++-Benutzergruppen und ist ein viermaliger Visual C++-MVP.

Marc Gregoire ist ein leitender Softwareentwickler aus Belgien, der Gründer der belgischen C++-Benutzergruppe, Autor von „Professional C++“ (Wiley), Koautor von „C++ Standard Library Quick Reference“ (Apress), technischer Redakteur bei einer Vielzahl von Büchern und hat seit 2007 die jährliche MVP-Auszeichnung für seine umfassenden VC++-Kenntnisse empfangen. Sie können Marc unter marc.gregoire@nuonsoft.com erreichen.