欢迎来到尧图网

客户服务 关于我们

您的位置:首页 > 健康 > 美食 > C、C++ 源文件混合编译合成库

C、C++ 源文件混合编译合成库

2025/2/26 0:40:36 来源:https://blog.csdn.net/Don_tknowwhat/article/details/144407869  浏览:    关键词:C、C++ 源文件混合编译合成库

文章目录

  • 混合编译
  • 使用依赖库的情况
  • 使用 `extern C`

其实以前也不怎么关注这点,因为基本上自己写的库和用的第三方依赖大部分都是全 C++ 实现,基本没有 C 实现,但是写算法题的时候还是会经常用到 printf 之类的 C 函数,就很自然而然的去用了,发现编译也能通过就没怎么管。

最近自己移植一个 Unix 上的物理模拟项目,需要用到 glad 之类的 OpenGL 辅助库,突然发觉,这个项目是纯 C 实现的,如果我搬迁到 Windows 上来,Windows 下的 GLFW 和 GLAD 库都好像是 C++ 编译的吧,至少我之前用 OpenGL 的时候都是写 C++ 项目,没有 C 项目。那么这个人用纯 C 实现的例子物理约束模拟也能通过链接,难道是 GLFW 和 GLAD 其实是 C 写的库?

于是尝试写了一个简单的混合编译测试。

混合编译

写一个简单的加法和减法函数,然后加法使用 C++ 编写,减法使用 C 编写,并对加法、减法各自单独编译为一个导出动态库目标,同时再添加一个混合编译目标,混合编译动态库的源文件就是囊括了 add.cppsub.c 的库,我看看如果编译的时候混合了不同类型的源文件,那么编译好的库的导出符号表里面每个函数到底是什么符号。

目录结构如下:

C:.
│  CMakeLists.txt
│
├─build
│  ├─Debug
│  │  ├─bin
│  │  │      libadd.dll
│  │  │      libcalc.dll
│  │  │      libsub.dll
│  │  │
│  │  └─lib
│  │          libadd.dll.a
│  │          libcalc.dll.a
│  │          libsub.dll.a
│  │
│  ├─install
│  │  ├─bin
│  │  │      libadd.dll
│  │  │      libcalc.dll
│  │  │      libsub.dll
│  │  │
│  │  ├─include
│  │  │  ├─add
│  │  │  │      add.h
│  │  │  │
│  │  │  ├─include
│  │  │  │  ├─add
│  │  │  │  │      add.h
│  │  │  │  │
│  │  │  │  └─sub
│  │  │  │          sub.h
│  │  │  │
│  │  │  └─sub
│  │  │          sub.h
│  │  │
│  │  └─lib
│  │      │  libadd.dll.a
│  │      │  libcalc.dll.a
│  │      │  libsub.dll.a
│  │      │
│  │      └─cmake
│  │          └─calc
│  │                  calcConfig.cmake
│  │                  calcConfigVersion.cmake
│  │                  calcTargets-debug.cmake
│  │                  calcTargets.cmake
│
├─cmake
│      calcConfig.cmake.in
│
├─include
│  ├─add
│  │      add.h
│  │
│  └─sub
│          sub.h
│
└─srcadd.cppCMakeLists.txtsub.c

对两个函数各自写一个头文件放在 include 目录下对应的子目录里面,这样 CMake 编译的时候好分别为两个目标指定单独的头文件位置。然后 src 目录下分别使用 add.cppsub.c 实现 addsub 函数。

头文件:

// add.h
#ifndef __ADD_H__
#define __ADD_H__int add(int a, int b);#endif // sub.h
#ifndef __SUB_H__
#define __SUB_H__int sub(int a, int b);#endif

源文件:

// add.cpp
#include "add/add.h"int add(int a, int b) {return a + b;
}// sub.c
#include "sub/sub.h"int sub(int a, int b) {return a - b;
}

然后编译使用 CMake 来构建,项目根目录下的 CMake 内容如下:

cmake_minimum_required(VERSION 3.20)
project(calc VERSION 1.0.0)set(CMAKE_EXPORT_COMPILE_COMMANDS ON)
set(CMAKE_RUNTIME_OUTPUT_DIRECTORY ${CMAKE_CURRENT_BINARY_DIR}/${CMAKE_BUILD_TYPE}/bin)
set(CMAKE_LIBRARY_OUTPUT_DIRECTORY ${CMAKE_CURRENT_BINARY_DIR}/${CMAKE_BUILD_TYPE}/bin)
set(CMAKE_ARCHIVE_OUTPUT_DIRECTORY ${CMAKE_CURRENT_BINARY_DIR}/${CMAKE_BUILD_TYPE}/lib)
set(CMAKE_INSTALL_PREFIX ${CMAKE_CURRENT_BINARY_DIR}/install)set(CMAKE_C_STANDARD 11)
set(CMAKE_CXX_STANDARD 14)include_directories(include)add_subdirectory(src)

源文件目录 src 的 CMakeLists 如下,负责单独构建 add 目标动态库、sub 目标动态库以及一个混合库目标 calc ,并设置好对应的导出配置,这样 cmake --build build --target install 之后,就会按照导出配置文件,其他项目使用我们这个库的时候可以通过 find_package 轻松引入相关的依赖头文件和依赖库的位置。

# ==================== build add lib ====================
add_library(add SHARED add.cpp)
set_target_properties(add PROPERTIESVERSION ${PROJECT_VERSION}SOVERSION ${PROJECT_VERSION}
)
target_include_directories(add PUBLIC$<BUILD_INTERFACE:${CMAKE_SOURCE_DIR}/include/add>$<INSTALL_INTERFACE:include/add>
)
install(TARGETS addEXPORT ${PROJECT_NAME}_targetsRUNTIME DESTINATION binLIBRARY DESTINATION binARCHIVE DESTINATION lib)
install(DIRECTORY ${PROJECT_SOURCE_DIR}/include/addDESTINATION includeFILES_MATCHING PATTERN "*.h"
)# ==================== build sub lib ====================
add_library(sub SHARED sub.c)
set_target_properties(sub PROPERTIESVERSION ${PROJECT_VERSION}SOVERSION ${PROJECT_VERSION}
)
target_include_directories(sub PUBLIC$<BUILD_INTERFACE:${CMAKE_SOURCE_DIR}/include/sub>$<INSTALL_INTERFACE:include/sub>
)
install(TARGETS subEXPORT ${PROJECT_NAME}_targetsRUNTIME DESTINATION binLIBRARY DESTINATION binARCHIVE DESTINATION lib)
install(DIRECTORY ${PROJECT_SOURCE_DIR}/include/subDESTINATION includeFILES_MATCHING PATTERN "*.h"
)# ==================== build sub lib ====================
add_library(calc SHARED add.cpp sub.c)
set_target_properties(calc PROPERTIESVERSION ${PROJECT_VERSION}SOVERSION ${PROJECT_VERSION}
)
target_include_directories(calc PUBLIC$<BUILD_INTERFACE:${CMAKE_SOURCE_DIR}/include>$<INSTALL_INTERFACE:include>
)
install(TARGETS calcEXPORT ${PROJECT_NAME}_targetsRUNTIME DESTINATION binLIBRARY DESTINATION binARCHIVE DESTINATION lib)
install(DIRECTORY ${PROJECT_SOURCE_DIR}/includeDESTINATION includeFILES_MATCHING PATTERN "*.h"
)# export to calcTargets.cmake
# 安装库和头文件
include(CMakePackageConfigHelpers)
configure_package_config_file(${CMAKE_SOURCE_DIR}/cmake/${PROJECT_NAME}Config.cmake.in${CMAKE_BINARY_DIR}/cmake/${PROJECT_NAME}Config.cmakeINSTALL_DESTINATION lib/cmake/${PROJECT_NAME}
)write_basic_package_version_file(${CMAKE_BINARY_DIR}/cmake/${PROJECT_NAME}ConfigVersion.cmakeVERSION ${PROJECT_VERSION}COMPATIBILITY AnyNewerVersion
)install(FILES${CMAKE_BINARY_DIR}/cmake/${PROJECT_NAME}Config.cmake${CMAKE_BINARY_DIR}/cmake/${PROJECT_NAME}ConfigVersion.cmakeDESTINATION lib/cmake/${PROJECT_NAME}
)# 安装 CMake 配置文件,生成 xxxTargets.cmake 文件
install(EXPORT ${PROJECT_NAME}_targetsFILE ${PROJECT_NAME}Targets.cmakeNAMESPACE ${PROJECT_NAME}::DESTINATION lib/cmake/${PROJECT_NAME}
)

CMake 完成编译和安装之后,在指定的安装目录下会生成复制过来的目标文件以及供其他项目导入此包的 xxxConfig.cmake 文件。

C:.
├─bin
│      libadd.dll
│      libcalc.dll
│      libsub.dll
│
├─include
│  ├─add
│  │      add.h
│  │
│  ├─include
│  │  ├─add
│  │  │      add.h
│  │  │
│  │  └─sub
│  │          sub.h
│  │
│  └─sub
│          sub.h
│
└─lib│  libadd.dll.a│  libcalc.dll.a│  libsub.dll.a│└─cmake└─calccalcConfig.cmakecalcConfigVersion.cmakecalcTargets-debug.cmakecalcTargets.cmake

对应每个目标库的头文件也都复制搬运过来了。

使用依赖库的情况

随便编写一个其他的 C++ 和 C 文件,并使用 CMake 构建指定为可执行二进制文件,看看 C 测试文件能不能用 C++ 动态库的函数 add 以及 C++ 测试文件能不能用 C 的动态库函数 sub

首先是准备一个编译两个测试文件所需的 CMakeLists 文件,负责将 cpp 和 c 文件添加为可执行二进制目标,并链接到上述导出库。

cmake_minimum_required(VERSION 3.20)
project(import VERSION 1.0.0)set(CMAKE_EXPORT_COMPILE_COMMANDS ON)set(DEBUG_OUTPUT_DIR ${CMAKE_CURRENT_BINARY_DIR}/${CMAKE_BUILD_TYPE})
set(CMAKE_RUNTIME_OUTPUT_DIRECTORY ${DEBUG_OUTPUT_DIR}/bin)
set(CMAKE_LIBRARY_OUTPUT_DIRECTORY ${DEBUG_OUTPUT_DIR}/bin)
set(CMAKE_ARCHIVE_OUTPUT_DIRECTORY ${DEBUG_OUTPUT_DIR}/lib)set(CMAKE_C_STANDARD 11)
set(CMAKE_CXX_STANDARD 14)find_package(calc REQUIREDPATHS ../../../addsub/build/install/lib/cmake/calc
)
message("${CALC_INCLUDE_DIRS}")
message("${CALC_LIBRARIES}")
include_directories(${CALC_INCLUDE_DIRS})add_executable(test-c test-c.c)
target_link_libraries(test-c ${CALC_LIBRARIES})add_executable(test-cpp test-cpp.cpp)
target_link_libraries(test-cpp ${CALC_LIBRARIES})set(CMAKE_INSTALL_PREFIX ${DEBUG_OUTPUT_DIR})
foreach(lib IN LISTS CALC_LIBRARIES)get_target_property(lib_path ${lib} LOCATION)install(FILES ${lib_path}DESTINATION bin)
endforeach(lib IN LISTS CALC_LIBRARIES)

其中的 install 指令能够帮助我们把引入的依赖库目标,提取 Location 属性也就是依赖库文件位置,直接复制到调试文件输出的目录,这样在 Windows 下也能直接 VScode 一键 F5 调试了,不会出现动态库不在二进制可执行文件所在目录下导致调试无法启动。

CMake 配置项目输出:

[main] 正在配置项目: import 
[proc] 执行命令: "C:\Users\fredom\Program Files\MinGW64\mingw64\bin\cmake.EXE" -DCMAKE_BUILD_TYPE:STRING=Debug -DCMAKE_EXPORT_COMPILE_COMMANDS:BOOL=TRUE "-DCMAKE_C_COMPILER:FILEPATH=C:\Users\fredom\Program Files\MinGW64\mingw64\bin\gcc.exe" "-DCMAKE_CXX_COMPILER:FILEPATH=C:\Users\fredom\Program Files\MinGW64\mingw64\bin\g++.exe" -DCMAKE_EXPORT_COMPILE_COMMANDS=ON --no-warn-unused-cli -SC:/Users/fredom/workspace/test/cc/import -Bc:/Users/fredom/workspace/test/cc/import/build -G "MinGW Makefiles"
[cmake] Not searching for unused variables given on the command line.
[cmake] C:/Users/fredom/workspace/addsub/build/install/include/add;C:/Users/fredom/workspace/addsub/build/install/include/sub;C:/Users/fredom/workspace/addsub/build/install/include
[cmake] calc::add;calc::sub;calc::calc
[cmake] -- Configuring done (0.2s)
[cmake] -- Generating done (0.0s)
[cmake] -- Build files have been written to: C:/Users/fredom/workspace/test/cc/import/build

然后 cmake --build build --target install 自动过一遍配置和生成目标,看看能不能走到安装动态库到调试目录这个 make 目标。

[main] 正在生成文件夹: c:/Users/fredom/workspace/test/cc/import/build 
[build] 正在启动生成
[proc] 执行命令: "C:\Users\fredom\Program Files\MinGW64\mingw64\bin\cmake.EXE" --build c:/Users/fredom/workspace/test/cc/import/build --config Debug --target install -j 22 --
[build] [ 25%] Linking C executable Debug\bin\test-c.exe
[build] [ 50%] Linking CXX executable Debug\bin\test-cpp.exe
[build] C:/Users/fredom/Program Files/MinGW64/mingw64/bin/../lib/gcc/x86_64-w64-mingw32/14.2.0/../../../../x86_64-w64-mingw32/bin/ld.exe: CMakeFiles\test-c.dir/objects.a(test-c.c.obj): in function `main':
[build] C:/Users/fredom/workspace/test/cc/import/test-c.c:8:(.text+0x96): undefined reference to `add'
[build] collect2.exe: error: ld returned 1 exit status
[build] mingw32-make[2]: *** [CMakeFiles\test-c.dir\build.make:102: Debug/bin/test-c.exe] Error 1
[build] mingw32-make[1]: *** [CMakeFiles\Makefile2:84: CMakeFiles/test-c.dir/all] Error 2
[build] mingw32-make[1]: *** Waiting for unfinished jobs....
[build] C:/Users/fredom/Program Files/MinGW64/mingw64/bin/../lib/gcc/x86_64-w64-mingw32/14.2.0/../../../../x86_64-w64-mingw32/bin/ld.exe: CMakeFiles\test-cpp.dir/objects.a(test-cpp.cpp.obj): in function `main':
[build] C:/Users/fredom/workspace/test/cc/import/test-cpp.cpp:8:(.text+0x31): undefined reference to `sub(int, int)'
[build] collect2.exe: error: ld returned 1 exit status
[build] mingw32-make[2]: *** [CMakeFiles\test-cpp.dir\build.make:102: Debug/bin/test-cpp.exe] Error 1
[build] mingw32-make[1]: *** [CMakeFiles\Makefile2:110: CMakeFiles/test-cpp.dir/all] Error 2
[build] mingw32-make: *** [Makefile:135: all] Error 2
[proc] 命令“"C:\Users\fredom\Program Files\MinGW64\mingw64\bin\cmake.EXE" --build c:/Users/fredom/workspace/test/cc/import/build --config Debug --target install -j 22 --”已退出,代码为 2
[driver] 生成完毕: 00:00:00.589
[build] 生成已完成,退出代码为 2

根据输出来看,是编译错误了,并且错误发生在链接器 ld.exe 尝试为我们的目标在提供的导出库 calc 中寻找导出符号的阶段。可以看到 c 测试文件的错误是无法找到导出库的 cpp 动态库中的 add 函数定义符号,而 cpp 测试文件则是无法从提供的导出库中找到 sub(int, int) 函数定义符号。

使用 Windows 开发套件 VisualStudio 提供的 dumpbin 工具查看 dll 动态库文件中导出符号都有哪些。对 addsubcalc 三个库查看它们的导出符号表:

calc.dll 导出符号表:

File Type: DLLSection contains the following exports for libcalc.dll00000000 characteristics675948A3 time date stamp Wed Dec 11 16:09:07 20240.00 version1 ordinal base2 number of functions2 number of namesordinal hint RVA      name1    0 000015A0 _Z3addii2    1 000015C0 sub

add.dll 导出符号表:

File Type: DLLSection contains the following exports for libadd.dll00000000 characteristics67590BFE time date stamp Wed Dec 11 11:50:22 20240.00 version1 ordinal base1 number of functions1 number of namesordinal hint RVA      name1    0 000015A0 _Z3addii

sub.dll 导出符号表:

File Type: DLLSection contains the following exports for libsub.dll00000000 characteristics67590BFE time date stamp Wed Dec 11 11:50:22 20240.00 version1 ordinal base1 number of functions1 number of namesordinal hint RVA      name1    0 000015A0 sub

这里使用 MinGW64 编译出来的动态库,Windows 的 VS 编译工具链中的工具一样可以读出导出符号表。可以看到,使用 C++ 实现的 add 函数的编译产物中,函数符号是经过 C++ 编译器的 name mangling(名称修饰)的,而 C 实现的 sub 函数没有。这是因为 C++ 中有函数重载的概念,为了区分不同参数列表的同名函数,需要将参数列表的内容作为一种签名附带到函数名称上。

使用 extern C

如果我们在测试项目编译 C++ 源文件的时候告诉编译器,sub 这个函数是 C 实现的,不需要经过 C++ 编译中的函数名称修饰。因为 calc 库本身 sub 子模块还是 C 实现的,不能强求将库的实现从 C 迁移到 C++ ,一个更好的迁移方案是,对声明接口的头文件使用 extern "C" 预处理宏,结合 __cplusplus 这个编译器在编译 C++ 源文件时会自动携带的宏,可以配合完成“告诉编译器在动态库找这个接口的定义符号时,不要对函数进行名称修饰,尽管你在编译 C++ 源文件”。

所以,修改过后的 sub 函数的接口声明头文件内容改成:

#ifndef __SUB_H__
#define __SUB_H__#ifdef __cplusplus
extern "C" {
#endifint sub(int a, int b);#ifdef __cplusplus
}
#endif#endif

这样其他用户在引入这个库使用的时候,只要在 C++ 源文件中引入这个库, extern "C" 包含范围会启用,被包含的接口声明在编译期间查找提供的动态库中的导出符号表时,函数名称不经过编译器修饰。

现在测试项目的 CMake 构建阶段输出是:

[main] 正在生成文件夹: c:/Users/fredom/workspace/test/cc/import/build 
[build] 正在启动生成
[proc] 执行命令: "C:\Users\fredom\Program Files\MinGW64\mingw64\bin\cmake.EXE" --build c:/Users/fredom/workspace/test/cc/import/build --config Debug --target install -j 22 --
[build] [ 25%] Building C object CMakeFiles/test-c.dir/test-c.c.obj
[build] [ 50%] Building CXX object CMakeFiles/test-cpp.dir/test-cpp.cpp.obj
[build] [ 75%] Linking C executable Debug\bin\test-c.exe
[build] [100%] Linking CXX executable Debug\bin\test-cpp.exe
[build] C:/Users/fredom/Program Files/MinGW64/mingw64/bin/../lib/gcc/x86_64-w64-mingw32/14.2.0/../../../../x86_64-w64-mingw32/bin/ld.exe: CMakeFiles\test-c.dir/objects.a(test-c.c.obj): in function `main':
[build] C:/Users/fredom/workspace/test/cc/import/test-c.c:8:(.text+0x96): undefined reference to `add'
[build] collect2.exe: error: ld returned 1 exit status
[build] mingw32-make[2]: *** [CMakeFiles\test-c.dir\build.make:102: Debug/bin/test-c.exe] Error 1
[build] mingw32-make[1]: *** [CMakeFiles\Makefile2:84: CMakeFiles/test-c.dir/all] Error 2
[build] mingw32-make[1]: *** Waiting for unfinished jobs....
[build] [100%] Built target test-cpp
[build] mingw32-make: *** [Makefile:135: all] Error 2
[proc] 命令“"C:\Users\fredom\Program Files\MinGW64\mingw64\bin\cmake.EXE" --build c:/Users/fredom/workspace/test/cc/import/build --config Debug --target install -j 22 --”已退出,代码为 2
[driver] 生成完毕: 00:00:00.823
[build] 生成已完成,退出代码为 2

可以看到 test-c.c 文件的编译还是错误,无法找到 add 函数的定义,但是 test-cpp.cpp 文件编译成功了,这是符合预期的,因为从刚才我们对三个动态库的导出符号表的检视来看,动态库中的 add 函数实际上名称是 _Z3addii ,编译器编译 C 源文件的时候,不会执行函数名称修饰,也自然就无法在提供的库中找到对应的导出符号,也就是说 C++ 源文件编译通过 extern "C 还是可以挽救一下兼容性调用 C 实现的函数的,但是反过来 C 就没法调用 C++ 实现的函数了。

版权声明:

本网仅为发布的内容提供存储空间,不对发表、转载的内容提供任何形式的保证。凡本网注明“来源:XXX网络”的作品,均转载自其它媒体,著作权归作者所有,商业转载请联系作者获得授权,非商业转载请注明出处。

我们尊重并感谢每一位作者,均已注明文章来源和作者。如因作品内容、版权或其它问题,请及时与我们联系,联系邮箱:809451989@qq.com,投稿邮箱:809451989@qq.com