ringill's Journal
 
[Most Recent Entries] [Calendar View] [Friends View]

Friday, December 8th, 2006

    Time Event
    4:03p
    [.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 сек.




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

    << Previous Day 2006/12/08
    [Calendar]
    Next Day >>

About LJ.Rossia.org