C++动态库编程 | C++名称改编、标准C接口、extern “C”、函数调用约定以及def文件详解

news2024/9/27 21:20:20

目录

1、导入导出声明

2、C++函数名称改编与extern "C"

3、函数调用约定与跨语言调用

3.1、函数调用约定

3.2、跨语言调用dll库接口

3.3、函数调用约定以哪个为准

4、def文件的使用

5、在C++程序中引用ffmpeg库中的头文件链接报错问题

6、最后


VC++常用功能开发汇总(专栏文章列表,欢迎订阅,持续更新...)icon-default.png?t=N6B9https://blog.csdn.net/chenlycly/article/details/124272585C++软件异常排查从入门到精通系列教程(专栏文章列表,欢迎订阅,持续更新...)icon-default.png?t=N6B9https://blog.csdn.net/chenlycly/article/details/125529931C++软件分析工具从入门到精通案例集锦(专栏文章正在更新中...)icon-default.png?t=N6B9https://blog.csdn.net/chenlycly/article/details/131405795C/C++基础与进阶(专栏文章,持续更新中...)icon-default.png?t=N6B9https://blog.csdn.net/chenlycly/category_11931267.html       最近有个前同事打微信电话问一个包含ffmpeg开源库头文件后编译链接失败的问题,其实很简单,只需要在包含头文件时加上extern "C"就可以解决了。今天正好有时间,就来详细讲讲C++ dll动态库编程中关于导出接口相关的内容,包括接口的导入导出声明C++函数名称改编extern "C"作用、标准C接口函数调用约定声明跨语言调用dll接口def文件等内容,本文将通过一个具体的dll动态库实例来详细展开,希望能给大家(特别是新人)提供一定的借鉴或参考。

1、导入导出声明

        在Windows平台编写C++ dll动态库时,提供给外部调用的接口需要添加__declspec(dllexport)导出声明,这样外部模块才能调用这些导出接口。一般我们会在dll动态库的api头文件中添加这样的定义:(以下 dll 动态库实例是在 Visual Studio 中创建的,即 IDE 开发环境为 Visual Studio

#ifdef WIN32
#ifdef HCNETSDKDLL_EXPORTS
#define NET_SDK_API __declspec(dllexport)  // 声明为导出
#else
#define NET_SDK_API __declspec(dllimport)  // 声明为导入
#endif  KdvMt_Pc_EXPORTS
#else
#define NET_SDK_API
#endif   WIN32


#ifdef __cplusplus
    extern "C"
    {
#endif
        // 设置业务消息回调接口
        NET_SDK_API void __stdcall SetMsgCallBack(PMsgCallBackFunc pMsgCallBackFunc);
        // 初始化SDK库
        NET_SDK_API DWORD __stdcall InitNetSDK();
        // 登录
        NET_SDK_API DWORD __stdcall LoginServer(const TLoginParam& tLoginParam);

#ifdef __cplusplus
    }
#endif

其中宏HCNETSDKDLL_EXPORTS是dll库中定义的,在dll工程属性配置的C/C++ -> 预处理 -> 预处理定义中可以看到该宏的定义,如下 :

该宏是创建dll工程时自动生成的,宏名称的前半部分就是工程名称。

关于__declspec(dllexport)和__declspec(dllimport)

1)对dll库本身而言,接口是要导出给外部调用的,所以导出接口要声明为__declspec(dllexport);

2)对要调用dll库的外部模块,则是要引入dll库的导出接口,所以要使用__declspec(dllimport)。

2、C++函数名称改编与extern "C"

        C++之所以支持函数重载,是因为C++编译器在编译代码时会对函数名称进行改编。改编后的函数名称包含参数信息,这样就能将重载的函数区分开来了。下面是个简单的函数重载范例:

int AddNum( int a, int b );
double AddNum( double a, double b);

重载的函数名称是相同的,但参数类型是不同的。要将函数重载(overload)和函数重写(override)区分开来,两者有着本质的区别。

       创建了一个简单的dll动态库工程,在工程中提供了几个导出接口,如下所示:

// NET_SDK_API宏用来指定是导入还是导出
#ifdef WIN32
#ifdef HCNETSDKDLL_EXPORTS
#define NET_SDK_API __declspec(dllexport)
#else
#define NET_SDK_API __declspec(dllimport)
#endif  KdvMt_Pc_EXPORTS
#else
#define NET_SDK_API
#endif   WIN32


// 设置业务消息回调接口
NET_SDK_API void SetMsgCallBack(PMsgCallBackFunc pMsgCallBackFunc);
// 初始化SDK库
NET_SDK_API DWORD InitNetSDK();
// 登录
NET_SDK_API DWORD LoginServer(const TLoginParam& tLoginParam);

编译代码,生成dll库文件HCNetSDKDll.dll,然后用Dependency Walker查看该dll库导出接口的名称(进行名称改编后的名称):

从上图中可以看出,改编后的函数名称中包含了函数参数信息

注意,在Win10及以上系统中Dependency Walker打开dll库会很慢,可能是Dependency Walker工具比较老,对新的Win10及以上系统兼容性不太好。有时可能需要数分钟才能打开dll文件,在使用时要耐心等待。

       但有时C++编写的dll模块可能会被C语言程序或者其他语言(比如C#)程序调用,需要导出标准C的接口(函数只有函数名,没有其他额外的信息)才能正常被调用。一般C++项目中,各个模块使用的都是C++开发语言,IDE开发工具基本都是一样的,不用考虑这样的问题。

       C++编译器在默认情况下会对函数名称进行改编,如何让编译器不对函数名称进行改编呢?可以使用extern "C"将所有的导出接口包起来。extern "C"标识告诉编译器在编译时以C语言的方式去处理,不要对声明的函数接口进行名称改编同样以上述dll工程为例,将导出接口用extern "C"包起来,如下所示:

#ifdef WIN32
#ifdef HCNETSDKDLL_EXPORTS
#define NET_SDK_API __declspec(dllexport)
#else
#define NET_SDK_API __declspec(dllimport)
#endif  KdvMt_Pc_EXPORTS
#else
#define NET_SDK_API
#endif   WIN32


#ifdef __cplusplus
    extern "C"  // 使用extern "C"
    {
#endif
        // 设置业务消息回调接口
        NET_SDK_API void __stdcall SetMsgCallBack(PMsgCallBackFunc pMsgCallBackFunc);
        // 初始化SDK库
        NET_SDK_API DWORD __stdcall InitNetSDK();
        // 登录
        NET_SDK_API DWORD __stdcall LoginServer(const TLoginParam& tLoginParam);

#ifdef __cplusplus
    }
#endif

然后重新编译代码,再用Dependency Walker查看dll库的导出接口,如下所示:

确实生成了只有函数名的导出接口。

       此外,C++中可以导出函数,也可以导出整个类。对于导出类,可以直接在外部直接使用类。但extern “C”只对导出函数起作用,对导出类(整个类导出)的成员函数不起作用。

3、函数调用约定与跨语言调用

        让C++实现的dll库导出标准C接口,使用extern "C"就好了,事情到此好像就结束了,但事实上并没有结束。Windows平台上还有个函数调用约定的概念。

3.1、函数调用约定

       调用约定是用来声明函数的,常见的函数调用约定有__cdecl C调用、__fastcall快速调用以及__stdcall标准调用等。调用约定决定了函数调用时参数入栈的先后顺序(参数不一定使用栈传递,可能会直接使用寄存器传递),还决定了谁来释放传递参数占用的栈空间不同的开发语言,默认的调用约定可能是不一样的,比如C++中默认的是C调用、C#中默认的是标准调用。如果存在dll库跨语言调用时,一定要明确声明dll库导出接口的调用约定。

       Windows提供的系统API函数使用的都是标准调用约定,比如获取窗口文字的API函数GetWindowText:(WINAPI是函数调用约定宏,对应__stdcall标准调用)

#if !defined(_USER32_)
#define WINUSERAPI DECLSPEC_IMPORT
#define WINABLEAPI DECLSPEC_IMPORT
#else
#define WINUSERAPI
#define WINABLEAPI
#endif

#ifndef WINAPI
#define WINAPI __stdcall
#endif

WINUSERAPI
int
WINAPI /* 此处设置函数调用约定为__stdcall*/
GetWindowTextW(
    __in HWND hWnd,
    __out_ecount(nMaxCount) LPWSTR lpString,
    __in int nMaxCount);


#ifdef UNICODE
#define GetWindowText  GetWindowTextW
#else
#define GetWindowText  GetWindowTextA
#endif // !UNICODE

参照Windows系统API函数,我们一般也将dll库的导出接口声明为标准约定。这个地方需要注意一下,除了导出接口都要明确声明调用约定,回调函数也要声明调用约定。给dll库设置回调函数,dll库通过调用回调函数,给主调模块回调数据。回调函数是dll中声明的,但在上层调用模块实现的(完整的函数代码实现),回调函数的调用是在dll库内部的。

       关于函数调用约定的详细内容,可以参考我之前写的文章:

C/C++函数的调用约定详解icon-default.png?t=N6B9https://blog.csdn.net/chenlycly/article/details/125354572

3.2、跨语言调用dll库接口

       为什么在跨语言调用的场景下需要明确声明dll库的导出接口的函数调用呢?是有原因的,假设调用C++实现的dll库的上层模块或程序是C#语言开发的,如果在dll的头文件中不明确声明导出接口的调用约定,则在使用Visual Studio编译C++实现的dll文件时,由于没有函数调用约定,默认使用C调用,而C调用下传递参数占用的栈空间是主调函数去释放的,所以dll库中编译生成的函数代码中就不会有清理传递参数占用的栈空间的二进制代码。

       而上层C#模块,默认是标准调用,在编译到调用dll库导出接口的代码时,由于标准调用下传递参数占用的栈空间是由被调用函数释放的,所以不会生成释放传递参数栈空间的二进制代码。所以在这种场景下,主调函数不会清理传递参数占用的栈空间,被调函数函数也不会清理被调函数占用的栈空间,这样就导致了栈不平衡,就会导致使用ebp去寻址栈内存出现异常,进而引发崩溃。

       我们以前就遇到过这样的问题,我们提供给第三方厂商的软件SDK是用C++实现的,第三方厂商C#开发的程序来调用我们的SDK模块,当时就因为在声明回调函数时没有指定函数调用约定,导致栈不平衡,引发了崩溃

3.3、函数调用约定以哪个为准

       使用Visual Studio创建的dll工程,在工程属性配置(C/C++ -> 高级 -> 调用约定)中,默认为_cdecl C调用,如下所示:

 这是创建工程时的默认配置。

        我们也可以在函数声明处指定调用约定,如下所示:

// 初始化SDK库(将该函数的调用约定指定为__stdcall标准调用)
NET_SDK_API DWORD __stdcall InitNetSDK();

当函数前有指定调用约定,且工程属性配置中也有设置调用约定时,以函数前声明的调用约定为准

4、def文件的使用

       在我们的示例代码中添加调用约定的声明,声明为__stdcall标准调用,如下所示:

#ifdef WIN32
#ifdef HCNETSDKDLL_EXPORTS
#define NET_SDK_API __declspec(dllexport)
#else
#define NET_SDK_API __declspec(dllimport)
#endif  KdvMt_Pc_EXPORTS
#else
#define NET_SDK_API
#endif   WIN32


#ifdef __cplusplus
    extern "C"  // 使用extern "C"
    {
#endif
        // 设置业务消息回调接口
        NET_SDK_API void __stdcall SetMsgCallBack(PMsgCallBackFunc pMsgCallBackFunc);
        // 初始化SDK库
        NET_SDK_API DWORD __stdcall InitNetSDK();
        // 登录
        NET_SDK_API DWORD __stdcall LoginServer(const TLoginParam& tLoginParam);

#ifdef __cplusplus
    }
#endif

重新编译代码,然后使用Dependency Walker工具查看新生成的dll文件,结果看到函数符号变了(本来添加了extern “C”标识后,已经导出了标准C接口,结果添加了__stdcall调用约定后,又不再是标准C函数了),如下:

不再是标准的C接口了,接口前面多了个下划线,接口后面多了个数字,这个数字其实是参数占用的栈空间大小。

       考虑跨语言调用dll库的场景,我们需要导出标准的C接口,结果即使使用了extern "C",还是没有生成标准C接口,这可如何是好呢?是有办法的,下面就轮到def模块定义文件登场了!def文件内容比较简单,主要分两块:

1)第一块是LIBRARY语句部分,指明对应的dll库名称;

2)第二块是EXPORTS语句部分,用来指定要导出的接口。

我们将要导出的接口都罗列到EXPORTS语句部分里面就好了,这样最终生成的dll库文件中导出的就是标准C接口了。本范例中的def文件如下所示:

要手动生成def文件比较简单,先手动创建一个.txt文件,然后手动将之改成.def后缀,然后手动将LIBRARY语句和EXPORTS语句部分的文字拷贝进来修改一下即可。

       所以,在我们这个dll动态库示例工程中,要实现导出标准的C接口,要使用导入导出声明,要声明函数调用约定,要使用extern "C",也要使用到def文件。

5、在C++程序中引用ffmpeg库中的头文件链接报错问题

       一个前同事在其C++项目中引用了ffmpeg开源库中的头文件,结果编译时报链接的错误,如下所示:

找到我,让我帮忙看一下如何处理这个错误。因为ffmpeg库是用C语言开发的,编译生成的dll库的导出接口肯定是标准C接口。在C++项目中直接包含ffmpeg中的头文件,编译时默认链接的是经过名称改编的函数符号,而ffmpeg.lib中的都是标准C接口,函数符号只有函数名,不是改编后的符号,所以链接时找不到,报错了!

       其实这处理起来也比较简单,在包含头文件时用extern "C"包住就可以了,如下所示:

重新编译代码,就没问题了,不再报错了。extern "C"标识是告诉C++编译器以C语言方式去处理,去链接标准的C接口,所以在引入的ffmpeg.lib中能找到标准的C接口,所以就不再报错了。

       其实很多开源库都是C语言实现的,比如sqlitelibcurl等,在这些开源库提供的api头文件中就添加了extern "C"。比如sqlite开源数据库中的sqlite3.h头文件中:

在curl多协议网络传输开源库中的curl.h头文件中:

6、最后

       本文通过一个具体的dll动态库工程实例,详细讲解了如何一步一步地实现标准C导出接口的过程,这其中包括接口的导入导出声明、extern "C"作用、标准C接口、函数调用约定声明、跨语言调用dll接口以及def文件等内容。

       这里面涉及到的内容,C++新手是需要了解的,甚至有很多C++老手也不太清楚,希望本文能帮到大家,给大家提供一个借鉴或参考!

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/934422.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

Day46|leetcode 139.单词拆分

leetcode 139.单词拆分 题目链接:139. 单词拆分 - 力扣(LeetCode) 视频链接:动态规划之完全背包,你的背包如何装满?| LeetCode:139.单词拆分_哔哩哔哩_bilibili 题目概述 给你一个字符串 s 和一…

一文讲通物联网嵌入式

最近有很多同学问我,物联网近几年一直是科技的热点,嵌入式和物联网有什么关系呢?我在这里统一给大家讲解一下。 嵌入式是应用于物联网产品方向的一种嵌入式操作系统。类似于Android系统是谷歌开发的移动操作系统,嵌入式实际上也是…

如何使用 Tailwind CSS 设计高级自定义动画

使用Tailwind CSS掌握动画技术,为用户带来难忘的体验 开篇 动画已经成为网页设计的重要组成部分,使开发人员能够创建引人入胜和互动的用户体验。 Tailwind CSS,一款流行的实用型CSS框架,提供了一套强大的工具,可以轻松…

8款优秀的AI绘画工具,让您的绘画创作发挥无限想象力

今天为大家推荐8款优秀的AI绘画工具,帮助您在绘画创作中发挥无限想象力。这些工具结合了人工智能技术和艺术创作,能够帮助您实现更高水平的绘画作品。无论您是专业画家还是刚刚入门的爱好者,这些AI绘画工具都能为您提供灵感和支持。让我们一起…

【CSS】定位 ( 子元素绝对定位 父元素相对定位 | 代码示例 )

一、子元素绝对定位 父元素相对定位 绝对定位 要和 带有定位 的 父容器 搭配使用 ; 子元素 使用绝对定位 , 父元素要使用 相对定位 ; 子元素使用 绝对定位 , 在布局中不会保留其位置 , 子元素完全依赖 父容器 的位置 , 此时就要求父容器必须稳定 , 如果父容器使用了 绝对布…

Python-pyqt不同窗口数据传输【使用静态函数】

文章目录 前言程序1:caogao1.py输入数据界面程序2:caogao2.py接收数据界面 程序3 :将输入数据界面和接收数据界面组合成一个总界面讲解 总结 前言 在编写pyqt 页面时有时候需要不同页面进行数据传输。本文讲解静态函数方法。直接看示例。 程…

《纽约时报》禁止人工智能厂商未经许可抓取其内容

记录纸在吸收一切的人工智能商业模式上戳了洞 8月初,《纽约时报》更新了服务条款(TOS),禁止抓取其用于人工智能训练的文章和图片。报告广告周刊。此举正值科技公司继续利用人工智能语言应用赚钱之际,例如ChatGPT聊天机器人和谷歌诗人该公司通…

嵌入式系统入门实战:探索基本概念和应用领域

嵌入式系统是一种专用的计算机系统,它是为了满足特定任务而设计的。这些系统通常具有较低的硬件资源(如处理器速度、内存容量和存储容量),但具有较高的可靠性和实时性。嵌入式系统广泛应用于各种领域,如家用电器、汽车、工业控制、医疗设备等。 嵌入式系统的基本概念 微控…

AI个体户的崛起:普通人“屁胡”的机会、模式和风险

前几篇文章写的是大模型本身以及垂域中的系统型超级应用(AI Agent类),这些机会很大,不管你是真的做得来大模型,还是能做得了某个行业的系统型超级应用,成功者最终都会是这个时代的成功者。 是什么样子的时…

华为数通方向HCIP-DataCom H12-821题库(单选题:121-140)

第121题 关于OSPF特性描述错误的是 A、OSPF采用链路状态算法。 B、每个路由器通过泛洪 LSA 向外发布本地链路状态信息 C、每台 OSPF 设备都会收集其它路由器发来的LSA 所有的LSA 放在一起便组成了链路状态数据库LSDB, D、OSPF 区域0中所有路由器的 LSDB 都相同…

Spring中@Value注解取值为null问题排查

文章目录 一、背景二、Value 取值为 null 原因分析2.1. Value 取值为 null 常见原因分析常见现象一:类没有交给 Spring 管理,比如类没有加上 Component 等注解常见现象二:手动 new 对象实例,没有从 Spring 容器中获取常见现象三&a…

基于人工电场算法优化的BP神经网络(预测应用) - 附代码

基于人工电场算法优化的BP神经网络(预测应用) - 附代码 文章目录 基于人工电场算法优化的BP神经网络(预测应用) - 附代码1.数据介绍2.人工电场优化BP神经网络2.1 BP神经网络参数设置2.2 人工电场算法应用 4.测试结果:5…

Redis.conf详解

Redis.conf详解 配置文件unit单位对大小写不敏感 包含 网络 bind 127.0.0.1 # 绑定的ip protected-mode yes # 保护模式 port 6379 # 端口设置通用 GENERAL daemonize yes # 以守护进程的方式运行 默认为no pidfile /var/run/redis_6379.pid #如果以后台的方式运行&#xff…

VUE笔记(三)vue的语法

一、计算属性 1、计算属性的概念 计算属性是依赖于源数据(data或者属性中的数据),在元数据的基础上进行逻辑运算后得到的新的数据,计算属性要依赖于源数据,源数据数据变化计算属性也会变化 2、计算属性的语法 在vue2中使用computed这个选…

【Python】模块、包

模块 Python模块(Module),是一个Python文件,以.py结尾。模块能定义函数,类和变量,模块里也能保护可执行的代码。 不同模块,同名的功能,如果都被导入,那么后者会覆盖前者…

【LeetCode: 56. 合并区间+贪心+双指针+标识+模拟】

🚀 算法题 🚀 🌲 算法刷题专栏 | 面试必备算法 | 面试高频算法 🍀 🌲 越难的东西,越要努力坚持,因为它具有很高的价值,算法就是这样✨ 🌲 作者简介:硕风和炜,…

计算一组数据的高中位数statistics.median_high()

【小白从小学Python、C、Java】 【计算机等考500强证书考研】 【Python-数据分析】 计算一组数据的高中位数 statistics.median_high() [太阳]选择题 下列说法错误的是? import statistics print(【执行】statistics.median_high([1, 5, 4, 3, 6, 2])) print(statistic…

cortex-A7中断实验 --- STM32MP157

实验目的 1,实现KEY1/LEY2/KE3三个按键,中断触发打印一句话 2,实现KEY1/LEY2/KE3三个按键按下之后,灯的状态取反 一,异常源和异常模式 1,异常源:异常源引发处理器进入对应的异常模式 2&…

Springboot profile多环境配置

1. 前言 profile用于多环境的激活和配置,用来切换生产,测试,本地等多套不通环境的配置。如果每次去更改配置就非常麻烦,profile就是用来切换多环境配置的。 2. 配置方法 三种方式。 2.1 多profile文件方式 在resource目录下新…