2005-02-11
| Rate This Article: | Add This Article To: |
Despite the breadth and depth of the .Net Framework, it cannot do everything. There are times when you need to do something that is not covered by the Framework. In my experience, this situation comes up in two contexts. One is when there is some capability that you know is part of the Windows API but is not yet in the .Net Framework. The other is when you are programming an external device whose drivers are written for the non-.Net programmer. The result is the same: in both cases, your program will need to call unmanaged code. More specifically, it needs to call code that is provided as part of a dynamic link library or DLL (a Windows DLL in the first case, a manufacturer-provided DLL in the second).
Windows and device DLLs are almost always written in C. This means that you, the .Net programmer, face two challenges:
- How do you call functions in an unmanaged code DLL from your managed code .Net program?
- How do you map the C/Windows data types passed to and returned from the DLL functions to the Common Language Runtime (CLR) types used in .Net?
These questions fall under the general area of interoperability (interop for short). In .Net, or more specifically in the CLR, the most important interop tool is called Platform Invoke (or P-Invoke). P-Invoke is a service that permits managed code to call unmanaged functions that are implemented in DLLs. In the .Net documentation this is referred to as consuming unmanaged DLLs. Let's take a look at how this is done.
Identifying the DLL Function
Before your managed code can call a DLL function, it must know the name of the function and where it is located — in other words, in which DLL. You do this with the DllImport keyword. This is in the System.Runtime.InteropServices namespace. For an example, I will use the Windows Beep() function, which has the
following declaration (in C):
BOOL Beep(
DWORD dwFreq,
// Frequency
DWORD dwDuration
//Duration in milliseconds
};
To make this function accessible in your managed code, you would write the DllImport statement like this:
[DllImport("Kernel32.dll")]
static extern Boolean Beep(
UInt32 frequency, UInt32 duration);
Don't worry about the data types for now; I'll get to them soon. But with this DllImport statement in your managed code, you can call the Beep function like any other function:
Beep(1000, 1500);
As is often the case, the return value of Windows API functions is ignored, because it does not provide any useful information. (DllImport has optional parameters that are needed in some situations. You can learn about these in the .Net online documentation.)
Before you can use functions in a DLL, you must know about them. Where can you get this information? For the Windows API, I recommend one of the several reference books that are available. These books provide information about using each API function including its arguments and return type and the DLL it is contained in (The Windows API is contained in a small set of DLLs that are part of the Windows installation). For third party DLLs, the product documentation should contain this information.
As I mentioned, Windows DLLs are part of the Windows installation and are found by the program without any special effort on your part. Nor do you have to include the DLL files in your distribution package. For third party DLLs, it's best to put them in the application folder where they will be found by the DllImport service. In this case, you do have to include the DLLs in any distribution package you may create.
Mapping Data Types
One of the tasks performed by the P-Invoke service is to marshal data across the managed/unmanaged code boundary. The data types used by DLL functions rarely have the same names as the CLR data types, so it is incumbent on the programmer to determine the correct CLR data types to use. The table below lists the commonly use data types in unmanaged C functions and in the Windows API along with the CLR equivalents.
| Unmanaged Windows API types | Unmanaged C language types | CLR type |
| HANDLE | void* | IntPtr |
| BYTE | unsigned char | Byte |
| SHORT | short | Int16 |
| WORD | unsigned short | UInt16 |
| INT, LONG | int, long | Int32 |
| BOOL | int, long | Int32 or Boolean |
| UINT | unsigned int | UInt32 |
| DWORD, ULONG | unsigned long | UInt32 |
| CHAR | char | Char |
| LPSTR, LPCSTR, LPWSTR, LPCWSTR | char*, wchar_t* | String or StringBuilder |
| FLOAT | float | Single |
| DOUBLE | double | Double |
If those asterisks have you looking for a footnote, perhaps you are not familiar with C programming. The asterisk is used to designate a pointer, a type of data storage in which a variable does not contain the data, per se, but rather contains the memory address of the data. You needn't be concerned with the details of pointers as long as you use the appropriate CLR type.
You can see now how I came up with the DllImport declaration for the Beep() function. For the BOOL return value, I used the CLR type Boolean, and for the two DWORD arguments I used the CLR type UInt32. With the correct data types in place, the P-Invoke data marshalling works properly, ensuring that the DLL function is passed the correct argument types and that the return value, if used by the calling program, is correct.
Wrapping the DLL Functions
If you are going to use unmanaged DLL functions in your program, it's a good idea to wrap them in a class. Aside from the advantages of modularity and reusability, providing a class wrapper is advisable because consuming DLL functions can in some cases be prone to errors. Encapsulating the DLL declarations and calls makes your job easier when it comes time to debug
whatever problems come up. The general approach is to declare (using DllImport)
the function in the class and then create a public method that calls the DLL. Exception handling can be performed within the class. You can create a separate class for each DLL function or, perhaps more conveniently, create a single class that wraps a set of perhaps related DLL functions.
Let's look at an example. The class shown here encapsulates a single Windows API function, Beep(). To add other functions, simply add a DllImport statement
and a public method for each one. Add the class to any project that needs to call the API functions.
using System;
using System.Runtime.InteropServices;
namespace WinAPI
{
public class WindowsAPI
{
[DllImport("Kernel32.dll")]
static extern Boolean Beep(UInt32 duration, UInt32 frequency);
public void WinBeep(UInt32 duration, UInt32 frequency)
{
Beep(duration, frequency);
}
}
}
Then in your program, calling the function is a simple matter. You must import the class's namespace:
using WinAPI;
Then instantiate the class:
WindowsAPI wAPI = new WindowsAPI();
...and finally call the appropriate method:
wAPI.WinBeep(1000, 1500);
Managed code provides many advantages. When it's possible to create an entire application in managed code it is almost always the best way to go. The world is still full of unmanaged code, however, and sometimes the .Net programmer will need to make use of it.
|
![]() |
|


