Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Parameterising DllImport for use in a C# application

We have a supplier who provides a library for access to their hardware. Unfortunately, if you have multiple devices, you need to import their library multiple times, with different dll names. As a consequence, we have a metric ton of duplicated code, and I'm worried that it will soon become be a maintenance nightmare.

What we have at the moment is somthing like:

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

If I were using C or C++, I would just define the functions one file and #include them multiple times in the static classes, not pretty but better than the alternative, but in C# I don't have that option.

If anyone has any clever ideas about how to effectively define a factory which would allow us to generate as many static device classes as we need, I would be very interested.

Thanks,

Edit: The function prototypes are quite videly varied, so any method which relies on them being the same wouldn't be suitable. Thanks for the suggestions so far, I wasn't expacting so many ideas quite so quickly.

like image 877
Mark Booth Avatar asked Nov 02 '09 11:11

Mark Booth


2 Answers

Just some considerations:

Alternative #one

EDIT: this approach requires changing compiled methods, which is hard and requires injection, assembly modification or other methods that are commonly used in AOP-land. Consider approach two below, which is easier.

  1. Remove all functions with the same signature, leave one of each
  2. Use GetIlAsByteArray to create a dynamic method of your DllImport method
  3. Use the technique described here to manipulate the IL of the function, here you can change the DllImport attributes etc.
  4. Create a delegate of these functions and cache your calls
  5. Return the delegate

Alternative #two:

EDIT: This alternative approach seems a bit involved at first, but someone already did the work for you. Look up this excellent CodeProject article and simply download and use its code to dynamically create DllImport style methods. Basically, it comes down to:

  1. Remove all DllImport
  2. Create your own DllImport wrapper: takes a dll name and a function name (assuming all signatures are equal)
  3. The wrapper does a "manual" DllImport with LoadLibrary or LoadLibraryEx using the dllimport API functions
  4. The wrapper creates a method for you with MethodBuilder.
  5. Returns a delegate to that method you can use as a function.

Alternative #three

EDIT: looking further, there's an easier approach: simply use DefinePInvokeMethod which does all you need. The MSDN link already gives a good example, but a full wrapper that can create any Native DLL based on DLL and function name is provided at this CodeProject article.

  1. Remove all your DllImport style signatures
  2. Create a simple wrapper method around DefinePInvokeMethod
  3. Make sure to add simple caching (dictionary?) to prevent building the whole method on each call
  4. Return a delegate from the wrapper.

Here's how this approach looks in code, you can reuse the returned delegate as much as you like, the costly building of the dynamic method should be done only once per method.

EDIT: updated the code sample to work with any delegate and to automatically reflect the correct return type and parameter types from the delegate signature. This way, we decoupled the implementation completely from the signature, which is, given your current situation, the best we can do. Advantages: you have type safety and single-point-of-change, which means: very easily manageable.

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

Other approaches are possible, I guess (like the templating approach mentioned by someone else in this thread).

Update: added a link to excellent codeproject article.
Update: third and way easier approach added.
Update: added code sample
Update: updated code sample to work seamlessly with any function prototype
Update: fixed dreadful error: typeof(Function02) should be typeof(T) of course

like image 138
10 revs Avatar answered Nov 15 '22 17:11

10 revs


How about using T4 (Text Template Transformation Toolkit). Create a .tt file with the following content:

<#@ 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 will then convert this into:

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

    }
}
like image 28
Mark Heath Avatar answered Nov 15 '22 18:11

Mark Heath