# 在 AutoCAD 内运行 C# 短脚本

`cad.RunCSharp(source,args,doc,options)` 将完整 C# 源码按次内存编译，在当前 CAD 文档上下文执行，再返回普通数据。适合一次性查询、计算、批量图元操作。常驻事件、菜单和后台服务仍使用 [CompileCSharp 插件](csharp.html)。

- 通信继续使用 COM / LispFunction / `com.cad.task`，没有新 Socket 或管道。
- 小型宿主首次准备、加载后复用；业务脚本不反复 `dotnet build` 或 NETLOAD。
- 这是同步短脚本执行器，**不是安全沙箱、强制终止机制或已实现完整热卸载的插件系统**。

当前短脚本宿主仅支持.NET8+ AutoCAD；中望/浩辰使用CompileCSharp DLL或COM/LISP，见[兼容与迁移](compatibility.html)。

## 最小调用

以下只读取当前文档并返回数据，不绘图；要求已经打开目标文档。

```aardio
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另取宿主：

```aardio
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`，提供以下**同步**静态方法：

```csharp
public static object Execute(Document doc, Database db, Editor ed,
                             Transaction tr, JsonElement args)
```

- `doc/db/ed` 属于任务绑定的活动文档；`tr` 由宿主创建。实际事务可以是 `Transaction` 的子类，不应比较具体类名。
- `args` 是 JSON 参数；省略时为 JSON null。支持对象、数组、字符串、数值、布尔值、Unicode、NUL 等 JSON 数据。
- 返回普通标量、数组/列表、字符串键字典、匿名对象或简单 POCO，也可以返回 `JsonElement`。返回值在提交事务前编码，外部得到 aardio 的普通表/数组/标量。
- 不返回 `Document/Database/DBObject/Transaction`、委托、Task、反射对象等活对象；图元身份返回 Handle 字符串。数值精度仍受 JSON/调用方数值类型约束。
- 请求上限 4 MiB；输出上限 2 MiB、深度 32、遍历节点预算 100000。NaN/Infinity、循环引用或不可编码结果正常报错。
- 自动引用实际 CAD 的 `AcCoreMgd/AcDbMgd/AcMgd` 及当前 .NET Core/Desktop 框架。首版不提供任意 NuGet/第三方引用管理；需要长期依赖的复杂插件走 CompileCSharp。

结果转换不把脚本动态类型交给长期序列化元数据缓存；只将宿主自身类型和已经编码的 JSON 送出。

## 事务边界

宿主锁定目标文档，开启事务，调用入口并编码返回结果，最后提交。正常调用的 `result.committed` 表示宿主事务已提交；编译或入口错误不会执行脚本。

脚本使用传入的 `tr.GetObject`、`tr.AddNewlyCreatedDBObject` 等标准 CAD API；**不要自行 Commit/Abort/Dispose 传入的事务，不切换或关闭文档，不修改宿主全局状态**。新建 DBObject 等本地资源按 AutoCAD API 约定释放。

执行异常或结果编码错误发生在宿主提交前时，未提交事务会释放并回滚事务内数据库修改。实机已验证“创建圆后抛异常”和“创建圆后返回不可编码的 CAD 活对象”均未留下圆。

这不是任意副作用事务：文件/网络/事件/系统变量、其他文档、脚本自行提交的事务不在这项承诺内。不要注册长期事件或启动持久后台线程；需要这些功能时改走常驻插件。提示词和入口检查不是隔离边界。

## 完成、失败与恢复

`run`/增强 `RunCSharp` 返回 `result,err,task`：

- 创建失败（源码/SDK/构建/参数错误）：`null,err`。
- 提交后保留 `task.path`。`result.ok` 同时表示传输完成和 C# 业务成功。
- `phase` 区分 `host_load`、`request`、`compiler`（编译器初始化）、`compile`、`entry`、`context`、`execute`、`result`、`commit` 等阶段。C# 编译诊断包含编号、消息和行列。
- `state=timeout, terminal=false` 只是等待快照，代码可能继续运行。**不要换任务ID重发**；取消也不能中断已开始的 C#。
- `compileMs/executeMs/encodeMs/totalMs` 是 CAD 宿主内分段耗时；`run` 的 `elapsed` 还包含宿主准备、传输与等待。不能把内存编译毫秒数当作完整往返耗时。

需要在等待前持久化路径时使用 `create`：

```aardio
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：

```aardio
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。

## 选项与宿主缓存

- `timeout/startTimeout/method/directory/nudge` 等沿用 [任务选项](tasks.html)，`directory` 仍是任务目录。
- `hostDirectory` 指定宿主构建与缓存目录。默认在当前用户 LocalAppData 的 `aardio/com.cad/script-hosts`。
- `sdkOptions` 传给 `process.dotnet`，例如 `{install=false}`。通常无需指定 SDK 路径或版本。
- 首次加载与 `NetLoad` 共用内部实现，无需安全选项或手动配置目录。CAD 允许时临时调整加载检查并恢复；受限时由 CAD 原生提示或拒绝，出现确认框请由用户处理。宿主成功加载并完成设置恢复后才执行业务脚本，后续直接复用。
- 首次用户确认可能超过等待时间；超时后继续读取原任务，不重新提交。加载失败报告 `phase=host_load`。

内部复用 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，二者不可混淆。

只在需要时查看统计：

```aardio
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 免受任意错误/恶意代码影响，也不能强制停止死循环。
