Параметризация DllImport для использования в приложении c#

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

то, что мы имеем на данный момент, что-то вроде:

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

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

спасибо,

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

3 ответов


просто некоторые соображения:

альтернатива #one

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

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

вариант № два:

EDIT: этот альтернативный подход кажется немного вовлеченным сначала, но кто-то уже сделал работу за вас. Искать этот отличный CodeProject статья и просто загрузите и используйте его код для динамического создания методов стиля DllImport. В основном, это сводится к:

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

альтернатива #три

EDIT: глядя дальше, есть более простой подход: просто используйте DefinePInvokeMethod который делает все, что вам нужно. Ссылка MSDN уже дает хороший пример, но полная оболочка, которая может создать любую собственную DLL на основе DLL и имени функции, предоставляется в этот CodeProject статья.

  1. удалите все подписи стиля DllImport
  2. создайте простой метод обертки вокруг DefinePInvokeMethod
  3. обязательно добавьте простое кэширование (словарь?) чтобы предотвратить построение всего метода при каждом вызове
  4. возврат делегата из оболочки.

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

EDIT: обновлен образец кода для работы с любым делегатом и автоматического отражения правильного типа возврата и типов параметров из подписи делегата. Таким образом, мы полностью отделили реализацию от подписи, что, учитывая вашу текущую ситуацию, является лучшим, что мы можем сделать. Преимущества: Вы имеете тип безопасность и одиночн-пункт -- изменения, который значит: очень легко управляемый.

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

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

обновление: добавлена ссылка отличная статья codeproject.
обновление: добавлен третий и более легкий подход.
обновление: добавил пример кода
обновление: обновленный образец кода для работы с любым прототипом функции
обновление: исправлена страшная ошибка: typeof(Function02) должно быть typeof(T) конечно!--11-->


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