cad.net dll动态加载和卸载
20240817补
acad卸载不太完美,我把卸载猜想写到下面链接中,大家自行实现.
主要是针对COM的GCHandle问题.
工程Loadx分支中呈现全部的旧代码.
https://gitee.com/inspirefunction/CadLabelBar/issues/IAI0ZZ
当时我搞卸载的时候怎么没看到下面链接,新版net出来微软就弄了?
什么JIT特化会进行内联之类的,我net3.5怎么阻止...
https://www.bookstack.cn/read/dotnet/18b062d94c6a58fa.md
需求
应用需求
1: 我们cad.net开发都会面临一个问题,
cad一直打开的状态下netload两次同名,但版本不一样的dll,
它只会用第一次载入的.也没法做到热插拔...
2: 制作一个拖拉dll到cad加载,
但是不想通过发送netload到命令栏以明文形式加载...
已有的资料 明经netloadx 似乎是不二之选...
但真正令我开始研究是因为
若海提出的: 明经netloadx
在 a.dll 引用了 b.dll 时候,为什么不会成功调用...
我试图尝试直接 Assembly.Load(File.ReadAllBytes(path))
在加载目录的每个文件,并没有报错,
然后出现了一个情况,能使用单独的命令,
却还是不能跨dll调用,也就是会有运行出错(runtime error).
注明: Assembly.Load(byte),
转为byte是为了实现热插拔,
Assembly.LoadForm()没有byte重载,
也就无法拷贝到内存中去,故此不考虑.
南胜写了一篇文章回答了,但是他代码出现了几个问题:
- 他获取的路径是clr寻找路径之一,我需要改到加载路径上面的.
各位自行去看看clr的寻找未知dll的方式. - 以及他只支持一个引用的dll,而我需要知道引用的引用...的dll.
所以对他的代码修改一番.
工程开始
项目地址在这里,我博客只是带你慢慢看.
https://gitee.com/inspirefunction/CadLabelBar/tree/loadx/若海加载项目增加卸载版
项目结构
首先,共有四个项目.
- cad主插件项目:直接netload的项目.
- cad次插件:testa,testb [给a引用],testc [给b引用],后面还有套娃也可以...
cad子插件项目
// testa项目代码
namespace LoadTesta {
public class MyCommands {
[CommandMethod(nameof(testa))]
public static void testa() {
var doc = Acap.DocumentManager.MdiActiveDocument;
if (doc is null) return;
doc.Editor.WriteMessage("\r\n 自带函数" + mameof(testa));
}
[CommandMethod(nameof(gggg))]
public void gggg() {
var doc = Acap.DocumentManager.MdiActiveDocument;
if (doc is null) return;
doc.Editor.WriteMessage("\r\n ****" + nameof(gggg));
testb.MyCommands.TestBHello();
}
}
}
// testb项目代码
namespace LoadTestb {
public class MyCommands {
public static void TestBHello() {
var doc = Acap.DocumentManager.MdiActiveDocument;
if (doc is null) return;
doc.Editor.WriteMessage("\r\n ****" + nameof(TestBHello));
testc.MyCommands.TestcHello();
}
[CommandMethod(nameof(testb))]
public static void testb() {
var doc = Acap.DocumentManager.MdiActiveDocument;
if (doc is null) return;
doc.Editor.WriteMessage("\r\n 自带函数" + nameof(testb));
}
}
}
// testc项目代码
namespace LoadTestc {
public class MyCommands {
public static void TestCHello() {
var doc = Acap.DocumentManager.MdiActiveDocument;
if (doc is null) return;
doc.Editor.WriteMessage("\r\n ****" + nameof(TestCHello));
}
[CommandMethod(nameof(testc))]
public static void testc() {
var doc = Acap.DocumentManager.MdiActiveDocument;
if (doc is null) return;
doc.Editor.WriteMessage("\r\n 自带函数" + nameof(testc));
}
}
}
迭代版本号
必须更改版本号最后是*,否则无法重复加载(所有)
如果想加载时候动态修改dll的版本号,需要学习PE读写.(此文略)
net framework要直接编辑项目文件.csproj,启用由vs迭代版本号:
<PropertyGroup>
<Deterministic>False</Deterministic>
</PropertyGroup>
然后修改AssemblyInfo.cs
net standard只需要增加.csproj的这里,没有自己加一个:
<PropertyGroup>
<AssemblyVersion>1.0.0.*</AssemblyVersion>
<FileVersion>1.0.0.0</FileVersion>
<Deterministic>False</Deterministic>
</PropertyGroup>
cad主插件项目
概念
先说一下测试环境和概念,
cad主插件
上面写了一个命令,这个命令调用了WinForm窗体
让它接受拖拽dll文件,拿到dll的路径,然后链式加载.
这个时候需要直接启动cad,
然后调用netload命令加载cad主插件
的dll.
如果采用vs调试cad启动的话还是会出错.
经过若海两天的Debug发现了: 不能在vs调试状态下运行cad!应该直接启动它!
猜想:这个时候令vs托管了cad的内存,
令所有 Assembly.Load(byte)
都进入了托管内存上面,
vs自动占用到 obj\Debug
文件夹下的dll.
我开了个新文章写这个问题
启动cad之后,用命令调用出WinForm窗体
,再利用拖拽testa.dll
的方式,就可以链式加载到所有的dll了!
再修改testa.dll
重新编译,再拖拽到WinForm窗体
加载,
再修改testb.dll
重新编译,再拖拽到WinForm窗体
加载,
再修改testc.dll
重新编译,再拖拽到WinForm窗体
加载
.....如此如此,这般这般.....
WinForm窗体
拖拽这个函数网络搜一下基本能搞定,我就不贴代码了,
接收拖拽之后就有个testa.dll
的路径,再调用传给加载函数就好了.
调用方法
AssemblyDependent ad = new();
List<LoadState> ls = new();
ad.Load(item, ls);
var msg = AssemblyDependent.PrintMessage(ls);
if (msg != null)
MessageBox.Show(msg, "加载完毕!");
else
MessageBox.Show("无任何信息", "加载出现问题!");
链式加载
#define HarmonyPatch
#define HarmonyPatch_1
namespace IFoxCAD.LoadEx;
/*
* 因为此处引用了 nuget的 Lib.Harmony
* 所以单独分一个工程出来作为cad工程的引用
* 免得污染了cad工程的纯洁
*/
#if HarmonyPatch_1
[HarmonyPatch("Autodesk.AutoCAD.ApplicationServices.ExtensionLoader", "OnAssemblyLoad")]
#endif
public class AssemblyDependent {
#if HarmonyPatch
// 这个是不能删除的,否则就不执行了
// HarmonyPatch hook method 返回 false 表示拦截原函数
public static bool Prefix() { return false; }
#endif
/// <summary>
/// 拦截cad的Loader异常:默认是<paramref name="false"/>
/// </summary>
public bool PatchExtensionLoader = false;
/// <summary>
/// 当前域加载事件,运行时缺失查找加载路径
/// </summary>
public event ResolveEventHandler CurrentDomainAssemblyResolveEvent {
add { AppDomain.CurrentDomain.AssemblyResolve += value; }
remove { AppDomain.CurrentDomain.AssemblyResolve -= value; }
}
/// <summary>
/// 链式加载dll依赖
/// </summary>
public AssemblyDependent() {
CurrentDomainAssemblyResolveEvent += AssemblyHelper.DefaultAssemblyResolve;
}
#region 获取加载链
/// <summary>
/// 加载程序集
/// </summary>
/// <param name="dllFullName">dll的文件位置</param>
/// <param name="loadStates">返回加载链</param>
/// <param name="byteLoad">true字节加载,false文件加载</param>
/// <returns>参数加载成功标识<paramref name="dllFullName"/></returns>
[MethodImpl(MethodImplOptions.NoInlining)]
public bool Load(string dllFullName, List<LoadState> loadStates, bool byteLoad = true) {
if (dllFullName is null)
throw new ArgumentNullException(nameof(dllFullName));
// 相对路径要先转换
dllFullName = Path.GetFullPath(dllFullName);
if (!File.Exists(dllFullName))
throw new ArgumentException("路径不存在");
// 获取加载链
HashSet<string> dllFullNameSet = new();
var cadAssembly = AppDomain.CurrentDomain.GetAssemblies();
var cadAssemblyRef = AppDomain.CurrentDomain.ReflectionOnlyGetAssemblies();
GetAllRefPaths(cadAssembly, cadAssemblyRef, dllFullName, dllFullNameSet);
var assSet = cadAssembly.Select(ass => ass.FullName).ToHashSet();
// 加载链进行加载
foreach(var arf in dllFullNameSet) {
// 版本号没变不加载
if (assSet.Contians(AssemblyName.GetAssemblyName(arf).FullName)) {
loadStates.Add(new LoadState(arf, false));
continue;
}
try {
var ass = GetPdbAssembly(arf);
if (ass is null) {
if (byteLoad) ass = Assembly.Load(File.ReadAllBytes(arf));
else ass = Assembly.LoadFile(arf);
}
loadStates.Add(new LoadState(arf, true, ass));
} catch { loadStates.Add(new LoadState(arf, false));/*错误造成*/ }
}
return dllFullNameSet.Contains(dllFullName);
}
/// <summary>
/// 在debug模式的时候才获取PBD调试信息
/// </summary>
/// <param name="path"></param>
/// <param name="byteLoad"></param>
/// <returns></returns>
[MethodImpl(MethodImplOptions.NoInlining)]
Assembly? GetPdbAssembly(string? path)
{
#if DEBUG
//为了实现Debug时候出现断点,见链接,加依赖
// https://www.cnblogs.com/DasonKwok/p/10510218.html
// https://www.cnblogs.com/DasonKwok/p/10523279.html
var dir = Path.GetDirectoryName(path);
var pdbName = Path.GetFileNameWithoutExtension(path) + ".pdb";
var pdbFullName = Path.Combine(dir, pdbName);
if (File.Exists(pdbFullName))
return Assembly.Load(File.ReadAllBytes(path), File.ReadAllBytes(pdbFullName));
#endif
return null;
}
/// <summary>
/// 递归获取加载链
/// </summary>
/// <param name="cadAssembly">程序集_内存区</param>
/// <param name="cadAssemblyRef">程序集_映射区</param>
/// <param name="dllFullName">dll文件位置</param>
/// <param name="dllFullNamesOut">返回的集合</param>
/// <returns></returns>
[MethodImpl(MethodImplOptions.NoInlining)]
void GetAllRefPaths(Assembly[] cadAssembly, Assembly[] cadAssemblyRef,
string dllFullName, HashSet<string> dllFullNameSet) {
if (dllFullName is null)
throw new ArgumentNullException(nameof(dllFullName));
if (!File.Exists(dllFullName)) return;
if (!dllFullNameSet.Add(dllFullName)) return;
var assemblyAsRef = GetAssembly(cadAssembly, cadAssemblyRef, dllFullName);
if (assemblyAsRef is null)
return;
// dll拖拉加载路径
var sb = new StringBuilder();
sb.Append(Path.GetDirectoryName(dllFullName));
sb.Append("\\");
// 获取映射区的dll,进行递归获取依赖路径
var files = assemblyAsRef.GetReferencedAssemblies()
.Select(ass => sb.ToString() + ass.Name + ".dll")
.Where(file => !dllFullNameSet.Contains(file) && File.Exists(file))
.ToArray();
foreach(var file in files) {
GetAllRefPaths(cadAssembly, cadAssemblyRef, file, dllFullNameSet);
}
}
/// <summary>
/// 在内存区和映射区找已经加载的程序集
/// </summary>
/// <param name="cadAssembly">程序集_内存区</param>
/// <param name="cadAssemblyRef">程序集_映射区</param>
/// <param name="dllFullName">dll文件位置</param>
/// <returns></returns>
[MethodImpl(MethodImplOptions.NoInlining)]
Assembly? GetAssembly(Assembly[] cadAssembly, Assembly[] cadAssemblyRef,
string dllFullName) {
// 路径转程序集名
var assName = AssemblyName.GetAssemblyName(dllFullName).FullName;
// 内存区
var assemblyAs = cadAssembly.FirstOrDefault(ass => ass.FullName == assName);
if (assemblyAs is not null) return assemblyAs;
// 映射区
var assemblyAsRef = cadAssemblyRef.FirstOrDefault(ass => ass.FullName == assName);
if (assemblyAsRef is not null) return assemblyAsRef;
// 内存区和映射区都没有,
// 把dll加载到映射区,用来找依赖表
var byteRef = File.ReadAllBytes(dllFullName);
if (!PatchExtensionLoader) {
// 若用名称参数,没有依赖会报错,所以用字节参数.
// 不能重复加载同一个dll,用hashset排除.
return Assembly.ReflectionOnlyLoad(byteRef);
}
#if HarmonyPatch_1
// QQ1548253108: 我这里会报错,提供了解决方案.
// 方案一: 在类上面加 [HarmonyPatch("Autodesk.AutoCAD.ApplicationServices.ExtensionLoader", "OnAssemblyLoad")]
const string ext = "Autodesk.AutoCAD.ApplicationServices.ExtensionLoader";
Harmony hm = new(ext);
hm.PatchAll();
assemblyAsRef = Assembly.ReflectionOnlyLoad(byteRef);
hm.UnpatchAll(ext);
#endif
#if HarmonyPatch_2
// 方案二:跟cad耦合了
const string ext = "Autodesk.AutoCAD.ApplicationServices.ExtensionLoader";
var docAss = typeof(Autodesk.AutoCAD.ApplicationServices.Document).Assembly;
var a = docAss.GetType(ext);
var b = a.GetMethod("OnAssemblyLoad");
Harmony hm = new(ext);
hm.Patch(b, new HarmonyMethod(GetType(), "Dummy"));
assemblyAsRef = Assembly.ReflectionOnlyLoad(byteRef);
hm.UnpatchAll(ext);
#endif
return assemblyAsRef;
}
/// <summary>
/// 加载信息
/// </summary>
public static string PrintMessage(List<LoadState> loadStates)
{
if (loadStates is null) return "";
var sb = new StringBuilder();
var ok = loadStates.FindAll(a => a.State);
var no = loadStates.FindAll(a => !a.State);
if (ok.Count != 0) {
sb.AppendLine("** 这些文件加载成功!");
foreach (var item in ok) {
sb.AppendLine("++ ");
sb.AppendLine(item.DllFullName);
}
}
if (no.Count != 0) {
sb.AppendLine("** 这些文件已被加载过,同时重复名称和版本号,跳过!");
foreach (var item in no) {
sb.AppendLine("-- ");
sb.AppendLine(item.DllFullName);
}
}
return sb.ToString();
}
#endregion
#region 删除文件
/// <summary>
/// Debug的时候删除obj目录,防止占用
/// </summary>
/// <param name="dllFullName">dll文件位置</param>
public void DebugDelObjFiles(string dllFullName)
{
var filename = Path.GetFileNameWithoutExtension(dllFullName);
var path = Path.GetDirectoryName(dllFullName);
var pdb = path + "\\" + filename + ".pdb";
if (File.Exists(pdb))
File.Delete(pdb);
var list = path.Split('\\');
if (list[list.Length - 1] == "Debug" && list[list.Length - 2] == "bin")
{
var projobj = path.Substring(0, path.LastIndexOf("bin")) + "obj";
FileEx.DeleteFolder(projobj);
}
}
#endregion
#region 移动文件
/// <summary>
/// Debug的时候移动obj目录,防止占用
/// </summary>
public void DebugMoveObjFiles(string? dllFullName, Action action)
{
// 临时文件夹_pdb的,无论是否创建这里都应该进行删除
const string Temp = "Temp";
string? temp_Pdb_dest = null;
string? temp_Pdb_source = null;
string? temp_Obj_dest = null; ;
string? temp_Obj_source = null;
try
{
var filename = Path.GetFileNameWithoutExtension(dllFullName);
var path = Path.GetDirectoryName(dllFullName);
//新建文件夹_临时目录
temp_Pdb_dest = path + $"\\{Temp}\\";
//移动文件进去
temp_Pdb_source = path + "\\" + filename + ".pdb";
FileEx.MoveFolder(temp_Pdb_source, temp_Pdb_dest);
//检查是否存在obj文件夹,有就递归移动
var list = path.Split('\\');
if (list[list.Length - 1] == "Debug" && list[list.Length - 2] == "bin")
{
var proj = path.Substring(0, path.LastIndexOf("bin"));
temp_Obj_source = proj + "obj";
temp_Obj_dest = proj + $"{Temp}";
FileEx.MoveFolder(temp_Obj_source, temp_Obj_dest);
}
action.Invoke();
}
finally
{
// 还原移动
FileEx.MoveFolder(temp_Pdb_dest, temp_Pdb_source);
FileEx.DeleteFolder(temp_Pdb_dest);
FileEx.MoveFolder(temp_Obj_dest, temp_Obj_source);
FileEx.DeleteFolder(temp_Obj_dest);
}
}
#endregion
}
/// <summary>
/// 加载程序集和加载状态
/// </summary>
public struct LoadState {
public Assembly? Assembly;
public string DllFullName;
public bool State;
public LoadState(string dllFullName, bool state, Assembly? assembly = null)
{
DllFullName = dllFullName;
State = state;
Assembly = assembly;
}
}
public class FileEx
{
/// <summary>
/// 判断含有文件名和后缀
/// </summary>
/// <param name="pathOrFile">路径或者完整文件路径</param>
static bool ContainFileName(string? pathOrFile)
{
// 判断输入的是单文件,它可能不存在
var a = Path.GetDirectoryName(pathOrFile);
var b = Path.GetFileName(pathOrFile);
var c = Path.GetExtension(pathOrFile);
// 是文件
return a.Length > 0 && b.Length > 0 && c.Length > 0;
}
/// <summary>
/// 移动文件夹中的所有文件夹与文件到另一个文件夹
/// </summary>
/// <param name="sourcePathOrFile">源文件夹</param>
/// <param name="destPath">目标文件夹</param>
public static void MoveFolder(string? sourcePathOrFile, string? destPath)
{
if (sourcePathOrFile is null)
throw new ArgumentException(nameof(sourcePathOrFile));
if (destPath is null)
throw new ArgumentException(nameof(destPath));
if (ContainFileName(destPath))
destPath = Path.GetDirectoryName(destPath);
// 目标目录不存在,则创建目录
if (!Directory.Exists(destPath))
Directory.CreateDirectory(destPath);
if (ContainFileName(sourcePathOrFile)) {
// 如果是单个文件,就移动到目录就好了
if (File.Exists(sourcePathOrFile)) {
destPath += "\\" + Path.GetFileName(sourcePathOrFile);
File.Move(sourcePathOrFile, destPath);
}
return;
}
// 如果是文件就改为路径
if (!Directory.Exists(sourcePathOrFile))
{
sourcePathOrFile = Path.GetDirectoryName(sourcePathOrFile);
if (!Directory.Exists(sourcePathOrFile))
throw new DirectoryNotFoundException("源目录不存在!");
}
MoveFolder2(sourcePathOrFile, destPath);
}
/// <summary>
/// 移动文件夹中的所有文件夹与文件到另一个文件夹
/// </summary>
/// <param name="sourcePath">源文件夹</param>
/// <param name="destPath">目标文件夹</param>
static void MoveFolder2(string sourcePath, string destPath)
{
//目标目录不存在则创建
if (!Directory.Exists(destPath))
Directory.CreateDirectory(destPath);
//获得源文件下所有文件
var files = Directory.GetFiles(sourcePath);
foreach(var file in files) {
string destFile = Path.Combine(destPath, Path.GetFileName(file));
// 覆盖模式
if (File.Exists(destFile)) File.Delete(destFile);
File.Move(file, destFile);
}
//获得源文件下所有目录文件
var folders = Directory.GetDirectories(sourcePath);
foreach(var fo in folders) {
string destDir = Path.Combine(destPath, Path.GetFileName(fo));
// Directory.Move 不能在不同卷中移动.
// Directory.Move(c, destDir);
// 采用递归的方法实现
MoveFolder2(fo, destDir);
}
}
/// <summary>
/// 递归删除文件夹目录及文件
/// </summary>
/// <param name="dir"></param>
/// <returns></returns>
public static void DeleteFolder(string? dir) {
if (dir is null) throw new ArgumentException(nameof(dir));
if (!Directory.Exists(dir)) return;
foreach (string d in Directory.GetFileSystemEntries(dir)) {
if (File.Exists(d)) File.Delete(d); // 删除文件
else DeleteFolder(d); // 递归删除子文件夹
}
Directory.Delete(dir, true); // 删除已空文件夹
}
}
运行域事件
最重要是这个事件,
它会在运行的时候找已经载入内存上面的程序集.
关于AppDomain.CurrentDomain.AssemblyResolve
事件,
它用于在程序集加载失败时重新加载程序集,
通常用于动态加载程序集的场景
你可以通过该事件自定义程序集的查找和加载逻辑,确保程序集能够正确加载.
0x01 动态加载要注意所有的引用外的dll的加载顺序
0x02 指定版本: Acad2008若没有这个事件,会使动态命令执行时候无法引用当前的程序集函数
0x03 目录构成: 动态加载时,dll的地址会在系统的动态目录里,而它所处的程序集(运行域)是在动态目录里.
0x04 命令构成: cad自带的netload会把所处的运行域给改到cad自己的,而动态加载不通过netload,所以要自己去改.
调用
AppDomain.CurrentDomain.AssemblyResolve += AssemblyHelper.DefaultAssemblyResolve;
AppDomain.CurrentDomain.ReflectionOnlyAssemblyResolve += AssemblyHelper.ReflectionOnlyAssemblyResolve;
封装
using System.Diagnostics;
namespace IFoxCAD.LoadEx;
public class AssemblyHelper {
[MethodImpl(MethodImplOptions.NoInlining)]
public static Assembly? ReflectionOnlyAssemblyResolve(object sender, ResolveEventArgs e)
{
var cadAss = AppDomain.CurrentDomain.ReflectionOnlyGetAssemblies();
return Resolve(cadAss, sender, e);
}
[MethodImpl(MethodImplOptions.NoInlining)]
public static Assembly? DefaultAssemblyResolve(object sender, ResolveEventArgs e)
{
var cadAss = AppDomain.CurrentDomain.GetAssemblies();
return Resolve(cadAss, sender, e);
}
[MethodImpl(MethodImplOptions.NoInlining)]
public static Assembly? Resolve(Assembly[] cadAss, object sender, ResolveEventArgs e)
{
// e.Name含有名称和版本号,都一致的直接调用它
var result = cadAss.FirstOrDefault(ass => e.Name == ass.GetName().FullName);
if (result is not null)
return result;
// 上面没有获取到表示字符串不同,
// 提取名称部分,再获取最后的版本号,
// 调用最后的可用版本
var eName = GetAssemblyName(e.Name);
result = cadAss.Where(ass => eName == GetAssemblyName(ass.GetName().FullName))
.OrderByDescending(ass => ass.GetName().Version)
.FirstOrDefault();
if (result is null)
{
// cad21+vs22 容易触发这个资源的问题
// https://stackoverflow.com/questions/4368201
string[] fields = e.Name.Split(',');
string name = fields[0];
string culture = fields[2];
if (name.EndsWith(".resources") && !culture.EndsWith("neutral"))
return null;
var sb = new StringBuilder();
sb.AppendLine($"{nameof(LoadEx)}------------------------------------------------------------");
sb.AppendLine(nameof(DefaultAssemblyResolve) + "出错,程序集无法找到它");
sb.AppendLine("++参数名:: " + GetAssemblyName(e.Name));
sb.AppendLine("++参数完整信息:: " + e.Name);
for (int i = 0; i < cadAss.Length; i++)
sb.AppendLine("-------匹配对象:: " + GetAssemblyName(cadAss[i].GetName().FullName));
sb.AppendLine($"程序集找不到,遇到无法处理的错误,杀死当前进程!");
sb.AppendLine($"{nameof(LoadEx)}------------------------------------------------------------");
Debug.WriteLine(sb.ToString());
// Process.GetCurrentProcess().Kill();
Debugger.Break();
}
return result;
}
static string GetAssemblyName(string argString) {
return argString.Substring(0, argString.IndexOf(','));
}
}
调试
卸载DLL(20210430补充,同时更改了上面的链式加载)
卸载需要修改工程结构,并且最后发生了一些问题没能解决.
项目结构
- cad主插件工程,引用-->通讯类工程
- 通讯类工程(继承MarshalByRefObject接口的)
- 其他需要加载的子插件工程:cad子插件项目作为你测试加载的dll,里面有一个cad命令gggg,它将会用来验证我们是否成功卸载的关键.
和以往的工程都不一样的是,我们需要复制一份acad.exe目录的所有文件到一个非C盘目录,如下:
修改 主工程
,属性页
:
生成
,输出
: G:\AutoCAD 2008\ <--由于我的工程是.net standard,所以这里将会生成各类net文件夹
调试
,可执行文件
: G:\AutoCAD 2008\net35\acad.exe <--在net文件夹放acad.exe目录所有文件
为什么?因为通讯类.dll必须要在acad.exe旁边,否则无法通讯,会报错,似乎是权限问题,至于有没有其他方法,我不知道...
通讯结构图
程序创建的时候就会有一个主域,然后我们需要在主域上面创建: 新域
然后新域上创建通讯类,利用通讯类在新域进行链式加载,这样程序集都会在新域上面,
这样主域就能够调用新域程序集的方法了.
无法跨 AppDomain 传递 GCHandle
在新域上面创建通讯类--卸载成功,
但是关闭cad程序的时候弹出了报错:
System.ArgumentException:“无法跨 AppDomain 传递 GCHandle。”
我查询这个错误内容,
大部分是讲COM的,而我包内刚好使用了COM.
我将会在这里讨论它:
https://gitee.com/inspirefunction/CadLabelBar/issues/IAI0ZZ
做做实验,.实现一个没引用cad.dll的dll,我用了winform,
是可以卸载,且不会报GC错误.
代码
cad主程序dll工程
命令
using RemoteAccess;
using System.Diagnostics;
using Autodesk.AutoCAD.Runtime;
using Acap = Autodesk.AutoCAD.ApplicationServices.Application;
using Exception = System.Exception;
// 载入dll内的cad的命令
// 1 利用动态编译在主域下增加命令.
// 2 挂未知命令反应器,找不到就去域内找(比较简单,这个实验成功)
namespace JoinBox;
public class LoadAcadDll : IAutoGo {
public Sequence SequenceId() => Sequence.Last;
// 关闭进程时候实现卸载执行
public void Terminate() {
// 不可以忽略,因为直接关闭cad的时候是通过这里进行析构,而且优先于析构函数.
// 而析构对象需要本类 _jAppDomain 提供域,否则拿不到.
JJUnLoadAppDomain();
}
/// <summary>
/// 用于和其他域通讯的中间类
/// </summary>
public static JAppDomain? _jAppDomain = null;
// 命令卸载程序域
[CommandMethod("JJUnLoadAppDomain")]
public void JJUnLoadAppDomain() {
_jAppDomain?.Dispose();
_jAppDomain = null;
}
// 测试无引用cad的dll环境,纯winform
// 这样调用是成功的,GC释放很成功
[CommandMethod(nameof(LoadTest2))]
public void LoadTest2() {
if (_jAppDomain is null) return;
var result = _jAppDomain.JRemoteLoader.Invoke("客户端.HelloWorld", "GetTime", new object[] { "我是皮卡丘" });
System.Windows.Forms.MessageBox.Show(result.ToString());
}
// 未知命令,查找全部程序集
// 然后调用,测试卸载之后的gggg命令是否仍然有用
public void CmdUnknown() {
var dm = Acap.DocumentManager;
var md = dm.MdiActiveDocument;
// 反应器->未知命令
md.UnknownCommand += (sender, e) => {
if (_jAppDomain is null) return;
var globalCommandName = e.GlobalCommandName;
_jAppDomain.Init();
var jrl = _jAppDomain.JRemoteLoader;
// 产生:不可序列化的错误
// 因为主域需要和其他域沟通,那么主域的变量都无法传递过去
// 所以需要以参数封送传递到其他域.
jrl?.TraversideLoadAssemblys((jrl2, assembly, ars) => {
var cmd = ars[0].ToString().ToUpper();
var cmdMap = CommandHelper.GetCmdMap(assembly);
if (cmdMap.TryGetValue(cmd, out var info)) {
var sp = info.Namespace + "." + info.ClassName;
var returnValue = jrl2.Invoke(assembly, sp, cmd);
return true; // 这里仍然结束循环
}
return null;
}, new object[] { globalCommandName });
};
}
public void Initialize() {
// 引用Acad的类,此dll拥有引用的dll.
string dll = @"H:\解决方案\动态加载项目\若海加载项目增加卸载版\ClassLibrary1\bin\Debug\net35\ClassLibrary1.dll";
// 无引用Acad的类,就可以卸载
dll = @"H:\解决方案\动态加载项目\若海加载项目增加卸载版\客户端\bin\Debug\客户端.dll";
try {
_jAppDomain = new JAppDomain("MyJoinBoxAppDomain", dll);
_jAppDomain.Init();
var jrl = _jAppDomain.JRemoteLoader;
jrl.LoadAssembly(dll);
// 加载不成功就结束
if (!jrl.LoadOK) {
Debug.WriteLine(jrl.LoadErrorMessage);
JJUnLoadAppDomain();
return;
}
// 调用方法
object retstr = jrl.Invoke("LoadTesta.MyCommands", "gggg");
} catch (System.Exception exception) {
Debugger.Break();
Debug.WriteLine(exception.Message);
}
CmdUnknown();
}
}
反射导出cad命令
using System.Collections.Generic;
using System.Reflection;
using System;
using System.IO;
using Autodesk.AutoCAD.Runtime;
namespace JoinBox;
public static class CommandHelper {
/// <summary>
/// 反射导出Acad的注册的命令
/// </summary>
/// <param name="dllFileNames">Acad注册的命令的Dll</param>
/// <returns></returns>
public static Dictionary<string, LoadAcadCmdsInfo> GetCmdMap(Assembly ass) {
Dictionary<string, LoadAcadCmdsInfo> map = new();
var tyeps = new Type[] { };
// 反射时候还依赖其他的dll就会这个错误
try { tyeps = ass?.GetTypes(); }
catch (ReflectionTypeLoadException) { return map; }
// 获取类型的所有公共方法
var ts = tyeps.Where(type => type.IsClass && type.IsPublic);
foreach (var type in ts) {
var methods = type.GetMethods(BindingFlags.Public | BindingFlags.Instance | BindingFlags.Static)
.Where(m => m.GetCustomAttributes(typeof(CommandMethodAttribute), false).Length > 0);
.Select(m => new { Method = m, Attribute = m.GetCustomAttribute<CommandMethodAttribute>() });
foreach (var item in methods) {
var method = item.Method;
var cadAtt = item.CommandMethodAttribute;
var dllName = Path.GetFileNameWithoutExtension(
ass.ManifestModule.Name.Substring(0, ass.ManifestModule.Name.Length - 4));
var info = new LoadAcadCmdsInfo {
DllName = dllName, // 不一定有文件名
Namespace = type.Namespace,
ClassName = type.Name,
CmdName = cadAtt.GlobalName.ToUpper(),
MethodName = method.Name,
};
map[info.CmdName] = info;
}
return map;
}
}
}
[Serializable]
public class LoadAcadCmdsInfo {
public string DllName;
public string Namespace;
public string ClassName;
public string CmdName;
public string MethodName;
}
通讯类dll工程
创建程序域和初始化通讯类JAppDomain
using System;
using System.IO;
using System.Reflection;
namespace RemoteAccess;
public class JAppDomain : IDisposable {
/// <summary>
/// 新域
/// </summary>
AppDomain _newAppDomain;
/// <summary>
/// 新域的通讯代理类(通过它和其他程序域沟通)
/// </summary>
public RemoteLoader? JRemoteLoader {
if (Disposed) return null;
return _jRemoteLoader
}
RemoteLoader _jRemoteLoader;
/// <summary>
/// 程序域的创建和释放
/// </summary>
/// <param name="newAppDomainName">新程序域名</param>
/// <param name="assemblyPlugs">子目录,多个目录用分号间隔</param>
public JAppDomain(string newAppDomainName, string assemblyPlugs = null) {
// 如果是文件就转为路径
var ap = assemblyPlugs;
if (!string.IsNullOrEmpty(ap)) ap = Path.GetDirectoryName(ap);
// 插件目录
var path = RemoteLoaderTool.GetAssemblyPath(true);
path = Path.GetDirectoryName(path);
if (!string.IsNullOrEmpty(ap) && ap != path)
ap += ";" + path;
// 创建App域
var ads = new AppDomainSetup {
// 名称
ApplicationName = newAppDomainName,
// 应用程序根目录
ApplicationBase = AppDomain.CurrentDomain.BaseDirectory,
// 子目录(相对形式)
// 在AppDomainSetup中加入外部程序集的所在目录,
// 多个目录用分号间隔
PrivateBinPath = ap ??= path,
};
// 设置缓存目录
ads.CachePath = ads.ApplicationBase;
// 获取或设置指示影像复制是打开还是关闭
ads.ShadowCopyFiles = "true";
// 获取或设置目录的名称,这些目录包含要影像复制的程序集
ads.ShadowCopyDirectories = ads.ApplicationBase;
ads.DisallowBindingRedirects = false;
ads.DisallowCodeDownload = true;
ads.ConfigurationFile = AppDomain.CurrentDomain.SetupInformation.ConfigurationFile;
#if false
// 从安全策略证据新建程序域(应该是这句导致通讯类无法获取文件)
var adevidence = AppDomain.CurrentDomain.Evidence;
// 创建第二个应用程序域
AppDomainFactory.JAppDomain = AppDomain.CreateDomain(newAppDomainName, adevidence, ads);
#endif
_newAppDomain = AppDomain.CreateDomain(newAppDomainName, null, ads);
}
// 此处有报错,因此不要设置在构造函数上面
public void Init() {
// 获取RemoteLoader
string assemblyName = AppDomain.CurrentDomain.GetAssemblies()
.Where(ass => ass == typeof(RemoteLoader).Assembly)
.Select(ass => ass.FullName)
.FirstOrDefault();
if (assemblyName is null)
throw new ArgumentNullException(nameof(RemoteLoader) + "程序域不存在");
try {
#if true2
var mainAppDomain = AppDomain.CurrentDomain;
// 如果通讯类是主域的话表示新域没用,加载进来的东西都在主域.
// 做了个寂寞,释放了空域
_jRemoteLoader = mainAppDomain.CreateInstanceAndUnwrap(
assemblyName,
typeof(RemoteLoader).FullName) as RemoteLoader;
#else
// 在新域创建通讯类,
// 关闭Acad会引发错误: System.ArgumentException:“无法跨 AppDomain 传递 GCHandle。”
// 可能是COM内存对象没有释放?用了非托管对象没有释放?
_jRemoteLoader = _newAppDomain.CreateInstanceAndUnwrap(
assemblyName,
typeof(RemoteLoader).FullName) as RemoteLoader;
#endif
} catch (Exception) {
this.Dispose(true);
throw new ArgumentNullException(
"需要将*通讯类.dll*扔到acad.exe,而c盘权限太高了," +
"所以直接复制一份acad.exe所有文件到你的主工程Debug目录," +
"调试都改到这个目录上面的acad.exe");
}
}
public bool Disposed = false;
public void Dispose() {
Dispose(true);
GC.SuppressFinalize(this);
}
~JAppDomain() => Dispose(false);
protected virtual void Dispose(bool disposing) {
if (Disposed) return;
Disposed = true;
// 若系统卸载出错,而手动卸载没出错,
// 要留意JRemoteLoader对象在什么域的什么对象上.
if (disposing) {
JRemoteLoader.Dispose();
AppDomain.Unload(_newAppDomain);
}
}
}
通讯类RemoteLoader
参考文章1 https://www.cnblogs.com/zlgcool/archive/2008/10/12/1309616.html
参考文章2 https://www.cnblogs.com/roucheng/p/csdongtai.html
using System;
using System.Collections.Generic;
using System.Reflection;
using System.Runtime.Remoting;
namespace RemoteAccess;
[Serializable]
public class AssemblyInfo {
public Assembly Assembly;
public string Namespace;
public string Class;
public string Method;
public string TypeFullName;
}
/// <summary>
/// 通讯代理类
/// </summary>
[Serializable]
public class RemoteLoader : MarshalByRefObject {
/// <summary>
/// 加载成功
/// </summary>
public bool LoadOK { get; set; }
/// <summary>
/// 加载的错误信息,可以获取到链条中段的错误
/// </summary>
public string LoadErrorMessage { get; set; }
AssemblyDependent _adep;
public RemoteLoader() { }
/// <summary>
/// 域内进行链式加载dll
/// </summary>
/// <param name="file"></param>
public void LoadAssembly(string file) {
_adep = new AssemblyDependent(file);
_adep.CurrentDomainAssemblyResolveEvent +=
RunTimeCurrentDomain.DefaultAssemblyResolve;
_adep.Load();
LoadOK = _adep.LoadOK;
LoadErrorMessage = _adep.LoadErrorMessage;
}
// 加载cad的东西只能在外面做,
// 而且远程通讯方法又必须在MarshalByRefObject接口下,
// 所以这提供遍历加载的程序集的方法
/// <summary>
/// 遍历程序集封送加载
/// </summary>
/// <param name="func"><see cref="RemoteLoader"/>封送本类|
/// <see cref="Assembly"/>封送程序集|
/// <see cref="object[]"/>封送参数|
/// <see cref="object"/>封送返回值,<see cref="!null"/>结束循环.
/// </param>
/// <param name="ars">外参数传入封送接口</param>
public object? TraversideLoadAssemblys(
Func<RemoteLoader, Assembly, object[], object> func,
object[] ars = null) {
if (_adep is null) return null;
object value = null;
// TODO 它为什么没有map缓存
foreach (var assembly in _adep.MyLoadAssemblys) {
try { value = func?.Invoke(this, assembly, ars); } catch { }
if (value is not null) break;
}
return value;
}
// 方法调用
public object Invoke(Assembly assembly, string spaceClass, string methodName, params object[] args) {
if (assembly is null)
throw new ArgumentNullException(nameof(assembly));
Type spaceClassType = assembly.GetType(spaceClass);
if (spaceClassType is null)
throw new ArgumentNullException(nameof(spaceClassType), "命名空间.类型");
// 1,大小写要严格
// 2,参数要数量要相同,因为重载机制
var method = spaceClassType.GetMethods()
.Where(m => string.Equals(m.Name, methodName))
.FirstOrDefault(m => m.GetParameters().Length == args.Length);
if (method is null)
throw new ArgumentNullException(methodName+"方法不存在");
// 若出错表示跨域,此句也会导致GC释放出错
return methodName.Invoke(Activator.CreateInstance(spaceClassType), args);
// 此调用方式需要 Assembly.LoadForm 否则无法查找影像文件
// BindingFlags bfi = BindingFlags.Instance | BindingFlags.Public | BindingFlags.CreateInstance;
// return Activator.CreateInstanceFrom(assembly.FullName,
// spaceClass + "." + methodName,
// false, bfi, null, args, null, null, null).Unwrap();
}
}
通讯类RemoteLoader其余
- 本文的
cad主插件项目
-链式加载
- 本文的
cad主插件项目
-运行域事件
(完)
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· winform 绘制太阳,地球,月球 运作规律
· AI与.NET技术实操系列(五):向量存储与相似性搜索在 .NET 中的实现
· 超详细:普通电脑也行Windows部署deepseek R1训练数据并当服务器共享给他人
· 【硬核科普】Trae如何「偷看」你的代码?零基础破解AI编程运行原理
· 上周热点回顾(3.3-3.9)