The PInvoke Service

I l @ ve RuBoard

PInvoke allows you to call native, or unmanaged, functions that are implemented in dynamic-link libraries (DLLs). To call a native method, you must first define a prototype in your application, providing the necessary information so the CLR can resolve the function reference. PInvoke can do a lot of the work for you, including marshaling most types and capturing Win32 errors. You can use PInvoke to call Win32 API functions (not a huge surprise) ”this is, after all, what the .NET Framework classes do extensively. The other common use of PInvoke is for registering managed callback functions.

Warning

Security issues can arise when you call native methods . The CLR and the .NET Framework largely prevent the infamous buffer overrun security vulnerabilities, but you can easily introduce such problems with improper use of native methods. Also, your application must run in a trusted security environment ” otherwise , it will fail.


Calling Native Methods

Visual Basic .NET provides two ways to access native platform methods: the Declare statement and the DllImport attribute. Most of the samples in this chapter will use the Declare statement, but it's important to understand when it might be preferable to use the DllImport attribute. To illustrate both of these options, we'll use the GetUserName function from the Advapi32 library as an example. This function retrieves the username for the user account the application is running under. The following function definition is from the MSDN documentation that accompanies Visual Studio .NET:

 BOOLGetUserName(LPTSTRlpBuffer,//namebuffer LPDWORDnSize//sizeofnamebuffer); 

Note

For now, we'll ignore marshaling issues and focus on the syntax of Declare and DllImport . The code for this example can be found in the GetUserName sample that accompanies this chapter.


Now that we have a function to import, let's do just that ”starting with the Declare statement.

The Declare Statement

The Declare statement is the recommended mechanism for importing native methods in Visual Basic .NET. The syntax should be familiar to seasoned Visual Basic developers. For others, the syntax will be mostly easy to understand. The following is the general form of the Declare statement syntax:

 [PublicPrivateProtectedFriendProtectedFriend][Shadows]_ Declare[AnsiUnicodeAuto][SubFunction]nameLib "libname" _ [Alias "aliasname" ][([arglist])][Astype] 

The information you must provide to the Declare statement is fairly straightforward: the function name, the library the function comes from, any arguments, and, if necessary, a return type. If the function has a VOID return type, you can declare the method as a Sub . A method with a specific return type should be declared as a Function . The Declare statement can be used in two contexts: in a module or a class.

Note

You cannot explicitly define a Declare method as Shared because all methods that are defined using Declare are implicitly Shared ” they can never be class instance member functions.


You can also provide optional information. The access-level modifiers ( Public , Private , and so forth) should be familiar to you. There are also the string encoding options ( Ansi , Unicode , and Auto ) and the Alias option. Before I discuss these, let's look at how to define the GetUserName function:

 DeclareUnicodeFunctionGetUserNameWLib "advapi32.dll" _ (ByValbufferAsStringBuilder,_ ByRefsizeAsInteger)AsBoolean 

This is what the Declare statement for the aforementioned GetUserName function ends up looking like. There's a bit of parameter wizardry going on here. We're using the Unicode version of GetUserName and passing a StringBuilder object ”but you can ignore these details for now. The example does, however, illustrate how the Declare statement works.

I defined the GetUserNameW method as a Function because the original function returns a nonvoid: Boolean . Otherwise, I would have declared the method as a Sub . Because I did not use the Alias option, in this example the function name must match the name of the native function. (More on this later.) The only exception to maintaining exact naming is with functions that support platform-specific strings. Most functions in the Win32 API that support strings have two versions: ANSI and Unicode. Typically, if a function definition supports both ANSI and Unicode, the specific native function name will be appended with an A (ANSI) or a W (Unicode). This segues nicely into the next part of our discussion of the Declare syntax: the string encoding options.

Note

The MSDN Platform SDK documentation does not typically list the actual implementation names for specific functions. (See the GetUserName function.) Instead, a note in the requirements section indicates that a function is implemented as Unicode and/or ANSI versions.


String-encoding options

Three types of API functions are common throughout the Win32 library: ANSI, Unicode, and platform-dependent. When you need to deal with strings, the difference between an ANSI function and its Unicode equivalent (if one exists) is extremely important. Granted, many functions don't take strings or characters as arguments or return values. I consider these functions to be string-encoding-indifferent, so you can omit the string encoding options for those functions. (This will default the string encoding to Ansi ” just an interesting technical detail.) For all of the other functions, the string encoding options are extremely important. Here's a quick overview of the options:

  • Ansi

    Converts all strings to ANSI values. If no modifier is specified, Ansi is the default. This means that for ANSI functions you can omit this option. If the function deals with strings, you should include it anyway because it makes the declaration easier to read.

  • Unicode

    Converts all strings to Unicode values. If you're calling a Win32 function whose name terminates with a W , you must use this option. The W suffix indicates a Unicode method.

  • Auto

    Converts the strings according to CLR rules based on the name of the method (or the alias name, if specified) and the targeted platform. This option is very important if your application might run on different platforms. For example, Windows NT, 2000, and XP are all Unicode platforms, but Windows 98 is ANSI by default. If you need to be able to run in both environments, the Auto option will give you the flexibility you need. If you specify this option, you do not need to specify the specific ANSI or Unicode function (ending with A or W ). This is especially important when a function takes a platform-dependent string (such as LPTSTR ). You just specify the function name, and the runtime will handle the rest.

Visual Basic .NET makes it easy to call ANSI or Unicode functions. The only difference you really need to worry about is the string encoding option. It makes sense to use the Auto option to ensure that your applications can easily move among multiple platforms. If you use only ANSI functions, you'll limit your application's portability with no noticeable benefit. To ensure that your code will work on most platforms, use the Auto option (unless, of course, an API is platform-specific).

 DeclareAutoFunctionGetUserNameLib "advapi32.dll" _ (ByValbufferAsStringBuilder,_ ByRefsizeAsInteger)AsBoolean 
The Alias option

The Alias option of the Declare statement allows you to define a method with any arbitrary name while still hooking up to the correct underlying function. The following example illustrates this by defining a function called GetUserName , which uses the GetUserNameW function:

 DeclareUnicodeFunctionGetUserNameLib "advapi32.dll" _ Alias "GetUserNameW" _ (ByValbufferAsStringBuilder,_ ByRefsizeAsInteger)AsBoolean 

Alias can allow you to clean up your imported method names. Recall that the ANSI Win32 functions have an A appended to the function name and the Unicode function have a W appended to the function name. Explicitly declaring your functions in this way can be messy and is, frankly, pointless in Visual Basic .NET. You need to know that information only when you create your Declare statement. Other methods that call the imported function don't need to know whether you're calling the underlying ANSI or Unicode method ”the standard marshaling service handles all of that work for you. In some situations, the actual function name is cryptic or unintuitive, so it is worthwhile to provide an alternate name that indicates what the function actually does.

Caution

I am not recommending that you completely rename any function that you import using Declare . Far from it. You'll generally want to maintain a similar naming scheme for the sake of clarity. I'm simply noting that in some situations you might want to rename a function. (You should not do this arbitrarily, however.)


Limitations of Declare

The Declare statement is powerful and easy to use, but it hides certain implementation details from you, including a whole set of PInvoke options. These options include one for controlling how Win32 errors are handled, one for controlling how DLL entry points are resolved, and one for specifying function-calling conventions. Granted, you'll rarely need to worry about these situations ” Declare does, after all, handle the vast majority of them. When Declare isn't good enough, the DllImport attribute will give you all the control you need.

The DllImport Attribute

The DllImport attribute is a universal mechanism for defining native methods in the .NET world. In languages such as Visual C#, which lack a statement like Declare , the DllImport attribute is the only means of importing native methods. Of course, Visual Basic developers have never wanted to follow the pack, and the Visual Basic team extended the existing Declare syntax to work in Visual Basic .NET. But an unfortunate reality of using Declare is that you're not exposed to all of the inner workings of PInvoke. (This is also arguably a tremendous benefit.) For those who are never satisfied with the default or who need more precise control in specific instances, the DllImport attribute provides all you need.

Let's look at the previously defined Declare statement for the GetUserName function to see why it doesn't necessarily provide the controls you need. This function has a few strange requirements (note the use of StringBuilder as a parameter), but we'll ignore those for a moment.

 DeclareUnicodeFunctionGetUserNameLib "advapi32.dll" Alias_  "GetUserNameW" (ByValbufferAsStringBuilder,_ ByRefsizeAsInteger)AsBoolean 

This example defines a method called GetUserName and tells the Visual Basic .NET compiler that it uses the GetUserNameW API from advapi32.dll and that the Unicode keyword indicates that all strings are marshaled as Unicode. Great. Everyone should be on the same page. Now let's take a peek under the covers. In cases such as this, the actual Microsoft intermediate language (MSIL) code produced by the Visual Basic compiler can be very instructive. Here's what's produced when you run the MSIL Disassembler (ILDASM) on the GetUserName.exe sample executable:

 .methodpublicstaticpinvokeimpl("advapi32.dll" as "GetUserNameW"  nomangleunicodelasterrwinapi) boolGetUserName(class[mscorlib]System.Text.StringBuilderbuffer, int32&size)cilmanagedpreservesig { } 

The real magic here is in the pinvokeimpl attribute. You can see that the function, the library, and a bunch of options are specified. In fact, you can see some options that are not available through Declare : nomangle , lasterr , winapi , and preservesig . The unicode option was specified in the original Declare statement (as a part of the AutoAnsiUnicode Declare syntax), and the nomangle option tells the runtime that it must look for the exact function name specified. (It cannot append A or W .) The lasterr option tells the runtime to store the last Win32 error. This stored error value can be retrieved by calling the GetLastWin32Error method of the Marshal class The winapi option is a little more difficult to explain, but suffice it to say that it indicates that the method should be called using the platform's default calling convention. (More on this shortly.) What all of this really means is that the Declare statement is doing some work behind the scenes for you. As a result, there are certain defaults ( lasterr and winapi ) that you cannot alter and must accept if you're using Declare .

ILDASM and You

ILDASM, which is included with the .NET Framework SDK, can be run on any .NET assembly (DLL or executable) to generate a human-readable form of the compiler-generated MSIL output. As you gain familiarity with developing in Visual Basic .NET, you'll come across situations where you have several implementation options and you want to know which is the most efficient. Comparing the compiler output can be an instructive way to determine an efficient alternative. I use ILDASM quite frequently to confirm that Visual Basic is doing what I expect it to do. It's also a great learning tool for understanding how MSIL works.

The easiest way to access the Ildasm.exe tool is to run the Visual Studio .NET command prompt. From the Start menu, choose All Programs, Microsoft Visual Studio .NET, Visual Studio .NET Tools. For more information about using ILDASM, consult the documentation that accompanies Visual Basic .NET. I highly recommend the ILDASM tutorial.

The DllImport attribute allows you to fully customize the PInvoke attributes to meet your needs. A verbose equivalent to the above Declare statement follows . (A number of the properties specified for the DllImport attribute are already defaults, but we're listing them to show you their usage.)

 <DllImport("advapi32.dll",EntryPoint:="GetUserNameW",_ CharSet:=CharSet.Unicode,ExactSpelling:=True,_ CallingConvention:=CallingConvention.Winapi,PreserveSig:=True,_ SetLastError:=True)>_ PublicSharedFunctionGetUserName2(ByValbufferAsStringBuilder,_ ByRefsizeAsInteger)AsBoolean 'Nocodeherebutuschickens! EndFunction 

Notice also that the DllImport attribute is applied to an empty function. This is because the function is needed to provide the signature for the underlying function, and the DllImport attribute causes all calls to the GetUserName2 function to be forwarded to the GetUserNameW library function. As a result, any code contained in the GetUserName2 function would never be called, and the Visual Basic .NET compiler will generate an error if any executable code is contained within the method. Table 4-1 lists the properties supported by the Dll ­Import class.

Note

You must define your Sub or Function as Shared ; otherwise, you'll get a compiler error. The only exception is if the Function or Sub is defined in a module (implicitly shared).


Table 4-1. DllImport Attribute Properties

Property

Type

Description

CallingConvention

CallingConvention

Specifies how a native method is invoked by the runtime. The default value is CallingConvention.StdCall . The Declare default is CallingConvention.Winapi .

CharSet

CharSet

Controls name mangling and indicates how to marshal string arguments to the method. This is essentially what allows conversions to and from ANSI and Unicode character sets.

EntryPoint

String

Indicates the name or ordinal of the DLL entry point to be called. Ordinal values are prefixed with the # character. You'll generally provide a method name, but ordinals can be useful if the entry point is known but the function name is not.

ExactSpelling

Boolean

Indicates whether the name of the entry point in the unmanaged DLL should be modified to correspond to the CharSet value specified in the CharSet field. If this is set to True and the CharSet property is set to Ansi or Unicode , an A or a W will be appended to the name of the function specified in the EntryPoint property.

PreserveSig

Boolean

Allows you to work with functions that return an HRESULT and have out parameters. (A lot of COM methods fall into this category.) The default value for PreserveSig is True , which means that the function is used as defined. If you're invoking a method that returns an HRESULT , you might want set this property to False so you can call the method in a more natural way. See the MSDN documentation if you're interested in pursuing this further.

SetLastError

Boolean

Indicates that the callee will call the Win32 API SetLastError before returning from the attributed method. The runtime will then call the GetLastError Win32 API and cache the value returned, making it accessible to your application through the Marshal.Get ­LastWin32Error function.

The CallingConvention property definitely gives you a peek under the covers. Table 4-2 describes the possible values of the CallingConvention enumeration. You can pretty much gloss over this material ”it's probably more than you want to know ”but we've provided it here for the sake of completeness. What you really need to know is this: if you're calling methods from the Win32 library, either always use Declare or specify the Winapi CallingConvention . The Winapi option is the most flexible because it will adapt to the platform your application is running on. (Different Windows platforms use different calling conventions.) Of course, your own custom libraries or legacy code might require a different calling convention. In this case, the default StdCall is most likely to suit your purpose.

Table 4-2. CallingConvention Enumeration Members

Member

Description

Cdecl

The caller cleans the stack. This enables the calling of functions with a variable number of arguments.

StdCall

The callee cleans the stack. This is the default convention for calling unmanaged functions from managed code.

ThisCall

The first parameter is the this pointer, which is stored in register ECX. Other parameters are pushed on the stack. This calling convention is used to call methods of classes exported from an unmanaged DLL.

Winapi

Uses the default platform calling convention. For example, on Windows it's StdCall and on Windows CE it's Cdecl .

You can see that there's a lot to the DllImport attribute. Not to worry, though ”as we've mentioned, the defaults for DllImport are designed to address the most common cases. If you do need to fiddle with the settings, however, you need a good understanding of what exactly you're trying to target.

Now we get to take a look at a subject I've only skirted up until this point: marshaling.

Marshaling Types

Marshaling, for the uninitiated, is the process of moving data across process boundaries. In the Visual Basic .NET context, this can also mean moving data across managed and unmanaged process boundaries. Marshaling is essential when you need to translate the Windows API function definitions into their Visual Basic .NET equivalents, using either the Declare or DllImport syntax. Marshaling can get tricky, especially when you're defining the function parameters and return types for imported functions. There's often more than one way to define individual parameters, and the differences are not always obvious. We'll start with a summary of generic marshaling and then look at some specific examples that demonstrate how to put this information into practice.

Basic Types

Many value types have the same managed and unmanaged memory layout, which allows the default interop marshaler to take care of everything for you. The .NET Framework documentation refers to these types as blittable types. They include the signed and unsigned varieties of Byte , Short , Integer , and Long . The CLR provides two additional types that are exclusively for dealing with unmanaged pointers and handles: IntPtr and UIntPtr . One-dimensional arrays of blittable types and structures that contain only blittable types can also be considered blittable types (in that you do not have to worry about marshaling issues). Table 4-3 compares these types. More complex types with more complex memory layout considerations ” Boolean , arrays of more than one dimension, arrays of nonblittable types, Class , String , and Structure ” usually require some special handling.

Table 4-3. A Comparison of Basic Types

Native Type

Visual Basic .NET Equivalent

Size (in Bytes)

SHORT

Short, Int16

2

WORD

Short, Int16

2

DWORD

Integer, Int32, UInt32

4

INT

Integer, Int32

4

UINT

Integer, UInt32

4

LPXXX

IntPtr, UIntPtr

4 (on 32-bit platforms)

8 (on 64-bit platforms)

HANDLE

IntPtr

4 (on 32-bit platforms)

8 (on 64-bit platforms)

Of course, this table doesn't tell the whole story. I haven't yet addressed the difference between ByVal and ByRef parameters.

ByRef vs. ByVal

In Visual Basic .NET, the default for parameters is ByVal . This makes a lot of sense, but sometimes you need to pass a pointer to a basic type instead of just the pointer value. In this case, you must define your function parameter as ByRef instead of ByVal . The CLR will ensure that the parameter is properly marshaled. This applies to all value types, including structures.

You can also decorate parameters with the In and Out attributes, which control whether the parameter is for read only ( In ), for output only ( Out ), or for both input and output ( In , Out ). Because Visual Basic considers In as a protected keyword, you must specify the attribute as either [In] or InAttribute . Passing a ByVal parameter with In , Out attributes is equivalent to passing ByRef . If necessary, you can also decorate a parameter with the MarshalAs attribute. This attribute allows you to explicitly specify how the interop marshaler should handle the data. Let's revise our previous example, replacing the ByVal and ByRef syntax with In , Out attributes to demonstrate how they work.

 <DllImport("advapi32.dll",EntryPoint:="GetUserNameW",_ CharSet:=CharSet.Unicode,ExactSpelling:=True,_ CallingConvention:=CallingConvention.Winapi,PreserveSig:=True,_ SetLastError:=True)>_ PublicSharedFunctionGetUserName2(_ <[In]>bufferAsStringBuilder,_ <InAttribute,Out>sizeAsInteger)AsBoolean 'Nocodeherebutuschickens! EndFunction 

Note how I've used both forms of the In attribute and shown how to decorate your function parameters with them. Pretty easy to understand. Realize, however, that you do not need to specify these attributes ” ByVal and ByRef are usually sufficient.

Dealing with constants

The Win32 API is rife with constants and predefined values, which are used as parameters to all sorts of functions. Therefore, when we import functions, we naturally have to deal with constants and predefined values, often redefining the constants in our own code. Take the following example of the MessageBeep function from the User32 library:

 BOOLMessageBeep(UINTuType//soundtype); 

This function takes an unsigned integer as a parameter. The valid constants for this function are listed in Table 4-4. In the WinUser.h header file, these constants are defined using a series of #define statements. When you work with a function that requires constant parameters, such as MessageBeep , you have three options for defining these constants when you import the function into Visual Basic .NET:

  • Pass the integer equivalent of the constant. (This works, but the result is hard to read and, more importantly, harder to understand.)

  • Define the constant in your code and pass the constant to the function. (This is better, but it's still not very .NET-ish.)

  • Create an Enum to contain the constant values and define the method parameter as the enum type. (This looks a lot better.)

Table 4-4. The MessageBeep Constants

Value

Sound

-1

Simple beep. If the sound card is not available, the sound is generated using the speaker.

MB_ICONASTERISK

SystemAsterisk

MB_ICONEXCLAMATION

SystemExclamation

MB_ICONHAND

SystemHand

MB_ICONQUESTION

SystemQuestion

MB_OK

SystemDefault

Digging into the WinUser.h header file, I was able to find the definitions for each of these constants. For completeness, here they are:

 #defineMB_OK0x00000000L #defineMB_ICONHAND0x00000010L #defineMB_ICONQUESTION0x00000020L #defineMB_ICONEXCLAMATION0x00000030L #defineMB_ICONASTERISK0x00000040L 

Now we need to consider how to deal with these constants in our applications. Using the first option, we don't need to do anything out of the ordinary ”just pass the integer value to the function. Using the second option, we can define a set of constants:

 'Thiswouldsatisfythesecondoption ConstMB_SIMPLEAsInteger=-1 ConstMB_ICONHANDAsInteger=&H10 ConstMB_ICONQUESTIONAsInteger=&H20 ConstMB_ICONEXCLAMATIONAsInteger=&H30 ConstMB_ICONASTERISKAsInteger=&H40 ConstMB_OKAsInteger=&H0 

Taking this example even further, as in the third option, we can group the constants into an enumerated type. This doesn't look terribly different but is somewhat cleaner than the previous two options.

 'Thiswouldsatisfythethirdoption PublicEnumSystemBeepsAsInteger Simple=-1 OK=&H0 IconHand=&H10 IconQuestion=&H20 IconExclamation=&H30 IconAsterisk=&H40 EndEnum 

The following example shows how you might define the MessageBeep function using these three options. We first define a Function that takes an Integer as the sole parameter. This allows us to pass either a variable (constant or otherwise) or just an integer value, and it basically satisfies the requirements for both the first and second options. The second function is far more interesting. You've seen the enumerated type SystemBeeps that specifies members with the same values as the argument constants. We then define the function prototype as taking an argument of type SystemBeeps . Because the SystemBeeps structure was defined as an Integer , it is equivalent to the declaring the parameter as an Integer but has the added benefit of enforcing specific values on the parameter type. You can play with this and other examples in the SimplePInvoke sample project included with this book's sample files.

 'Forfirstandsecondoption DeclareFunctionMessageBeepLib "User32.dll" _ (ByValtypeAsInteger)AsBoolean 'Forthirdoption DeclareFunctionMessageBeepLib "User32.dll" _ (ByValtypeAsSystemBeeps)AsBoolean 

The following example shows how different each calling method looks. Ask yourself which option leads to more readable and maintainable code.

 'Option1-Ugly MessageBeep(-1) 'Option2-Better MessageBeep(MB_SIMPLE) 'Option3Makessensetome! MessageBeep(SystemBeeps.Simple) 

By far the best option is to handle the constant parameters as enumerated types. There are exceptions, of course. If you have only a single constant to worry about, say MAX_SIZE , declaring a constant is the best way to go. Using enumerations generally gives you much more control and flexibility than simple constants. By defining a function parameter as an enum, you eliminate the possibility that someone will provide an invalid parameter value. It also allows you to better control the allowed parameters by eliminating certain options (through omission) or creating additional custom options.

The cool thing here is that enums are always implemented as a basic type. This means you really don't have to worry about any marshaling issues. Plus, you can customize your enums to represent Byte , Integer , Long , or Short . This gives you a lot of flexibility, helps organize the constant values into a single location, provides a type-safe way to ensure valid function parameters, and generally makes your code easier to read. What more could you ask for?

Marshaling Strings

Dealing with strings is a bit tricky. If you've worked with APIs, you might know what I mean. As I've mentioned, strings cannot always be marshaled in a conventional way. When you're importing native methods that sport String parameters or return types, you must be mindful of the requirements imposed by the function's behavior. It is important to understand where the strings are allocated and who is responsible for releasing that memory. Recall the GetUserName example:

 BOOLGetUserName(LPTSTRlpBuffer,//namebuffer LPDWORDnSize//sizeofnamebuffer); 

The question is, how do we translate this function definition into something Visual Basic .NET can work with? The trick here is that the GetUserName function expects the calling method to allocate a string and pass a pointer to that memory and a pointer to a DWORD containing the allocation size of the string. In this case, you must use StringBuilder as the first argument because GetUserName modifies the string passed to it ”which is not normally allowed. (Note that strings are immutable objects in Visual Basic .NET ”you cannot modify the contents of a String object.) Thankfully, StringBuilder provides the necessary functionality. Furthermore, because the StringBuilder class is a reference type, you must pass it ByVal instead of ByRef . (Otherwise, it would be a reference to a reference type.) The second argument can be defined as an Integer (a DWORD is 4 bytes ”see Table 4-3) and must be passed as a ByRef (because the function requires a pointer, not just a copy of the value).

 DeclareAutoFunctionGetUserNameLib "advapi32.dll" _ (ByValbufferAsStringBuilder,ByRefsizeAsInteger)AsBoolean ... 'Usingtheimportedmethod ConstMAX_UNLENAsInteger=257 DimsbAsNewStringBuilder() DimlengthAsInteger length=MAX_UNLEN sb.Capacity=MAX_UNLEN GetUserName(sb,length) Console.WriteLine(sb.ToString()) 

There are very specific rules related to marshaling strings. The two situations that you need to be most concerned with are passing string parameters and functions that return strings. We'll look at these in turn .

Strings as parameters

You might be surprised at the number of ways that strings can be passed to methods. Sometimes a string is passed as a read-only parameter, and other times a method requires a pointer to a string buffer so the string can be modified. (Recall the GetUserName function used in several examples so far.) Table 4-5 shows how to specify parameters given certain parameter requirements.

Table 4-5. String Marshaling Rules

Parameter Requirement

Equivalent Visual Basic .NET Parameter

A pointer to a string for input; string will not be modified.

ByVal As String

A string that can be replaced with another string. (The caller owns memory or is responsible for deallocating memory.)

ByRef As String

A pointer to a string that you don't own.

ByVal As IntPtr

A pointer to a pointer (handle) to a string that you don't own.

ByRef As IntPtr

A pointer to a string buffer that can be read and written to.

ByVal As StringBuilder

(You can also use ByVal As String if the function is imported using the Declare statement.)

An array of strings.

ByVal strAry As String()

A pointer to an array of strings.

ByRef strAry As String()

Ultimately, you must understand what the requirements are for the function you're importing. Whether the function needs to modify the string will influence your choice of StringBuilder versus String . (This applies mainly to functions imported with the DllImport attribute.) Always keep in mind, however, that importing functions with the Declare statement will simplify string marshaling for you. Declare will handle marshaling strings back to the caller, which lets you avoid using StringBuilder as a parameter type altogether. Now let's take up the issue of string ownership.

Functions that return strings

Functions that return strings pose some interesting challenges. When you deal with strings, you must pay special attention to who is allocating the string and who is responsible for releasing it. This information will allow you to define the correct declaration of the function in your code. There are two possibilities for string ownership.

  • A function returns a string, and you own the result.

  • A function returns a string but retains ownership of the resource.

If the function returns a string that you own (that is, if you're responsible for destroying it when you're done with it), you should declare the function return type As String . This allows the CLR to handle the string cleanup for you, and you can use this function and the returned string in a normal fashion.

 Function...AsString 

If, on the other hand, a function returns a pointer to a string that resides in a memory location that you do not own, you must be careful. Defining the return type as a string is unacceptable because the CLR would eventually attempt to release that memory. This could cause some interesting problems if, say, the memory were kernel resident. In this situation, you're better off not dealing with the string directly as a managed String object. Instead, deal with it as a pointer:

 Function...AsIntPtr 

An example will definitely help. Let's consider the GetCommandLine function from the Kernel32 library. The function prototype looks like this:

 LPTSTRGetCommandLine(VOID); 

Note

This scenario is contrived. You should never use this API via PInvoke; instead, you should use System.Environment.CommandLine to read the parameters. In general, always use the managed equivalent if one is available.


GetCommandLine is a pretty simple function. It returns the command-line arguments for your application. It takes no arguments and returns a pointer to a string. Easy, right? When you import the function, you might assume that, because a platform-dependent string is being returned, you can define the Declare statement like this:

 DeclareAutoFunctionGetCommandLineLib "Kernel32.dll" ()AsString 

Makes sense, right? But there's a problem. It might not be obvious, but I just made an error. I made an assumption about the return type. Think about it. Who allocates the returned string? There are only two possibilities: the GetCommandLine function allocated the string or I did. In this situation, it must have been the GetCommandLine function (actually, it's the operating system in this case, but the point is still valid) because I sure didn't give it anything to work with. This raises the question of who's responsible for releasing the memory associated with the returned string.

If you delve deeper into the definition of the GetCommandLine function, you'll find out that it returns a pointer to a string that's created by the operating system at the application's startup. The caller is supposed to use the pointer to read the string, which represents the program's arguments, but the caller is not supposed to modify or release the underlying string. If we define the function return value as a string object, the CLR will try to free the memory represented by that string. If this happens and some other function depends on the existence of this string, bye-bye application.

The proper way to handle this situation is to not explicitly treat the return value as a string object. Instead, you define the return type as a pointer to a memory location ( IntPtr ). This allows you to have a pointer to the memory location of the string without giving the CLR the impression that it needs to clean up anything for you. Great, problem solved . But this raises another question. Once you have a pointer to the string, how do you access it? Not to worry. That particular quandary is solved by the Marshal class (from the System.Run ­time.InteropServices namespace). Check out the following example:

 DeclareAutoFunctionGetCommandLineLib "Kernel32.dll" ()AsIntPtr ... DimparamAsIntPtr=GetCommandLine() DimcommandLineAsString=Marshal.PtrToStringAuto(param) 

You can see how I used the Marshal.PtrToStringAuto method to convert the LPTSTR (a platform-dependent string) to a Visual Basic .NET string. (There are other methods for dealing with ANSI and Unicode strings.) Generally, if you need to deal with strings in this way, the specific Marshal.PtrToString method you use should depend on the underlying string type. Also, remember that this example applies only to the situation in which we're not responsible for releasing the returned string's memory. If a function returns a pointer to a string and assumes that the caller will free it, it's perfectly acceptable to declare the return type as String and everyone will be happy.

Note

You should get familiar with methods of Marshal class; this class is used wherever manual marshaling is needed. A common mistake is using inappropriate managed types for the supposed native equivalent. (For example, managed Long is 64 bits and unmanaged long is only 32 bits.) This gets even more complicated when you're dealing with structures. Thankfully, you can check the size of a structure using the Marshal.SizeOf method. Before you ever try to call a native DLL from Visual Basic .NET, first check whether the code works correctly in the unmanaged world and then convert it into a Visual Basic .NET equivalent.


This wraps up our discussion of string marshaling. Now let's look at Structure and Class marshaling.

Marshaling Structures

Structures can present a variety of marshaling challenges. The structure, which is a value type, often requires little additional manipulation ”the default marshaling is usually sufficient. This frees you from having to worry about a lot of implementation details. When you pass structures or classes to a function, the default marshaler takes care of the layout of its members. However, if the function expects a specific layout, you might need to alter the memory of the structure to conform to what the API expects. You can do this using the StructLayout attribute (which we'll describe later).

Strings tend to present a basic problem with structures. Recall that structures that contain only basic types can usually be marshaled by the standard marshaling service without your having to specify any additional layout information. Strings are a big exception.

Structures and strings

Strings can present some interesting marshaling challenges for structures (surprise, surprise). There are, of course, several variations on the theme. Typically, you'll see one of the following scenarios:

  • A string as a pointer to a string ( LPSTR , LPTSTR , or LPWSTR ).

  • A string as a pointer to character buffer ( CHAR , TCHAR , or WCHAR ). The size of the buffer is usually passed as another member of the structure.

  • A string as an embedded fixed-size character array ( CHAR[] , TCHAR[] , or WCHAR[] ).

To handle these situations, you can use the MarshalAs attribute to control how the individual elements of a structure (including strings) are marshaled. Let's work through an example to see how to handle strings in a structure. An interesting example, yet again from the Win32 API, is the OSVERSIONINFO structure, which is used in conjunction with the GetVersionEx function from the Kernel32 library:

 BOOLGetVersionEx(LPOSVERSIONINFOlpVersionInfo//versioninformation); 

This function fills an existing structure with information about the current operating system. This Visual C++ structure looks like this:

 typedefstruct_OSVERSIONINFO{ DWORDdwOSVersionInfoSize; DWORDdwMajorVersion; DWORDdwMinorVersion; DWORDdwBuildNumber; DWORDdwPlatformId; TCHARszCSDVersion[128]; }OSVERSIONINFO; 

Most of the members of the OSVERSIONINFO structure are fairly straightforward and easy to understand, but the last element raises a question. The szCSDVersion element is defined as a fixed array of TCHAR (platform-dependent) characters. You might be tempted to specify a StringBuilder , but you'd be incorrect. You should specify the element as a string and decorate it with the MarshalAs attribute. This will allow you to specify how the string should be marshaled (in this case, as a ByValTStr with a size of 128 characters). Check out the following example:

 PublicStructureOSVersionInfo PublicOsVersionInfoSizeAsInteger PublicmajorVersionAsInteger PublicminorVersionAsInteger PublicbuildNumberAsInteger PublicplatformIdAsInteger <MarshalAs(UnmanagedType.ByValTStr,SizeConst:=128)>_ PublicversionAsString EndStructure 

This is only a single example, and unfortunately space is limited. You can play around with the MarshalAs attribute for the other scenarios you're likely to encounter.

More Info

See the GOTDOTNET Web site (http://www.gotdotnet.com) for utilities and samples to help you with marshaling issues.


Now that we've looked at strings, let's spend a little time looking at the StructLayout attribute.

The StructLayout attribute

StructLayout provides a set of options (described in Table 4-6) for controlling the physical layout of a given structure. (Sounds obvious, right?) If you omit the StructLayout attribute for any structure, the runtime will default to the StructLayout.Automatic setting. This means that the standard marshaling service will be used and assumptions will be made about the structure's memory layout.

Table 4-6. The StructLayout Attribute Options

Options

Type

Values

Description

LayoutKind (constructor)

Enum

Auto

Explicit

Sequential

Controls the layout of an object when it's exported to unmanaged code.

CharSet

Enum

Ansi

Auto

Unicode

Indicates how string data fields within the class should be marshaled.

Pack

Integer

0, 1, 2, 4, 8, 16, 32, 64, or 128

Controls the alignment of data fields of a class or structure in memory. Used in conjunction with the LayoutKind.Sequential option. A value of 0 indicates that the packing alignment is set to the default for the current platform. The default packing size is 8.

Size

Integer

Unrestricted

Indicates the absolute size of the structure or class. This is primarily for use by compiler writers and should generally be avoided.

Alternatively, you can define your structure's layout explicitly. This is more common when you have odd-member byte offsets and need to conform to an explicit layout. The following example is directly equivalent to the previous definition of the OSVersionInfo structure. The only difference is that we specify all of the member element's byte offsets.

 <StructLayout(LayoutKind.Explicit)>_ StructureOSVersionInfo <FieldOffset(0)>PublicOsVersionInfoSizeAsInteger <FieldOffset(4)>PublicmajorVersionAsInteger <FieldOffset(8)>PublicminorVersionAsInteger <FieldOffset(12)>PublicbuildNumberAsInteger <FieldOffset(16)>PublicplatformIdAsInteger <FieldOffset(20),MarshalAs(UnmanagedType.ByValTStr,SizeConst:=128)>_ PublicversionAsString EndStructure 

There's a lot more you can control in a structure's layout, but that's a matter for you to explore on your own. Now let's look at everyone's favorite topic: shortcuts.

Structure definition shortcuts

You'll often run into structures that contain other structures. In other words, some structures are composites. A good example is the CONSOLE_SCREEN_BUFFER_INFO structure shown below. This structure contains multiple instances of two additional structure types: COORD and SMALL_RECT . You might think that if you want to properly define the CONSOLE_SCREEN_BUFFER_INFO structure, you must also define the contained structures. This is not the case. In fact, you don't have to worry about contained structures in this situation.

 typedefstruct_CONSOLE_SCREEN_BUFFER_INFO{ COORDdwSize; COORDdwCursorPosition; WORDwAttributes; SMALL_RECTsrWindow; COORDdwMaximumWindowSize; }CONSOLE_SCREEN_BUFFER_INFO; typedefstruct_COORD{ SHORTX; SHORTY; }COORD; typedefstruct_SMALL_RECT{ SHORTLeft; SHORTTop; SHORTRight; SHORTBottom; }SMALL_RECT; 

Confused? The trick is to recognize that the contained structures take up space in a very standard way. You can substitute basic types without having to worry about defining the other contained structures. The only limitation is that you must preserve the memory layout. Recall the basic types shown in Table  4-3 on page 119. Because we know the size of each of these types, we should be able to get away with substitutions.

Take the COORD structure as an example. COORD contains two members, both of type SHORT . We know that the SHORT type is 2 bytes in size, so the total size of the COORD structure must be 4 bytes. It just so happens that the Integer type is also 4 bytes. Logically, we could replace any definition of COORD with an Integer , and everyone would be happy. By extension, the SMALL_RECT structure takes up 8 bytes (4 x 2 byte SHORT values). We could then substitute a Long (also 8 bytes) and still maintain the correct structure layout ” sort of. The only problem with this structure definition is there is a Short field right in the middle of the other fields: wAttributes . When you want to simplify your layouts, the easiest case is when all the fields are even multiples of 4 bytes. (This is a consequence of running on 32-bit [4-byte] processor architectures.) Unfortunately, a Short is 2 bytes and that complicates the memory layout story. Consequently, to maintain our correct in-memory layout, we need to use the StructLayout attribute to tell the runtime what the structure's memory layout should look like. Our new definition for CONSOLE_SCREEN_BUFFER_INFO might look like this:

 <StructLayout(LayoutKind.Sequential,Pack:=4)>_ PublicStructureCONSOLE_SCREEN_BUFFER_INFO PublicdwSizeAsInteger PublicdwCursorPositionAsInteger PublicwAttributesAsShort PublicsrWindowAsLong PublicdwMaximumWindowSizeAsInteger EndStructure 

Of course, there are certain disadvantages to performing the substitution. The most important is that you lose the ability to access the data contained by the structure. By defining the contained structures as basic types, we cannot access the data as if they were contained structures. This is not a problem if we have no interest in the structure member, but it can be a pain if we want to work with a COORD or a SMALL_RECT structure. In that case, we must define the structures and change the definition of the containing structure to contain those specific types. We can then access the members using dot notation ”a big convenience. Of course, nothing can prevent you from substituting a basic type at first and then defining one or more of the contained types on an as-needed basis. It's just that having to define all structures and substructures at once can be a pain ”especially if you aren't interested in the information. This method can give you an expedient workaround.

The Marshal Class

You will see the Marshal class many times in this book. The reason is obvious: it's so darn useful. You saw it before when I discussed returning strings from functions. Here you'll get a broad overview of the class. Table 4-7 lists some of the methods specific to platform invoke operations. The Marshal class provides many utilities that allow you to read and write unmanaged memory, convert types, copy managed arrays to unmanaged memory, and much, much more. Whenever you get stuck in a marshaling jam, remember this class.

Table 4-7. Selected Shared Methods of the Marshal Class

Method

Description

AllocHGlobal

Overloaded. Allocates a block of memory using GlobalAlloc .

Copy

Overloaded. Copies data between a managed array and an unmanaged memory pointer.

DestroyStructure

Frees all substructures pointed to by the specified native memory block.

FreeBSTR

Frees a BSTR using SysFreeString .

FreeHGlobal

Frees memory previously allocated from the unmanaged native heap of the process using AllocHGlobal .

GetExceptionCode

Retrieves a code that identifies the type of the exception that occurred.

GetExceptionPointers

Retrieves a machine-independent description of an exception and information about the machine state for the thread when the exception occurred.

GetLastWin32Error

Returns the error code returned by the last unmanaged function called using platform invoke that had the SetLast ­Error flag set.

PtrToStringAnsi

Overloaded. Copies all or part of an ANSI string to a managed String object.

PtrToStringAuto

Overloaded. Copies an unmanaged string to a managed String object.

PtrToStringBSTR

Copies a Unicode string stored in native heap to a managed String object.

PtrToStringUni

Overloaded. Copies an unmanaged Unicode string to a managed String object.

PtrToStructure

Overloaded. Marshals data from an unmanaged block of memory to a managed object.

ReadByte

Overloaded. Reads a single byte from an unmanaged pointer.

ReadInt16

Overloaded. Reads a 16-bit integer from native heap.

ReadInt32

Overloaded. Reads a 32-bit integer from native heap.

ReadInt64

Overloaded. Reads a 64-bit integer from native heap.

ReadIntPtr

Overloaded. Reads a processor native sized integer from native heap.

ReAllocHGlobal

Resizes a block of memory previously allocated using AllocHGlobal .

SizeOf

Overloaded. Returns the unmanaged size of a class used via Marshal in bytes.

StringToBSTR

Allocates a BSTR and copies the string contents into it.

StringToHGlobalAnsi

Copies the contents of a managed String object into native heap, converting into ANSI format as it copies.

StringToHGlobalAuto

Copies the contents of a managed String object into native heap, converting into ANSI format if required.

StringToHGlobalUni

Copies the contents of a managed String object into native heap.

StructureToPtr

Marshals data from a managed object to an unmanaged block of memory.

WriteByte

Overloaded. Writes a single-byte value into native heap.

WriteInt16

Overloaded. Writes a 16-bit integer value into native heap.

WriteInt32

Overloaded. Writes a 32-bit integer value into native heap.

WriteInt64

Overloaded. Writes a 64-bit integer value into native heap.

WriteIntPtr

Overloaded. Writes a processor native-sized integer value into native heap.

This table contains a somewhat abbreviated list of the members. We've omitted most of the COM-related members for the sake of simplicity, but you're more than welcome to investigate those on your own.

To wrap up our marshaling section, let's look at how to implement callback functions.

Implementing Callback Functions

Some interfaces require that a pointer to a function be passed, usually to handle events or generic tasks . To show this in action, I developed the ExtendedConsole sample (included with this book's sample files) to, in part, enable a console application to handle control-key events. For example, how do you handle the situation when the user hits the Ctrl+C key combination? This is not something that is provided by the System.Console class. Searching the MSDN library, I found the function SetConsoleCtrlHandler , which allows you to register a callback function (or a series of callbacks) that is called whenever a specific set of control events occurs (including Ctrl+C). Here's what SetConsoleCtrlHandler looks like:

 BOOLSetConsoleCtrlHandler(PHANDLER_ROUTINEHandlerRoutine,//handlerfunction BOOLAdd//addorremovehandler); 

Of course, this leads us down the merry old dependency trail. We need the definition of the HandlerRoutine parameter (the following example), and consequently the possible values for the dwCtrlType specified by the HanderRoutine prototype (from WinCon.h).

 #defineCTRL_C_EVENT0 #defineCTRL_BREAK_EVENT1 #defineCTRL_CLOSE_EVENT2 //3isreserved! //4isreserved! #defineCTRL_LOGOFF_EVENT5 #defineCTRL_SHUTDOWN_EVENT6 BOOLWINAPIHandlerRoutine(DWORDdwCtrlType//controlsignaltype); 

So what do we do here? We simply start off by creating an Enum called ConsoleEventKey , which contains the constant values. Then ”and this is the interesting part ”we create a delegate (remember them from Chapter 2?), ControlHandler , that roughly matches the prototype for the HandlerRoutine function. (Note that we don't have to worry about exact function naming ”just the function's signature.) Because the ConsoleEventKey enum is declared as an Integer , we can substitute that enum for the DWORD argument for ControlHandler . Now all that needs to be done is to declare the SetConsoleCtrlHandler method:

 'Thecontroleventthatoccurred.(fromwincom.h) PublicEnumConsoleEventKeyAsInteger CTRL_C=0 CTRL_BREAK=1 CTRL_CLOSE=2 CTRL_LOGOFF=5 CTRL_SHUTDOWN=6 EndEnum'Thisisthedefinitionofthecallbackfunction 'Handlertobecalledwhenaconsoleeventoccurs. PrivateDelegateFunctionControlHandler_ (ByValkeyAsConsoleEventKey)AsInteger 'Hereisthefunctionweneedtocalltoregisteracallback PrivateDeclareFunctionSetConsoleCtrlHandlerLib "kernel32.dll" _ (ByValeAsControlHandler,ByValaddAsBoolean)AsBoolean 

The declaration of SetConsoleCtrlHandler is straightforward. We specify the function's first argument as a ControlHandler delegate type. Everything else should be self-explanatory at this stage. The ExtendedConsole sample shows this in action. The ConsoleControlEvents class provides the functionality for hooking up a ControlHandler delegate. We also define a ConsoleEvent event that clients can subscribe to and handle:

 'Eventfiredwhenaconsoleeventoccurs PublicEventConsoleEventAsControlEventHandler PrivateeventHandlerAsControlHandler PublicSubNew() MyBase.New() 'savethistoaprivatevarsotheGCdoesn'tcollectit... eventHandler=NewControlHandler(AddressOfMe.OnConsoleEvent) SetConsoleCtrlHandler(eventHandler,True) EndSub ProtectedFunctionOnConsoleEvent(ByValeventKeyAsConsoleEventKey)_ AsInteger DimeAsNewControlEventArgs(eventKey) RaiseEventConsoleEvent(Me,e) Ife.CancelThen Return1 Else Return0 EndIf EndFunction 

That takes care of the callback function. It really isn't that complicated. Essentially, all you need to do is define a delegate method, following most of the same rules as for the Declare statement. Visual Basic .NET really can't make it much easier than that!

Wrapping Things Up

Ultimately, both the Declare statement and the DllImport attribute use the same underlying mechanism to do their jobs. The major difference between the two is the accessibility of certain PInvoke settings. In most situations, the loss of flexibility of the Declare statement is perfectly acceptable, given its ease of use and simple syntax. I recommend that you use the Declare statement whenever possible. Granted, in some situations it might be necessary to use DllImport (for legacy libraries that use alternate calling conventions, for instance). But those situations should be relatively rare.

We've encountered a lot of marshaling issues already, but we've still barely scratched the surface. You'll be glad to know that most of the information about PInvoke marshaling can be applied to COM interop, but there's much more than can be adequately addressed in a single chapter. If you need to delve further, take a look at .NET and COM by Adam Nathan (Sams Publishing, 2002).

I l @ ve RuBoard


Designing Enterprise Applications with Microsoft Visual Basic .NET
Designing Enterprise Applications with Microsoft Visual Basic .NET (Pro-Developer)
ISBN: 073561721X
EAN: 2147483647
Year: 2002
Pages: 103

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