Visual CSharp 2005 Recipes (2006) [eng]
.pdf440 C H A P T E R 1 2 ■ U N M A N A G E D C O D E I N T E R O P E R A B I L I T Y
How It Works
To use a C function from an external library, all you need to do is declare it appropriately. The common language runtime (CLR) automatically handles the rest, including loading the DLL into memory when the function is called and marshaling the parameters from .NET data types to C data types. The
.NET service that supports this cross-platform execution is named PInvoke (Platform Invoke), and the process is usually seamless. Occasionally, you will need to do a little more work, such as when you need to support in-memory structures, callbacks, or mutable strings.
PInvoke is often used to access functionality in the Win32 API, particularly Win32 features that are not present in the set of managed classes that make up the .NET Framework. Three core libraries make up the Win32 API:
•Kernel32.dll includes operating system-specific functionality such as process loading, context switching, and file and memory I/O.
•User32.dll includes functionality for manipulating windows, menus, dialog boxes, icons, and so on.
•GDI32.dll includes graphical capabilities for drawing directly on windows, menus, and control surfaces, as well as for printing.
As an example, consider the Win32 API functions used for writing and reading INI files, such as
GetPrivateProfileString and WritePrivateProfileString, in Kernel32.dll. The .NET Framework does not include any classes that wrap this functionality. However, you can import these functions using the attribute DllImportAttribute, like this:
[DllImport("kernel32.DLL", EntryPoint="WritePrivateProfileString")] private static extern bool WritePrivateProfileString(string lpAppName,
string lpKeyName, string lpString, string lpFileName);
The arguments specified in the signature of the WritePrivateProfileString method must match the DLL method, or a runtime error will occur when you attempt to invoke it. Remember that you do not define any method body, because the declaration refers to a method in the DLL. The EntryPoint portion of the attribute DllImportAttribute is optional in this example. You do not need to specify the EntryPoint when the declared function name matches the function name in the external library.
The Code
The following is an example of using some Win32 API functions to get INI file information. It declares the unmanaged functions used and exposes public methods to call them. (Other Win32 API functions for getting INI file information not shown in this example include those that retrieve all the sections in an INI file.) The code first displays the current value of a key in the INI file, modifies it, retrieves the new value, and then writes the default value.
using System;
using System.Runtime.InteropServices; using System.Text;
namespace Apress.VisualCSharpRecipes.Chapter12
{
class Recipe12_01
{
// Declare the unmanaged functions.
[DllImport("kernel32.dll", EntryPoint = "GetPrivateProfileString")] private static extern int GetPrivateProfileString(string lpAppName,
string lpKeyName, string lpDefault, StringBuilder lpReturnedString, int nSize, string lpFileName);
C H A P T E R 1 2 ■ U N M A N A G E D C O D E I N T E R O P E R A B I L I T Y |
441 |
[DllImport("kernel32.dll", EntryPoint = "WritePrivateProfileString")] private static extern bool WritePrivateProfileString(string lpAppName,
string lpKeyName, string lpString, string lpFileName);
static void Main(string[] args)
{
string val;
// Obtain current value.
val = GetIniValue("SampleSection", "Key1", "\\initest.ini"); Console.WriteLine("Value of Key1 in [SampleSection] is: "
+val);
//Write a new value. WriteIniValue("SampleSection", "Key1", "New Value",
"\\initest.ini");
//Obtain the new value.
val = GetIniValue("SampleSection", "Key1", "\\initest.ini"); Console.WriteLine("Value of Key1 in [SampleSection] is now: "
+val);
//Write original value. WriteIniValue("SampleSection", "Key1", "Value1",
"\\initest.ini");
//Wait to continue. Console.WriteLine(Environment.NewLine); Console.WriteLine("Main method complete. Press Enter."); Console.ReadLine();
}
public static string GetIniValue(string section, string key, string filename)
{
int chars = 256;
StringBuilder buffer = new StringBuilder(chars); string sDefault = "";
if (GetPrivateProfileString(section, key, sDefault, buffer, chars, filename) != 0)
{
return buffer.ToString();
}
else
{
return null;
}
}
public static bool WriteIniValue(string section, string key, string value, string filename)
{
return WritePrivateProfileString(section, key, value, filename);
}
}
}
442C H A P T E R 1 2 ■ U N M A N A G E D C O D E I N T E R O P E R A B I L I T Y
■Note The GetPrivateProfileString method is declared with one StringBuilder parameter (lpReturnedString). This is because this string must be mutable; when the call completes, it will contain the returned INI file information. Whenever you need a mutable string, you must substitute StringBuilder in place of the String class. Often, you will need to create the StringBuilder object with a character buffer of a set size, and then pass the size of the buffer to the function as another parameter. You can specify the number of characters in the StringBuilder constructor. See recipe 2-1 for more information about using the StringBuilder class.
Usage
You can test this program quite easily. First, in the application folder, create the inittest.ini file shown here:
[SampleSection]
Key1=Value1
Now, execute Recipe12-01.exe. You will get an output such as this:
Value of Key1 in [SampleSection] is: Value1
Value of Key1 in [SampleSection] is now: New Value
Main method complete. Press Enter.
12-2. Get the Handle for a Control, Window, or File
Problem
You need to call an unmanaged function that requires the handle for a control, a window, or a file.
Solution
Many classes, including all Control-derived classes and the FileStream class, return the handle of the unmanaged Windows object they are wrapping as an IntPtr through a property named Handle. Other classes also provide similar information; for example, the System.Diagnostics.Process class provides a Process.MainWindowHandle property in addition to the Handle property.
How It Works
The .NET Framework does not hide underlying details such as the operating system handles used for controls and windows. Although you usually will not use this information, you can retrieve it if you need to call an unmanaged function that requires it. Many Microsoft Windows API functions, for example, require control or window handles.
The Code
As an example, consider the Windows-based application shown in Figure 12-1. It consists of a single window that always stays on top of all other windows regardless of focus. (This behavior is enforced by setting the Form.TopMost property to true.) The form also includes a timer that periodically calls the unmanaged GetForegroundWindow and GetWindowText WinAPI functions to determine which window is currently active.
C H A P T E R 1 2 ■ U N M A N A G E D C O D E I N T E R O P E R A B I L I T Y |
443 |
Figure 12-1. Retrieving information about the active window
One additional detail in this example is that the code also uses the Form.Handle property to get the handle of the main application form. It then compares with the handle of the active form to test if the current application has focus. The following is the complete code for this form.
using System;
using System.Windows.Forms;
using System.Runtime.InteropServices; using System.Text;
namespace Apress.VisualCSharpRecipes.Chapter12
{
public partial class ActiveWindowInfo : Form
{
public ActiveWindowInfo()
{
InitializeComponent();
}
// Declare external functions. [DllImport("user32.dll")]
private static extern IntPtr GetForegroundWindow();
[DllImport("user32.dll")]
private static extern int GetWindowText(IntPtr hWnd, StringBuilder text, int count);
private void tmrRefresh_Tick(object sender, EventArgs e)
{
int chars = 256;
StringBuilder buff = new StringBuilder(chars);
//Obtain the handle of the active window. IntPtr handle = GetForegroundWindow();
//Update the controls.
if (GetWindowText(handle, buff, chars) > 0)
{
lblCaption.Text = buff.ToString();
444 C H A P T E R 1 2 ■ U N M A N A G E D C O D E I N T E R O P E R A B I L I T Y
lblHandle.Text = handle.ToString(); if (handle == this.Handle)
{
lblCurrent.Text = "True";
}
else
{
lblCurrent.Text = "False";
}
}
}
}
}
■Caution The Windows Forms infrastructure manages window handles for forms and controls transparently. Changing some of their properties can force the CLR to create a new native window behind the scenes, and a new handle gets wrapped with a different handle. For that reason, you should always retrieve the handle before you use it (rather than storing it in a member variable for a long period of time).
12-3. Call an Unmanaged Function That Uses
a Structure
Problem
You need to call an unmanaged function that accepts a structure as a parameter.
Solution
Define the structure in your C# code. Use the attribute System.Runtime.InteropServices. StructLayoutAttribute to configure how the structure fields are laid out in memory. Use the static SizeOf method of the System.Runtime.Interop.Marshal class if you need to determine the size of the unmanaged structure in bytes.
How It Works
In pure C# code, you are not able to directly control how type fields are laid out once the memory is allocated. Instead, the CLR is free to arrange fields to optimize performance, especially in the context of moving memory around during garbage collection. This can cause problems when interacting with legacy C functions that expect structures to be laid out sequentially in memory to follow their definition in include files. Fortunately, the .NET Framework allows you to solve this problem by using the attribute StructLayoutAttribute, which lets you specify how the members of a given class or structure should be arranged in memory.
The Code
As an example, consider the unmanaged GetVersionEx function provided in the Kernel32.dll file. This function accepts a pointer to an OSVERSIONINFO structure and uses it to return information about the current operating system version. To use the OSVERSIONINFO structure in C# code, you must define it with the attribute StructLayoutAttribute, as shown here:
C H A P T E R 1 2 ■ U N M A N A G E D C O D E I N T E R O P E R A B I L I T Y |
445 |
[StructLayout(LayoutKind.Sequential)] public class OSVersionInfo {
public int dwOSVersionInfoSize; public int dwMajorVersion; public int dwMinorVersion; public int dwBuildNumber; public int dwPlatformId;
[MarshalAs(UnmanagedType.ByValTStr, SizeConst=128)] public String szCSDVersion;
}
Notice that this structure also uses the attribute System.Runtime.InteropServices. MarshalAsAttribute, which is required for fixed-length strings. In this example, MarshalAsAttribute specifies the string will be passed by value and will contain a buffer of exactly 128 characters, as specified in the OSVERSIONINFO structure. This example uses sequential layout, which means the data types in the structure are laid out in the order they are listed in the class or structure. When using sequential layout, you can also configure the packing for the structure by specifying a named Pack field in the StructLayoutAttribute constructor. The default is 8, which means the structure will be packed on 8-byte boundaries.
Instead of using sequential layout, you could use LayoutKind.Explicit; in which case, you must define the byte offset of each field using FieldOffsetAttribute. This layout is useful when dealing with an irregularly packed structure or one where you want to omit some of the fields that you do not want to use. Here is an example that defines the OSVersionInfo class with explicit layout:
[StructLayout(LayoutKind.Explicit)] public class OSVersionInfo {
[FieldOffset(0)]public int dwOSVersionInfoSize; [FieldOffset(4)]public int dwMajorVersion; [FieldOffset(8)]public int dwMinorVersion; [FieldOffset(12)]public int dwBuildNumber; [FieldOffset(16)]public int dwPlatformId; [MarshalAs(UnmanagedType.ByValTStr, SizeConst=128)] [FieldOffset(20)]public String szCSDVersion;
}
Now that you’ve defined the structure used by the GetVersionEx function, you can declare the function and then use it. The following console application shows all the code you will need. Notice that InAttribute and OutAttribute are applied to the OSVersionInfo parameter to indicate that marshaling should be performed on this structure when it is passed to the function and when it is returned from the function. In addition, the code uses the Marshal.SizeOf method to calculate the size the marshaled structure will occupy in memory.
using System;
using System.Runtime.InteropServices;
namespace Apress.VisualCSharpRecipes.Chapter12
{
class Recipe12_03
{
// Declare the external function. [DllImport("kernel32.dll")]
public static extern bool GetVersionEx([In, Out] OSVersionInfo osvi);
static void Main(string[] args)
{
446 C H A P T E R 1 2 ■ U N M A N A G E D C O D E I N T E R O P E R A B I L I T Y
OSVersionInfo osvi = new OSVersionInfo(); osvi.dwOSVersionInfoSize = Marshal.SizeOf(osvi);
//Obtain the OS version information. GetVersionEx(osvi);
//Display the version information.
Console.WriteLine("Class size: " + osvi.dwOSVersionInfoSize);
Console.WriteLine("Major Version: " + osvi.dwMajorVersion);
Console.WriteLine("Minor Version: " + osvi.dwMinorVersion);
Console.WriteLine("Build Number: " + osvi.dwBuildNumber);
Console.WriteLine("Platform Id: " + osvi.dwPlatformId);
Console.WriteLine("CSD Version: " + osvi.szCSDVersion);
Console.WriteLine("Platform: " + Environment.OSVersion.Platform);
Console.WriteLine("Version: " + Environment.OSVersion.Version);
// Wait to continue. Console.WriteLine(Environment.NewLine); Console.WriteLine("Main method complete. Press Enter."); Console.ReadLine();
}
}
// Define the structure and specify the layout type as sequential. [StructLayout(LayoutKind.Sequential)]
public class OSVersionInfo
{
public int dwOSVersionInfoSize; public int dwMajorVersion; public int dwMinorVersion; public int dwBuildNumber; public int dwPlatformId;
[MarshalAs(UnmanagedType.ByValTStr, SizeConst = 128)] public String szCSDVersion;
}
}
Usage
If you run this application on a Windows XP system, you will see information such as this:
Class size: 148
Major Version: 5
Minor Version: 1
Build Number: 2600
Platform Id: 2
CSD Version: Service Pack 2
Platform: Win32NT
Version: 5.1.2600.131072
C H A P T E R 1 2 ■ U N M A N A G E D C O D E I N T E R O P E R A B I L I T Y |
447 |
12-4. Call an Unmanaged Function That Uses
a Callback
Problem
You need to call an unmanaged function and allow it to call a method in your code.
Solution
Create a delegate that has the required signature for the callback. Use this delegate when defining and using the unmanaged function.
How It Works
Many of the Win32 API functions use callbacks. For example, if you want to retrieve the name of all the top-level windows that are currently open, you can call the unmanaged EnumWindows function in the User32.dll file. When calling EnumWindows, you need to supply a pointer to a function in your code. The Windows operating system will then call this function repeatedly, once for each top-level window that it finds, and pass the window handle to your code.
The .NET Framework allows you to handle callback scenarios like this without resorting to pointers and unsafe code blocks. Instead, you can define and use a delegate that points to your callback function. When you pass the delegate to the EnumWindows function, for example, the CLR will automatically marshal the delegate to the expected unmanaged function pointer.
The Code
Following is a console application that uses EnumWindows with a callback to display the name of every open window.
using System; using System.Text;
using System.Runtime.InteropServices;
namespace Apress.VisualCSharpRecipes.Chapter12
{
class Recipe12_04
{
// The signature for the callback method.
public delegate bool CallBack(IntPtr hwnd, int lParam);
//The unmanaged function that will trigger the callback
//as it enumerates the open windows. [DllImport("user32.dll")]
public static extern int EnumWindows(CallBack callback, int param);
[DllImport("user32.dll")]
public static extern int GetWindowText(IntPtr hWnd, StringBuilder lpString, int nMaxCount);