本文介紹了如何通過 C++ 的 工廠函數、動態庫(.so 文件)和 dlopen
/ dlsym
實現插件機制。這個機制允許程序在運行時動態加載和調用插件,而無需在編譯時知道插件的具體類型。
一、 動態插件機制
在現代 C++ 中,插件機制廣泛應用于需要擴展或靈活配置的場景,如:
-
策略模式:根據需求動態選擇不同策略。
-
插件化系統:如游戲引擎、服務器系統等,通過動態加載不同功能模塊。
通過使用 動態庫(.so 文件)和 工廠函數,可以實現插件的動態創建和管理。
二、插件機制核心流程
1. 創建插件接口(基類)
定義一個基類 PluginBase
,所有插件都繼承自它,實現自己的功能。
// plugin.hpp
class PluginBase {
public:virtual void run() = 0; // 純虛函數virtual ~PluginBase() {}
};
2. 實現具體插件
每個插件類實現 PluginBase
接口,并提供自己的功能。
#include "plugin.hpp"class PluginA : public PluginBase {
public:void run() override {std::cout << "PluginA is running!" << std::endl;}
};// 工廠函數
extern "C" PluginBase* create_plugin_0() {return new PluginA();
}
編譯命令:
g++ -fPIC -shared plugin_0.cpp plugin_1.cpp -o libplugin.so
將plugin_*.cpp編譯為動態庫:libplugin.so
3. 主程序加載插件
主程序通過 dlopen
加載動態庫,通過 dlsym
查找工廠函數,調用工廠函數創建插件對象,并執行其方法。
// main.cpp
#include <iostream>
#include <dlfcn.h>
#include "plugin.hpp"int main() {void* handle = dlopen("./libplugin.so", RTLD_LAZY); // 打開動態庫if (!handle) {std::cerr << "? Failed to open plugin: " << dlerror() << std::endl;return -1;}typedef PluginBase* (*CreateFunc)(); // 定義工廠函數指針類型// 查找兩個插件的工廠函數CreateFunc create_plugin_0 = (CreateFunc)dlsym(handle, "create_plugin_0");if (!create_plugin_0) {std::cerr << "? Failed to find symbol: " << dlerror() << std::endl;dlclose(handle);return -1;}CreateFunc create_plugin_1 = (CreateFunc)dlsym(handle, "create_plugin_1");if (!create_plugin_1) {std::cerr << "? Failed to find symbol: " << dlerror() << std::endl;dlclose(handle);return -1;}// 調用工廠函數創建插件對象PluginBase* plugin_0 = create_plugin_0();plugin_0->run(); // 執行插件功能PluginBase* plugin_1 = create_plugin_1();plugin_1->run(); // 執行插件功能// 釋放資源delete plugin_0;delete plugin_1;dlclose(handle); // 關閉動態庫return 0;
}
編譯命令:
g++ main.cpp -o main -ldl
鏈接libdl.so動態庫,dlopen、dlsym 這類函數屬于 Linux 系統的動態鏈接庫(libdl)
三、核心概念解析
1. typedef
和 函數指針
通過 typedef
為函數指針起別名,使得函數指針的聲明更加簡潔易讀。
typedef PluginBase* (*CreateFunc)();
-
CreateFunc
現在是指向無參、返回PluginBase*
的函數指針類型。 -
它允許我們用簡單的名字表示工廠函數類型。
2. dlopen
和 dlsym
-
dlopen
:打開動態庫并返回一個句柄,程序可以通過該句柄加載庫中的函數。 -
dlsym
:根據符號名稱在動態庫中查找對應的函數地址。
這些函數屬于 POSIX 標準,提供了 運行時加載和調用動態庫的能力。
3. 工廠函數
工廠函數是動態庫中暴露給主程序的接口,負責創建插件對象實例。通過 extern "C"
來確保該函數不進行 C++ 名字修飾,從而避免不同編譯器或鏈接時產生不同的符號名稱。
extern "C" PluginBase* create_plugin_0() {return new PluginA();
}
5. dlsym
返回值和強制類型轉換
CreateFunc create_plugin_0 = (CreateFunc)dlsym(handle, "create_plugin_0");
-
dlsym
返回void*
類型,表示它是一個通用的指針。void*
是一個 不帶類型信息的指針,它可以指向任何類型的對象或函數。 -
通過 強制轉換
(CreateFunc)
,將void*
轉換為我們預定義的 函數指針類型CreateFunc
。
這樣,通過create_plugin_0()
等工廠函數返回的插件對象指針,可以調用其定義的run
等方法。
四、 完整的工作流程
步驟 | 描述 |
---|---|
1. 插件開發 | 定義基類接口 PluginBase ,實現具體插件類,編寫工廠函數。 |
2. 編譯插件 | 使用 g++ 編譯源文件為動態庫 .so 文件。 |
3. 主程序 | 主程序使用 dlopen 加載動態庫,dlsym 查找工廠函數,創建插件對象。 |
4. 執行功能 | 通過插件對象調用具體功能(如 run() )。 |
5. 釋放資源 | 刪除插件對象,關閉動態庫。 |
五、完整demo
- plugin.hpp
// plugin.hpp
#ifndef PLUGIN_HPP
#define PLUGIN_HPPclass PluginBase {
public:virtual void run() = 0;virtual ~PluginBase() {}
};#endif
- plugin_0.cpp
// plugin.cpp
#include <iostream>
#include "plugin.hpp"// 派生類
class MyPlugin_0 : public PluginBase {
public:void run() override {std::cout << "🚀 MyPlugin_0 is running!" << std::endl;}
};// 工廠函數,必須用 C 接口導出,防止 C++ 名字修飾
extern "C" PluginBase* create_plugin_0() {return new MyPlugin_0();
}
- plugin_1.cpp
// plugin_1.cpp
#include <iostream>
#include "plugin.hpp"// 派生類
class MyPlugin_1 : public PluginBase {
public:void run() override {std::cout << "🚀 MyPlugin_1 is running!" << std::endl;}
};// 工廠函數,必須用 C 接口導出,防止 C++ 名字修飾
extern "C" PluginBase* create_plugin_1() {return new MyPlugin_1();
}
- main.cpp
// main.cpp
#include <iostream>
#include <dlfcn.h>
#include "plugin.hpp"int main() {// 1. 打開動態庫void* handle = dlopen("./libplugin.so", RTLD_LAZY);if (!handle) {std::cerr << "? Failed to open plugin: " << dlerror() << std::endl;return -1;}// 2. 查找工廠函數,先拿到目標函數指針,再基于這個指針創建對象。typedef PluginBase* (*CreateFunc)(); /* 函數指針語法結構:返回類型 (*指針名)(參數列表);typedef 原類型 新類型名;CreateFunc定義了一個指向“PluginBase* (*CreateFunc)()”此類函數的函數指針的別名*/CreateFunc create_plugin_0 = (CreateFunc)dlsym(handle, "create_plugin_0");// dlsym(handle, "create_plugin_0")返回的是一個 void* 也就是一個空句柄,將這個句柄強制轉換為CreateFunc// create_plugin_0即為目標函數(工廠函數)句柄,通過create_plugin_0()即可完成調用。if (!create_plugin_0) {std::cerr << "? Failed to find symbol: " << dlerror() << std::endl;dlclose(handle);return -1;}CreateFunc create_plugin_1 = (CreateFunc)dlsym(handle, "create_plugin_1");if (!create_plugin_1) {std::cerr << "? Failed to find symbol: " << dlerror() << std::endl;dlclose(handle);return -1;}// 3. 創建插件對象并調用PluginBase* plugin_0 = create_plugin_0(); // 將函數指針PluginBase*指向具體的create_plugin_0()plugin_0->run();PluginBase* plugin_1 = create_plugin_1();plugin_1->run();// 4. 釋放資源delete plugin_0;delete plugin_1;dlclose(handle);return 0;
}
- build.sh
g++ -fPIC -shared plugin_0.cpp plugin_1.cpp -o libplugin.so # 將plugin_*.cpp編譯為動態庫:libplugin.so
g++ main.cpp -o main -ldl # 鏈接libdl.so動態庫,dlopen、dlsym 這類函數屬于 Linux 系統的動態鏈接庫(libdl)
echo "Build complete!"# 執行:
# 賦予腳本文件執行權限:chmod +x build.sh
# 執行編譯腳本./build.sh
# 運行 ./main