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");
}
小技巧:用宏来控制导出/导入声明,这样头文件在DLL内部和外部都能用。编译DLL时定义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 知识体系总览

下面这张图,把本章的核心逻辑串起来了:

动态库与静态库导出机制 静态库 (.lib / .a) 编译时链接,打包进EXE 动态库 (.dll / .so) 运行时加载,多进程共享 Windows DLL 导出 __declspec(dllexport) / __declspec(dllimport) 默认不导出,需显式标记 宏控制:MYLIB_EXPORTS 开关 Linux .so 导出 __attribute__((visibility("default"))) 配合 -fvisibility=hidden 使用 默认全导出,需手动隐藏 跨平台统一宏定义 #ifdef _WIN32 → __declspec(dllexport/dllimport) #elif __linux__ → __attribute__((visibility))

说白了,跨平台库导出的核心就两点:一是理解各平台的默认行为,二是用宏封装差异。Windows默认不导出,你得主动标记;Linux默认全导出,你得主动隐藏。方向正好相反,但最终目标一致——只暴露该暴露的。

我的建议:新项目直接上CMake,它内置了对GenerateExportHeader的支持,能自动生成跨平台的导出宏。省去手写宏的麻烦,也减少出错概率。

嗯,关于动态库和静态库的导出,今天就聊到这儿。记住一个原则:接口要稳,实现要藏。下次你遇到跨平台库链接问题,先检查符号可见性设置——八成是这里出了岔子。


公众号:蓝海资料掘金营,微信deep3321