开启掘金成长之旅!这是我参与「掘金日新计划 · 2 月更文挑战」的第 3 天,点击查看活动详情
在 C# 中,可以通过 DllImport 调用C++ 的非托管 DLL 程序。
一、准备
环境:VS2019
参考: 微软官方教程 WINDOWS核心编程第五版
二、创建 C++ DLL
2.1 创建 C++ dll 项目
会自动生成:dllmain.cpp 和 pch.cpp
2.1.1 pch.cpp 预编译标头文件说明
此文件的目的是加快生成过程。 应在此处包含任何稳定的标头文件,例如标准库标头(如 <vector>)。 预编译标头仅在它或它包含的任何文件发生更改时进行编辑。 如果只在项目源代码中进行更改,则生成将跳过对预编译标头的编译。
预编译标头的编译器选项为 /Y。 在“项目属性”页,该选项位于“配置属性”>“C/C++”>“预编译头”下。 可以选择不使用预编译标头,并可以指定标头文件名以及输出文件的名称和路径。
其中 pch.cpp
// pch.cpp: 与预编译标头对应的源文件
#include "pch.h"
// 当使用预编译的头时,需要使用此源文件,编译才能成功。
2.1.2 DLL 应用程序的入口点函数:dllmain.cpp 说明
一个 DLL 可以有一个入口点函数。系统会在不同的时候调用这个入口点函数。这些调用是通知性质的,通常被 DLL 用来执行一些与进程或线程有关的初始化和清理工作。
如果 DLL 不需要这些通知,那么我们可以不必在源代码中实现这个函数。
如果想要在 DLL 中接收通知,那么我们可以像下面这样来实现入口点函数:
// dllmain.cpp : 定义 DLL 应用程序的入口点。
#include "pch.h"
BOOL APIENTRY DllMain( HMODULE hModule,
DWORD ul_reason_for_call,
LPVOID lpReserved
)
{
switch (ul_reason_for_call)
{
case DLL_PROCESS_ATTACH:
//DLL 被映射到进程(process)地址空间
case DLL_THREAD_ATTACH:
//一个线程(thread)被创建
case DLL_THREAD_DETACH:
//一个线程(thread)被退出清理
case DLL_PROCESS_DETACH:
//DLL 从进程(process)地址空间被解映射
break;
}
return TRUE;
}
注意事项:
(1)DllMain区分大小写
(2)其他名称,编译链接可以通过,但入口点函数将永远不会被调用
(3)hModule 包含该 DLL 实例的句柄,其值是基于 DLL 的地址,也就是说表示一个虚拟内存地址,DLL 的文件映射就被映射到进程地址空间中的这个位置,通常将这个参数保存在一个全局变量中,这样在调用资源载入函数(eg. DialogBox 和 LoadString)的时候,我们就可以使用它。如果 DLL 是隐式载入的,那么最后一个参数lpReserved ** 的值将不为零;如果DLL 是显式载入的,那么最后一个参数lpReserved ** 的值将为零。DLL 的 HINSTANCE 和 HMODULE 相同, 因此 hModule 可以被用在需要一个模块句柄的函数调用上。
(4)ul_reason_for_call 表示系统调用入口点函数的原因。这个参数的值可能是下列四个之一:
- DLL_PROCESS_ATTACH
- DLL_PROCESS_DETACH
- DLL_THREAD_ATTACH
- DLL_THREAD_DETACH
(5)DLL 使用 DllMain 函数对自己进行初始化。
2.2 创建自定义的功能函数
// TestMyDll.cpp 定义DLL应用程序的导出函数
#include "pch.h"
#include <math.h>
extern "C" __declspec(dllexport) int Add(int x, int y)
{
return x + y;
}
extern "C" __declspec(dllexport) void Pow(double *x, double y)
{
*x = pow(*x, y);
}
方法 Add 返回两个整数的和;
方法 Pow 计算 x 的 y 次方,并以指针的形式修改参数 x 地址处的值。
修饰符 extern "C" :\
- 首先,被它修饰的目标是“extern”的;
- 其次,被它修饰的目标是“C”的。
- 而被extern "C" 修饰的变量和函数是按照C语言方式编译和链接的。
__declspec(dllexport)的目的是为了将对应的函数放入到DLL动态库中。extern "C" __declspec(dllexport)加起来的目的是为了使用 DllImport 调用非托管 C++ 的 DLL 文件。因为使用 DllImport 只能调用由 C 语言函数做成的DLL。
编译生成 DLL 文件,产生的相关文件如下所示。
三、创建 C# 文件调用
将 C++ 的 dll 文件,放在 C# 工程目录的 \bin\Debug 中
创建 C# 中,导入 C++ 接口函数的类:
using System.Runtime.InteropServices; //DllImport
namespace TestCPlusDll
{
class CppDll
{
[DllImport("TestMyDll.dll", EntryPoint = "Add")]
public static extern int Add(int a, int b);
[DllImport("TestMyDll.dll", EntryPoint = "Pow")]
public static extern void Pow(ref double a, double b);
}
}
单独创建一个调用 C++ 函数的类,调用这些函数;同时也可以结合一些可视化工具来测试他们。
namespace TestCPlusDll
{
class TestCpp
{
int res = CppDll.Add(1, 2);
}
}
结合我们之前的例子,我们可以在 Naviswork中测试他们。
[PluginAttribute("BasicPlugIn.DBasicPlugin", //Plugin name
"ADSK", //4 character Developer ID or GUID
ToolTip = "Test C++ DLL tip",
DisplayName = "TestCppDll")] //Display name for the Plugin in the Ribbon
public class DBasicPlugin : AddInPlugin //Derives from AddInPlugin
{
public override int Execute(params string[] parameters)
{
int result = TestCPlusDll.CppDll.Add(1, 3);
string varString = Convert.ToString(result);
varString = "1+3=" + varString;
MessageBox.Show(Autodesk.Navisworks.Api.Application.Gui.MainWindow, varString);
return 0;
}
}
效果如下: