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#連携:C#側シリアライザの開発 #43

Open
yossi-tahara opened this issue Nov 3, 2017 · 7 comments
Open

C#連携:C#側シリアライザの開発 #43

yossi-tahara opened this issue Nov 3, 2017 · 7 comments

Comments

@yossi-tahara
Copy link
Owner

yossi-tahara commented Nov 3, 2017

この作業も少しステップを踏みます。

1.C#プロジェクトをCMakeで生成する

そもそもC#側をGUI対応にしたいので、Windows Formsとして生成中です。
概ねできたのですが、CMakeの不具合が2つ見つかっています。

CMake で C# のプロジェクトを作ろうとしてハマった話はこれです。
CMakeのGitLabにマージリクエストを投げる予定ですが、上記発見者の了解を貰おうと待っているところです。しばらく待って回答を貰えない場合は、必要なリンクを貼ってから投げようと思います。

もう一つは、正直ほっといても良いと思うのですが、Settings.Designer.csをC#プロジェクト・ファイルに仕込む時にミスがあるようで、Visual Studioが警告してきます。警告通り処理するとSettings1.Designer.csができてしまい、ちょっと気持ち悪い状況です。
CMakeのソースをcloneしていたので、ちょっと覗いてみると意外に簡単に修正できそうなので修正してみたところ、少なくとも上記トライしているプロジェクトでは警告がなくなりました。
マージリクエストしようかどうか考え中です。CTestするとFailするので悩ましい状況です。

2.ストリームにてデータ交換できるようにする

C#とC++間のデータ交換はシリアライズされたバイト列で行います。それをC#側クライアントexeとC++側サーバdllにてやり取りしたいため、その仕組みを実装する予定です。

C#側はStreamを派生、C++側はstdd::streambufを派生して出来るはずですので、チャレンジしてみます。

3.そしてC#側のシリアライザ開発に着手します

最初はデバッグしやすいようJson形式で考えています。
その後Binary形式の開発になりますが、こちらはもしかするとクラウドソーシングで外注するかもしれません。恐らく年内は厳しいと思います。

@yossi-tahara
Copy link
Owner Author

yossi-tahara commented Nov 6, 2017

CMakeに上記のマージリクエストを投げてみました。
それなりにチェックしたつもりですが、何かやらかしていないか心配。

fix warning around 'Properties\Settings.settings' in Visual Studio
CSharp: fix generation fail in environment except English

この2件、マージされました。
Settingsの方は検討不足が見つかり修正しました。こちらは自動テストしたいとの話がでてます。対応できるかどうか考え中です。

@yossi-tahara
Copy link
Owner Author

C#の言語バージョン

Visual Studio 2015は言語バージョン6、Visual Studio 2017は言語バージョン7です。
どちらも.NET 4.5以上であれば動作するそうです。

TheolizerはVisual Studio 2015以降に対応していますので、C#プロジェクトも同様とします。
そこで、TheolizerのC#部分については言語バージョン6で開発を進めます。
ユーザプログラムは言語バージョン6以上にて開発できます。

C#の32/64bit

32bitと64bit対応がややこしいです。
C#はAnyCPUモードがあり、.NET 4.5以降はデフォルトでは64bit OSでも32bitで起動するようです。
この時、dllも32bitでないと例外が発生してdllを呼び出せません。

複雑な問題を避けるため、CMakeで32bitにて生成した時はデフォルトのAnyCPU、64bitにて生成した時はx64(64bit)でC#プロジェクトを生成することにしました。何か問題が発生したらその時再検討します。

C#とC++間のデータ交換

シリアライズ済のデータを交換する部分にはC#側はStream、C++側はstd::istream, std::ostreamにて試作を進めています。
C#側のガベージコレクションによりC++側処理をなるべくブロックしないようにするため、C++からC#へ情報伝達のための関数はvoidのみでデータの戻りなしとします。これによりC#の処理を待たずにC++側処理を継続できるのでGCの影響を大幅に軽減できるはずです。std::ostreamのバッファ・サイズを十分に確保すれば皆無にすることも可能な筈です。(ユーザプログラムの作りにもよります。C++側がイベント通知後、C#からの要求を待つような作りをするとGCの影響を受けます。)

そこで、試作については下記の3本のストリームにてデータ交換する予定です。(試作中に変更する可能性もあります。)

  1. C#からC++への処理要求(C# → C++)
  2. 処理要求への応答(C++ → C#)
  3. C++からC#へのイベント(C++ → C#)

2.と3.をマルチプレックスして1本にすることも恐らく可能ですが、多少オーバーヘッドが増えるのでまずは上記仕組みで進めます。

C++側のスレッド構成

まずはC++はdllで試作を進めていますが、C#からの要求を受け取って処理するためのスレッドが必要です。(スタックフルなコルーチンでも可能ですが、当初はスレッドを使います。)
dllがロードされた時に専用スレッドを1つ起動し、これからユーザのmain()関数を呼び出し、main()関数にて必要なセットアップを行います。そして、このスレッドでC#からの処理要求をデシリアライズし、そのままユーザ関数を呼び出す方向で検討中です。
C++側をexeで実装して他のプロセスへサービスしたり、他のコンピュータ上のプロセスへサービスしたりすることも想定していますので、その時、同じ構造で開発できるのは好ましいと思います。

イベント通知が必要になる場合(機器制御ではよく使います)は、main()関数処理でサブスレッドを起動してそちらに機器管理等のループを回し、そのスレッドが必要に応じてイベント通知を発行するイメージで考えています。

なお、main()関数スレッドで機器管理ループを回して、処理要求をサブ・スレッドで受け取ることも可能と思います。

非同期処理は今のところ考えていません。非同期処理関数を呼び出すためのスレッドをどうするのかの問題を考えるとサブ・スレッドを用いた同期処理がベストと考えています。

@yossi-tahara
Copy link
Owner Author

現状

本日(2017/11/11)、csharp_integrationブランチへプッシュしました。
C#連携のための接続手順部分を実装し、その上にC# <-> C++間のメモリ・ストリームを実装、テキスト・データの単純なやり取りができるようになりました。やっとC#側シリアライザの開発に着手できます。

std::streambufに苦しみました。C#側はあっさりできましたが、性能をなるべく落とさないようにするため、殆どの処理をC++側で実装したこととstd::iostreamに苦労しました。メンバ関数の多さと直感的でない関数名が頭痛いです。そして、手を抜くとバイト単位でやり取りするのが頭痛いです。C++→C#側は複数バイト処理に対応しましたが、C#→C++側はまだ対応していません。どこかで時間をとってトライします。

連携クラスDllIntegratorを導入しました

これがC#/C++の両方に存在し、C#とC++の接続を管理するクラスとなります。
現在はDLL連携ですので1つのみで十分の筈ですからシングルトンにしています。
複数の連携を貼ることも可能な筈ですが、あまり使わない気がする割に使い方が無駄に複雑になりそうなので今は1つに限定しています。開発を進めながら検討します。

ちょっとアイデアが浮かびました

今は、C#がクライアント、C++がサーバで考えています。C#が要求を送りC++が応答を返します。
この構成であればC#のGC(ガベージコレクション)の遅延によるC++側のブロックを最小限にできるというメリットもありますし、素直な構成だと思います。

ところで、C++側はVisual C++に限定する必要は無いはずです。ネットワーク経由で連携できますから、IoT等のデバイスでC++11でプログラミング可能な機器なら理論上対応できる筈です。コンパイラがgccやclangなら現実的な期間で対応できるだろうと思います。

そして、IoT等への応用を考えるとC++をクライアントにしたいケースもありそうです。
この場合は、C++から要求を出してC#の応答を待つ構造になりますのでGCの影響は回避できませんが、それを除けば対応できる筈です。

更に、頑張ればC# dllをネイティブC++ exeから直接呼び出すなんて荒業も不可能ではなさそうです。
COM経由でC++からC#への接続関数を1つ呼べれば、後はストリームによるやり取りで連携可能です。

夢は広がるんですが、手が追いついていません。少しづつ進めていきます。

yossi-tahara added a commit that referenced this issue Nov 10, 2017
------ auto generated message by Theolizer
TheolizerDriver  : 2689a03ecf40efd14f058639020ad294
TheolizerLibrary : dee0663b4cbe3f7825471d270af1e67e
Library's Header : be7336ad80b64315add4e50dfa3ebf05
------ end of message
yossi-tahara added a commit that referenced this issue Nov 18, 2017
  文字列や配列処理はまだ。
  また、C++側はまだ単純なエコーバックなので、タイミングによってC#側がデータの終了判定に失敗する。
  この問題はシリアライザで処理する時には発生しない。

------ auto generated message by Theolizer
TheolizerDriver  : bbc4897a5dd0332296422621b3284e94
TheolizerLibrary : 7ad3c30327146d6d6f79a3708ba215f1
Library's Header : be7336ad80b64315add4e50dfa3ebf05
------ end of message
@yossi-tahara
Copy link
Owner Author

33f0c65のコミットにて、C#側からの関数呼び出しのイメージを実装しました。その概念を説明します。

インテグレータ・クラス(DllIntegrator)

これが接続を管理します。ストリームは3本用意します。それぞのストリーム毎にシリアライザを生成します。

  • 要求用ストリーム(C#→C++)
  • 応答用ストリーム(C++→C#)
  • 通知用ストリーム(C++→C#)

私自身の経験上はマスター/スレーブ接続ではこの3本を使うと必要な通信をスムーズに行えました。現ターゲットではマスターはC#です。マスターがクライアント(お客様.exe)となり、スレーブ側はお客様にサービスを行う(サーバ.dll)という考え方になります。
要求→応答にメイン・スレッドを使用し、サブ・スレッドで非同期に送られてくる通知を受け取ることを想定しています。

通知用ストリームを逆方向に流したり、応答を返したりしたいケースもあると思います。
その時は、そのような管理を行うインテグレータ・クラスを作って対応できる筈です。

メンバ関数の呼び出し

C++側でコーディングしたクラスの内、「C#連携」として保存先指定したメンバをメタ・シリアライザにてC#側クラス定義へ自動変換する予定ですが、その時のメンバ関数の動作モデルの一部を記述しています。

最終的な姿は、下記を想定しています。

  1. C#側でオブジェクトを生成し、メンバ関数を呼び出したら、内部で関数クラス(this、および、それぞれのパラメタをメンバとするもの)をコンストラクトし、それをシリアライズしてC++側へ送ります。

  2. C++側では初めて送られてきたオブジェクトなら、デフォルト・コンストラクタで生成し、対応するメンバ関数を呼び出します。この中身はユーザが記述したものになります。

  3. そのメンバ関数の実行が完了したら、戻り値、および、非const参照されたパラメータを戻り用のクラスに纏めてコンストラクトし、シリアライザにてC#へ返却します。

  4. C#側はそれをデシリアライズして呼び出し元へ返却します。

  5. 2回目以降の呼び出し時、C#で指定したオブジェクトに対応するC++側オブジェクトの関数を適切に呼び出すため、共有テーブルをC#/C++両方に設けて対応付ける予定です。

  6. ユーザ・プログラムが管理していない共有オブジェクトはTheolizer側で破棄できますが、ユーザ・プログラムも管理している共有オブジェクトは自動的に破棄できません。そこで、ユーザ・プログラムが必要なメンバ関数呼び出しを通じてオブジェクトの破棄を同期させるものとし、もしも不正操作した(片方で破棄されているオブジェクトへ要求を投げるなど)時は例外を投げる方向で考えています。

  7. メンバ関数としてコンストラクタを定義できるようにすることも可能だろうと思います。コンストラクタはthisをパラメータに持たないだけで済むはずと想定しています。(何か見落としがあるかも。)

上記の内、要求を投げるための関数クラスのオブジェクトを生成してJsonにてシリアライズしてC++へ投げるところまで実装しています。今後、C++側のデシリアライザで受け取り、応答を返却する部分までを第一ステップとして開発を進めます。
次に共有テーブルを開発して、Issue #42 の「4-1.C#側シリアライザの開発」の設計が完了するイメージです。

その後、Issue #42 の「4-2.C#側メタ・デシリアライザの開発」に着手します。

使用するインテグレータ・オブジェクト

DLLモデルでは連携接続は1本で十分と思うので、シングルトンにしていますのでインテグレータ・オブジェクトは1つだけ有効です。
しかし、本質的に複数の接続にも対応できる構造なので、必要に応じて複数生成できる設計にしています。(シングルトンにしなければOK)
複数接続対応方法は悩ましいですが、現時点ではスレッドにバインドさせる設計としました。
スレッド・ローカル・ストレージに使用するインテグレータ・オブジェクトを登録して使います。

DllIntegratorはシングルトンなのでスレッド・ローカル・ストレージへ記録する必要はないのですが、上述した 1. 処理は自動生成するコードにて行います。この中で使用するインテグレータ(接続)を獲得し、そのシリアライザを取り出してそこへシリアライズします。このインテグレータの取り出しは DllIntegratorだけでなく、他のインテグレータでも使えるようにしたいため、スレッド・ローカル・ストレージ(staticクラスThreadIntegrator)を使用してみました。

別案としては、各メンバ関数呼出し時のパラメータでインテグレータを指定することが考えられます。ほとんど常に決まったパラメータを毎回指定するのは面倒ですし、バグの元なのでこの案は棄却しました。
ならば、オブジェクトのコンストラクト時にインテグレータを指定することも考えられます。今後、こちらの案へ変更する可能性もあります。ただ、使い方が面倒になるのも事実なので取り敢えず優先順位は落としています。

yossi-tahara added a commit that referenced this issue Nov 20, 2017
------ auto generated message by Theolizer
TheolizerDriver  : 6c53ef20c964c17f46239caa662fb77a
TheolizerLibrary : 2040eb742423f16db70cd2d7054ec745
Library's Header : d41d8cd98f00b204e9800998ecf8427e
------ end of message
@yossi-tahara
Copy link
Owner Author

C#側のシリアライザが一応動作しました。
まだ、実装していない機能が多数ありますが、基礎部分は出来たと思います。

基本はC++側で提供するオブジェクトのメンバ関数をC#側から呼べるようにしたというものになります。実際のデータ交換はシリアライザを用いるためコピー・ベースとなりますが、必要な情報を全てコピーするため、アドレスを除けばオブジェクトを転送できていることになります。

現在のテスト実装内容

  1. ユーザが用意する予定のクラス
     ・ メンバ関数int func0(UserClassSub const&)を持つUserClassMain
     ・ UserClassSub
    将来的には、ビルド時にこれらをメタ・シリアライズしてC#へ送りC#にてメタ・デシリアライズしてユーザ・クラスのソースを生成する予定。現在はまるっと未実装。

  2. C#ユーザ側処理
    C#側ユーザ・プログラムでUserClassSubとUserClassMainをコンストラクトしてfunc0()を呼び出す。

  3. C#側 インテグレータ処理
    それらをポイントするfunc0UserClassMainをコンストラクトし、シリアライズしてC++側へ送信。
    その際、func0UserClassMainの型ID(Theolizer独自に振っているTypeIndex)を送る。

  4. C++側インテグレータ処理
    型IDを受け取り、それとマッチするfunc0UserClassMainをコンストラクトする。(この辺はboost::anyテクニック使用)
    ユーザ定義のfunc0UserClassMain::func0を呼び出す。

  5. C++ユーザ側処理
    func0で、func0UserClassMainを修正し、かつ、適当な戻り値を返却する。

  6. C++側インテグレータ処理
    受け取った戻り値とfunc0UserClassMainを含むfunc0UserClassMainReturnクラスをコンストラクトし、シリアライズしてC#側へ送信。

  7. C#側インテグレータ処理
    要求した側なのでfunc0UserClassMainReturnが返却されることは分かっているので、func0UserClassMainReturnをデシリアライズする。この時、自UserClassMainへ反映される。
    戻り値を取り出し、それを持ってfunc0からreturnする。

  8. C#ユーザ処理
    受け取った戻り値とUserClassMainの内容をハードコーディングでtextBoxへ表示する。

残務

  • enum型と配列に未対応
  • 基底クラスに対応できる筈だが未検証
  • オブジェクト追跡は当面未対応の予定
  • BinarySerializerに未対応
    現在はデバッグをやりやすいJsonSerializerにて開発中。流れ完成後、BinarySerializer開発予定。派生Serializer部だけなので比較的短期間で開発できる筈。
  • 共有オブジェクトに未対応
    つまり一往復しか同じオブジェクトを維持できない。同じオブジェクトについて2往復目もC++側は新規にオブジェクトがコンストラクトされる。今後、共有テーブルを使って2往復目以降も同じオブジェクトへの要求はC++側の同一オブジェクトにて処理できるようにする予定。
  • メタ・シリアライズとメタ・デシリアライズも未対応
    メタ・シリアライズを既存のヘッダ生成を少し修正する方向で処理する予定。
    メタ・デシリアライズは従来通りC#にて開発予定。メタ・データのデシリアライザはDynamicJsonではなくC#側Theolizerを使用予定

今後の予定

少し別件で忙しくなりそうなので時間がかかりますが、上記残務を順次進めていきます。
元々年内は無理だろうと思っていましたが、別件も入ってきたこともあり、やはり厳しそうです。
来春くらいをめどに一通り完成させたいと考えています。

yossi-tahara added a commit that referenced this issue Nov 26, 2017
  関数クラスの回復処理実装前
C#側のインテグレータを共通部(core_integrator)とカスタム部(integrator_dll)へ分離
------ auto generated message by Theolizer
TheolizerDriver  : 3212bd352466f7713ae15e990795c2c6
TheolizerLibrary : 7772ee1a2415186b0d3674930ee43da8
Library's Header : d41d8cd98f00b204e9800998ecf8427e
------ end of message
yossi-tahara added a commit that referenced this issue Nov 26, 2017
詳細は→#43 (comment)

------ auto generated message by Theolizer
TheolizerDriver  : 48d07d3ad3551d725ca23930a68a4cba
TheolizerLibrary : 716c9d95044e8e4ab1422cff4d6a2d24
Library's Header : d41d8cd98f00b204e9800998ecf8427e
------ end of message
yossi-tahara added a commit that referenced this issue Nov 30, 2017
linux側Sharedライブラリ版で.soなしエラー発生し、対策した
  LD_LIBRARY_PATHの修正

------ auto generated message by Theolizer
TheolizerDriver  : 4ce275cf7be1c5029d748e6b37afe48a
TheolizerLibrary : 35a023364efdd5b356bc4a78269b1d06
Library's Header : d41d8cd98f00b204e9800998ecf8427e
------ end of message
@yossi-tahara
Copy link
Owner Author

現在、共有オブジェクトの仕組みを試作しています。
C++とC#ではメモリ管理の考え方が全く異なるため、意外に苦労しています。しかし、やっと概ね目処が付きつつあります。

C#のように参照があれば開放されない方式とします。
C++側はユーザ・プログラムがtheolizer::SharedPointerにて保持されている限り開放されないようにします。(生ポインタ等での参照はGCのないC++では対応できないので)
内部的な実装はstd::shared_ptrですが、ユーザ・プログラムで保持しているかどうかをC#側へ伝達する必要があるためラップしています。C++で保持している時は通常の参照、保持していない時は弱参照で保持することで、C#側のユーザ・プログラムが参照していなければ開放できる仕組みとしました。

「C++からC#への通知」を実装しつつ上記の仕組みを試作しています。この「通知」の仕組みが出来上がったら、細かい部分を整えた後、メタ・シリアライザ開発に着手します。

yossi-tahara added a commit that referenced this issue Feb 7, 2018
  これにより、issue #43 の試作はほぼ完了。
  他の環境でのビルド等の細かい調整を行った後、メタ・シリアライズを着手する。
------ auto generated message by Theolizer
TheolizerDriver  : 8fbef0ef7df37d6f366d6cd196904e3d
TheolizerLibrary : 58c82a663903f4a46cfe5dba517cb1e4
Library's Header : d41d8cd98f00b204e9800998ecf8427e
------ end of message
yossi-tahara added a commit that referenced this issue Feb 12, 2018
  linux用にfPIC_ON設定追加
  これがTRUEの時、
    TheolizerLib/Testのコンパイル・オプション-fPICを追加する。
    リンクするboostをfPIC版に切り替える。
  なお、reference_and_testは最小限のテストへシュリンクしている。

------ auto generated message by Theolizer
TheolizerDriver  : 9497aad84d549596f73340bfe5a6876c
TheolizerLibrary : 5cdcd195b3db63abe09079b819156a49
Library's Header : d41d8cd98f00b204e9800998ecf8427e
------ end of message
yossi-tahara added a commit that referenced this issue Feb 12, 2018
------ auto generated message by Theolizer
TheolizerDriver  : 9497aad84d549596f73340bfe5a6876c
TheolizerLibrary : 5cdcd195b3db63abe09079b819156a49
Library's Header : d41d8cd98f00b204e9800998ecf8427e
------ end of message
@yossi-tahara
Copy link
Owner Author

共有オブジェクトの仕組みの試作完了し、C++側についてMinGWとgccでビルドできるようになりました。

ただし、MinGWによるDLLをC#から呼び出すことには成功していません。
64bitのMinGWのDLLをビルドし64bitのC#から呼び出すと例外0x8007000Bが発生します。
極簡単なDLLとそれを呼び出すC#プログラムで試しても同様な結果となりました。
MinGWが生成する64bit DLLはC#に対応していないようです。
ですが、このような使い方をするケースは考えにくいのでこの件は保留します。
また、gccのlinux共有ライブラリをWindows側C#から直接呼ぶことはできませんので、呼び出し可能であることは検証していません。
MinGWやgccについてはパイプやTCP/IP等によるリモート接続をサポートする際に対応します。

以上によりライブラリ・レベルでの技術的課題をほぼクリアできたと思いますので、メタ・シリアライザ開発を着手します。

下記残務については、メタ・シリアライザ開発と前後して対応します。

  • enum型と配列対応
  • 基底クラス対応検証
  • BinarySerializer対応

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

1 participant