2
\$\begingroup\$

The point is to get the serial number of one of the hard drives on Windows. My code is based on an example from MSDN. I tried to make it an exception-safe, self-contained function. It works, to the best of my knowledge. All comments are welcome, especially about correctness, exception safety and performance issues.

#define _WIN32_DCOM
#include <Wbemidl.h>
#include <comdef.h>

#include <memory>
#include <optional>
#include <string_view>

#pragma comment(lib, "wbemuuid.lib")

namespace ComWrappers {
    struct ComDeleter final {
        template <class T>
        auto operator()(T* const p) const noexcept -> void {
            if (p) p->Release();
        }
    };

    template <class T>
    using ComPtr = ::std::unique_ptr<T, ComDeleter>;

    struct ComInit final {
    private:
        bool _succeeded;

    public:
        ComInit() noexcept
            : _succeeded{ SUCCEEDED(::CoInitializeEx(nullptr, COINIT_MULTITHREADED)) }
        {}

        [[nodiscard]] operator bool() const noexcept { return _succeeded; }

        ~ComInit() noexcept {
            if (_succeeded) ::CoUninitialize();
        }
    };
}  // namespace ComWrappers

[[nodiscard]] auto GetHardDriveSerialNumber() noexcept -> ::std::optional<::std::wstring> {
    using namespace ::ComWrappers;

    // Initialize COM
    ComInit comOwner;
    if (!comOwner) return ::std::nullopt;

    // locator to WMI
    ComPtr<::IWbemLocator> loc = nullptr;
    {
        ::IWbemLocator* pLoc = nullptr;
        auto const hres =
            ::CoCreateInstance(CLSID_WbemLocator, 0, CLSCTX_INPROC_SERVER,
                IID_IWbemLocator, reinterpret_cast<LPVOID*>(&pLoc));

        if (FAILED(hres)) return ::std::nullopt;
        loc.reset(pLoc);
    }

    ComPtr<::IWbemServices> svc = nullptr;
    {  // Connect to WMI
        ::IWbemServices* pSvc = nullptr;
        auto const hres = loc->ConnectServer(
            _bstr_t(L"ROOT\\CIMV2"), 
            nullptr, nullptr, 0, 0, 0, 0, 
            &pSvc
        );
        if (FAILED(hres)) return ::std::nullopt;
        svc.reset(pSvc);
    }

    {  // Set security levels
        auto const hres = ::CoSetProxyBlanket(
                svc.get(),     
                RPC_C_AUTHN_WINNT,             
                RPC_C_AUTHZ_NONE,              
                nullptr,                       
                RPC_C_AUTHN_LEVEL_CALL,        
                RPC_C_IMP_LEVEL_IMPERSONATE,   
                nullptr,                       
                EOAC_NONE                      
            );

        if (FAILED(hres)) return ::std::nullopt;
    }

    ComPtr<::IEnumWbemClassObject> enumerator = nullptr;
    {  // Make requests of WMI
        ::IEnumWbemClassObject* pEnumerator = nullptr;
        auto const hres = svc->ExecQuery(
            bstr_t("WQL"), bstr_t("SELECT SerialNumber FROM Win32_PhysicalMedia"),
            WBEM_FLAG_FORWARD_ONLY | WBEM_FLAG_RETURN_IMMEDIATELY, nullptr,
            &pEnumerator);

        if (FAILED(hres)) return ::std::nullopt;
        enumerator.reset(pEnumerator);
    }

    ::std::optional<::std::wstring> result = ::std::nullopt;

    // Get the data
    while (true) {
        ComPtr<::IWbemClassObject> clsObj = nullptr;
        {
            ::IWbemClassObject* pclsObj = nullptr;
            ULONG uReturn = 0;
            auto const hres = enumerator->Next(WBEM_INFINITE, 1, &pclsObj, &uReturn);

            if (FAILED(hres) || uReturn == 0) break;
            clsObj.reset(pclsObj);
        }

        VARIANT vtProp{};

        // Get serial number
        auto const hres = clsObj->Get(L"SerialNumber", 0, &vtProp, 0, 0);

        if (FAILED(hres)) continue;

        // RAII for variant class
        struct VariantOwner {
            VARIANT* v;
            ~VariantOwner() noexcept {
                ::VariantClear(v);
            }
        } variantOwner{ &vtProp };

        // can't allow empty strings
        if (*vtProp.bstrVal == '\0') continue;

        try {
            result = vtProp.bstrVal;
        }
        catch (...) {
            // no need to do anything as
            // a failure would return the default 'nullopt'
        }
        break;
    }

    return result;
}


// Example test driver

#include <iostream>

auto main() -> int {
    auto const hid = GetHardDriveSerialNumber();
    if (hid) ::std::wcout << *hid << '\n';
    else ::std::wcout << "Failed to get serial number\n";
}
\$\endgroup\$
5
  • \$\begingroup\$ good show using std::unique_ptr for COM. I've found that most people have not stumbled onto that trick. Good adding RAII to COM VARIENT, but shouldn't that be part of the COM_wrappers namespace and library? \$\endgroup\$
    – JDługosz
    Apr 29, 2021 at 22:23
  • \$\begingroup\$ What if the thread you call this on is already activated into some kind of COM apartment that's not necessarily a Multithreaded apartment? \$\endgroup\$
    – JDługosz
    Apr 29, 2021 at 22:40
  • \$\begingroup\$ @JDługosz The VariantOwner class should only call VariantClear if clsObj->Get succeeds. For that reason, it's a bit too specialized and I didn't want to put to the general namespace. \$\endgroup\$ Apr 30, 2021 at 5:57
  • \$\begingroup\$ @JDługosz I am not sure about your second comment. Do you recommend I check for RPC_E_CHANGED_MODE? \$\endgroup\$ Apr 30, 2021 at 5:59
  • \$\begingroup\$ What I've done is create my own thread that wasn't used already by the caller. I don't recall a CHANGED_MODE ... just an error if it was already set up, and maybe a count if it's called more than once in a compatible way? \$\endgroup\$
    – JDługosz
    Apr 30, 2021 at 15:34

1 Answer 1

1
\$\begingroup\$
if (FAILED(hres)) return ::std::nullopt;
svc.reset(pSvc);

It bothers me that this is repeated, and more generally, you need boilerplate to use one of the COM Wrappers. Ideally, &svc would return the thing that the COM calls actually wanted. Since you are not making a special COM wrapper but relying on unique_ptr you can instead make a non-member function that's not called operator&. So:

loc->ConnectServer(
    _bstr_t(L"ROOT\\CIMV2"), 
    nullptr, nullptr, 0, 0, 0, 0, 
    COM(svc) // 👈 no separate "plain" variable
    );

You would not need a separate plain pointer that needs to be reset into the variable you really wanted. You could rely on this still being nullptr on failure, without having to check the HRESULT, in cases where you were not diagnosing the issue.

Generally, though, turn the HRESULT into an error_code, with a common function. I have some memory that, in the past anyway, MSVC++ did not have a fully fleshed out error_category for Windows errors, but Boost did. I don't know if that's still the case, but I want to point out that this is available, complete with the error text look up.

You could have the helper I called COM here simply return the address of the underying pointer directly, or it could return a proxy that has both an operator T** and an operator void** so you don't need the cast in the CoCreateInstance call.

\$\endgroup\$
3
  • \$\begingroup\$ I don't think your COM function would work since we don't have a reference (or pointer) to the internal pointer of unique_ptr. Could you give a simple implementation of what you have in mind? \$\endgroup\$ Apr 30, 2021 at 6:05
  • 1
    \$\begingroup\$ I think I got it. It's a good idea. \$\endgroup\$ Apr 30, 2021 at 6:35
  • \$\begingroup\$ directly we know that the only instance data in the unique_ptr is the underlying pointer, so just reinterpret_cast. Include some static_assert to verify that assumption in case you use some special debug version. proxy you got it. \$\endgroup\$
    – JDługosz
    Apr 30, 2021 at 15:18

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge that you have read and understand our privacy policy and code of conduct.

Not the answer you're looking for? Browse other questions tagged or ask your own question.