Article

# DevMSI: An Example C++ MSI/Wix Deferred Custom Action DLL

, 1 Oct 2013
 Rate this:
An example DLL that can be used for Deferred Custom Actions in WiX/MSI or from a standalone app.

## Introduction

The companion CodeProject article EchoInst: An Example Wix Install of the WDK Echo Driver shows how this DLL could be integrated into a WiX project for installing a device driver during MSI installation and removed during MSI un-installation.

The full source code for DevMsi can be found on CodeProject here

## Background

The Windows Installer XML (WiX) is a toolset that builds Windows installation packages from XML source code. A developer can also extend WiX/MSI technology in a variety of ways, including though entry points in C/C++ DLL. These extension DLLs fall under a subset of actions called "Custom Actions" by WiX/MSI.

The MSI architecture requires that some kinds of custom actions are required to be "Deferred". In particular, any actions that need to run outside the user's security context must be deferred to gain the necessary privilege levels. For more details on this subject, read the MSDN article Deferred Execution Custom Actions.

WiX 3.7 adds Wizards to Visual Studio 2012 to make creating C/C++ Custom Action DLLs a trivial task.  However, it is not immediately obvious how to pass or receive parameters to a Deferred Custom Action DLL. The purpose of this article is to provide a working and hopefully useful example, in particular for Windows Driver developers.

## Why Custom Action Type 1 (.DLL)? Why not Type 2 or 18 (.EXE)?

If the developer just wants to execute some kind of custom code during the installation process, then an EXE would be the immediate (and obvious) choice. Two reasons why the developer might prefer to use a C/C++ DLL are as follows:

Logging.  Custom Action DLL's have the ability to add entries to the MSI log file during installation or un-installation. Otherwise, the custom executable will have to log its information to a separate location, adding another item that would need to be gathered to debug an installation failure.

Display. A Custom Action that is a command line utility will show a "DOS box" to the user during installation. This display can be suppressed through some clever WiX coding, or through creating a utility that is a "Windows program" but doesn't display any UI. However, if the developer has to do extra work just to avoid graphic unpleasantness, he may wish to investigate the .DLL approach just to simplify the installation code.

## Property Constraints of Deferred Custom Action DLLs

According to the MSDN article Obtaining Context Information for Deferred Execution Custom Actions, there are very few properties (three) that can be retrieved during a deferred custom action. The only property that is really usable to pass parameters to the action is the CustomActionData property.

This property can be easily retrieved from the DLL, with code similar to the following:

    LPWSTR pszCustomActionData = NULL;
hr = WcaGetProperty(L"CustomActionData", &pszCustomActionData);
ExitOnFailure(hr, "Failed to get Custom Action Data.");
// ...
LExit:
ReleaseStr(pszCustomActionData); 

This mechanism allows a single string to be passed into the DLL. If multiple parameters need to be passed, they will have to be embedded within the string.  Since C/C++ programmers are familiar with the argc/argv mechanism for passing arguments into a function like main(), one reasonable mechanism to parse the parameters passed via CustomActionData is the Windows API function CommandLineToArgvW().

The DevMsi project follows this pattern for each of its entry points in CustomAction.cpp.  It performs necessary initialization, fetches the CustomActionData property, parses this property into an argc/argv array, and then hands the parsed argument list to a function to actually do the work.

## Debugging Deferred Custom Action DLLs

One of the disadvantages of using a Deferred Action Custom Action DLL is that debugging is more difficult since the DLL is only accessed through the MSI install/uninstall process.  If the code in the DLL is non-trivial, there is value in finding another way to debug the code outside of a MSI environment.

Since the mechanism described above extracts the arguments into an argc/argv format, and then calls another function to actually "do the work", it becomes a trivial exercise to add a second entry point into the DLL at the "do the work" function, so that an external test application can call the function for debugging or command line testing.

An example of the "do the work" function might be as follows:

#ifdef _DEVMSI_EXPORTS
#  define DEVMSI_API __declspec(dllexport)
#else
#  define DEVMSI_API __declspec(dllimport)
#endif
//...
HRESULT DEVMSI_API DoCreateDevnode( int argc, LPWSTR* argv ); 

With this type of format, a test application can easily be written to start testing the DLL at the appropriate point, as follows:

int _tmain(int argc, _TCHAR* argv[])
{
int result = -1;
if ( argc < 2 ) {
return result;
}
LPCTSTR opName  = argv[1];
argc -=2;
argv +=2;
if ( !_tcsicmp( opName, TEXT("create") ) ) {
result = SUCCEEDED( DoCreateDevnode( argc, argv ) )
? 0 : -1;
}
return result;
} 

## (Deferred) Custom Action DLL Entry Points

In addition to the standard DllMain entry point, which must do some MSI setup/initialization when processes are attached/detached, there will be additional entry points for the various (Deferred) Custom Actions exposed by the DLL.  DevMsi defines these entry points in CustomAction.def.  Each of them follows a similar pattern to the following in CustomAction.cpp:

UINT __stdcall CreateDevnode(MSIHANDLE hInstall)
{
return CustomActionArgcArgv( hInstall, DoCreateDevnode, "CreateDevnode" );
}  

The CustomActionArgcArgv is a method that does the setup described in "Property Constraints of Deferred Custom Action DLLs" above, relying on the "do the work" function (e.g. DoCreateDevnode) to perform the custom action on the extracted parameter list.  The first parameter (hInstall) is required by the MSI framework, and the last parameter is just a name for logging purposes.

## Logging Messages and Errors

If a DLL is designed for use either within an MSI environment or from a test harness, then logging output must be able to detect which method is in use and route output either to stdout/stderr (in case of a test harness) or to the MSI logging system.  The DevMsi project includes a LogResult() method which does this detection and routing as needed.

With this method, during debugging it is easy to see what error(s) may be generated by the test harness so that the developer can handle the problem.  Even after deployment in the MSI, it is fairly easy to get the log messages from the install, which would include the log output from the deferred custom action.

One example of successful log entries in an MSI log file follows:

MSI (s) (80:D4) [15:49:20:435]: Invoking remote custom action. DLL: C:\Windows\Installer\MSI4E61.tmp, Entrypoint: CreateDevnode
CreateDevnode:  Initialized.
CreateDevnode:  Custom Action Data = '"C:\Program Files\WDK Samples\Drivers\echo.inf" root\Echo'.
CreateDevnode:  hwid = 'root\Echo', class = 'C:\Program Files\WDK Samples\Drivers\echo.inf'.
CreateDevnode:  Converting 'C:\Program Files\WDK Samples\Drivers\echo.inf' from INF Path to Class GUID.
CreateDevnode:  Class GUID {78A1C341-4539-11D3-B88D-00C04FAD5171} will be used.
CreateDevnode:  SetupDiCreateDeviceInfoList() succeeded.
CreateDevnode:  SetupDiCreateDeviceInfo() succeeded.
CreateDevnode:  SetupDiSetDeviceRegistryProperty() succeeded.
CreateDevnode:  SetupDiCallClassInstaller() succeeded.
CreateDevnode:  DoCreateDevnode() Complete. 

In case the user is not aware of an easy way to create a log file from the command line, the following commands may be of help:

msiexec /i myMsi.msi /l*v myInstallLogFile.log
msiexec /x myMsi.msi /l*v myUninstallLogFile.log 

#### DevMsi Build Environment

DevMsi was built in a Visual Studio 2012 installation, with the Windows 7 WDK and Wix 3.7 installed.  It does assume that the WIX environment variable is set to the installed path for Wix 3.7 (or newer).  The project was build and tested for AMD64 platforms.

#### Running DevMsiTest.exe

If the user wants to run DevMsiTest to add or remove a device as an example, the following commands should allow that to happen from the console, after putting DevMsi.dll and DevMsiTest.exe in the same directory:

rem The following command will create a non-PnP device root\foo in the System class:
DevMsi.exe create System root\foo
rem The following command will remove any matching non-PnP device named root\bar:
DevMsi.exe remove root\bar
rem The following command will uninstall a service named zed:
DevMsi.exe remService zed   

Another article will provide a working example on how to use DevMsi within a Wix install environment to install and uninstall the echo device from the Windows WDK.

## Conclusion

Once the parameter-passing mechanism for Wix C/C++ Deferred Custom Action DLLs is understood, the process of extending the MSI to do other necessary tasks becomes much simpler to implement.  It is the author's hope that this article and related code are helpful in learning how to use Custom Actions for their projects.

In particular, since device driver installation of non-PnP drivers does not appear to be well explained via searches on the net, it is the author's hope that this sample may help future device driver installer writers to avoid some of the headaches involved with adding and removing non-PnP device nodes.

## History

Software Developer (Senior) Seagate Technology
United States
Joe is a Sr. Software Engineer for Seagate Technology. He greatly enjoys practical applications of C/C++ and problem solving in general. He and his family live in a wonderful spot of the world named Longmont, CO (USA).

 First Prev Next
 Very useful Sergey Podobry 25-Sep-13 22:37
 My vote of 5 Member 8676845 22-Sep-13 22:55
 Found a little bug in AutoClose.h Member 10206625 2-Sep-13 2:38
 Re: Found a little bug in AutoClose.h Joe Marley 1-Oct-13 11:07
 My vote of 5 Blake Miller 5-Apr-13 7:26
 Last Visit: 31-Dec-99 18:00     Last Update: 10-Jul-14 19:45 Refresh 1