How to Call C/C++ DLLs in C# Using P/Invoke
Platform Invoke (P/Invoke) is a bridge provided by the .NET runtime
that allows managed C# applications to call unmanaged functions
implemented inside dynamic link libraries (DLLs) written in C or C++.
This guide outlines the core mechanics of P/Invoke, detailing how to
export native functions, declare them in C# using the
DllImport attribute, marshal data types between managed and
unmanaged memory, and avoid common runtime pitfalls.
1. Exporting the C/C++ Function
Before C# can invoke a native function, the C/C++ compiler must
export the function without name mangling. Standard C++ compilers mangle
function names to support overloading, which breaks the standard symbol
lookup used by P/Invoke. To prevent this, export functions using
extern "C" and the __declspec(dllexport)
attribute.
// NativeLib.cpp
#define EXPORT_API extern "C" __declspec(dllexport)
EXPORT_API int Add(int a, int b)
{
return a + b;
}
EXPORT_API void PrintMessage(const char* message)
{
printf("%s\n", message);
}Compile this code into a DLL (e.g., NativeLib.dll).
2. Setting Up P/Invoke in C
To consume the exported functions in C#, reference the
System.Runtime.InteropServices namespace and use the
[DllImport] attribute alongside a
static extern method declaration.
using System;
using System.Runtime.InteropServices;
class Program
{
// Define the DLL name and calling convention
[DllImport("NativeLib.dll", CallingConvention = CallingConvention.Cdecl)]
public static extern int Add(int a, int b);
[DllImport("NativeLib.dll", CallingConvention = CallingConvention.Cdecl, CharSet = CharSet.Ansi)]
public static extern void PrintMessage(string message);
static void Main()
{
int sum = Add(10, 25);
Console.WriteLine($"Result from C++: {sum}");
PrintMessage("Hello from managed C#!");
}
}Place the compiled NativeLib.dll in the same output
directory as the C# executable (e.g., bin/Debug/net8.0/),
or ensure its directory is included in the system's
PATH.
3. Data Marshaling
The CLR's P/Invoke marshaler handles conversions between managed and unmanaged representations of data. While primitive types transfer automatically, complex types require specific mapping.
Primitive Types
Primitive types like int, float,
double, and byte share the same representation
in memory (blittable types) and require no special configuration:
int(C++) \(\rightarrow\)int(C#)double(C++) \(\rightarrow\)double(C#)bool(C++) \(\rightarrow\)[MarshalAs(UnmanagedType.I1)] bool(C#)
Strings
Strings in .NET are immutable Unicode sequences
(UTF-16), while C/C++ usually expects null-terminated
char* (ANSI/UTF-8) or wchar_t* (UTF-16). Use
the CharSet property in [DllImport] to define
how strings are converted:
CharSet.Ansi: Marshals C#stringtoconst char*.CharSet.Unicode: Marshals C#stringtoconst wchar_t*.
Passing by Reference (Pointers)
To pass parameters as pointers to allow the unmanaged function to
modify their values, use the ref or out
keywords in C#.
C++:
EXPORT_API void GetDimensions(int* width, int* height)
{
*width = 1920;
*height = 1080;
}C#:
[DllImport("NativeLib.dll", CallingConvention = CallingConvention.Cdecl)]
public static extern void GetDimensions(out int width, out int height);
// Usage:
GetDimensions(out int w, out int h);Structs
To pass custom structures, match the memory layout explicitly using
the [StructLayout] attribute.
C++:
struct Vector3 {
float x;
float y;
float z;
};
EXPORT_API void ProcessVector(Vector3* vec) {
vec->x *= 2;
}C#:
[StructLayout(LayoutKind.Sequential)]
public struct Vector3
{
public float X;
public float Y;
public float Z;
}
[DllImport("NativeLib.dll", CallingConvention = CallingConvention.Cdecl)]
public static extern void ProcessVector(ref Vector3 vec);4. Key Configuration Rules
- Calling Conventions: Default C++ functions use
__cdecl, while Windows API functions typically use__stdcall. Mismatches result in aPInvokeStackImbalanceruntime exception. Always explicitly match the convention in the attribute viaCallingConvention = CallingConvention.CdeclorCallingConvention.StdCall. - Platform Architecture: Ensure target architectures
match. A 64-bit C# application (
x64) cannot load a 32-bit (x86) native DLL. Set the C# project configuration to match the native library build target.