|
|
[.NET] Вызов низкоуровневого кода из CLR
Речь идёт о вызове низкоуровневой функции, принадлежащей динамической библиотеке (DLL), из управляемого кода (на C#, например). Есть несколько способов это сделать:
- P/Invoke наиболее распространённый способ. Прототип вызываемой функции описывается в коде на C# с указанием директивы
DllImport , и вызывается элементарно:
[DllImport("mylib.dll")]
static public extern int MyFunc (int param1, int param2);
static void Main ()
{
int res = MyFunc(10, 20);
}
Это работает медленно. Я не знаю, почему. Если такие вызовы редки ничего страшного, но если их миллионы (а сама вызываемая функция проста), то избыточность становится заметна. UPDATE: На современной .NET уже быстро, поэтому всё последующее можно не читать.
Чтобы вызывать функцию из DLL миллионы раз, достаточно один раз получить её адрес. Процесс вызова прост: кладём аргументы на стек, кладём адрес функции на стек, делаем calli и ret . Получить адрес можно вызовом GetProcAddress , через DllImport из kernel32.dll . Медленно, но всего один раз на миллион вызовов.
Для работы со стеком напрямую можно задействовать MSIL, скомпилировав код на этом языке в отдельную сборку. Это товарищ Neil Cowburn догадался. Вот пример: делаем файл FunctionCaller.il
.assembly extern mscorlib {}
.assembly FunctionCaller {}
.class public FunctionCaller
{
.method public static int32 IntMethod_Int_Int (native int pfn, int32 param1, int32 param2)
{
.maxstack 3
ldarg.1 // для передачи по ссылке следует использовать ldarga
ldarg.2
ldarg.0
calli unmanaged stdcall int32 (int32, int32)
ret
}
}
Компилируем его командой ilasm.exe /dll FunctionCaller.il , и FunctionCaller.dll подключаем в проект как Reference. А вот код на C#:
[DllImport("kernel32.dll")]
static extern IntPtr LoadLibrary (string file_name);
[DllImport("kernel32.dll")]
static extern IntPtr GetProcAddress (IntPtr module, string proc_name);
[DllImport("kernel32.dll")]
static extern bool FreeLibrary (IntPtr module);
static void Main ()
{
IntPtr lib = LoadLibrary("mylib.dll");
IntPtr myfunc_ptr = GetProcAddress(lib, "MyFunc");
int res = FunctionCaller.IntMethod_Int_Int(myfunc_ptr, 10, 20);
FreeLibrary(lib);
}
- Указанный код для работы со стеком можно конструировать на этапе выполнения, пользуясь
механизмами System.Reflection . Полноценный пример приведён в microsoft.public.dotnet.languages.csharp. Однако, там используется вызов MethodInfo.Invoke, который хоть и быстрее, чем P/Invoke, но тоже избыточен.
Для преодоления его избыточности есть приём создание делегата с помощью CreateDelegate . Вот как это делается:
delegate int MyFuncInvoker (int param1, int param2);
static void Main ()
{
IntPtr lib = LoadLibrary("mylib.dll");
IntPtr myfunc_ptr = GetProcAddress(lib, "MyFunc");
AssemblyName assembly_name = new AssemblyName("Invokable");
AssemblyBuilder assembly_builder =
AppDomain.CurrentDomain.DefineDynamicAssembly(assembly_name, AssemblyBuilderAccess.Run);
ModuleBuilder module_builder = assembly_builder.DefineDynamicModule("CInvoker");
// тип возвращаемого значения и типы параметров нашей функции
Type return_type = typeof(int);
Type[] parameter_types = new Type[] { typeof(int), typeof(int) };
DynamicMethod dynamic_method = new DynamicMethod("Invoker",
MethodAttributes.Public | MethodAttributes.Static, CallingConventions.Standard,
return_type, parameter_types, module_builder, false);
// начинаем конструировать код, аналогичный вышеприведённому на MSIL
ILGenerator il_generator = dynamic_method.GetILGenerator();
// положить параметры на стек; опять же, для передачи по ссылке следует использовать Ldarga
il_generator.Emit(OpCodes.Ldarg, 0);
il_generator.Emit(OpCodes.Ldarg, 1);
// положить адрес функции на стек
il_generator.Emit(OpCodes.Ldc_I4, myfunc_ptr.ToInt32());
// (для 64-битных систем следует написать (OpCodes.Ldc_I8, myfunc_ptr.ToInt64())
// сделать calli и ret
il_generator.EmitCalli(OpCodes.Calli, CallingConvention.StdCall, return_type, parameter_types);
il_generator.Emit(OpCodes.Ret);
// код сконструировали; теперь создаём делегат
MyFuncInvoker myfunc_invoker = (MyFuncInvoker)dynamic_method.CreateDelegate(typeof(MyFuncInvoker));
int res = myfunc_invoker(10, 20);
FreeLibrary(lib);
} Сравнительное время миллиона вызовов пустой функции
P/Invoke
|
~5 сек.
|
Код от ILGenerator , вызванный через MethodInfo.Invoke()
|
~3.7 сек.
|
Код от ILGenerator , вызванный через DynamicMethod.CreateDelegate() |
~0.5 сек.
|
Вызов через отдельную сборку на MSIL
|
~0.5 сек.
|
Вызовы двумя последними способами происходят быстрее в десять раз. Чем они отличаются между собой? Первый лучше тем, что не требует добавлять в проект отдельную сборку.
|
|