.NETプログラムでのアンマネージC ++コードの再使用

.NET C ++



約1年前、COMライブラリ、C ++ / CLIなどを登録せずに、.NETプログラムから純粋なC ++で記述されたクラスのメソッドを呼び出す方法に関する記事書きました



今日は、別の非常に便利な独自のアプローチについてお話しします。さらに、このトピックは、すばらしいツールReflection.Emitについて詳しく知りたいすべてのhabretchikiにとって興味深いものになります(私の考えでは、このトピックはハブで十分にカバーされていません)。





例として、いくつかのC ++クラスとテストフォームを持つ同じ古いアプリケーションを見みましょう。



それでは始めましょう。 まず、古い例を取り上げて、何らかの方法でCOMに関連するすべてのゴミを削除しましょう。 さらに、HRESULT、GUID、およびその他の悪霊を、存在の有無に関わらず除去します。 コードはすぐにほぼ半分になりました:)代わりに、オブジェクトを解放するDisposeメソッドを1つ追加します。



したがって、最も単純なC ++クラスは次のようになります。



class CHello

{

public :

LIBCALL Dispose()

{

delete this ;

}



LIBCALL SetName(LPWSTR AName)

{

mName = AName;

}



LIBCALL Say(HWND AParent)

{

wstring message = L "Hello, my friend " + mName;

MessageBox(AParent, message.c_str(), L "From C++" , MB_ICONINFORMATION);

}



private :

wstring mName;

};










ここでLIBCALL == virtual void __stdcall



C#の部分に移りましょう。 最初に行う必要があるのは、エクスポートされたC ++クラスを記述するインターフェイスを宣言することです(ここで関数を宣言する順序を維持することが重要です)。

[InvokerObject(EObjectType.Hello)]

public interface IHello : IDisposable

{

void SetName([MarshalAs(UnmanagedType.LPWStr)] string AName);

void Say( IntPtr AParent);

}










インターフェイスがIDisposableを継承しているため、オブジェクトの最初のメソッドは実際にはDisposeメソッドになります。 InvokerObject属性について詳しく説明します。



C#プログラムでC ++オブジェクトを使用するには、次のように記述します



IHello hello = Invoker.Create<IHello>();

hello.SetName(txtName.Text);

hello.Say(Handle);










本当に効く



コードを超えて





これで、最も興味深いものに移ることができます。すべてが内部からどのように機能するかです。 今日のプログラムの主役は、CILオペコードCalliです。 このオペコードを使用すると、指定された一連の引数を使用して、任意のマシンアドレスで関数を呼び出すことができます。 私たちのvraperのすべての仕事が構築されるのは彼の上です。



アンマネージ関数を呼び出すように設計されたラッパーオブジェクトの作成に関するすべての作業は、Invokerクラスによって行われます。 ここでは完全なコードを提供するのではなく、そのアイデアと作業の原則についてのみ説明します(興味のある人は誰でもアーカイブの最後にサンプルの完全なソースコードをダウンロードできます)。



Invokerクラスのアルゴリズムは次のとおりです。

  1. 動的アセンブリを作成します( AppDomain.CurrentDomain.DefineDynamicAssembly
  2. このアセンブリでは、指定されたインターフェイスから継承されたクラスを作成します
  3. UnmanagedクラスへのIntPtrポインターを受け入れるクラスでコンストラクターを作成し、インターフェイスのメソッドの数に応じて、必要な数の関数アドレスを仮想メソッドテーブルから読み取ります。
  4. インターフェイスのすべてのメソッドを調べます
  5. メソッドごとに、Calliを使用して、目的の引数セットを取り、C ++クラスの最終メソッドを呼び出す独自の関数を作成します。




ハラチトリーが本質を理解しやすくするために、このクラスを作成する機能のコメントコードをここに示します(ステップ2および3)。

// (+1 .. Dispose)

int k = InterfaceType.GetMethods().Count() + 1;



// unmanaged

typeBuilder = InvokerDynamicAssembly.Instance.Builder.DefineType(TypeName, TypeAttributes.Class | TypeAttributes.Public);

//

typeBuilder.AddInterfaceImplementation(InterfaceType);



//

ptrThis = typeBuilder.DefineField( "ptr" , typeof ( IntPtr ), FieldAttributes.Private);

methods = typeBuilder.DefineField( "methods" , typeof ( IntPtr []), FieldAttributes.Private);

vtbl = typeBuilder.DefineField( "vtbl" , typeof ( IntPtr ), FieldAttributes.Private);



// , VTBL unmanaged

ConstructorBuilder constructorBuilder = typeBuilder.DefineConstructor(MethodAttributes.Public, CallingConventions.Standard, new Type[] { typeof (System. IntPtr ) });

constructorBuilder.DefineParameter(1, ParameterAttributes.In, "Pointer" );

ILGenerator ctorGen = constructorBuilder.GetILGenerator();



//

ctorGen.Emit(OpCodes.Ldarg_0);

ctorGen.Emit(OpCodes.Call, ObjectCtorInfo);



// ptrThis

ctorGen.Emit(OpCodes.Ldarg_0);

ctorGen.Emit(OpCodes.Ldarg_1);

ctorGen.Emit(OpCodes.Stfld, ptrThis);



//

ctorGen.Emit(OpCodes.Ldarg_0);

ctorGen.Emit(OpCodes.Ldarg_1);

ctorGen.Emit(OpCodes.Call, ReadIntPtr); // Marshal.ReadIntPtr

ctorGen.Emit(OpCodes.Stfld, vtbl);



ctorGen.Emit(OpCodes.Ldarg_0);

ctorGen.Emit(OpCodes.Ldc_I4_S, k);

ctorGen.Emit(OpCodes.Newarr, typeof ( IntPtr ));

ctorGen.Emit(OpCodes.Stfld, methods);



//

for ( int i = 0; i < k; i++)

{

ctorGen.Emit(OpCodes.Ldarg_0);

ctorGen.Emit(OpCodes.Ldfld, methods);

ctorGen.Emit(OpCodes.Ldc_I4_S, i);



ctorGen.Emit(OpCodes.Ldarg_0);

ctorGen.Emit(OpCodes.Ldfld, vtbl);

ctorGen.Emit(OpCodes.Ldc_I4, i * IntPtr .Size);

ctorGen.Emit(OpCodes.Add);

ctorGen.Emit(OpCodes.Call, ReadIntPtr);



ctorGen.Emit(OpCodes.Stelem, typeof ( IntPtr ));

}



//

ctorGen.Emit(OpCodes.Ret);



//

AddMethods();



createdType = typeBuilder.CreateType();










次に、InvokerObjectAttributeについて説明します。 Invokerクラスの最も重要な機能:



/// <summary>

///

/// </summary>

/// <returns> </returns>

public static T Create<T>()

where T : class , IDisposable

{

object [] attr = typeof (T).GetCustomAttributes( true );

foreach ( var it in attr)

{

if (it is InvokerObjectAttribute)

{

var objectType = (it as InvokerObjectAttribute).ObjectType;



Invoker inv = new Invoker();

inv.InterfaceType = typeof (T);

inv.Pointer = Lib.CreateObject(objectType);

inv.InitializeType();

return inv.CreateInstance() as T;

}

}

return null ;

}








ここで、InvokerObjectAttributeを使用して作成する必要があるオブジェクトを見つけ、P \ Invoke Lib呼び出しを使用して、CreateObjectを使用して、C ++ライブラリに目的のタイプの新しいアンマネージオブジェクトを作成し、ポインターを返します。



特に興味深いのは、インターフェイスへの最初のアクセスでのみ動的アセンブリ内で新しい型を宣言し、後で既存の型を使用して新しいポインターを渡すことができることです。 次のように機能します。



private void InitializeType()

{

if (InvokerDynamicAssembly.Instance.HasType(TypeName))

createdType = InvokerDynamicAssembly.Instance.GetType(TypeName);

else

CreateType();

}










明らかに、Invokerの主なタスクは、アンマネージ関数の呼び出しをラップするメソッドを作成することです。 Sayメソッドのラッパーは次のようになります。

.method public hidebysig virtual

instance void Say (

native int ''

) cil managed

{

// Method begins at RVA 0x2164

// Code size 29 (0x1d)

.maxstack 4



IL_0000: ldarg.0

IL_0001: ldfld native int InvokerDynamicAssembly.net2c.IHello::ptr

IL_0006: ldarg.1

IL_0007: ldarg.0

IL_0008: ldfld native int [] InvokerDynamicAssembly.net2c.IHello::methods

IL_000d: ldc.i4.s 2

IL_000f: nop

IL_0010: nop

IL_0011: nop

IL_0012: ldelem.any [mscorlib]System. IntPtr

IL_0017: calli System.Void(System. IntPtr ,System. IntPtr )

IL_001c: ret

}








ご覧のとおり、入力に渡された引数を受け取り、それらを使用してアンマネージオブジェクトの仮想メソッドを呼び出します。



ILスパイライブ



アプローチの利点



1. COM Interopよりも約1.6倍速く動作します。 私はC ++ / CLIと比較しませんでした、希望する人はテストして購読を解除できます。



2. MTA \ STAからリンク解除します。 COM相互運用機能は、スレッドアパートメント状態を厳密に遵守する必要がありますが、非常に頻繁に(ほとんどの場合、私の記憶では)プログラマを支援する代わりに、C#でオブジェクトを操作する際に多くの不必要な困難が生じます。 この方法には、この欠点がまったくありません。 彼はストリームへのバインディングを実行しません。



3.使いやすさおよび少量の必要なコードの点で優れています(もちろん、一度だけ書かれたInvokerクラスはカウントしません)。



主な短所のうち-MarshalAs属性を使用して正しいマーシャリングを行う方法がわかりませんでした(完全な手動実装のオプションを除く)。 そのため、現時点では(Unicode表現を提供するために)文字列型に対してのみ個別のマーシャリングが実装され、他の型はデフォルトでマーシャリングされます。 個人的には、これは私にとって特別な問題ではありません。アンマネージコードでは、厳密なパフォーマンスの最適化を必要とするアルゴリズムのみを出すため、原則として、最も単純なタイプ(ポインターと数値)の十分なパラメーターがあります。 しかし、誰かがこの問題の適切な解決策を教えてくれた場合、私は非常に感謝します。私はかなり長い間このタスクについて困惑してきたからです。



PS C#でプログラムを作成しているが、まだ伝聞だけでILに精通している場合は、トピックの短い紹介を読むことをお勧めします: www.wasm.ru/series.php?sid=22



PPSはい、約束したとおり、ほとんどすべての不名誉の完全なソースを添付することを忘れていました: public.66bit.ru/files/2011.10.07/7458c3ca96a602091fe049117974fab4/Net2C.rar



All Articles