only for RuBoard - do not distribute or recompile Previous Section Next Section

3.13 Interop with Native DLLs

PInvoke, short for Platform Invocation Services, lets C# access functions, structs, and callbacks in unmanaged DLLs. For example, perhaps you wish to call the MessageBox function in the Windows user32.dll:

int MessageBox(HWND hWnd, LPCTSTR lpText, 
               LPCTSTR lpCation, UINT uType);

To call this function, write a static extern method decorated with the DllImportattribute:

using System.Runtime.InteropServices;
class MsgBoxTest {
  [DllImport("user32.dll")]
  static extern int MessageBox(int hWnd, string text, 
                               string caption, int type);
  public static void Main(  ) {
    MessageBox(0, "Please do not press this button again.",
                  "Attention", 0);
  }
}

PInvoke then finds and loads the required Win32 DLLs and resolves the entry point of the requested function. The CLR includes a marshaler that knows how to convert parameters and return values between .NET types and unmanaged types. In this example the int parameters translate directly to four-byte integers that the function expects, and the string parameters are converted to null-terminated arrays of characters using one-byte ANSI characters under Win9x or two-byte Unicode characters under Windows NT, 2000, and XP.

3.13.1 Marshaling Common Types

The CLR marshaler is a .NET facility that knows about the core types used by COM and the Windows API and provides default translations to CLR types for you. The bool type, for instance, can be translated into a two-byte Windows BOOL type or a four-byte Boolean type. You can override a default translation using the MarshalAs attribute:

using System.Runtime.InteropServices;
static extern int Foo([MarshalAs(UnmanagedType.LPStr)]
                      string s);

In this case, the marshaler was told to use LPStr, so it will always use ANSI characters. Array classes and the StringBuilder class will copy the marshaled value from an external function back to the managed value, as follows:

using System;
using System.Text;
using System.Runtime.InteropServices;
class Test {
  [DllImport("kernel32.dll")]
  static extern int GetWindowsDirectory(StringBuilder sb,
                                        int maxChars);
   static void Main(  ) {
      StringBuilder s = new StringBuilder(256);
      GetWindowsDirectory(s, 256);
      Console.WriteLine(s);
   }
}

3.13.2 Marshaling Classes and Structs

Passing a class or struct to a C function requires marking the struct or class with the StructLayout attribute:

using System;
using System.Runtime.InteropServices;
[StructLayout(LayoutKind.Sequential)]
struct SystemTime {
   public ushort wYear; 
   public ushort wMonth;
   public ushort wDayOfWeek; 
   public ushort wDay; 
   public ushort wHour; 
   public ushort wMinute; 
   public ushort wSecond; 
   public ushort wMilliseconds; 
}
class Test {
   [DllImport("kernel32.dll")]
   static extern void GetSystemTime(ref SystemTime t);
   static void Main(  ) {
      SystemTime t = new SystemTime(  );
      GetSystemTime(ref t);
      Console.WriteLine(t.wYear);
   }
}

In both C and C#, fields in an object are located at n number of bytes from the address of that object. The difference is that a C# program finds this offset by looking it up using the field name; C field names are compiled directly into offsets. For instance, in C, wDay is just a token to represent whatever is at the address of a SystemTime instance plus 24 bytes.

For access speed and future widening of a datatype, these offsets are usually in multiples of a minimum width, called the pack size. For .NET types, the pack size is usually set at the discretion of the runtime, but by using the StructLayout attribute, field offsets can be controlled. The default pack size when using this attribute is 8 bytes, but it can be set to 1, 2, 4, 8, or 16 bytes (pass Pack=packsize to the StructLayout constructor), and there are also explicit options to control individual field offsets. This lets a .NET type be passed to a C function.

3.13.3 In and Out Marshaling

The previous Test example works if SystemTime is a struct and t is a ref parameter, but is actually less efficient:

struct SystemTime {...}
static extern void GetSystemTime(ref SystemTime t);

This is because the marshaler must always create fresh values for external parameters, so the previous method copies t when going in to the function and then copies the marshaled t when coming out of the function. By default, pass-by-value parameters are copied in, C# ref parameters are copied in/out, and C# out parameters are copied out, but there are exceptions for the types that have custom conversions. For instance, array classes and the StringBuilder class require copying when coming out of a function, so they arein/out. It is occasionally useful to override this behavior, with the in and out attributes. For example, if an array should be read-only, the in modifier indicates to copy only the array going into the function, and not the one coming out of it:

static extern void Foo([in] int[] array);

3.13.4 Callbacks from Unmanaged Code

C# can not only call C functions but can also be called by C functions, using callbacks. In C# a delegate type is used in place of a function pointer:

class Test {
   delegate bool CallBack(int hWnd, int lParam);
   [DllImport("user32.dll")]
   static extern int EnumWindows(CallBack hWnd, int lParam);
   static bool PrintWindow(int hWnd, int lParam) {
      Console.WriteLine(hWnd);
      return true;
   }
   static void Main(  ) {
      CallBack e = new CallBack(PrintWindow);
      EnumWindows(e, 0);
   }
}

3.13.5 Predefined Interop Support Attributes

The FCL provides a set of attributes you can use to mark up your objects with information used by the CLR marshaling services to alter their default marshaling behavior.

This section describes the most common attributes you will need when interoperating with native Win32 DLLs. These attributes all exist in the System.Runtime.InteropServices namespace.

3.13.5.1 DllImport attribute

Syntax:

[DllImport (dll-name
  [, EntryPoint=function-name]?
  [, CharSet=charset-enum]?
  [, SetLastError=true|false]?
  [, ExactSpelling=true|false]?
  [, PreserveSig=true|false]?
  [, CallingConvention=callconv-enum]?
)] 
(for methods)

The DllImport attribute annotates an external function that defines a DLL entry point. The parameters for this attribute are:

dll-name

A string specifying the name of the DLL.

function-name

A string specifying the function name in the DLL. This is useful if you want the name of your C# function to be different from the name of the DLL function.

charset-enum

A CharSet enum, specifying how to marshal strings. The default value is CharSet.Auto, which converts strings to ANSI characters on Win9x and Unicode characters on Windows NT, 2000, and XP.

SetLastError

If true, preserves the Win32 error info. The default is false.

ExactSpelling

If true, the EntryPoint must exactly match the function. If false, name-matching heuristics are used. The default is false.

PreserveSig

If true, the method signature is preserved exactly as it was defined. If false, an HRESULT transformation is performed.

callconv-enum

A CallingConvention enum, specifying the mode to use with the EntryPoint. The default is StdCall.

3.13.5.2 StructLayout attribute

Syntax:

[StructLayout(layout-enum
  [, Pack=packing-size]?
  [, CharSet=charset-enum]?
  [, Size=absolute-size])?
] 
(for classes, structs)

The StructLayout attribute specifies how the data members of a class or struct should be laid out in memory. Although this attribute is commonly used when declaring structures that are passed to or returned from native DLLs, it can also define data structures suited to file and network I/O. The parameters for this attribute are:

layout-enum

A LayoutKind enum, which can be 1) sequential, which lays out fields one after the next with a minimum pack size; 2) union, which makes all fields have an offset of 0, so long as they are value types; or 3) explicit, which lets each field have a custom offset.

packing-size

An int specifying whether the packing size is 1, 2, 4, 8, or 16 bytes. The default value is 8.

charset-enum

A CharSet enum, specifying how to marshal strings. The default value is CharSet.Auto, which converts strings to ANSI characters on Win9x and Unicode characters on Windows NT, 2000, and XP.

absolute-size

Specifies the size of the struct or class. This has to be at least as large as the sum of all the members.

3.13.5.3 FieldOffset attribute

Syntax:

[FieldOffset (byte-offset)] 
(for fields)

The FieldOffset attribute is used within a class or struct that has explicit field layout. This attribute can be applied to a field and specifies the field offset in bytes from the start of the class or struct. Note that these offsets don't have to be strictly increasing and can overlap, thus creating a union data structure.

3.13.5.4 MarshalAs attribute

Syntax:

[MarshalAs(unmanaged-type
  [, named-parameters])?
] 
(for fields, parameters, return values)

The MarshalAs attribute overrides the default marshaling behavior that the marshaler applies to a parameter or field. The unmanaged-type value is taken from the UnmanagedType enum; see the following list for the permissible values:

Bool
LPStr
VBByRefStr
I1
LPWStr
AnsiBStr
U1
LPTStr
TBStr
I2
ByValTStr
VariantBool
U2
IUnknown
FunctionPtr
I4
IDispatch
LPVoid
U4
Struct
AsAny
I8
Interface
RPrecise
U8
SafeArray
LPArray
R4
ByValArray
LPStruct
R8
SysInt
CustomMarshaler
BStr
SysUInt
NativeTypeMax 
Error
Currency

For a detailed description of how and when to use each of these enum values, as well as other legal named-parameters, see the .NET Framework SDK documentation.

3.13.5.5 In attribute

Syntax:

[In] 
(for parameters)

The In attribute specifies that data should be marshaled into the caller and can be combined with the Out attribute.

3.13.5.6 Out attribute

Syntax:

[Out] 
(for parameters)

The Out attribute specifies that data should be marshaled out from the called method to the caller and can be combined with the In attribute.

only for RuBoard - do not distribute or recompile Previous Section Next Section