6、动态库与静态库:Windows下DLL导出与Linux下.so导出
说到跨平台开发,动态库和静态库的导出问题,绝对是绕不开的坎。我早年从Windows转向Linux开发时,就被这个差异狠狠绊了一跤——明明代码逻辑一模一样,编译出来的库就是没法用。后来才明白,两个平台的符号可见性机制,压根儿就不是一回事。
6.1 静态库 vs 动态库:先搞清楚本质区别
静态库和动态库,说白了就是链接时机不同。静态库在编译链接阶段就打包进可执行文件里,动态库则是在运行时才加载。你想想看,静态库就像把工具直接塞进工具箱,动态库则是需要时再去仓库取。
| 特性 | 静态库 | 动态库 |
|---|---|---|
| 链接时机 | 编译时 | 运行时 |
| 文件大小 | 可执行文件变大 | 可执行文件较小 |
| 更新方式 | 需重新编译 | 替换库文件即可 |
| 内存占用 | 每个进程一份 | 多进程共享 |
| 兼容性 | 无版本问题 | 需注意ABI兼容 |
我个人习惯是:小工具用静态库,省心;大型项目用动态库,方便迭代。但不管哪种,跨平台时都得处理符号导出问题。
6.2 Windows下的DLL导出:__declspec(dllexport)
Windows的DLL导出机制,其实挺直白的。你用__declspec(dllexport)告诉编译器:这个函数或变量,我要对外公开。对应的,调用方用__declspec(dllimport)声明:这个符号来自外部DLL。
我在项目中遇到过最典型的写法是这样的:
// mylib.h
#ifdef MYLIB_EXPORTS
#define MYLIB_API __declspec(dllexport)
#else
#define MYLIB_API __declspec(dllimport)
#endif
MYLIB_API int add(int a, int b);
MYLIB_API void print_version(void);
// mylib.c
#define MYLIB_EXPORTS
#include "mylib.h"
MYLIB_API int add(int a, int b) {
return a + b;
}
MYLIB_API void print_version(void) {
printf("Version 1.0\n");
}
MYLIB_EXPORTS,调用方不定义即可。
嗯,这里要注意:Windows下默认所有符号都是不导出的。你如果不加__declspec(dllexport),外部根本看不到你的函数。我曾经有个同事忘了加导出声明,调试了一整天,最后发现DLL导出的符号表是空的——尴尬。
6.3 Linux下的.so导出:__attribute__((visibility))
Linux这边,玩法不太一样。默认情况下,GCC会把所有非静态函数都导出。听起来很方便?其实这是个安全隐患——你不想暴露的内部函数,全暴露了。
所以现代Linux开发中,我们通常用-fvisibility=hidden编译选项,把默认可见性设为隐藏,然后手动标记要导出的符号。
// mylib.h
#define MYLIB_API __attribute__((visibility("default")))
MYLIB_API int add(int a, int b);
MYLIB_API void print_version(void);
// mylib.c
#include "mylib.h"
int add(int a, int b) {
return a + b;
}
void print_version(void) {
printf("Version 1.0\n");
}
// 内部函数,不导出
static void internal_helper(void) {
// 只有本文件可见
}
编译时加上:
gcc -fvisibility=hidden -shared -o libmylib.so mylib.c
-fvisibility=hidden,那么__attribute__((visibility))就白写了。所有符号默认导出,你标记的"default"毫无意义。我曾经踩过这个坑,检查了半天才发现是编译选项漏了。
6.4 跨平台统一宏定义
既然两个平台机制不同,我们自然要写一套跨平台的宏。我个人习惯这样封装:
// export.h
#pragma once
#if defined(_WIN32) || defined(_WIN64)
#ifdef MYLIB_EXPORTS
#define MYLIB_API __declspec(dllexport)
#else
#define MYLIB_API __declspec(dllimport)
#endif
#elif defined(__linux__) || defined(__APPLE__)
#define MYLIB_API __attribute__((visibility("default")))
#else
#define MYLIB_API
#endif
这样写的好处是:Windows和Linux共用一套头文件,只需要在编译DLL时定义MYLIB_EXPORTS。Linux那边不需要这个宏,因为__attribute__((visibility))始终生效。
6.5 避坑指南:我踩过的几个雷
讲几个实际教训,你遇到了能少走弯路:
- 名字改编问题:Windows下C++函数导出时,名字会被改编(name mangling)。用
extern "C"可以解决,但会失去重载能力。我建议C接口用extern "C",C++接口保留改编名。 - 静态变量导出:DLL中导出全局变量,记得用
__declspec(dllexport)修饰定义,用__declspec(dllimport)修饰声明。Linux下直接用__attribute__((visibility))即可。 - 资源释放问题:谁分配谁释放。DLL中malloc的内存,不要在EXE中free。不同CRT版本可能导致堆损坏。我建议统一用接口封装分配和释放操作。
6.6 知识体系总览
下面这张图,把本章的核心逻辑串起来了:
说白了,跨平台库导出的核心就两点:一是理解各平台的默认行为,二是用宏封装差异。Windows默认不导出,你得主动标记;Linux默认全导出,你得主动隐藏。方向正好相反,但最终目标一致——只暴露该暴露的。
GenerateExportHeader的支持,能自动生成跨平台的导出宏。省去手写宏的麻烦,也减少出错概率。
嗯,关于动态库和静态库的导出,今天就聊到这儿。记住一个原则:接口要稳,实现要藏。下次你遇到跨平台库链接问题,先检查符号可见性设置——八成是这里出了岔子。
公众号:蓝海资料掘金营,微信deep3321