Добавил:
Опубликованный материал нарушает ваши авторские права? Сообщите нам.
Вуз: Предмет: Файл:

Visual CSharp 2005 Recipes (2006) [eng]

.pdf
Скачиваний:
48
Добавлен:
16.08.2013
Размер:
4.04 Mб
Скачать

C H A P T E R 1 2

■ ■ ■

Unmanaged Code Interoperability

The Microsoft .NET Framework is an extremely ambitious platform, combining a managed runtime (the common language runtime, or CLR), a platform for hosting web applications (Microsoft ASP.NET), and an extensive class library for building all types of applications. However, as expansive as the .NET Framework is, it does not duplicate all the features that are available in unmanaged code. Currently, the .NET Framework does not include every function that is available in the Win32 API, and many businesses are using complex proprietary solutions that they have built with COM-based languages such as Microsoft Visual Basic 6 and Visual C++ 6.

Fortunately, Microsoft does not intend for businesses to abandon the code base they have built up when they move to the .NET platform. Instead, the .NET Framework is equipped with interoperability features that allow you to use legacy code from .NET Framework applications and even access .NET assemblies as though they were COM components. The recipes in this chapter describe how to do the following:

Call functions defined in a DLL, get the handles for a control or window, invoke an unmanaged function that uses a structure, invoke unmanaged callback functions, and retrieve unmanaged error information (recipes 12-1 through 12-5)

Use COM components from .NET Framework applications, release COM components, and use optional parameters (recipes 12-6 through 12-8)

Use ActiveX controls from .NET Framework applications (recipe 12-9)

Expose the functionality of a .NET assembly as a COM component (recipe 12-10)

12-1. Call a Function in an Unmanaged DLL

Problem

You need to call a C function in a DLL. This function might be a part of the Win32 API or your own legacy code.

Solution

Declare a method in your C# code that you will use to access the unmanaged function. Declare this method as both extern and static, and apply the attribute System.Runtime.InteropServices. DllImportAttribute to specify the DLL file and the name of the unmanaged function.

439

440 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);

Соседние файлы в предмете Программирование на C++