Whether a developer is trying to call a library of his existing unmanaged code, accessing unmanaged code in the operating system not exposed in any managed API, or trying to achieve maximum performance for a particular algorithm that performs faster by avoiding the runtime overhead of type checking and garbage collection, at some point she must call into unmanaged code. The CLI provides this capability through P/Invoke. With P/Invoke, you can make API calls into exported functions of unmanaged DLLs. All of the APIs invoked in this section are Windows APIs. Although the same APIs are not available on other platforms, developers can still use P/Invoke for APIs native to their platform, or for calls into their own DLLs. The guidelines and syntax are the same. Declaring External FunctionsOnce the target function is identified, the next step of P/Invoke is to declare the function with managed code. Just like all regular methods that belong to a class, you need to declare the targeted API within the context of a class, but by using the extern modifier. Listing 17.1 demonstrates how to do this. Listing 17.1. Declaring an External Method
In this case, the class is VirtualMemoryManager, because it will contain functions associated with managing memory. (This particular function is available directly off the System.Diagnostics.Processor class, so there is no need to declare it in real code.) extern methods are always static and don't include any implementation. Instead, the DllImport attribute, which accompanies the method declaration, points to the implementation. At a minimum, the attribute needs the name of the DLL that defines the function. The runtime determines the function name from the method name. However, it is possible to override this default using the EnTRyPoint named parameter to provide the function name. (The .NET platform will automatically attempt calls to the Unicode [...W] or ASCII [...A] API version.) It this case, the external function, GetCurrentProcess(), retrieves a pseudohandle for the current process which you will use in the call for virtual memory allocation. Here's the unmanaged declaration: HANDLE GetCurrentProcess(); Parameter Data TypesAssuming the developer has identified the targeted DLL and exported function, the most difficult step is identifying or creating the managed data types that correspond to the unmanaged types in the external function. [1]Listing 17.2 shows a more difficult API.
Listing 17.2. The VirtualAllocEx() API
VirtualAllocEx() allocates virtual memory that the operating system specifically designates for execution or data. To call it, you also need corresponding definitions in managed code for each data type; although common in Win32 programming, HANDLE, LPVOID, SIZE_T, and DWORD are undefined in the CLI managed code. The declaration in C# for VirtualAllocEx(), therefore, is shown in Listing 17.3. Listing 17.3. Declaring the VirtualAllocEx() API in C#
One distinct characteristic of managed code is the fact that primitive data types such as int do not change size based on the processor. Whether 16, 32, or 64 bits, int is always 32 bits. In unmanaged code, however, memory pointers will vary depending on the processor. Therefore, instead of mapping types such as HANDLE and LPVOID simply to ints, you need to map to System.IntPtr, whose size will vary depending on the processor memory layout. This example also uses an AllocationType enum, which I discuss in the section Simplifying API Calls with Wrappers, later in this chapter. Using ref Rather Than PointersFrequently, unmanaged code uses pointers for pass-by-reference parameters. In these cases, P/Invoke doesn't require that you map the data type to a pointer in managed code. Instead, you map the corresponding parameters to ref (or out), depending on whether the parameter is in-out or just out. In Listing 17.4, lpflOldProtect, whose data type is PDWORD, is an example that returns the "pointer to a variable that receives the previous access protection of the first page in the specified region of pages." Listing 17.4. Using ref and out Rather Than Pointers
In spite of the fact that lpflOldProtect is documented as [out], the description goes on to mention that the parameter must point to a valid variable and not NULL. The inconsistency is confusing, but common. The guideline is to use ref rather than out for P/Invoke type parameters since the callee can always ignore the data passed with ref, but the converse will not necessarily succeed. The other parameters are virtually the same as VirtualAllocEx(), except that the lpAddress is the address returned from VirtualAllocEx(). In addition, flNewProtect specifies the exact type of memory protection: page execute, page read-only, and so on. Using StructLayoutAttribute for Sequential LayoutSome APIs involve types that have no corresponding managed type. To call these requires redeclaration of the type in managed code. You declare the unmanaged COLOREF struct, for example, in managed code (see Listing 17.5). Listing 17.5. Declaring Types from Unmanaged Structs
Various Microsoft Windows color APIs use COLORREF to represent RGB colors (levels of red, green, and blue). The key in this declaration is StructLayoutAttribute. By default, managed code can optimize the memory layouts of types, so layouts may not be sequential from one field to the next. To force sequential layouts so that a type maps directly and can be copied bit for bit (blitted) from managed to unmanaged code and vice versa, you add the StructLayoutAttribute with the LayoutKind.Sequential enum value. (This is also useful when writing data to and from filestreams where a sequential layout may be expected.) Since the unmanaged (C++) definition for struct does not map to the C# definition, there is not a direct mapping of unmanaged struct to managed struct. Instead, developers should follow the usual C# guidelines about whether the type should behave like a value or a reference type, and whether the size is small (approximately less than 16 bytes). Error HandlingOne inconvenient characteristic of Win32 API programming is the fact that it frequently reports errors in inconsistent ways. For example, some APIs return a value (0, 1, false, and so on) to indicate an error, and others set an out parameter in some way. Furthermore, the details of what went wrong require additional calls to the GetLastError() API and then an additional call to FormatMessage() to retrieve an error message corresponding to the error. In summary, Win32 error reporting in unmanaged code seldom occurs via exceptions. Fortunately, the P/Invoke designers provided a mechanism for handling this. To enable this, given the SetLastError named parameter of the DllImport attribute is true, it is possible to instantiate a System .ComponentModel.Win32Exception() that is automatically initialized with the Win32 error data immediately following the P/Invoke call (see Listing 17.6). Listing 17.6. Win32 Error Handling
This enables developers to provide the custom error checking that each API uses while still reporting the error in a standard manner. Listing 17.1 and Listing 17.3 declared the P/Invoke methods as internal or private. Except for the simplest of APIs, wrapping methods in public wrappers that reduce the complexity of the P/Invoke API calls is a good guideline that increases API usability and moves toward object-oriented type structure. The AllocExecutionBlock() declaration in Listing 17.6 provides a good example of this. Using SafeHandleFrequently, P/Invoke involves a resource, such as a window handle, that code needs to clean up after using it. Instead of requiring developers to remember this and manually code it each time, it is helpful to provide a class that implements IDisposable and a finalizer. In Listing 17.7, for example, the address returned after VirtualAllocEx() and VirtualProtectEx() requires a follow-up call to VirtualFreeEx(). To provide built-in support for this, you define a VirtualMemoryPtr class that derives from System.Runtime.InteropServices.SafeHandle (this is new in .NET 2.0). Listing 17.7. Managed Resources Using SafeHandle
System.Runtime.InteropServices.SafeHandle includes the abstract members IsInvalid and ReleaseHandle(). In the latter, you place your cleanup code; the former indicates whether the cleanup code has executed yet. With VirtualMemoryPtr, you can allocate memory simply by instantiating the type and specifying the needed memory allocation.
Calling External FunctionsOnce you declare the P/Invoke functions, you invoke them just as you would any other class member. The key, however, is that the imported DLL must be in the path, including the executable directory, so that it can be successfully loaded. Listing 17.6 and Listing 17.7 provide demonstrations of this. However, they rely on some constants. Since flAllocationType and flProtect are flags, it is a good practice to provide constants or enums for each. Instead of expecting the caller to define these, encapsulation suggests you provide them as part of the API declaration, as shown in Listing 17.9. Listing 17.9. Encapsulating the APIs Together
The advantage of enums is that they group together each value. Furthermore, they limit the scope to nothing else besides these values. Simplifying API Calls with WrappersWhether it is error handling, structs, or constant values, one goal of good API developers is to provide a simplified managed API that wraps the underlying Win32 API. For example, Listing 17.10 overloads VirtualFreeEx() with public versions that simplify the call. Listing 17.10. Wrapping the Underlying API
Function Pointers Map to DelegatesOne last P/Invoke key is that function pointers in unmanaged code map to delegates in managed code. To set up a Microsoft Windows timer, for example, you would provide a function pointer that the timer could call back on, once it had expired. Specifically, you would pass a delegate instance that matched the signature of the callback. GuidelinesGiven the idiosyncrasies of P/Invoke, there are several guidelines to aid in the process of writing such code.
The last bullet implies C#'s support for pointers, described in the next section. |