C++中插件使用举例
插件并不是在構(gòu)建時(shí)鏈接的,而是在運(yùn)行時(shí)發(fā)現(xiàn)并加載的。因此,用戶可以利用你定義好的插件API來編寫自己的插件。這樣他們就能以指定方式擴(kuò)展API的功能。插件庫是一個(gè)動(dòng)態(tài)庫,它可以獨(dú)立于核心API編譯,在運(yùn)行時(shí)根據(jù)需要顯示加載。不過插件也可以使用靜態(tài)庫,比如在嵌入式系統(tǒng)中,所有插件都是在編譯時(shí)靜態(tài)鏈接到應(yīng)用程序中的。
你總是可以引入自己的插件文件擴(kuò)展名。例如,Adobe Illustrator使用的插件擴(kuò)展名是.aip,而Microsoft Excel插件的擴(kuò)展名是.xll。
很多商業(yè)化軟件包都允許使用C/C++插件擴(kuò)展其核心功能。在API中采樣插件模型有以下優(yōu)點(diǎn):
(1).更為通用:使你的API可以用于解決更大范圍內(nèi)的問題,而不需要為所有問題提供解決方案。
(2).更新量小:以插件形式存在的功能很容易獨(dú)立于應(yīng)用程序而更新,只需引入新版本的插件即可。相比發(fā)布整個(gè)應(yīng)用程序的新版本,這種方式的更新量要小得多。
一般而言,如果要?jiǎng)?chuàng)建插件系統(tǒng),有兩個(gè)主要特性是必須要設(shè)計(jì)的:
(1).插件API:要?jiǎng)?chuàng)建插件,用戶必須編譯并鏈接插件API。插件API是你提供給用戶的用于創(chuàng)建插件的接口。
(2).插件管理器:這是核心API代碼中的一個(gè)對(duì)象(一般設(shè)計(jì)為單例),負(fù)責(zé)管理所有插件的生命周期,即插件的加載、注冊(cè)和卸載等各個(gè)階段。該對(duì)象也叫做插件注冊(cè)表。
當(dāng)為API設(shè)計(jì)插件時(shí),一些設(shè)計(jì)決策會(huì)影響插件系統(tǒng)的精確架構(gòu):
(1).C還是C++:C++規(guī)范并沒有明確地定義ABI。因此,不同的編譯器,甚至同一編譯器的不同版本,生成的代碼可能無法做到二進(jìn)制兼容。這就暗示我們,對(duì)插件系統(tǒng)而言,如果客戶在開發(fā)插件時(shí)使用了一個(gè)ABI不同的編譯器,那么這樣的插件可能無法加載。相反,純C代碼的ABI有明確的含義,可以跨平臺(tái)和跨編譯器工作。
(2).版本控制:需要確定某插件構(gòu)建時(shí)使用的API版本是否與你的API版本兼容。
(3).內(nèi)部元數(shù)據(jù)還是外部元數(shù)據(jù):元數(shù)據(jù),比如可讀的名字和版本信息,既可以在插件代碼內(nèi)部定義,也可以通過一種簡單的外部文件格式指定。使用外部元數(shù)據(jù)的優(yōu)點(diǎn)是,并不需要加載所有插件,就能了解所有可用對(duì)象的集合。
(4).插件管理器是通用的還是專用的:插件管理器的一種實(shí)現(xiàn)方法是,使它的層次非常低,實(shí)現(xiàn)通用性,也就是說,它只是簡單地加載插件并訪問其中的符號(hào)。然而,這樣做意味著插件管理器不了解API中是否存在具體類型。其結(jié)果是對(duì)象可能必須以void*指針的形式返回,在使用之前再轉(zhuǎn)化為具體的類型。或者,插件管理器可以以最低限度前向聲明插件中任何對(duì)象的類型,這種方案的類型安全性更好,但也正因如此,它無法獨(dú)立于你的API而實(shí)現(xiàn)。
(5).安全性:你必須決定你對(duì)用戶插件的信任程度。插件是可以運(yùn)行在進(jìn)程之中的任意編譯過的代碼。因此,插件有可能做任何事情,包括訪問它不應(yīng)該訪問的數(shù)據(jù),以及刪除最終用戶硬盤上的文件,甚至讓整個(gè)應(yīng)用程序奔潰。如果你需要防護(hù)這種惡意插件,可以考慮創(chuàng)建一種基于套接字的方案,使插件運(yùn)行在獨(dú)立的進(jìn)程中,通過IPC通道與核心API通信。
(6).靜態(tài)庫還是動(dòng)態(tài)庫:插件也可以定義為靜態(tài)庫,這意味著插件必須編譯到應(yīng)用程序中。對(duì)消費(fèi)型應(yīng)用程序而言,更常見的方案是采用動(dòng)態(tài)庫,因?yàn)橛脩艨梢跃帉懽约旱牟寮?#xff0c;并且在運(yùn)行時(shí)擴(kuò)展應(yīng)用程序。編寫靜態(tài)插件還有一個(gè)約束,你必須確保任意兩個(gè)插件中沒有定義相同的符號(hào),也就是說,每個(gè)插件中初始化函數(shù)的命名必須是唯一的。
因?yàn)榭缙脚_(tái)和跨編譯器的ABI問題,支持C++插件有些困難,有些方法可以在插件中更安全地使用C++:
(1).使用抽象基類:實(shí)現(xiàn)抽象基類的虛方法可以使插件與ABI問題隔離,因?yàn)樘摲椒ㄕ{(diào)用通常是用類的虛函數(shù)表中的索引來表示的。
(2).自由函數(shù)使用C鏈接:為了避免C++ ABI問題,插件API中的所有全局函數(shù)都要使用C鏈接方式,也就是說,它們應(yīng)該使用extern “C”聲明。同理,為了最大化可移植性,插件傳遞給核心API的回調(diào)函數(shù)也應(yīng)該使用C鏈接方式。
(3).避免使用STL和異常:STL類(如std::string和std::vector)的不同實(shí)現(xiàn)可能不是ABI兼容的。因此,核心API與插件API之間的函數(shù)調(diào)用應(yīng)該盡量避免使用這些容器。同樣,因?yàn)椴煌幾g器之間異常的ABI往往也是不穩(wěn)定的,所以在你的插件API中也應(yīng)該避免。
(4).不要混用內(nèi)存分配器:插件鏈接的內(nèi)存分配器可能與你的API不同。要么所有的對(duì)象由插件分配并回收,要么將控制傳給核心API,由核心API負(fù)責(zé)所有對(duì)象的創(chuàng)建與銷毀。但核心API絕對(duì)不要釋放插件分配的對(duì)象,反之亦然。
插件API:插件API是你提供給用戶的用于創(chuàng)建插件的接口。當(dāng)核心API加載一個(gè)插件時(shí),為了讓插件正常工作,它需要知道應(yīng)該調(diào)用哪個(gè)函數(shù)或者要訪問哪個(gè)符號(hào)。這意味著插件中應(yīng)該明確定義具名的入口點(diǎn),用戶在創(chuàng)建插件時(shí)必須提供。這一點(diǎn)可以以不同的方式實(shí)現(xiàn)。
插件管理器:需要處理以下任務(wù):
(1).加載所有插件的元數(shù)據(jù):這些元數(shù)據(jù)既可以保存在單獨(dú)的文件中(比如xml文件),也可以嵌入到插件內(nèi)部。如果是后一種情況,為了收集所有插件的元數(shù)據(jù),插件管理器需要加載所有可用的插件。你可以以元數(shù)據(jù)的形式向用戶提供可用插件列表,以供他們選擇。
(2).將動(dòng)態(tài)庫加載到內(nèi)存中,提供對(duì)庫中符號(hào)的訪問能力,并在必要時(shí)卸載庫。在Unix(也包括Mac OSX)平臺(tái)上,這會(huì)涉及dlopen、dlclose、dlsym等函數(shù),而在Windows平臺(tái)上,涉及的是LoadLibrary、FreeLibrary及GetProcAddress等函數(shù)。
(3).當(dāng)插件加載時(shí),調(diào)用其初始化例程;而當(dāng)插件卸載時(shí),調(diào)用其清理例程。
因?yàn)椴寮芾砥鳛橄到y(tǒng)中的所有插件提供了單一訪問點(diǎn),所以它往往以單例模式實(shí)現(xiàn)。從設(shè)計(jì)角度看,我們可以將插件管理器看做一組插件實(shí)例的集合,其中每個(gè)插件實(shí)例表示一個(gè)插件,并提供了加載和卸載該插件的功能。
插件版本控制:既可以讓插件使用與核心API相同的版本號(hào),也可以為其引入專門的插件API版本號(hào)。我建議采用后者,因?yàn)椴寮嗀PI實(shí)際上是從核心API分離出來的接口,兩者可能以不同的頻率修改。除此之外,用戶可以選擇指定該插件支持的API的最小版本號(hào)和最大版本號(hào)。更普遍的做法是指定最小版本號(hào)。最小/最大版本號(hào)也可以通過外部元文件格式指定。
注:以上內(nèi)容摘自《C++ API設(shè)計(jì)》
以下是測(cè)試代碼:組織結(jié)構(gòu)如下圖所示:
src目錄下存放所有的API和核心庫code,common.hpp中的接口可認(rèn)為是核心API,plugin.hpp中為插件API,編譯此目錄可生成動(dòng)態(tài)庫address。
plugin目錄下存放插件API的實(shí)現(xiàn),編譯此目錄可生成一個(gè)名字為plugin_area.fbc的插件。
tests目錄為調(diào)用核心API和插件API的code,用來驗(yàn)證生成動(dòng)態(tài)庫address和插件plugin_area.fbc的正確性。
可通過配置文件如json或xml來指定需要加載的插件,在code中解析此配置文件,這樣替換插件時(shí)可無需重新編譯,直接修改配置文件即可。
通過腳本build.sh和CMakeLists.txt來編譯測(cè)試代碼,執(zhí)行build.sh 0生成plugin_area.fbc插件,執(zhí)行build.sh 1生成address動(dòng)態(tài)庫和執(zhí)行文件Plugin_Test。插件和動(dòng)態(tài)庫/執(zhí)行文件的生成是獨(dú)立的,它們?cè)诰幾g生成時(shí)無任何依賴關(guān)系。
各個(gè)文件內(nèi)容如下:
plugin/plugin_area.cpp:
#include <string.h>
#include <iostream>
#include <string>
#include <stdexcept>
#include "plugin.hpp"class Area : public Base {
public:Area() = default;~Area() = default;const char* version() override { return "1.0.0"; }const char* name() override { return "plugin_area"; }int get_area(const fbc_rect_t& rect) override { return ((rect.right - rect.left) * (rect.bottom - rect.top) + 10); }
};#ifdef __cplusplus
extern "C" {
#endifFBC_API Base* get_plugin_instance(const char* name)
{Area* area = new Area();if (strcmp(area->name(), name) != 0) {fprintf(stderr, "plugin name mismatch: %s, %s\n", area->name(), name);delete area;throw std::runtime_error("plugin name mismatch");return nullptr;}return area;
}FBC_API std::string get_plugin_name_version(Base* handle)
{if (!handle) {fprintf(stdout, "handle cann't equal nullptr\n");throw std::runtime_error("handle cann't equal nullptr");return "";}Area* area = dynamic_cast<Area*>(handle);std::string str(area->name());str += ".fbc.";str += area->version();return str;
}FBC_API void release_plugin_instance(Base* handle)
{delete dynamic_cast<Area*>(handle);
}#ifdef __cplusplus
}
#endif
src/common.hpp:
#ifndef FBC_PLUGIN_TEST_COMMON_HPP_
#define FBC_PLUGIN_TEST_COMMON_HPP_#ifdef _MSC_VER#ifdef DLL_EXPORTS#define FBC_API __declspec(dllexport)#else#define FBC_API#endif // _MSC_VER
#else#ifdef DLL_EXPORTS#define FBC_API __attribute__((visibility("default")))#else#define FBC_API#endif
#endiftypedef struct fbc_rect_t {int left, top;int right, bottom;
} fbc_rect_t;FBC_API char* get_csdn_blog_address();
FBC_API char* get_github_address();#endif // FBC_PLUGIN_TEST_COMMON_HPP_
src/common.cpp:
#include "common.hpp"FBC_API char* get_csdn_blog_address()
{return "https://blog.csdn.net/fengbingchun";
}FBC_API char* get_github_address()
{return "https://github.com//fengbingchun";
}
src/plugin.hpp:
#ifndef FBC_PLUGIN_TEST_PLUGIN_HPP_
#define FBC_PLUGIN_TEST_PLUGIN_HPP_#include "common.hpp"class Base {
public:virtual const char* version() = 0;virtual const char* name() = 0;virtual int get_area(const fbc_rect_t& rect) = 0;virtual ~Base() = default;
};#ifdef __cplusplus
extern "C" {
#endifFBC_API Base* get_plugin_instance(const char* name);
FBC_API std::string get_plugin_name_version(Base* handle);
FBC_API void release_plugin_instance(Base* handle);#ifdef __cplusplus
}
#endif #endif // FBC_PLUGIN_TEST_PLUGIN_HPP_
test/test.cpp:
#include <iostream>
#include <string>
#include <stdexcept>
#ifdef _MSC_VER
#include <windows.h>
#else
#include <dlfcn.h>
#endif
#include "common.hpp"
#include "plugin.hpp"int main()
{// test general dynamic libraryfprintf(stdout, "csdn blog address: %s\n", get_csdn_blog_address());fprintf(stdout, "github address: %s\n", get_github_address());// test pluginconst std::string plugin_name {"plugin_area"}, plugin_suffix {"fbc"};fbc_rect_t rect = {1, 2, 31, 52};#ifdef _MSC_VERHINSTANCE handle = LoadLibrary((plugin_name+"."+plugin_suffix).c_str());if (!handle) {fprintf(stderr, "fail to load plugin: %s, %d\n", plugin_name.c_str(), GetLastError());return -1;}typedef Base* (*LPGETINSTANCE)(const char* name);LPGETINSTANCE lpGetInstance = (LPGETINSTANCE)GetProcAddress(handle, "get_plugin_instance");if (!lpGetInstance) {fprintf(stderr, "fail to GetProcAddress: get_plugin_instance, %d\n", GetLastError());return -1;}Base* instance = nullptr;try {instance = (*lpGetInstance)(plugin_name.c_str());fprintf(stdout, "plugin name: %s, version: %s\n", instance->name(), instance->version());} catch (const std::exception& e) {fprintf(stderr, "exception: %s\ntest fail\n", e.what());return -1;}fprintf(stdout, "area: %d\n", instance->get_area(rect));typedef std::string (*LPVERSIONNAME)(Base* base);LPVERSIONNAME lpVersionName = (LPVERSIONNAME)GetProcAddress(handle, "get_plugin_name_version");if (!lpVersionName) {fprintf(stderr, "fail to GetProcAddress: get_plugin_name_version, %d\n", GetLastError());return -1;}try {fprintf(stdout, "plugin name version: %s\n", (*lpVersionName)(instance).c_str());} catch (const std::exception& e) {fprintf(stderr, "exception: %s\ntest fail\n", e.what());return -1;}typedef void (*LPRELEASEINSTANCE)(Base* base);LPRELEASEINSTANCE lpReleaseInstance = (LPRELEASEINSTANCE)GetProcAddress(handle, "release_plugin_instance");if (!lpReleaseInstance) {fprintf(stderr, "fail to GetProcAddress: release_plugin_instance, %d\n", GetLastError());return -1;}fprintf(stdout, "destroy Base\n");(*lpReleaseInstance)(instance);FreeLibrary(handle);
#elsevoid* handle = dlopen((plugin_name+"."+plugin_suffix).c_str(), RTLD_LAZY);if (!handle) {fprintf(stderr, "fail to load plugin: %s\n", plugin_name.c_str());return -1;}typedef Base* (*pGetInstance)(const char* name);pGetInstance pInstance = (pGetInstance)dlsym(handle, "get_plugin_instance");if (!pInstance) {fprintf(stderr, "fail to dlsym: get_plugin_instance\n");return -1;}Base* instance = nullptr;try {instance = (*pInstance)(plugin_name.c_str());fprintf(stdout, "plugin name: %s, version: %s\n", instance->name(), instance->version());} catch (const std::exception& e) {fprintf(stderr, "exception: %s\ntest fail\n", e.what());return -1;}fprintf(stdout, "area: %d\n", instance->get_area(rect));typedef std::string (*pVersionName)(Base* base);pVersionName pvername = (pVersionName)dlsym(handle, "get_plugin_name_version");if (!pvername) {fprintf(stderr, "fail to dlsym: get_plugin_name_version\n");return -1;}try {fprintf(stdout, "plugin name version: %s\n", (*pvername)(instance).c_str());} catch (const std::exception& e) {fprintf(stderr, "exception: %s\ntest fail\n", e.what());return -1;}typedef void (*pReleaseInstance)(Base* base);pReleaseInstance prelins = (pReleaseInstance)dlsym(handle, "release_plugin_instance");if (!prelins) {fprintf(stderr, "fail to dlsym: release_plugin_instance\n");return -1;}fprintf(stdout, "destroy Base\n");(*prelins)(instance);dlclose(handle);
#endiffprintf(stdout, "test finish\n");return 0;
}
build.sh:
#! /bin/bashusage() {echo "usage: $0 param"echo "if build plugin, then execute: $0 0"echo "if build src and test, then execute: $0 1"exit -1
}if [ $# != 1 ]; thenusage
fireal_path=$(realpath $0)
echo "real_path: ${real_path}"
dir_name=`dirname "${real_path}"`
echo "dir_name: ${dir_name}"build_dir=${dir_name}/build
mkdir -p ${build_dir}
cd ${build_dir}
if [ "$(ls -A ${build_dir})" ]; thenecho "directory is not empty: ${build_dir}"
elseecho "directory is empty: ${build_dir}"
fiplatform=`uname`
echo "##### current platform: ${platform}"if [ ${platform} == "Linux" ]; thenif [ $1 == 0 ]; thenecho "########## build plugin ##########"cmake -DBUILD_PLUGIN=ON ..elif [ $1 == 1 ]; thenecho "########## build src and test ##########"cmake -DBUILD_PLUGIN=OFF ..elseusagefimake
elseif [ $1 == 0 ]; thenecho "########## build plugin ##########"cmake -G"Visual Studio 15 2017" -A x64 -DBUILD_PLUGIN=ON ..elif [ $1 == 1 ]; thenecho "########## build src and test ##########"cmake -G"Visual Studio 15 2017" -A x64 -DBUILD_PLUGIN=OFF ..elseusageficmake --build . --target ALL_BUILD --config Release
ficd -
CMakeLists.txt:
PROJECT(Plugin_Test)
CMAKE_MINIMUM_REQUIRED(VERSION 3.9)SET(CMAKE_CXX_FLAGS_DEBUG "${CMAKE_CXX_FLAGS_DEBUG} -std=gnu++0x")
SET(CMAKE_CXX_FLAGS_RELEASE "${CMAKE_CXX_FLAGS_RELEASE} -std=gnu++0x")
SET(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -Wall -O2 -std=gnu++0x")
SET(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -std=gnu++0x -Wall -O2")SET(PATH_TEST_DIR ${CMAKE_CURRENT_SOURCE_DIR}/./../../../demo/Plugin_Test/demo1/test)
SET(PATH_SRC_DIR ${CMAKE_CURRENT_SOURCE_DIR}/./../../../demo/Plugin_Test/demo1/src)
SET(PATH_PLUGIN_DIR ${CMAKE_CURRENT_SOURCE_DIR}/./../../../demo/Plugin_Test/demo1/plugin)INCLUDE_DIRECTORIES(${PATH_SRC_DIR})FILE(GLOB_RECURSE PLUGIN_CPP_LIST ${PATH_PLUGIN_DIR}/*.cpp)
FILE(GLOB_RECURSE SRC_CPP_LIST ${PATH_SRC_DIR}/*.cpp)
FILE(GLOB_RECURSE TEST_CPP_LIST ${PATH_TEST_DIR}/*.cpp)ADD_DEFINITIONS(-DDLL_EXPORTS)IF(BUILD_PLUGIN)MESSAGE(STATUS "########## BUILD PLUGIN ##########")ADD_LIBRARY(plugin_area SHARED ${PLUGIN_CPP_LIST})SET_TARGET_PROPERTIES(plugin_area PROPERTIES PREFIX "" SUFFIX ".fbc")
ELSE()MESSAGE(STATUS "########## BUILD SRC AND TEST ##########")#SET(CMAKE_WINDOWS_EXPORT_ALL_SYMBOLS ON) # can generate address.lib in windowsADD_LIBRARY(address SHARED ${SRC_CPP_LIST}) ADD_EXECUTABLE(Plugin_Test ${TEST_CPP_LIST})IF(WIN32)TARGET_LINK_LIBRARIES(Plugin_Test address)ELSE()TARGET_LINK_LIBRARIES(Plugin_Test address dl)ENDIF()
ENDIF()
此測(cè)試代碼可同時(shí)在Windows和Linux下執(zhí)行。
在Windows下執(zhí)行結(jié)果如下:
在Linux下執(zhí)行結(jié)果如下:
GitHub:https://github.com/fengbingchun/Messy_Test
總結(jié)
以上是生活随笔為你收集整理的C++中插件使用举例的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 开源库BearSSL介绍及使用
- 下一篇: C和C++安全编码笔记:指针诡计