基于.net的插件框架 技术栈 参考架构说明
前言
之前研究动态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框架”这类的软件;