前言

之前研究动态dll载入,收集了一些资料,发现其实可以通过这个技术很容易实现一个dll插件框架,代码量很少,且很容易扩展,最主要是,对托管语言很友好


一、框架本体

使用.net6.0框架进行开发,仅为容器,本身并不具备多少功能,功能的实现要靠模块插件。

核心代码

1、引用托管插件核心代码

//托管dll集合
public List<Assembly> Assemblies
{
    get { return (List<Assembly>)GetValue(assemblies1Property); }
    set { SetValue(assemblies1Property, value); }
}

// Using a DependencyProperty as the backing store for assemblies1.  This enables animation, styling, binding, etc...
public static readonly DependencyProperty assemblies1Property =
    DependencyProperty.Register("Assemblies", typeof(List<Assembly>), typeof(MainWindow), null);

//获取托管dll集合的方法
static List<Assembly> GetAssemblies()
{
    List<Assembly> assemblies = new();
    var filepaths = Directory.EnumerateFiles("Modules").Where(x => x.Contains(".ft.dll"));
    if (filepaths.Any())
    {
        int errCount = 0;
        string errMsg = "";
        foreach (var filepath in filepaths)
        {
            AssemblyDependencyResolver resolver = new(filepath);
            AssemblyLoadContext assemblyLoadContext = new(Guid.NewGuid().ToString("N"), true);

            using var fs = new FileStream(filepath, FileMode.Open, FileAccess.Read);
            try
            {
                Assembly assembly = assemblyLoadContext.LoadFromStream(fs);
                assemblies.Add(assembly);
            }
            catch (Exception ex)
            {
                errCount ++;
                errMsg += ex.Message + Environment.NewLine;
                //throw;
            }
        }
        if(errCount > 0)
        {
            MessageBox.Show($"{errCount}个dll加载失败"+errMsg);
        }
    }
    return assemblies;
}

//插件调用示例
private void Button_Click_1(object sender, RoutedEventArgs e)
{
    var assembly = listBox.SelectedItem as Assembly;
    if(assembly != null)
    {
        try
        {
            var types = assembly.GetTypes();
            if (types.Length <= 0) return;
            var type = types.AsEnumerable().FirstOrDefault(x => x != null && x.Name.Contains("Window"), null);
            if(type != null)
            {
                ConstructorInfo constructor = type.GetConstructor(Type.EmptyTypes);
                object classObject = constructor.Invoke(Array.Empty<object>());

                var method = type.GetMethod("ShowUI");
                if (method != null)
                {
                    method.Invoke(classObject, null);
                }
                else
                {
                    MessageBox.Show("未找到启动入口");
                }
            }
            else
            {
                MessageBox.Show("未找到启动类");
            }
        }
        catch (Exception ex)
        {
            MessageBox.Show(ex.Message);
            //throw;
        }
    }
}

2、引用非托管插件的核心代码(方式1,需引用3F / Conari库)

using ConariL exp = new("D:\\Project\\Person\\FieldToolsModV\\FieldToolsModV\\bin\\Debug\\net6.0-windows\\Modules\\CppTest.Demo.dll");
int apiRes = exp.DLR.Sum<int>(5,6);
MessageBox.Show($"测试api1执行结果为:{apiRes}");

3、引用非托管插件的核心代码(方式2,常规)

//非托管dll调用类
public class DllInvoke
{
    /// <summary>
    /// LoadLibraryFlags
    /// </summary>
    public enum LoadLibraryFlags : uint
    {
        DONT_RESOLVE_DLL_REFERENCES = 0x00000001,

        LOAD_IGNORE_CODE_AUTHZ_LEVEL = 0x00000010,

        LOAD_LIBRARY_AS_DATAFILE = 0x00000002,

        LOAD_LIBRARY_AS_DATAFILE_EXCLUSIVE = 0x00000040,

        LOAD_LIBRARY_AS_IMAGE_RESOURCE = 0x00000020,

        LOAD_LIBRARY_SEARCH_APPLICATION_DIR = 0x00000200,

        LOAD_LIBRARY_SEARCH_DEFAULT_DIRS = 0x00001000,

        LOAD_LIBRARY_SEARCH_DLL_LOAD_DIR = 0x00000100,

        LOAD_LIBRARY_SEARCH_SYSTEM32 = 0x00000800,

        LOAD_LIBRARY_SEARCH_USER_DIRS = 0x00000400,

        LOAD_WITH_ALTERED_SEARCH_PATH = 0x00000008
        }

    /// <summary>
    /// 释放资源
    /// </summary>
    public void Dispose()
    {
        FreeLibrary(hLib);
    }

    [DllImport("kernel32.dll", SetLastError = true)]
    private static extern IntPtr LoadLibraryEx(string lpFileName, IntPtr hReservedNull, LoadLibraryFlags dwFlags);
    [DllImport("kernel32.dll")]
    private extern static IntPtr GetProcAddress(IntPtr lib, String funcName);
    [DllImport("kernel32.dll")]
    private extern static bool FreeLibrary(IntPtr lib);
    /// <summary>
    /// 模块句柄
    /// </summary>
    public IntPtr hLib;
    /// <summary>
    /// 错误代码
    /// </summary>
    public int ErrCode { get; set; } = 0;
    /// <summary>
    /// 以dll名称创建dll调用实例
    /// </summary>
    /// <param name="DllName"></param>
    public DllInvoke(String DllName)
    {
        hLib = LoadLibraryEx(DllName, IntPtr.Zero, LoadLibraryFlags.LOAD_WITH_ALTERED_SEARCH_PATH);
        if (hLib == IntPtr.Zero)
        {
            ErrCode = Marshal.GetLastWin32Error(); //只有SetLastError = true时,才能获取到Error Code
            throw new($"非托管dll实例创建失败,错误ErrCode:{ErrCode}");
        }
    }

    ~DllInvoke()
    {
        //FreeLibrary(hLib);
    }

    /// <summary>
    /// 将要执行的函数转换为委托
    /// </summary>
    /// <param name="ApiName">api名称</param>
    /// <param name="t">委托类型</param>
    /// <returns></returns>
    public Delegate Invoke(String ApiName, Type t)
    {
        if (ErrCode != 0)
        {
            throw new($"非托管dll调用失败,错误ErrCode:{ErrCode}");
        }
        IntPtr api = GetProcAddress(hLib, ApiName);
        return Marshal.GetDelegateForFunctionPointer(api, t);
    } 
}

//主窗口
public partial class MainWindow : Window
{
    public MainWindow()
    {
        this.DataContext = this;
        this.Closed += (s, e) =>
        {
            Environment.Exit(0);
        };
        InitializeComponent();
    }
    
    //窗口调用委托
    private delegate int OpenCppWin();
    //调用实现
    private async Task<int> ExampleAPI2(string path)
    {
        string dllName = path;
        DllInvoke customerDll = new DllInvoke(dllName);
        if (customerDll.hLib == IntPtr.Zero)
        {
            return -1;
        }
        OpenCppWin testApi = (OpenCppWin)customerDll.Invoke("ShowUI", typeof(OpenCppWin));
        await Task.Run(() =>
        {
            testApi();
        });
        return 0;
    }
}

二、扩展库

使用.netstand2.0,进行一些接口的定义和内置方法的编写。
使用了3F / DllExport库进行了非托管导出配置(动态dll),可使用对应版本进行使用,近似于C++的导出,后续考虑直接使用C++做动态dll。(本来计划开放dll的com互操作性,非托管语言可注册com后进行调用使用,但发现3F / DllExport只支持static的导出,通过接口实现的无法导出,额外的实现会显得更加麻烦)。

(一)IExport接口

包含插件必须要实现的方法,供框架调用。托管语言可直接继承该接口,进行方法实现,非托管语言可直接定义对应方法进行实现。

(二)Inner库

包含一些内置的方法,可直接供托管语言进行调用。暂时使用3F / DllExport进行导出。


三、模块插件编写

(一)托管语言(C#以及其他托管语言)

按照规范实现IExport接口的所有方法,可直接引用扩展库进行接口继承和预定义方法直接调用。

1、C#编写规范

推荐使用.net6进行开发。C#的使用非常简单,可以直接引用对应dll,进行实现即可。需要注意的是,为了避免程序集引用丢失,需要将所有导出dll合并为一个,可以使用第三方加密/加壳工具,也可在项目中引用Costura.Fody程序包(nuget),输出时候会自动合并。

2、其他托管语言编写规范

F#、VB等与C#类似。
C++实际上可以直接使用C#导出托管dll,但有一个问题无法解决,就是C#无法调用使用了CLR托管dll的C++导出dll方法,一旦调用就会崩溃,我相信有解决方法,但中文社区一点资料都查不到,后面有时间了一定会专门花时间去了解一下。

(二)非托管语言(C/C++以及其他非托管语言)

1、C/C++编写规范

关键在于定义导出函数并实现与动态dll的调用,示例如下:

//导出函数示例
extern "C" _declspec(dllexport) char* HostingTest();//导出的返回值为字符串
extern "C" _declspec(dllexport) int Sum(int a, int b);

//动态dll调用示例
void DynamicUse()
{
    HMODULE module = LoadLibrary(L"VideoNetClient.dll");
    if (module == NULL)
    {
        printf("加载VideoNetClient.dll失败\n");
        return;
    }
    typedef int(*AddFunc)(); // 定义函数指针类型
    AddFunc add;
    add = (AddFunc)GetProcAddress(module, "VideoNetClient_Start");

    int sum = add();
    printf("动态调用,sum = %d\n", sum);
}

2、其他编写规范(待补充)

和C++类似,不同语言有不同规范,对应实现即可。

四、结语

1、项目源码后续会整理后上传到gitee,当然,代码很少,用不用源码都差不多;
2、技术栈非常简单,代码逻辑也非常简单,只要思维到位,很容易扩展;
3、用途一:稍微修改完善一下,即可实现软件的动态插件式更新;
4、用途二:可以很容易做出如“QQ框架”这类的软件;