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

目录

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++老手也不太清楚,希望本文能帮到大家,给大家提供一个借鉴或参考!

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

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

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

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

ppt如何转pdf文档?用这个方法可将ppt转pdf

在现代社会中,PPT(幻灯片)已成为一种常见的演示工具,被广泛应用于学术、商务、培训等领域。然而,PPT文件的使用和分享存在一些问题,例如文件格式不兼容、内容修改易被篡改等。为了解决这些问题,将PPT转换为PDF格式已成…

leetcode 541.反转字符串II

⭐️ 题目描述 🌟 leetcode链接:https://leetcode.cn/problems/reverse-string-ii/ ps: 这道题描述的有点晦涩难懂,意思就是每隔k个反转k个,末尾不够k个时全部反转,开始就不够k个也全部反转。 代码&#…

HarmonyOS ArkUI 属性动画入门详解

HarmonyOS ArkUI 属性动画入门详解 前言属性动画是什么?我们借助官方的话来说,我们自己简单归纳下 参数解释举个例子旋转动画 位移动画组合动画总结 前言 鸿蒙OS最近吹的很凶,赶紧卷一下。学习过程中发现很多人吐槽官方属性动画这一章比较敷…

【ubuntu】 20.04 网络连接器图标不显示、有线未托管、设置界面中没有“网络”选项等问题解决方案

问题 在工作中 Ubuntu 20.04 桌面版因挂机或不当操作,意外导致如下问题 1、 Ubuntu 网络连接图标消失 2、 有线未托管 上图中展示的是 有线 已连接 ,故障的显示 有限 未托管 或其他字符 3、 ”设置“ 中缺少”网络“选项 上图是设置界面&#xff0c…

Mysql中explain执行计划信息中字段详解

Mysql中explain执行计划信息中字段详解 1. 获取执行计划2. 字段含义2.1 id2.2 select_type2.3 table2.4 partitions2.5 type2.6 possible_keys2.7 key2.8 ley_len2.9 ref2.10 rows2.11 extra 1. 获取执行计划 explain select * from t1; --或 desc select * from t1;2. 字段含…

Java入职第十一天,深入了解静态代理和动态代理(jdk、cglib)

一、代理模式 一个类代表另一个类去完成扩展功能,在主体类的基础上,新增一个代理类,扩展主体类功能,不影响主体,完成额外功能。比如买车票,可以去代理点买,不用去火车站,主要包括静态代理和动态代理两种模式。 代理类中包含了主体类 二、静态代理 无法根据业务扩展,…

java八股文面试[Spring]——如何实现一个IOC容器

什么是IOC容器 IOC不是一种技术,只是一种思想,一个重要的面向对象编程的法则,它能指导我们如何设计出松耦合,更优良的程序。传统应用程序都是由我们在类内部主动创建依赖对象,从而导致类与类之间高耦合,难于…

阿里云服务器搭建FRP实现内网穿透-P2P

前言 在了解frp - p2p之前,请先了解阿里云服务器搭建FRP实现内网穿透-转发: 文章地址 1、什么是frp - p2p frp(Fast Reverse Proxy)是一个开源的反向代理工具,它提供了多种功能,包括端口映射、流量转发和内网穿透等。…

电脑视频编辑软件前十名 电脑视频编辑器怎么剪辑视频

对于大多数创作者而言,视频后期工作基本都是在剪辑软件上进行的。一款适合自己的视频剪辑软件,能够节省出大量的时间和金钱成本,让剪辑师省钱又省心。那么有关电脑视频编辑软件前十名,电脑视频编辑器怎么剪辑视频的相关问题&#…

Django基础1——项目实现流程

文章目录 一、前提了解二、准备开发环境2.1 创建项目2.1.1 pycharm创建2.1.2 命令创建 2.2 创建应用 例1:效果实现例2:网页展示日志文件 一、前提了解 基本了解: 官网Django是Python的一个主流Web框架,提供一站式解决方案&#xf…

机器学习深度学习——NLP实战(自然语言推断——注意力机制实现)

👨‍🎓作者简介:一位即将上大四,正专攻机器学习的保研er 🌌上期文章:机器学习&&深度学习——NLP实战(自然语言推断——数据集) 📚订阅专栏:机器学习&…

C语言暑假刷题冲刺篇——day5

目录 一、选择题 二、编程题 🎈个人主页:库库的里昂 🎐CSDN新晋作者 🎉欢迎 👍点赞✍评论⭐收藏✨收录专栏:C语言每日一练✨相关专栏:代码小游戏、C语言初阶、C语言进阶🤝希望作者…

JDBC详解

文章目录 一、引言1.1 如何操作数据库1.2 实际开发中,会采用客户端操作数据库吗? 二、JDBC(Java Database Connectivity)2.1 什么是 JDBC?2.2 JDBC 核心思想2.2.1 MySQL 数据库驱动2.2.2 JDBC API 2.3 环境搭建 三、JD…

使用飞桨实现的第一个AI项目——波士顿的房价预测

part1.首先引入相应的函数库: 值得说明的地方: (1)首先,numpy是一个python库,主要用于提供线性代数中的矩阵或者多维数组的运算函数,利用import numpy as np引入numpy,并将np作为它的别名 part…

4.16 TCP 协议有什么缺陷?

目录 升级 TCP 的工作很困难 TCP 建立连接的延迟 TCP 存在队头阻塞问题 网络迁移需要重新建立 TCP 连接 升级 TCP 的工作很困难;TCP 建立连接的延迟;TCP 存在队头阻塞问题;网络迁移需要重新建立 TCP 连接; 升级 TCP 的工作很…

Android开发之性能测试工具Profiler

前言 性能优化问题,在我们开发时都会遇到,但是在小厂和对自己要求不严格的情况下,我都很少去做性能优化; 在性能优化上,基本大家都是通过自己的开发经验和性能分析工具来发现问题,今天给大家分享一下小编最…

机器学习理论笔记(二):数据集划分以及模型选择

文章目录 1 前言2 经验误差与过拟合3 训练集与测试集的划分方法3.1 留出法(Hold-out)3.2 交叉验证法(Cross Validation)3.3 自助法(Bootstrap) 4 调参与最终模型5 结语 1 前言 欢迎来到蓝色是天的机器学习…

js中作用域的理解?

1.作用域 作用域,即变量(变量作用域又称上下文)和函数生效(能被访问)的区域或集合 换句话说,作用域决定了代码区块中变量和其他资源的可见性 举个例子 function myFunction() {let inVariable "函数内部变量"; } myFunction();//要先执行这…

SQL注入漏洞复现:探索不同类型的注入攻击方法

这篇文章旨在用于网络安全学习,请勿进行任何非法行为,否则后果自负。 准备环境 sqlilabs靶场 安装:详细安装sqlmap详细教程_sqlmap安装教程_mingzhi61的博客-CSDN博客 一、基于错误的注入 简介 基于错误的注入(Error-based I…