C ++ / CLIのネイティブライブラリの.NETラッパー

翻訳者の序文



この記事は、Marcus Heegeの著書Expert C ++ / CLI:.NET for Visual C ++ Programmersの第10章の翻訳です。 この章では、ささいなケースからネイティブクラスの階層や仮想メソッドのサポートまで、ネイティブC ++クラスのラッパークラスの作成について説明します。



この翻訳のアイデアは、 「。NETのアンマネージC ++ライブラリ」の記事の後に登場しました 完全な統合 翻訳は予想よりも長くかかりましたが、おそらくここに示すアプローチはコミュニティにとっても役立つでしょう。



内容



  1. ネイティブライブラリのラッパーの作成

    1. 始める前に

      1. 別のDLLまたはネイティブライブラリプロジェクトへの統合?
      2. ネイティブライブラリのどの部分にラッパーを介してアクセスできるようにする必要がありますか?
    2. 言語相互作用
    3. クラスのラッパーの作成

      1. ネイティブ型からCLSに一致する型へのマッピング
      2. C ++例外からマネージ例外へのマッピング
      3. マネージアレイのネイティブタイプへのマッピング
      4. 他の非プリミティブ型のマッピング
      5. 継承と仮想メソッドのサポート
    4. 一般的な推奨事項

      1. ラッパーを最初から単純化する
      2. .NETの哲学を考慮に入れる
    5. おわりに




ネイティブライブラリのラッパーの作成



ネイティブライブラリにラッパーを書き込む必要がある状況は数多くあります。 コードを変更できるライブラリのラッパーを作成できます。 Win32 APIの一部をラップできますが、そのラッパーはFCLで使用できません。 サードパーティライブラリのラッパーを作成している可能性があります。 ライブラリは、静的または動的(DLL)のいずれかです。 さらに、CまたはC ++ライブラリのいずれかです。 この章には、実用的なヒント、一般的なヒント、およびいくつかの特定の問題の解決策が含まれています。



始める前に



コードを作成する前に、ライブラリの作成と使用の両方の観点から、ラッパーとその長所と短所を作成するためのさまざまなアプローチを検討する必要があります。



別のDLLまたはネイティブライブラリプロジェクトへの統合?



Visual C ++プロジェクトには、マネージコードにコンパイルされたファイルが含まれている場合があります[詳細については、本の第7章を参照してください]。 ラッパーをネイティブライブラリに統合するのは、ライブラリが1つ少なくなるため、良いアイデアのように思えるかもしれません。 さらに、ラッパーをDLLに統合する場合、クライアントアプリケーションは追加の動的ライブラリをロードする必要がありません。 DLLのロードが少ないほど、ロード時間が短くなり、必要な仮想メモリが少なくなり、元のベースアドレスにロードできないためにライブラリがメモリ内で移動する可能性が低くなります。



ただし、ラップされたライブラリにラッパーを含めることは一般的には役に立ちません。 理由をよりよく理解するには、静的ライブラリとDLLの構築を個別に検討する必要があります。



どんなに奇妙に聞こえても、マネージ型を静的ライブラリに含めることができます。 ただし、これにより、タイプアイデンティティの問題が発生しやすくなります。 型が定義されているアセンブリは、その識別子の一部です。 したがって、CLRは、型名が一致する場合でも、異なるアセンブリの2つの型を区別できます。 2つの異なるプロジェクトが静的ライブラリの同じマネージ型を使用する場合、この型は両方のアセンブリにコンパイルされます。 アセンブリは型識別子の一部であるため、同じ静的ライブラリで定義されていても、識別子が異なる2つの型を取得します。



ライブラリをロードするにはブート時にCLR 2.0が必要になるため、マネージ型をネイティブDLLに統合することもお勧めしません。 ライブラリを使用するアプリケーションがネイティブコードのみにアクセスする場合でも、ライブラリをダウンロードするには、コンピューターにCLR 2.0がインストールされ、アプリケーションが以前のバージョンのCLRをダウンロードしないことが必要です。



ネイティブライブラリのどの部分にラッパーを介してアクセスできるようにする必要がありますか?



よくあることですが、コードを書き始める前に開発者のタスクを明確に定義しておくと非常に便利です。 これは、90年代初期のプログラム開発に関する二流の本からの引用のように聞こえますが、タスク設定はネイティブライブラリのラッパーにとって特に重要です。



ネイティブライブラリのラッパーの作成を開始すると、タスクは明白に見えます。既存のライブラリが既にあり、マネージAPIがその機能をマネージコードの世界にもたらす必要があります。



ほとんどのプロジェクトでは、このような一般的な説明は完全に不十分です。 問題をより明確に理解しないと、ネイティブC ++ライブラリクラスごとにラッパーを作成する可能性があります。 ライブラリに複数の単一の中央抽象化が含まれる場合、ネイティブコードで1対1のラッパーを作成しないでください。 これにより、特定のタスクに関係のない問題を強制的に解決し、多くの未使用コードを生成します。



タスクをよりよく説明するには、問題全体を考えてください。 問題をより明確に定式化するには、次の2つの質問に答える必要があります。





原則として、これらの質問に答えることで、不要な部分をトリミングし、使用シナリオに基づいてラッパーの抽象化を追加することにより、タスクを簡素化できます。 たとえば、次のネイティブAPIを使用します。



namespace NativeLib { class CryptoAlgorithm { public: virtual void Encrypt(/* ...      ... */) = 0; virtual void Decrypt(/* ...      ... */) = 0; }; class SampleCipher : public CryptoAlgorithm { /* ...      ... */ }; class AnotherCipherAlgorithm : public CryptoAlgorithm { /* ...      ... */ }; }
      
      





このAPIは、プログラマに次の機能を提供します。





これらすべての機能をマネージラッパーに実装することは、見かけよりもはるかに複雑です。 継承サポートを実装することは特に困難です。 後で示すように、仮想メソッドのサポートには追加のプロキシクラスが必要です。これにより、実行時のオーバーヘッドが発生し、コードの作成に時間がかかります。



ただし、1つまたは2つのアルゴリズムにのみラッパーが必要になる可能性が非常に高くなります。 このAPIのラッパーが継承をサポートしていない場合、その作成は単純化されます。 この単純化により、抽象CryptoAlgorithmクラスのラッパーを作成する必要がなくなります。 仮想メソッドを使用すると、暗号化と復号化は他の方法と同じように機能します。 継承をサポートしたくないことを明確にするために、SampleCipherおよびAnotherCipherAlgorithmのラッパーをシールクラスとして宣言するだけです。



言語相互作用



.NETを作成する際の主な目標の1つは、異なる言語間の相互運用性を提供することでした。 ネイティブライブラリのラッパーを作成する場合、ライブラリを使用する開発者はC#または他の.NET言語を使用する可能性が高いため、異なる言語と対話する機能は特に重要です。 共通言語インフラストラクチャ(CLI)は、.NET仕様の基盤です(詳細については、本の第1章を参照してください)。 この仕様の重要な部分は、Common Type System(CTS)です。 すべての.NET言語は共通の型システムに基づいていますが、すべての言語がこのシステムのすべての機能をサポートしているわけではありません。



言語が相互に通信できるかどうかを明確に判断するために、CLIには共通言語仕様(CLS)が含まれています。 CLSは、.NET言語を使用する開発者と、異なる言語から使用できるライブラリ開発者との間の契約です。 CLSは、各.NET言語がサポートする必要がある機能の最小セットを定義します。 ライブラリをCLSに対応する.NET言語から使用するには、ライブラリのパブリックインターフェイスで使用される言語の機能をCLSの機能によって制限する必要があります。 ライブラリのパブリックインターフェイスは、アセンブリで定義された可視性publicを持つすべてのタイプと、public、public protectedまたはprotectedの可視性を持つそのようなタイプのすべてのメンバーを参照します。



CLSCompliantAttribute属性を使用して、タイプまたはそのメンバーを対応するCLSとして指定できます。 デフォルトでは、この属性でマークされていないタイプはCLSに準拠していないと見なされます。 この属性をアセンブリレベルで適用すると、デフォルトですべてのタイプがCLSマッチングと見なされます。 次の例は、この属性をアセンブリおよびタイプに適用する方法を示しています。



 [assembly: CLSCompliant(true)]; //    public  CLS,     namespace ManagedWrapper { public ref class SampleCipher sealed { // ... }; [CLSCompliant(false)] //        CLS public ref class AnotherCipherAlgorithm sealed { // ... }; }
      
      





CLSルール11によれば、アセンブリの外部に表示されるクラスメンバーシグネチャ(メソッド、プロパティ、フィールド、イベント)に存在するすべての型は、CLSに準拠する必要があります。 [CLSCompliant]属性を正しく適用するには、パラメータータイプがCLSメソッドと一致するかどうかを知る必要があります。 CLS準拠を判断するには、型が宣言されているアセンブリの属性と、型自体の属性を確認する必要があります。



フレームワーククラスライブラリ(FCL)もCLSCompliant属性を使用します。 mscorlibおよび他のほとんどのFCLライブラリは、アセンブリレベルで[CLSCompliant(true)]属性を使用し、CLSに一致しないタイプを[CLSCompliant(false)]属性でマークします。



mscorlibの次のプリミティブ型は、不適切なCLSとしてフラグが付けられていることに注意してください:System :: SByte、System :: UInt16、System :: UInt32、およびSystem :: UInt64。 これらの型(または同等のchar、unsigned short、unsigned int、unsigned long、およびC ++のunsigned long long型)は、CLS準拠と見なされる型メンバーの署名では使用できません。



タイプがCLS準拠と見なされる場合、特に明記されていない限り、そのすべてのメンバーもそのように見なされます。 例:



 using namespace System; [assembly: CLSCompliant(true)]; //    public  CLS,     namespace ManagedWrapper { public ref class SampleCipher sealed // SampleCipher  CLS -   { public: void M1(int); // M2    CLS,          CLS [CLSCompliant(false)] void M2(unsigned int); }; }
      
      





残念ながら、C ++ / CLIコンパイラは、CLSに対応するものとしてマークされたタイプがCLSルールに違反している場合、警告を表示しません。 タイプをCLSとしてマークするかどうかを理解するには、次の重要なCLSルールを知る必要があります。





翻訳者のメモ-intとは何ですか^
C#とは異なり、C ++ / CLIでは、パックされた値の型を明示的に指定できます。 例:

  System::Int32 value = 1; System::Object ^boxedObject = value; //   value;  object boxedObject = value;  C# System::Int32 ^boxedInt = value; //    value,     C#    
      
      









C ++クラスのラッパーの作成



C ++の型システムと.NETのCTSにはいくつかの類似点がありますが、C ++クラスのマネージラッパー型を作成すると、多くの場合、不快な驚きが生じます。 明らかに、マネージコードに類似物がないC ++機能を使用する場合、ラッパーを作成するのは困難です。 たとえば、ライブラリが多重継承を広範囲に使用している場合。 ただし、使用されるすべてのC ++機能のマネージコードに同様の構造が存在する場合でも、ラッパーAPIでのネイティブAPIの反映は明らかではない場合があります。 考えられる問題を見てみましょう。



NativeLib :: SampleCipher型のフィールドを持つマネージクラスを宣言することはできません[詳細は本の第8章にあります]。 マネージクラスのフィールドはネイティブ型へのポインターにしかなれないため、NativeLib :: SampleCipher *型のフィールドを使用する必要があります。 ネイティブクラスインスタンスは、ラッパーコンストラクターで作成し、デストラクタで破棄する必要があります。



 namespace ManagedWrapper { public ref class SampleCipher sealed { NativeLib::SampleCipher* pWrappedObject; public: SampleCipher(/*      */) { pWrappedObject = new NativeLib::SampleCipher(/*      */); } ~SampleCipher() { delete pWrappedObject; } /* ... */ }; }
      
      





デストラクタに加えて、ファイナライザを実装する価値もあります[詳細については、本の第11章を参照してください]。



ネイティブ型からCLSに一致する型へのマッピング



ラッパークラスを作成したら、.NETのクライアントコードがラップされたオブジェクトのメンバーにアクセスできるようにするメソッド、プロパティ、およびイベントを追加する必要があります。 ラッパーを.NET言語から使用するには、ラッパークラスのメンバーの署名のすべての型がCLSに準拠している必要があります。 ネイティブAPIの署名の符​​号なし整数の代わりに、通常、同じサイズの符号付き数値を使用できます。 ネイティブのポインターと参照に相当するものを選択することは、いつもほど簡単ではありません。 時々、ネイティブポインターの代わりにSystem :: IntPtrを使用できます。 この場合、マネージコードはネイティブポインターを受け取り、それをさらに呼び出すための入力パラメーターとして渡すことができます。 これは、バイナリレベルでは、System :: IntPtrがネイティブポインターと同じ構造を持っているため可能です。 その他の場合、1つ以上のパラメーターを手動で変換する必要があります。 これには時間がかかる場合がありますが、これを避けることはできません。 ラッパーのさまざまなオプションを考えてみましょう。



参照渡しのセマンティクスを持つC ++リンクまたはポインターがネイティブ関数に渡される場合、関数のラッパーで追跡リンクを使用することをお勧めします[参照 下の注]。 ネイティブ関数の形式が次のとおりだとします。



 void f(int& i);      : void fWrapper(int% i) { int j = i; f(j); i = j; }
      
      





翻訳者注-追跡リンクとは
C ++ / CLIの追跡リンクまたは追跡参照は、C#のrefおよびoutパラメーターに似ています。



ネイティブ関数を呼び出すには、intへのネイティブ参照を渡す必要があります。 この引数では、追跡リンクからネイティブリンクへの型変換がないため、手動でマーシャリングする必要があります。 intからint&への標準型変換があるため、int型のローカル変数が使用されます。これは、参照によって渡される引数のバッファーとして機能します。 ネイティブ関数を呼び出す前に、バッファーはパラメーターiとして渡された値で初期化されます。 ネイティブ関数からラッパーに戻った後、パラメーターiの値はバッファーjの変更に従って更新されます。



この例からわかるように、マネージコードとネイティブコードを切り替えるコストに加えて、ラッパーはマーシャリングタイプにCPU時間を費やすことを余儀なくされます。 後で示すように、より複雑なタイプの場合、これらのコストは大幅に高くなる可能性があります。



C#を含む他のいくつかの.NET言語は、参照によって渡される引数と値を返すためだけに使用される引数を区別することに注意してください。 参照によって渡される引数の値は、呼び出しの前に初期化する必要があります。 呼び出されたメソッドはその値を変更できますが、これを行う義務はありません。 引数が戻りのみに使用される場合、初期化されずに渡され、メソッドは値を変更または初期化する必要があります。



デフォルトでは、追跡リンクはリンクによる通過を意味すると見なされます。 引数を値を返すためだけに使用する場合は、次の例に示すように、System :: Runtime :: InteropServices名前空間のOutAttribute属性を使用します。



 void fWrapper([Out] int% i);
      
      





ネイティブ関数の引数タイプには、多くの場合、次の例のようにconst修飾子が含まれます。



 void f(int& i1, const int& i2);
      
      





const修飾子は、メソッドシグネチャのオプションの修飾子に変換されます[詳細については、本の第8章を参照してください]。 呼び出し元がconst修飾子を受け入れない場合でも、fWrapperメソッドは引き続きマネージコードから呼び出すことができます。



 void fWrapper(int% i1, const int% i2);
      
      





ネイティブ関数のパラメーターとして配列へのポインターを渡すには、追跡リンクを使用するだけでは十分ではありません。 このケースを解析するために、ネイティブSampleCipherクラスに暗号化キーを受け入れるコンストラクターがあるとします。



 namespace NativeLib { class SampleCipher : public CryptoAlgorithm { public: SampleCipher(const unsigned char* pKey, int nKeySizeInBytes); /* ...       ... */ }; }
      
      





この場合、const unsigned char *をconst unsigned char%にマップするだけでは不十分です。これは、ネイティブタイプコンストラクターに渡される暗号化キーに複数のバイトが含まれているためです。 次のアプローチを使用するのが最適です。



 namespace ManagedWrapper { public ref class SampleCipher { NativeLib::SampleCipher* pWrappedObject; public: SampleCipher(array<Byte>^ key); /* ... */ }; }
      
      





このコンストラクターでは、ネイティブコンストラクターの両方の引数(pKeyおよびnKeySizeInBytes)がマネージ配列型の単一の引数にマップされます。 これは、管理配列のサイズが実行時に決定できるため、実行できます。



このコンストラクターの実装方法は、ネイティブSampleCipherクラスの実装に依存します。 コンストラクターがpKey引数として渡されたキーの内部コピーを作成する場合、キーにピンポインターを渡すことができます。



 SampleCipher::SampleCipher(array<Byte>^ key) { if (!key) throw gcnew ArgumentNullException("key"); pin_ptr<unsigned char> pp = &key[0]; pWrappedObject = new NativeLib::SampleCipher(pp, key->Length); }
      
      





ただし、ネイティブSampleCipherクラスが次のように実装されている場合、ドッキングポインターは使用できません。



 namespace NativeLib { class SampleCipher : public CryptoAlgorithm { const unsigned char* pKey; const int nKeySizeInBytes; public: SampleCipher(const unsigned char* pKey, int nKeySizeInBytes) : pKey(pKey), nKeySizeInBytes(nKeySizeInBytes) {} /* ...         ... */ }; }
      
      





このコンストラクターは、クライアントがキーを含むメモリを解放しないことを要求し、キーへのポインターはSampleCipherクラスのインスタンスが破棄されるまで有効のままです。 ラッパーコンストラクターは、これらの要件を満たしていません。 ラッパーにはマネージ配列記述子が含まれていないため、ネイティブクラスインスタンスが破棄される前に、ガベージコレクターが配列を収集できます。 メモリが解放されないようにオブジェクトのハンドルを保存した場合でも、ガベージコレクション中に配列を移動できます。 この場合、ネイティブポインターはマネージ配列を指しません。 キーを含むメモリがガベージコレクション中に解放または移動されないように、キーをネイティブヒープにコピーする必要があります。



これを行うには、コンストラクタとマネージラッパーデストラクタの両方を変更する必要があります。 次のコードは、コンストラクタとデストラクタの可能な実装を示しています。



 public ref class SampleCipher { unsigned char* pKey; NativeLib::SampleCipher* pWrappedObject; public: SampleCipher(array<Byte>^ key) : pKey(0), pWrappedObject(0) { if (!key) throw gcnew ArgumentNullException("key"); pKey = new unsigned char[key->Length]; if (!pKey) throw gcnew OutOfMemoryException("Allocation on C++ free store failed"); try { Marshal::Copy(key, 0, IntPtr(pKey), key->Length); pWrappedObject = new NativeLib::SampleCipher(pKey, key->Length); if (!pWrappedObject) throw gcnew OutOfMemoryException("Allocation on C++ free store failed"); } catch (Object^) { delete[] pKey; throw; } } ~SampleCipher() { try { delete pWrappedObject; } finally { //  pKey,    pWrappedObject   delete[] pKey; } } /*       */ };
      
      





翻訳者注-記述子とは?
ハンドルハンドルは、マネージヒープ内のオブジェクトへの参照です。 C#の用語では、これは単なるオブジェクト参照です。



難解な問題(本の第11章で説明)を考慮しない場合、ここで提供されるコードは、例外が発生した場合でもリソースの正しい割り当てと解放を保証します。 ManagedWrapper :: SampleCipherのインスタンスの作成中にエラーが発生した場合、割り当てられたすべてのリソースが解放されます。 デストラクタは、ラップされているオブジェクトのデストラクタが例外をスローした場合でも、キーを含むネイティブ配列を解放するように実装されます。



このコードは、マネージラッパーの特徴的なオーバーヘッドも示しています。 ラップされたネイティブ関数をマネージコードから呼び出すオーバーヘッドに加えて、ネイティブタイプとマネージタイプ間のマッピングのオーバーヘッドが追加されることがよくあります。



C ++例外からマネージ例外へのマッピング



例外に耐性のあるリソース管理に加えて、マネージラッパーは、ネイティブライブラリによってスローされたC ++例外をマネージ例外にマッピングすることにも注意する必要があります。 たとえば、SampleCipherアルゴリズムが128ビットと256ビットのキーのみをサポートしているとします。 NativeLib :: SampleCipherコンストラクターは、間違ったサイズのキーが渡されるとNativeLib :: CipherExceptionをスローする可能性がありました。 C ++例外は、システム::ランタイム:: InteropServices :: SEHExceptionタイプの例外にマップされます。これは、ライブラリコンシューマにとってはあまり便利ではありません(本書の第9章で説明)。 したがって、ネイティブ例外をキャッチし、同等のデータを含むマネージ例外をスローする必要があります。



コンストラクターの例外を表示するには、次の例に示すように、関数レベルでtryブロックを使用できます。 これにより、コンストラクタの本体だけでなく、クラスメンバーの初期化中にスローされた例外をキャッチします。



 SampleCipher::SampleCipher(array<Byte>^ key) try : pKey(0), pWrappedObject(0) { ..//       } catch(NativeLib::CipherException& ex) { throw gcnew CipherException(gcnew String(ex.what())); }
      
      





この例ではクラスメンバーを初期化しても例外はスローされませんが、関数レベルでtryブロックを使用します。 この方法では、クラスに新しいメンバーを追加したり、別のクラスからSampleCipher継承を追加したりしても、例外がキャッチされます。



マネージアレイのネイティブタイプへのマッピング



コンストラクタの実装を理解したので、EncryptメソッドとDecryptメソッドを分析します。 以前、これらのメソッドの署名の指定は延期されていましたが、今では完全に提供しています:



 class CryptoAlgorithm { public: virtual void Encrypt( const unsigned char* pData, int nDataLength, unsigned char* pBuffer, int nBufferLength, int& nNumEncryptedBytes) = 0; virtual void Decrypt( const unsigned char* pData, int nDataLength, unsigned char* pBuffer, int nBufferLength, int& nNumEncryptedBytes) = 0; };
      
      





暗号化または復号化する必要があるデータは、pDataおよびnDataLengthパラメーターを使用して渡されます。 EncryptまたはDecryptを呼び出す前に、メモリバッファを割り当てる必要があります。 pBufferパラメーターの値はこのバッファーへのポインターでなければならず、そのサイズはnBufferLengthパラメーターの値として渡される必要があります。 出力データのサイズは、nNumEncryptedBytesパラメーターを使用して返されます。



暗号化と復号化を表示するには、次のメソッドをManagedWrapper :: SampleCipherに追加できます。



 namespace ManagedWrapper { public ref class SampleCipher sealed { // ... void Encrypt( array<Byte>^ data, array<Byte>^ buffer, int% nNumOutBytes) { if (!data) throw gcnew ArgumentException("data"); if (!buffer) throw gcnew ArgumentException("buffer"); pin_ptr<unsigned char> ppData = &data[0]; pin_ptr<unsigned char> ppBuffer = &buffer[0]; int temp = nNumOutBytes; pWrappedObject->Encrypt(ppData, data->Length, ppBuffer, buffer->Length, temp); nNumOutBytes = temp; } }
      
      





この実装は、NativeLib :: SampleCipher :: Encryptが非ブロッキング操作であり、妥当な時間で実行を完了することを前提としています。 そのような仮定を立てることができない場合は、ネイティブのEncryptメソッドの間、管理対象オブジェクトを固定しないでください。これを行うには、アレイをEncryptに転送する前にマネージドアレイをネイティブメモリにコピーし、暗号化されたデータをネイティブメモリからバッファマネージドアレイにコピーします。一方では、これは型のマーシャリングに追加のコストを必要とし、他方では、長期的な統合を防ぎます。



他の非プリミティブ型のマッピング



以前は、表示される関数のすべてのパラメータータイプは、プリミティブタイプ、またはポインターまたはプリミティブタイプへの参照のいずれかでした。 C ++クラスまたはポインターまたはC ++クラスへの参照をパラメーターとして受け取る関数を表示する必要がある場合、多くの場合、追加のアクションが必要です。特定の状況に応じて、ソリューションは異なる場合があります。特定の例のさまざまなソリューションを表示するには、ラッパーを必要とする別のネイティブクラスを検討します。



 class EncryptingSender { CryptoAlgorithm& cryptoAlg; public: EncryptingSender(CryptoAlgorithm& cryptoAlg) : cryptoAlg(cryptoAlg) {} void SendData(const unsigned char* pData, int nDataLength) { unsigned char* pEncryptedData = new unsigned char[nDataLength]; int nEncryptedDataLength = 0; //    cryptoAlg.Encrypt(pData, nDataLength, pEncryptedData, nDataLength, nEncryptedDataLength); SendEncryptedData(pEncryptedData, nEncryptedDataLength); } private: void SendEncryptedData(const unsigned char* pEncryptedData, int nDataLength) { /*     ,   */ } };
      
      





このクラスの名前について推測できるように、その目的は暗号化されたデータを送信することです。議論の一環として、データの送信先や使用するプロトコルは関係ありません。暗号化には、CryptoAlgorithmから継承されたクラス(SampleCipherなど)を使用できます。暗号化アルゴリズムは、タイプCryptoAlgorithm&のコンストラクターパラメーターを使用して指定できます。コンストラクターに渡されたCryptoAlgorithmクラスのインスタンスは、仮想Encryptメソッドを呼び出すときにSendDataメソッドで使用されます。次の例は、ネイティブコードでEncryptingSenderを使用する方法を示しています。



 using namespace NativeLib; unsigned char key[] = {0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15}; SampleCipher sc(key, 16); EncryptingSender sender(sc); unsigned char pData[] = { '1', '2', '3' }; sender.SendData(pData, 3);
      
      





NativeLib :: EncryptingSenderのラッパーを作成するには、マネージクラスManagedWrapper :: EncryptingSenderを定義できます。 SampleCipherクラスのラッパーと同様に、ラップされたオブジェクトへのポインターをフィールドに格納する必要があります。 EncryptingSenderラップクラスのインスタンスを作成するには、NativeLib :: CryptoAlgorithmクラスのインスタンスが必要です。サポートする暗号化アルゴリズムがSampleCipherのみであるとします。次に、配列<unsigned char> ^の値を暗号化キーとして使用するコンストラクターを定義できます。 ManagedWrapper :: SampleCipherクラスのコンストラクターと同様に、EncryptingSenderコンストラクターはこの配列を使用してネイティブクラスNativeLib :: SampleCipherをインスタンス化できます。次に、このオブジェクトへのリンクをNativeLib :: EncryptingSenderコンストラクターに渡すことができます。



 public ref class EncryptingSender { NativeLib::SampleCipher* pSampleCipher; NativeLib::EncryptingSender* pEncryptingSender; public: EncryptingSender(array<Byte>^ key) try : pSampleCipher(0), pEncryptingSender(0) { if (!key) throw gcnew ArgumentNullException("key"); pin_ptr<unsigned char> ppKey = &key[0]; pSampleCipher = new NativeLib::SampleCipher(ppKey, key->Length); if (!pSampleCipher) throw gcnew OutOfMemoryException("Allocation on C++ free store failed"); try { pEncryptingSender = new NativeLib::EncryptingSender(*pSampleCipher); if (!pEncryptingSender) throw gcnew OutOfMemoryException("Allocation on C++ free store failed"); } catch (Object^) { delete pSampleCipher; throw; } } catch(NativeLib::CipherException& ex) { throw gcnew CipherException(gcnew String(ex.what())); } // ..           };
      
      





このアプローチを使用すると、CryptoAlgorithm型のパラメーターをマネージ型にマップする必要がありません。ただし、このアプローチが制限されている場合があります。たとえば、新しいインスタンスを作成するのではなく、SampleCipherの既存のインスタンスを転送する機会を与えたいとします。これを行うには、コンストラクターManagedWrapper :: EncryptingSenderにタイプSampleCipher ^のパラメーターが必要です。コンストラクター内でNativeLib :: EncryptingSenderクラスのインスタンスを作成するには、ManagedWrapper :: SampleCipherでラップされたNativeLib :: SampleCipherクラスのオブジェクトを取得する必要があります。ラップされたオブジェクトを取得するには、新しいメソッドを追加する必要があります。



 public ref class SampleCipher sealed { unsigned char* pKey; NativeLib::SampleCipher* pWrappedObject; internal: [CLSCompliant(false)] NativeLib::SampleCipher& GetWrappedObject() { return *pWrappedObject; } ...   SampleCipher    ... };
      
      





次のコードは、このようなコンストラクターの可能な実装を示しています。

 public ref class EncryptingSender { NativeLib::EncryptingSender* pEncryptingSender; public: EncryptingSender(SampleCipher^ cipher) { if (!cipher) throw gcnew ArgumentException("cipher"); pEncryptingSender = new NativeLib::EncryptingSender(cipher->GetWrappedObject()); if (!pEncryptingSender) throw gcnew OutOfMemoryException("Allocation on C++ free store failed"); } // ...    EncryptingSender    ... };
      
      





これまでのところ、この実装では、ManagedWrapper :: SampleCipherクラスのインスタンスのみを渡すことができます。CryptoAlgorithmのラッパーの実装でEncryptingSenderを使用するには、異なるラッパーのGetWrappedObjectの実装がポリモーフィックになるように設計を変更する必要があります。これは、管理インターフェースを使用して実現できます。



 public interface class INativeCryptoAlgorithm { [CLSCompliant(false)] NativeLib::CryptoAlgorithm& GetWrappedObject(); };
      
      





このインターフェイスを実装するには、SampleCipherラッパーを次のように変更する必要があります。

 public ref class SampleCipher sealed : INativeCryptoAlgorithm { // ... internal: [CLSCompliant(false)] virtual NativeLib::CryptoAlgorithm& GetWrappedObject() = INativeCryptoAlgorithm::GetWrappedObject { return *pWrappedObject; } };
      
      





ラッパーライブラリを使用するコードはラップされたオブジェクトメソッドを直接呼び出さないため、このメソッドは内部として実装されます。クライアントにラップされたオブジェクトへの直接アクセスを許可する場合は、System :: IntPtrを使用してポインターを渡す必要があります。System:: IntPtr型はCLSに対応しているためです。



ManagedWrapper :: EncryptingSenderクラスのコンストラクターは、INativeCryptoAlgorithm ^型のパラメーターを受け入れます。暗号化されたEncryptingSenderラップインスタンスの作成に必要なNativeLib :: CryptoAlgorithmクラスのオブジェクトを取得するには、INativeCryptoAlgorithm ^型パラメーターでGetWrappedObjectメソッドを呼び出すことができます。



 EncryptingSender::EncryptingSender(INativeCryptoAlgorithm^ cipher) { if (!cipher) throw gcnew ArgumentException("cipher"); pEncryptingSender = new NativeLib::EncryptingSender(cipher->GetWrappedObject()); if (!pEncryptingSender) throw gcnew OutOfMemoryException("Allocation on C++ free store failed"); }
      
      







継承と仮想メソッドのサポート



他の暗号化アルゴリズムのラッパーを作成し、それらにINativeCryptoAlgorithmサポートを追加する場合、コンストラクターManagedWrapper :: EncryptingSenderに渡すこともできます。ただし、暗号化アルゴリズムをマネージコードに実装してEncryptingSenderに転送することはまだできません。マネージクラスでは、ネイティブクラスの仮想メソッドを単にオーバーライドすることはできないため、これには改善が必要です。これを行うには、マネージラッパークラスの実装を再度変更する必要があります。



今回は、NativeLib :: CryptoAlgorithmをラップする抽象マネージクラスが必要です。 GetWrappedObjectメソッドに加えて、このラッパークラスは2つの抽象メソッドを提供する必要があります。



 public ref class CryptoAlgorithm abstract { public protected: virtual void Encrypt( array<Byte>^ data, array<Byte>^ buffer, int% nNumOutBytes) abstract; virtual void Decrypt( array<Byte>^ data, array<Byte>^ buffer, int% nNumOutBytes) abstract; //        };
      
      





暗号化アルゴリズムを実装するには、ManagedWrapper :: CryptoAlgorithmから派生したマネージクラスを作成し、仮想メソッドEncryptおよびDecryptをオーバーライドする必要があります。ただし、これらの抽象メソッドは、仮想メソッドNativeLib :: CryptoAlgorithm EncryptおよびDecryptをオーバーライドするには不十分です。ネイティブクラスの仮想メソッド(この例ではNativeLib :: CryptoAlgorithm)は、ネイティブサクセサークラスでのみオーバーライドできます。したがって、NativeLib :: CryptoAlgorithmを継承し、必要な仮想メソッドをオーバーライドするネイティブクラスを作成する必要があります。



 class CryptoAlgorithmProxy : public NativeLib::CryptoAlgorithm { public: virtual void Encrypt( const unsigned char* pData, int nNumInBytes, unsigned char* pBuffer, int nBufferLen, int& nNumOutBytes); virtual void Decrypt( const unsigned char* pData, int nNumInBytes, unsigned char* pBuffer, int nBufferLen, int& nNumOutBytes); //        };
      
      





このクラスは、暗号化と復号化を実装するマネージクラスの仲介として機能するため、CryptoAlgorithmProxyと呼ばれます。仮想メソッドの実装は、ManagedWrapper :: CryptoAlgorithmクラスの同等の仮想メソッドを呼び出す必要があります。このため、CryptoAlgorithmProxyには、ManagedWrapper :: CryptoAlgorithmクラスのインスタンスハンドルが必要です。コンストラクターパラメーターとして渡すことができます。記述子を保存するには、gcrootテンプレートが必要です。(CryptoAlgorithmProxyはネイティブクラスであるため、記述子タイプフィールドを含めることはできません。)



 class CryptoAlgorithmProxy : public NativeLib::CryptoAlgorithm { gcroot<CryptoAlgorithm^> target; public: CryptoAlgorithmProxy(CryptoAlgorithm^ target) : target(target) {} // Encrypt  Decrypt    };
      
      





マネージクラスは、ネイティブの抽象CryptoAlgorithmクラスのラッパーとして機能する代わりに、特定のCryptoAlgorithmProxyの子孫のラッパーとして機能します。次のコードは、これを行う方法を示しています。



 public ref class CryptoAlgorithm abstract : INativeCryptoAlgorithm { CryptoAlgorithmProxy* pWrappedObject; public: CryptoAlgorithm() { pWrappedObject = new CryptoAlgorithmProxy(this); if (!pWrappedObject) throw gcnew OutOfMemoryException("Allocation on C++ free store failed"); } ~CryptoAlgorithm() { delete pWrappedObject; } internal: [CLSCompliant(false)] virtual NativeLib::CryptoAlgorithm& GetWrappedObject() = INativeCryptoAlgorithm::GetWrappedObject { return *pWrappedObject; } public protected: virtual void Encrypt( array<Byte>^ data, array<Byte>^ buffer, int% nNumEncryptedBytes) abstract; virtual void Decrypt( array<Byte>^ data, array<Byte>^ buffer, int% nNumEncryptedBytes) abstract; };
      
      





前述のように、CryptoAlgorithmProxyクラスは、管理がManagedWrapper :: CryptoAlgorithmの同等のメソッドに転送されるように仮想メソッドを実装する必要があります。次のコードは、CryptoAlgorithmProxy :: EncryptがManagedWrapper :: CryptoAlgorithm :: Encryptを呼び出す方法を示しています。



 void CryptoAlgorithmProxy::Encrypt( const unsigned char* pData, int nDataLength, unsigned char* pBuffer, int nBufferLength, int& nNumOutBytes) { array<unsigned char>^ data = gcnew array<unsigned char>(nDataLength); Marshal::Copy(IntPtr(const_cast<unsigned char*>(pData)), data, 0, nDataLength); array<unsigned char>^ buffer = gcnew array<unsigned char>(nBufferLength); target->Encrypt(data, buffer, nNumOutBytes); Marshal::Copy(buffer, 0, IntPtr(pBuffer), nBufferLength); }
      
      







一般的な推奨事項



前述の特定の手順に加えて、マネージラッパーを作成するための一般的な推奨事項も考慮する必要があります。



ラッパーを最初から単純化する



前のセクションからわかるように、クラス階層のラッパーの作成には非常に時間がかかる場合があります。マネージクラスが仮想メソッドを再定義できるような方法でC ++クラスラッパーを作成する必要がある場合がありますが、多くの場合、この実装は実用的ではありません。必要な機能を決定することは、タスクを簡素化するための鍵です。



車輪を再発明する必要はありません。いくつかのライブラリのラッパーを作成する前に、FCLに必要なメソッドを備えた既製のクラスが含まれていないことを確認してください。FCLは見た目以上のものを提供できます。たとえば、BCLにはすでにかなりの数の暗号化アルゴリズムが含まれています。これらは、システム::セキュリティ::暗号化名前空間にあります。必要な暗号化アルゴリズムが既にFCLにある場合、そのためのラッパーを再作成する必要はありません。FCLにラッパーを作成するアルゴリズムの実装が含まれていないが、アプリケーションがネイティブAPIで実装されたアルゴリズムに関連付けられていない場合、通常はFCLの標準アルゴリズムのいずれかを使用することをお勧めします。



.NETの哲学を考慮に入れる



タイプはCLSルールに従うだけでなく、可能であれば、.NETの哲学にも適合する必要があります。[型とそのメンバーを決定するためのさまざまなオプションについては、本の第5章で説明しています。]たとえば、





型システムの機能に加えて、FCLの機能も考慮してください。 FCLがセキュリティ関連のアルゴリズムを実装していることを考えると、FCLアルゴリズムの互換性を考慮してください。これを行うには、FCLで採用された設計を受け入れ、抽象基本クラスSymmetricAlgorithmからクラスを継承し、System :: Security :: Cryptography名前空間からICryptoTransformインターフェイスを実装する必要があります。



FCL設計に適応すると、通常、ライブラリコンシューマーの観点からラッパーライブラリが簡素化されます。このアプローチの複雑さは、ネイティブAPIの設計と、サポートを実装するFCLタイプの設計に依存します。これらの追加の人件費が許容できるかどうかは、それぞれの場合に個別に決定されます。この例では、セキュリティアルゴリズムは1つの特定のケースでのみ使用されると想定できるため、FCLと統合する価値はありません。



マッピングを作成するライブラリが表形式のデータを管理する場合、ADO.NETと呼ばれるFCLパーツのSystem :: Data :: DataTableおよびSystem :: Data :: DataSetクラスを検討してください。これらのタイプの検討はこのテキストの範囲を超えていますが、ラッパーの作成に適用できるため、言及に値します。



DataTableまたはDataSetで表形式のデータをラップすると、これらのデータコンテナーが.NETプログラミングで広く使用されているため、ライブラリユーザーの作業が楽になります。どちらのタイプのインスタンスもXMLまたはバイナリ形式でシリアル化でき、.NET RemotingおよびWebサービスを介して送信でき、Windowsフォーム、Windows Presentation Foundation(WPF)およびADO.NETアプリケーションのアプリケーションのデータソースとしてよく使用されます。 。どちらのタイプも、いわゆるdiffgram、データベースビューに似たビュー、およびフィルターによる変更の追跡をサポートしています。



おわりに



ネイティブライブラリの優れたラッパーを作成するには、特定の品質と機能が必要です。まず第一に、優れた建築スキルが必要です。ネイティブの1対1ライブラリをマッピングすることは、めったに良い解決策ではありません。ライブラリユーザーが必要としない機能を削除することで、タスクを大幅に簡素化できます。残りのライブラリ機能のファサードを作成するには、CLS、FCL機能、および.NETで一般的に使用されるアプローチの知識が必要です。



このように、C ++クラスのラッパーを作成するための多くのオプションがあります。ラッパーがマネージコードでクライアントに提供する機能に応じて、実装は複雑な場合も比較的単純な場合もあります。 .NETからネイティブクラスをインスタンス化して呼び出す必要がある場合、主なタスクは、ネイティブクラスのメソッドをマネージクラスのメソッドにマップして、CLSを監視することです。ネイティブ型の階層も考慮する必要がある場合、ラッピングはさらに複雑になる可能性があります。



ネイティブライブラリ上のラッパーの作成には、ネイティブリソースのラップも含まれます(たとえば、ラップされたオブジェクトの作成に必要なアンマネージメモリ)。リソースの信頼できるリリースは、別の記事[この本の第11章で説明します]のトピックです。



- 11
11 IDisposable, , (ThreadAbortException, StackOverflowException ..) SafeHandle. , , , .




All Articles