文章目录
- 混合编译
- 使用依赖库的情况
- 使用 `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.cpp
和 sub.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
│
└─src
add.cpp
CMakeLists.txt
sub.c
对两个函数各自写一个头文件放在 include
目录下对应的子目录里面,这样 CMake 编译的时候好分别为两个目标指定单独的头文件位置。然后 src
目录下分别使用 add.cpp
和 sub.c
实现 add
和 sub
函数。
头文件:
// 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 PROPERTIES
VERSION ${PROJECT_VERSION}
SOVERSION ${PROJECT_VERSION}
)
target_include_directories(
add PUBLIC
$<BUILD_INTERFACE:${CMAKE_SOURCE_DIR}/include/add>
$<INSTALL_INTERFACE:include/add>
)
install(
TARGETS add
EXPORT ${PROJECT_NAME}_targets
RUNTIME DESTINATION bin
LIBRARY DESTINATION bin
ARCHIVE DESTINATION lib
)
install(
DIRECTORY ${PROJECT_SOURCE_DIR}/include/add
DESTINATION include
FILES_MATCHING PATTERN "*.h"
)
# ==================== build sub lib ====================
add_library(sub SHARED sub.c)
set_target_properties(
sub PROPERTIES
VERSION ${PROJECT_VERSION}
SOVERSION ${PROJECT_VERSION}
)
target_include_directories(
sub PUBLIC
$<BUILD_INTERFACE:${CMAKE_SOURCE_DIR}/include/sub>
$<INSTALL_INTERFACE:include/sub>
)
install(
TARGETS sub
EXPORT ${PROJECT_NAME}_targets
RUNTIME DESTINATION bin
LIBRARY DESTINATION bin
ARCHIVE DESTINATION lib
)
install(
DIRECTORY ${PROJECT_SOURCE_DIR}/include/sub
DESTINATION include
FILES_MATCHING PATTERN "*.h"
)
# ==================== build sub lib ====================
add_library(calc SHARED add.cpp sub.c)
set_target_properties(
calc PROPERTIES
VERSION ${PROJECT_VERSION}
SOVERSION ${PROJECT_VERSION}
)
target_include_directories(
calc PUBLIC
$<BUILD_INTERFACE:${CMAKE_SOURCE_DIR}/include>
$<INSTALL_INTERFACE:include>
)
install(
TARGETS calc
EXPORT ${PROJECT_NAME}_targets
RUNTIME DESTINATION bin
LIBRARY DESTINATION bin
ARCHIVE DESTINATION lib
)
install(
DIRECTORY ${PROJECT_SOURCE_DIR}/include
DESTINATION include
FILES_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.cmake
INSTALL_DESTINATION lib/cmake/${PROJECT_NAME}
)
write_basic_package_version_file(
${CMAKE_BINARY_DIR}/cmake/${PROJECT_NAME}ConfigVersion.cmake
VERSION ${PROJECT_VERSION}
COMPATIBILITY AnyNewerVersion
)
install(
FILES
${CMAKE_BINARY_DIR}/cmake/${PROJECT_NAME}Config.cmake
${CMAKE_BINARY_DIR}/cmake/${PROJECT_NAME}ConfigVersion.cmake
DESTINATION lib/cmake/${PROJECT_NAME}
)
# 安装 CMake 配置文件,生成 xxxTargets.cmake 文件
install(
EXPORT ${PROJECT_NAME}_targets
FILE ${PROJECT_NAME}Targets.cmake
NAMESPACE ${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
└─calc
calcConfig.cmake
calcConfigVersion.cmake
calcTargets-debug.cmake
calcTargets.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 REQUIRED
PATHS ../../../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 动态库文件中导出符号都有哪些。对 add
、sub
、calc
三个库查看它们的导出符号表:
calc.dll 导出符号表:
File Type: DLL
Section contains the following exports for libcalc.dll
00000000 characteristics
675948A3 time date stamp Wed Dec 11 16:09:07 2024
0.00 version
1 ordinal base
2 number of functions
2 number of names
ordinal hint RVA name
1 0 000015A0 _Z3addii
2 1 000015C0 sub
add.dll 导出符号表:
File Type: DLL
Section contains the following exports for libadd.dll
00000000 characteristics
67590BFE time date stamp Wed Dec 11 11:50:22 2024
0.00 version
1 ordinal base
1 number of functions
1 number of names
ordinal hint RVA name
1 0 000015A0 _Z3addii
sub.dll 导出符号表:
File Type: DLL
Section contains the following exports for libsub.dll
00000000 characteristics
67590BFE time date stamp Wed Dec 11 11:50:22 2024
0.00 version
1 ordinal base
1 number of functions
1 number of names
ordinal hint RVA name
1 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" {
#endif
int 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++ 实现的函数了。