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

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

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

Спасибо,

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

Ответ 1

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

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

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

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

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

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

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

Альтернативный # три

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

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

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

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

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

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

Ответ 2

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

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

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