Click here to Skip to main content
15,919,028 members
Articles / Desktop Programming / Win32

Using Win32 Transactions

Rate me:
Please Sign up or sign in to vote.
5.00/5 (20 votes)
21 Jul 2022MIT10 min read 10.5K   306   32   8
This article demonstrates how to use Win32 transactions with file and registry operations.
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++.

Image 1

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:

C++
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.

C++
//
// This function creates a transaction with default settings, which is what
// is appropriate in most cases.
//
HANDLE CreateTransactionSimple(LPWSTR Description){
    return CreateTransaction(
        NULL,               //Using default security.
        NULL,               //Reserved
        0,                  //Create options, only relevant for inheriting handles
        0,                  //Reserved
        0,                  //Reserved
        0,                  //Timeout
        Description);       //User readable 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.

C++
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:

C++
//
// This function acts as a simplified wrapper for creating a 
// transacted file handle to hide an obnoxiously long argument list 
// that has several reserved parameters, and a couple that are
// fine with default values for our use.
//
HANDLE CreateFileTransactedSimple(
    const LPCTSTR& filepath,    //the file we want to create or open
    DWORD desiredAccess,        //the type of requested access
    DWORD createDisposition,    //optional specifier to determine 
                                //if we want to open, or create, or always create, ...
    const HANDLE& transaction)  //the transaction under which this filehandle is covered.
{
    return CreateFileTransacted(
        filepath,
        desiredAccess,
        FILE_SHARE_READ,        //We allow others to open the file for read access.
                                //For newly created files, this is pointless 
                                //because no one will see the file 
                                //until we commit. But for previously created files, 
                                //other clients see the last committed file
                                //while we are still updating it.
        NULL,                   //File security will be default
        createDisposition,
        FILE_ATTRIBUTE_NORMAL,  //the file is a regular file
        NULL,                   //no template file is used
        transaction,
        NULL,                   //No need for a special miniversion of the file
        NULL);                  //reserved
}

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:

C++
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:

C++
//
// Simplified wrapper for creating a registry key under a transaction to hide an
// obnoxiously long argument list that has several reserved parameters, 
// and a couple that are
// fine with default values for our use.
//
LSTATUS RegCreateKeyTransactedSimple(
    HKEY parentKey,             //location where we want to open a new key
    const LPCTSTR& regkey,      //keyname
    REGSAM samDesired,          //requested rights
    HKEY& regkeyhandle,         //resulting registry key of the child.
    const HANDLE& transaction)  //transaction under which the key is opened.
{
    return RegCreateKeyTransacted(
        parentKey,
        regkey,
        0,                      //reserved
        NULL,                   //user class. can be ignored
        REG_OPTION_NON_VOLATILE,//the change is to be permanent
        samDesired,
        NULL,               //security attributes. NULL -> default security inherited
        &regkeyhandle,
        NULL,               //disposition feedback ->> was it created or opened? 
                            //don't care.
        transaction,
        NULL);              //reserved
}

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.

C++
   //Create the transaction covering the actions in this example
    HANDLE transaction = CreateTransactionSimple();
    if (transaction == INVALID_HANDLE_VALUE)
    {
        cout << "Failed to create Win32 Transaction\n";
        return GetLastError();
    }

    // … Do stuff here

    //Commit or rollback the transaction depending on whether the changes were
    //successful and not cancelled by the user
    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.

C++
#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.

C++
//Location in the registry where the path to the newly create file is to be stored
HKEY registryRoot = HKEY_CURRENT_USER;
LPCTSTR regkey = TEXT("Win32Transaction");
LPCTSTR valueName = TEXT("ConfigFile");

DWORD error = NO_ERROR;

//Open or create a registry key which is connected to the transaction
HKEY regkeyhandle = NULL;
if (ERROR_SUCCESS != RegCreateKeyTransactedSimple(
    registryRoot, regkey, KEY_READ | KEY_WRITE, regkeyhandle, transaction))
    error = GetLastError();

//write the path of the file to the registry.
//At this point no error checks have been
//performed so there is still the possibility
//that the 2nd half of this example will
//trigger an error.
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.

C++
//Create a file of which the path is based on the user input.
//No input validation was done so this part can trigger an error
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
    {
        //so far so good, put something in the file and close it.
        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.

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

License

This article, along with any associated source code and files, is licensed under The MIT License


Written By
Software Developer
Belgium Belgium
I am a former professional software developer (now a system admin) with an interest in everything that is about making hardware work. In the course of my work, I have programmed device drivers and services on Windows and linux.

I have written firmware for embedded devices in C and assembly language, and have designed and implemented real-time applications for testing of satellite payload equipment.

Generally, finding out how to interface hardware with software is my hobby and job.

Comments and Discussions

 
QuestionTransactions Pin
alan@1st-straw.com22-Aug-22 7:46
alan@1st-straw.com22-Aug-22 7:46 
AnswerRe: Transactions Pin
Bruno van Dooren22-Aug-22 12:12
mvaBruno van Dooren22-Aug-22 12:12 
QuestionI'd have to test the ever loving elephant out of this..... Pin
charlieg25-Jul-22 8:58
charlieg25-Jul-22 8:58 
AnswerRe: I'd have to test the ever loving elephant out of this..... Pin
Bruno van Dooren25-Jul-22 11:26
mvaBruno van Dooren25-Jul-22 11:26 
GeneralRe: I'd have to test the ever loving elephant out of this..... Pin
charlieg27-Jul-22 11:13
charlieg27-Jul-22 11:13 
GeneralRe: I'd have to test the ever loving elephant out of this..... Pin
Bruno van Dooren27-Jul-22 19:34
mvaBruno van Dooren27-Jul-22 19:34 
GeneralMy vote of 5 Pin
johnbergman222-Jul-22 9:02
johnbergman222-Jul-22 9:02 
Great information! Thanks for sharing.
GeneralMy vote of 5 Pin
Andrea Simonassi21-Jul-22 22:27
Andrea Simonassi21-Jul-22 22:27 

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Praise Praise    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.