Typesafe Registry for Windows

Typesafe Registry for Windows

By Antony Pinchbeck

Overload, 9(42):, April 2001


The windows API provides generic methods for transferring data to and from the registry. This generality means that the actual data needs to be coerced into and out of a byte array. This on it's own is not enough; the type of the data is also required. The functions also provide the type of data. When reading data from the windows registry the following function is used in win32.

LONG RegQueryValueEx(
  HKEY hKey,            // handle to key
  LPCTSTR lpValueName,  // value name
  LPDWORD lpReserved,   // reserved
  LPDWORD lpType,       // type buffer
  LPBYTE lpData,        // data buffer
  LPDWORD lpcbData      // size of data buffer
);

As you can see the data is returned as a number of bytes, which means code that uses this function is often written as:

int StartParam;
DWORD Type = 0;
DWORD Size = sizeof(StartParam);
RegQueryValueEx(hkey, "Start", NULL, &Type, (BYTE*)&StartParam, &Size);

To properly check that the value has been read correctly the Type variable would be checked for the correct type e.g. REG_DWORD . If there was a mismatch then an error needs to be reported as the data in StartParam is meaningless. A better approach would be to wrap the registry API. All the error checking will now be in a single class and therefore much easier to manage. In order for this to provide a typesafe interface the designer must make the class support at least the built-types for the registry. The class would also have to be designed for derivation so that it could be extended to support bool for example.

The registry interface has been wrapped in C++ many times so one more won't hurt. I like to use STL, so I require that the interface makes the registry look like an STL container, the following is required:

  • Read data in a typesafe way.

  • Write data in a typesafe way.

  • Provide a mechanism to iterate values.

  • Provide a mechanism to iterate keys.

  • Errors to be propagated as exceptions.

  • Provide a simple and concise interface.

  • Encapsulate all in a namespace

Initially I set about writing a simple program to open a registry key and read a DWORD value. I created a simple class to encapsulate the concept of an HKEY value:

class Key {
public:
  typedef ref_count<Key> RCKey;
  Key(HKEY key): mKey(key) {}
  ~Key() {::RegCloseKey(mKey);}
  HKEY GetHandle() const {return mKey;}
private:
  HKEY mKey;
};

Opening a registry key is a stand alone operation so I created the Open function:

Key::RCKey Open(HKEY key, LPCTSTR sub_key){
  HKEY temp_key;
  CheckReturn(::RegOpenKeyEx(key, sub_key, 0, KEY_ALL_ACCESS, &temp_key));
  return Key::RCKey(new Key(temp_key));
}

The registry key will be closed when the Key instance goes out of scope. Why return the key as reference counted object? I did not want a function using a registry key to have access errors because the key had been closed elsewhere in the code. This type of problem could be difficult to track down.

Now moving on to the more interesting parts.

Reading and writing a DWORD to a registry entry

typedef std::vector<BYTE> Buffer;
DWORD GetValue(LPCTSTR sub_key){
  DWORD length = 0;
  DWORD type;
  try {
    CheckReturn(::RegQueryValueEx(mKey,sub_key, 0, &type, 0, &length));
    Buffer buffer;
    buffer.resize(length);
    CheckReturn(::RegQueryValueEx(mKey, sub_key, 0, &type, &buffer[0],&length));
    return *( reinterpret_cast<DWORD *>(&buffer[0]));
  }
  catch(const Exception& e) {
    std::stringstream s;
    s << "Registry Error: Accessing '" << sub_key << "' " << e.What();
    throw Exception(s.str().c_str(), e.ErrorCode());
  }
}

The CheckReturn function tests the return code and will throw an exception if it is not ERROR_SUCCESS . Why catch the exception within the function, why not just let it propagate through. If you use the Win32 FormatMessage function with a registry error you will see just generic text like 'File not found'. This is clearly inadequate when the class can report extra information to the client.

Because of the non-typesafe interface to the registry reading a string requires the same sequence of operations, but casting to a string as the end result. A function would be written for each return type that needs to be retrieved from the registry. Alas a problem looms, the return type is not part of the function signature. So by having two functions:

DWORD GetValue(LPCTSTR sub_key);
std::string GetValue(LPCTSTR sub_key);

The compiler will generate errors with the functions only differing by return type.

Hmmmm ...

With a slight modification the functions now look like:

void GetValue(LPCTSTR sub_key, DWORD& dw);
void GetValue(LPCTSTR sub_key, std::string& str);

Now there are no errors, but we have to write a function for each type we need to support. That could be a lot of typing, plus the functions are almost the same.

(enter stage left: Sir template with the sword of hope)

By exploiting the use of traits and template functions, a generic typesafe interface can be created.

Commonality

The method of retrieving the data from the registry is the same regardless of the data type.

Variability

The method of converting the data is dependent upon the type required.

By using a ConvertTraits class the variability can be specified by the type of data required. This involves creating a null implementation for the general case and specific ones for the data types required. The null case will then cause the compiler to emit errors for unsupported types.

template <typename _t> struct ConvertTraits;
template<>
struct ConvertTraits<DWORD> {
  enum { reg_type = REG_DWORD };
  static LPCTSTR exceptionMessage(){ return "REG_DWORD: Can't convert to DWORD";
}
  static void Read(DWORD& dw, DWORD type,Buffer& buffer){
    dw = *(reinterpret_cast<DWORD *>(&buffer[0]));
  }
//...
};
template<>
struct ConvertTraits<std::string>{
  enum { reg_type = REG_SZ };
  static LPCTSTR exceptionMessage(){ return "REG_SZ: Can't convert to string";
}
  static void Read(std::string& str, DWORD type, Buffer& buffer){
  str = std::string(reinterpret_cast<TCHAR *>(&buffer[0]));
  }
//...
};

The template function to retrieve the data from the registry uses the traits class to perform the specific conversions required.

template<typename _t>
void GetValue(LPCTSTR sub_key, _t& val){
  typedef ConvertTraits<_t> traits;
  DWORD length = 0;
  DWORD type = 0;
  try {
    CheckReturn(::RegQueryValueEx(mKey, sub_key, 0, &type, 0, &length));
    Buffer buffer;
    buffer.resize(length);
    CheckReturn(::RegQueryValueEx(mKey, sub_key, 0, &type, &buffer[0], &length));
    if(traits::reg_type == type)
    traits::Read(val, type, buffer);
  else
    throw Exception(traits::exceptionMessage(), 0);
  }
  catch(const Exception& e){
    std::stringstream s;
    s << "Registry Error: Accessing '" << sub_key << "' " << e.What();
    throw Exception(s.str().c_str(), e.ErrorCode());
  }
}

With this function part of the Key class we can now write convenient typesafe routines:

void Test(){
  try {
    registry::Key::RCKey key = registry::Open(HKEY_LOCAL_MACHINE, "SOFTWARE\\Test");
// read value
    DWORD dw;
    key->GetValue("DwordTest", dw);
    // some string stuff
    std::string str;
    key->GetValue("StringTest", str);
  }
  catch(const registry::Exception& e){
    std::stringstream s;
    s << e.What() << " " << std::hex << e.ErrorCode();
    ::OutputDebugString(s.str().c_str());
    ::OutputDebugString("\n");
  }
}

Writing values back to the registry is easier than reading them, but with one twist. The type of the data is required. This is the type to map to in the registry, DWORD to REG_DEWORD and std::string to REGS_SZ . In the ConvertTraits class an enum reg_type is defined, this is the type used by the registry. The write function of the traits class is simple, convert the user type to a byte array, for DWORD it could be:

template<>
struct ConvertTraits<DWORD>{
//...
  static void Write(DWORD& dw, Buffer& buffer) {
    buffer.resize(sizeof(DWORD));
    ::memcpy(&buffer[0], &dw, sizeof(DWORD));
  }
};

and a string:

template<>
struct ConvertTraits<std::string> {
//...
  static void Write(std::string& str, Buffer& buffer) {
    buffer.resize(str.length());
    std::copy(str.begin(), str.end(), buffer.begin());
  }
};

Then the SetValue template function can be written using the traits reg_type to define the type used by the registry.

template<typename _t> 
void SetValue(LPCTSTR sub_key, _t& val){
  typedef ConvertTraits<_t> traits;
  try   {
    Buffer buffer;
    traits::Write(val, buffer);
    CheckReturn(::RegSetValueEx(mKey, sub_key, 0, traits::reg_type, 
                          &buffer[0], buffer.size()));
  }
  catch(const Exception& e) {
    std::stringstream s;
    s << "Registry Error: Writing '" << sub_key << "' " << e.What();
    throw Exception(s.str().c_str(), e.ErrorCode());
  }
}

The final part is to generate two iterators to allow traversal of the sub-keys and values for the key object. The sub-key iterator returns the name of the key. The value iterator returns a structure containing both the name and the type of the entry. The registry class can then be used in any place where a forward iterator is valid.

registry::Key::value_iterator value_it = key->ValueBegin();
while(value_it != key->ValueEnd()){
  registry::Key::value_iterator::value_type data = (*value_it);
  ++value_it;
}
registry::Key::key_iterator key_it = key->KeyBegin();
while(key_it != key->KeyEnd()){
  std::string key_txt = (*key_it);
  ++key_it;
}

The registry key class is open for extension, to add a new type all a user of the class is required to do is provide a suitable ConvertTraits class and leave the rest to the existing code.

This article and source code as a VC6 project is available by sending an email to with the subject line of registry.

References

C++ Programming Language (Stroustrup)

MSDN (Microsoft)






Your Privacy

By clicking "Accept Non-Essential Cookies" you agree ACCU can store non-essential cookies on your device and disclose information in accordance with our Privacy Policy and Cookie Policy.

Current Setting: Non-Essential Cookies REJECTED


By clicking "Include Third Party Content" you agree ACCU can forward your IP address to third-party sites (such as YouTube) to enhance the information presented on this site, and that third-party sites may store cookies on your device.

Current Setting: Third Party Content EXCLUDED



Settings can be changed at any time from the Cookie Policy page.