Details of Platform Invoke

To fully use P/Invoke services, you need to understand the concepts of marshaling and callbacks. This section explores each of these topics. Pay particular attention to marshaling because COM Interop uses the same underlying marshaling service as P/Invoke. COM Interop is covered in Chapters 8, “Using COM/COM+ in Managed Code,” and 9, “Using Managed Code as a COM/COM+ Component.”

Note

It would not be practical to show examples from every language throughout this chapter, so the focus has been narrowed to C#. The same features that are shown are available in most languages supported in the .NET Framework.


Declaring the Function: DllImport Attribute

All P/Invoke calls are declared through DllImportAttribute. In its simplest form, DllImportAttribute declares the name of the DLL in which the function containing the unmanaged code lives. This attribute is applied to an external static function that, by default, has the same name as the unmanaged function. After this function is declared, managed code can call the function by name to initiate a managed to unmanaged transition and call the function in the DLL specified as unmanaged code. You have already seen a couple of examples using the DllImport attribute.

To use the DllImportAttribute, you need to know what functions are defined in the DLL. One place that you can look is the documentation. Often, the documentation states which DLL implements the function. Many of the functions that Microsoft (and others) document include some indication in to which DLL the function is implemented in. If you don't have this documentation or it is incomplete, then you can use dumpbin -exports foo.dll or link -dump -exports foo.dll. You need to have both dumpbin.exe and link.exe in your PATH environment variable or these calls will fail. Currently, both of these programs are in Program FilesMicrosoft Visual Studio .NETVc7in). Executing this function on kernel32.dll yields Listing 7.5.

Listing 7.5. DumpBin Output for Kernel.dll
Microsoft (R) COFF/PE Dumper Version 7.00.9372.1
Copyright (C) Microsoft Corporation.  All rights reserved.

Dump of file kernel32.dll

File Type: DLL

  Section contains the following exports for KERNEL32.dll

    00000000 characteristics
    3A1B5EB6 time date stamp Tue Nov 21 23:50:46 2000
        0.00 version
           1 ordinal base
         823 number of functions
         823 number of names

    ordinal hint RVA      name

          1    0 0000CC71 AddAtomA
          2    1 0000CC61 AddAtomW
          3    2 00044521 AddConsoleAliasA
          4    3 000444EA AddConsoleAliasW
          5    4 000452D8 AllocConsole
          6    5 0003308F AllocateUserPhysicalPages
          7    6 00007828 AreFileApisANSI
          8    7 00043C78 AssignProcessToJobObject
          9    8 00028DE8 BackupRead
         10    9 00029044 BackupSeek
. . .
       708  2C3 00006779 Sleep
709 2C4 00006787 SleepEx
. . .

If you wanted to P/Invoke the Sleep function, you now know that it is in kernel32.dll. To learn what the arguments are to this function, you need to consult the documentation for this function. You can use dumpbin to similarly examine other DLL files. All you need to call the function with P/Invoke are the DLL in which the function is implemented, the name of the function, and the arguments it takes.

Take the following call declaration:

 [DllImport("unmanaged.dll")]
public static extern void Foo();

Listing 7.5 shows a declaration of a function that is implemented in the DLL "unmanaged.dll". The name of the function defaults to the name given to the managed function. Listing 7.5 shows that the managed function does not return anything, and no arguments exist. The CLR looks for a function in "unmanaged.dll" that has the same signature as specified by the managed function. In the DLL "unmanaged.dll", a function should exist called Foo that takes no arguments and does not return anything. If the CLR does not find the function as specified, you get an EntryPointNotFoundException when you try to call the function. From C, the function should be declared like the following:

extern "C" _declspec(dllexport) void Foo();

DllImportAttribute has several options that can be used to change the behavior of the interop:

  • CallingConvention— This changes the calling convention that is used to call the unmanaged function. Following are the possible values for the CallingConvention enum:

    Cdecl— The caller cleans the stack.

    FastCall— This is not supported in this version of the CLR.

    StdCall— The callee cleans the stack. This is the default.

    ThisCall— The first argument passed is the this pointer.

    WinapiStdCall on Windows, Cdecl on Windows CE.

    [DllImport("unmanaged.dll", CallingConvention=
    CallingConvention.StdCall)]
    

    This declaration indicates that the runtime should use StdCall as a calling convention (this is the default). Practically speaking, you should take the default and just be aware that other options are available. Using StdCall covers a vast majority of the cases where interop is useful.

  • CharSet— This controls the name mangling (with macros) and the way that string arguments are passed to the unmanaged function. The following are possible values for the CharSet enum:

    Ansi— Marshal strings as 1 byte strings. If the parameter ExactSpelling is false or not specified, setting the CharSet to Ansi also has the effect of having the runtime look for the function with an 'A' appended to the end of the name, in addition to the name of the function.

    Auto— The character set is set to Unicode for Windows NT and Ansi for Windows 95.

    Unicode— Marshal strings as Unicode 2-byte characters. As with Ansi, if ExactSpelling is false or is not specified, setting CharSet to Unicode results in the runtime looking for the function with a 'W' appended to the name in addition to searching for the name of the function.

    [DllImport("unmanaged.dll", CharSet=CharSet.Unicode)]
    
  • EntryPoint— mThis specifies the name or ordinal of the DLL. This function allows the programmer to override the default of taking the name of the function that is declared in the managed code:

    [DllImport("unmanaged.dll", EntryPoint="Foo")]
    public static extern void FooBar();
    

    Now the managed code references this function as FooBar, but the runtime looks for the signature Foo in the DLL. The signature still needs to match that defined in the DLL.

  • ExactSpelling— mThis specifies whether the name of the function should be appended with 'A' or 'W' for accessing ANSI or Unicode respectively. This parameter in the DllImportAttribute is a Boolean:

    [DllImport("unmanaged.dll", ExactSpelling=false)]
    public static extern void FooBar();
    

    With this declaration, the runtime looks for FooBarA and FooBar. If CharSet is set to Unicode, it would look for FooBarW and FooBar.

  • PreserveSig— mTypically, all COM functions return an HRESULT value that indicates whether the function succeeded. ATL and VB transform a COM function that returns HRESULT to a function that returns the parameter marked with [out, retval] in the IDL. If the wrapper function call for ATL or VB finds that the “raw” COM call returned an error, then the wrapper function throws an exception to indicate the error. This parameter of DllImportAttribute indicates whether you want this kind of function transformation to occur. You would rarely want this transformation for P/Invoke, so the default of false is proper.

  • SetLastError— mThis is another Boolean parameter. If it is true, it causes a call to the Win32 function SetLastError before returning from the method. If you need to retrieve the error set by this unmanaged code, you can call the static method Marshal.GetLastWin32Error. The error code that was returned by the last managed method called will be returned. Listing 7.6 shows an example of using this option to call the LogonUser Win32 API.

Listing 7.6. DllImport Specification for a Group of Functions That Support LogonUser
struct NativeMethods
{
    [DllImport("advapi32.dll", SetLastError=true)]
    public static extern bool LogonUser(string lpszUsername,
                                        string lpszDomain,
                                        string lpszPassword,
                                        int dwLogonType,
                                        int dwLogonProvider,
                                        out IntPtr phToken);
    [DllImport("Kernel32.dll")]
    public static extern uint FormatMessage(
        uint dwFlags,        // source and processing options
        IntPtr lpSource,    // message source
        uint dwMessageId,    // message identifier
        uint dwLanguageId,    // language identifier
        StringBuilder lpBuffer,    // message buffer
        uint nSize,         // maximum size of message buffer
        IntPtr arguments    // array of message inserts
        );
    [DllImport("Kernel32.dll")]
    public static extern int CloseHandle(IntPtr token);
}

Using the specifications in Listing 7.6, you can call LogonUser. If it returns false (indicating that the logon was unsuccessful), you can call Marshal.GetLastWin32Error to find out why. (You probably did not supply the correct password.) You can also pass the value returned from Marshal.GetLastWin32Error to FormatMessage (another P/Invoke call to unmanaged code to get a human-readable string explaining what the error was). By specifying SetLastError, you are assured that the error code returned will be applicable to the method that you tried to call.

Marshaling

Marshaling in the classical sense is the process of packing one or more items into a data buffer for transport over a communication channel (serial, network, and so on). Its companion process, demarshaling, takes that data stream and converts it back into its constituent parts. .NET interop does not use a traditional communication channel; rather, a layer is inserted between your managed code and your unmanaged code that translates your arguments and return values to the correct format. Look at the following declaration:

[DllImport("unmanaged.dll")]
public static extern int AddInteger(int a, int b);

You can call as follows:

int retInt = Native.AddInteger(1, 2);
Console.WriteLine("AddInteger after: {0} ", retInt);

The preceding snippet is part of a project built to exercise some of the P/Invoke marshaling in the Marshal directory.

To interop, you do not need to have the source available, but it is available in this case. If you check the debug option to allow stepping into unmanaged code, you can step into the DLL code. The AddInteger function looks like this:

UNMANAGED_API int AddInteger(int a, int b)
{
    return a + b;
}

This is a simple example because int in unmanaged code is similar to int in managed code. Little if any true marshaling is taking place. It is possible to interop with many functions with this kind of ease.

Marshaling Simple Value Types

For simple value types, interop is easy. Table 7.1 shows how to specify a function or marshal simple value types.

Table 7.1. Simple Value Marshaling Translation
Unmanaged Type Managed Type
unsigned char byte
short short
unsigned short ushort
int int
unsigned int uint
long int
unsigned long uint
DWORD uint
char char
float float
double double

If the arguments of the unmanaged function are of the type in the first column, then you declare the managed function to have the type of the second column. Notice that with C#, a long is 64 bits. If you were to specify a long when the argument for the unmanaged function was a long, the result would be too many bytes on the unmanaged end.

If you need to pass the address of a simple value type so that it can be filled in by the unmanaged function, add the keyword out to the declaration as follows:

[DllImport("unmanaged.dll")]
public static extern void FloatOutFunc(float a, out float o);

This function passes the address to a float to the unmanaged function. If you need to pass information in both directions, use the keyword ref.

Marshaling Strings

The next sample (still from the Marshal directory) requires you to marshal a string between managed and unmanaged code. The .NET Framework “knows” about strings, so the declaration is simple and straightforward:

[DllImport("unmanaged.dll")]
public static extern int StringLength(string str);

Calling and using the function is equally as simple:

string str = "This is a test";
int retLength = Native.StringLength(str);
Console.WriteLine("String after: {0} ", retLength);

This unmanaged function takes in a string and returns the length of the string. You know that a string in C# is a 16-bit per character Unicode sequence. The default for marshaling strings is to marshal all strings as 8-bit per character null-terminated char arrays (char *). By default, the runtime is converting string to a char * array. The signature of the unmanaged code is as follows:

UNMANAGED_API int StringFunction(LPCSTR str);

If the unmanaged code that you were trying to call took a Unicode sequence (or at least a wchar_t * array), then the declaration of the function would look like this:

[DllImport("unmanaged.dll")]
public static extern int UnicodeStringLength(
     [MarshalAs(UnmanagedType.LPWStr)] string str);

or this:

[DllImport("unmanaged.dll", CharSet=CharSet.Unicode)]
public static extern int UnicodeStringLength(string str);

You call the function just as you called the ANSI string length function. The signature of the unmanaged function looks like this:

UNMANAGED_API int UnicodeStringLength(wchar_t * str);

Table 7.2 summarizes marshaling for string arguments.

Table 7.2. String Marshaling Translation
Marshal Description Unmanaged String Type Managed String Type
Return value char * String
Value char * String
Reference char * StringBuilder

You have seen examples of passing a string by value. The other two marshaling contexts require some explanation.

The declaration for returning a string looks familiar:

[DllImport("unmanaged.dll")]
public static extern string ConcatString(string a, string b);

Using the function is the same as you would expect; however, the implementation of the unmanaged function might require some modification. Because the .NET runtime assumes that memory allocation is done with CoTaskMemAlloc, it calls CoTaskMemFree after the string has been copied to the result string. The implementation needs to look like Listing 7.7.

Listing 7.7. Implementation of an Unmanaged Function That Allocates Memory
UNMANAGED_API char* ConcatString(char* a, char* b)
{
    size_t size = strlen(a) + strlen(b) + 1;

    // CoTaskMemAlloc must be used instead of the new operator
    // because code on the managed side will call Marshal.FreeCoTaskMem
    // to free this memory.

    char* buffer = (char*)CoTaskMemAlloc( size * sizeof(char) );

    strcpy(buffer, a);
    strcat(buffer, b);

    return buffer;
}

If you want to use P/Invoke to interop with a function that uses new or malloc, a clean solution doesn't really exist. If you have access to the source of the DLL, you could write a small DLL that wraps your function. The code for this function would look something like Listing 7.8.

Listing 7.8. Implementation of a Wrapper Around an Unmanaged Function That Allocates Memory
UNMANAGED_API char* WrapperConcatString(char* a, char* b)
{
    size_t size = strlen(a) + strlen(b) + 1;

    // This is assuming that the source to ConcatString is unavailable
    // The function allocates enough memory to hold the concatenation of
    // a and b. This is a stub function that the managed code calls instead
    // of directly calling ConcatString because we need to return memory
    // that is allocated with CoTaskMemAlloc.

    char* buffer = (char*)CoTaskMemAlloc( size * sizeof(char) );

    char* temp = ConcatString(a, b);

    strcpy(buffer, temp);
    delete [] temp;

    return buffer;
}

This wrapper function assumes that the ConcatString function allocates memory with new []. When the string is returned from ConcatString, it is copied to a buffer that is allocated with CoTaskMemFree. The intermediary buffer is deleted, and the pointer to the buffer is returned. The pointer is then freed with CoTaskMemFree after the contents are copied to the result string.

Another instance of marshaling strings listed in Table 7.2 that requires some explanation is when the string is passed by reference to unmanaged code. This implies that the unmanaged code is to fill in the string with some value. You need to use the StringBuilder class to pre-allocate a memory buffer large enough to hold the string that is expected from the function. The declaration would look like the following:

[DllImport("unmanaged.dll")]
public static extern void ConcatOutString(string a, string b, StringBuilder o);

To call the function, you would use code like the following:

StringBuilder sbr = new StringBuilder(128);
Native.ConcatOutString(a, b, sbr);
Console.WriteLine("After String Out Concat: {0} ", sbr);

To call an unmanaged function that passes back a string in one of the arguments, you need to allocate space to hold the string. The preceding snippet of code allocates space for 128 bytes. This buffer is then passed to the unmanaged code. The runtime converts the StringBuilder instance to a string pointer into which memory can be copied. The implementation for this type of function would look something like this:

UNMANAGED_API void ConcatOutString(char* a, char* b, char * o)
{
    size_t size = strlen(a) + strlen(b) + 1;
    strcpy(o, a);
    strcat(o, b);
}

Notice that the last argument (char *o) is large enough to contain the result of the concatenation of both strings. In addition, if Unicode characters are used in the unmanaged function, then the declaration requires that each string be decorated with the [MarshalAs(UnmanagedType.LPWStr)], or that the character set of the function be declared as CharSet.Unicode.

Marshaling Structures

A specific structure is often either passed to the unmanaged code or passed back from the unmanaged code. Of course, a structure in unmanaged code is different from a structure in managed code; therefore, a structure needs to be marshaled when passed to or from unmanaged code. Little needs to be done to declare a function that writes to or reads from managed structures. Most of the work is declaring the structure in such a way in managed code that it mimics the layout in unmanaged code. By default, managed code makes no guarantees as to the layout of the data in a structure. In fact, it is more likely that the final position of a field in a structure is more determined by its size than how it is specified in the declaration and definition. In unmanaged code, particularly 'C' code, the declaration and definition almost exclusively determine the layout of a structure. For example, look at the structure in Listing 7.9.

Listing 7.9. Simple Raw Structure
public struct TestStruct
{
    public byte a;
    public short b;
    public int c;
    public byte d;
    public short e;
    public int f;
}

It is more likely that the layout of this structure in memory will cluster fields a and d, b and e, and c and f together. You would not want to pass this to unmanaged code. For this reason, an attribute is available that can specify the layout of a structure. Listing 7.10 shows the same structure now specifying the layout.

Listing 7.10. Structure with a Specific Layout
[StructLayout(LayoutKind.Sequential)]
public struct TestStruct
{
    public byte a;
    public short b;
    public int c;
    public byte d;
    public short e;
    public int f;
}

Now the runtime will not move the fields around and the order will be a, b, c, d, e, and f, as expected for an unmanaged structure. When passing structures to and from unmanaged code, you should always specify the StructLayoutAttribute; otherwise, the memory layout will be unpredictable. For structures that are marshaled with P/Invoke, this marshaling seems to be automatic LayoutKind.Sequential. (You shouldn't count on this fact, however. For classes in which a layout attribute is not attached to the class, an exception is thrown: System.Runtime.InteropServices.MarshalDirectiveException. Additional information: Cannot marshal parameter #1: The type definition of this type has no layout information).

After you have the marshaling characteristics specified, you can call the unmanaged code and pass in a managed structure, as shown in Listing 7.11.

Listing 7.11. Calling an Unmanaged Function with a Structure Argument
Native.TestStruct s = new Native.TestStruct();
s.a = 0x11;
s.b = 0x22;
s.c = 0x33;
s.d = 0x44;
s.e = 0x55;
s.f = 0x66;
Native.TestStructFunction(ref s);

In this sample, the structure is being passed by reference so that the called function can modify the structure and pass it back. If the structure is passed by value, then all modifications in the called function are ignored.

As a slightly more complicated example, the following declares a function that takes two complex numbers, adds them together, and returns the result as a return value. The structure looks like Listing 7.12.

Listing 7.12. A Complex Number to Be Passed to Unmanaged Code
[StructLayout(LayoutKind.Sequential)]
public struct Complex
{
    public Complex(float re, float im)
    {
        this.re = re;
        this.im = im;
    }
    float re;
    float im;
}

The declaration of the function looks like this:

[DllImport("unmanaged.dll")]
public static extern Complex ComplexAdd(Complex a, Complex b);

Again, with little code, you can pass instances of structures that are defined in managed code to unmanaged code.

Not only can the layout be controlled with an attribute attached to the structure, but the marshaling of individual fields of the structure can also be controlled with attributes. For example, you could have a string as a field in a structure. That field could be controlled with marshaling attributes. For example, you could have a function that takes the following structure:

typedef struct _StructStringRef
{
    wchar_t *a;
    char *b;
}

This structure is particularly tricky because it contains both an ANSI string and a Unicode string. You cannot simply specify a CharSet that would be used to marshal the whole class. You must specify how each string in managed space will be marshaled. You end up with the structure declaration in managed space that is shown in Listing 7.13.

Listing 7.13. A Structure with ANSI and Unicode Strings
[StructLayout(LayoutKind.Sequential)]
public struct StructStringRef
{
    public StructStringRef (string a, string b)
    {
        this.a = a;
        this.b = b;
    }
    [MarshalAs( UnmanagedType.ByValTStr)]
    public string a;
    [MarshalAs( UnmanagedType.ByValTStr)]
    public string b;
}

Now you can call an unmanaged function that takes this structure as follows:

Native.StructStringRef sr = new Native.StructStringRef("Testing.",
                                                       "One, Two, Three.");
Native.TestStructStringRef(sr);

This sample shows an example of specifying a marshaling type to a specific field. You can specify any one of the marshaling types to individual fields as appropriate.

Marshaling Arrays

It is possible to marshal arrays of all of the simple value types that have been discussed so far, as well as arrays of strings. You can call a function that takes an array of integers with the following declaration:

[DllImport("unmanaged.dll")]
public static extern int IntArrayFunction(int [] ia, int length);

Now you can call this function with the following code:

int[] ia = {1, 2, 3, 4} ;
Console.WriteLine("Int array before: {0} ", ia.Length);
retInt = Native.IntArrayFunction(ia, ia.Length);
Console.WriteLine("Int array after: {0} ", retInt);

The signature for the unmanaged implementation of this function looks like this:

UNMANAGED_API int IntArrayFunction(int *pia, int length);

All of the value types listed in Table 7.1 can be easily marshaled in the same way that has been shown for int. Even string arrays can be passed to an unmanaged function:

[DllImport("unmanaged.dll")]
public static extern int StringArrayFunction(string [] sa, int length);

The arrays are not restricted to one dimension. Listing 7.14 shows the declaration for a function that takes two two-dimensional matrices in and outputs an array that is the result of multiplying the two input matrices.

Listing 7.14. Declaring a Function That Multiplies Two Matrices
 [DllImport("unmanaged.dll")]
public static extern void MatrixMultiplyFunction(int [,] a,
                                                 int [,] b,
            [Out] int [,] c,
            int x, int y, int z);

It is easy to call unmanaged code using this signature. You simply allocate the arrays as would seem appropriate to the language. Listing 7.15 shows one possible call to the multiplication routine shown in Listing 7.14.

Listing 7.15. Calling an Unmanaged Array Multiplication Function
int [,] am = new int [2,2] {{1,-2} ,{3,4} } ;
int [,] bm = new int [2,3] {{5,4,-2} ,{-3,0,1} } ;
int [,] cm = new int [2,3];
Native.MatrixMultiplyFunction(am, bm, cm, 2, 2, 3);

You can see that this can easily be extended to higher order dimensions.

It is also possible to marshal arrays of structures. To illustrate this, you can create an unmanaged function that takes an array of complex numbers, averages them, and returns the result. The declaration would look like this:

[DllImport("unmanaged.dll")]
public static extern Complex ComplexAverage(Complex [] a, int length);

Calling this function would look something like what is shown in Listing 7.16.

Listing 7.16. Returning the Average of an Array of Complex Numbers
Native.Complex [] ca = new Native.Complex [] {new Native.Complex(1,2),
                                              new Native.Complex(3,4),
                                              new Native.Complex(5,6),
                                              new Native.Complex(7,8)} ;
Native.Complex cr = Native.ComplexAverage(ca, ca.Length);
Console.WriteLine("Complex average: {0}  {1} ", cr.Real, cr.Imaginary);

Callbacks

You can pass a delegate from managed code to unmanaged code. The marshaled delegate will be treated by the unmanaged code as if it were a function pointer. This effectively allows unmanaged code to callback into managed code. Unfortunately, as of the first version of the .NET Framework, it is not possible to do the reverse (pass a function pointer back from unmanaged code that managed code could callback into unmanaged code). Perhaps this functionality will be available in the next version.

Note

The programs for this section are available in the Callback directory as a single solution called Callback.


For the first simple case, an array of integers will be passed to unmanaged code. In addition, a delegate will be called from unmanaged code for each member of the array. The input arguments to the callback will be the index of the array and the value that was at that index. The declarations that are required for this callback to work are shown in Listing 7.17.

Listing 7.17. Declarations That Are Required for Unmanaged Code to Callback into Managed Code
public delegate void IntegerEchoCallback(int index, int integerValue);
[DllImport("unmanaged.dll")]
. . .
public static extern void IntegerCallback(int [] intArray,
                                          int nints,
                                          IntegerEchoCallback callback);

Calling the function requires the construction of the delegate and simply calling the function as shown in Listing 7.18.

Listing 7.18. Illustrating a Callback That Echoes the Members of an Integer Array
static void IntegerEcho(int index, int integerValue)
{
    Console.WriteLine("{0}  - {1} ", index, integerValue);
}
. . .
int [] ia = new int [] {1, 2, 3, 4, 5} ;
IntegerEchoCallback integerCallback = new IntegerEchoCallback(IntegerEcho);
Native.IntegerCallback(ia, ia.Length, integerCallback);

This is a simple example. It is possible to put directives on the callback to control the marshaling behavior. One example would be doing what is shown in Listings 7.17 and 7.18, but with strings. Listing 7.19 shows the declarations to use for a string callback.

Listing 7.19. Declarations for a Callback Involving Strings
public delegate void StringEchoCallback(int index,
                                        [MarshalAs(UnmanagedType.LPWStr)]
                                        string str);
. . .
[DllImport("unmanaged.dll", CharSet=CharSet.Unicode)]
public static extern void StringCallback(string [] stringArray,
                                         int nstrings,
                                         StringEchoCallback callback);

Notice that the delegate has a specific marshaling attribute attached to one of its arguments. Now when the unmanaged code calls back, the argument will be marshaled in reverse. (Here, it's converted from a pointer to a Unicode string to a string class.)

With these declarations in place, you can call the unmanaged function, specifying the callback address as shown in Listing 7.20.

Listing 7.20. Calling an Unmanaged Function with a Callback That Echoes Strings
static void StringEcho(int index, string str)
{
    Console.WriteLine("{0}  - {1} ", index, str);
}
. . .
string [] sa = new string [] {"This", "is", "a", "test"} ;
StringEchoCallback stringCallback = new StringEchoCallback(StringEcho);
Native.StringCallback(sa, sa.Length, stringCallback);

Using VC++ Managed Extensions

This chapter originally indicated that it would only show samples in C#, but VC++ has two different models that can be used for interop. The first uses the VC++ version of DllImport. You will see some samples using VC++ just for completeness, but except for the syntax differences, VC++ is much like C#. The other model is IJW (It-Just-Works). This model does not require DllImport, and the legacy DLL is linked into the application much like you have always linked in DLLs in the past. IJW has many advantages and disadvantages that will become clear as you read the following sections.

Using VC++ DllImport

Listing 7.21 shows some lines from the VCMarshal solution to give you an idea of the flavor of the VC++ implementation of DllImport.

Listing 7.21. Using DllImport with VC++
namespace Native
{
    [DllImport("unmanaged")]
    extern "C" int AddInteger(int a, int b);
    [DllImport("unmanaged")]
    extern "C" int StringLength(String* str);
    [DllImport("unmanaged", CharSet=CharSet::Unicode)]
    extern "C" int UnicodeStringLength(String* str);

    [DllImport("unmanaged.dll")]
    extern "C" unsigned char ByteFunc(unsigned char a);
    [DllImport("unmanaged.dll")]
    extern "C" short ShortFunc(short a);
    [DllImport("unmanaged.dll")]
    extern "C" unsigned short WordFunc(short a);
    [DllImport("unmanaged.dll")]
    extern "C" unsigned int UintFunc(unsigned int a);
    [DllImport("unmanaged.dll")]
    extern "C" int LongFunc(int a);
    [DllImport("unmanaged.dll")]
    extern "C" unsigned int UnsignedLongFunc(unsigned int a);
    [DllImport("unmanaged.dll")]
    extern "C" char CharFunc(char a);
    [DllImport("unmanaged.dll")]
    extern "C" float FloatFunc(float a);
    [DllImport("unmanaged.dll")]
    extern "C" double DoubleFunc(double a);

. . .
}

int _tmain(void)
{
    Console::WriteLine(S"Hello World");
    int retInt = Native::AddInteger(1, 2);
    Console::WriteLine(S"AddInteger after: {0} ", __box(retInt)); 

    String *str = S"This is a test";
    int retLength = Native::StringLength(str);
    Console::WriteLine(S"String length: {0} ", __box(retLength));

    retLength = Native::UnicodeStringLength(str);
    Console::WriteLine(S"Unicode string length: {0} ", __box(retLength));

    unsigned char byteRet = Native::ByteFunc(10);
    short shortRet = Native::ShortFunc(10);
    unsigned short ushortRet = Native::WordFunc(10);
    unsigned int uintRet = Native::UintFunc(10);
    int longRet = Native::LongFunc(10);
    unsigned int ulongRet = Native::UnsignedLongFunc(10);
    char charRet = Native::CharFunc((char)10);
    float floatRet = Native::FloatFunc(10);
    double doubleRet = Native::DoubleFunc(10);

Notice that, unlike C#, char and long are compatible (size-wise) with their unmanaged counterparts. In addition, most of what used to be objects are now pointers to objects (string a becomes String *a). The values supplied as arguments to Console::WriteLine need to be __box'd to compile and display correctly. For the most part, all of these modifications are related to syntax and are not much different from using DllImport and P/Invoke with C#.

Using VC++ IJW

Traditionally, VC++ applications could link in DLLs using an export library that contained enough information about the DLL to allow it to be loaded and the required function called. Information about the signature of the function to be called was contained in a header file that described each of the functions in the DLL. This method of importing DLLs still works, but now, the C++ code that is doing the importing can be managed.

It's unnecessary to use DllImport when using IJW. One simple include that describes the signatures of the functions that you want to call is all that is required. The header file might be part of an existing project if you have the source, or it might be a system file that describes the function, such as for MessageBox (which is declared in WinUser.h). You will need to find the header file that is appropriate for the function that you want to call. If you know the signature for the function that you want to call, it might be possible to omit a header describing the function. (Many of these headers describe more than you want to know about everything you don't care about, and mixed in there is the description of the function that you want to call. If you can neatly describe your function without the associated header file, then do so.) For the sample, just include this:

#include "unmanaged.h"

If your function only contains simple value types with a one-to-one mapping between the type in managed code and the type in unmanaged code, it is probably a blittable type and little marshaling is taking place. Blittable types can be copied from one location to the other with no translation or transformation required. Listing 7.22 shows a call to a function where the arguments and the return are simple blittable types.

Listing 7.22. Using IJW in VC++ with Simple Types
int retInt = AddInteger(1, 2);
Console::WriteLine(S"AddInteger after: {0} ", __box(retInt));

Notice that the arguments 1 and 2 are passed to the function and the addition of the two are returned, which is also an integer. No marshaling is required. You want to do the same thing with a string (add two strings together). Listing 7.23 shows how to do this with IJW.

Listing 7.23. Using IJW in VC++ with a String
String *str = S"This is a test";
IntPtr sPtr = Marshal::StringToHGlobalAnsi(str);
int retLength = StringLength((char*)sPtr.ToPointer());
Console::WriteLine(S"String length: {0} ", __box(retLength));
Marshal::FreeHGlobal(sPtr);

The code that is involved to do a simple operation like this can be overwhelming. This is one of the drawbacks of using this method of interop. The code is substantial, and it's highly possible to make a mistake that will cause a leak or a crash of your application. At the same time, because you are doing all of this work explicitly, you have more control and are able to see the exact cost of marshaling. Other languages and methods of interoperation hide this level of detail.

First, you create and initialize the String. This is mandatory because the marshaling requires a String input to properly marshal. Notice that the String does not directly participate in the marshaling process; the data is simply copied from the String and then it is left alone. Next, you marshal the data and allocate memory to handle the marshaled data. The Marshal class has several methods that handle the marshaling. Now that the String has been marshaled, you can pass it to the unmanaged function. It is necessary to pull a pointer from the marshaled data (void *) and cast it to (char *) for the unmanaged function. Finally, you print the results of the function call and free the allocated memory. Requiring unmanaged code to explicitly free allocated memory is counter to the idea of managed code, but that is the way things are done for interop using IJW. If you do not free the memory, your program will leak memory.

Just for variety, a simple sample of a function that returns a String that is the result of the concatenation of two input Strings is included next. Listing 7.24 shows the essential parts of calling this function.

Listing 7.24. Using IJW in VC++ to Concatenate Two Strings
IntPtr aPtr = Marshal::StringToHGlobalUni(a);
IntPtr bPtr = Marshal::StringToHGlobalUni(b);
String *result = ConcatUnicodeString((wchar_t*)aPtr.ToPointer(),
                                     (wchar_t*)bPtr.ToPointer());
Console::WriteLine(S"After Unicode String Concat: {0} ", result);
Marshal::FreeHGlobal(aPtr);
Marshal::FreeHGlobal(bPtr);

To show that it's possible to marshal different types of strings, this sample marshals the Strings to Unicode rather than ANSI. Notice that you still allocate memory and marshal as before, and you still have to free the memory afterward, but you don't allocate or explicitly do any marshaling with the return value, which is a String.

The following are the advantages of IJW:

  • DllImport is not needed. You do not need to duplicate the signature of the unmanaged function that you want to call. Just use the existing header file and link the import library.

  • Using IJW is slightly faster. With DllImport, every call requires a check to see if pinning the data is required or if the data needs to be copied. Because all of this work is done explicitly by the developer in IJW, these checks are not required.

  • Because nothing is hidden from the developer, it is possible to see exactly where performance is being hurt and possibly come up with solutions.

  • For some calls, it might be possible to marshal the data once and use it many times. This is a clear win in favor of IJW. It is not possible for DllImport to “cache” the marshaled data for subsequent calls or to know that the same data is to be passed to another function. The work to marshal the data can be saved for that call in IJW.

IJW also has some disadvantages:

  • Doing the marshaling for every argument in most function calls is tedious and prone to error. It also significantly muddies up the code so that it is sometimes hard to understand the flow of the program.

  • An extra call to ToPointer() is required to maintain portability between 32-bit and 64-bit operating systems.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
18.227.190.93