aardio 文档

在 AutoCAD 内运行 C# 短脚本

cad.RunCSharp(source,args,doc,options) 将完整 C# 源码按次内存编译,在当前 CAD 文档上下文执行,再返回普通数据。适合一次性查询、计算、批量图元操作。常驻事件、菜单和后台服务仍使用 CompileCSharp 插件。

当前短脚本宿主仅支持.NET8+ AutoCAD;中望/浩辰使用CompileCSharp DLL或COM/LISP,见兼容与迁移。

最小调用

以下只读取当前文档并返回数据,不绘图;要求已经打开目标文档。

import com.cad;
var cad=com.cad(true,"AutoCAD.Application");
if(!cad || !cad.Documents.Count) error("请先打开目标图纸");
var doc=cad.ActiveDocument;
cad.ShowForeground();

var source=/***
using System.Text.Json;
using Autodesk.AutoCAD.ApplicationServices;
using Autodesk.AutoCAD.DatabaseServices;
using Autodesk.AutoCAD.EditorInput;
public static class Script {
    public static object Execute(Document doc, Database db, Editor ed,
                                 Transaction tr, JsonElement args) {
        return new {
            answer=args.GetProperty("a").GetInt32()+args.GetProperty("b").GetInt32(),
            document=doc.Name
        };
    }
}
***/;

var result,err=cad.RunCSharp(source,{a=12;b=30},doc);
if(!result || !result.ok) error(err);
print(result.data);

成功时 result.data.answer 为 42。再次修改源码仍使用同一个 Script.Execute,不必生成新的命令名。宿主内部为每次业务程序集生成唯一身份。

技能同样保留增强cad;使用普通子模块时显式传入这个实例,不从doc.Application另取宿主:

import com.cad.script;
var result,err,task=com.cad.script.run(cad,source,{a=12;b=30},doc);

本库不依赖 Autos。source 可以是字符串、buffer 或 .cs 路径;UTF-16LE 文件应有 BOM。不处理 aardio 模板,也不是 .csx/REPL,不保留跨次脚本变量。

C# 入口与数据

必须定义无命名空间的 public static class Script,提供以下同步静态方法:

public static object Execute(Document doc, Database db, Editor ed,
                             Transaction tr, JsonElement args)

结果转换不把脚本动态类型交给长期序列化元数据缓存;只将宿主自身类型和已经编码的 JSON 送出。

事务边界

宿主锁定目标文档,开启事务,调用入口并编码返回结果,最后提交。正常调用的 result.committed 表示宿主事务已提交;编译或入口错误不会执行脚本。

脚本使用传入的 tr.GetObject、tr.AddNewlyCreatedDBObject 等标准 CAD API;不要自行 Commit/Abort/Dispose 传入的事务,不切换或关闭文档,不修改宿主全局状态。新建 DBObject 等本地资源按 AutoCAD API 约定释放。

执行异常或结果编码错误发生在宿主提交前时,未提交事务会释放并回滚事务内数据库修改。实机已验证“创建圆后抛异常”和“创建圆后返回不可编码的 CAD 活对象”均未留下圆。

这不是任意副作用事务:文件/网络/事件/系统变量、其他文档、脚本自行提交的事务不在这项承诺内。不要注册长期事件或启动持久后台线程;需要这些功能时改走常驻插件。提示词和入口检查不是隔离边界。

完成、失败与恢复

run/增强 RunCSharp 返回 result,err,task:

需要在等待前持久化路径时使用 create:

import com.cad.script;
var task,err=com.cad.script.create(cad,source,{a=12;b=30},doc);
if(!task) error(err);
print(task.path); // 在此保存恢复路径
var submitted,err=task.submit();
if(!submitted) error(err);
var result,err=task.wait();
print(result,err);

只读原回执,不提交、不准备 SDK:

var original,err=com.cad.script.open(task.path);
if(!original) error(err);
var result,err=original.wait(20000);
print(result,err);

open 得到的 read/wait 会解码 C# 结果;直接用 com.cad.task.open 仍可查看底层传输回执,但其中 data 是宿主的原始 JSON 字符串。submit/cancel/nudge 原样委托既有任务机制,默认 post、nudge=false。源码请求文件提交后不得修改,宿主核对 SHA256。

选项与宿主缓存

内部复用 CompileCSharp 的实际 runtimeconfig/版本判断,在 CAD 内使用与目标运行时匹配的 SDK 所带 Roslyn。例如 SDK10 可以在外部编译 net8 宿主,但不能据此把 SDK10 编译器的运行时依赖任意加载进 .NET8 CAD。缺少匹配 SDK 时可由 process.dotnet 私有安装,不改全局 PATH。

编译器服务另置于一个私有、常驻的 AssemblyLoadContext,薄入口宿主不强引用 Roslyn;跨边界只传框架类型(源码、路径、字节、JSON)。这避免与 CAD 或其他插件所带的 Roslyn 冲突。本机 CAD 目录自带 4.10,而所选 SDK8.0.425 为 4.11;直接放入默认上下文的初版在冷启动失败,隔离后按实际 4.11 完成调用。这里的编译器上下文刻意常驻,和每次业务脚本的 collectible ALC 不同。

宿主缓存键包含宿主/编译器服务源码、CAD 路径/版本/托管 DLL 内容和编译器版本;同一缓存构建用进程互斥保护。只准备而不加载可用 com.cad.script.prepare(app,options),返回 host,err,info。缓存清单只在两个小组件均构建成功、依赖复制完成后发布。

构建目录、Roslyn 依赖和任务记录默认保留,不覆盖/删除被 CAD 加载的 DLL。关闭相关 CAD 后可以清理自己不再需要的缓存和请求记录。

已验证范围与卸载事实

2026-09-24 在本机 AutoCAD 2026 / .NET8 实测了参数、匿名结果、几何回读、异常回滚、保存重开、恢复与连续 100 份源码。旧 Framework 不走短脚本宿主;.NET10 选型路径仍需相应 CAD 实机验收。

每次业务脚本使用 collectible ALC,并在单独的 NoInlining 执行帧中请求卸载。本机 CAD 中,执行过这些脚本的 ALC 在诊断 GC 后仍存活;空程序集仅加载不执行可以回收。独立 .NET 进程里的动态匿名结果测试可以回收。尚未定位 CAD 内存活上下文的具体根引用,不能宣称完整热卸载已经解决。

最终隔离版本冷启动后的 100 个微小脚本样本:托管内存净增加约 8.09 MiB,进程私有内存末值反而比批次前低约 7.37 MiB,存活 ALC 从 8 到 108。私有内存会随 GC/运行时波动,末值下降不代表脚本 ALC 已卸载。这个样本支持继续使用短脚本路线,但不是长期内存上限,也不能用 IL 大小估算真实占用。暖态内存编译中位约 9.5 ms,而包含现有 COM 任务往返的中位约 1.66 s,二者不可混淆。

只在需要时查看统计:

var result,err=com.cad.script.stats(cad,false,doc); // 不触发全局GC
print(result,err);

com.cad.script.stats(cad,true,doc) 是显式 GC 诊断,可能暂停整个 CAD;不要放进日常每次脚本执行。ALC 隔离不保护 CAD 免受任意错误/恶意代码影响,也不能强制停止死循环。

Markdown 格式