C#とC ++クロスプラットフォームの相互作用

対話するためにC#とネイティブC ++コード(またはC)の必要性に対処する必要がありましたか? 理由は異なる可能性があります。ライブラリがすでに存在し、C / C ++で記述しやすく、アプリケーションの一部の開発が異なるチームによって実行されます、_______________(必要な情報を入力してください)。



言語は完全に異なる公理のセットに基づいていることが知られています。



C#(正確にはCLR)では、固定サイズ型(まれな注意事項を含む)を扱っており、サポートされているターゲットプラットフォームのJITコンパイラーでコードをコンパイルできます(特に明記されていない限り)。



C ++の世界では、すべてが完全に異なります。異なるプラットフォーム(hi、size_t)にコンパイルするとき、同じ型は異なるサイズを持つことができ、コードは異なるプラットフォーム、オペレーティングシステム、その他の喜びに対して異なる方法で生成されます。



カットの下で、これらの機能で友達を作ろうとします。



アンマネージライブラリがマネージアプリケーションに接続されているアンマネージ(ネイティブ、アンマネージ)コードとマネージ(マネージ)の相互作用には、プラットフォーム呼び出し(p /呼び出し)メカニズムがあります。 この相互作用はインプロセスとして分類されます。

次の制限があります。





もちろん、このリストは不完全ですが、何が起こっているのかがわかります。



p / Invokeの操作のすべての側面を考慮するわけではありませんが、p / Invokeが異なるアーキテクチャ(x86とx64など)で呼び出しの問題を解決する方法のみに焦点を当てますが、他のアーキテクチャとオペレーティングシステムには触れません。この記事で説明する内容は、理論をさらに発展させるために理論的には十分です。 この宿題は、それを必要とする人のために検討します。



それでは、ボールをスピンしましょう。



C ++のアンマネージライブラリから特定の関数セットをインポートしてC#コードから呼び出す必要がありますが、2つのアーキテクチャx86とx64を同時にサポートする必要があり、C#ホストアプリケーションが実行されるプラットフォームに応じてそれらを選択します。



MS Visual Studio 2015 Community Editionを例として使用していますが、他のツールを使用して開発する場合はすべて機能するはずです。 CMakeと他の喜び(今のところ)は気にしません。



進化プロセスのソースコードは、リンクを介してgithubで入手できます。



2つのプロジェクト(C#のコンソールアプリケーションタイプのCrossPlatformInteropとWin32プロジェクト/ DLLタイプのCrossPlatformLibrary)でソリューションを作成した後、出力ディレクトリが$(SolutionDir)Output \ $(Configuration)\になるように構成し、C ++の場合、収集するファイルのプロジェクト名は$(ProjectName)-$(PlatformShortName).dll。x86とx64で異なるファイルを取得します。



構成結果は、リポジトリのproject-setupブランチで表示できます。



C ++で簡単な関数を実装します。これは2つの数字を受け取り、文字列をフォーマットし、コールバック関数を介してマネージコードに渡すという形で暴力的なアクティビティをシミュレートします。



// header typedef void(__stdcall* Notification)(const char*); int32_t CROSSPLATFORMLIBRARY_API __stdcall ProcessData(int32_t start, int32_t count, Notification notification);
      
      





ソースコード
 // source int32_t __stdcall ProcessData(int32_t start, int32_t count, Notification notification) { if (notification == nullptr) { return 0; } int32_t result = 0; for (int32_t i = 0; i < count; ++i) { char buffer[64]; result += sprintf_s(buffer, "Notification %d from C++", i + start); notification(buffer); Sleep(rand() % 500 + 500); } return result; }
      
      





ここでは、データ型のサイズと呼び出し規則が明示的に示されていることに注意してください。 別の言語とやり取りするため、これを知る必要があり、移植可能なC ++コードを記述するための規則はここでは機能しません。 しかし、size_tのような型とは異なり、C#のどの型がそれに対応する固定サイズであるかを常に知っています。



ここには微妙な点が1つあります。C++ではvoid *またはT *のように見えるポインターは、プラットフォームごとにサイズが異なりますが、C#側からは可変サイズの特殊な型IntPtrに変換されます。 そのため、ポインターのマーシャリングにより、コンパイラーが役立ちます。



コンパイラが名前で動作する場合、コンパイラは名前を変換し、オブジェクトのタイプ、引数、戻り値、呼び出し規約などをエンコードします。 この操作は装飾(マングリング)と呼ばれます。 関数名は、MicrosoftコンパイラーによってProcessData @@ YGHHHP6GXPBD @ Z @ ZまたはProcessData @@ YAHHHP6AXPEBD @ Z @ Zの形式に変換されます(1つの違いを見つけます-ポインターのサイズによって異なります)。 結局、リンカがC ++プロジェクトで誓ったとき、このようなものを見ましたか?



このような名前で作業するのは不便です。そのため、外部プログラムインターフェイスのコンパイラに、 extern "C"



関数を宣言に追加することで、より読みやすくするように依頼します。 __cdecl呼び出し規約を使用する場合、質問はありませんが、__ stdcallを使用する場合、名前は「通常」になりませんが、x86の場合は_ProcessData @ 12のようになります(犬の後にスタックで占有されるバイト数が示されます)。 もちろん、プロジェクトでdefファイルを作成し、そこにエクスポートする関数のリストを指定できますが、それは行いません。



Windowsではライブラリを操作するときにこの規則を使用するのが一般的であるため、__ stdcallを使用します。



さらに、この関数をインポートするには、次のコードを記述するだけで十分です。



  public class LibraryImport { [UnmanagedFunctionPointer(CallingConvention.StdCall, CharSet=CharSet.Ansi)] public delegate void Notification(string value); [DllImport("CrossPlatformLibrary-x86", CallingConvention=CallingConvention.StdCall)] public static extern int ProcessData(int start, int count, Notification notification); }
      
      





使用法は次のようになります。



 LibraryImport.ProcessData(1, 10, s => Console.WriteLine(s));
      
      





しかし、コードが64ビット環境で実行されると、クラスをロードするときにBadImageFormatExceptionが発生します。つまり、互換性のない形式のライブラリのイメージをロードしようとします。 画像に互換性がない理由を説明する必要がないことを願っています。 32ビット環境から64ビットライブラリをインポートすると、同じ問題が発生します。



もちろん、21世紀の20年を急速に完了し、32ビットシステムを埋める時が来たと言えますが、32ビットシステムを搭載したWindowsにタブレットがあり、仕事中の古い鉄の公園で、32キューボールも回転します。 そして一般的に、このアプローチは公平であり、他のプロセッサアーキテクチャへの移行になります(ARMおよびその他のバイカルがdotnerで完全にサポートされる幸せな瞬間を見るために生きますか?)。



このコードには別の問題がありますが、後で分析します。



それでは、完全なインポートを始めましょう。 .NETでの型の読み込みが遅延しているという事実に注意してください。つまり、ランタイムがクラスを必要とするまで、そのクラスは解析およびコンパイルされません。 つまり、タイプを参照しない場合、誤ったライブラリのインポートが存在する可能性があります。



最初に行うことは、インポートされたメソッドをpr索好きな目から隠すことです。 一般に、内側のキッチンから何かを突き出しすぎるのは悪いことです。 インポートしたメソッドをプライベートにし、外側にラッパーメソッドを提供します。 メソッドのリストをインターフェースに入れます。



ソースコード
  [UnmanagedFunctionPointer(CallingConvention.StdCall, CharSet = CharSet.Ansi)] public delegate void Notification(string value); public interface ILibraryImport { int ProcessData(int start, int count, Notification notification); } internal class LibraryImport_x86 : ILibraryImport { [DllImport("CrossPlatformLibrary-x86", CallingConvention = CallingConvention.StdCall, ExactSpelling = false, EntryPoint = "_ProcessData@12")] private static extern int ProcessDataInternal(int start, int count, Notification notification); public int ProcessData(int start, int count, Notification notification) { return ProcessDataInternal(start, count, notification); } } internal class LibraryImport_x64 : ILibraryImport { [DllImport("CrossPlatformLibrary-x64", CallingConvention = CallingConvention.StdCall, ExactSpelling = false, EntryPoint = "ProcessData")] private static extern int ProcessDataInternal(int start, int count, Notification notification); public int ProcessData(int start, int count, Notification notification) { return ProcessDataInternal(start, count, notification); } }
      
      





クラスは内部として宣言されており、インターフェイスはパブリックであることに注意してください。 もちろん、無駄なことではありませんが、アプリケーションとは別にラッパーライブラリを作成しませんでしたが、まあ、アイデアは明確でなければなりません。



次に、現在のランタイムのビット数に応じて、目的のクラスのインスタンスを提供するローダーを作成する必要があります。



ソースコード
  public static class LibraryImport { public static ILibraryImport Select() { if (IntPtr.Size == 4) // 32-bit application { return new LibraryImport_x86(); } else // 64-bit application { return new LibraryImport_x64(); } } }
      
      







ここでは、2つのオプションしか選択できないと仮定しています。 一般的な場合、これはどのクラスをどの実装に使用するかについての決定が行われる場所です。 そして、ここで他の多くの奇妙なことをすることができます。



使用はすでに非常に簡単です:



  class Program { static void Main(string[] args) { ILibraryImport import = LibraryImport.Select(); import.ProcessData(1, 10, s => Console.WriteLine(s)); } }
      
      





提案されたソリューションには大きな欠点があります。かなり大量のステレオタイプコードの複製です。インポートされた各関数のシグネチャは5箇所に存在し、3つの異なる方法で使用されます。 そして、ネイティブコード内のいくつかの場所。 残念ながら、コード生成を除き、オプションの数を最小限に抑える方法は思いつきませんでした。 しかし、もしあなたがもっとエレガントな何かをすることができたら、私は書きます。



このコードに関する別の問題を指摘することを約束しました。 議論中のトピックに直接関係するわけではありませんが、私はまだ言及したいと思います。 しかし、ネタバレの下。



<spoiler title = "ポインターのハングの問題>呼び出しが非同期であると仮定します。たとえば、ボタンをクリックすると、作業プロトコルやその他の有用なものを生成するコードをバックグラウンドで実行しますが、ボタンハンドラーは終了し、したがって、ガベージコレクターによってすべてのローカルオブジェクトを収集できます。 そして、このようなオブジェクトと非常に重要なオブジェクトがあります。コールバック関数をカプセル化するデリゲートです。 任意の期間の後、コードは単純に、nullポインターまたはさらに悪いことに任意のメモリ領域にアクセスするという理解できないエラーでクラッシュします。 そして、すべては、アンマネージコード内の関数ポインターがまだ生きており、それが参照するデリゲートがもう存在しないため、おそらくメモリがクリアされ、ハンギングポインターがあります。



これを防ぐには、オブジェクトにフィールドとして保存するか、状況に応じて他の方法でデリゲートの寿命を延ばす必要があります。



この場合、アンマネージスレッドを停止する方法を検討する必要があります。



今日は以上です。 これが誰かに役立つことを願っています。



All Articles