Calling Unmanaged Code

This document provides an overview of how to call unmanaged code (such as Windows API functions or functions in native libraries) from your .NET Framework applications.

Introduction to Interoperability

The .NET Framework provides powerful mechanisms for interacting with existing unmanaged codebases. This is crucial for leveraging existing libraries, operating system functionalities, or performance-critical components written in languages like C or C++. The primary technologies for this are Platform Invoke (P/Invoke) and COM Interop.

This guide focuses on P/Invoke, which allows managed code to call functions exported from unmanaged DLLs.

Platform Invoke (P/Invoke)

P/Invoke is a service that enables managed code to call functions implemented in unmanaged libraries. It's a way for your .NET code to directly call functions in native DLLs without requiring a COM wrapper.

The core of P/Invoke involves using the [DllImport] attribute in your managed code.

Declaring a Native Function

You declare a native function in your managed code as if it were a static method. The [DllImport] attribute tells the .NET runtime which DLL contains the function and provides other information for correct invocation.

Tip: Always ensure the native library is accessible in the location where your .NET application is running or in the system's PATH.
using System.Runtime.InteropServices;

public class NativeMethods
{
    [DllImport("kernel32.dll", SetLastError = true)]
    public static extern int MessageBox(
        IntPtr hWnd,
        string lpText,
        string lpCaption,
        uint uType);
}

In this example:

  • kernel32.dll is the native library containing the MessageBox function.
  • SetLastError = true indicates that the function might set the Windows error code, which can be retrieved using Marshal.GetLastWin32Error().
  • The method signature mirrors the unmanaged function's signature as closely as possible.

Common P/Invoke Attributes

Several attributes are used with [DllImport] and for marshalling:

  • [DllImport]: Specifies the DLL name and other import options.
  • [StructLayout]: Controls the memory layout of structures.
  • [MarshalAs]: Explicitly specifies how a parameter or return value should be marshaled.
  • [PreserveSigAttribute]: Prevents the compiler from modifying a method's signature, particularly useful for COM interop.

StructLayout Attribute

When interoperating with structures, you often need to specify the memory layout to match the unmanaged structure. This is done using the [StructLayout] attribute.

[StructLayout(LayoutKind.Sequential, CharSet = CharSet.Unicode)]
public struct POINT
{
    public int X;
    public int Y;
}

LayoutKind.Sequential ensures fields are laid out in memory in the order they are declared.

CharSet.Unicode specifies that strings should be marshaled as Unicode (UTF-16).

Data Marshaling

Marshaling is the process of converting data types between managed code and unmanaged code. The .NET Framework's marshaler handles many common conversions automatically, but sometimes explicit control is needed.

Common Data Type Mappings

.NET Type Unmanaged Type (C/C++) Notes
bool BOOL Mapped to 4-byte integer.
byte unsigned char
char char Depends on CharSet.
short short
int int
long long Typically 32-bit on 32-bit systems, 64-bit on 64-bit systems. Use Int64 for guaranteed 64-bit.
float float
double double
string char*, LPSTR, LPWSTR Depends on CharSet.
IntPtr void*, HANDLE Platform-specific pointer size.
DateTime FILETIME, SYSTEMTIME Requires specific marshalling.

Marshaling with [MarshalAs]

You can use the [MarshalAs] attribute to override default marshaling behavior or to handle complex types.

[DllImport("my_native_lib.dll")]
public static extern int ProcessBuffer(
    [MarshalAs(UnmanagedType.LPArray, SizeParamIndex = 1)] byte[] data,
    int dataLength);

Here, UnmanagedType.LPArray specifies that the byte array should be marshaled as a C-style array of bytes.

Function Signatures

Matching the function signature exactly is critical for successful P/Invoke. This includes the return type, parameter types, and calling convention.

Calling Conventions

Native functions can use different calling conventions (e.g., __cdecl, __stdcall). The default for P/Invoke is __stdcall. You can specify it explicitly using the CallingConvention enum in [DllImport].

[DllImport("user32.dll", CallingConvention = CallingConvention.StdCall)]
public static extern int CallWindowProc(
    IntPtr lpPrevWndFunc,
    IntPtr hWnd,
    uint Msg,
    IntPtr wParam,
    IntPtr lParam);

Error Handling

For functions that return an integer status code and set the last error (e.g., Windows API), set SetLastError = true in [DllImport] and use Marshal.GetLastWin32Error().

using System.Runtime.InteropServices;

public class FileOperations
{
    [DllImport("kernel32.dll", SetLastError = true)]
    private static extern bool CloseHandle(IntPtr hObject);

    public static void SafelyCloseHandle(IntPtr handle)
    {
        if (handle != IntPtr.Zero)
        {
            if (!CloseHandle(handle))
            {
                int errorCode = Marshal.GetLastWin32Error();
                throw new System.ComponentModel.Win32Exception(errorCode);
            }
        }
    }
}

Calling Managed Code from Unmanaged Code

Sometimes, unmanaged code needs to call back into your managed code. This is achieved by passing a delegate (representing a managed method) to the unmanaged code. The .NET marshaler will then convert this delegate into a function pointer that the unmanaged code can call.

Using Delegates for Callbacks

Define a delegate type that matches the signature of the function pointer expected by the unmanaged code.

using System.Runtime.InteropServices;

// Define the delegate type
[UnmanagedFunctionPointer(CallingConvention.Cdecl)]
public delegate int CallbackFunctionDelegate(int value);

public class ManagedCallbacks
{
    // This method will be called from unmanaged code
    public static int MyCallbackHandler(int data)
    {
        System.Console.WriteLine($"Managed callback received: {data}");
        return data * 2;
    }

    // Assume this is how you pass the delegate to unmanaged code
    [DllImport("my_native_lib.dll", CallingConvention = CallingConvention.Cdecl)]
    public static extern void SetCallback(CallbackFunctionDelegate callback);

    public static void RegisterCallback()
    {
        // Create an instance of the delegate
        CallbackFunctionDelegate callbackDelegate = new CallbackFunctionDelegate(MyCallbackHandler);

        // Pass the delegate to the unmanaged function
        SetCallback(callbackDelegate);
    }
}

The [UnmanagedFunctionPointer] attribute specifies the calling convention for the delegate when it's marshaled as a function pointer.

Important: To prevent the delegate from being garbage collected while it's being used by unmanaged code, you must keep a reference to the delegate instance. In the example above, callbackDelegate holds this reference.

Best Practices

  • Keep it Simple: Only expose the necessary functionality from your native libraries.
  • Use `IntPtr` and `SafeHandle`: For handles, use IntPtr or preferably SafeHandle derived classes for better resource management.
  • Specify `CharSet`: Explicitly set the CharSet in [DllImport] to avoid ambiguity. CharSet.Auto can lead to platform-dependent behavior.
  • Handle Errors: Always check return values and use Marshal.GetLastWin32Error() when SetLastError = true.
  • Manage Memory: Be mindful of memory allocated by unmanaged code. Ensure it's freed appropriately, often by calling a corresponding unmanaged cleanup function.
  • Use `BestFitMapping` and `ThrowOnUnmappableChar`: These MarshalAsAttribute options control how characters that cannot be mapped between charsets are handled.
  • Understand 32-bit vs. 64-bit: Ensure your P/Invoke declarations and native libraries match the architecture of your application.