C++中插件使用舉例

插件並不是在構建時鏈接的,而是在運行時發現並加載的。因此,用戶可以利用你定義好的插件API來編寫自己的插件。這樣他們就能以指定方式擴展API的功能。插件庫是一個動態庫,它可以獨立於核心API編譯,在運行時根據需要顯示加載。不過插件也可以使用靜態庫,比如在嵌入式系統中,所有插件都是在編譯時靜態鏈接到應用程序中的

你總是可以引入自己的插件文件擴展名。例如,Adobe Illustrator使用的插件擴展名是.aip,而Microsoft Excel插件的擴展名是.xll。

很多商業化軟件包都允許使用C/C++插件擴展其核心功能。在API中採樣插件模型有以下優點:

(1).更爲通用:使你的API可以用於解決更大範圍內的問題,而不需要爲所有問題提供解決方案。

(2).更新量小:以插件形式存在的功能很容易獨立於應用程序而更新,只需引入新版本的插件即可。相比發佈整個應用程序的新版本,這種方式的更新量要小得多。

一般而言,如果要創建插件系統,有兩個主要特性是必須要設計的:

(1).插件API:要創建插件,用戶必須編譯並鏈接插件API。插件API是你提供給用戶的用於創建插件的接口。

(2).插件管理器:這是核心API代碼中的一個對象(一般設計爲單例),負責管理所有插件的生命週期,即插件的加載、註冊和卸載等各個階段。該對象也叫做插件註冊表。

當爲API設計插件時,一些設計決策會影響插件系統的精確架構:

(1).C還是C++:C++規範並沒有明確地定義ABI。因此,不同的編譯器,甚至同一編譯器的不同版本,生成的代碼可能無法做到二進制兼容。這就暗示我們,對插件系統而言,如果客戶在開發插件時使用了一個ABI不同的編譯器,那麼這樣的插件可能無法加載。相反,純C代碼的ABI有明確的含義,可以跨平臺和跨編譯器工作。

(2).版本控制:需要確定某插件構建時使用的API版本是否與你的API版本兼容。

(3).內部元數據還是外部元數據:元數據,比如可讀的名字和版本信息,既可以在插件代碼內部定義,也可以通過一種簡單的外部文件格式指定。使用外部元數據的優點是,並不需要加載所有插件,就能瞭解所有可用對象的集合。

(4).插件管理器是通用的還是專用的:插件管理器的一種實現方法是,使它的層次非常低,實現通用性,也就是說,它只是簡單地加載插件並訪問其中的符號。然而,這樣做意味着插件管理器不瞭解API中是否存在具體類型。其結果是對象可能必須以void*指針的形式返回,在使用之前再轉化爲具體的類型。或者,插件管理器可以以最低限度前向聲明插件中任何對象的類型,這種方案的類型安全性更好,但也正因如此,它無法獨立於你的API而實現。

(5).安全性:你必須決定你對用戶插件的信任程度。插件是可以運行在進程之中的任意編譯過的代碼。因此,插件有可能做任何事情,包括訪問它不應該訪問的數據,以及刪除最終用戶硬盤上的文件,甚至讓整個應用程序奔潰。如果你需要防護這種惡意插件,可以考慮創建一種基於套接字的方案,使插件運行在獨立的進程中,通過IPC通道與核心API通信。

(6).靜態庫還是動態庫:插件也可以定義爲靜態庫,這意味着插件必須編譯到應用程序中。對消費型應用程序而言,更常見的方案是採用動態庫,因爲用戶可以編寫自己的插件,並且在運行時擴展應用程序。編寫靜態插件還有一個約束,你必須確保任意兩個插件中沒有定義相同的符號,也就是說,每個插件中初始化函數的命名必須是唯一的。

因爲跨平臺和跨編譯器的ABI問題,支持C++插件有些困難,有些方法可以在插件中更安全地使用C++:

(1).使用抽象基類:實現抽象基類的虛方法可以使插件與ABI問題隔離,因爲虛方法調用通常是用類的虛函數表中的索引來表示的。

(2).自由函數使用C鏈接:爲了避免C++ ABI問題,插件API中的所有全局函數都要使用C鏈接方式,也就是說,它們應該使用extern “C”聲明。同理,爲了最大化可移植性,插件傳遞給核心API的回調函數也應該使用C鏈接方式。

(3).避免使用STL和異常:STL類(如std::string和std::vector)的不同實現可能不是ABI兼容的。因此,核心API與插件API之間的函數調用應該儘量避免使用這些容器。同樣,因爲不同編譯器之間異常的ABI往往也是不穩定的,所以在你的插件API中也應該避免。

(4).不要混用內存分配器:插件鏈接的內存分配器可能與你的API不同。要麼所有的對象由插件分配並回收,要麼將控制傳給核心API,由核心API負責所有對象的創建與銷燬。但核心API絕對不要釋放插件分配的對象,反之亦然。

插件API:插件API是你提供給用戶的用於創建插件的接口。當核心API加載一個插件時,爲了讓插件正常工作,它需要知道應該調用哪個函數或者要訪問哪個符號。這意味着插件中應該明確定義具名的入口點,用戶在創建插件時必須提供。這一點可以以不同的方式實現。

插件管理器:需要處理以下任務:

(1).加載所有插件的元數據:這些元數據既可以保存在單獨的文件中(比如xml文件),也可以嵌入到插件內部。如果是後一種情況,爲了收集所有插件的元數據,插件管理器需要加載所有可用的插件。你可以以元數據的形式向用戶提供可用插件列表,以供他們選擇。

(2).將動態庫加載到內存中,提供對庫中符號的訪問能力,並在必要時卸載庫。在Unix(也包括Mac OSX)平臺上,這會涉及dlopen、dlclose、dlsym等函數,而在Windows平臺上,涉及的是LoadLibrary、FreeLibrary及GetProcAddress等函數。

(3).當插件加載時,調用其初始化例程;而當插件卸載時,調用其清理例程。

因爲插件管理器爲系統中的所有插件提供了單一訪問點,所以它往往以單例模式實現。從設計角度看,我們可以將插件管理器看做一組插件實例的集合,其中每個插件實例表示一個插件,並提供了加載和卸載該插件的功能。

插件版本控制:既可以讓插件使用與核心API相同的版本號,也可以爲其引入專門的插件API版本號。我建議採用後者,因爲插件API實際上是從核心API分離出來的接口,兩者可能以不同的頻率修改。除此之外,用戶可以選擇指定該插件支持的API的最小版本號和最大版本號。更普遍的做法是指定最小版本號。最小/最大版本號也可以通過外部元文件格式指定。

注:以上內容摘自《C++ API設計

以下是測試代碼:組織結構如下圖所示:

src目錄下存放所有的API和核心庫code,common.hpp中的接口可認爲是核心API,plugin.hpp中爲插件API,編譯此目錄可生成動態庫address。

plugin目錄下存放插件API的實現,編譯此目錄可生成一個名字爲plugin_area.fbc的插件。

tests目錄爲調用核心API和插件API的code,用來驗證生成動態庫address和插件plugin_area.fbc的正確性。

可通過配置文件如json或xml來指定需要加載的插件,在code中解析此配置文件,這樣替換插件時可無需重新編譯,直接修改配置文件即可。

通過腳本build.sh和CMakeLists.txt來編譯測試代碼,執行build.sh 0生成plugin_area.fbc插件,執行build.sh 1生成address動態庫和執行文件Plugin_Test。插件和動態庫/執行文件的生成是獨立的,它們在編譯生成時無任何依賴關係。

各個文件內容如下:

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" {
#endif

FBC_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
#endif

typedef 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" {
#endif

FBC_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 library
	fprintf(stdout, "csdn blog address: %s\n", get_csdn_blog_address());
	fprintf(stdout, "github address: %s\n", get_github_address());

    // test plugin
	const std::string plugin_name {"plugin_area"}, plugin_suffix {"fbc"};
	fbc_rect_t rect = {1, 2, 31, 52};

#ifdef _MSC_VER
    HINSTANCE 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);
#else
	void* 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);
#endif

	fprintf(stdout, "test finish\n");
	return 0;
}

build.sh:

#! /bin/bash

usage() {
    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 ]; then
    usage
fi

real_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})" ]; then
	echo "directory is not empty: ${build_dir}"
else
	echo "directory is empty: ${build_dir}"
fi

platform=`uname`
echo "##### current platform: ${platform}"

if [ ${platform} == "Linux" ]; then
    if [ $1 == 0 ]; then
        echo "########## build plugin ##########"
        cmake -DBUILD_PLUGIN=ON ..
    elif [ $1 == 1 ]; then
        echo "########## build src and test ##########"
        cmake -DBUILD_PLUGIN=OFF ..
    else
        usage
    fi

    make
else
    if [ $1 == 0 ]; then
        echo "########## build plugin ##########"
        cmake -G"Visual Studio 15 2017" -A x64 -DBUILD_PLUGIN=ON ..
    elif [ $1 == 1 ]; then
        echo "########## build src and test ##########"
        cmake -G"Visual Studio 15 2017" -A x64 -DBUILD_PLUGIN=OFF ..
    else
        usage
    fi

    cmake --build . --target ALL_BUILD --config Release
fi

cd -

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 windows
    ADD_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()

此測試代碼可同時在Windows和Linux下執行。

在Windows下執行結果如下:

在Linux下執行結果如下:

GitHubhttps://github.com/fengbingchun/Messy_Test

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章