Ever since Windows Vista, the Win32 subsystem has support for using transactions with file and registry operations (among others). Using transactions, those operations can be tied together and either committed or rolled back in a single operation. This article demonstrates how to do that in C++.
Introduction
The Win32 subsystem is the interface that sits between a user application and the Windows system. It’s a cohesive set of APIs on which all higher level frameworks are built. Virtually every application that runs on Windows will use the Win32 subsystem because the .NET Framework, Java runtime libraries, MFC, … all translate their higher level features to Win32 calls.
The Win32 subsystem itself doesn’t get a whole lot of interest from programmers because there is little need to interact with it directly. Still, it has some interesting features that can be very useful. And because any language will be able to make Win32 calls, adding these features to your toolbox is very easy.
In this article, I want to explain a feature that, in my opinion, has received far too little love: transactions. I will describe a scenario where this feature has an immense added value. I will talk a bit about the API itself, and then I will explain how I’ve implemented it in my application.
Scenario
Consider the scenario where we have an application whose configuration is updated. In our example, the application has a config file that can be somewhere on the disk, and a registry value indicating the path of the file. If the application wants to save its most recent configuration, it updates the registry file with the name of the new file and saves the file to disk.
This is a very basic example of two changes that need to succeed or fail together. If the file cannot be created, the registry value should not be updated. Now for this example, the answer is trivial: we write the file first, and only update the registry after. That is foolproof.
Or is it? Sure, we can write the file first. But what if we overwrite it? Then we need to make a temporary copy first, perform the write, restore the file if there was an error and verify that the restore was successful. That is already a decent amount of extra code for a simple operation. It gets a lot more complex quickly if it is even slightly more involved than one file operation with one registry key update. Suppose the file is updated by two different operations, and the second operation fails? Or suppose there is one file to which something needs to be added, and one file that needs to be overwritten?
I am of the same school of thought as John Robbins who wrote ‘Debugging Applications’: a good programmer needs to always look at their design, ask themselves ‘what if …’ and then formulate an answer. If you ‘what if’ your code to death, it is likely that half the actual code is to deal with the ‘what if’s’. In the example I mentioned: if there are multiple individual files or registry keys, and you need to manually program rollbacks for every possible error scenario so that you always leave the system in a known state, that can be a very complex task.
Transactions
Consider the possibility of doing file and registry updates in database-like transactions in the scenario I outlined earlier. Programming the changes, making temporary backups, restoring those backups in case of problems, removing previous stale backups, verifying that the system state was not modified, … can be complex and error prone.
With transactions however, we literally don’t have to care about any of that.
We open a transaction handle and simply perform all IO operations via that transaction. After all changes are made, we look at the error state of the actions we have performed and either commit the transaction or rollback. If we commit, all changes become active simultaneously. And if we detect an error and tell the transaction to rollback, it’s as if nothing ever happened, regardless of what happened.
Even if the system should lose power or crash halfway, there will be no configuration information in an unknown state.
Win32 Transactions
Ever since Windows Vista, Windows has the ability to perform actions in a transactional way, making it possible to update files, registry keys, named pipes, … as part of a transaction that can be committed or rolled back as one operation where either all operations are made active, or none are made active. The way Microsoft has implemented transactions is basically to add two sets of functionality: functions to manage the transaction object itself, and functions to connect your operations to those transactions.
The former are the easiest. Of those, we will look at CreateTransaction
, CommitTransaction
and RollbackTransaction
in more detail. Note that there is a much larger suite of management routines. These are beyond the scope of this article. You can read more about those here.
The latter aren’t exactly difficult to use, but they are ugly and can be a bit nuanced. I will explain two of them in detail: CreateFileTransacted
and RegCreateKeyTransacted
. There is a great number of operations for which Microsoft has implemented transaction support, and they basically just took the every function they wanted to add support to, glued Transacted
to the function name, and updated the parameter list to create a new function.
For the sake of completeness, I need to point out that it is possible to implement custom transaction managers and resource managers. If you have some sort of object management implemented in your system design, then it is possible to make it transaction aware so that it can work together with the rest of the Win32 transaction eco system. That too is well beyond the scope of this article. You can read more about that at this link.
Sadly there is one regrettable thing that I must mention. Because there hasn’t been a significant adoption of the technology, Microsoft is putting notices in the documentation, encouraging users to find other solutions for NTFS transactions, and is warning that NTFS transaction may be removed in the future.
I do hope it doesn’t come to that because, IMO, NTFS transactions are a phenomenal technology that should have been pushed harder. Registry and other transactions do not come with that warning at this time. That alone is worth putting in the time for, because the ability to handle complex registry updates in a transactional manner is great.
Creating the Transaction
This is perhaps the simplest part of the process. You can simply create a transaction handle and that’s it. Microsoft has documented it as follows:
HANDLE CreateTransaction(
[in, optional] LPSECURITY_ATTRIBUTES lpTransactionAttributes,
[in, optional] LPGUID UOW,
[in, optional] DWORD CreateOptions,
[in, optional] DWORD IsolationLevel,
[in, optional] DWORD IsolationFlags,
[in, optional] DWORD Timeout,
[in, optional] LPWSTR Description);
UOW
, IsolationLevel
and IsolationFlags
are reserved parameters so we just ignore them. lpTransactionAttributes
is a way to assign a specific security descriptor to the transaction, which is something you don’t need if you’re a third party developer like us who creates and uses the transaction in a single process where the ACL comes from either the Primary user or the Impersonation user. The only parameter that may be of use is the Description parameter.
Because we don’t need these parameters in most cases, I have created a wrapper for it. It is possible to reuse the same function name and create an overloaded function but I prefer to do it like this, to make it obvious, this is not an official function, and to make it clear to another developer that this is indeed a simplified function with a more limited scope.
HANDLE CreateTransactionSimple(LPWSTR Description){
return CreateTransaction(
NULL, NULL, 0, 0, 0, 0, Description); }
CommitTransaction
and RollbackTransaction
do not require additional explanation because they just take the transaction handle as input and either commit or roll back the transaction respectively.
CreateFileTransacted
The CreateFileTransacted
function name expands to either an ASCII or Unicode version. I’m showing the Unicode version. Most of these parameters are the same as the non-transactional version. I’m not going to describe them here. Of interest are only the three last parameters.
HANDLE CreateFileTransactedW(
[in] LPCWSTR lpFileName,
[in] DWORD dwDesiredAccess,
[in] DWORD dwShareMode,
[in, optional] LPSECURITY_ATTRIBUTES lpSecurityAttributes,
[in] DWORD dwCreationDisposition,
[in] DWORD dwFlagsAndAttributes,
[in, optional] HANDLE hTemplateFile,
[in] HANDLE hTransaction,
[in, optional] PUSHORT pusMiniVersion,
PVOID lpExtendedParameter);
lpExtendedParameter
is reserved so we just pass in NULL. hTransaction
is the transaction we are using. If the file handle is created, it is linked to the transaction. Subsequent file operations can all be done using the normal APIs which don’t make a difference between transacted files or other types of file.
pusMiniversion
is a not used except in very special cases. What that parameter does is if you are opening that file from multiple places, who gets to see which version of that file while transactions are ongoing. By default, the transaction that is modifying the file sees the dirty view of the file, and other clients see the view of the file as it was when it was last committed.
I created a simplified wrapper for this function too:
HANDLE CreateFileTransactedSimple(
const LPCTSTR& filepath, DWORD desiredAccess, DWORD createDisposition, const HANDLE& transaction) {
return CreateFileTransacted(
filepath,
desiredAccess,
FILE_SHARE_READ, NULL, createDisposition,
FILE_ATTRIBUTE_NORMAL, NULL, transaction,
NULL, NULL); }
Aside from the parameters that are reserved or unused, the wrapper also specifies that the file is a regular file without special attributes (compressed, encrypted, ….). It also specifies that when we are using it, others can open the file for reading. For newly created files, this is pointless because they won’t even see the file. But if we open an existing file, others can still see the previous view until we commit.
RegCreateKeyTransacted
This function creates a registry key handle which may be used for registry operations under a transaction. It is documented as follows:
LSTATUS RegCreateKeyTransactedW(
[in] HKEY hKey,
[in] LPCWSTR lpSubKey,
DWORD Reserved,
[in, optional] LPWSTR lpClass,
[in] DWORD dwOptions,
[in] REGSAM samDesired,
[in, optional] const LPSECURITY_ATTRIBUTES lpSecurityAttributes,
[out] PHKEY phkResult,
[out, optional] LPDWORD lpdwDisposition,
[in] HANDLE hTransaction,
PVOID pExtendedParemeter
);
As with the previous function, I’m not going to cover the parameters that are also in the regular function call. Only the last two parameters are of interest, and the are conceptually the same as with the previous function. As soon as the registry key handle is created, the handle is linked to the transaction and you can use the normal APIs for using it.
For this function, I also wrote a wrapper:
LSTATUS RegCreateKeyTransactedSimple(
HKEY parentKey, const LPCTSTR& regkey, REGSAM samDesired, HKEY& regkeyhandle, const HANDLE& transaction) {
return RegCreateKeyTransacted(
parentKey,
regkey,
0, NULL, REG_OPTION_NON_VOLATILE, samDesired,
NULL, ®keyhandle,
NULL, transaction,
NULL); }
Creating the Application for Our Scenario
With all that out of the way, we can put everything together.
The Overall Structure
The structure of the client is very simple and easy to understand. After we create the transaction, we do all the relevant configurations. At the end, we either commit or roll back. It couldn’t be simpler and it’s definitely much less lines of code and much more reliable than a bunch of manually created data backup and restore code.
HANDLE transaction = CreateTransactionSimple();
if (transaction == INVALID_HANDLE_VALUE)
{
cout << "Failed to create Win32 Transaction\n";
return GetLastError();
}
if (error == NO_ERROR){
cout << "Committing transaction\n";
CommitTransaction(transaction);
}
else{
cout << "Rolling back the transaction\n";
RollbackTransaction(transaction);
}
CloseHandle(transaction);
}
Getting the User Input
The logic of our program is that we get a new filename from the user which is acting as our pretend settings file.
Note that we don’t do any input validation on purpose here. The user can provide a filename with illegal characters. This will trigger an error and can be used to demonstrate the effectiveness of the transactions in dealing with errors.
Since the rest of the application is TCHAR
aware, but the standard library doesn’t work with this concept, I implement these two options explicitly.
#ifdef _UNICODE
cout << "Enter the name of the file to be created:\n";
wstring filename;
getline(wcin, filename);
cout << "Enter the root folder or press enter for c:\\temp\\:\n";
wstring rootfolder;
getline(wcin, rootfolder);
if (rootfolder.empty())
rootfolder = wstring(TEXT("C:\\TEMP\\"));
wstring filepath = rootfolder + filename;
#else
cout << "Enter the name of the file to be created:\n";
string filename;
getline(cin, filename);
cout << "Enter the root folder or press enter for c:\\temp\\:\n";
string rootfolder;
getline(cin, rootfolder);
if (rootfolder.empty())
rootfolder = string(TEXT("C:\\TEMP\\"));
string filepath = rootfolder + filename;
#endif
Writing the Registry
Here, we update the registry after opening a registry key using the transaction we create earlier. You’ll note that the functions for updating the registry are the same ones you are used to. Although here I wrapped the SetRegValueEx
function to hide the ugly typecasting that is required to write a string to the registry.
HKEY registryRoot = HKEY_CURRENT_USER;
LPCTSTR regkey = TEXT("Win32Transaction");
LPCTSTR valueName = TEXT("ConfigFile");
DWORD error = NO_ERROR;
HKEY regkeyhandle = NULL;
if (ERROR_SUCCESS != RegCreateKeyTransactedSimple(
registryRoot, regkey, KEY_READ | KEY_WRITE, regkeyhandle, transaction))
error = GetLastError();
if (error == NO_ERROR) {
if (ERROR_SUCCESS != SetRegValueExTString
(regkeyhandle, valueName, filepath.c_str()))
error = GetLastError();
else
if (ERROR_SUCCESS != RegCloseKey(regkeyhandle))
error = GetLastError();
}
Writing the File
As with the registry access, after we create the file handle, we can use the normal file IO functions for performing file IO.
if (error == NO_ERROR) {
HANDLE fileHandle = CreateFileTransactedSimple(
filepath.c_str(), GENERIC_READ | GENERIC_WRITE, CREATE_ALWAYS, transaction);
if (fileHandle == INVALID_HANDLE_VALUE)
error = GetLastError();
else
{
if (WriteFileTString(fileHandle, TEXT("Hello transacted world!")))
{
if (!CloseHandle(fileHandle))
error = GetLastError();
}
else
error = GetLastError();
}
}
Optionally Choosing to Roll Back
For our purposes, we give the user the choice between committing or rolling back the changes. We do this to give them the time to manually check the registry or the folder on disk and verify that the changes are not yet active.
In the real world, you would probably not do this, although you could make an overview of the changes that were made, ask the user to review them before activating everything.
if (error == NO_ERROR)
{
char choice;
do {
cout << "Changes have been made.\n";
cout << "Enter C to commit or R to rollback.\n";
cin >> choice;
if(__isascii(choice) && islower(choice))
choice = _toupper(choice);
} while (choice != 'C' && choice != 'R');
if(choice == 'R')
error = ERROR_CANCELLED;
}
else {
cout << "An error was detected during the changes.\n";
}
Running the Application
The source code for this application is included with the article, as is a built version which you can run on your system. I built it against the static runtime libraries so you can run it without needing a specific version of the runtime library installed.
The registry setting is stored under HKEY_CURRENT_USER
which should never be a security problem. The file is stored in C:\temp unless you provide an alternative.
Conclusion
As you can see, Win32 Transactions are an incredibly powerful feature that deserves a lot more attention than it has received so far. I encourage everyone to look at them in more detail, and use them where appropriate. Not only can transactions save you a ton of manual coding, but they will also improve the reliability of your program.
History
- 21st July, 2022: Initial version