Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles / containers / virtual-machine

Simple Method of DLL Export without C++/CLI

5.00/5 (44 votes)
27 Jun 2009CPOL4 min read 191.5K   5.9K  
Article describes how to build an assembly that exposes functions to unmanaged code without C++/CLI
Sample output

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:

C#
[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:

C#
namespace DummyLibrary
{
    public class DummyClass
    {
        public static void DummyMethod() { }
    }
}

after compiling and disassembling gives this code:

MSIL
.assembly extern mscorlib
{
  .publickeytoken = (B7 7A 5C 56 19 34 E0 89 )
  .ver 2:0:0:0
}
.assembly DummyLibrary
{
  // Assembly attributes…
  .hash algorithm 0x00008004
  .ver 1:0:0:0
}
.module DummyLibrary.dll
.imagebase 0x00400000
.file alignment 0x00000200
.stackreserve 0x00100000
.subsystem 0x0003       // WINDOWS_CUI
.corflags 0x00000001    //  ILONLY

// =============== CLASS MEMBERS DECLARATION ===================

.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
  } // end of method DummyClass::DummyMethod

  .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
  } // end of method DummyClass::.ctor
} // end of class DummyLibrary.DummyClass

A very simple wrapper for the above class:

C++
#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:

MSIL
// Referenced assemblies…
.assembly Wrapper
{
  // Assembly attributes…
  .hash algorithm 0x00008004
  .ver 1:0:3466:3451
}
.module Wrapper.dll
.imagebase 0x10000000
.file alignment 0x00000200
.stackreserve 0x00100000
.subsystem 0x0002       // WINDOWS_GUI
.corflags 0x00000010    // 
.vtfixup [1] int32 retainappdomain at D_0000A000 // 06000001
// Other .vtfixup entries…

// C++/CLI implementation details…

.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
} // end of global method DummyMethod

.data D_0000A000 = bytearray (
                 01 00 00 06) 

// Raw data…

The important differences between these two IL listings are:

  1. .corflags keyword which tells Windows how to load the assembly
  2. .vtfixup keyword which adds an empty slot to assembly VTable
  3. .data keyword which reserves memory to store RVA (Relative Virtual Address) for corresponding VTable entry
  4. .vtentry keyword which assigns method with VTable entry
  5. .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:

MSIL
.assembly extern mscorlib
{
  .publickeytoken = (B7 7A 5C 56 19 34 E0 89 
  .ver 2:0:0:0
}
.assembly DummyLibrary
{
  // Assembly attributes…
  .hash algorithm 0x00008004
  .ver 1:0:0:0
}
.module DummyLibrary.dll
.imagebase 0x00400000
.file alignment 0x00000200
.stackreserve 0x00100000
.subsystem 0x0003       // WINDOWS_CUI
.corflags 0x00000002    //  32BITSREQUIRED
.vtfixup [1] int32 fromunmanaged at VT_01
.data VT_01 = int32(0)

// =============== CLASS MEMBERS DECLARATION ===================

.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
  } // end of method DummyClass::DummyMethod

  .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
  } // end of method DummyClass::.ctor

} // end of class DummyLibrary.DummyClass

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:

MSIL
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:

C#
[DllExport]
public static void DummyMethod() { }

DummyMethod will be available as DummyMethod static entry point.

C#
[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 strings 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:

C++
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 strings are transparently marshaled from and to plain zero-terminated strings. To use arrays, you must specify through MarshalAs attribute how to get array length:

C#
[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;
}
C++
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:
C#
[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;
}
C++
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:

C#
public delegate void Callback([MarshalAs(UnmanagedType.LPTStr)]string name);

[DllExport]
public static void DoCallback(Callback callback)
{
    if (callback != null)
        callback(".NET assembly");
}
C++
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:

C#
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

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)