Using PInvoke to Call Functions in the Win32 API


Although it’s possible to do a great deal using the functionality provided in the .NET Framework, at times you’ll need to use code that wasn’t written for .NET, for example:

  • You need to call a Microsoft Windows API function that doesn’t have a .NET equivalent.

  • You have some code in a DLL that originated outside .NET and can’t be rewritten.

  • You have code that needs to be written in a language that’s not yet supported by the .NET Framework.

Whatever the reason, the code you’re calling exists outside the .NET-managed environment, so you need a way to pass function calls into and out of .NET. The mechanism is called PInvoke (for Platform Invoke, pronounced “p-invoke” rather than “pin-voke”), and is provided to let you call functions in DLLs.

Using PInvoke involves adding a prototype to your code that uses attributes to tell .NET about the function you’re proposing to call. In particular, you need to tell it the name of the DLL containing the function, the name of the function, what arguments the function takes, and what the function returns.

A mechanism such as PInvoke is necessary to enable communication between managed and unmanaged code. Take strings as an example: a string in managed C++ is a pointer to a String object, but in unmanaged C++, a string isn’t represented by an object. Instead, a string is a pointer to a series of memory locations that contain characters and is terminated by a null. If you’re going to pass string data between managed and unmanaged code, something has to convert between the corresponding managed and unmanaged data types. This conversion process is called marshaling, and it is one of the tasks that PInvoke performs for you.

start sidebar
Identifying Functions

There are two points that you need to be aware of when identifying functions to call using PInvoke. Although you usually identify a function in a DLL by name, you can also assign a function in a DLL a number that can be used to execute the function at run time. If you need to, you can identify a DLL function to PInvoke using this ordinal number.

When you call Windows API functions, you can also have two or more versions of functions that take characters or strings as arguments because Windows can support more than one character encoding. For example, standard Microsoft Windows 2000 supports both the ASCII (one byte per character) and Unicode (two bytes per character) character encodings. This means that both ASCII and Unicode versions of each function must exist, identified by an “A” or a “W", respectively, added to the end of the function name (for example, MessageBoxW). Although you can call the different versions directly, the C++ compiler will map a call to MessageBox onto the correct function depending on whether you’re using ASCII or Unicode in your program.

As you’ll discover in the exercise later in this section, you can specify which version of a function you want to use with PInvoke. If you don’t explicitly pick one, the ASCII version will be chosen.

end sidebar

The following exercise shows you how to call an unmanaged function in one of the Windows system DLLs. The obvious candidate for this exercise is MessageBox for two reasons: first, it’s a stand-alone function and doesn’t require any setting up, and second, it’s obvious whether the call has worked or not!

The MessageBox function—that is, the MessageBoxA and MessageBoxW functions—live in the User32.dll system DLL. Three system DLLs contain the unmanaged Windows API code:

  • User32.dll, which contains functions for message handling, timers, menus, and communications

  • Kernel32.dll, which contains low-level operating system functionality for memory management and resource handling

  • GDI32.dll, which contains the GDI graphics subsystem code

How do you know which DLL holds a particular system function? If you look the function up in the Platform SDK, you’ll usually find a clue in the “Requirements” section at the end of the topic. For example, the Help topic for MessageBox has the following line:

Library: Use User32.lib

This line tells you that if you want to use MessageBox in traditional C++ code, you’ll have to link with a library named User32.lib, and the name User32 tells you that the code actually lives in User32.dll.

Now that we know where the MessageBox function can be found, here’s the exercise:

  1. Start a new Visual C++ Console Application (.NET) project named Message.

  2. Add a using namespace statement to the top of the project:

    using namespace System::Runtime::InteropServices;

    The attribute class that you’ll be using is part of the System::Runtime::InteropServices namespace, and it’s much easier to use if you declare the namespace.

  3. Add the prototype for the MessageBox function before the _tmain routine:

    // Declare the HWND typedef typedef void* HWND; // Set up the import [DllImportAttribute("User32.dll", CharSet=CharSet::Auto)] extern "C" int MessageBox(HWND hw, String* text, String* caption, unsigned int type); 

    HWND is one of the traditional Windows data types and is simply a typedef for a void pointer. The prototype for the MessageBox function is declared using the DllImportAttribute attribute class. The two parameters passed to the attribute are the name of the DLL housing the function, and (because this is a function that uses characters or strings) an indication of which version to use. CharSet::Auto leaves it up to the target platform to decide which version to call and how to convert the string arguments.

    The actual Windows API function is a C function rather than a C++ function, so extern “C” is used to ensure that the compiler generates the correct calling sequence. Note how String pointers are used to pass string information, whereas the original function would require a Windows LPTSTR type. The PInvoke marshaling automatically converts the data when making the call.

  4. Add code to the _tmain function to call MessageBox:

    int _tmain() { Console::WriteLine(S"PInvoke Example"); String* theText = S"Hello World!"; String* theCaption = S"A Message Box..."; MessageBox(0, theText, theCaption, 0); return 0; }

    When you build and run the code, you’ll see a MessageBox displayed on the screen:

The DllImportAttribute Class

You used the DllImportAttribute class in the previous exercise to provide a prototype for an unmanaged function. This class has a number of fields (data members) that can be used when constructing the prototype, and they’re listed in the following table:

Field

Description

CallingConvention

Defines the calling convention used when passing arguments to the unmanaged function.

CharSet

Defines how characters and strings are to be handled during marshaling.

EntryPoint

Indicates the name or ordinal number of the DLL function to be called.

ExactSpelling

Indicates whether the name of the entry point should be modified to correspond to the character set in use.

PreserveSig

Used for COM methods, this field should be set to true if the return values from methods shouldn’t be altered in any way.

SetLastError

If true, the caller can use the Win32 GetLastError function to determine whether an error occurred.

Let’s look at the more common fields in detail. CallingConvention defines how arguments are passed between the managed and unmanaged code, and will take one of the values in the CallingConvention enumeration. Different languages use different ways of passing arguments, so Windows supports a number of different calling conventions. C and C++ normally use the C calling convention, often known as Cdecl, whereas many other Windows languages use the standard calling convention, often abbreviated to StdCall. You call Windows API functions using StdCall, which is the default unless you use the CallingConvention field to choose another.

CharSet lets you specify how characters and strings are to be marshaled, and takes one of the values from the CharSet enumeration. You can specify CharSet::Ansi, in which case all characters and strings are converted to one-byte ANSI characters and an “A” is appended to the name of the DLL entry point. Choosing CharSet::Unicode converts characters and strings to use two-byte Unicode characters and appends a “W” to the entry point name. However, it’s usually sufficient to specify CharSet::Auto, which chooses the best option for the host system.

The EntryPoint field lets you specify the name or ordinal number of the entry point in the DLL. If you don’t specify this field, as in the preceding exercise, the entry point name is taken to be the function name given in the prototype. A name given using the EntryPoint field takes precedence over the prototype name, so this gives you the ability to provide synonyms for unmanaged functions if you want to refer to them by another name when calling them in your code. The following code fragment shows how you could define a synonym for the MessageBox function:

[DllImportAttribute("User32.dll", EntryPoint="MessageBox", CharSet=CharSet::Auto)] extern "C" int WindowsMessageBox(HWND hw, String* text, String* caption, unsigned int type);

You call the function as WindowsMessageBox, and the call is mapped onto the MessageBox entry point in User32.dll.

Passing Structures

You’ll often need to pass structured data to arguments to unmanaged functions, and you must do this carefully. In particular, you need to specify the way structures are laid out in memory to be sure that they are passed around correctly. You specify the layout of structures and classes using the StructLayoutAttribute and FieldOffsetAttribute classes.

You add StructLayoutAttribute to managed types to define a formatted type with a particular layout. There are three possible layout types that you can specify for a formatted type:

  • Automatic layout (LayoutKind::Auto), in which the run time might reorder the members if it is more efficient. You never use automatic layout for types that are going to be used with PInvoke because you need to be sure that everything stays in the same order.

  • Explicit layout (LayoutKind::Explicit), in which members are ordered according to byte offsets specified by FieldOffset attributes on each field.

  • Sequential layout (LayoutKind::Sequential), in which members appear in unmanaged memory in the same order they appear in the managed definition.

The following exercise shows how to call an unmanaged Windows API function that needs to be passed a structure. The function is GetSystemPowerStatus, which reports on the AC and battery status of the system. The Windows API defines a structure SYSTEM_POWER_STATUS, which contains the status information. The definition of this unmanaged structure is shown here:

typedef struct _SYSTEM_POWER_STATUS { BYTE ACLineStatus; BYTE BatteryFlag; BYTE BatteryLifePercent; BYTE Reserved1; DWORD BatteryLifeTime; DWORD BatteryFullLifeTime; } SYSTEM_POWER_STATUS, *LPSYSTEM_POWER_STATUS;

The prototype for the GetSystemPowerStatus function in the API documentation is this:

BOOL GetSystemPowerStatus( LPSYSTEM_POWER_STATUS lpSystemPowerStatus // status );

The function takes a pointer to a SYSTEM_POWER_STATUS structure, fills it in, and hands back the filled structure, returning a Boolean value to let you know whether it worked. Your task is to call this function, passing over a structure, and display the results.

  1. Create a new Visual C++ Console Application (.NET) project named PowerMonitor.

  2. Add the following line after using namespace System:

    using namespace System::Runtime::InteropServices;

    This will make it easier to refer to the attributes we’ll be using later.

  3. Define a managed equivalent for the structure:

    [StructLayoutAttribute(LayoutKind::Sequential)] __gc class PStat { public: System::Byte ACLineStatus; System::Byte BatteryFlag; System::Byte BatteryLifePercent; System::Byte Reserved1; System::UInt32 BatteryLifeTime; System::UInt32 BatteryFullLifeTime; };

    Our equivalent of SYSTEM_POWER_STATUS is a managed class named PStat. The original definition contains two Windows data types: BYTE, which represents a one-byte integer, and so can be represented by the System::Byte type; and DWORD, which is a 32-bit unsigned integer, and so is represented by System::UInt32. The StructLayoutAttribute is attached to the class, and LayoutKind::Sequential is specified so that the layout of the members will remain the same as the data is passed through PInvoke.

  4. Define the prototype for the GetSystemPowerStatus function:

    // Define the BOOL type typedef int BOOL; // Prototype for the function [DllImportAttribute("Kernel32.dll", CharSet=CharSet::Auto)] BOOL GetSystemPowerStatus(PStat* ps);

    BOOL is a Windows type representing a Boolean value and is actually a typedef for an integer. It has been widely used in the Windows API because C lacks a true Boolean type. The prototype uses the real name of the function as it occurs in Kernel32.dll, and the single argument is given as a pointer to our managed type.

  5. Write the code to call the function. Edit the _tmain function to create a PStat object and use it to call the function:

    int _tmain() { Console::WriteLine(S"Power Status Test..."); PStat* ps = new PStat(); BOOL b = GetSystemPowerStatus(ps); Console::WriteLine(S"Got status, return was {0}", __box(b)); return 0; }

    If the call worked, the return value should be nonzero, which represents a Boolean true value.

  6. Add code to report on the members of the class:

    // Report on the AC line status Console::Write(S"AC line power status is "); switch(ps->ACLineStatus) { case 0: Console::WriteLine(S"‘off’"); break; case 1: Console::WriteLine(S"‘on’"); break; case 255: Console::WriteLine(S"‘unknown’"); break; } // Report on the battery status Console::Write(S"Battery charge status is ({0})", __box(ps->BatteryFlag)); if (ps->BatteryFlag & 1) Console::Write(S" ’high’"); if (ps->BatteryFlag & 2) Console::Write(S" ’low’"); if (ps->BatteryFlag & 4) Console::Write(S" ’critical’"); if (ps->BatteryFlag & 8) Console::Write(S" ’charging’"); if (ps->BatteryFlag & 128) Console::Write(S" ’no system battery’"); Console::WriteLine(); // What’s the percentage charge left in the battery? Console::WriteLine(S"Battery life is {0}%", __box(ps->BatteryLifePercent)); // How many seconds battery life is left? if (ps->BatteryLifeTime == -1) Console::WriteLine(S"Battery life in seconds: Unknown"); else Console::WriteLine(S"Battery seconds remaining: {0} secs", __box(ps->BatteryLifeTime)); 

    The first check is on the ACLineStatus field, which will have the value 0 (on), 1 (off), or 255 (unknown). The second check is on the status of the battery, and this value can be made up of one or more of the values 1 (high charge), 2 (low charge), 4 (critically low charge), 8 (charging), and 128 (no battery present). Each of these represents a particular bit position within the result, and the bitwise OR operator ( & ) is used to check which bits are set.

    The final two checks print out the percentage of lifetime left in the battery and the number of seconds. If the function can’t determine the number of seconds, it will return -1 in this field.

  7. Build and run the program. This illustration shows the output I got when I ran the program on my laptop:

    click to expand




Microsoft Visual C++  .NET(c) Step by Step
Microsoft Visual C++ .NET(c) Step by Step
ISBN: 735615675
EAN: N/A
Year: 2003
Pages: 208

flylib.com © 2008-2017.
If you may any questions please contact us: flylib@qtcs.net