Introduction
In .NET, you can make managed and unmanaged code work together. To call an unmanaged function from managed code, you can use Platform Invoke technology (shortly P/Invoke). P/Invoke is available in all managed languages. Using P/Invoke is as simple as defining correct method signature and adding a DllImport
attribute to it. Usually it seems like this:
[DllImport("user32.dll", CharSet = CharSet.Auto, SetLastError = false)]
static extern IntPtr SendMessage(IntPtr hWnd, UInt32 Msg, IntPtr wParam, IntPtr lParam);
But when you need to call a managed function from unmanaged code, a common way to do it is to write a wrapper — separate mixed mode DLL implemented in C++/CLI (formerly Managed Extensions for C++) that exports unmanaged functions and can access managed classes, or implement whole class library in C++/CLI. This is an advanced task and requires knowledge of both managed languages and C++. I was wondering why there is no DllExport
attribute that will allow to expose flat API from any managed language.
Inside .NET Assembly
Code written in managed language is compiled into bytecode — commands for .NET virtual machine. This bytecode can be easily disassembled into MSIL (Microsoft Intermediate Language) which looks similar to machine assembly language. You can view IL code using ildasm.exe included in .NET SDK or Reflector tool. This simple class:
namespace DummyLibrary
{
public class DummyClass
{
public static void DummyMethod() { }
}
}
after compiling and disassembling gives this code:
.assembly extern mscorlib
{
.publickeytoken = (B7 7A 5C 56 19 34 E0 89 )
.ver 2:0:0:0
}
.assembly DummyLibrary
{
.hash algorithm 0x00008004
.ver 1:0:0:0
}
.module DummyLibrary.dll
.imagebase 0x00400000
.file alignment 0x00000200
.stackreserve 0x00100000
.subsystem 0x0003
.corflags 0x00000001
.class public auto ansi beforefieldinit DummyLibrary.DummyClass
extends [mscorlib]System.Object
{
.method private hidebysig static void
DummyMethod() cil managed
{
.custom instance void [DllExporter]DllExporter.DllExportAttribute::.ctor() =
( 01 00 00 00 )
.maxstack 8
IL_0000: nop
IL_0001: ret
}
.method public hidebysig specialname rtspecialname
instance void .ctor() cil managed
{
.maxstack 8
IL_0000: ldarg.0
IL_0001: call instance void [mscorlib]System.Object::.ctor()
IL_0006: ret
}
}
A very simple wrapper for the above class:
#include "stdafx.h"
void __stdcall DummyMethod(void)
{
DummyLibrary::DummyClass::DummyMethod();
}
LIBRARY "Wrapper"
EXPORTS
DummyMethod
after compiling and disassembling gives lots of IL code, but in short it will look like this:
.assembly Wrapper
{
.hash algorithm 0x00008004
.ver 1:0:3466:3451
}
.module Wrapper.dll
.imagebase 0x10000000
.file alignment 0x00000200
.stackreserve 0x00100000
.subsystem 0x0002
.corflags 0x00000010
.vtfixup [1] int32 retainappdomain at D_0000A000
.method assembly static void modopt
([mscorlib]System.Runtime.CompilerServices.CallConvStdcall)
DummyMethod() cil managed
{
.vtentry 1 : 1
.export [1] as DummyMethod
.maxstack 0
IL_0000: call void [DummyLibrary]DummyLibrary.DummyClass::DummyMethod()
IL_0005: ret
}
.data D_0000A000 = bytearray (
01 00 00 06)
The important differences between these two IL listings are:
.corflags
keyword which tells Windows how to load the assembly .vtfixup
keyword which adds an empty slot to assembly VTable
.data
keyword which reserves memory to store RVA (Relative Virtual Address) for corresponding VTable
entry .vtentry
keyword which assigns method with VTable
entry .export
keyword which adds method into export table and assigns an entry point name to it.
If you add these keywords to first IL listing properly and assemble it with ilasm.exe, you will get an assembly that exports unmanaged APIs without using mixed-mode wrapper. Final IL code will look like:
.assembly extern mscorlib
{
.publickeytoken = (B7 7A 5C 56 19 34 E0 89
.ver 2:0:0:0
}
.assembly DummyLibrary
{
.hash algorithm 0x00008004
.ver 1:0:0:0
}
.module DummyLibrary.dll
.imagebase 0x00400000
.file alignment 0x00000200
.stackreserve 0x00100000
.subsystem 0x0003
.corflags 0x00000002
.vtfixup [1] int32 fromunmanaged at VT_01
.data VT_01 = int32(0)
.class public auto ansi beforefieldinit DummyLibrary.DummyClass
extends [mscorlib]System.Object
{
.method private hidebysig static void DummyMethod() cil managed
{
.custom instance void [mscorlib]System.ObsoleteAttribute::.ctor() = ( 01 00 00 00 )
.custom instance void
.vtentry 1 : 1
.export [1] as DummyMethod
.maxstack 8
IL_0000: nop
IL_0001: ret
}
.method public hidebysig specialname rtspecialname
instance void .ctor() cil managed
{
.maxstack 8
IL_0000: ldarg.0
IL_0001: call instance void [mscorlib]System.Object::.ctor()
IL_0006: ret
}
}
When the resulting DLL is loaded by unmanaged executable, CLR will be initialized and will replace reserved RVA entries with actual addresses. Exported function calls will be intercepted by CLR and corresponding managed code will be executed.
DllExporter
Obviously editing IL code manually after each change is irrational. So I decided to write a utility that will perform these actions automatically after each build. To mark which methods will be exported, you need to add reference to DllExporter.exe to your project and add DllExporter.DllExport
attribute to selected static
methods. Instance methods cannot be exported. After build, you can run DllExporter.exe <path to assembly> and program will disassemble the given assembly, create VTFixup entries, replace DllExport
attributes with references to corresponding VTable
entries and removes DllExporter.exe assembly reference. Resulting assembly will be saved into AssemblyName.Exports.dll. You do not need DllExporter.exe to use the resulting assembly. Resulting assembly will be 32-bit only. To run DllExporter
after each build, you can go to Visual Studio -> Project Properties -> Build Events and add the following post-build commands:
DllExporter.exe $(TargetFileName)
move $(TargetName).Exports$(TargetExt) $(TargetFileName)
Examples
You can mark any static
method with [DllExport]
. Method does not need to be public
. Instance methods marked with [DllExport]
will be ignored. Now define class DummyClass
with some methods:
[DllExport]
public static void DummyMethod() { }
DummyMethod
will be available as DummyMethod static
entry point.
[DllExport(EntryPoint = "SayHello")]
[return:MarshalAs(UnmanagedType.LPTStr)]
public static string Hello([MarshalAs(UnmanagedType.LPTStr)]string name)
{
return string.Format("Hello from .NET assembly, {0}!", name);
}
You can use EntryPoint
property to define entry point name different to method name. Complex types like string
s and arrays should be correctly marshaled to unmanaged code using MarshalAs
attribute. To use your managed DLL from unmanaged application, you should get function pointer with LoadLibrary
and GetProcAddress
like for any other DLL:
typedef LPTSTR (__stdcall *HelloFunc)(LPTSTR name);
HMODULE hDll = LoadLibrary(L"DummyLibrary.dll");
if (!hDll)
return GetLastError();
HelloFunc helloFunc = (HelloFunc)GetProcAddress(hDll, "SayHello");
if (!helloFunc)
return GetLastError();
wprintf(L"%s\n", helloFunc(L"unmanaged code"));
Unmanaged C++ does not know about .NET types but .NET string
s are transparently marshaled from and to plain zero-terminated string
s. To use arrays, you must specify through MarshalAs
attribute how to get array length:
[DllExport]
public static int Add([MarshalAs
(UnmanagedType.LPArray, SizeParamIndex = 1)]int[] values, int count)
{
int result = 0;
for (int i = 0; i < values.Length; i++)
result += values[i];
return result;
}
typedef int (__stdcall *AddFunc)(int values[], int count);
AddFunc addFunc = (AddFunc)GetProcAddress(hDll, "Add");
if (!addFunc)
return GetLastError();
int values[] = {1, 2, 3, 4, 5 };
wprintf(L"Sum of integers from 1 to 5 is: %d\n",
addFunc(values, sizeof(values) / sizeof(int)));
You can pass structures by value, pointer or reference:
[StructLayout(LayoutKind.Sequential)]
public struct DummyStruct
{
public short a;
public ulong b;
public byte c;
public double d;
}
[DllExport]
public static DummyStruct TestStruct()
{ return new DummyStruct { a = 1, b = 2, c = 3, d = 4 }; }
[DllExport]
public static void TestStructRef(ref DummyStruct dummyStruct)
{
dummyStruct.a += 5;
dummyStruct.b += 6;
dummyStruct.c += 7;
dummyStruct.d += 8;
}
struct DummyStruct
{
short a;
DWORD64 b;
byte c;
double d;
};
typedef DummyStruct (__stdcall *StructFunc)(void);
typedef void (__stdcall *StructRefFunc)(DummyStruct& dummyStruct);
typedef void (__stdcall *StructPtrFunc)(DummyStruct* dummyStruct);
StructFunc structFunc = (StructFunc)GetProcAddress(hDll, "TestStruct");
if (!structFunc)
return GetLastError();
DummyStruct dummyStruct = structFunc();
wprintf(L"Struct fields are: %d, %llu, %hhu, %g\n",
dummyStruct.a, dummyStruct.b, dummyStruct.c, dummyStruct.d);
StructRefFunc structRefFunc = (StructRefFunc)GetProcAddress(hDll, "TestStructRef");
if (!structRefFunc)
return GetLastError();
structRefFunc(dummyStruct);
wprintf(L"Another struct fields are: %d, %llu, %hhu, %g\n",
dummyStruct.a, dummyStruct.b, dummyStruct.c, dummyStruct.d);
StructPtrFunc structPtrFunc = (StructPtrFunc)GetProcAddress(hDll, "TestStructRef");
if (!structPtrFunc)
return GetLastError();
structPtrFunc(&dummyStruct);
wprintf(L"Yet another struct fields are: %d, %llu, %hhu, %g\n",
dummyStruct.a, dummyStruct.b, dummyStruct.c, dummyStruct.d);
Finally, you can exchange unmanaged code with delegates:
public delegate void Callback([MarshalAs(UnmanagedType.LPTStr)]string name);
[DllExport]
public static void DoCallback(Callback callback)
{
if (callback != null)
callback(".NET assembly");
}
typedef void (__stdcall *CallbackFunc)(Callback callback);
void __stdcall MyCallback(LPTSTR name)
{
wprintf(L"Hello from unmanaged code, %s!\n", name);
}
CallbackFunc callbackFunc = (CallbackFunc)GetProcAddress(hDll, "DoCallback");
if (!callbackFunc)
return GetLastError();
callbackFunc(&MyCallback);
For more complex cases, like working with unmanaged classes you still need to use C++/CLI, but using only managed language you still can create extensions for unmanaged applications, for example, plugins for Total Commander and vice versa.
Using Assembly with Managed Code
If you are running 64-bit OS and try to use assembly with exports in another managed application, you probably get BadImageFormat
exception because assembly is 32-bit and .NET applications by default are running in 64-bit mode. In this case, you should make your application 32-bit: Visual Studio -> Project Properties -> Build -> Platform Target -> x86. You can use assembly with exports directly or through P/Invoke — the result will be the same:
class Program
{
static void Main(string[] args)
{
Console.WriteLine(DummyLibrary.DummyClass.Hello(".NET application"));
Console.WriteLine(SayHello(".NET application"));
}
[DllImport("DummyLibrary.dll", CharSet = CharSet.Unicode)]
public static extern string SayHello(string name);
}
Information Sources
Some information was taken from the article Exporting Managed code as Unmanaged by Jim Teeuwen.
History
- 28th June, 2009: Initial version