なかなか時間が取れていませんが、C#連携用のメタ・シリアライザ開発を進めてます。
概ねの方針を決定できましたので開示します。
1.既存のヘッダを修正します
現在は、NoTypeCheck、TypeCheck、TypeCheckByIndexの3種類をサポートしています。
NoTypeCheckは型チェックなし。TypeCheckとTypeCheckByIndexは、型チェックのためヘッダに型情報を入れています。TypeCheckは型名、TypeCheckByIndexは型に割り振ったインデックス番号をTHEOLIZER_PROCESSで保存/回復するデータに記録し、型の不一致が起きないことをチェックしています。
データ内に記録する制御用データが最小なのでTypeCheckByIndexが最も効率が高いです。
NoTypeCheckはヘッダ情報が少ないため、小さなデータのやり取りには向いていますが、名前対応型のクラスについてはメンバ変数名をデータ中に記録するため大きなデータでは効率が悪化します。
そこで、TypeCheckByIndexをベースにメタ・シリアライズ・データをヘッダ形式で出力するようにします。
enum型に属するシンボルのリストや、各型のTheolizerとしての拡張情報(enum型の定義情報などなど)を追加するイメージです。
なお、開発工数削減のため、最も必要性の薄いTypeCheckを削除します。
以上の結果、従来のデータとの互換性を失います。
2.プリミティブの構成を固定します。
現在、プリミティブの構成は派生シリアライザで決定しています。
これにより、JsonSerializerとXmlSerializerの文字列はUTF-8の1種類のみ、BinarySerializerとFastSerializerはエンコード変換しません。BinarySerializerは型チェックするのでstd::string, std::wstring, std::u16string, std::u32stringの4種類を異なるプリミティブとしています。
JsonSerializerとXmlSerializerでUTF-8以外のデータをstd::string, std::wstring, std::u16string, std::u32string型変数との間で保存/回復する時は、それぞれの派生シリアライザが自動的にエンコードを変換しています。
さて、C#連携では性能をできるだけ上げるため早期にBinarySerializerを使えるようにしたいと考えています。その際、BinarySerializerで用いる文字列のエンコードはC#の文字エンコードであるUTF-16LEが最適です。
そこで、BinarySerializerはstd::string, std::wstring, std::u16string, std::u32stringのデータを全てUTF-16LEへ自動的に変換して保存/回復します。これによりC#側ではエンコード変換が発生しません。
また、C++側の文字列をstd::wstring、もしくは、std::u16stringで記録した場合、C++側でもエンコード変換が発生しません。なお、経験的にはC++側はstd::stringでUTF-8を用いて文字列を管理する方が、他のASCIIで文字列を取り扱うライブラリとの親和性が高く、開発工数を節減できると思います。性能的に問題ないならC++側はstd::stringのUTF-8で文字列を管理することをご検討下さい。
そのためのサポートしてtheolizer::u8stringが便利と思います。
3.ファイル・フォーマットを見直します
保存/回復する変数の型にTypeIndexという番号を割り当てて管理しています。現在は配列とポインタにも元の型とは異なる専用のTypeIndexを割り当てています。
しかし、これはヘッダを肥大化させるのであまり好ましくありませんので下記対応を行います。
-
TypeIndexは配列やポインタについては割り当てません
基本の型についてのみTypeIndexを割り当てます。int型等の基本型、enum型、クラス、実体化したテンプレートに割り当てます。それらの型の配列やポインタは同じTypeIndexをベースとして表現します。
型チェックのためにはデータが配列やポインタであることを判定する必要がありますので、付加情報を追加します。
ただし、付加情報の肥大化を防ぐため、サポートする配列の最大の次元数は3とします。3次元を超える配列を使いたいケースは稀です。また、もしも4次元以上の配列をシリアライズしたい場合は、一旦クラスでラップすることで対応可能です。
-
配列へのポインタをサポートから外します
現在のTheolizerは配列へのポインタをサポートしています。(例えば、'typedef int IntArray[100]; IntArray* IntArrayPointer;'のIntArrayPointerは、int[100]型へのポインタです。このIntArrayPointerをオーナーポインタとして保存/回復をサポートしています。)
しかし、そもそもC/C++では配列全体へのポインタを使うことは稀です。(配列の要素へのポインタは普通に使いますが、配列全体へのポインタは言語仕様上、使いづらいためです。)
そして、配列へのポインタをサポートすると「配列へのポインタ」の配列、『「配列へのポインタ」の配列へのポインタ』、...を構成することが可能となるのでTypeIndexの付加情報で管理するためにはTypeIndexを可変長にする必要があります。
使用頻度の低い機能のサポートのためにTypeIndexが複雑化することを避けるため「配列へのポインタ」をサポートから外します。
4.メタ・シリアライズについて
-
C++側のメタ・シリアライザ
これはユーザ・プログラムの各種クラスやenumの定義情報等が必要ですので、ユーザ・プログラム内でC#連携を指定してシリアライザをコンストラクトすることで出力されるようにします。
具体的な起動手順は未定ですが、ユーザ・プログラムがオプションを指定して起動されたら、メタ・シリアライズ機能を呼び出して頂くイメージになる予定です。
僅かなコードをmain()関数やWinMain()関数へ組込むことで対応できるようにする予定です。
-
C#側のメタ・デシリアライザ
メタ・シリアライズ・データを解析し、C#のenumやクラス定義ソースを出力する、メタ・デシリアライザを追加します。こちらはユーザ・プログラムではなく独立したプログラムをTheolizer側で用意します。
5.バージョン対応について
Issue #42 の「4-1.C#側シリアライザの開発」に記載したように、C#側はそのソースが自動生成された時の最新版のみをサポートします。C++側は自分より古いもの全てをサポートします。
これにより、C++側の「サーバ」を最新版へアップしておくことで古いクライアントも機能できる状態にする計画です。これはTheolizerのバージョン変更対応機能を使用します。
C++側ユーザ・プログラムは、古いバージョンのユーザ・プログラムが出力したデータを回復する新しいバージョンのユーザ・プログラムを開発することは比較的容易です。逆に古いバージョンのデータを出力することもTheolizerのサポートにより可能ですが、バージョンアップ対応に比べると手間がかかります。
C#連携の場合、C#側とC++側のユーザ・プログラムのバージョンが一致していない場合、ユーザ・プログラムは旧バージョン・データ出力に対応する必要があります。従って、事情が許す場合はC++側とC#側のユーザ・プログラムのバージョンを一致させる運用にすることがお薦めです。
6. メンバ関数呼び出しの中継について
C++側ユーザ・プログラムが呼び出すメンバ関数、および、C#側ユーザ・プログラムが呼び出すメンバ関数については、当該メンバ関数呼び出しを相手側へ中継するため、下記の処理を行います。(C#側からの呼び出しのみ説明します。C++側からの呼び出しはこの逆のサブセットです。)
-
C#側のメンバ関数にて、メンバ関数クラス・オブジェクトを生成する
メンバ関数クラスは、当該関数の引数をメンバ変数として持つクラスです。これをシリアライズしてC++側へ送ることで、C++側の該当のメンバ関数呼び出しに各引数を伝達します。
-
その際に、共有オブジェクトを共有テーブルへ登録する
thisは往復しますから、常に共有オブジェクトとなります。また、C++側で定義したメンバ関数のシグニチャで非const参照渡しされている引数も同様に往復しますから、やはり共有オブジェクトとなります。
-
C++側スレッドはデシリアライズしたメンバ関数クラス・オブジェクトを取り出す
そして、その引数を与えて指定のメンバ関数を呼び出します。これにより、C#側のメンバ関数呼び出しがC++側へ中継されます。
-
C++側のユーザ・プログラムが定義したメンバ関数から帰ってきたら
this、戻り値、非const参照引数をメンバ変数とするメンバ関数の戻りクラス・オブジェクトを生成し、それをシリアライズしてC#側へ返却します。
-
C#側のメンバ関数を送信したスレッドがそれを受信し、
メンバ関数の戻りクラス・オブジェクトを取り出す際に、this、および、非const参照引数へ反映後、C++側から送られてきた戻り値をreturnします。
なお、C++側のユーザ・プログラムがメンバ関数を呼び出す時は、C#側のガベージ・コレクションによるブロックを回避するため、FIFOへ書き込んだらC#側スレッドの受信処理を待つこと無くreturnします。つまり、上記の4以降はスキップされます。
7. Visual Studioへの組み込み方について
C#とC++のプロジェクトをCMakeにて生成できることは判っていますが、プロジェクトのメンテナンス方法の学習はたいへんです。C++だけならまだしも、C#プロジェクトまでCMakeだけで管理するのは厳しそうです。
そこで、Visual Studioについては、Visual Studioのtoolset機能を使い、ユーザ・プログラム開発はCMakeフリーにする方向で検討中です。
8. Theolizerビルド・システムの見直し
Theolizer自体のビルド・システムも見直す予定です。Issue #37 に記載したCTestの変更の内容より、ビルドとテストは明確に分けるべきと判断しました。現在はテストをビルドに依存させることでテストを起動すればビルドされるようにしています。しかし、CMakeの一般的な流れに戻し、ビルドとテストを独立させ、CMakeスクリプトにより必要に応じて連続呼び出しするようにします。
また、Theolizerドライバも含むテストについて、よりスマートに記述できることが分かりました。現在はCMakeスクリプトで直接コンパイラを起動しています。素直にCMakeでプロジェクトを生成してCMakeでビルドし、CTestでテストするよう変更する予定です。
9. テンプレート対応について
STLコンテナを交換できるようにしたいと思います。そこで、非侵入型手動のテンプレートはC#連携でもサポートする予定です。
またC#はジェネリックの明示的特殊化や部分的特殊化をサポートしていません。現在のTheolizerも同様です。そこで、明示的特殊化と部分的特殊化は非対応のままと致します。
また、標準のコンテナ名はC#とC++では異なります。またC++側はかなり細分化されています。そこで、C++側の複数のコンテナ(vectorやlist等)をC#の1つのコンテナ(List等)に対応させる仕様にする予定です。
なお、メンバ変数の変更に自動的に対応する侵入型半自動テンプレート、および、非侵入型完全自動テンプレート対応の難易度は高く、その必要性は比較的低いと思いますので、C#連携としては当面は対応しないことにします。必要性が見えてきたら再検討します。
なかなか時間が取れていませんが、C#連携用のメタ・シリアライザ開発を進めてます。
概ねの方針を決定できましたので開示します。
1.既存のヘッダを修正します
現在は、NoTypeCheck、TypeCheck、TypeCheckByIndexの3種類をサポートしています。
NoTypeCheckは型チェックなし。TypeCheckとTypeCheckByIndexは、型チェックのためヘッダに型情報を入れています。TypeCheckは型名、TypeCheckByIndexは型に割り振ったインデックス番号をTHEOLIZER_PROCESSで保存/回復するデータに記録し、型の不一致が起きないことをチェックしています。
データ内に記録する制御用データが最小なのでTypeCheckByIndexが最も効率が高いです。
NoTypeCheckはヘッダ情報が少ないため、小さなデータのやり取りには向いていますが、名前対応型のクラスについてはメンバ変数名をデータ中に記録するため大きなデータでは効率が悪化します。
そこで、TypeCheckByIndexをベースにメタ・シリアライズ・データをヘッダ形式で出力するようにします。
enum型に属するシンボルのリストや、各型のTheolizerとしての拡張情報(enum型の定義情報などなど)を追加するイメージです。
なお、開発工数削減のため、最も必要性の薄いTypeCheckを削除します。
以上の結果、従来のデータとの互換性を失います。
2.プリミティブの構成を固定します。
現在、プリミティブの構成は派生シリアライザで決定しています。
これにより、JsonSerializerとXmlSerializerの文字列はUTF-8の1種類のみ、BinarySerializerとFastSerializerはエンコード変換しません。BinarySerializerは型チェックするのでstd::string, std::wstring, std::u16string, std::u32stringの4種類を異なるプリミティブとしています。
JsonSerializerとXmlSerializerでUTF-8以外のデータをstd::string, std::wstring, std::u16string, std::u32string型変数との間で保存/回復する時は、それぞれの派生シリアライザが自動的にエンコードを変換しています。
さて、C#連携では性能をできるだけ上げるため早期にBinarySerializerを使えるようにしたいと考えています。その際、BinarySerializerで用いる文字列のエンコードはC#の文字エンコードであるUTF-16LEが最適です。
そこで、BinarySerializerはstd::string, std::wstring, std::u16string, std::u32stringのデータを全てUTF-16LEへ自動的に変換して保存/回復します。これによりC#側ではエンコード変換が発生しません。
また、C++側の文字列をstd::wstring、もしくは、std::u16stringで記録した場合、C++側でもエンコード変換が発生しません。なお、経験的にはC++側はstd::stringでUTF-8を用いて文字列を管理する方が、他のASCIIで文字列を取り扱うライブラリとの親和性が高く、開発工数を節減できると思います。性能的に問題ないならC++側はstd::stringのUTF-8で文字列を管理することをご検討下さい。
そのためのサポートしてtheolizer::u8stringが便利と思います。
3.ファイル・フォーマットを見直します
保存/回復する変数の型にTypeIndexという番号を割り当てて管理しています。現在は配列とポインタにも元の型とは異なる専用のTypeIndexを割り当てています。
しかし、これはヘッダを肥大化させるのであまり好ましくありませんので下記対応を行います。
TypeIndexは配列やポインタについては割り当てません
基本の型についてのみTypeIndexを割り当てます。int型等の基本型、enum型、クラス、実体化したテンプレートに割り当てます。それらの型の配列やポインタは同じTypeIndexをベースとして表現します。
型チェックのためにはデータが配列やポインタであることを判定する必要がありますので、付加情報を追加します。
ただし、付加情報の肥大化を防ぐため、サポートする配列の最大の次元数は3とします。3次元を超える配列を使いたいケースは稀です。また、もしも4次元以上の配列をシリアライズしたい場合は、一旦クラスでラップすることで対応可能です。
配列へのポインタをサポートから外します
現在のTheolizerは配列へのポインタをサポートしています。(例えば、'typedef int IntArray[100]; IntArray* IntArrayPointer;'のIntArrayPointerは、
int[100]型へのポインタです。このIntArrayPointerをオーナーポインタとして保存/回復をサポートしています。)しかし、そもそもC/C++では配列全体へのポインタを使うことは稀です。(配列の要素へのポインタは普通に使いますが、配列全体へのポインタは言語仕様上、使いづらいためです。)
そして、配列へのポインタをサポートすると「配列へのポインタ」の配列、『「配列へのポインタ」の配列へのポインタ』、...を構成することが可能となるのでTypeIndexの付加情報で管理するためにはTypeIndexを可変長にする必要があります。
使用頻度の低い機能のサポートのためにTypeIndexが複雑化することを避けるため「配列へのポインタ」をサポートから外します。
4.メタ・シリアライズについて
C++側のメタ・シリアライザ
これはユーザ・プログラムの各種クラスやenumの定義情報等が必要ですので、ユーザ・プログラム内でC#連携を指定してシリアライザをコンストラクトすることで出力されるようにします。
具体的な起動手順は未定ですが、ユーザ・プログラムがオプションを指定して起動されたら、メタ・シリアライズ機能を呼び出して頂くイメージになる予定です。
僅かなコードをmain()関数やWinMain()関数へ組込むことで対応できるようにする予定です。
C#側のメタ・デシリアライザ
メタ・シリアライズ・データを解析し、C#のenumやクラス定義ソースを出力する、メタ・デシリアライザを追加します。こちらはユーザ・プログラムではなく独立したプログラムをTheolizer側で用意します。
5.バージョン対応について
Issue #42 の「4-1.C#側シリアライザの開発」に記載したように、C#側はそのソースが自動生成された時の最新版のみをサポートします。C++側は自分より古いもの全てをサポートします。
これにより、C++側の「サーバ」を最新版へアップしておくことで古いクライアントも機能できる状態にする計画です。これはTheolizerのバージョン変更対応機能を使用します。
C++側ユーザ・プログラムは、古いバージョンのユーザ・プログラムが出力したデータを回復する新しいバージョンのユーザ・プログラムを開発することは比較的容易です。逆に古いバージョンのデータを出力することもTheolizerのサポートにより可能ですが、バージョンアップ対応に比べると手間がかかります。
C#連携の場合、C#側とC++側のユーザ・プログラムのバージョンが一致していない場合、ユーザ・プログラムは旧バージョン・データ出力に対応する必要があります。従って、事情が許す場合はC++側とC#側のユーザ・プログラムのバージョンを一致させる運用にすることがお薦めです。
6. メンバ関数呼び出しの中継について
C++側ユーザ・プログラムが呼び出すメンバ関数、および、C#側ユーザ・プログラムが呼び出すメンバ関数については、当該メンバ関数呼び出しを相手側へ中継するため、下記の処理を行います。(C#側からの呼び出しのみ説明します。C++側からの呼び出しはこの逆のサブセットです。)
C#側のメンバ関数にて、メンバ関数クラス・オブジェクトを生成する
メンバ関数クラスは、当該関数の引数をメンバ変数として持つクラスです。これをシリアライズしてC++側へ送ることで、C++側の該当のメンバ関数呼び出しに各引数を伝達します。
その際に、共有オブジェクトを共有テーブルへ登録する
thisは往復しますから、常に共有オブジェクトとなります。また、C++側で定義したメンバ関数のシグニチャで非const参照渡しされている引数も同様に往復しますから、やはり共有オブジェクトとなります。
C++側スレッドはデシリアライズしたメンバ関数クラス・オブジェクトを取り出す
そして、その引数を与えて指定のメンバ関数を呼び出します。これにより、C#側のメンバ関数呼び出しがC++側へ中継されます。
C++側のユーザ・プログラムが定義したメンバ関数から帰ってきたら
this、戻り値、非const参照引数をメンバ変数とするメンバ関数の戻りクラス・オブジェクトを生成し、それをシリアライズしてC#側へ返却します。
C#側のメンバ関数を送信したスレッドがそれを受信し、
メンバ関数の戻りクラス・オブジェクトを取り出す際に、this、および、非const参照引数へ反映後、C++側から送られてきた戻り値をreturnします。
なお、C++側のユーザ・プログラムがメンバ関数を呼び出す時は、C#側のガベージ・コレクションによるブロックを回避するため、FIFOへ書き込んだらC#側スレッドの受信処理を待つこと無くreturnします。つまり、上記の4以降はスキップされます。
7. Visual Studioへの組み込み方について
C#とC++のプロジェクトをCMakeにて生成できることは判っていますが、プロジェクトのメンテナンス方法の学習はたいへんです。C++だけならまだしも、C#プロジェクトまでCMakeだけで管理するのは厳しそうです。
そこで、Visual Studioについては、Visual Studioのtoolset機能を使い、ユーザ・プログラム開発はCMakeフリーにする方向で検討中です。
8. Theolizerビルド・システムの見直し
Theolizer自体のビルド・システムも見直す予定です。Issue #37 に記載したCTestの変更の内容より、ビルドとテストは明確に分けるべきと判断しました。現在はテストをビルドに依存させることでテストを起動すればビルドされるようにしています。しかし、CMakeの一般的な流れに戻し、ビルドとテストを独立させ、CMakeスクリプトにより必要に応じて連続呼び出しするようにします。
また、Theolizerドライバも含むテストについて、よりスマートに記述できることが分かりました。現在はCMakeスクリプトで直接コンパイラを起動しています。素直にCMakeでプロジェクトを生成してCMakeでビルドし、CTestでテストするよう変更する予定です。
9. テンプレート対応について
STLコンテナを交換できるようにしたいと思います。そこで、非侵入型手動のテンプレートはC#連携でもサポートする予定です。
またC#はジェネリックの明示的特殊化や部分的特殊化をサポートしていません。現在のTheolizerも同様です。そこで、明示的特殊化と部分的特殊化は非対応のままと致します。
また、標準のコンテナ名はC#とC++では異なります。またC++側はかなり細分化されています。そこで、C++側の複数のコンテナ(vectorやlist等)をC#の1つのコンテナ(List等)に対応させる仕様にする予定です。
なお、メンバ変数の変更に自動的に対応する侵入型半自動テンプレート、および、非侵入型完全自動テンプレート対応の難易度は高く、その必要性は比較的低いと思いますので、C#連携としては当面は対応しないことにします。必要性が見えてきたら再検討します。