Click here to Skip to main content
15,867,568 members
Articles / Containers / Virtual Machine

Simple Method of DLL Export without C++/CLI

Rate me:
Please Sign up or sign in to vote.
5.00/5 (44 votes)
27 Jun 2009CPOL4 min read 182.1K   5.9K   112   41
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)


Written By
Russian Federation Russian Federation
This member has not yet provided a Biography. Assume it's interesting and varied, and probably something to do with programming.

Comments and Discussions

 
QuestionFAQ Pin
leo andress21-Jul-23 22:39
leo andress21-Jul-23 22:39 
Questionimple Method of DLL Export without C++/CLI Pin
Natasha78993-Jul-23 20:00
Natasha78993-Jul-23 20:00 
PraiseAppreciation Pin
Member 1589784418-Jan-23 0:56
Member 1589784418-Jan-23 0:56 
Questionfix issue Pin
Suraj Pundir 202221-Nov-22 22:23
Suraj Pundir 202221-Nov-22 22:23 
QuestionPlease example post-build command Pin
Member 156958303-Jul-22 23:52
Member 156958303-Jul-22 23:52 
Questionthank you Pin
Member 1567108412-Jun-22 10:42
Member 1567108412-Jun-22 10:42 
QuestionThanks for the help Pin
Member 1564548321-May-22 23:16
Member 1564548321-May-22 23:16 
PraiseMessage Closed Pin
20-Feb-22 8:08
Yogesh Sharma 202220-Feb-22 8:08 
QuestionMessage Closed Pin
9-May-21 9:23
Lootera Gang9-May-21 9:23 
QuestionCall Using this Pin
Jhon Jason12-Apr-20 23:46
Jhon Jason12-Apr-20 23:46 
NewsAutomated tool with no source dependencies Pin
Yves Goergen12-Feb-15 10:01
Yves Goergen12-Feb-15 10:01 
Generala really good job Pin
luca_covolo15-Jan-14 2:57
luca_covolo15-Jan-14 2:57 
Questionarray to unmanaged Pin
Member 38516803-Mar-13 21:35
Member 38516803-Mar-13 21:35 
AnswerRe: array to unmanaged Pin
basti T11-Feb-15 23:46
basti T11-Feb-15 23:46 
QuestionPassing parameters by reference Pin
JimmyRopes1-Mar-13 4:02
professionalJimmyRopes1-Mar-13 4:02 
QuestionUsing on WindowsCe Classes Pin
Kitro1-Aug-12 4:03
Kitro1-Aug-12 4:03 
Questioncanot be loaded in Delphi project Pin
ww2010092928-Nov-11 14:49
ww2010092928-Nov-11 14:49 
AnswerRe: canot be loaded in Delphi project Pin
ViRuSTriNiTy25-Aug-12 22:07
ViRuSTriNiTy25-Aug-12 22:07 
QuestionRe: Call using VB6 Pin
mla15415-Jul-11 7:22
mla15415-Jul-11 7:22 
AnswerRe: Call using VB6 Pin
Natasha78993-Jul-23 19:20
Natasha78993-Jul-23 19:20 
GeneralDebugger does not hit on breakpoints Pin
NavinBiz11-Feb-11 20:18
NavinBiz11-Feb-11 20:18 
GeneralRe: Debugger does not hit on breakpoints Pin
dmihailescu14-Jul-11 5:19
dmihailescu14-Jul-11 5:19 
QuestionAfter doing an export of my class Pin
Member 470178810-Oct-10 22:57
Member 470178810-Oct-10 22:57 
GeneralBetter solution!! PinPopular
Wojciech Nagórski27-Aug-10 22:13
Wojciech Nagórski27-Aug-10 22:13 
GeneralRe: Better solution!! Pin
dmihailescu14-Jul-11 5:11
dmihailescu14-Jul-11 5:11 

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Praise Praise    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.