Interop in C#

One of the important capabilities of C# is being able to interoperate with existing code, whether it’s based on COM or is in a native DLL. This chapter provides a brief overview of how interop works.

1. Using COM Objects

To call a COM object, the first step is to define a proxy (or wrapper) class that specifies the functions in the COM object, along with additional information. This is a fair amount of work, which you can avoid in most cases by using the tlbimp utility. This utility reads the COM typelib information and then creates the proxy class automatically. This will work in many situations, but if you need more control over marshalling, you may have to write the proxy class by hand. In this case, you use attributes to specify how marshalling should be performed.

Once the proxy class is written, it’s used like any other .NET class, and the runtime handles the ugly stuff.

2. Being Used by COM Objects

The runtime also lets .NET objects be used in place of COM objects. The tlbexp utility creates a typelib that describes the COM objects so that other COM-based programs can determine the object’s interface, and the regasm utility registers an assembly so it can be accessed through COM. When COM accesses a .NET class, the runtime creates the .NET object, fabricating what­ever COM interfaces are required and marshalling the data between the .NET world and the COM world.

3. Calling Native DLL Functions

C# can call C functions written in native code through a runtime feature known as platform invoke. The DllImport attribute specifies the file that the function is located in and can also

specify the default character marshalling. In many cases, that attribute is all you need, but if a value is passed by reference, you can specify ref or out to tell the marshaller how to pass the value. Here’s an example:

using System;

using System.Runtime.InteropServices;

class Test {

[DllImport(“user32.dll”)]

public static extern int MessageBox(IntPtr h, string m, string c, int type);

public static void Main()

{

int retval = MessageBox(IntPtr.Zero, “Hello”, “Caption”, 0);

}

}

When this code runs, a message box will appear. Note that the code uses MessageBox() rather than the ASCII- or Unicode-specific versions; the runtime will automatically use the appropriate function (MessageBoxA() or MessageBoxW()) based on the platform, but you can specify up front exactly which variant to pick.

IntPtr, which is used as the type for the first parameter in the MessageBox() call, represents pointers or handles that are platform-specific. The advantage of IntPtr over raw integral types is that IntPtr is defined to match the pointer size of the underlying platform, making conversion to 64-bit (or wider) platforms much easier.

C# can’t use C++ classes directly; to use such objects, they must be exposed in a .NET- compliant way using the C++/CLI or as COM objects.

3.1. Pointers and Declarative Pinning

It’s common for C-style functions to take pointers as their parameters. Since C# supports pointers in an unsafe context, it’s straightforward to use such functions. This example calls ReadFile() from kernel32.dll:

// file=ReadFileUnsafe.cs

// compile with: csc /unsafe ReadFileUnsafe.cs using System;

using System.Runtime.InteropServices;

using System.Text;

class FileRead

{

const uint GENERIC_READ = 0x80000000;

const uint OPEN_EXISTING = 3;

IntPtr handle;

public FileRead(string filename)

{

// opens the existing file…

handle = CreateFile( filename,

GENERIC_READ,

0,

0,

OPEN_EXISTING,

0,

0);

}

[DllImport(“kernel32”, SetLastError=true)]

static extern IntPtr CreateFile(

string filename,

uint desiredAccess,

uint shareMode,

uint attributes,                          // really SecurityAttributes pointer

uint creationDisposition,

uint flagsAndAttributes,

uint templateFile);

// SetLastError =true is used to tell the interop layer to keep track of

//underlying Windows errors

[DllImport(“kernel32”, SetLastError=true)]

static extern unsafe bool ReadFile(

IntPtr hFile,

void* lpBuffer,

int nBytesToRead,

int* nBytesRead,

int overlapped);

public unsafe int Read(byte[] buffer, int count)

{

int n = 0;

fixed (byte* p = buffer)

{

ReadFile(handle, p, count, &n, 0);

}

return n;

}

}

class Test {

public static void Main(string[] args)

{

FileRead fr = new FileRead(args[0]);

byte[] buffer = new byte[128];

ASCIIEncoding e = new ASCIIEncoding();

// loop through, read until done…

Console.WriteLine(“Contents”);

while (fr.Read(buffer, 128) != 0)

{

Console.Write(“{0}”, e.GetString(buffer));

}

}

}

In this example, the FileRead class encapsulates the code to read from the file. It declares the functions to import and the unsafe Read function.

Calling the ReadFile() function presents a dilemma. The byte[] array buffer is a managed variable, which means the garbage collector can move it at any time. But ReadFile() expects that the buffer pointer passed to it won’t move during the call to ReadFile().

The fixed statement bridges the two worlds. It “pins” the byte[] buffer by setting a flag on the object so the garbage collector won’t move the object if a collection occurs inside the fixed block. This makes it safe to pass the pointer to the buffer to ReadFile(). After the call, the flag is cleared and execution continues.

This approach is nice in that it has low overhead—unless a garbage collection occurs while the code is inside the fixed block, which is unlikely.

This sample works fine, but the class is subject to the usual constraints on code written with unsafe. See Chapter 38 for more information.

A Safe Version

Since pointer support isn’t required for .NET languages, other languages need to be able to call functions such as ReadFile() without using pointers. The runtime provides a considerable amount of support to make the marshalling from managed to unmanaged types (including pointer types) transparent.

Therefore, you can rewrite the previous example without using unsafe. All that’s required is to change the extern declaration for ReadFile() and the Read() function:

[DllImport(“kernel32”, SetLastError=true)]

static extern bool ReadFile(

IntPtr hFile,

byte[] buffer,

int nBytesToRead,

ref int nBytesRead,

int overlapped);

public int Read(byte[] buffer, int count)

{

int n = 0;

ReadFile(handle, buffer, count, ref n, 0);

return n;

}

In this code, the pointer parameter for the buffer has been changed to a byte[], and the number of characters read is defined as a ref int instead of an int*.

In this version, the runtime will do the pinning of the buffer automatically rather than having to be explicit, and because unsafe isn’t required, this version isn’t subject to the same restrictions as the previous example.

3.2. Structure Layout

The runtime allows a structure to specify the layout of its data members by using the StructLayout attribute. By default, the layout of a structure is automatic, which means the runtime is free to rearrange the fields. When using interop to call into native or COM code, you require better control.

When specifying the StructLayout attribute, you can specify three kinds of layout using the LayoutKind enum:

  • Auto, where the runtime chooses the appropriate way to lay out the members.
  • Sequential, where all fields are in declaration order. For sequential layout, you can use the Pack property to specify the type of packing.
  • Explicit, where every field has a specified offset. In explicit layout, the StructOffset attribute must be used on every member to specify the offset in bytes of the element.

Additionally, you can specify the CharSet property to set the default marshalling for string data members.

By default, the C# compiler sets sequential layout for all structs.

3.3. Calling a Function with a Structure Parameter

To call a function with a structure parameter, you need to define the structure with the appro­priate parameters. This example shows how to call GetWindowPlacement():

using System;

using System.Runtime.InteropServices;

struct Point {

public int x;

public int y;

public override string ToString()

{

return(String.Format(“({0}, {1})”, x, y));

}

}

struct Rect {

public int left;

public int top;

public int right;

public int bottom;

public override string ToString()

{

return(String.Format(“({0}, {1})\n   ({2}, {3})”,

left, top, right, bottom));

}

}

struct WindowPlacement

{

public uint length;

public uint flags;

public uint showCmd;

public Point minPosition;

public Point maxPosition;

public Rect normalPosition;

public override string ToString()

{

return(String.Format(“min, max, normal:\n{0}\n{1}\n{2}”,

minPosition, maxPosition, normalPosition));

}

}

class Window

{

[DllImport(“user32”)]

static extern IntPtr GetForegroundWindow();

[DllImport(“user32”)]

static extern bool GetWindowPlacement(IntPtr handle, ref WindowPlacement wp);

public static void Main()

{

IntPtr window = GetForegroundWindow();

WindowPlacement wp = new WindowPlacement();

wp.length = (uint) Marshal.SizeOf(wp);

bool result = GetWindowPlacement(window, ref wp);

if (result)

{

Console.WriteLine(wp);

}

}

}

3.4. Fixed-Size Buffers

It’s common practice for a C language application to use fixed-sized buffers to store data, both in memory and on disk. A fixed-sized buffer of some primitive type such as int or char is easy and quick to populate—the data that’s needed to populate the entire buffer can be copied over the entire buffer using the C runtime memcpy or an equivalent command, populating all the elements in the buffer in a single operation. The simplicity and speed of accessing fixed-sized buffers comes at a considerable cost in terms of code correctness and security and is notorious as the source of many serious security breaches, which is why .NET uses a different model.

In the .NET model, all access to elements in a buffer is checked, and because arrays are reference types, an array declared as part of a structure doesn’t physically live inside the struc­ture. Instead, a reference to the array is placed inside the structure, which points to the location of the array on the heap. This means that a memcpy or equivalent wouldn’t work (even if legal), as the memory inside the buffer isn’t laid out in memory correctly. This causes headaches and inefficiencies when dealing in heavy interop scenarios.

In C# 2.0, it’s now possible to overcome this problem with an extension to the fixed keyword, which allows arrays to be declared as fixed-sized inside unsafe code blocks. Fixed arrays will typically be part of a structure that’s passed to a native API. The Windows API function GetVersionEx is a good example of an API where you can use fixed-sized buffers. The single parameter that’s passed to the function is a pointer to an OSVERSIONINFO structure defined in C as follows:

typedef struct _OSVERSIONINFO {

DWORD dwOSVersionInfoSize;

DWORD dwMajorVersion;

DWORD dwMinorVersion;

DWORD dwBuildNumber;

DWORD dwPlatformId;

TCHAR szCSDVersion[128];

} OSVERSIONINFO;

Although you could call this function without fixed-sized buffers by using marshalling attributes, you’ll receive a performance penalty. To convert this structure to a C# struct, use the following declaration:

unsafe struct OSVERSIONINFO {

public uint dwOSVersionInfoSize;

public uint dwMajorVersion;

public uint dwMinorVersion;

public uint dwBuildNumber;

public uint dwPlatformId;

public fixed char szCSDVersion[128];

}

You can now call the GetVersionEx function using the OSVERSIONINFO buffer without any marshalling between the C# code and Windows API:

[DllImport(“Kernel32.dll”, CharSet = CharSet.Unicode)]

static extern bool GetVersionEx(ref OSVERSIONINFO lpVersionInfo);

unsafe static void Main(string[] args)

{

OSVERSIONINFO versionInfo = new OSVERSIONINFO();

versionInfo.dwOSVersionInfoSize = (uint)sizeof(OSVERSIONINFO);

bool res = GetVersionEx(ref versionInfo);

Console.WriteLine(Marshal.PtrToStringUni(new IntPtr(versionInfo.szCSDVersion)));

}

As with all unsafe code, this involves a risk, and if the size of memory blocks don’t line up correctly, you have the potential for application crashes and security vulnerabilities.

3.5. Hooking Up to a Windows Callback

The Win32 API sometimes uses callback functions to pass information to the caller asynchro­nously. The closest analogy to a callback function in C# (and in the CLR) is a delegate, so the runtime interop layer can map a delegate to a callback. Here’s an example that does this for the SetConsoleHandler() API (the one used to catch Ctrl+C):

using System;

using System.Threading;

using System.Runtime.InteropServices;

class ConsoleCtrl {

public enum ConsoleEvent {

CTRL_C = 0,                    // From wincom.h

CTRL_BREAK = 1,

CTRL_CLOSE = 2,

CTRL_LOGOFF = 5,

CTRL SHUTDOWN = 6

}

public delegate void ControlEventHandler(ConsoleEvent consoleEvent);

public event ControlEventHandler ControlEvent;

// save delegate so the GC doesn’t collect it.

ControlEventHandler eventHandler;

public ConsoleCtrl()

{

// save this to a private var so the GC doesn’t collect it…

eventHandler = new ControlEventHandler(Handler);

SetConsoleCtrlHandler(eventHandler, true);

}

private void Handler(ConsoleEvent consoleEvent)

{

if (ControlEvent != null)

ControlEvent(consoleEvent);

}

[DllImport(“kernel32.dll”)]

static extern bool SetConsoleCtrlHandler(ControlEventHandler e, bool add);

}

class Test

{

public static void MyHandler(ConsoleCtrl.ConsoleEvent consoleEvent)

{

Console.WriteLine(“Event: {0}”, consoleEvent);

}

public static void Main()

{

ConsoleCtrl cc = new ConsoleCtrl();

cc.ControlEvent += new ConsoleCtrl.ControlEventHandler(MyHandler);

Console.WriteLine(“Enter ‘E’ to exit”);

Thread.Sleep(15000); // sleep 15 seconds

}

}

The ConsoleCtrl class encapsulates the API function. It defines a delegate that matches the signature of the Win32 callback function and then uses that as the type passed to the Win32 function. It exposes an event that other classes can hook up to.

The one subtlety of this example has to do with the following line:

ControlEventHandler eventHandler;

This line is required because the interop layer will pass a pointer to the delegate to the Win32 function, but the garbage collector has no way to know that pointer exists. If the delegate isn’t stored in a place the garbage collector can find, it’s collected the next time the garbage collector runs, which is bad.

3.6. Design Guidelines

The following sections highlight a few guidelines to help you decide what method of interop to use and how to use it.

C# or C++?

The two options for doing interop with existing C libraries are calling functions directly from C# using platform invoke and using C++/CLI to encapsulate the C functions in a nice managed class written in C++.

Which is the better choice depends upon the interface being called. It’s easy to tell by how much effort it takes to get it working. If it’s straightforward, then doing it in C# is easy. If you find yourself wondering how to do that in C#, or you start using a lot of unsafe code, then it’s likely that doing it using the Managed Extensions is a better choice. This is especially true for complex interfaces, where a structure contains pointers to other structures or the sizes of a structure aren’t fixed.

In such cases, you’ll need to do a translation from the C-style way of doing things to the .NET-managed way of doing things. This might involve grabbing a value out of a union or walking through a structure of variable size and then putting the values into the appropriate variable on a collection class. Doing such an operation is a lot easier in C++ than it is in C#, and you likely already have working C++ code on which you can base your C# code.

Marshalling in C#

When defining functions in C#, consider the following guidelines:

  • In general, the data marshalling layer does the right thing. Choose the type that’s closest to the type you want.
  • For opaque types (such as pointers) where all you really care about is the size of the variable, just use an IntPtr.
  • To control data marshalling, use the MarshalAs attribute. This is most often used to control string marshalling.
  • Rather than using a pointer type for a parameter, define it using ref or out.
  • Read about data marshalling in the .NET Framework’s developer specifications.
  • If things get ugly, switch to using C++/CLI. Switching to C++/CLI can range from writing a small wrapper that’s then called from C# to using C++/CLI for large portions of an application the optimum technique depends on the skills of the developers available.

Source: Gunnerson Eric, Wienholt Nick (2005), A Programmer’s Introduction to C# 2.0, Apress; 3rd edition.

Leave a Reply

Your email address will not be published. Required fields are marked *