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