C++20 模块化编程:实现可插拔的插件架构

在 C++20 之前,插件化系统往往依赖于传统的共享库(DLL/so)以及复杂的运行时加载机制,导致跨平台兼容性、编译依赖和符号冲突等问题。C++20 引入的模块系统为构建插件化架构提供了新的思路。本文将从模块定义、编译管理、运行时装载以及插件注册四个层面,阐述如何使用 C++20 模块实现一个可插拔的插件框架,并给出完整代码示例。

1. 设计思路

  • 模块化分层:核心库(Engine)与插件(Plugin)在编译时分离,核心不依赖插件实现,只通过抽象接口进行交互。
  • 统一命名空间:所有模块使用统一的顶层命名空间 plugin_framework,避免符号冲突。
  • 编译管理:使用 CMaketarget_sources 配合 INTERFACE 目标管理模块文件,保证每个模块只编译一次。
  • 插件注册:采用 C++17 的 inline 变量 + std::vector 进行全局插件列表维护,插件实现通过 extern "C" 导出 register_plugin 函数,核心库在启动时动态扫描插件目录并调用。

2. 目录结构

plugin_framework/
├── CMakeLists.txt
├── core/
│   ├── CMakeLists.txt
│   ├── engine.hpp
│   └── engine.cpp
├── plugin_api/
│   ├── CMakeLists.txt
│   ├── module.hpp
│   └── module.cpp
└── plugins/
    ├── plugin_hello/
    │   ├── CMakeLists.txt
    │   ├── hello_module.hpp
    │   └── hello_module.cpp
    └── plugin_math/
        ├── CMakeLists.txt
        ├── math_module.hpp
        └── math_module.cpp

3. 核心库 engine.hpp

#pragma once
#include <vector>
#include <memory>
#include <functional>
#include <string>
#include <filesystem>
#include <iostream>

namespace plugin_framework {

// 前向声明
struct Module;

// 抽象插件接口
struct Module {
    virtual ~Module() = default;
    virtual std::string name() const = 0;
    virtual void execute() = 0;
};

using ModuleFactory = std::function<std::unique_ptr<Module>()>;

void load_plugins(const std::filesystem::path& dir);
void run_all();

} // namespace plugin_framework

4. 核心库 engine.cpp

#include "engine.hpp"
#include <dlfcn.h>
#include <filesystem>
#include <iostream>

namespace plugin_framework {

static std::vector<std::unique_ptr<Module>> g_modules;

void load_plugins(const std::filesystem::path& dir) {
    for (auto const& entry : std::filesystem::directory_iterator(dir)) {
        if (entry.is_regular_file() && entry.path().extension() == ".so") {
            void* handle = dlopen(entry.path().c_str(), RTLD_NOW);
            if (!handle) {
                std::cerr << "dlopen failed: " << dlerror() << '\n';
                continue;
            }
            using RegFn = void(*)();
            dlerror(); // clear
            RegFn reg = (RegFn)dlsym(handle, "register_plugin");
            const char* dlsym_error = dlerror();
            if (dlsym_error) {
                std::cerr << "dlsym failed: " << dlsym_error << '\n';
                dlclose(handle);
                continue;
            }
            reg(); // 注册插件
        }
    }
}

void run_all() {
    for (auto& mod : g_modules) {
        std::cout << "Running plugin: " << mod->name() << '\n';
        mod->execute();
    }
}

// 插件注册入口
extern "C" void register_plugin(ModuleFactory factory) {
    g_modules.emplace_back(factory());
}
} // namespace plugin_framework

5. 插件 API module.hpp

#pragma once
#include <memory>
#include "engine.hpp"

namespace plugin_framework {

struct Module {
    virtual ~Module() = default;
    virtual std::string name() const = 0;
    virtual void execute() = 0;
};

using ModuleFactory = std::function<std::unique_ptr<Module>()>;

} // namespace plugin_framework

6. 示例插件 hello_module.hpp

#pragma once
#include <string>
#include "module.hpp"

class HelloModule : public plugin_framework::Module {
public:
    std::string name() const override { return "HelloModule"; }
    void execute() override { std::cout << "Hello from HelloModule!\n"; }
};

7. 示例插件 hello_module.cpp

#include "hello_module.hpp"
#include <memory>

extern "C" void register_plugin(plugin_framework::ModuleFactory factory) {
    plugin_framework::register_plugin(factory);
}

static plugin_framework::ModuleFactory hello_factory = []{
    return std::make_unique <HelloModule>();
};

// 动态链接库加载时自动调用
__attribute__((constructor))
static void init() {
    register_plugin(hello_factory);
}

8. CMake 配置

根 CMakeLists.txt

cmake_minimum_required(VERSION 3.22)
project(PluginFramework LANGUAGES CXX)

set(CMAKE_CXX_STANDARD 20)
set(CMAKE_CXX_STANDARD_REQUIRED ON)

add_subdirectory(core)
add_subdirectory(plugin_api)
add_subdirectory(plugins)

core/CMakeLists.txt

add_library(core STATIC engine.cpp)
target_include_directories(core PUBLIC ${CMAKE_CURRENT_SOURCE_DIR})

plugin_api/CMakeLists.txt

add_library(plugin_api STATIC module.cpp)
target_include_directories(plugin_api PUBLIC ${CMAKE_CURRENT_SOURCE_DIR})

plugins/CMakeLists.txt

add_subdirectory(plugin_hello)
add_subdirectory(plugin_math)

plugins/plugin_hello/CMakeLists.txt

add_library(plugin_hello SHARED hello_module.cpp)
target_link_libraries(plugin_hello PRIVATE core plugin_api)
target_include_directories(plugin_hello PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}/../..)
set_target_properties(plugin_hello PROPERTIES PREFIX "")

9. 运行步骤

mkdir build && cd build
cmake ..
make -j$(nproc)
# 生成的核心库 core/libcore.so 与插件插件共享库 plugin_hello/libplugin_hello.so 位于 build 目录
# 运行示例程序(假设你编写一个 main.cpp)

main.cpp

#include "engine.hpp"
#include <filesystem>

int main() {
    plugin_framework::load_plugins("./plugins"); // 加载插件目录
    plugin_framework::run_all();                 // 运行所有插件
    return 0;
}

编译并运行:

g++ -std=c++20 -o demo main.cpp core/libcore.so
LD_LIBRARY_PATH=plugins ./demo

10. 关键点回顾

  1. 模块分离:核心库不直接引用插件实现,降低编译耦合。
  2. 统一注册机制:使用 extern "C" + __attribute__((constructor)) 自动在库加载时注册插件。
  3. 共享库命名:去掉默认前缀 lib,让插件更易于识别与加载。
  4. 编译系统:CMake 的 target_sourcesINTERFACE 目标可进一步简化模块化管理。

通过上述方式,你可以在 C++20 环境下快速搭建一个可插拔、跨平台的插件化架构。未来还可以扩展为热更新、版本管理、权限隔离等高级功能。祝你编码愉快!

发表评论