C#调用C++ (非托管方式)

1,518 阅读4分钟

开启掘金成长之旅!这是我参与「掘金日新计划 · 2 月更文挑战」的第 3 天,点击查看活动详情

在 C# 中,可以通过 DllImport 调用C++ 的非托管 DLL 程序。

一、准备

环境:VS2019

参考微软官方教程 WINDOWS核心编程第五版

二、创建 C++ DLL

2.1 创建 C++ dll 项目

image.png

image.png

image.png

会自动生成:dllmain.cpppch.cpp

image.png

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 的 HINSTANCEHMODULE 相同, 因此 hModule 可以被用在需要一个模块句柄的函数调用上。
(4)ul_reason_for_call 表示系统调用入口点函数的原因。这个参数的值可能是下列四个之一:

- DLL_PROCESS_ATTACH   
- DLL_PROCESS_DETACH 
- DLL_THREAD_ATTACH 
- DLL_THREAD_DETACH 


(5)DLL 使用 DllMain 函数对自己进行初始化。

2.2 创建自定义的功能函数

image.png

image.png

// 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 计算 xy 次方,并以指针的形式修改参数 x 地址处的值。
修饰符 extern "C" :\

  • 首先,被它修饰的目标是“extern”的;
  • 其次,被它修饰的目标是“C”的。
  • 而被extern "C" 修饰的变量和函数是按照C语言方式编译和链接的。
  • __declspec(dllexport) 的目的是为了将对应的函数放入到DLL动态库中。
  • extern "C" __declspec(dllexport) 加起来的目的是为了使用 DllImport 调用非托管 C++ 的 DLL 文件。因为使用 DllImport 只能调用由 C 语言函数做成的DLL。

编译生成 DLL 文件,产生的相关文件如下所示。

image.png

三、创建 C# 文件调用

image.png

image.png

将 C++ 的 dll 文件,放在 C# 工程目录的 \bin\Debug

image.png

创建 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;
        }
    }

效果如下:

DLLAdd.gif

这里需要注意的是,除了将 C# 生成的 DLL 放入 Plugin 目录之外,还需要将 C++ 生成的 DLL 放入 Plugin。