查看: 84|回复: 3

C++导出动态库并提供通用接口(1)

[复制链接]

4

主题

9

帖子

16

积分

新手上路

Rank: 1

积分
16
发表于 2023-1-7 21:55:23 | 显示全部楼层 |阅读模式
有时候我们经常需要在C++里开发高性能的函数库并提供给其他语言或者程序使用,比如封装Python接口或者前端程序的通信接口。之前介绍过在windows下用Pybind11实现Python与C++混合编程与链接,就是用C++生成了高性能的动态库.pyd文件提供给Python调用

不过.pyd是特殊的动态库文件,仅能让指定版本的python使用,这里我们要探讨通用的动态库形式,即windows下直接编译生成的dll文件。通过C++工程导出生成的一组DLL文件,对不同语言分别提供接口即能直接链接使用(C/C++/Python,提供h文件/py文件接口)。
VC中的链接库

静态库:包含了完整的库代码,即静态库中的指令全部被直接嵌入在最终生成的exe文件中。在vs2019生成静态库的工程,编译后只产生一个.lib文件。
动态库:包含可由多个程序同时使用的代码和数据的库,DLL不是可执行的文件,函数的可执行代码位于一个DLL中,该DLL中包含多个已经被编译的分开存储的函数,在vs2019中生成动态库的工程,编译后产生一个.lib和一个.dll文件。
静态库的lib包含函数的代码本身(包括函数的索引,也包括实现),在编译时将代码加入程序当中。动态库的lib包含了函数所在的DLL文件和文件中函数位置的信息,函数实现代码由运行时加载DLL提供。lib是编译时要输入的,而dll是动态库的程序运行时需要的,在windows平台我们大多数时候以动态库的形式导出。
C++导出工程设置

不使用VS默认的动态链接库工程模板,而是从空的C++项目开始设置“myfunc”,调整输出目录,把配置类型改为动态库.dll。


预编译头加上一个自定义的导出宏DLL_EXPORTS


这样我们就可以根据这个宏来控制函数是导出函数还是外部导入,写法如下
#if defined(DLL_EXPORTS)
#define MYFUNC_API __declspec(dllexport)
#else
#define MYFUNC_API __declspec(dllimport)
#endif
导出C++函数,需要在函数前或类名后加上关键字__declspec(dllexport),否则就是普通函数,只能内部访问。如果在别的项目引入该头文件,没有DLL_EXPORTS预编译宏,那么就是外部导入函数(该项目使用外部myfunc.dll中的函数)
在导出项目“myfunc”的头文件中定义两个函数作为导出函数,我们定义了宏 MYFUNC_API 作为导出关键字,需要在函数声明前加上该标志
myfunc.h

#if defined(DLL_EXPORTS)
#define MYFUNC_API __declspec(dllexport)
#else
#define MYFUNC_API __declspec(dllimport)
#endif
namespace myfunc {
        MYFUNC_API int* add(int* arr1, int* arr2, size_t arr_len);
        MYFUNC_API double* add(double* arr1, double* arr2, size_t arr_len);
}



myfunc.cpp

#include "myfunc.h"

MYFUNC_API int* myfunc::add(int* arr1, int* arr2, size_t arr_len)
{
        int* res = new int[arr_len];

#pragma omp parallel for
        for (int i = 0; i < arr_len; i++)
        {
                res = arr1 + arr2;
        }
        return res;
}

MYFUNC_API double* myfunc::add(double* arr1, double* arr2, size_t arr_len)
{
        double* res = new double[arr_len];

#pragma omp parallel for
        for (int i = 0; i < arr_len; i++)
        {
                res = arr1 + arr2;
        }
        return res;
}
定义了两个数组相加的函数,内部使用openMP并行加速,导出到dll,编译生成后在输出目录中得到myfunc.lib和myfunc.dll两个文件。
外部C++项目使用DLL

外部C++项目编译时需要使用 myfunc.h 和 myfunc.lib 文件(函数声明和函数定义入口位置),生成可执行文件后运行时需要使用 myfunc.dll 文件。
新建一个控制台C++工程,输出目录和中间目录配置与myfunc配置相同,这是为了方便在输出目录直接找到依赖的 lib 和 dll ;项目属性->链接器->常规->附加库目录:$(OutDir),项目属性->链接器->输入->附加依赖项:myfunc.lib。




如果不想手动改属性文件,也可以在引入外部函数声明前加上 #pragma comment(lib,"myfunc.lib") 指令,即在头文件这样书写:
#if defined(DLL_EXPORTS)
#define MYFUNC_API __declspec(dllexport)
#else
#define MYFUNC_API __declspec(dllimport)
#pragma comment(lib,"myfunc.lib")
#endif
测试应用工程 funcTest 没有定义 DLL_EXPORTS,那么就把 MYFUNC_API 开头的函数声明为从外部导入的函数并且编译时链接对应的 lib 文件,这样只需引入头文件接口即可。在C/C++附加包含目录里加上myfunc.h的上级路径,在 funcTest 代码中直接 #include "myfunc.h" 即可。


添加包含目录后就可以在外部依赖项里看到引入的头文件,不需要单独拷贝多份到每个项目中来。


测试外部函数 myfunc::add() ,成功运行
#include <iostream>
#include "myfunc.h"

#define ARR_LEN_1 10

template <typename T>
void printArr(T* arr, size_t len) {
        for (size_t i = 0; i < len; i++)
        {
                std::cout << arr << "\t";
        }
        std::cout << "\n";
}

int main()
{
        int a1[ARR_LEN_1] = { 1,2,3,4,5,6,7,8,9,10 };
        int a2[ARR_LEN_1] = { 1,2,3,4,5,6,7,8,9,10 };
        int* a3 = myfunc::add(a1, a2, ARR_LEN_1);
        printArr<int>(a1, ARR_LEN_1);
        printArr<int>(a2, ARR_LEN_1);
        printArr<int>(a3, ARR_LEN_1);
        std::cin.get();
        if (a3) delete a3;
}


有时候我们更改了dll中的函数,但是引用它的项目并不知晓,还是使用旧版的函数,这样可能会产生潜在错误。这时候就要添加项目依赖项来控制项目生成的顺序


这样编译时总是先生成myfunc,再生成funcTest,保证funcTest中用到的总是最新的lib和dll。
C++导出函数名混乱的问题

C++代码项目导入该头文件可以正常使用,但是C工程就不行了,这涉及到C++编译器对函数名的特殊处理。
利用visual studio的dumpbin工具可以查看dll中的函数名在VS中如保快速查看DLL或exe的已导出的函数,新建一个外部工具“DLL导出查看”,命令输入dumpbin.exe的路径,在对应的VC版本下面找,我这里是 C:\Program Files (x86)\Microsoft Visual Studio\2019\Community\VC\Tools\MSVC\14.29.30133\bin\Hostx64\x64\dumpbin.exe,参数为 /exports $(TargetName)$(TargetExt),初始目录 $(TargetDir),即在目标目录下查看生成的目标文件,这里即查看工程输出目录下的“myfunc.dll”文件。


选中该项目下的一个文件或者项,在“工具”选项下选择刚才建立的“DLL导出查看”,即可显示DLL中的导出函数名:


可以看到有两个函数“?add@myfunc@@YAPEAHPEAH0_K@Z”和“?add@myfunc@@YAPEANPEAN0_K@Z”,这一长串“乱码”其实就是刚才我们导出的两个add函数,和原始函数名相差巨大。
C++ 动态库导出函数名“乱码”及解决 - 流水江湖 - 博客园 (cnblogs.com)
简单来说就是C++编译器背着程序员把函数名打乱重组了,因为C++是支持命名空间、多态和同名函数重载的(不同参数类型或数量),C++编译器会按照一定规则去修饰原函数名称得到真正的唯一识别的函数名,并且不同C++编译器生成的规则也不同,直接解析调用约定很是繁琐,这样其他语言想要使用这个dll就没有办法知道确切的函数名了。解决方案是导出为C语言的函数,C语言很“干净”,这样导出就是“所见即所得“,使用 extern "C"。
一个通用的做法是在 myfunc.h 在增加一个C语言版本的接口 cmyfunc.h,重新命名为C风格的函数一起加入到dll中,这样一份dll中既有C++的函数又有C的函数,只需用到不同的头文件即可。
cmyfunc.h

#pragma once

#if defined(DLL_EXPORTS)
#define MYFUNC_API __declspec(dllexport)
#else
#define MYFUNC_API __declspec(dllimport)
#pragma comment(lib,"myfunc.lib")
#endif

MYFUNC_API int* myfuncAdd_i(int* arr1, int* arr2, size_t arr_len);
MYFUNC_API double* myfuncAdd_d(double* arr1, double* arr2, size_t arr_len);

cmyfunc.cpp

#include "myfunc.h"

extern "C" { //作用在所有cmyfunc.h中的函数上
#include "cmyfunc.h"
}

MYFUNC_API int* myfuncAdd_i(int* arr1, int* arr2, size_t arr_len)
{
        return myfunc::add(arr1, arr2, arr_len);
}

MYFUNC_API double* myfuncAdd_d(double* arr1, double* arr2, size_t arr_len)
{
        return myfunc::add(arr1, arr2, arr_len);
}
cmyfunc.cpp中的函数不需要重新实现,只需要调用一次myfunc.h中的函数即可,相当于给C++函数套了一个C的接口。重新编译、查看,可以看到dll中的函数名增加了两个,就是我们刚才在cmyfunc.h中定义的,看着舒服多了。


新建一个C语言项目进行测试运行成功(VS中没有C语言项目模板,需要新建空的C++项目,添加源文件时.cpp改为.c即可)
#include "cmyfunc.h"
#include <stdio.h>

#define ARR_LEN_1 10

void printArri(int* arr, size_t len) {
        for (size_t i = 0; i < len; i++)
        {
                printf("%d\t", arr);
        }
        printf("\n");
}
int main()
{
        int a1[ARR_LEN_1] = { 1,2,3,4,5,6,7,8,9,10 };
        int a2[ARR_LEN_1] = { 1,2,3,4,5,6,7,8,9,10 };
        int* a3 = myfuncAdd_i(a1, a2, ARR_LEN_1);
        printArri(a1, ARR_LEN_1);
        printArri(a2, ARR_LEN_1);
        printArri(a3, ARR_LEN_1);
        if (a3) free(a3);
}


下一篇我们来讲解如何在python中使用原生的dll文件。
回复

使用道具 举报

1

主题

5

帖子

7

积分

新手上路

Rank: 1

积分
7
发表于 2023-1-7 21:56:01 | 显示全部楼层
为什么我使用多线程MT编译了64位dll 软件查看 函数也导出成功了 但是报一堆api-ms开头的dll找不到 头疼
回复

使用道具 举报

4

主题

9

帖子

16

积分

新手上路

Rank: 1

积分
16
发表于 2023-1-7 21:56:23 | 显示全部楼层
编译成功不代表运行成功,运行时要加载dll,你要把api-ms 开头的dll 都拷贝到运行目录来
回复

使用道具 举报

3

主题

10

帖子

16

积分

新手上路

Rank: 1

积分
16
发表于 2023-1-7 21:56:55 | 显示全部楼层
这些dll 在你的visual studio编译工具箱里,但是可能没有加入系统环境变量,运行时找不到,可以用dumpbin /dependents查看一下依赖项,把依赖的dll都拷贝过来
回复

使用道具 举报

您需要登录后才可以回帖 登录 | 立即注册

本版积分规则

快速回复 返回顶部 返回列表