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 __cplusplusextern "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 __cplusplusextern "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
#endifWINUSERAPI
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 __cplusplusextern "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.rhkb.cn/news/107396.html

如若内容造成侵权/违法违规/事实不符,请联系长河编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

基于RUM高效治理网站用户体验入门-价值篇

用户体验 用户体验基本包含访问网站的性能、可用性和正确性。通俗的讲,就是一把通过用户访问测量【设计者】意图的尺子。 本文目的 网站如何传递出设计者的意图,可能收页面加载时间太长、或者页面在用户的浏览器中渲染时间太慢,或者第三方设…

使用Maven父工程构建spring boot子工程

1.父工程删除src目录,pom文件配置parent为spring-boot-starter-parent 2.创建子工程,子工程引入一个springboot相关依赖 注意:子工程引入springboot相关依赖之后子工程才能被解析为springboot模块

单片机学习-蜂鸣器如何发出声音

硬件电路 软件编写 ①发出声音 #include "reg52.h" typedef unsigned int u16; // 重新定义 类型 typedef unsigned char u8; // 重新定义 类型sbit BEEP P2^5; //定义 P2第五个管教 为BEEP // 延时函数 void delay_time(u16 times) {while(times--); } vo…

qt tableview setEditTriggers解析

EditTrigger EditTrigger是QAbstractItemView Class的Public Function This property holds which actions will initiate item editing 此属性保存哪些操作将启动项编辑 This property is a selection of flags defined by EditTrigger, combined using the OR operator. T…

python编程环境使用技巧2-python环境迁移

Python环境迁移步骤 将Python环境从一个计算机迁移到另一个计算机可以按照以下步骤进行: 1-备份环境: 在源计算机上,使用pip工具备份当前Python环境的包列表到一个文本文件。在命令行终端中执行以下命令: pip freeze > requi…

layUI 中 穿梭框无法获取值的细节问题

初始化的时候一定要指定id,不然就会出现无法调用 获得右侧数据和实例重载的方法

C++ Primer 第2章 变量和基本类型

C Primer 第2章 变量和基本类型 2.1 基本内置类型2.1.1 算术类型一、带符号类型和无符号类型练习 2.1.2 类型转换一、含有无符号类型的表达式 2.1.3 字面值常量一、整型和浮点型字面值二、字符和字符串字面值三、转义序列四、指定字面值的类型五、布尔字面值和指针字面值 2.2 变…

网络字节序——TCP接口及其实现简单TCP服务器

网络字节序——TCP接口及其实现简单TCP服务器 文章目录 网络字节序——TCP接口及其实现简单TCP服务器简单TCP服务器的实现1. 单进程版:客户端串行版2. 多进程版:客户端并行版netstat查看网络信息3.多线程版:并行执行log.hpp 守护进程fg、bg s…

Vue的使用

Vue的使用 Vue到底是啥?Vue中包含了两部分虚拟DOM 模块化编程虚拟DOM,在我们重用模板的时候,在Vue中存在虚拟DOM 虚拟DOM是为了更好的去重用我们的DOM (增加元素的时候,先去虚拟DOM找是否存在,如果有那么不用生成&am…

android系统启动流程之init启动分析

先根据上图来描述下安卓整个系统的启动流程: 当上电时,系统先执行BootRom, 加载引导程序执行。 然后进入bootloader,在安卓系统中基本上这个bootloader是uboot, 通过uboot引导启动内核,此时运行在kernel空间,这时的i…

【Go 基础篇】Go语言中的自定义错误处理

错误是程序开发过程中不可避免的一部分,而Go语言以其简洁和高效的特性闻名。在Go中,自定义错误(Custom Errors)是一种强大的方式,可以为特定应用场景创建清晰的错误类型,以便更好地处理和调试问题。本文将详…

基于Googlenet深度学习网络的交通工具种类识别matlab仿真

目录 1.算法运行效果图预览 2.算法运行软件版本 3.部分核心程序 4.算法理论概述 5.算法完整程序工程 1.算法运行效果图预览 2.算法运行软件版本 matlab2022a 3.部分核心程序 ....................................................................................% 获…

SpringCloud之断路器聚合监控

一、Hystrix Turbine简介 看单个的Hystrix Dashboard的数据并没有什么多大的价值,要想看这个系统的Hystrix Dashboard数据就需要用到Hystrix Turbine。Hystrix Turbine将每个服务Hystrix Dashboard数据进行了整合。Hystrix Turbine的使用非常简单,只需要…

CSS中如何改变鼠标指针样式(cursor)?

聚沙成塔每天进步一点点 ⭐ 专栏简介⭐ CSS中改变鼠标指针样式(cursor)⭐ 示例:⭐ 写在最后 ⭐ 专栏简介 前端入门之旅:探索Web开发的奇妙世界 记得点击上方或者右侧链接订阅本专栏哦 几何带你启航前端之旅 欢迎来到前端入门之旅…

废品回收抢单派单小程序开源版开发

废品回收抢单派单小程序开源版开发 用户注册和登录:用户可以通过手机号码注册和登录小程序,以便使用废品回收抢单派单功能。废品回收订单发布:用户可以发布废品回收订单,包括废品种类、数量、回收地点等信息。废品回收抢单&#…

【CSS】网站 网格商品展示 模块制作 ( 清除浮动需求 | 没有设置高度的盒子且内部设置了浮动 | 使用双伪元素清除浮动 )

一、清除浮动需求 ( 没有设置高度的盒子且内部设置了浮动 ) 绘制的如下模块 : 在上面的盒子中 , 没有设置高度 , 只设置了一个 1215px 的宽度 ; 在列表中每个列表项都设置了 浮动 ; /* 网格商品展示 */ .box-bd {/* 处理列表间隙导致意外换行问题一排有 5 个 228x270 的盒子…

Aidex 移动端快速开发框架# RuoYi-Uniapp项目,uniapp vue app项目跨域问题

参考地址: manifest.json官方配置文档:manifest.json 应用配置 | uni-app官网 Chrome 调试跨域问题解决方案之插件篇: uni-app H5跨域问题解决方案(CORS、Cross-Origin) - DCloud问答 其实uni-app官方有解决跨域的办…

逆向抓包大神

0x01 前言 抓包应该是我们逆向的第一步,只有先抓到包,才能决定我们是否要进行脱壳、逆向。万一他没有加密、万一数据不是我们想要的那岂不是白忙活了。但是目前很APP都设置了门槛,比如新版的抖音、淘宝、天眼查等挂上代理就直接无数据或者就…

Skywalking Kafka Tracing实现

背景 Skywalking默认场景下,Tracing对于消息队列的发送场景,无法将TraceId传递到下游消费者,但对于微服务场景下,是有大量消息队列的业务场景的,这显然无法满足业务预期。 解决方案 Skywalking的官方社区中&#xf…

企业展示小程序的制作流程及关键步骤详解

在移动互联网时代,企业展示小程序已成为各个行业推广和展示的重要工具。搭建一个企业展示小程序不仅能够提高企业形象,还能够增加用户粘性和提升用户体验。下面我们来看一下如何从零基础搭建一个企业展示小程序,并顺利上线。 第一步&#xff…