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重载,
也就无法拷贝到内存中去,故此不考虑.

img

南胜写了一篇文章回答了,但是他代码出现了几个问题:

  • 他获取的路径是clr寻找路径之一,我需要改到加载路径上面的.
    各位自行去看看clr的寻找未知dll的方式.
  • 以及他只支持一个引用的dll,而我需要知道引用的引用...的dll.
    所以对他的代码修改一番.

工程开始

项目地址在这里,我博客只是带你慢慢看.
https://gitee.com/inspirefunction/CadLabelBar/tree/loadx/若海加载项目增加卸载版

项目结构

首先,共有四个项目.

  1. cad主插件项目:直接netload的项目.
  2. cad次插件:testa,testb [给a引用],testc [给b引用],后面还有套娃也可以...

netload命令加载

cad主插件

加载

cad次插件

cad次插件testA

引用

testB

testC

test....

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
img

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(','));
    }
}

调试

另见 cad.net dll动态加载之后如何调试

卸载DLL(20210430补充,同时更改了上面的链式加载)

卸载需要修改工程结构,并且最后发生了一些问题没能解决.

项目结构

  1. cad主插件工程,引用-->通讯类工程
  2. 通讯类工程(继承MarshalByRefObject接口的)
  3. 其他需要加载的子插件工程: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旁边,否则无法通讯,会报错,似乎是权限问题,至于有没有其他方法,我不知道...

通讯结构图

cad主插件工程

主域

新域

创建域

创建通讯类

通讯类

执行链式加载子插件工程

新域程序集

卸载域

破坏通讯类

跨域执行方法

检查通讯类生命

程序创建的时候就会有一个主域,然后我们需要在主域上面创建: 新域
然后新域上创建通讯类,利用通讯类在新域进行链式加载,这样程序集都会在新域上面,
这样主域就能够调用新域程序集的方法了.

无法跨 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其余

  1. 本文的 cad主插件项目 - 链式加载
  2. 本文的 cad主插件项目 - 运行域事件
    (完)
posted @ 2020-10-18 03:58  惊惊  阅读(8971)  评论(1编辑  收藏  举报
相关博文:
阅读排行:
· winform 绘制太阳,地球,月球 运作规律
· AI与.NET技术实操系列(五):向量存储与相似性搜索在 .NET 中的实现
· 超详细:普通电脑也行Windows部署deepseek R1训练数据并当服务器共享给他人
· 【硬核科普】Trae如何「偷看」你的代码?零基础破解AI编程运行原理
· 上周热点回顾(3.3-3.9)
点击右上角即可分享
微信分享提示