首页
关于我
友链
小程序
舔狗日记
小黑屋
音乐解锁
阿狸和桃子
Search
1
将校园卡复制到小米手环
26 阅读
2
windows11系统自带VPN连接不上群辉VPN服务器问题
22 阅读
3
使用ESP8266完成校园网WEB认证
19 阅读
4
数字振镜XY2-100协议
17 阅读
5
Verilog中为inout类型赋值
17 阅读
技术
生活
登录
Search
标签搜索
qt
mqtt
FreeRTOS
stm32
iar
VPN
光猫破解
esp8266
群晖
VideoStation
exti
c++
eclipse
bootloader
pixhawk
遥控器
失控保护
ARM
NAS
ENPASS
AMENG
累计撰写
79
篇文章
累计收到
17
条评论
首页
栏目
技术
生活
页面
关于我
友链
推荐
舔狗日记
小黑屋
音乐解锁
阿狸和桃子
搜索到
1
篇与
的结果
2024-03-29
C与C++相互调用的问题
案例描述首先简单描述一下同事的案例场景: 同事的项目工程依赖其他两个部门A和B提供的动态库libA.so,libB.so。它们之间的关系是:libB.so 依赖 libA.so 生成。之后他再依赖libB.so库生成abupvdiapp可执行程序。在最终生成可执行程序时,会提示部分符号未定义。而这些符号是在libA.so。依赖关系如下图:排查思路: 1、首先确认编译生成可执行程序abupvdiapp时,是否链接了libA.so。打开cmake 中的VERBOSE参数,发现的确有-lA -lB链接信息。 2、查看libB.so是否依赖libA.so。通过ldd libB.so查看,发现libB.so 并不依赖libA.so。这是我第一个疑惑点。 3、查看未定义符号adm_vdi_init是否在libA.so定义。结果是存在。 4、查看未定义符号adm_vdi_init是否被libB.so引用。结果是有被引用,但是符号不匹配。 其实到这里我已经知道什么原因了。后续在A.h的头文件中增加extern "C"即可。有兴趣的朋友可以按照下面的示例演示一遍,加深影响。<br/>//A.cinclude <stdio.h>int adm_vdi_init(){ printf("i'm adm_vdi_init\n");}//A.hextern int adm_vdi_init();//B.cppinclude<A.h>include<stdio.h>int adm_host_init(){ adm_vdi_init(); printf("i'm adm_host_init\n"); return 0;}//B.hextern int adm_host_init();//main.cppinclude<B.h>int main(){ adm_host_init(); return 0;}<br/>编译流程如下:修改后://A.hifdef __cplusplusextern "C" {endifextern int adm_vdi_init();ifdef __cplusplus}endif 我相信有经验的朋友肯定已经知道问题的原因了,但是对于未接触相关案例的同学,估计还是一头雾水,特别是一直从事C语言开发的工程师,估计还不清楚发生了什么。如果你有同样的疑问,请继续往下阅读,一定不会让你失望。原理 我们知道模块之间的函数或全局变量的引用,其实就是对符号的引用。在链接过程中就是通过这个符号寻找对应的代码,实现上下文的跳转。 在历史的长河中,先辈们发现随着项目工程的扩大,很容易出现不同模块定义了相同的全局变量或对外函数,导致符号相同的情况。这样就导致链接时,不知道应该链接到哪一个代码段。但是上述的现象是一个趋势,很难去避免。因此,出现了符号修饰的概念。符号修饰即根据一定的规则,对源码中的符号进行修饰,进行区分。 在较新的GCC编译工具中,并不会对C 符号进行修饰。如上面的A.c文件中,定义了adm_vdi_init函数,编译之后的符号表中,依旧是adm_vdi_init。 C++因为支持类,继承,重载,名称空间等这些特定,因此GCC编译工具,会对C++进行符号修饰。C++符号的修饰规则可参考该GNU C++的符号改编机制介绍_gnu c++的符号装饰机制-CSDN博客如图:B.cpp文件中adm_host_init函数经过修饰,变为了_Z13adm_host_initv。分析: 1、_Z:属于标识 2、13:adm_host_init字符串长度 3、adm_host_init :函数名 4、v:参数void 了解以上原理后,我们就明白为什么在A.h的头文件中增加extern "C",就编译通过了。libA.so是C语言,因此不会进行符号修饰。B.cpp 是C++,会进行符号修饰,编译器并不知道adm_vdi_init是否进行了符号修饰,所以它默认进行了修饰。所以libB.so引用的符号与libA.so的符号并不匹配。而extern "C"就是明确告诉编译器,adm_vdi_init是C编译的,并没有进行符号修饰。C++调用 C 同事的案例,其实就是C++(B.cpp)调用C(A.c)导致的问题。在这里我再简单总结一下: C 并不会进行符号修饰,而C++默认会对符号进行修饰,因此会导致C++ 认知中A.h 的符号与 A.o的实际符号不匹配,虽然动态库libB.so 已经能生成,但是在链接阶段,符号重定位时,就会出现undefined reference to错误。<br/> 因此,C 代码以SDK的方式提供给外部使用时,应该在头文件中用extern "C"修饰。如:<br/>#ifdef __cplusplusextern "C" {endif .....ifdef __cplusplus}endifC调用C++ C++ 调用C 的方式大家可能比较常见,因为C++更适合用于开发偏上层应用,C更适合底层开发。天然的存在依赖关系,所以在工作中也比较常见。 而C 调用C++ 的场景就比较少了,一般是因为公司内部原因了。还是用上面的示例,将 main.cpp 改为main.c。 该现象与我们预期相符,因为libB.so是C++ 编译的库,所以会对符号进行修饰。但是main.c 是C,gcc 并不会进行符号修饰。 但是我们如何解决符号的问题呢?很难受,并没有extern "C++"的参数。在这里我提供三种思路:最简单的方式 最简单的方式就是将main.c 改为main.cpp 。要求GCC 以C++的方式去编译,自然会进行符号修饰。但是这样容易出现其他问题,因为C依赖的很多头文件,C++可能并不支持。为了做到兼容,可能会修改更多的代码。封装一层C接口如我们增加一个libC.so,代码如下:// C.hifdef __cplusplusextern "C" {endifextern int adm_C_init();ifdef __cplusplus}endif//C.cppinclude<C.h>include<B.h>int adm_C_init(){ adm_host_init(); return 0;}// main.cinclude<C.h>int main(){ adm_C_init(); return 0;}注意:其中C.cpp中显示声明了extern "C"修饰adm_C_init,因此libC.so中则不会对adm_C_init进行符号修饰。<br/><br/>直接引用被修饰的符号 封装C接口的方式比较麻烦,还有一种就是比较简单,不易理解的方式。就是main.c 直接引用 libB.so中修饰过后的符号。比如,我们通过nm libB.so | grep adm_host_init查看修饰后的符号为_Z13adm_host_initv。我们可以这样修改main.c。//main.cextern int _Z13adm_host_initv();int main(){ _Z13adm_host_initv(); return 0;}<br/>总结 综上所述,相信大家应该理解C 与 C++ 之间相互调用为什么存在一些困难的原因。工作中我们也应该注意一些事项。比如:C语言编译的SDK,对外提供头文件时,为了兼容c++,应该用extern "C"修饰<br/>
2024年03月29日
9 阅读
0 评论
0 点赞