无需 C++/CLI 的 DLL 导出简单方法





5.00/5 (42投票s)
本文介绍如何在不使用 C++/CLI 的情况下构建一个将函数暴露给非托管代码的程序集。

引言
在 .NET 中,您可以使托管代码和非托管代码协同工作。要从托管代码调用非托管函数,您可以使用平台调用技术(简称 P/Invoke)。P/Invoke 在所有托管语言中都可用。使用 P/Invoke 就像定义正确的函数签名并为其添加 DllImport
属性一样简单。通常,它看起来像这样:
[DllImport("user32.dll", CharSet = CharSet.Auto, SetLastError = false)]
static extern IntPtr SendMessage(IntPtr hWnd, UInt32 Msg, IntPtr wParam, IntPtr lParam);
但是,当您需要从非托管代码调用托管函数时,一种常见的方法是编写一个包装器——一个用 C++/CLI(以前称为 C++ 的托管扩展)实现的独立的混合模式 DLL,该 DLL 导出非托管函数并可以访问托管类,或者完全用 C++/CLI 实现类库。这是一项高级任务,需要同时了解托管语言和 C++。我想知道为什么没有一个 DllExport
属性可以允许从任何托管语言导出扁平 API。
在 .NET 程序集内部
用托管语言编写的代码会被编译成字节码——.NET 虚拟机指令。此字节码可以轻松地反编译为 MSIL(Microsoft 中间语言),它看起来类似于机器汇编语言。您可以使用 .NET SDK 中包含的 ildasm.exe 或 Reflector 工具查看 IL 代码。这个简单的类
namespace DummyLibrary
{
public class DummyClass
{
public static void DummyMethod() { }
}
}
编译和反汇编后会得到这段代码:
.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
上面类的非常简单的包装器
#include "stdafx.h"
void __stdcall DummyMethod(void)
{
DummyLibrary::DummyClass::DummyMethod();
}
LIBRARY "Wrapper"
EXPORTS
DummyMethod
编译和反汇编后会得到大量的 IL 代码,但总的来说,它会是这样的:
// 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…
这两个 IL 列表之间的重要区别在于:
.corflags
关键字,它告诉 Windows 如何加载程序集。.vtfixup
关键字,它向程序集的VTable
添加一个空槽。.data
关键字,它保留内存以存储相应VTable
条目的 RVA(相对虚拟地址)。.vtentry
关键字,它将方法与VTable
条目关联起来。.export
关键字,它将方法添加到导出表中并为其分配入口点名称。
如果您将这些关键字正确地添加到第一个 IL 列表并使用 ilasm.exe 进行组装,您将得到一个无需使用混合模式包装器即可导出非托管 API 的程序集。最终的 IL 代码将如下所示:
.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
当非托管可执行文件加载生成的 DLL 时,CLR 将被初始化,并用实际地址替换保留的 RVA 条目。导出的函数调用将被 CLR 拦截,并执行相应的托管代码。
DllExporter
显然,每次更改后手动编辑 IL 代码是不明智的。因此,我决定编写一个实用程序,在每次构建后自动执行这些操作。要标记哪些方法将被导出,您需要将 DllExporter.exe 的引用添加到您的项目中,并为选定的 static
方法添加 DllExporter.DllExport
属性。实例方法不能被导出。构建完成后,您可以运行 DllExporter.exe <程序集路径>,该程序将反汇编给定的程序集,创建 VTFixup 条目,将 DllExport
属性替换为对相应 VTable
条目的引用,并删除 DllExporter.exe 程序集引用。生成的程序集将保存到 AssemblyName.Exports.dll 中。您不需要 DllExporter.exe 来使用生成的程序集。生成的程序集将仅限 32 位。要在每次构建后运行 DllExporter
,您可以转到 Visual Studio -> 项目属性 -> 生成事件,并添加以下生成后命令:
DllExporter.exe $(TargetFileName)
move $(TargetName).Exports$(TargetExt) $(TargetFileName)
示例
您可以用 [DllExport]
标记任何 static
方法。方法不需要是 public
。标记了 [DllExport]
的实例方法将被忽略。现在定义一个包含一些方法的 DummyClass
类:
[DllExport]
public static void DummyMethod() { }
DummyMethod
将作为 DummyMethod static
入口点可用。
[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);
}
您可以使用 EntryPoint
属性定义与方法名不同的入口点名称。像 string
s 和数组这样的复杂类型应该使用 MarshalAs
属性正确地封送(marshal)到非托管代码。要从非托管应用程序中使用您的托管 DLL,您应该像使用任何其他 DLL 一样,通过 LoadLibrary
和 GetProcAddress
获取函数指针:
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"));
非托管 C++ 不了解 .NET 类型,但 .NET string
s 会被透明地封送为零终止的 string
s。要使用数组,您必须通过 MarshalAs
属性指定如何获取数组长度:
[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)));
您可以按值、指针或引用传递结构:[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);
最后,您可以与委托交换非托管代码:
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);
对于更复杂的情况,例如与非托管类一起工作,您仍然需要使用 C++/CLI,但仅使用托管语言,您仍然可以为非托管应用程序创建扩展,例如 Total Commander 的插件,反之亦然。
使用具有托管代码的程序集
如果您运行的是 64 位操作系统,并且尝试在另一个托管应用程序中使用包含导出的程序集,您可能会收到 BadImageFormat
异常,因为程序集是 32 位的,而 .NET 应用程序默认情况下以 64 位模式运行。在这种情况下,您应该将您的应用程序设置为 32 位:Visual Studio -> 项目属性 -> 生成 -> 平台目标 -> x86。您可以直接使用包含导出的程序集,也可以通过 P/Invoke 使用——结果将是相同的:
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);
}
信息来源
部分信息来源于 Jim Teeuwen 的文章 Exporting Managed code as Unmanaged。
历史
- 2009 年 6 月 28 日:初始版本