Call Unmanaged DLLs from C#, Killing Processes Cleanly | ||
|
||
Download the code for this article: CQA0208.exe (47 KB) |
||
Q How do I call a DLL or Win32® API function from my C# code if the function has a string (char*) output parameter? I can pass a string for an input parameter, but how do I get the returned string? Also, how do I call functions that require a struct or a callback, like GetWindowsRect and EnumWindows? I'm trying to convert from C++ and MFC, and I can't cope!
E. Nguyen
A One of the major benefits of Microsoft® .NET is that it provides a language-independent development system. You can write classes in Visual Basic®, C++, C#—whatever—and use them in other languages; you can even derive from classes in a different language. But what happens when you want to call some old-school unmanaged DLL? You have to somehow translate .NET objects into the structs, char*'s, and function pointers C expects. In techno lingo, your parameters must be marshaled. Marshaling is a big topic, but luckily you don't have to know much to get the job done. To call a DLL function from C#, first you must provide a declaration, something programmers using Visual Basic have been doing for years. In C#, it's DllImport: In C#, you use DllImport to tell the compiler where the entry point lives and bundle your wrapper functions inside a class. You can give this class any name you like; I chose Win32. You can even put the class inside a namespace, like I did in Figure 1. To compile Win32API.cs, type: Now you have a Win32API.dll you can use in any C# project. The compiler knows to find SetWindowText in user32.dll and automatically converts your string to LPTSTR (TCHAR*) before calling. Amazing! How does .NET perform this magic? Every C# type has a default marshaling type. For strings, it's LPTSTR. But what happens if you try this for GetWindowText, where the string is an out parameter, not an in parameter? It doesn't work because strings are immutable. Really. You may not have noticed, but whenever you do something to a string, a new string is created. To modify the one and only actual string, you have to use StringBuilder: The default marshaling type for StringBuilder is also LPTSTR, but now GetWindowText can modify your actual string. So the short answer to your first question is, use StringBuilder.All that seems super, but what happens if the default marshaling type isn't what you want? For example, suppose you want to call GetClassName? Windows® gurus know that unlike most API functions, GetClassName takes LPSTR (char*), even in Unicode builds. If you pass a string, the common language runtime (CLR) will convert it to TCHARs—oops! Never fear, you can use MarshalAs to override the default: Now when you call GetClassName, .NET passes your string as an ANSI char, not a wide one. Whew!So much for strings. What about structs and callbacks? As you might guess, .NET has a way to handle them, too. Take a simple sample, GetWindowRect. This function stuffs a RECT with a window's screen coordinates: How do you call GetWindowRect from C#? How do you pass the RECT? You do it as a C# struct, using another attribute, StructLayout. Once you have your struct defined, you can implement the wrapper like so: It's important to use ref so the CLR will pass your RECT as a reference so the function can modify your object, not a nameless stack copy. With GetWindowRect defined, you can call it like so: Note that you must use the ref in the call as well as the declaration—picky! The default marshaling type for C# structs is—what else?—LPStruct, so there's no need for MarshalAs. But if you made RECT a class instead of a struct, you'd have to implement the wrapper like this: In C#, just like C++, there are lots of ways to do things. But System.Drawing already has a Rectangle struct for handling rectangles, so why reinvent the wheel? The runtime already knows how to marshal a Rectangle as a Win32 RECT. Please note, however, that there's no need to call GetWindowRect (or Get/SetWindowText for that matter) in real code, since the Windows.Forms Control class has properties for this: Control.DisplayRectangle to get the window rectangle, and Control.Text to get or set the text. You only need the API wrappers I've shown if for some reason you only have an HWND, not a Control-derived object.Strings, structs, Rectangles... what else? Oh yes, callbacks. How do you pass a callback from C# to unmanaged code? All you have to do is remember to say "delegate." Once you've declared your delegate/callback type, the wrapper goes like this: Since the delegate line merely declares a delegate type, you have to provide an actual delegate in your class then pass it to the wrapper like so: Astute readers will notice I glossed over the problem of lparam. In C, if you give EnumWindows an LPARAM, Windows will notify your callback with it. Typically lparam is a pointer to some struct or class that contains context info that you need to do whatever it is you're doing. But remember, you can never say "pointer" in .NET! So what to do? In this case, you can declare your lparam as IntPtr and use a GCHandle to wrap it: Don't forget to call Free when you're finished! Sometimes in C# you actually get to free your own memory, just like in the old days. To access the lparam "pointer" inside your enumerator, use GCHandle.Target. Figure 2 shows a class I wrote that encapsulates EnumWindows in an array. Instead of fussing with delegates and callbacks, you can write the following: Pretty neat! Believe it or not, there's a method to all this madness. Everything makes sense once you get the hang of it. You can even use DllImport-style wrappers in managed C++. With .NET, you can move freely between managed and unmanaged worlds, as long as you do the proper translation. Most of the time you don't have to do anything; it just works. Occasionally, you need MarshalAs or a wrapper like GCHandle. For more on interop, read "Platform Invoke Tutorial" in the .NET documentation.Figure 3 shows a little program I wrote. ListWin lists the top-level windows with switches so you can display HWNDs, class names, titles, and/or window rectangles, using RECT or Rectangle. Figure 4 shows a sample run. I stripped some of the code to save space here; as always, you can grab the full source from the link at the top of this article. |