Параметризация DllImport для использования в приложении C # - PullRequest
11 голосов
/ 02 ноября 2009

У нас есть поставщик, который предоставляет библиотеку для доступа к своему оборудованию. К сожалению, если у вас есть несколько устройств, вам нужно импортировать их библиотеку несколько раз с разными именами 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 ++, я бы просто определил функции одним файлом и #include их несколько раз в статических классах, не очень, но лучше, чем альтернативные, но в C # у меня нет такой опции.

Если у кого-нибудь есть какие-нибудь умные идеи о том, как эффективно определить фабрику, которая позволила бы нам генерировать столько классов статических устройств, сколько нам нужно, я был бы очень заинтересован.

Спасибо

Редактировать: Прототипы функций довольно разнообразны, поэтому любой метод, который полагается на то же самое, не подойдет. Пока что спасибо за предложения, я не так быстро излагал столько идей.

Ответы [ 3 ]

14 голосов
/ 02 ноября 2009

Только некоторые соображения:

Альтернатива # один

РЕДАКТИРОВАТЬ: этот подход требует изменения скомпилированных методов, что является сложным и требует внедрения, модификации сборки или других методов, которые обычно используются в AOP-земле. Рассмотрим второй подход ниже, который проще.

  1. Удалите все функции с одной и той же подписью, оставьте одну из каждой
  2. Используйте GetIlAsByteArray для создания динамического метода вашего DllImport метода
  3. Используйте метод, описанный здесь для манипулирования IL функции, здесь вы можете изменить атрибуты DllImport и т. Д.
  4. Создайте делегата этих функций и кешируйте ваши звонки
  5. Вернуть делегата

Альтернатива # два:

РЕДАКТИРОВАТЬ: Этот альтернативный подход сначала кажется немного сложным, но кто-то уже сделал эту работу за вас. Посмотрите эту превосходную статью CodeProject и просто загрузите и используйте ее код для динамического создания методов стиля DllImport. В основном это сводится к:

  1. Удалить все DllImport
  2. Создайте свою собственную оболочку DllImport: принимает имя dll и имя функции (при условии, что все подписи равны)
  3. Оболочка выполняет "ручной" DllImport с LoadLibrary или LoadLibraryEx с использованием функций API dllimport
  4. Оболочка создает метод для вас с MethodBuilder.
  5. Возвращает делегата этого метода, который вы можете использовать в качестве функции.

Альтернатива № три

РЕДАКТИРОВАТЬ: если смотреть дальше, есть более простой подход: просто используйте DefinePInvokeMethod, который делает все, что вам нужно. Ссылка MSDN уже дает хороший пример, но полная обертка, которая может создать любую Собственную DLL на основе DLL и имени функции, предоставлена ​​в этой статье CodeProject .

  1. Удалите все ваши подписи в стиле DllImport
  2. Создайте простой метод обёртки вокруг DefinePInvokeMethod
  3. Обязательно добавьте простое кеширование (словарь?), Чтобы не создавать целый метод при каждом вызове
  4. Вернуть делегата из оболочки.

Вот как этот подход выглядит в коде: вы можете использовать возвращаемый делегат столько раз, сколько хотите, дорогостоящее создание динамического метода должно выполняться только один раз для метода.

РЕДАКТИРОВАТЬ: обновил образец кода для работы с любым делегатом и автоматически отражать правильный тип возврата и типы параметров из подписи делегата. Таким образом, мы полностью отделили реализацию от подписи, что, учитывая вашу текущую ситуацию, лучшее, что мы можем сделать. Преимущества: у вас есть безопасность типов и единая точка изменения, что означает: очень легко управлять.

// 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());

Возможно, возможны и другие подходы (например, подход к шаблонам, упомянутый кем-то еще в этой теме).

Обновление: добавлена ​​ссылка на отличная статья кода проекта .
Обновление: Добавлен третий и более простой подход.
Обновление: добавлен пример кода
Обновление: обновленный пример кода для бесперебойной работы с любым прототипом функции
Обновление: исправлена ​​ужасная ошибка: typeof(Function02) должно быть typeof(T) конечно

4 голосов
/ 02 ноября 2009

Как насчет использования 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);

    }
}
3 голосов
/ 02 ноября 2009

Я бы также предложил использовать нативные LoadLibrary и GetProcAddress.

В последнем случае вы просто вызываете Marshal.GetDelegateForFunctionPointer с типом делегата, который соответствует сигнатуре метода pinvoke.

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...