Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

C#連携:メタ・シリアライザ開発 #45

Open
yossi-tahara opened this issue Mar 14, 2018 · 9 comments

Comments

@yossi-tahara
Copy link
Owner

commented Mar 14, 2018

なかなか時間が取れていませんが、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を割り当てています。
しかし、これはヘッダを肥大化させるのであまり好ましくありませんので下記対応を行います。

  1. TypeIndexは配列やポインタについては割り当てません
    基本の型についてのみTypeIndexを割り当てます。int型等の基本型、enum型、クラス、実体化したテンプレートに割り当てます。それらの型の配列やポインタは同じTypeIndexをベースとして表現します。
    型チェックのためにはデータが配列やポインタであることを判定する必要がありますので、付加情報を追加します。
    ただし、付加情報の肥大化を防ぐため、サポートする配列の最大の次元数は3とします。3次元を超える配列を使いたいケースは稀です。また、もしも4次元以上の配列をシリアライズしたい場合は、一旦クラスでラップすることで対応可能です。

  2. 配列へのポインタをサポートから外します
    現在のTheolizerは配列へのポインタをサポートしています。(例えば、'typedef int IntArray[100]; IntArray* IntArrayPointer;'のIntArrayPointerは、int[100]型へのポインタです。このIntArrayPointerをオーナーポインタとして保存/回復をサポートしています。)

しかし、そもそもC/C++では配列全体へのポインタを使うことは稀です。(配列の要素へのポインタは普通に使いますが、配列全体へのポインタは言語仕様上、使いづらいためです。)
そして、配列へのポインタをサポートすると「配列へのポインタ」の配列、『「配列へのポインタ」の配列へのポインタ』、...を構成することが可能となるのでTypeIndexの付加情報で管理するためにはTypeIndexを可変長にする必要があります。
使用頻度の低い機能のサポートのためにTypeIndexが複雑化することを避けるため「配列へのポインタ」をサポートから外します。

4.メタ・シリアライズについて

  1. C++側のメタ・シリアライザ
    これはユーザ・プログラムの各種クラスやenumの定義情報等が必要ですので、ユーザ・プログラム内でC#連携を指定してシリアライザをコンストラクトすることで出力されるようにします。
    具体的な起動手順は未定ですが、ユーザ・プログラムがオプションを指定して起動されたら、メタ・シリアライズ機能を呼び出して頂くイメージになる予定です。
    僅かなコードをmain()関数やWinMain()関数へ組込むことで対応できるようにする予定です。

  2. 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++側からの呼び出しはこの逆のサブセットです。)

  1. C#側のメンバ関数にて、メンバ関数クラス・オブジェクトを生成する
    メンバ関数クラスは、当該関数の引数をメンバ変数として持つクラスです。これをシリアライズしてC++側へ送ることで、C++側の該当のメンバ関数呼び出しに各引数を伝達します。

  2. その際に、共有オブジェクトを共有テーブルへ登録する
    thisは往復しますから、常に共有オブジェクトとなります。また、C++側で定義したメンバ関数のシグニチャで非const参照渡しされている引数も同様に往復しますから、やはり共有オブジェクトとなります。

  3. C++側スレッドはデシリアライズしたメンバ関数クラス・オブジェクトを取り出す
    そして、その引数を与えて指定のメンバ関数を呼び出します。これにより、C#側のメンバ関数呼び出しがC++側へ中継されます。

  4. C++側のユーザ・プログラムが定義したメンバ関数から帰ってきたら
    this、戻り値、非const参照引数をメンバ変数とするメンバ関数の戻りクラス・オブジェクトを生成し、それをシリアライズしてC#側へ返却します。

  5. 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#連携としては当面は対応しないことにします。必要性が見えてきたら再検討します。

yossi-tahara added a commit that referenced this issue May 6, 2018

Binaryシリアライザの文字列保存時、保存するバイト数計算をバグっていた Issue #45
  valigrandの指摘で発見
無効なTypeIndexの表示時、[Invalid TypeIndex]と表示するよう変更
不要なデバッグ文削除
なお、現在のテストはシュリンク・テストである。disable_test.hで大きくげずっている。

------ auto generated message by Theolizer
TheolizerDriver  : be072ca726f351870bae99e839fb37b8
TheolizerLibrary : a2ccf8d244fcd1b8e28b646e04cdb562
Library's Header : d41d8cd98f00b204e9800998ecf8427e
------ end of message
@yossi-tahara

This comment has been minimized.

Copy link
Owner Author

commented May 6, 2018

上述の1、2、3についてやっと実装できました。csharp_integrationブランチへプッシュしています。
内部的に、型の管理方法を大きく変更しました。従来、std::size_t型で単純な番号で管理していましたが、少し構造を持つようにしました。その際、ほぼ使わないだろう型のサポートを取りやめました。

a. 配列やポインタでない「素」の型をシリアル番号(bit4~)で管理します
b. 配列は3次元配列までサポートします。この情報に2ビット(bit3,2)確保し、下記のように割り当てます。
 0:非配列
 1:1次元配列
 2: 2次元配列
 3: 3次元配列

c. ポインタはオブジェクト追跡情報として2ビット(bit1,0)確保し、下記のように割り当てます。
 0: 通常のインスタンス(オブジェクト追跡無し)
 1: 通常のインスタンス(被ポインタとしてオブジェクト追跡)
 2: 通常のポインタ(オブジェクト追跡有り)
 3: オーナーポインタ(オブジェクト追跡有り)

上記のa, b, cを内部的にunsigned型で管理します。
unsignedが16ビットのマシンなら合わせて4,096のクラスとenum型を管理できます。(16ビットCPUならば現実的な線と思います。)
一般的なunsignedが32ビットのマシンなら合わせて28ビット268,435,456のクラスとenum型を管理できるので十分と思います。

これをテキスト型のシリアライザ(JsonやXml)へ出力する時にデバッグしやすいよう、aを仮数部、b,cを4ビットの指数部として表現します。例えば素の型のシリアル番号が12の通常のポインタの1次元ン配列の場合、12e9と出力します。

サポートを取りやめた型について

上記の仕組みから、次の型のサポートを取りやめました。

  • 4次元以上の配列
    4次元以上の配列を保存/回復や通信したいケースはほとんどないと思いますが、どうしても使いたい場合は一度クラスで受けることで使用可能です。

  • 配列全体へのポインタ
    これを許すと TypeIndexを上述のように単純な形で表現できなくなります。
    現在はポインタの配列まで許しています。配列全体へのポインタを許すと、ポインタの配列へのポインタの配列への・・・を許すことになります。複雑になりすぎます。
    C++ではそもそも配列全体へのポインタを定義できますが、非常に使いにくいです。 そこで、ここで切り捨てることにしました。

さて、これでやっとメタ・シリアライズとC#側メタ・デシリアライズに着手できます。

@yossi-tahara

This comment has been minimized.

Copy link
Owner Author

commented May 13, 2018

フルテストを実施しました。
若干修正を行い、ほぼ通りました。
ただし、MinGWx32はreference_and_testのbasic2だけメモリ不足でリンクに失敗します。
(msvcはx32でも通ります。gccはx32のテストをしていないので不明です。)

MinGWはクラス数が多いと対応できない問題も有ります。linux(gcc)とwindowsの間での無理がたたっているのかも知れません。
元々MinGWはgcc向け対応をWIndowsで開発しやすくする目的もあるのでサポートしました。
MinGWのx32対応は近々の内に断念しようと思います。もしかするとMinGW自体を断念し、clangへ切り替えるかも知れません。より効率的に開発できる可能性に期待しています。(丁寧なエラーメッセージやMacへの移植性等)

yossi-tahara added a commit that referenced this issue May 13, 2018

MinGWx32以外のフルテストにパス issue #45
  32bit環境のstd::size_t変換警告対応
  Doxygen警告回避
MinGWx32はリンク時にメモリ不足エラーとなる
  MinGWは断念してclangへ切り替える方向とするのでこの問題は保留する

------ auto generated message by Theolizer
TheolizerDriver  : fb7725d098b30761d5e349ea9c412c9c
TheolizerLibrary : e76d7f51402b4902ddc0d3f67396c962
Library's Header : d41d8cd98f00b204e9800998ecf8427e
------ end of message
@yumetodo

This comment has been minimized.

Copy link

commented May 13, 2018

MinGWx32はreference_and_testのbasic2だけメモリ不足でリンクに失敗します。
MinGWのx32対応は近々の内に断念しようと思います。

large address awareでどうにかなったりしないですかね・・・

@yossi-tahara

This comment has been minimized.

Copy link
Owner Author

commented May 13, 2018

なるほど。期待できそうですね。やってみます。

やってみました。どうもダメなようです。
MinGWのldはout of memoryの時、どんなパラメータを受け取ったか表示しないの判りにくいのです。
しかし、x64とx32の両方のケースで -Wl,--large-address-awareを渡すと、x64側は「--large-address-awareを知らないオプション」と言ってエラーを返す(これは32bit用のオプションらしいです)のでパラメータとしては渡っている筈です。
しかし、相変わらずout of memoryで終了します。

@yumetodo

This comment has been minimized.

Copy link

commented May 13, 2018

AviUtlのlarge address aware設定だと初回管理者権限必須だったりするので管理者権限試してみてください。

あとダメ元で4GB patch当ててみるとか
http://www.ntcore.com/4gb_patch.php

@yossi-tahara

This comment has been minimized.

Copy link
Owner Author

commented May 14, 2018

情報ありがとうございます。
管理者権限での起動はやってみましたが、結果変わらずでした。MinGWへのパッチは怖いので止めておきます。
以前もMinGWでセグメント数多すぎの対処で苦労して、最終的にはMinGW用のテストの組み合わせ量をを減らして対処したことがあります。
今回も問題がでているのはテスト部なので、MinGW x32のテストを更にスペックダウンする方向もありかなとも思います。

でも、今の時代MinGWx32の自動テストは断念してもよいような気がしています。
MinGWx32のユーザさんなら、もしも問題がでた時は問題報告できることを期待したいです。

@yumetodo

This comment has been minimized.

Copy link

commented May 14, 2018

まあ実際Mingw32とかもう滅んでいいだろうという思いはある。特許のせいで例外モデルにSEHが使えないので。

@yossi-tahara

This comment has been minimized.

Copy link
Owner Author

commented May 14, 2018

うへっ、SEHって特許があるのですか。びっくり。

@yumetodo

This comment has been minimized.

Copy link

commented May 15, 2018

まあだいぶ曖昧なライン。
https://gcc.gnu.org/wiki/WindowsGCCImprovements

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
2 participants
You can’t perform that action at this time.