C# アプリケーションで使用するための DllImport のパラメータ化
質問
ハードウェアにアクセスするためのライブラリを提供するサプライヤーがいます。残念ながら、複数のデバイスがある場合は、異なる dll 名でライブラリを複数回インポートする必要があります。その結果、膨大な量の重複コードが存在し、すぐにメンテナンスの悪夢になるのではないかと心配しています。
現時点で私たちが持っているものは次のようなものです:
namespace MyNamespace {
public static class Device01 {
public const string DLL_NAME = @"Device01.dll";
[DllImport(DLL_NAME, EntryPoint = "_function1")]
public static extern int Function1(byte[] param);
...
[DllImport(DLL_NAME, EntryPoint = "_function99")]
public static extern int Function99(int param);
}
....
public static class Device16 {
public const string DLL_NAME = @"Device16.dll";
[DllImport(DLL_NAME, EntryPoint = "_function1")]
public static extern int Function1(byte[] param);
...
[DllImport(DLL_NAME, EntryPoint = "_function99")]
public static extern int Function99(int param);
}
}
C または C++ を使用している場合は、1 つのファイルで関数を定義し、静的クラスで複数回 #include するだけです。きれいではありませんが、代替手段よりは優れていますが、C# にはそのオプションがありません。
必要な数の静的デバイス クラスを生成できるファクトリを効果的に定義する方法について、誰かが賢いアイデアを持っているなら、私は非常に興味があります。
ありがとう、
編集:関数プロトタイプは非常に多様であるため、それらが同じであることに依存する方法は適切ではありません。これまでの提案に感謝します。これほど多くのアイデアをすぐに思いつくとは思っていませんでした。
解決
いくつかの考慮事項:
代替 #1
編集:このアプローチでは、コンパイルされたメソッドを変更する必要がありますが、これは困難であり、注入、アセンブリの変更、または AOP ランドで一般的に使用されるその他のメソッドが必要です。より簡単な以下の 2 つのアプローチを検討してください。
- 同じシグネチャを持つすべての関数を削除し、それぞれ 1 つずつ残します
- 使用
GetIlAsByteArray
の動的メソッドを作成するにはDllImport
方法 - 使用 ここで説明されているテクニック 関数の IL を操作するには、ここで DllImport 属性などを変更できます。
- これらの関数のデリゲートを作成し、呼び出しをキャッシュします
- デリゲートを返す
代替案 2:
編集:この代替アプローチは、最初は少し複雑に思えますが、すでに誰かが作業を行ってくれています。見上げる この優れた CodeProject 記事 そしてそのコードをダウンロードして使用するだけで、DllImport スタイルのメソッドを動的に作成できます。基本的には次のようになります。
- すべての DllImport を削除します
- 独自の DllImport ラッパーを作成します。DLL 名と関数名を受け取ります (すべての署名が等しいと仮定します)。
- ラッパーは「手動」DllImport を実行します。
LoadLibrary
またはLoadLibraryEx
dllimport API 関数の使用 - ラッパーはメソッドを作成します。
MethodBuilder
. - 関数として使用できるメソッドへのデリゲートを返します。
代替案 #3
編集:さらに詳しく見てみると、もっと簡単なアプローチがあります。単純に使用する DefinePInvokeMethod
これで必要なことはすべて完了します。MSDN リンクにはすでに良い例が示されていますが、DLL と関数名に基づいてネイティブ DLL を作成できる完全なラッパーは次の場所で提供されています。 このコードプロジェクトの記事.
- DllImport スタイルの署名をすべて削除します。
- 単純なラッパー メソッドを作成する
DefinePInvokeMethod
- 呼び出しごとにメソッド全体が構築されないように、単純なキャッシュ (辞書?) を必ず追加してください。
- ラッパーからデリゲートを返します。
このアプローチがコードでどのように見えるかは次のとおりです。返されたデリゲートは好きなだけ再利用できます。コストのかかる動的メソッドの構築はメソッドごとに 1 回だけ行う必要があります。
編集:任意のデリゲートで動作し、デリゲートの署名から正しい戻り値の型とパラメーターの型を自動的に反映するようにコード サンプルを更新しました。このようにして、実装を署名から完全に分離しました。これが、現在の状況を考慮すると、私たちができる最善の方法です。利点:型安全性と単一変更点が備わっています。これは次のことを意味します。非常に簡単に管理できます。
// expand this list to contain all your variants
// this is basically all you need to adjust (!!!)
public delegate int Function01(byte[] b);
public delegate int Function02();
public delegate void Function03();
public delegate double Function04(int p, byte b, short s);
// TODO: add some typical error handling
public T CreateDynamicDllInvoke<T>(string functionName, string library)
{
// create in-memory assembly, module and type
AssemblyBuilder assemblyBuilder = AppDomain.CurrentDomain.DefineDynamicAssembly(
new AssemblyName("DynamicDllInvoke"),
AssemblyBuilderAccess.Run);
ModuleBuilder modBuilder = assemblyBuilder.DefineDynamicModule("DynamicDllModule");
// note: without TypeBuilder, you can create global functions
// on the module level, but you cannot create delegates to them
TypeBuilder typeBuilder = modBuilder.DefineType(
"DynamicDllInvokeType",
TypeAttributes.Public | TypeAttributes.UnicodeClass);
// get params from delegate dynamically (!), trick from Eric Lippert
MethodInfo delegateMI = typeof(T).GetMethod("Invoke");
Type[] delegateParams = (from param in delegateMI.GetParameters()
select param.ParameterType).ToArray();
// automatically create the correct signagure for PInvoke
MethodBuilder methodBuilder = typeBuilder.DefinePInvokeMethod(
functionName,
library,
MethodAttributes.Public |
MethodAttributes.Static |
MethodAttributes.PinvokeImpl,
CallingConventions.Standard,
delegateMI.ReturnType, /* the return type */
delegateParams, /* array of parameters from delegate T */
CallingConvention.Winapi,
CharSet.Ansi);
// needed according to MSDN
methodBuilder.SetImplementationFlags(
methodBuilder.GetMethodImplementationFlags() |
MethodImplAttributes.PreserveSig);
Type dynamicType = typeBuilder.CreateType();
MethodInfo methodInfo = dynamicType.GetMethod(functionName);
// create the delegate of type T, double casting is necessary
return (T) (object) Delegate.CreateDelegate(
typeof(T),
methodInfo, true);
}
// call it as follows, simply use the appropriate delegate and the
// the rest "just works":
Function02 getTickCount = CreateDynamicDllInvoke<Function02>
("GetTickCount", "kernel32.dll");
Debug.WriteLine(getTickCount());
他のアプローチも可能だと思います (このスレッドで他の人が言及したテンプレート アプローチなど)。
アップデート: へのリンクを追加しました 優れたコードプロジェクトの記事.
アップデート: 3 番目に、より簡単なアプローチが追加されました。
アップデート: コードサンプルを追加しました
アップデート: あらゆる関数プロトタイプとシームレスに動作するようにコード サンプルを更新しました
アップデート: 恐ろしいエラーを修正しました: typeof(Function02)
あるべきです typeof(T)
もちろん
他のヒント
使ってみてはどうでしょうか T4 (テキスト テンプレート変換ツールキット)。次の内容の .tt ファイルを作成します。
<#@ template language="C#" #>
using System.Runtime.InteropServices;
namespace MyNamespace {
<# foreach(string deviceName in DeviceNames) { #>
public static class <#= deviceName #>
{
public const string DLL_NAME = @"<#= deviceName #>.dll";
<# foreach(string functionName in FunctionNames) { #>
[DllImport(DLL_NAME, EntryPoint = "<#= functionName #>")]
public static extern int <#= functionName.Substring(1) #>(byte[] param);
<# } #>
}
<# } #>
}
<#+
string[] DeviceNames = new string[] { "Device01", "Device02", "Device03" };
string[] FunctionNames = new string[] { "_function1", "_function2", "_function3" };
#>
Visual Studio はこれを次のように変換します。
using System.Runtime.InteropServices;
namespace MyNamespace {
public static class Device01
{
public const string DLL_NAME = @"Device01.dll";
[DllImport(DLL_NAME, EntryPoint = "_function1")]
public static extern int function1(byte[] param);
[DllImport(DLL_NAME, EntryPoint = "_function2")]
public static extern int function2(byte[] param);
[DllImport(DLL_NAME, EntryPoint = "_function3")]
public static extern int function3(byte[] param);
}
public static class Device02
{
public const string DLL_NAME = @"Device02.dll";
[DllImport(DLL_NAME, EntryPoint = "_function1")]
public static extern int function1(byte[] param);
[DllImport(DLL_NAME, EntryPoint = "_function2")]
public static extern int function2(byte[] param);
[DllImport(DLL_NAME, EntryPoint = "_function3")]
public static extern int function3(byte[] param);
}
public static class Device03
{
public const string DLL_NAME = @"Device03.dll";
[DllImport(DLL_NAME, EntryPoint = "_function1")]
public static extern int function1(byte[] param);
[DllImport(DLL_NAME, EntryPoint = "_function2")]
public static extern int function2(byte[] param);
[DllImport(DLL_NAME, EntryPoint = "_function3")]
public static extern int function3(byte[] param);
}
}
また、ネイティブを使用することをお勧めします LoadLibrary
そして GetProcAddress
.
後者の場合は、単に呼び出すだけです Marshal.GetDelegateForFunctionPointer
pinvoke メソッドのシグネチャと一致するデリゲート型を使用します。