ONDotNet.com    
 Published on ONDotNet.com (http://www.ondotnet.com/)
 See this if you're having trouble printing code examples


Using P/Invoke to Access Win32 APIs

by James Park
02/19/2002

While Microsoft has incorporated much functionality into the .NET Framework class libraries, significant additional functionality resides outside of the managed world of .NET. COM interoperability is necessary in order to access native system APIs, such as shell integration, DirectX, Microsoft Office, and the Windows Registry, as well as custom legacy COM objects. COM interoperability in .NET can be a tricky issue for developers, who have to deal with issues such as figuring out the appropriate data types to use and marshalling data between managed and unmanaged code.

.NET provides access to COM components through its P/Invoke facility. P/Invoke allows developers to invoke native unmanaged methods from managed code. In this article, we will walk through an example of COM interoperability from within C#.

A Simple Plan

Most applications need some way of storing and retrieving configuration data. On Windows, this is usually done using the Windows registry. .NET conveniently provides managed classes to manipulate keys in the registry, but does not expose any way to subscribe to receive notification of changes made to registry keys. Key change notifications are necessary for any long-lived processes, such as servers, since it is not ideal to have to restart a server every time a configuration change is made.

Fortunately for the .NET developer, there exists a way to register for key change notifications via the P/Invoke facility.

Listing 1: Simple registry key change notification console application

using System;
using System.Runtime.InteropServices;

class RegistryTester {
    const int INFINITE = -1;
    const long HKEY_LOCAL_MACHINE = 0x80000002L; 
    const long REG_NOTIFY_CHANGE_LAST_SET = 0x00000004L; 

    [DllImport("kernel32.dll", EntryPoint="CreateEvent")]
    static extern IntPtr CreateEvent(IntPtr eventAttributes, 
        bool manualReset, bool initialState, String name);

    [DllImport("advapi32.dll", EntryPoint="RegOpenKey")]
    static extern IntPtr RegOpenKey(IntPtr key, String subKey, 
        out IntPtr resultSubKey);

    [DllImport("advapi32.dll", EntryPoint="RegNotifyChangeKeyValue")]
    static extern long RegNotifyChangeKeyValue(IntPtr key, 
        bool watchSubTree, int notifyFilter, IntPtr regEvent, bool
        async);

    [DllImport("kernel32.dll", EntryPoint="WaitForSingleObject")]
    static extern long WaitForSingleObject(IntPtr handle, int timeOut);

    [DllImport("kernel32.dll", EntryPoint="CloseHandle")]
    static extern IntPtr CloseHandle(IntPtr handle);

    public static void Main(String[] args) {

        IntPtr myKey;
        IntPtr myEvent = CreateEvent((IntPtr)null, false, false, null);

        if (args.Length < 1) {
            Console.WriteLine("Usage: RegistryTester KEY");
            return;
        }

        String key = args[0];

        unchecked {
            RegOpenKey(new IntPtr((int)HKEY_LOCAL_MACHINE),
            key, out myKey);
        }

        RegNotifyChangeKeyValue(myKey, true, 
            (int)REG_NOTIFY_CHANGE_LAST_SET, myEvent, true);

        Console.WriteLine("Waiting on " + key + "...");

        WaitForSingleObject(myEvent, INFINITE);

        Console.WriteLine(key + " has been modified.");
        
        CloseHandle(myEvent);
    }
}

To accomplish registry key notification, we need to call a variety of Win32 registry and synchronization functions.

Related Reading

COM & .NET Component Services
By Juval Löwy

For each Win32 function call, you need to declare a static extern method and apply a DllImport attribute to that method. For this example, we use two parameters of the DllImportattribute to indicate the specific DLL and DLL entry point we need to call.

Here is a complete list and description of the parameters for the DllImport attribute (from MSDN):

The trickiest part of COM interop is determining how to map types between the Win32 call and the static extern method that you declare. A simple rule of thumb to follow is that whenever a Win32 method needs a handle or pointer, you should use an IntPtr. For pointers to strings, you can simply use the .NET String type. For example, in RegOpenKey, the first parameter is an HKEY type, which is a handle to an open registry key. We can map this to a .NET IntPtr type. The second parameter is a LPCTSTR type, which is a pointer to a null-terminated string containing the name of the key to open. This can be mapped to a .NET String type.

Some Win32 functions might need constant values declared in external header files. You can search for these constants in the header files included with Visual C++ or Cygwin. For this example, we located the constant HKEY_LOCAL_MACHINE in winreg.h and REG_NOTIFY_CHANGE_LAST_SET in winnt.h.

Compile and run this program, providing a registry key under the HKEY_LOCAL_MACHINE node that you wish to monitor for changes. An example key might be SOFTWARE\Microsoft. The program creates an event object and opens the specified registry key. It calls RegNotifyChangeKeyValue with both of these objects. This function tells Windows to set the event object to a signalled state when the specified registry key is modified. By calling WaitForSingleObject on the event, the program blocks until the registry key is modified and the event is signalled.

Mo Keys

A real-world application will probably need notification of changes to multiple keys. However, since WaitForSingleObject is a blocking method, one would have to spawn a thread for every key. If you need notification of changes to a large number of keys, this would be an inefficient use of resources. One can instead use the Win32 API call WaitForMultipleObjects.

[DllImport("kernel32.dll", EntryPoint="WaitForMultipleObjects")]
static extern unsafe int WaitForMultipleObjects(int numHandles, 
    IntPtr* handleArrays, bool waitAll, int timeOut);

To request change notification of multiple keys, you must create an event object for each key and wait on them to be signalled. The second parameter to this call is a pointer to such an array of event handles: HANDLE*, which can be mapped to an IntPtr*. Because we are using pointers and therefore dealing with unsafe code, we must mark this method as unsafe. By using unsafe code, you can access memory directly, like in C/C++, but you have to give up some of the nice things about .NET, such as garbage collection and bounds checking. Pointer operations, such as &, *, and ->, are all available within a method marked unsafe. WaitForMultipleObjectsreturns when either any one or all of the specified event objects in handleArrays are in the signalled state or when the time-out interval elapses.

Listing 2: Multiple registry key change notification console application

using System;
using System.Runtime.InteropServices;

class RegistryTester {
    const int INFINITE = -1;
    const int WAIT_OBJECT_0 = 0;
    const long HKEY_LOCAL_MACHINE = 0x80000002L; 
    const long REG_NOTIFY_CHANGE_LAST_SET = 0x00000004L; 

    [DllImport("advapi32.dll", EntryPoint="RegNotifyChangeKeyValue")]
    static extern long RegNotifyChangeKeyValue(IntPtr key, 
        bool watchSubTree, int notifyFilter, IntPtr regEvent, bool   
        async);

    [DllImport("advapi32.dll", EntryPoint="RegOpenKey")]
    static extern IntPtr RegOpenKey(IntPtr key, String subKey, 
        out IntPtr resultSubKey);

    [DllImport("kernel32.dll", EntryPoint="CreateEvent")]
    static extern IntPtr CreateEvent(IntPtr eventAttributes, 
        bool manualReset, bool initialState, String name);

    [DllImport("kernel32.dll", EntryPoint="WaitForMultipleObjects")]
    static extern unsafe int WaitForMultipleObjects(int numHandles, 
        IntPtr* handleArrays, bool waitAll, int timeOut);

    [DllImport("kernel32.dll", EntryPoint="CloseHandle")]
    static extern IntPtr CloseHandle(IntPtr handle);

    public unsafe static void Main(String[] args) {

        int ret = 0;

        if (args.Length < 2) {
            Console.WriteLine("Usage: RegistryTester NUMKEYS KEY1     
                KEY2...");
            return;
        }

        int numKeys = 0;

        try {
            numKeys = int.Parse(args[0]);
        } catch (Exception) {
            Console.WriteLine("Invalid argument for NUMKEYS");
            return;
        }

        if (numKeys != args.Length - 1) {
            Console.WriteLine("Did not provide correct number 
                of key arguments.");
            return;
        }

        String[] keys = new String[numKeys];
        
        for (int i=0; i < numKeys; i++) {
            keys[i] = args[i+1]; // Start at the 3rd command line arg
        }

        IntPtr[] eventHandles = new IntPtr[numKeys];

        for (int i=0; i < numKeys; i++) {
            eventHandles[i] = CreateEvent((IntPtr)null, false, false, 
                null);
                
            IntPtr myKey;

            unchecked {
                RegOpenKey(new IntPtr((int)HKEY_LOCAL_MACHINE),
                    keys[i], out myKey);
            }

            RegNotifyChangeKeyValue(myKey, true,
                (int)REG_NOTIFY_CHANGE_LAST_SET, eventHandles[i], 
                 true);
        }

        Console.WriteLine("Waiting on " + numKeys + " keys.");

        fixed (IntPtr* handlePtr = eventHandles) {
            ret = WaitForMultipleObjects(numKeys, handlePtr, false, 
                INFINITE);
        }

        Console.WriteLine(keys[ret - WAIT_OBJECT_0] + " was changed.");
    }
}

Once an array of event handlers is created, you must pass a pointer to them into the WaitForMultipleObjects call:


fixed (IntPtr* handlePtr = eventHandles) {
    ret = WaitForMultipleObjects(numKeys, handlePtr, false, INFINITE);
}

Because we are passing a pointer to an array into an unmanaged call, we must first pin the array in memory via the fixed keyword. By fixing the array in memory, we are indicating to the .NET CLR garbage collector that this piece of memory should not be touched while within the scope of the fixed keyword. The static void Main method is also marked as unsafe, since it using unmanaged code. To compile this application, you must use the /unsafe compiler flag.

Summary

While COM interopability can seem tricky in the beginning, by using a few simple rules of thumb, you can easily overcome most scenarios involving calls to COM objects.

James Park is a co-founder of O(1) Software, a soon-to-be-launched startup, which is developing .NET-based P2P software and providing consulting services based around .NET technologies.


Return to the .NET DevCenter.

Copyright © 2009 O'Reilly Media, Inc.