関数ポインターは奇妙な生き物です。void*
それらは必ずしもデータ ポインターと同じサイズであるとは限らないため、安全にキャストしたり戻したりすることはできません。ただし、C++ (および C) 仕様では、任意の関数ポインターを別の関数ポインター型に安全にキャストできます (ただし、定義済みの動作が必要な場合は、呼び出す前に、後で以前の型にキャストする必要があります)。これは、任意のデータ ポインタを安全にキャストしvoid*
たり戻したりする機能に似ています。
メソッドへのポインターは、非常に厄介な場所です。メソッド ポインターは、アプリケーションが 32 ビットであるか 64 ビットであるかなど、コンパイラーによっては、通常の関数ポインターよりも大きい場合があります。しかし、さらに興味深いのは、同じコンパイラ/プラットフォーム、すべてのメソッド ポインターが同じサイズであるとは限りません。仮想関数へのメソッド ポインターは、通常のメソッド ポインターよりも大きい場合があります。多重継承 (ダイヤモンド パターンの仮想継承など) が関係している場合、メソッド ポインターはさらに大きくなる可能性があります。これは、コンパイラとプラットフォームによっても異なります。これは、特にヒープにメモリを割り当てずに関数オブジェクト (任意のメソッドとフリー関数をラップする) を作成することが難しい理由でもあります (テンプレート sorceryを使用することで可能です)。
そのため、インターフェイスで関数ポインターを使用すると、プラグインの作成者が同じコンパイラーを使用していても、メソッド ポインターをフレームワークに戻すことは現実的ではなくなります。これは許容できる制約かもしれません。これについては後で詳しく説明します。
関数ポインタがコンパイラ間で同じサイズであるという保証はないため、関数ポインタを登録することにより、プラグインの作成者を、コンパイラと同じサイズの関数ポインタを実装するコンパイラに限定することになります。関数ポインターのサイズはコンパイラーのバージョン間で安定している傾向があるため (複数のコンパイラーで同じになる場合もあります)、これは実際には必ずしもそれほど悪いことではありません。
関数ポインターが指す関数を呼び出すときに、実際の問題が発生し始めます。関数の真の署名がわからない場合、関数を安全に呼び出すことはまったくできません (「機能しない」からセグメンテーション違反に至るまで、悪い結果が得られます) 。したがって、プラグインの作成者は、void
パラメーターをとらない関数のみを登録するようにさらに制限されます。
さらに悪いことに、関数呼び出しがアセンブラー レベルで実際に機能する方法は、シグネチャと関数ポインターのサイズだけに依存するわけではありません。呼び出し規則、例外の処理方法 (例外がスローされたときにスタックを適切に巻き戻す必要がある)、関数ポインターのバイトの実際の解釈 (データ ポインターよりも大きい場合、余分なバイトはどうするか) もあります。どのような順序で?) この時点で、プラグインの作成者は、あなたと同じコンパイラ (およびバージョン!) を使用することにほとんど制限されており、呼び出し規約と例外処理オプションを一致させるように注意する必要があります (たとえば、例外処理などの MSVC++ コンパイラを使用)。オプションで明示的に有効にするだけ/EHsc
です)、定義した正確なシグネチャを持つ通常の関数ポインターのみを使用します。
これまでのすべての制限は、多少の制限があるとしても、合理的であると見なすことができます。しかし、まだ終わりではありません。
std::string
ただし、同じコンパイラ (およびバージョン) を使用しても、STL を制御するいくつかの異なるフラグ/マクロがあるため、(または STL のほぼすべての部分)を投入すると、事態はさらに悪化します。これらのフラグは、文字列オブジェクトを表すバイトのサイズと意味に影響を与える可能性があります。実際には、2 つの異なるstruct 宣言が別々のファイルにあり、それぞれが同じ名前であり、それらが交換可能であることを望んでいるようなものです。明らかに、これは機能しません。フラグの例は_HAS_ITERATOR_DEBUGGING
. これらのオプションは、デバッグ モードとリリース モードの間でも変更できることに注意してください。これらのタイプのエラーは、常にすぐに/一貫して現れるとは限らず、追跡するのが非常に難しい場合があります。
new
また、あるプロジェクトでは別のプロジェクトとは異なる方法で定義されnew
ている可能性があるため (たとえば、オーバーロードされている可能性があるため)、モジュール間の動的メモリ管理には十分注意する必要があります。削除するとき、仮想デストラクタを持つインターフェイスへのポインタがある場合があります。つまり、オブジェクトvtable
を適切に処理するために必要delete
であり、さまざまなコンパイラがすべて異なる方法で実装しvtable
ます。一般に、オブジェクトを割り当てるモジュールを割り当て解除するモジュールが必要です。より具体的には、オブジェクトの割り当てを解除するコードを、オブジェクトを割り当てたコードとまったく同じ条件下でコンパイルする必要があります。これが一つの理由ですstd::shared_ptr
構築時に「deleter」引数を取ることができます-同じコンパイラとフラグ(shared_ptr
モジュール間で s を共有する唯一の保証された安全な方法)を使用しても、どこでも同じnew
ではない可能性があるため、破壊される可能性があります。デリーターを使用すると、共有ポインターを作成するコードが、共有ポインターが最終的にどのように破棄されるかを制御します。(私はこの段落を適切な方法で挿入しました。モジュールの境界を越えてオブジェクトを共有しているようには見えません。)delete
shared_ptr
これはすべて、C++ に標準バイナリ インターフェイス ( ABI ) がないためです。それは自由に参加できる場所であり、自分の足を撃つことは非常に簡単です (時には気付かずに)。
それで、希望はありますか?もちろんです!代わりに C API をプラグインに公開し、プラグインにも C API を公開させることができます。C API は事実上あらゆる言語と相互運用できるため、これは非常に優れています。例外がプラグイン関数の上にバブルアップしないことを確認することを除いて (これは作成者の懸念です)、例外について心配する必要はありません。また、コンパイラ/オプションに関係なく安定しています (STL コンテナーを渡さないと仮定します)。など)。cdecl
宣言された関数のデフォルトである標準呼び出し規約 ( ) は 1 つだけですextern "C"
。void*
、実際には、同じプラットフォーム上のすべてのコンパイラで同じになります (たとえば、x64 では 8 バイト)。
あなた (およびプラグイン作成者) は、2 つの間のすべての外部通信が C API を使用する (つまり、相互運用のために C モジュールのふりをする) 限り、C++ でコードを書くことができます。
C 関数ポインタも実際にはコンパイラ間で互換性がある可能性がありますが、これに依存したくない場合は、プラグインにアドレスの代わりに関数名( ) を登録させることができます。 Windows の場合 (同様に、Linux と Mac OS X には と があります)。これは、で宣言された関数の名前マングリングが無効になっているため機能します。const char*
LoadLibrary
GetProcAddress
dlopen
dlsym
extern "C"
登録された関数を単一のプロトタイプ型に制限する直接的な方法はないことに注意してください (そうしないと、既に述べたように、適切に呼び出すことができません)。プラグイン関数に特定のパラメーターを与える (または値を取得する) 必要がある場合は、異なるプロトタイプで異なる関数を個別に登録して呼び出す必要があります (ただし、すべての関数ポインターを共通の関数ポインターに折りたたむことができます)。内部で入力し、最後にのみキャストバックします)。
最後に、メソッド ポインターを直接サポートすることはできませんが (C API には存在しませんが、C++ API でも可変サイズであるため、簡単に格納できません)、プラグインが「user- data" 関数を登録するときの不透明なポインター。これは、関数が呼び出されるたびに関数に渡されます。これにより、プラグインの作成者は、メソッドの周りに関数ラッパーを記述し、メソッドを適用するオブジェクトをユーザー データ パラメーターに格納する簡単な方法が得られます。user-data パラメーターは、プラグインの作成者が必要とする他のものにも使用できます。これにより、プラグイン システムとのインターフェースと拡張がはるかに簡単になります。もう 1 つの使用例は、ラッパーとユーザー データに格納された追加の引数を使用して、異なる関数プロトタイプ間で適応させることです。
これらの提案は、次のようなコードにつながります (Windows の場合 -- コードは他のプラットフォームでも非常に似ています)。
// Shared header
extern "C" {
typedef void (*plugin_function)(void*);
bool registerFunction(int plugin_id, const char* function_name, void* user_data);
}
// Your plugin registration code
hModule = LoadLibrary(pluginDLLPath);
// Your plugin function registration code
auto pluginFunc = (plugin_function)GetProcAddress(hModule, function_name);
// Store pluginFunc and user_data in a map keyed to function_name
// Calling a plugin function
pluginFunc(user_data);
// Declaring a plugin function
extern "C" void aPluginFunction(void*);
class Foo { void doSomething() { } };
// Defining a plugin function
void aPluginFunction(void* user_data)
{
static_cast<Foo*>(user_data)->doSomething();
}
この返信が長くなって申し訳ありません。そのほとんどは、「C++ 標準は相互運用性に拡張されていません。少なくとも事実上の標準があるため、代わりに C を使用してください」と要約できます。
注: プラグインがまったく同じ状況でコンパイルされるという前提の下で、通常の C++ API (関数ポインターまたはインターフェイスなどを使用して) を設計するのが最も簡単な場合があります。これは、すべてのプラグインを自分で開発する必要がある場合 (つまり、DLL がプロジェクトのコアの一部である場合) には妥当です。これは、プロジェクトがオープンソースの場合にも機能します。この場合、誰もがプロジェクトとプラグインをコンパイルするまとまりのある環境を個別に選択できますが、ソース コード以外でプラグインを配布することが難しくなります。
更新: コメントで ern0 が指摘したように、モジュールの相互運用の詳細を (C API を介して) 抽象化して、メイン プロジェクトとプラグインの両方がより単純な C++ API を処理するようにすることができます。以下は、そのような実装の概要です。
// iplugin.h -- shared between the project and all the plugins
class IPlugin {
public:
virtual void register() { }
virtual void initialize() = 0;
// Your application-specific functionality here:
virtual void onCheeseburgerEatenEvent() { }
};
// C API:
extern "C" {
// Returns the number of plugins in this module
int getPluginCount();
// Called to register the nth plugin of this module.
// A user-data pointer is expected in return (may be null).
void* registerPlugin(int pluginIndex);
// Called to initialize the nth plugin of this module
void initializePlugin(int pluginIndex, void* userData);
void onCheeseBurgerEatenEvent(int pluginIndex, void* userData);
}
// pluginimplementation.h -- plugin authors inherit from this abstract base class
#include "iplugin.h"
class PluginImplementation {
public:
PluginImplementation();
};
// pluginimplementation.cpp -- implements C API of plugin too
#include <vector>
struct LocalPluginRegistry {
static std::vector<PluginImplementation*> plugins;
};
PluginImplementation::PluginImplementation() {
LocalPluginRegistry::plugins.push_back(this);
}
extern "C" {
int getPluginCount() {
return static_cast<int>(LocalPluginRegistry::plugins.size());
}
void* registerPlugin(int pluginIndex) {
auto plugin = LocalPluginRegistry::plugins[pluginIndex];
plugin->register();
return (void*)plugin;
}
void initializePlugin(int pluginIndex, void* userData) {
auto plugin = static_cast<PluginImplementation*>(userData);
plugin->initialize();
}
void onCheeseBurgerEatenEvent(int pluginIndex, void* userData) {
auto plugin = static_cast<PluginImplementation*>(userData);
plugin->onCheeseBurgerEatenEvent();
}
}
// To declare a plugin in the DLL, just make a static instance:
class SomePlugin : public PluginImplementation {
virtual void initialize() { }
};
SomePlugin plugin; // Will be created when the DLL is first loaded by a process
// plugin.h -- part of the main project source only
#include "iplugin.h"
#include <string>
#include <vector>
#include <windows.h>
class PluginRegistry;
class Plugin : public IPlugin {
public:
Plugin(PluginRegistry* registry, int index, int moduleIndex)
: registry(registry), index(index), moduleIndex(moduleIndex)
{
}
virtual void register();
virtual void initialize();
virtual void onCheeseBurgerEatenEvent();
private:
PluginRegistry* registry;
int index;
int moduleIndex;
void* userData;
};
class PluginRegistry {
public:
registerPluginsInModule(std::string const& modulePath);
~PluginRegistry();
public:
std::vector<Plugin*> plugins;
private:
extern "C" {
typedef int (*getPluginCountFunc)();
typedef void* (*registerPluginFunc)(int);
typedef void (*initializePluginFunc)(int, void*);
typedef void (*onCheeseBurgerEatenEventFunc)(int, void*);
}
struct Module {
getPluginCountFunc getPluginCount;
registerPluginFunc registerPlugin;
initializePluginFunc initializePlugin;
onCheeseBurgerEatenEventFunc onCheeseBurgerEatenEvent;
HMODULE handle;
};
friend class Plugin;
std::vector<Module> registeredModules;
}
// plugin.cpp
void Plugin::register() {
auto func = registry->registeredModules[moduleIndex].registerPlugin;
userData = func(index);
}
void Plugin::initialize() {
auto func = registry->registeredModules[moduleIndex].initializePlugin;
func(index, userData);
}
void Plugin::onCheeseBurgerEatenEvent() {
auto func = registry->registeredModules[moduleIndex].onCheeseBurgerEatenEvent;
func(index, userData);
}
PluginRegistry::registerPluginsInModule(std::string const& modulePath) {
// For Windows:
HMODULE handle = LoadLibrary(modulePath.c_str());
Module module;
module.handle = handle;
module.getPluginCount = (getPluginCountFunc)GetProcAddr(handle, "getPluginCount");
module.registerPlugin = (registerPluginFunc)GetProcAddr(handle, "registerPlugin");
module.initializePlugin = (initializePluginFunc)GetProcAddr(handle, "initializePlugin");
module.onCheeseBurgerEatenEvent = (onCheeseBurgerEatenEventFunc)GetProcAddr(handle, "onCheeseBurgerEatenEvent");
int moduleIndex = registeredModules.size();
registeredModules.push_back(module);
int pluginCount = module.getPluginCount();
for (int i = 0; i < pluginCount; ++i) {
auto plugin = new Plugin(this, i, moduleIndex);
plugins.push_back(plugin);
}
}
PluginRegistry::~PluginRegistry() {
for (auto it = plugins.begin(); it != plugins.end(); ++it) {
delete *it;
}
for (auto it = registeredModules.begin(); it != registeredModules.end(); ++it) {
FreeLibrary(it->handle);
}
}
// When discovering plugins (e.g. by loading all DLLs in a "plugins" folder):
PluginRegistry registry;
registry.registerPluginsInModule("plugins/cheeseburgerwatcher.dll");
for (auto it = registry.plugins.begin(); it != registry.plugins.end(); ++it) {
(*it)->register();
}
for (auto it = registry.plugins.begin(); it != registry.plugins.end(); ++it) {
(*it)->initialize();
}
// And then, when a cheeseburger is actually eaten:
for (auto it = registry.plugins.begin(); it != registry.plugins.end(); ++it) {
auto plugin = *it;
plugin->onCheeseBurgerEatenEvent();
}
これには、互換性のために C API を使用するという利点がありますが、C++ で記述されたプラグイン (および C++ であるメイン プロジェクト コード) に対してより高いレベルの抽象化も提供します。複数のプラグインを 1 つの DLL で定義できることに注意してください。マクロを使用して関数名の重複を一部除去することもできますが、この単純な例ではそうしませんでした。
ちなみに、これはすべて、相互依存関係のないプラグインを前提としています。プラグイン A がプラグイン B に影響を与える (または必要とする) 場合、必要に応じて依存関係を注入/構築するための安全な方法を考案する必要があります。プラグインがロードされる (または初期化される) 順序。その場合は、2 段階のプロセスがうまく機能します。すべてのプラグインをロードして登録します。各プラグインの登録時に、提供するサービスを登録させます。初期化中に、登録されたサービス テーブルを参照して、必要に応じて要求されたサービスを構築します。これにより、プラグインが登録または初期化される順序に関係なく、すべてのプラグインによって提供されるすべてのサービスが、使用が試行される前に登録されます。