JavaScript から C コードを呼び出す

エディタの JavaScript 環境からプラグインを呼び出して、プラグインと一緒に指定した .dll ファイル内の C コードを実行することができます。C コード内でいくつかの関数を定義し、エディタに用意されている一連の API を使用してエディタの JavaScript 環境にこれらの関数を公開します。プラグインの JavaScript コードまたは action 拡張機能からこれらの関数を呼び出すと、C 関数が自動的に呼び出されます。

編集環境からネイティブ コードを実行できるようにする必要があるときはいつでもこの機能が役立ちます。たとえば、別のアプリケーションまたはライブラリを呼び出して、一部の処理を実行しなければならない場合や、エディタの JavaScript サービスを介して公開されていないオペレーティング システム関数を利用したい場合があります。また、特に CPU を大量に使用する処理を実行する必要がある場合は、JavaScript でなく C でこの関数を実装する必要がある場合があります。

他のエディタ拡張機能と異なり、.stingray_plugin ファイルに何も追加しなくても .dll 拡張機能を設定することができます。ただし、必ずこのページの指示に従って、C コードを記述し、.dll ファイルにコンパイルして、プラグインの JavaScript のエディタにライブラリをロードおよびロード解除するタイミングを指示してから、JavaScript から公開されている C 関数を呼び出す必要があります。

その他の関連資料

エディタおよびプラグインの相互作用

エディタの C プラグイン インタフェースは、エディタとプラグイン間の相互作用に一貫性が維持されるよう構築されています。これらのすべての相互作用は、共通の API 定義セットに基づいています。双方がクエリーを送信して、相手がサポートする API を取得します。

次のイメージは、このワークフローの概要を示しています。

この全体的なパターンは、Stingray エンジンがランタイム機能を拡張するプラグインと相互作用する方法と非常に似ています。「エンジンを拡張する」を参照してください。

同期と非同期

プラグインに登録した関数は、JavaScript 環境内で同期または非同期に実行することができます。

エディタは Chromium Embedded Framework (CEF)を使用して HTML を表示し、そのウィジェット内で JavaScript を実行します。CEF は次に示す複数のプロセスを実行します。

同期間数はレンダリング プロセスで実行され、呼び出しをブロックします。非同期関数は、ブラウザ プロセス(またはブラウザ プロセスに属している UI スレッド)内で実行され、プロセスをブロックしません。これらの関数は、以降の操作を同期化するために使用する JavaScript の Promise を返します。非同期関数は、新しいウィジェットの作成のように、ユーザ インタフェースと相互作用する場合に特に役立ちます。promises および非同期 JavaScript の使い方の詳細については、「プラグインの開発に関するヒント」を参照してください。

いずれかのモードで関数を登録するには、プラグインがこのモードに固有のエディタ API を使用して .dll をロードする必要があります。両方のモードを同時に使用して拡張機能をロードすることができますが、ロードするたびに別のプロセスによって .dll がロードされます。現在、同期モードと非同期モードを使用する 2 つのインスタンス間でプラグインが通信することはできません。つまり、同期モードまたは非同期モードのいずれかを選択し、登録するすべての関数でこのモードを使用するようにした方がよい可能性があります。

スタートアップ

次は、エディタを拡張する .dll を記述して、プラグインの JavaScript コードからその .dll を呼び出すための基本的な手順です。

  1. C コードに plugin_api.h ファイルをインクルードします。

    #include "editor_plugin_api/editor_plugin_api.h"
    

    上図のように、エディタとプラグインが互いに呼び出しあう場合は、共通の ID セットを使用してこれらが要求している API を特定します。各 ID は常に editor_plugin_api.h ファイルで定義された特定の構造体に対応しています。したがって、ID と API の定義を一致させるには、エディタとプラグインがこのファイルをインクルードする必要があります。

  2. 次の署名を使用して関数を定義します。

    __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 内で関数名がめちゃくちゃにならなくなります。

  3. エディタはプラグインの 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.
  4. これまで、特定の時点、.dll がロードおよびロード解除されるタイミングで、プラグインを呼び出すようにエディタを設定してきました。

    エディタは、プラグインの plugin_loaded() および shutdown() 関数の実装を呼び出し、エディタから必要な任意のサービス API を要求するために関数コード内で使用できる get_editor_api 関数を渡します。get_editor_api の各呼び出し内で EditorApiId 列挙内の値を渡して、必要なインタフェースをエディタに指定します。これらのエディタ API の 1 つができたら、プラグインからその関数を呼び出して、JavaScript から呼び出せるように C 関数を登録および登録解除できます。一般的に、プラグインはこれらの関数を plugin_loaded() で登録し、shutdown() で登録解除します。

    取得する必要のある正確なエディタサービス API、および関数を登録および登録解除するために使用すべきプラグインは、同期関数を登録するか非同期関数を登録するかによって異なります。詳細については、以下のセクションを参照してください。

  5. プラグインを .dll ファイルにコンパイルします。

    C で開発することに慣れていない場合は、この作業が面倒になることがあります。独自の Visual Studio プロジェクトですべてが設定されているサンプル プラグインから作業を開始することをお勧めします。x64 プラットフォームをターゲットとする必要がある点に注意してください。

  6. 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、EditorApiEditorApi_V2EditorApi_V3 を選択できるようになります。唯一の違いは、登録することができるハンドラの種類です。

すべてのケースで、API は同じように動作します。

JavaScript の場合:

非同期関数の登録と呼び出し

JavaScript から同期的に呼び出される関数を登録する場合は、プラグインの get_editor_plugin_api() 関数の実装で EDITOR_PLUGIN_ASYNC_API_ID を確認する必要があります。エディタがこの ID を get_editor_plugin_api() に渡すと、プラグインで、新しい EditorPluginAsyncApi 構造体を作成し、そのメンバーがプラグイン内の関数を指すように設定して、その構造体をエディタに返す必要があります。

その後で、エディタに非同期関数を登録するために、EditorAsyncApi を使用します。

JavaScript の場合:

たとえば、次のようになります。

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 と C の間のデータ交換

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.cppEditorTestPlugin::test() 関数と copy_config_data_value() 関数を参照してください。

その他の C API

エディタは、プラグインにその他のサービス API も提供します。

これらの API の詳細については、リファレンス ドキュメントのデータ構造を参照してください。