ゲーム業界エンジニアの独り言

クライアント、サーバ、インフラなどなど興味あることなんでも紹介

APIのプラグインによる拡張性

今、「C++のためのAPIデザイン」を読んでます。 APIを使うユーザが、APIを拡張するため必要なプラグインについてまとめてみました。

C++のためのAPIデザイン

C++のためのAPIデザイン

目次

APIの拡張性とは?

拡張性とは、APIを使うユーザがAPI開発者の手を借りずに、 特定のニーズ用に、インターフェイスの振る舞いを変更出来る事です。 拡張性があれば、API開発者は焦点を定めた無駄のないインターフェイスに、 集中することが出来ます。

その一方、ユーザにはAPI開発者が予想もしなかった問題を解決できる、 柔軟性を与える事が出来るのです。

プラグインによる拡張

プラグインの最も一般的な方法は、APIライブラリ構築時に アプリケーションにリンクするダイナミックライブラリではなく、 実行時に認識され、ロードさせるダイナミックライブラリです。

そこで、ユーザに分かりやすいプラグインAPIを提供すれば、 ユーザがプラグインを記述し、ユーザはAPI開発者が指定した方法で、 機能を拡張する事が出来るのです。

ーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーー API    APIコード    → コンパイラ → APIライブラリ  ーーーーーーーーーーーーーーーーーーーーーーーー↓ーーーーーー    プラグインコード → コンパイラ → プラグインライブラリ  プラグイン ーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーー

プラグインライブラリはコアAPIとは別にコンパイル出来ます。 APIによって要求に応じてロードされるダイナミックライブラリです。

APIプラグインを採用する利点

ソフトウェアのパッケージ製品の多くが、C/C++プラグインで コア機能の拡張が可能です。

APIプラグインを採用する利点を以下に述べます。

・用途の拡張 APIが解決できる問題は広域に及ぶ可能性があります。 しかし、プラグインがあれば、全てのソリューションを実装する必要はありません。

・コミュニュティの発展 APIフレームワーク内で、ユーザに問題解決できる方法を与えると、 ユーザの貢献でAPIが拡張される、コミュニュティが構築出来る。

・更新の小容量化 プラグインで作られた機能は、新バージョンのプラグインを更新するだけで、 アプリケーションとは別に、簡単に更新することが出来ます。 アプリケーション全体の新バージョンを配布するより、ずっと小さい更新で済みます。

・将来性の確保 APIはある時点で、これ以上の更新が必要ない安定したレベルに達するものです。 そこで、プラグインの開発によって機能を進化させ続ければ、APIの有用性や、 重要性を長時間保つ事が出来ます。

・リスクの分散 プラグインを使うと、コアシステムを不安定にさせず、機能を変更する事が出来ます。 そのため、社内開発でもメリットがあります。

プラグインシステムの設計

プラグインを設計する方法はいくつもあります。 最善のソリューションはプロジェクトごとに異なります。

一般に、プラグインシステムを構築する際に、設計すべき機能は2つです。

1.プラグインAPI プラグインを作成するために、ユーザがコンパイルとリンクを行う対象のAPIです。 プラグインシステムで使用する、大きなコードベースのコアAPIとは区別します。

2.プラグインマネージャプラグインのライフサイクルを管理(ロード、登録、解放)する コアAPIコード内のオブジェクト(通常はシングルトン)です。 このオブジェクトはプラグインレジストリとも呼ばれます。

ーーーーーーーーーーーーーーーーーーーーーー コアAPI              プラグインマネージャ ーーーーー↓ーーーー↓ーーーーー↓ーーーーー  プラグイン1 プラグイン2 プラグイン3 プラグイン ーーーーーーーーーーーーーーーーーーーーーー

プラグインマネージャはプラグインを認識してロードします。

C++プラグイン実装

ここで説明するプラグインシステムでは、ファクトリメソッドを提供して、 C++のクラスをコアAPIに登録出来るようにします。 ユーザが3Dフラフィックレンダラのプラグインを作成し、コアAPIに登録します。

プラグインAPI

プラグインAPIはユーザーがプラグインを作成できるようにするための、 インターフェイスです。 ここでのファイル名は"pluginapi.h"とします。 これには、プラグインがコアAPIと通信するための機能が格納されます。

// pluginapi.h
#include "defines.h"
#include "renderer.h"

#define CORE_FUNC extern "C" CORE_API
#define PLUGIN_FUNC extern "C" PLUGIN_API

#define PLUGIN_INIT() PLUGIN_FUNC int PluginInit()
#define PLUGIN_FREE() PLUGIN_FUNC int PluginFree()

typedef IRender *(*RendererInitFunc)();
typedef void (*RendererFreeFunc)(IRender *);

CORE_FUNC void RegisterRenderer(
    const char *type,
    RendererInitFunc init_cb,
    RendererFreeFunc free_cb);

このヘッダではプラグイン用に、初期化関数とクリーンナップ関数を マクロで定義しています。 それぞれ、PLUGIN_INIT()とPLUGIN_FREE()です。

また、PLUGIN_FUNCマクロではコアAPIが呼び出せるように、 プラグイン関数を、エクスポートさせます。 逆に、CORE_FUNCマクロでは、プラグインが呼び出せるように、 コアAPI関数をエクスポートさせています。

最後に、RegisterRenderer()関数を提供し、プラグインに 新しいIRendererクラスをコアAPIに登録できるようにしました。

プラグインが新しいIRendererクラス用にInit()関数とFree()関数を 提供する必要があります。 これはプラグイン内で、メモリの割り当てと解放を確実に行わせるためです。

また、CORE_APIとPLUGIN_APIの定義にも注目してください。 これでWindows上で、正しいDLLエクスポート/インポートの修飾子が指定出来ます。 CORE_APIはコアAPIの一部の関数を修飾し、PLUGIN_APIプラグイン内で、 定義されることになる関数に使われます。 これらのマクロは"defines.h"に格納されています。

// defines.h
#ifdef _WIN32
#ifdef BUILDING_CORE
#define CORE_API __declspec(dllexport)
#define PLUGIN_API __declspec(dllimport)
#else
#define CORE_API __declspec(dllimport)
#define PLUGIN_API __declspec(dllexport)
#endif
#else
#define CORE_API
#define PLUGIN_API
#endif

これらのマクロを、正しく機能させるには、BUILDING_CORE定義セットで ビルドする必要があることに注意してくだい。 なお、この定義はプラグインコンパイルする時には必要ありません。

最後に、拡張する3Dフラフィックレンダラの基礎クラスが定義されている、 "renderer.h"のファイル内容を示します。

// renderer.h
#include <string>

class IRenderer
{
public:
    virtual ~IRenderer() {}
    virtual bool LoadScene(const char *file_name) = 0;
    virtual void SetViewportSize(int w, int h) = 0;
    virtual void SetCameraPosition(double x, double y, double z) = 0;
    virtual void SetLookAt(double x, double y, double z) = 0;
    virtual void Render() = 0;
};

プラグインのコード例

初歩的なプラグインAPIを開発したので、このAPIに対して構築したプラグインが、 どのようなものかを検証していきます。

ここに含めるべきパーツは以下の通りです。

1.新しいIRendererクラス 2.このクラスの作成と破棄を行うコールバック 3.作成/破棄コールバックをコアAPIに登録するプラグイン初期化ルーチン

このようにしたプラグインのコードを以下に示します。 このプラグインは「opengl」という新しいレンダラを定義し登録しています。

// plugin1.cpp
#include "pluginapi.h"
#include <iostream>

class OpenGLRenderer : public IRenderer
{
public:
    virtual ~OpenGLRenderer() {}
    virtual bool LoadScene(const char *file_name) { return true; }
    virtual void SetViewportSize(int w, int h) {}
    virtual void SetCameraPosition(double x, double y, double z) {}
    virtual void SetLookAt(double x, double y, double z) {}
    virtual void Render() { std::cout << "OpenGL Renderer" << std::endl; }
};

PLUGIN_FUNC IRenderer *CreateRenderer()
{
    return new OpenGLRenderer();
}

PLUGIN_FUNC void DestroyRenderer(IRenderer *r)
{
    delete r;
}

PLUGIN_INIT()
{
    RegisterRenderer("opengl", CreateRenderer, DestroyRenderer);
    return 0;
}

プラグインマネージャ

プラグインAPIと、このAPIに対するプラグインを構築したので、 次はこれらのプラグインをコアAPIにロードして登録します。 これらはプラグインマネージャの仕事です。

プラグインマネージャの具体的なタスクを以下に示します。

・全てのプラグインメタデータをロード 全てのプラグインメタデータをロードします。 メタデータは別ファイルに格納したり(XMLプラグイン自体に含んだりします。 このメタデータによって、使用可能なプラグインのリストが表示され、 ユーザは必要なプラグインを選択する事が出来ます。

・ダイナミックライブラリのロード ダイナミックライブラリをメモリにロードし、このライブラリの シンボルへのアクセスを提供します。必要であればアンロードも行います。 (これらの詳細な方法についてはここで説明しません。)

プラグインの初期化ルーチンとクリーンナップルーチンの呼び出し プラグインがロードされた時に、プラグインの初期化ルーチンを呼び出し、 プラグインがアンロードされた時に、クリーンナップルーチンを呼び出します。

プラグインマネージャは、システム内の全てのプラグインにアクセスするための、 窓口を提供するため、シングルトンで実装される事が多いです。

では、以下にプラグインマネージャの実装例を示します。

// pluginmanager.cpp
#include "defines.h"
#include <string>
#include <vector>

class CORE_API PluginInstance
{
public:
    explict PluginInstance(const std::string &name);
    ~PluginInstance():
    bool Load();
    bool Unload();
    bool IsLoaded();
    std::string GetFileName();
    std::string GetDisplayName();

private:
    PluginInstance(const PluginInstance &);
    const PluginInstance &operator =(const PluginInstance &);
    class Impl;
    Impl* mImpl;
};

class CORE_API PluginManager
{
public:
    static PluginManager &GetInstance();
    bool LoadAll();
    bool Load(const std::string &name);
    bool UnloadAll();
    bool Unload(const std::string &name);
    bool IsLoaded();
    std::vector<PluginInstance *> GetAllPlugins();

private:
    PluginManager();
    ~PluginManager();
    PluginManager(const PluginManager &);
    const PluginManager &operator =(const PluginManager &);
    std::vector<PluginInstance *> mPlugins;
};

プラグインメタデータは別ファイルに定義します。 以下にメタデータファイル(XML)の例を示します。

<?xml version="1.0" encoding='UTF-8' ?>
<plugins>
    <plugin filename="oglplugin">
        <name>OpenGL Renderer</name>
    </plugin>
    <plugin filename="dxplugin">
        <name>DirectX Renderer</name>
    </plugin>
    <plugin filename="mesaplugin">
        <name>Mesa Renderer</name>
    </plugin>
</plugins>

以下のコードは使用可能な全てのプラグイン名を表示します。

std::vector<PluginInstance *> plugins = 
    PluginManager::GetInstance().GetAllPlugins();

std::vector<PluginInstance *>::iteretor it;
for (it = plugins.begin(); it != plugins.end(); ++it)
{
    PluginInstance *pi = *it;
    std::cout << "Plugin: " << pi->GetDisplayName() << std::endl;
}

ダイナミックライブラリの作成と呼び出し方法は、次回説明しようと思います。