デスクトップアプリケーションの拡張を許可するために、サードパーティにAPIを提供する方法を理解できません。コンパイルされた言語(C++など)を使用している場合、ライブラリにリンクし、コアコードが呼び出す明確に定義されたAPIを提供すれば、実行時に動的ライブラリを拡張機能としてロードできることを理解しています。私が苦労しているのは、APIの一部がアーキテクチャの残りの部分にどのように適合するかです。通常、アプリケーションを複数のライブラリ(ライブラリ、モデル、UI、機能固有のライブラリなど)に分割します。しかし、拡張機能の開発者にこれらすべての権利にリンクさせたくありませんか?拡張API専用の別のライブラリを提供した場合、それはコアアプリケーションコンポーネントとどのように相互作用しますか?よく知られている拡張可能なアプリケーション(IDE、写真/オーディオ/ビデオ編集アプリ)はこれをどのように実装しますか?
私はビデオ編集アプリケーションのAPIに取り組んでいます。さまざまなタイプの拡張機能用に個別のSDK(およびAPI)があります。画像/動画処理用のAPIセットが1つあります。さまざまな形式のビデオをアプリケーションにインポートするための別のAPIセットがあります。ドキュメントモデルを探索するための別のAPIセットがあります。各APIには、開発者が拡張を作成するために構築できるライブラリとヘッダーを含むソフトウェア開発キット(SDK)があります。 (そして、それらには、それらがどのように機能するかを示すためのドキュメントとサンプルコードが含まれています。)
構造的には、拡張APIは、達成する必要のある作業の種類によって異なります。画像処理には、いくつかの主要な部分があります。
内部的には、アプリケーションがビデオのフレームを処理する必要がある場合、ディスクからフレームを読み取る拡張機能を呼び出します。フレームを取得すると、画像処理拡張機能を使用して設定を行います。次に、拡張機能を呼び出してレンダリングし、拡張機能がアプリケーションにコールバックして、UIに配置したさまざまなスライダーの値などの情報を取得します。その処理を行い、結果を出力フレームに入れます。次に、アプリは、必要なレンダリングごとのティアダウンを実行するように拡張機能に指示します。
Photoshop APIなどの一般的なAPIを見ると、ホストアプリケーションから情報を取得したり、情報を提供したりするために拡張機能が呼び出すことができる、ある種のデータ構造またはオブジェクトが提供されます。拡張自体は、アプリケーションが呼び出す関数またはメソッドの特定のセットを実装する必要があります。通常、ホストアプリケーションと拡張機能の間には明確に定義されたデータフローがありますが、その詳細はプラグインのタイプ、さらにはAPIを実装する特定のアプリケーションによって異なります。
プラグインAPIは、(完全に)抽象基本クラスによく似ています。抽象基本クラスとして表現することもできます。特定の名前と署名のプラグインエクスポート関数が必要です。次に、アプリケーションはこれらの関数を呼び出します
extern void Frobnicate(Frobber frob);
extern Bazzer MakeBazzer();
または、基本クラスを定義し、そのクラスのインスタンスを返す関数をプラグインにエクスポートする必要があります。
class FooPlugin
{
public:
virtual ~FooPlugin() = default();
void Frobnicate(Frobber frob) = 0;
Bazzer MakeBazzer() = 0;
}
extern FooPlugin & PluginInitialise();
パブリックインターフェースで使用されるタイプの定義が必要なだけで、プログラムの他の部分の定義から分離することができます。
メインアプリケーションの機能をプラグインで利用できるようにする1つの方法は、MS Word、MS Excel、Autocadなどのアプリケーションでプラグインメカニズムを使用する場合に検討できます。
実際、プログラムのほとんどの機能に完全なプログラムAPIを提供しています。 (上記の例では、VBAプログラムで使用できるAPIと同じです)。
あなたが言及した「拡張API専用のライブラリ」は、レイトダイナミックバインディングをサポートするテクノロジーを使用しています(Word、Excel、Autocadの場合はMicrosoftのCOMテクノロジーです)。
これにより、メインアプリケーション全体にリンクする必要なく、スタブライブラリに対してプラグインをリンクできます。次に、スタブlibは動的リンケージを使用してメインプログラムと通信します。
もちろん、C++では、プラグインに公開するメインアプリケーションの部分への抽象インターフェースを提供する一連の抽象基本クラスで十分です(プラグイン開発者が同じコンパイラーとランタイムlibバージョンをそのまま使用する場合)メインアプリケーションに使用されます)。
統合の処理に使用する特定のアプローチは、言語システムとパフォーマンスのニーズによって異なります。万能の答えはありません。
C++を使用していて、速度が重要なアプリケーション(たとえば、プラグインとの間で大量のデータを渡す)を使用している場合は、.soファイル(WindowsではDLL)を使用し、プラグインAPIのシンプルな抽象インターフェースを使用できます。 。注意が必要な「マーシャリング」と「リンケージ」の問題があるため、これは技術的に困難です。
非常にうまく機能する別のアプローチは、json-messagingです。プラグインをWebサービスとして動作させ、メッセージを送信(および回答を取得)することができます。または、そのパターンの少し単純なバリアント-パイプです:プラグインにjson-payload引数(標準入力)を取り、標準出力を介してjson形式の結果を返します)。これは非常に効率的ではありません(ただし、注意するとかなり効率的です)。しかし、それは高度に分離されているという利点があります-プラグインは任意の言語/システムで実装され、呼び出しコードは他の言語/プラットフォーム/システムを使用します。 {明らかに、上記でjsonを使用することについて特別なことは何もありません。これを行う一般的な方法です。10年前に私に尋ねた場合、私は同じことを言っていましたが、xmlを使用していました}。
私はすべての答えが好きで、この問題に取り組む方法がいくつあるかを示しています。しかし、具体的にはあなたの質問の一部について:
しかし、拡張機能の開発者にこれらすべての権利にリンクさせたくありませんか?拡張API専用の別のライブラリを提供した場合、それはコアアプリケーションコンポーネントとどのように相互作用しますか?
必要に応じて、プラグイン開発者が一部のライブラリにリンクしてプラグインを機能させるように要求し、それが許容できる場合に提供できます。私はいくつかの大きな商用アプリケーションに取り組んできましたが、すべてのケースでこれを回避しました。プラグインは、ソフトウェアのライブラリに(静的または動的に)リンクしません。当社のソフトウェアは、プラグインに厳密に動的にリンクします。
これを行う方法は、動的ディスパッチです。宇宙空間のどこかにある関数を指す関数ポインターがある場合、プラグインは、その関数を呼び出すために、その実装を提供するライブラリーにリンクする必要はありません。そして、それは基本的に私のドメインの人々が常にCとC++の両方でそれを行ってきた方法です。関数ポインタのシグネチャ/戻り値の型を次のように定義できます。
// Returns a pointer to an interface in the system given an ID.
// Real version might define calling conventions and look a little
// more ugly.
typedef void* FetchInterface(int interface_id);
そして、サードパーティが書き込むプラグインのプラグインエントリポイントは、次のような関数をエクスポートします。
// Imported and called by our Host application.
DYLIB_EXPORT void plugin_entry_point(FetchInterface* fetch);
そしてそれはそのように実装されるかもしれません:
void plugin_entry_point(FetchInterface* fetch)
{
SomeInterface* some_interface = fetch(SomeInterface::id);
some_interface->do_something(...);
}
そして、私たちのホストアプリケーションはそれを呼び出し、プラグインは、提供する関数ポインターを介して、処理したい必要なインターフェースをフェッチし、それらのインターフェースの関数を呼び出します。上記のSomeInterface
は、独自の関数ポインタを含むテーブル(struct
)です。
もちろん、プラグインが動作するためには、SDKのヘッダーを含める必要があります。これは、struct SomeInterface
などの定義がどのように定義されているかを確認するためです。ただし、何にもリンクする必要はありません。
gh、VoidポインタとC++でのCのようなコーディング!
純粋な仮想関数を持つクラスを介した抽象インターフェース(リンク要件をまだ回避する)の代わりに、中央APIにこのタイプ安全でないCのようなコードがある理由に疑問を感じるかもしれません。残念ながら、後者は異なるコンパイラ間でそれほど互換性がなく、他の言語のFFIで十分に理解されていません。
ここではAPIにCを使用することで、FFIはCを使用する傾向があるため、サードパーティがソフトウェアへのプラグインを、C#やLuaなどの言語で外部関数インターフェイスを使用して作成しました。関数のオーバーロード、仮想関数、コンストラクタ、デストラクタなどのC++の概念を必ずしも理解しているわけではありません)。これは、多くの言語でOpenGLを使用している人を見つける方法と似ていますが、C言語のAPIは必ずしもそのような言語での使用が想定されているわけではありません。
したがって、「最低限の共通点」C APIのこのような「ほぼ互換性」の側面を支持します。代わりに、他の言語での安全ではない低レベルのC APIでの作業を容易にするために、RAIIに準拠し、タイプセーフなC++ラッパーなどのラッパーを提供します。そして、C#のようなプラグインを作成するために他の人が使用することを予期していなかった言語に対してそれを行わない場合、サードパーティはC APIの上にラッパーlibを作成して、C#でプラグインを簡単に作成できるようにします。
バージョン管理と下位互換性
おそらくよりトリッキーな要件の1つは、私が扱った製品の一部に、古いプラグインとの強力な下位互換性要件があったことです(32ビットプラグインを作成するためのアダプターを含む、15年以上前に作成されたプラグインがまだソフトウェアで動作する必要がありました) 64ビットバージョンのソフトウェアでも動作します)。
そのため、struct
を使用したCスタイルのアプローチは、少し余裕があります。struct
の下部に追加するメンバーは、前のフィールドのメモリレイアウトに影響を与えないためです。そのため、これらのstructs
を指す以前のプラグインとの下位互換性に影響を与えることなく、既存の関数ポインターのテーブルの下部に新しい関数ポインターフィールドを追加できます。
時々、既存のインターフェースを変更するという誘惑がありますが、新しいプラグインの場合、下部に新しい機能を追加する以上の方法があります。そのためには、基本的に新しいバージョンのインターフェースを作成し、プラグインに適切なインターフェースを提供するためのフード。実際、私たちのプラグインは、上記のようにFetchInterface
でバージョンを渡します。
void plugin_entry_point(FetchInterface* fetch)
{
// Passing the PLUGIN_VERSION along here (defined in the SDK
// headers the plugin is using) can affect what interface is
// returned based on the plugin version.
SomeInterface* some_interface = fetch(PLUGIN_VERSION, SomeInterface::id);
some_interface->do_something(...);
}
このような長いレガシーとの下位互換性を維持することは、何があっても本当のPITAになる可能性があるため、レガシー互換性の要件が「非推奨」になるため、APIの設計方法を慎重に検討してAPIを非推奨にする必要性を減らす必要があります。最終的に古いコードを削除して保守を中止する機能を開発者に提供するのではなく、新しいインターフェースを使用することを提案します。