Skip to content
Gains Summary
Main Navigation 首页 / Home
C++ 编程 / C++ Programming
系统与高性能 / Systems & Performance
Web 开发 / Web Development
人工智能 / Artificial Intelligence
工业软件 / Industrial Software
其他内容 / Other Topics
C++ 编程 / C++系统与性能 / SystemsWeb 开发 / Web人工智能 / AI工业软件 / Industrial

外观

Sidebar Navigation

← C++ 编程 / C++ Programming

插件系统 / Plugin Systems

1. 插件工程:可演进的模块边界 / Plugin Engineering and Evolvable Module Boundaries

2. 运行时动态加载:LoadLibrary 与 dlopen —— 打开插件的大门 / Runtime Dynamic Loading with LoadLibrary and Dlopen

3. 插件接口设计:C ABI、函数表与受控 C++ 接口 / Plugin Interface Design with C ABI, Function Tables, and Controlled C++ Interfaces

4. CMake 构建完整插件项目 / Building a Complete Plugin Project with CMake

5. 插件架构设计模式:发现、生命周期、依赖与通信 / Plugin Architecture Patterns for Discovery, Lifecycle, Dependencies, and Communication

6. 跨平台插件开发:Windows / Linux / macOS 统一策略

7. 插件热加载与版本管理 / Plugin Hot Reloading and Version Management

8. 插件安全:沙箱、进程隔离与权限控制 / Plugin Security with Sandboxing, Process Isolation, and Permissions

9. 工业软件插件系统案例分析 / Industrial Software Plugin System Case Study

10. 综合实战:版本化 C ABI 插件系统 / Full Project: Versioned C ABI Plugin System

本页目录

运行时动态加载:LoadLibrary 与 dlopen —— 打开插件的大门 / Runtime Dynamic Loading with LoadLibrary and Dlopen ​

📅 创建时间:2026-07-13 🏷️ 标签:#动态加载 #dlopen #LoadLibrary #插件入门 #跨平台 📚 前置知识:windows build outputs, linux build outputs, static vs dynamic linking


📋 本章目标 ​

  • 理解"运行时动态加载"和"加载时链接"的本质区别
  • 掌握 Windows 的 LoadLibrary / GetProcAddress / FreeLibrary 完整用法
  • 掌握 Linux 的 dlopen / dlsym / dlclose / dlerror 完整用法
  • 理解 dlopen 的关键 flag:RTLD_LAZY vs RTLD_NOW、RTLD_GLOBAL vs RTLD_LOCAL
  • 能够用跨平台宏封装出统一的动态加载接口
  • 实现第一个简单的插件雏形:一个可扩展的计算器

第1部分:两种加载方式——本质区别 ​

┌─────────────────────────────────────────────────────────────────────────────┐
│           加载时链接 vs 运行时动态加载                                         │
├─────────────────────────────────────────────────────────────────────────────┤
│                                                                             │
│  加载时链接(Load-time Linking)                                              │
│  ┌───────────────────────────────────────────────────────────────┐         │
│  │ • 程序启动时,操作系统的加载器自动加载所有依赖的 .dll/.so       │         │
│  │ • 如果某个 .dll 找不到 → 程序启动失败                          │         │
│  │ • 依赖关系在链接时就已经固化(写在 PE/ELF 的导入表中)          │         │
│  │ • 例子:你在 CMake 里写的 target_link_libraries                │         │
│  └───────────────────────────────────────────────────────────────┘         │
│                                                                             │
│  运行时动态加载(Runtime Dynamic Loading)                                    │
│  ┌───────────────────────────────────────────────────────────────┐         │
│  │ • 程序运行时,由你自己的代码决定何时加载哪个库                  │         │
│  │ • 库不存在 → 你的代码收到错误,可以优雅处理(弹窗、降级、跳过)│         │
│  │ • 依赖关系不在 PE/ELF 中固化——对操作系统是"看不见的"            │         │
│  │ • 例子:VS Code 加载扩展、Photoshop 加载滤镜插件               │         │
│  │ • 核心意义:**程序不需要在编译时知道所有功能——可以在运行时扩展** │         │
│  └───────────────────────────────────────────────────────────────┘         │
│                                                                             │
│  用加载时链接:插件必须在编译时就确定 → 增加插件需要重新编译主程序             │
│  用运行时加载:插件可以在编译后任意时候添加 → 新增插件只需放入插件目录         │
│                                                                             │
└─────────────────────────────────────────────────────────────────────────────┘
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25

第2部分:Linux —— dlopen / dlsym / dlclose ​

2.1 核心 API ​

c
#include <dlfcn.h>

// 打开动态库
void* dlopen(const char* filename, int flags);

// 获取符号地址
void* dlsym(void* handle, const char* symbol);

// 关闭动态库
int dlclose(void* handle);

// 获取最近一次错误信息
char* dlerror(void);
1
2
3
4
5
6
7
8
9
10
11
12
13

2.2 dlopen 的 flags —— 这很重要 ​

┌─────────────────────────────────────────────────────────────────────────────┐
│              dlopen flags 详解                                                │
├─────────────────────────────────────────────────────────────────────────────┤
│                                                                             │
│  RTLD_LAZY(延迟解析):                                                     │
│  ┌───────────────────────────────────────────────────────────────┐         │
│  │ 符号在首次使用时才解析                                          │         │
│  │ 优点:加载快,没用到就不会报错                                   │         │
│  │ 缺点:如果符号不存在,用到时才报错 → 可能程序跑到一半才崩溃      │         │
│  └───────────────────────────────────────────────────────────────┘         │
│                                                                             │
│  RTLD_NOW(立即解析):                                                      │
│  ┌───────────────────────────────────────────────────────────────┐         │
│  │ dlopen 返回时就解析所有符号                                     │         │
│  │ 优点:早发现错误(dlopen 时就知道哪些符号缺失)                  │         │
│  │ 缺点:加载稍慢                                                │         │
│  │ 推荐:开发时用 RTLD_NOW,避免不稳定的延迟错误                    │         │
│  └───────────────────────────────────────────────────────────────┘         │
│                                                                             │
│  RTLD_GLOBAL:                                                              │
│  ┌───────────────────────────────────────────────────────────────┐         │
│  │ 这个库的符号对后续 dlopen 的其他库可见                           │         │
│  │ 场景:后续加载的插件需要引用当前插件的符号                       │         │
│  └───────────────────────────────────────────────────────────────┘         │
│                                                                             │
│  RTLD_LOCAL(默认):                                                        │
│  ┌───────────────────────────────────────────────────────────────┐         │
│  │ 这个库的符号只对自己可见                                       │         │
│  │ 推荐:通常用 LOCAL,避免符号污染和意外冲突                      │         │
│  └───────────────────────────────────────────────────────────────┘         │
│                                                                             │
│  推荐组合:RTLD_NOW | RTLD_LOCAL(立即解析 + 隔离符号)                       │
│                                                                             │
└─────────────────────────────────────────────────────────────────────────────┘
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34

2.3 完整示例 ​

cpp
// ====== plugin_math.cpp — 编译为 libmath_plugin.so ======
#include <cstdio>

extern "C" {

int add(int a, int b) {
    printf("[plugin] add(%d, %d) called\n", a, b);
    return a + b;
}

int multiply(int a, int b) {
    printf("[plugin] multiply(%d, %d) called\n", a, b);
    return a * b;
}

}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
bash
# 编译插件
g++ -fPIC -shared plugin_math.cpp -o libmath_plugin.so
1
2
cpp
// ====== host.cpp — 宿主程序 ======
#include <dlfcn.h>
#include <cstdio>
#include <cstdlib>

// 定义函数指针类型(必须和插件导出的函数签名一致!)
typedef int (*calc_func)(int, int);

int main() {
    // 1. 加载动态库
    void* handle = dlopen("./libmath_plugin.so", RTLD_NOW);
    if (!handle) {
        fprintf(stderr, "dlopen failed: %s\n", dlerror());
        return 1;
    }
    printf("Library loaded successfully\n");

    // 2. 获取函数地址
    calc_func add = (calc_func)dlsym(handle, "add");
    if (!add) {
        fprintf(stderr, "dlsym 'add' failed: %s\n", dlerror());
        dlclose(handle);
        return 1;
    }

    calc_func multiply = (calc_func)dlsym(handle, "multiply");
    if (!multiply) {
        fprintf(stderr, "dlsym 'multiply' failed: %s\n", dlerror());
        dlclose(handle);
        return 1;
    }

    // 3. 调用!
    printf("add(3, 5) = %d\n", add(3, 5));
    printf("multiply(3, 5) = %d\n", multiply(3, 5));

    // 4. 关闭
    dlclose(handle);
    printf("Library unloaded\n");

    return 0;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
bash
# 编译宿主(不需要 -lmath_plugin!不需要链接插件!)
g++ host.cpp -o host -ldl   # -ldl = libdl, 提供 dlopen/dlsym

# 运行
./host
# 输出:
# Library loaded successfully
# [plugin] add(3, 5) called
# add(3, 5) = 8
# [plugin] multiply(3, 5) called
# multiply(3, 5) = 15
# Library unloaded
1
2
3
4
5
6
7
8
9
10
11
12

关键点:宿主编译时完全不知道 math_plugin 的存在!-ldl 只是链接 libdl(提供 dlopen API),和插件本身无关。你可以编译完宿主后,再单独开发新插件。

2.4 dlerror 的正确用法 ​

cpp
// ❌ 错误:dlerror 不清空就不变,可能拿到之前的错误
void* handle = dlopen("./plugin.so", RTLD_NOW);
if (!handle) {
    printf("Error: %s\n", dlerror());    // 正确用法
}

// ⚠️ 陷阱:dlerror 返回上一次调用至今的错误
// 正确的错误检查流程:
dlerror();  // 先清空错误状态
void* sym = dlsym(handle, "some_function");
char* err = dlerror();
if (err != nullptr) {
    printf("dlsym error: %s\n", err);    // 这才是本次 dlsym 的错误
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14

第3部分:Windows —— LoadLibrary / GetProcAddress / FreeLibrary ​

3.1 核心 API ​

c
#include <windows.h>

// 加载 DLL
HMODULE LoadLibraryA(LPCSTR lpLibFileName);      // ANSI 版本
HMODULE LoadLibraryW(LPCWSTR lpLibFileName);     // Unicode 版本

// 获取函数地址
FARPROC GetProcAddress(HMODULE hModule, LPCSTR lpProcName);

// 释放 DLL
BOOL FreeLibrary(HMODULE hModule);

// 获取错误信息
DWORD GetLastError();
1
2
3
4
5
6
7
8
9
10
11
12
13
14

3.2 Windows 侧的完整示例 ​

cpp
// ====== plugin_math.cpp — 编译为 math_plugin.dll ======
// 定义导出宏
#ifdef MATH_PLUGIN_EXPORTS
    #define MATH_API __declspec(dllexport)
#else
    #define MATH_API __declspec(dllimport)
#endif

extern "C" {

MATH_API int add(int a, int b) {
    return a + b;
}

MATH_API int multiply(int a, int b) {
    return a * b;
}

}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
bash
# 编译插件 (MSVC Developer Command Prompt)
cl /LD plugin_math.cpp /DMATH_PLUGIN_EXPORTS /Fe:math_plugin.dll
# /LD = 生成 DLL
1
2
3
cpp
// ====== host.cpp — 宿主程序 ======
#include <windows.h>
#include <cstdio>

typedef int (*calc_func)(int, int);

int main() {
    // 1. 加载 DLL
    HMODULE handle = LoadLibraryA("math_plugin.dll");
    if (!handle) {
        fprintf(stderr, "LoadLibrary failed: %lu\n", GetLastError());
        return 1;
    }
    printf("DLL loaded successfully\n");

    // 2. 获取函数地址
    calc_func add = (calc_func)GetProcAddress(handle, "add");
    calc_func multiply = (calc_func)GetProcAddress(handle, "multiply");

    if (!add || !multiply) {
        fprintf(stderr, "GetProcAddress failed\n");
        FreeLibrary(handle);
        return 1;
    }

    // 3. 调用
    printf("add(3, 5) = %d\n", add(3, 5));
    printf("multiply(3, 5) = %d\n", multiply(3, 5));

    // 4. 释放
    FreeLibrary(handle);

    return 0;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
bash
# 编译宿主(不需要 math_plugin.lib!不需要导入库!)
cl host.cpp /Fe:host.exe
1
2

重要:和 Linux 一样,Windows 上运行时动态加载也不需要导入库(.lib)!直接用 LoadLibrary + GetProcAddress,完全绕过了编译时的符号检查。

3.3 GetProcAddress 也可以按序号获取 ​

cpp
// 按函数名(最常用)
auto func = (MyFunc)GetProcAddress(handle, "add");

// 按序号(如果 DLL 用 .def 文件导出了序号)
auto func = (MyFunc)GetProcAddress(handle, MAKEINTRESOURCEA(1));
1
2
3
4
5

第4部分:跨平台封装 —— write once, compile everywhere ​

cpp
// dynamic_loader.h
#pragma once

#ifdef _WIN32
    #include <windows.h>
    #define PLUGIN_HANDLE       HMODULE
    #define PLUGIN_LOAD(path)   LoadLibraryA(path)
    #define PLUGIN_GETSYM(h, n) GetProcAddress(h, n)
    #define PLUGIN_CLOSE(h)     FreeLibrary(h)
    #define PLUGIN_EXT          ".dll"
    
    inline const char* plugin_error() {
        static char buf[256];
        FormatMessageA(FORMAT_MESSAGE_FROM_SYSTEM, NULL,
                       GetLastError(), 0, buf, sizeof(buf), NULL);
        return buf;
    }
#else
    #include <dlfcn.h>
    #define PLUGIN_HANDLE       void*
    #define PLUGIN_LOAD(path)   dlopen(path, RTLD_NOW | RTLD_LOCAL)
    #define PLUGIN_GETSYM(h, n) dlsym(h, n)
    #define PLUGIN_CLOSE(h)     dlclose(h)
    #define PLUGIN_EXT          ".so"
    
    inline const char* plugin_error() {
        return dlerror();
    }
#endif
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
cpp
// 使用跨平台封装
#include "dynamic_loader.h"

typedef int (*calc_func)(int, int);

void load_and_call(const char* plugin_path) {
    PLUGIN_HANDLE h = PLUGIN_LOAD(plugin_path);
    if (!h) {
        fprintf(stderr, "Load failed: %s\n", plugin_error());
        return;
    }

    auto func = (calc_func)PLUGIN_GETSYM(h, "add");
    if (func) {
        printf("Result: %d\n", func(3, 5));
    }

    PLUGIN_CLOSE(h);
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19

第5部分:第一个插件雏形 —— 可扩展计算器 ​

设计思路:宿主程序定义好"运算操作"的函数签名 int (int, int),每个插件提供一组运算(add、subtract、multiply、divide 等),宿主编译好后,任何人写出符合签名的 .so 放到 plugins 目录就能被加载。

┌─────────────────────────────────────────────────────────────────────────────┐
│            可扩展计算器架构                                                   │
├─────────────────────────────────────────────────────────────────────────────┤
│                                                                             │
│  host(主程序)                                                              │
│  ┌───────────────────────────────────────────────────┐                     │
│  │ 1. 扫描 plugins/ 目录                              │                     │
│  │ 2. 对每个 .so/.dll:                               │                     │
│  │    a. dlopen / LoadLibrary                        │                     │
│  │    b. dlsym / GetProcAddress 获取 add/sub/mul/div  │                     │
│  │    c. 注册进函数表                                  │                     │
│  │ 3. 用户输入 "3 + 5" → 查表 → 调用对应函数           │                     │
│  │ 4. 程序退出时 dlclose / FreeLibrary                │                     │
│  └───────────────────────────────────────────────────┘                     │
│       │               │               │                                     │
│       ▼               ▼               ▼                                     │
│  ┌──────────┐  ┌──────────┐  ┌──────────┐                                  │
│  │basic.so  │  │advanced  │  │custom.so │   ← 第三方可随时添加!             │
│  │ add/sub  │  │ .so      │  │ 自定义   │                                    │
│  │ mul/div  │  │ pow/sqrt │  │ 运算     │                                    │
│  └──────────┘  └──────────┘  └──────────┘                                  │
│                                                                             │
└─────────────────────────────────────────────────────────────────────────────┘
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23

这个雏形将在第 07 篇(接口设计)和第 08 篇(CMake 构建)中扩展为完整的插件系统。


核心总结 ​

┌─────────────────────────────────────────────────────────────────────────────┐
│              运行时动态加载 核心速查                                          │
├─────────────────────────────────────────────────────────────────────────────┤
│                                                                             │
│  Linux:dlopen / dlsym / dlclose / dlerror                                  │
│  Windows:LoadLibrary / GetProcAddress / FreeLibrary / GetLastError        │
│                                                                             │
│  RTLD_NOW:dlopen 时解析所有符号(失败早发现)                               │
│  RTLD_LAZY:用到时才解析(加载快但可能迟到崩溃)                              │
│  RTLD_LOCAL:符号隔离(推荐)                                                │
│  RTLD_GLOBAL:符号对其他库可见                                               │
│                                                                             │
│  关键优势:                                                                   │
│  • 编译时不需要知道插件的存在                                                 │
│  • 插件可以独立开发和分发                                                     │
│  • 插件不存在 → 优雅降级,不是启动失败                                        │
│                                                                             │
│  -ldl:Linux 上需要链接 libdl(提供 dlopen 系列函数)                         │
│                                                                             │
└─────────────────────────────────────────────────────────────────────────────┘
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20

章节测试 ​

测试1:运行时 vs 加载时 ​

运行时动态加载相比加载时链接,最核心的优势是什么?

测试2:dlopen flags ​

RTLD_LAZY 和 RTLD_NOW 有什么区别?为什么推荐开发时用 RTLD_NOW?

测试3:错误处理 ​

cpp
// 这段代码有什么潜在问题?
void* handle = dlopen("./plugin.so", RTLD_LAZY);
void* sym = dlsym(handle, "some_func");
printf("Error: %s\n", dlerror());
1
2
3
4

请写出正确的错误处理写法。

测试4:GetProcAddress 按序号 ​

除了按函数名获取函数地址,GetProcAddress 还有什么方式?什么场景下有用?

测试5:跨平台封装 ​

为什么跨平台封装中,Linux 侧用 RTLD_NOW | RTLD_LOCAL 组合,而不是 RTLD_LAZY | RTLD_GLOBAL?

测试6:编译依赖 ​

用运行时动态加载的方式使用一个插件,宿主程序编译时需要链接那个插件的 .lib(Windows)或 .so(Linux)吗?为什么?


参考答案 ​

测试1答案 ​

答案:(1) 插件可在编译后任意添加,不需要重新编译宿主;(2) 插件不存在时程序可以优雅降级(弹窗、跳过、使用备用方案),而不是直接启动失败;(3) 实现了真正的"可扩展"架构——宿主定义接口,任何人可以实现。

测试2答案 ​

答案:RTLD_LAZY 延迟解析符号(使用时才解析),RTLD_NOW 立即解析(dlopen 时就解析)。推荐开发时用 RTLD_NOW 因为可以在 dlopen 时立即发现符号缺失问题——如果用了 LAZY,可能程序运行到一半调用某个不存在的函数时才崩溃,更难排查。

测试3答案 ​

答案:问题在于 dlerror() 的返回值是"自上次调用 dlerror 以来的错误",而非"最近一次 dl* 调用的错误"。正确的写法:

cpp
void* handle = dlopen("./plugin.so", RTLD_NOW);
if (!handle) {
    fprintf(stderr, "dlopen: %s\n", dlerror());
    return;
}

dlerror();  // 清空旧错误
void* sym = dlsym(handle, "some_func");
char* err = dlerror();
if (err) {
    fprintf(stderr, "dlsym: %s\n", err);
}
1
2
3
4
5
6
7
8
9
10
11
12

测试4答案 ​

答案:还可以按序号(ordinal)获取:GetProcAddress(handle, MAKEINTRESOURCEA(1))。这在以下场景有用:(1) DLL 使用 .def 文件按序号导出,函数名可能被故意隐藏(反逆向);(2) 避免函数名字符串比较的性能开销;(3) 处理没有公开函数名只有序号的系统 DLL。

测试5答案 ​

答案:

  • RTLD_NOW:插件开发场景下,宁可加载时慢一点也要早发现符号问题——如果插件缺少承诺的函数,应该立即报错而非运行时随机崩溃。
  • RTLD_LOCAL:隔离符号——防止插件 A 和插件 B 导出同名内部函数时发生符号冲突。每个插件的符号只对自己可见,互不干扰。

测试6答案 ​

答案:不需要链接插件库文件。因为:(1) 编译时宿主程序中没有对插件函数的"符号引用"——它们通过函数指针间接调用;(2) 对操作系统来说,这些动态库不是宿主的"依赖",而是程序运行时自己管理的;(3) 宿主只需要链接提供 dlopen/LoadLibrary 的系统库(Linux 上的 -ldl),不涉及任何具体插件。


相关笔记 ​

  • windows build outputs - Windows LoadLibrary 的底层(IAT、PE 加载)
  • linux build outputs - Linux dlopen 的底层(ld.so、GOT/PLT)
  • static vs dynamic linking - 加载时链接 vs 运行时加载
  • plugin interface design - 从函数指针到完整的插件接口设计

下一步学习 ​

  • [ ] 阅读 07 - 插件接口设计 —— 从 C 函数指针到 C++ 纯虚接口

学习状态:🟡 开始学习

最后更新于:

Pager
上一篇1. 插件工程:可演进的模块边界 / Plugin Engineering and Evolvable Module Boundaries
下一篇3. 插件接口设计:C ABI、函数表与受控 C++ 接口 / Plugin Interface Design with C ABI, Function Tables, and Controlled C++ Interfaces

持续记录,持续成长

Copyright © Tidenflow