エディタの JavaScript 環境からプラグインを呼び出して、プラグインと一緒に指定した .dll ファイル内の C コードを実行することができます。C コード内でいくつかの関数を定義し、エディタに用意されている一連の API を使用してエディタの JavaScript 環境にこれらの関数を公開します。プラグインの JavaScript コードまたは action 拡張機能からこれらの関数を呼び出すと、C 関数が自動的に呼び出されます。
編集環境からネイティブ コードを実行できるようにする必要があるときはいつでもこの機能が役立ちます。たとえば、別のアプリケーションまたはライブラリを呼び出して、一部の処理を実行しなければならない場合や、エディタの JavaScript サービスを介して公開されていないオペレーティング システム関数を利用したい場合があります。また、特に CPU を大量に使用する処理を実行する必要がある場合は、JavaScript でなく C でこの関数を実装する必要がある場合があります。
他のエディタ拡張機能と異なり、.stingray_plugin ファイルに何も追加しなくても .dll 拡張機能を設定することができます。ただし、必ずこのページの指示に従って、C コードを記述し、.dll ファイルにコンパイルして、プラグインの JavaScript のエディタにライブラリをロードおよびロード解除するタイミングを指示してから、JavaScript から公開されている C 関数を呼び出す必要があります。
サンプル プラグイン リポジトリには、エディタを拡張する .dll の使用例が含まれています。これらのサンプルをまだ持っていない場合は、「サンプル プラグイン」を参照してください。
確認するサンプルは、samples/editor_native_code の下にあります。
プラグインは、stingray-plugin リポジトリをベースにすることを強くお勧めします。このリポジトリには、Visual Studio 2015 を使用して .dll にコードをコンパイルするために必要になるすべてのものが既に設定されているため、より簡単に開始することができます。
SDK ヘッダ ファイルが必要になります。これらのファイルはサンプル リポジトリの stingray_sdk 内、または Stingray インストール フォルダ内の plugin_sdk/editor_plugin_api 内にあります。 (stingray-plugin リポジトリを使用する場合は、これらのヘッダが自動的に取得されるため、意識する必要はありません。)
リファレンス ドキュメントには、SDK ヘッダ ファイルで定義されている API の参照可能なガイドが含まれています。
エディタの C プラグイン インタフェースは、エディタとプラグイン間の相互作用に一貫性が維持されるよう構築されています。これらのすべての相互作用は、共通の API 定義セットに基づいています。双方がクエリーを送信して、相手がサポートする API を取得します。
まず、エディタがあらかじめ設定された get_editor_plugin_api 関数を呼び出して、プラグインにその API をクエリーします。プラグインは、それらの共有プラグインの API の定義から実装する、すべての関数へのポインタが含まれる構造体をエディタに提供します。
エディタはプラグインで定義されているこれらの関数を呼び出すときに、独自の get_editor_api 関数ポインタを渡します。プラグインはこの関数を呼び出してエディタをクエリーし、エディタ サービスがプラグインに公開している API を取得することができます。プラグインは返されたこれらの API の関数を呼び出して、エディタが必要なタスクを実行するようにします(通常は、JavaScript 環境に新しい関数を登録します)。
次のイメージは、このワークフローの概要を示しています。
この全体的なパターンは、Stingray エンジンがランタイム機能を拡張するプラグインと相互作用する方法と非常に似ています。「エンジンを拡張する」を参照してください。
プラグインに登録した関数は、JavaScript 環境内で同期または非同期に実行することができます。
エディタは Chromium Embedded Framework (CEF)を使用して HTML を表示し、そのウィジェット内で JavaScript を実行します。CEF は次に示す複数のプロセスを実行します。
ウィンドウの作成、ペイント、およびネットワーク アクセスを処理するメイン プロセスは、「ブラウザ」プロセスといいます。
レンダリングおよび JavaScript の実行は、「レンダリング」プロセスという独立したプロセスで行われます。
同期間数はレンダリング プロセスで実行され、呼び出しをブロックします。非同期関数は、ブラウザ プロセス(またはブラウザ プロセスに属している UI スレッド)内で実行され、プロセスをブロックしません。これらの関数は、以降の操作を同期化するために使用する JavaScript の Promise を返します。非同期関数は、新しいウィジェットの作成のように、ユーザ インタフェースと相互作用する場合に特に役立ちます。promises および非同期 JavaScript の使い方の詳細については、「プラグインの開発に関するヒント」を参照してください。
いずれかのモードで関数を登録するには、プラグインがこのモードに固有のエディタ API を使用して .dll をロードする必要があります。両方のモードを同時に使用して拡張機能をロードすることができますが、ロードするたびに別のプロセスによって .dll がロードされます。現在、同期モードと非同期モードを使用する 2 つのインスタンス間でプラグインが通信することはできません。つまり、同期モードまたは非同期モードのいずれかを選択し、登録するすべての関数でこのモードを使用するようにした方がよい可能性があります。
次は、エディタを拡張する .dll を記述して、プラグインの JavaScript コードからその .dll を呼び出すための基本的な手順です。
C コードに plugin_api.h ファイルをインクルードします。
#include "editor_plugin_api/editor_plugin_api.h"
上図のように、エディタとプラグインが互いに呼び出しあう場合は、共通の ID セットを使用してこれらが要求している API を特定します。各 ID は常に editor_plugin_api.h ファイルで定義された特定の構造体に対応しています。したがって、ID と API の定義を一致させるには、エディタとプラグインがこのファイルをインクルードする必要があります。
次の署名を使用して関数を定義します。
__declspec(dllexport) void *get_editor_plugin_api(unsigned api)
プラグイン インタフェースは C++ でなく C を使用して、各バージョンの C++ と各コンパイラ間で ABI の非互換性が生じる問題を回避します。C++ を使用してプラグインをコンパイルする場合は、次のように get_editor_plugin_api 関数を extern C ブロック内でラップできます。
extern "C" { __declspec(dllexport) void *get_editor_plugin_api(unsigned api) { ... } }
こうすると、コンパイルされた .dll 内で関数名がめちゃくちゃにならなくなります。
エディタはプラグインの get_editor_plugin_api 関数を呼び出すたびに、プラグインが提供できるインタフェースを識別する EditorPluginApiId を渡します。プラグインが要求されたインタフェースをサポートするように設定する場合は、get_editor_plugin_api の実装が応答して、この要求された API と一致する構造体の新しいインスタンスを作成する必要があります。プラグインがサポートするこのインタフェース内の関数ごとに、インスタンス内のこの関数のポインタを、プラグインで記述された関数に設定する必要があります。サポートするすべての関数を設定したら、構造体を返します。
The editor requests two different plug-in APIs from your plug-in: one for your plug-in to use when your want to register synchronous functions, and one for asynchronous functions. See the following sections for details. This example shows how to fulfill a request for the synchronous plug-in API (`EditorPluginSyncApi`): ~~~{cpp} __declspec(dllexport) void *get_editor_plugin_api(unsigned api) { if (api == EDITOR_PLUGIN_SYNC_API_ID) { static struct EditorPluginSyncApi editor_api = {nullptr}; editor_api.stingray_plugin_loaded = &PLUGIN_NAMESPACE::EditorTestPlugin::plugin_loaded; editor_api.get_name = &PLUGIN_NAMESPACE::EditorTestPlugin::get_name; editor_api.get_version= &PLUGIN_NAMESPACE::EditorTestPlugin::get_version; editor_api.shutdown = &PLUGIN_NAMESPACE::EditorTestPlugin::shutdown; return &editor_api; } return nullptr; } ~~~ You'll need to implement those functions in your plug-in -- like `plugin_loaded()`, `get_name()`, `get_version()`, and `shutdown()` in the example above. The editor uses the struct you return to call the functions you've defined. For now, your implementations can be empty, but this will be your chance to define what your plug-in will do in response to editor events.
これまで、特定の時点、.dll がロードおよびロード解除されるタイミングで、プラグインを呼び出すようにエディタを設定してきました。
エディタは、プラグインの plugin_loaded() および shutdown() 関数の実装を呼び出し、エディタから必要な任意のサービス API を要求するために関数コード内で使用できる get_editor_api 関数を渡します。get_editor_api の各呼び出し内で EditorApiId 列挙内の値を渡して、必要なインタフェースをエディタに指定します。これらのエディタ API の 1 つができたら、プラグインからその関数を呼び出して、JavaScript から呼び出せるように C 関数を登録および登録解除できます。一般的に、プラグインはこれらの関数を plugin_loaded() で登録し、shutdown() で登録解除します。
取得する必要のある正確なエディタサービス API、および関数を登録および登録解除するために使用すべきプラグインは、同期関数を登録するか非同期関数を登録するかによって異なります。詳細については、以下のセクションを参照してください。
プラグインを .dll ファイルにコンパイルします。
C で開発することに慣れていない場合は、この作業が面倒になることがあります。独自の Visual Studio プロジェクトですべてが設定されているサンプル プラグインから作業を開始することをお勧めします。x64 プラットフォームをターゲットとする必要がある点に注意してください。
JavaScript プラグインでは、コンパイルした .dll 関数をロードおよびロード解除するタイミングをエディタに伝える必要があります。そのためには、stingray JavaScript のネームスペースの関数を呼び出します。.dll がロードされている間は、プラグインの JavaScript コードは、.dll によって登録されたすべての関数を呼び出すことができます。その方法は、関数が同期的に呼び出されたか非同期的に呼び出されたかによって、わずかに異なります。詳細については以降のセクションを参照してください。
JavaScript から同期的に呼び出される関数を登録する場合は、プラグインの get_editor_plugin_api() 関数の実装で EDITOR_PLUGIN_SYNC_API_ID を確認する必要があります。エディタがこの ID を get_editor_plugin_api() に渡すと、プラグインで、新しい EditorPluginSyncApi 構造体を作成し、そのメンバーがプラグイン内の関数を指すように設定して、その構造体をエディタに返す必要があります。
そうすれば、エディタで同期関数を登録するときに、エディタからリクエストできる 3 つの異なるエディタ サービス API、EditorApi、EditorApi_V2、EditorApi_V3 を選択できるようになります。唯一の違いは、登録することができるハンドラの種類です。
EditorApi および EditorApi_V2 は C 関数へのポインタを登録します。V2 署名を使用している場合は、関数が別のエディタ API (EditorEvalApi や EditorLoggingApi など)を取得できるように、エディタは関数に get_editor_api 関数のポインタも渡します。
EditorApi_V3 は、lambda 式を登録できるように、std::function タイプも扱います。
すべてのケースで、API は同じように動作します。
register_native_function() を呼び出して関数を登録し、JavaScript に対して関数をどのように公開するかを決定するネームスペースと関数名を渡します。既に別のプラグインによって登録されている関数を登録しようとする可能性を減らすために、ネームスペースとしてプラグイン名を使用することをお勧めします。
通常は、.dll が JavaScript からロードされたときに呼び出される EditorPluginSyncApi::plugin_loaded() の実装でこれを実行します。
関数ハンドラが必要なくなった場合は、unregister_native_function() を呼び出して登録解除する必要があります。登録解除する関数の特定には、同じネームスペースと名前を使用します。
通常は、.dll が JavaScript からロード解除されたときに呼び出される EditorPluginSyncApi::shutdown() の実装でこれを実行します。
JavaScript の場合:
.dll をエディタにロードするには、stingray.loadNativeExtension() を呼び出します。この関数にディスク上の .dll ファイルのパスを渡します。関数は、プラグインをロード解除するために後で使用する識別子を返します。
ヒント: services/plugin-service モジュールの getPlugin() 関数を使用して、プラグインに関する情報を取得できます。返されるオブジェクトの $dir メンバーから、プラグインの .stingray_plugin ファイルのフォルダが分かるので、.stingray_plugin ファイルから .dll への相対パスが分かっていると仮定すると、パスを作成できます。
JavaScript から登録されている関数を呼び出すには、C からの登録に使用したネームスペースおよび関数名を使用して、window.namespace.name() として呼び出します。
その関数が不要になったときに .dll をロード解除するには、stingray.unloadNativeExtension() を呼び出します。.dll をロードしたときにエディタから返された識別子を渡します。
JavaScript から同期的に呼び出される関数を登録する場合は、プラグインの get_editor_plugin_api() 関数の実装で EDITOR_PLUGIN_ASYNC_API_ID を確認する必要があります。エディタがこの ID を get_editor_plugin_api() に渡すと、プラグインで、新しい EditorPluginAsyncApi 構造体を作成し、そのメンバーがプラグイン内の関数を指すように設定して、その構造体をエディタに返す必要があります。
その後で、エディタに非同期関数を登録するために、EditorAsyncApi を使用します。
どちらかの register_async_function() を呼び出して、関数を登録します。これにより、非同期関数は CEF プロセスで実行されるようになります。また、Qt アプリケーションの GUI スレッドで実行する場合は、代わりに register_async_gui_function() を使用します。この方法は、C 関数がなんらかの UI コンポーネントを提供し、それらの応答性を保持しようとする場合に便利な場合があります。
いずれの関数を呼び出す場合にも、JavaScript コード内で関数を識別するために後で使用する、一意の関数名を渡す必要があります。既に別のプラグインによって登録されている関数を登録しようとする可能性を減らすために、名前のどこかにプラグイン名を含めることをお勧めします。この識別子は、単なる文字列であるため、同期関数の名前に使用できない文字を含めることができます。例: .. たとえば、plugin-name.function-name として、関数を登録できます。
通常は、.dll が JavaScript からロードされたときに呼び出される EditorPluginSyncApi::plugin_loaded() の実装でこれを実行します。
関数ハンドラが必要なくなった場合は、unregister_async_function() または unregister_async_gui_function() を呼び出して登録解除する必要があります。登録解除する関数の特定には、同じ一意の名前を使用します。
通常は、.dll が JavaScript からロード解除されたときに呼び出される EditorPluginSyncApi::shutdown() の実装でこれを実行します。
JavaScript の場合:
.dll をエディタにロードするには、stingray.loadAsyncExtension() を呼び出します。この関数にディスク上の .dll ファイルのパスを渡します。関数は、プラグインをロード解除するために後で使用する識別子を解決する Promise を返します。
ヒント: services/plugin-service モジュールの getPlugin() 関数を使用して、プラグインに関する情報を取得できます。返されるオブジェクトの $dir メンバーから、プラグインの .stingray_plugin ファイルのフォルダが分かるので、.stingray_plugin ファイルから .dll への相対パスが分かっていると仮定すると、パスを作成できます。
JavaScript から登録されている非同期関数を呼び出すには、stingray.hostExecute() を呼び出して、C から関数を登録するために使用した識別子を渡します。戻り値は、返される、C 関数が返した任意のオブジェクト(ある場合)を解決する Promise です。
その関数が不要になったときに .dll をロード解除するには、stingray.unloadAsyncExtension() を呼び出します。.dll をロードしたときにエディタから約束された識別子を渡します。
たとえば、次のようになります。
stingray.loadAsyncExtension(path).then(function (id) { console.warn('Loaded async plugin ' + id); pluginAsyncId = id; }); // later... stingray.unloadAsyncExtension(pluginAsyncId).then(function () { console.warn('Plugin ' + pluginAsyncId + ' unloaded!'); pluginAsyncId = ''; });
JavaScript と .dll との間でデータをやりとりするには、ConfigValue を使用します。これは、C で定義された JSON に似た再帰的なデータ構造体で、本質的に JSON と同じ種類のデータ タイプを保存します。
JavaScript から登録されている関数を呼び出し、関数呼び出しでなんらかのパラメータを渡すと、エディタは ConfigValue オブジェクトとして登録されている C 関数にこれらのパラメータを渡します。同様に、C 関数は新しい ConfigValue を作成し、この構造体に戻り値を格納して返すことによって、JavaScript にデータを戻すことができます。すると、JavaScript コードは、エディタによって返されたオブジェクトから値を読み取ることができます。
注: 同期モードでは、JavaScript の関数に渡す各パラメータは、個別の ConfigValue として C 関数に渡されます。3 つのパラメータを関数に渡す場合は、3 つの ConfigValue オブジェクトを受け取ります。ただし、非同期モードでは、C 関数は常に、stingray.hostExecute に渡したのと同じオブジェクトが含まれている 1 つの ConfigValue を受け取ります。
入力データを読み取り、ConfigValue に含まれる戻り値をエンコードするには、C 関数で ConfigDataApi を使用します。この API は、API ID CONFIGDATA_API_ID を指定して get_editor_plugin 関数を呼び出すことで、エディタから取得できます。
ConfigValue 構造体は、次のさまざまな型をサポートしています。
ConfigDataApi を使用して ConfigValue オブジェクトから値を読み取る方法と、新しい ConfigValue オブジェクトを作成して戻り値を JavaScript レイヤに戻す方法の例については、サンプル editor_native_code/src/editor_native_plugin.cpp の EditorTestPlugin::test() 関数と copy_config_data_value() 関数を参照してください。
エディタは、プラグインにその他のサービス API も提供します。
EditorLoggingApi: メッセージ、警告、およびエラーをエディタの Log Console に送信するために使用できます。
EditorEvalApi: .dll 内からエディタの JavaScript 環境の JavaScript コードを評価するために使用できます。
これらの API の詳細については、リファレンス ドキュメントのデータ構造を参照してください。