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.
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.dllis the native library containing theMessageBoxfunction.SetLastError = trueindicates that the function might set the Windows error code, which can be retrieved usingMarshal.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.
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
IntPtror preferablySafeHandlederived classes for better resource management. - Specify `CharSet`: Explicitly set the
CharSetin[DllImport]to avoid ambiguity.CharSet.Autocan lead to platform-dependent behavior. - Handle Errors: Always check return values and use
Marshal.GetLastWin32Error()whenSetLastError = 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
MarshalAsAttributeoptions 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.