---
tags: Windows, C++
---
# Windows Registry - The C++ Way
:::info
## 參考文章
* `::RegGetValueA`
* https://docs.microsoft.com/en-us/windows/win32/api/winreg/nf-winreg-reggetvaluea#parameters
* MSDN 文章
* https://docs.microsoft.com/en-US/archive/msdn-magazine/2017/may/c-use-modern-c-to-access-the-windows-registry
* GitHub repo:
* https://github.com/oiu850714/MyRegistryWrapper
* 基本原理
* https://hackmd.io/_Zt6w-rxRY-nEUk_b423Kw
:::
## Intro
* Windows OS 提供一系列的 C API 來操作 Windows Registry
* e.g. `RegQueryValueEx`
* **fairly low level**
* 甚至還有回傳的字串不保證 null terminated 的問題,導致 caller 還要處理這種情況
* 從 Vista 開始提供了 [`::RegGetValue`](https://docs.microsoft.com/zh-tw/windows/win32/api/winreg/nf-winreg-reggetvaluea?redirectedfrom=MSDN) function,"比較" high level
* 但,他還是很 low level
1. 太過 generic
* 它可以處理各種 registry value type
* from `DWORD`/`QWORD`s to strings to binary data
2. 他還是 C API
* 這篇告訴你如何使用這個"比較" high level 的 C API 來當作基底,用 modern C++ 建構更 high level 的 interface
## Representing Errors Using Exceptions
* 先來建立 exception class
* 首先,`RegGetValue` 是 C API:
* 對 caller signal error 方式是直接用 return code
* type `LONG`
* 成功時回傳 `ERROR_SUCCESS`
* 失敗就回傳對應 code
* e.g.
* `ERROR_MORE_DATA`,這是指 output buffer 裝不下 registry data
* 寫一個 C++ exception 做封裝
1. 繼承 `std::runtime_error`
2. 內部儲存對應的 error code
```cpp
class RegistryError
: public std::runtime_error
{
public:
...
private:
LONG m_errorCode;
};
```
* 要封裝更多東西,也可將 `HKEY` 跟 `subkey`/`value`(忘了的話,參照[基本原理](#參考文章)) 放到 member,但 ctor 會變複雜一些
* 來寫一個 ctor
```cpp
RegistryError(const char* message, LONG errorCode)
: std::runtime_error{message}
, m_errorCode{errorCode}
{}
```
* 然後寫一個 errorCode 的 read-only getter:
```cpp
LONG ErrorCode() const noexcept
{
return m_errorCode;
}
```
* 之後 error handle 都可以用這個 exception 來處理
* 以下示範如何從 Windows registry 讀取各種型態的 registry data
## Reading a `DWORD` Value from the Registry
* 來 demo 如何從 registry 讀一個 `DWORD` data
* 先看 `::RegGetValue` 的 prototype
```c
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
);
```
* 這裡有用到 SAL,可參考[基礎概念筆記](https://hackmd.io/j3KlQJvnTYS516ju5H_e2A)
* 吐血,吃七個參數你敢信
* output buffer(`pvData`) 也是吃 `void*`,痛苦
* 然後因為 output buffer 是 `void*`,所以你需要給 input/output size 到底是多少(`pcbData`)
* 然後指定的 subkey/value 也是 C string,哀傷
* 先說結果,會直接包成以下的 wrapper 來讀取一個 `DWORD`:
```cpp
DWORD RegGetDword(
HKEY hKey,
const std::wstring& subKey,
const std::wstring& value
)
```
* 吃 `HKEY` handle,然後將 subkey 跟 value 用 `std::wstring` 傳進去
* 因為現在是要讀取一個 `DWORD` value
* 所以 `void*` 這種沒有型態的 interface 就可以免了,直接當作 function return value
* buffer size(`pcbData`) 也免了,因為 size 就是 `DWORD` 大小
* 這樣就少了兩個參數!
* 原本 C API `::RegGetValue` 的 `pdwType` 也免了,這是回傳 registry data 的型態,但現在就是指定要拿 `DWORD`
* 然後利用 `std::wstring` 取代 C string
:::warning
* 注意,上面提的 "value" 實際上是比較像 key,詳情 [registry 基本原理](#參考文章)
:::
* 總之,這個 wrapper 比原本的 C API 要簡潔又抽象多了
### 看 `RegGetDword` 的實作
* 這邊 MSDN 文章把 code 拆成一段一段的有點討厭...,先合起來
```cpp
DWORD RegGetDword(
HKEY hKey,
const std::wstring& subKey,
const std::wstring& value
)
{
DWORD data{};
DWORD dataSize = sizeof(data);
LONG retCode = ::RegGetValue(
hKey,
subKey.c_str(),
value.c_str(),
RRF_RT_REG_DWORD,
nullptr,
&data,
&dataSize
);
if (retCode != ERROR_SUCCESS)
{
throw RegistryError{"Cannot read DWORD from registry.", retCode};
}
return data;
}
```
* 其實還挺好理解的,我覺得可以自己看 MSDN 怎麼一步一步解說的
1. 先宣告一個 output buffer,這裡就是一個 DWORD:
```cpp
DWORD data{};
```
2. 再取得 output buffer 的 size,「剛好」也存在 DWORD:
```cpp
DWORD dataSize = sizeof(data);
```
* 注意,這裡故意不將 `dataSize` 宣告成 `const`
* 因為這個變數(的位址)會丟到 `::RegGetValue` 的 `_Inout_opt_` 參數,代表 `dataSize` 可能會被寫入
* 但那個參數也沒有 low level `const` 就是了,所以無法將 `dataSaize` 宣告成 `const`,否則無法編譯
* `dataSize` 最後會被 `::RegGetValue` 寫入實際上塞進 output buffer 的資料的長度
* 只是這裡一樣是 `DWORD`,所以大小不會變
* 之後會 demo 讀取從 Windows registry 讀取 variable length 的 string,那邊才會有用處
3. 最後拿這些參數真正呼叫 low level C API:
```cpp
LONG retCode = ::RegGetValue(
hKey,
subKey.c_str(),
value.c_str(),
RRF_RT_REG_DWORD, // DWORD
nullptr,
&data,
&dataSize
);
```
* 最後檢查 error code,若噴 error 則 throw exception:
```cpp
if (retCode != ERROR_SUCCESS)
{
throw RegistryError{"Cannot read DWORD from registry.", retCode};
}
```
* 然後回傳拿到的 DWORD value:
```cpp
return data;
```
* 加了這層 wrapper,client code 邏輯就會單純很多:
```cpp
DWORD data = RegGetDword(HKEY_CURRENT_USER, L"sub\\key\\path", L"MyDwordValue");
```
* 你只要傳入一個 Registry 的 handle(handle 到底是啥可參照[詳細說明](https://hackmd.io/ycZ78spUT9eNgc1Lldq4VA),或是[官方文件](https://docs.microsoft.com/en-us/windows/win32/sysinfo/handles-and-objects)),以及對應的 subkey 跟 value 即可,跟原本直接使用 C API 相比單純很多
* function 成功執行,你就拿到對應 DWORD
* 執行失敗就噴 exception,讓你可以比較容易將存取邏輯跟錯誤處理分離
* much higher level and much simpler than invoking `::RegGetValue`
* 也可以用類似邏輯拿到 `QWROD` registry value,內部實作將 `DWORD` 換成 `ULONGLONG` 即可
:::warning
不知為啥我用 VS 寫 code 無法使用 `QWORD` 宣告變數...要查一下,這邊按照文件使用 `ULONGLONG`,請看 [GitHub repo](#參考文章)
:::
## Reading a String Value from the Registry
* 從 Windows registry 拿 string data,跟拿 `DWORD`/`QWORD` 最大的差異就是,string 長度是不固定的
* 這也導致原本使用 C API 拿的時候就比較複雜
* 然後,這個 guide 處理 string 的方式很狂,我不確定是不是實務上這樣做比較好: **呼叫 C API 兩次**
* 第一次拿對應 value 的長度
* 第二次用那個長度宣告 output buffer,然後再真正拿資料
:::info
* 這裡作者有提到用 `std::(w)string` 跟 C API 串接的奇技淫巧:
* https://docs.microsoft.com/en-US/archive/msdn-magazine/2015/july/c-using-stl-strings-at-win32-api-boundaries
:::
* 先看 wrapper 的 prototype,長得跟 `WORD` 的很像
```cpp
std::wstring RegGetString(
HKEY hKey,
const std::wstring& subKey,
const std::wstring& value
)
```
* 看看這 API 多簡潔,根本看不到什麼實際上要呼叫兩次 C API 這種鬼東西
* 再來直接看實作,看看什麼是呼叫 API 兩次
* 第一次:
```cpp
DWORD dataSize{};
LONG retCode = ::RegGetValue(
hKey,
subKey.c_str(),
value.c_str(),
RRF_RT_REG_SZ, // string
nullptr,
nullptr,
&dataSize
);
```
* 這次不給 output buffer(設成 `nullptr`) 了,只給 output size;
* 而且 output size 也不先設定成特定大小(只有 value initialze),這是只有在不給 output buffer 時才能這樣設定,詳情 `::RegGetValue` API 文件:
* If `pvData` is `NULL`, and `pcbData` is non-`NULL`, the function returns `ERROR_SUCCESS` and stores the size of the data, in bytes, in the variable pointed to by `pcbData`. **This enables an application to determine the best way to allocate a buffer for the value's data**.
* 一樣如果呼叫失敗則 throw:
```cpp
if (retCode != ERROR_SUCCESS)
{
throw RegistryError{"Cannot read string from registry", retCode};
}
```
* **接下來就拿上面被設定好的 `dataSize` 來宣告一個對應大小 `std::wstring`**:
```cpp
std::wstring data;
data.resize(dataSize / sizeof(wchar_t));
```
* **注意 resize 的邏輯**
* C API 回傳的長度是以 byte 為單位,要除以 `wchar_t` 才會是 `wchar_t` 的數量
* 雖然 `data` 最後底層配置的空間還是跟 `dataSize` 一樣大就是了
* quick math!
* 最後再呼叫一次 C API,這時把這個空間當 output buffer 傳進去:
```cpp
retCode = ::RegGetValue(
hKey,
subKey.c_str(),
value.c_str(),
RRF_RT_REG_SZ, // string
nullptr,
&data[0],
&dataSize
);
```
:::danger
* 請仔細看到底傳了什麼鬼參數到 `::RegGetValue`
* 看完了? 可以繼續往下了
:::
* 別忘記 `data` 是 `std::wstring`,**要把他當 buffer 不是單純用 `&data`,這樣會 GG**
* 而是想辦法拿到 internal buffer 的位址,作法就是 `&data[0]`...
* 這樣好像一副當 `std::wstring::c_str()` 的 `const` 是塑膠似的...
:::info
* 其實用 `std::wstring::data` 也可以拿到 internal buffer,而且這個 function "不 modern",不確定為什麼沒有提到
* https://en.cppreference.com/w/cpp/string/basic_string/data
:::
* 真的寫到 buffer 之後一樣要做 error check:
```cpp
if (retCode != ERROR_SUCCESS)
{
throw RegistryError{"Cannot read string from registry", retCode};
}
```
* **然後你以為就可以回傳了? 才怪! 還有一堆大便要處理**
* *處理完之後你就會知道有這層 wrapper 有多重要了*
* 首先,雖然是第二次呼叫 C API,API 一樣會回傳這次實際寫入的大小到 `dataSize` 內
* **第一次跟第二次得到的長度有可能不同,畢竟不是只有你再存取 registry**
* **所以必須使用這個新的長度來 resize 這個當作 output buffer 的 `std::wstring`**
* 這很重要,因為你實際上是直接用 C API 去改 `std::wstring`(不管是用 `&data[0]` 還是 `data()` method) 的 internal buffer,若塞入的長度跟 `std::wstring` 維護的內部狀態不同的話,物件會爛掉
```cpp
DWORD stringLengthInWchars = dataSize / sizeof(wchar_t);
```
* 另外 C API **回傳的長度會包含 null character,必需扣掉**...
```cpp
stringLengthInWchars--; // Exclude the NUL written by the Win32 API
data.resize(stringLengthInWchars);
```
* 沒扣掉然後直接 resize 的話,假設 `stringLengthInWchars` 是 10,這時 `size()` 就會是 10,但是實際上 `[9]` 已經是 `'\0'` 了,這時 IO 就會爛掉
* 這讓我對第一次呼叫 C API 時拿到的 `dataSize` 感到更恐懼了
* 如果第二次拿字串之前 string 變大的話 buffer 會不夠大,可能會觸發 `ERROR_MORE_DATA`
* 另外這邊不用擔心用 `::RegGetValue` 拿到的字串沒有 null terminated,這個 API 保證一定會 null terminated,就算當時儲存的時候沒有加上去
* 更早版本的 `RegQueryValueEx` 就不保證這件事了,夭壽
* 這時候你終於可以回傳了...
```cpp
return data;
```
* client code:
```cpp
wstring s = RegGetString(HKEY_CURRENT_USER, subkey, L"MyStringValue");
```
* 簡潔,可讀性好
## Reading Multi-String Values from the Registry
* registry data 還有一種型態是 multi string
* Basically, this is *a set of double-NUL-terminated strings packed in a single registry value(data)*.
* 格式
* 多個 null-terminated strings 直接放在一起
* 最後一個 null-terminated string 後面再接一個 null
* *double NUL-terminated*
:::info
* 介紹 multi strings(double-NUL-terminated strings) 格式的文章:
* https://devblogs.microsoft.com/oldnewthing/20091008-00/?p=16443
* 有一個重點: double-NUL-terminated strings **實際上就是一堆 null terminated string 擺在一起,只是最後一個 string 的長度是 0**
* 但這也代表除了最後一個 string 之外,中間的 string 不能是空字串 XD
If you’re writing a helper class to manage double-null-terminated strings, make sure you watch out for these empty strings.
This reinterpretation of a double-null-terminated string as really a *list of strings with an empty string as the terminator* makes writing code to walk through a double-null-terminated string quite straightforward.
* 只是這篇 Registry MSDN 文章沒有用這個觀點來寫 parsing multi string 的邏輯就是了
* 在後面,自己看
:::
* 基本上,取 multi string data 的 wrapper,整個流程跟取 single string 類似
* 先呼叫 `::RegGetValue` 拿 whole data size
* 再創造對應大小的 buffer 當 output buffer
* **只是這的 high level wrapper 還需要把這個放了 multi string 的 raw output 做切割**,改成回傳 `std::vector<std::wstring>>`
* 所以 wrapper interface 會長這樣:
```cpp
std::vector<std::wstring> RegGetMultiString(
HKEY hKey,
const std::wstring& subKey,
const std::wstring& value
)
```
### 實作
* 第一步,先拿 data size:
```cpp
DWORD dataSize{};
LONG retCode = ::RegGetValue(
hKey,
subKey.c_str(),
value.c_str(),
RRF_RT_REG_MULTI_SZ, // multi string
nullptr,
nullptr,
&dataSize
);
```
* 跟拿 single string 的第一步差不多
* 拿不到就 throw:
```cpp
if (retCode != ERROR_SUCCESS)
{
throw RegistryError{"Cannot read multi-string from registry", retCode};
}
```
* 接下來,配置一個 buffer 來拿 data
```cpp
std::vector<wchar_t> data;
data.resize(dataSize / sizeof(wchar_t));
```
* **注意 buffer 型態是 `std::vector<wchar_t>`**,代表存了 `wchar_t` 的 "raw buffer",會比直接用 `std::wstring` 更精確
* 之後要自己 parsing 成多個 `std::wstring`
* 第二步,一樣把資料塞到 output buffer:
```cpp
retCode = ::RegGetValue(
hKey,
subKey.c_str(),
value.c_str(),
RRF_RT_REG_MULTI_SZ,
nullptr,
&data[0],
&dataSize
);
```
* 注意一樣有 `&data[0]` 這種寫法
* error check:
```cpp
if (retCode != ERROR_SUCCESS)
{
throw RegistryError{"Cannot read multi-string from registry", retCode};
}
```
* 然後也跟拿 single string 一樣,要用第二次呼叫 `::RegGetValue` 之後的 `dataSize` 來對 `data` 做 resize:
```cpp
data.resize( dataSize / sizeof(wchar_t) );
```
* 到這個時候,`data` 內存的就是 "double-NUL-terminated string sequence".
* 最後一步就是將這個 raw data 轉換成更抽象的 `std::vector<std::wstring>`:
```cpp
// 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;
// currStringPtr + currStringLength 會是某個字串的 null character
// + 1 就會是下一個字串的頭
// 若還是 null,則代表
// 這是最後一個 null-terminated string
// 之後的 null character,代表處理完所有字串了
}
```
* 用 [`wcslen`](https://en.cppreference.com/w/c/string/wide/wcslen) 拿一個 `wchar_t` 的字串長度!
* 最後 return 這個 `vector`:
```cpp
return result;
```
* caller 這樣呼叫即可:
```cpp
vector<wstring> multiString = RegGetMultiString(
HKEY_CURRENT_USER,
subkey,
L"MyMultiSz"
);
```
## Enumerating Values Under a Registry Key
* 還有一個常見情境是,取得某個 registry (sub)key 底下的全部 value
* 也有對應的 C API,[`::RegEnumValue`](https://docs.microsoft.com/zh-tw/windows/win32/api/winreg/nf-winreg-regenumvaluea?redirectedfrom=MSDN)
* 邪惡帝國官方解決方案
* https://github.com/Microsoft/wil/wiki/RAII-resource-wrappers
* 之後也會提
## A Safe Resource Manager for Raw HKEY Handles
## Wrapping Up