Войти в систему

Home
    - Создать дневник
    - Написать в дневник
       - Подробный режим

LJ.Rossia.org
    - Новости сайта
    - Общие настройки
    - Sitemap
    - Оплата
    - ljr-fif

Редактировать...
    - Настройки
    - Список друзей
    - Дневник
    - Картинки
    - Пароль
    - Вид дневника

Сообщества

Настроить S2

Помощь
    - Забыли пароль?
    - FAQ
    - Тех. поддержка



Пишет ringill ([info]ringill)
@ 2006-12-08 16:03:00


Previous Entry  Add to memories!  Tell a Friend!  Next Entry
[.NET] Вызов низкоуровневого кода из CLR
Речь идёт о вызове низкоуровневой функции, принадлежащей динамической библиотеке (DLL), из управляемого кода (на C#, например). Есть несколько способов это сделать:

  1. 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 уже быстро, поэтому всё последующее можно не читать.



  2. Чтобы вызывать функцию из 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);
    }
    

  3. Указанный код для работы со стеком можно конструировать на этапе выполнения, пользуясь
    механизмами 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 сек.




Вызовы двумя последними способами происходят быстрее в десять раз.
Чем они отличаются между собой? Первый лучше тем, что не требует добавлять в проект отдельную сборку.


(Читать комментарии) - (Добавить комментарий)


[info]aamonster@lj
2008-03-24 04:26 (ссылка)
Интересно. На аамонстер-псина-мыл-ру.
А то вариант с делегатом из первоначального поста мне так и не удалось скомпилить/запустить.

(Ответить) (Уровень выше) (Ветвь дискуссии)


[info]ringill@lj
2008-03-24 07:13 (ссылка)
Выложил, так сказать, в публичный доступ:

http://ringill.googlepages.com/clrtest.zip
Рассчитано на сборку в конфигурации Release.

А в приведённом коде у меня была ошибка: вместо

il_generator.Emit(OpCodes.Ldarg, 1);
il_generator.Emit(OpCodes.Ldarg, 2);

надо

il_generator.Emit(OpCodes.Ldarg, 0);
il_generator.Emit(OpCodes.Ldarg, 1);


Прошу прощения.

(Ответить) (Уровень выше)


(Читать комментарии) -