65.9K
CodeProject 正在变化。 阅读更多。
Home

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

starIconstarIconstarIconstarIconstarIcon

5.00/5 (42投票s)

2009 年 6 月 28 日

CPOL

4分钟阅读

viewsIcon

193439

downloadIcon

5975

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

Sample output

引言

在 .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.exeReflector 工具查看 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 列表之间的重要区别在于:

  1. .corflags 关键字,它告诉 Windows 如何加载程序集。
  2. .vtfixup 关键字,它向程序集的 VTable 添加一个空槽。
  3. .data 关键字,它保留内存以存储相应 VTable 条目的 RVA(相对虚拟地址)。
  4. .vtentry 关键字,它将方法与 VTable 条目关联起来。
  5. .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 属性定义与方法名不同的入口点名称。像 strings 和数组这样的复杂类型应该使用 MarshalAs 属性正确地封送(marshal)到非托管代码。要从非托管应用程序中使用您的托管 DLL,您应该像使用任何其他 DLL 一样,通过 LoadLibraryGetProcAddress 获取函数指针:

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 strings 会被透明地封送为零终止的 strings。要使用数组,您必须通过 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 日:初始版本
© . All rights reserved.