Deno op2 参数类型参考:Rust 侧 Op 参数完整支持矩阵与底层校验机制
本文以 Deno 仓库中的 valid_args.md 为核心,系统讲解 #[op2] 宏(op2)支持的 Rust 侧参数类型全集:每类 Rust 类型对应哪些 V8 类型、是否走 fastcall 快速路径、以及各自的语义陷阱(如字符串拷贝、空 buffer 传 null、重入修改风险等),并结合仓库源码揭示这张"支持矩阵"是如何被测试自动维护、与 op2/README.md 保持同步的。读完本文,你能在为 Deno 扩展编写 Rust op 时正确选型参数类型,并理解每行矩阵背后的实现与校验逻辑。
一、op2 与 valid_args.md 的角色定位
op2 是 libs/ops/op2/README.md 中定义的 #[op] 的替代实现("in-progress replacement"),用于把 Rust 函数注册为 JavaScript 可调用原语。在 op2 的参数体系中,一个核心问题是:Rust 函数签名中的某个参数类型,能否被宏识别?它能接受 JavaScript 侧的哪些 V8 值?调用时走 fastcall(V8 直接传参的优化路径)还是慢速路径?
valid_args.md 就是这份"参数支持矩阵"的权威清单。它的表格共五列:
- Supported:该类型是否已被 op2 生成器测试覆盖(
X表示已验证可编译生成); - Rust:op 函数参数侧使用的 Rust 类型(含
#[attr]属性前缀); - Fastcall:该参数是否支持 V8 fastcall 快速路径(
X表示支持); - V8:JavaScript 侧允许传入的 V8 值类型;
- Notes:语义注意事项(拷贝行为、空值行为、性能警告等)。
值得强调的是,这个文件不是人工维护的静态文档——它是 op2 宏测试套件的数据源。mod.rs 中的 test_valid_args_md 测试会把表中每一行都构造成一个真实的 op 函数,调用 generate_op2 实际跑一遍宏展开;同时把表格渲染成 HTML 写入 README 的 <!-- START ARGS --> 区块,两者必须完全一致否则测试失败。也就是说:表中每一行"支持"的类型,都有对应的宏展开证据;README 中的参数表格,则是由本表实时生成的。
二、完整参数支持矩阵
以下完整继承 valid_args.md 的全部条目,按类型分组并补充说明。
2.1 数值与布尔类型
| 支持 | Rust 参数类型 | Fastcall | V8 侧允许的类型 | 备注 |
|---|---|---|---|---|
| X | bool |
X | Bool |
|
| X | i8 |
X | Uint32, Int32, Number, BigInt |
|
| X | u8 |
X | Uint32, Int32, Number, BigInt |
|
| X | i16 |
X | Uint32, Int32, Number, BigInt |
|
| X | u16 |
X | Uint32, Int32, Number, BigInt |
|
| X | i32 |
X | Uint32, Int32, Number, BigInt |
|
| X | u32 |
X | Uint32, Int32, Number, BigInt |
|
| X | #[smi] ResourceId |
X | Uint32, Int32, Number, BigInt |
SMI 内部是有符号整数表示,但无符号 #[smi] 类型在 Rust 侧会被按位转换为无符号值;JavaScript 代码看到的仍然是有符号整数 |
| X | #[bigint] i64 |
X | Uint32, Int32, Number, BigInt |
|
| X | #[bigint] u64 |
X | Uint32, Int32, Number, BigInt |
|
| X | #[bigint] isize |
X | Uint32, Int32, Number, BigInt |
|
| X | #[bigint] usize |
X | Uint32, Int32, Number, BigInt |
|
| X | f32 |
X | Uint32, Int32, Number, BigInt |
|
| X | f64 |
X | Uint32, Int32, Number, BigInt |
要点解读:
- 32 位整数与浮点数均支持 fastcall,且 JS 侧
Number、BigInt与 typed array 元素类型都可以传入; #[smi]是 Deno 资源句柄(ResourceId)的专用标注。从源码结构看,signature.rs 中NumericArg枚举专门保留了__SMI__占位符用于承载#[smi]标注的参数,且注释明确"A placeholder argument for arguments annotated with #[smi]";编译测试 test_cases/sync/smi.rs 则同时验证了有符号/无符号#[smi]参数(Int16/Int32/Uint16/Uint32别名类型)与Option<#[smi]>三种形态;- 64 位整数必须显式标注
#[bigint],否则 64 位值无法在 JS 侧精确表达。
2.2 字符串类型(四档,性能与安全性逐级递减)
| 支持 | Rust 参数类型 | Fastcall | V8 | 备注 |
|---|---|---|---|---|
| X | #[string] String |
X | String |
Fastcall 仅在字符串为 Latin-1 时可用。总是创建一份堆分配的 UTF-8 拷贝 |
| X | #[string] &str |
X | String |
Fastcall 仅在 Latin-1 时可用。若数据放不进栈缓冲区才创建 owned String 拷贝;fastcall 下绝不分配,但会做 Latin-1 → UTF-8 拷贝 |
| X | #[string] Cow<str> |
X | String |
Fastcall 仅在 Latin-1 时可用。放不进栈时创建 Cow::Owned 拷贝;fastcall 下总是 Cow::Borrowed,但仍做 Latin-1 → UTF-8 拷贝 |
| X | #[string(onebyte)] Cow<[u8]> |
X | String |
最快的 String 类型参数。若字符串不是 Latin-1 则抛出 TypeError |
这四档差异的根源在 op2/README.md 的 "Strings" 一节:Rust 的 String 恒为 UTF-8,而 V8 字符串要么是双字节 UTF-16、要么是单字节 Latin-1;单字节 Latin-1 与 UTF-8 不是字节兼容的(码位 128–255 在 UTF-8 中占两个字节)。因此 op 参数中的一切字符串都至少需要一次拷贝来保证不把 Latin-1 数据误当 UTF-8 处理;op2 的优化在于尽可能用栈缓冲区避免堆分配。
选型建议(与矩阵 Notes 一致):
- 只要一个临时引用、追求最低开销:
#[string] &str; - 有时能借用、有时需持有:
#[string] Cow<str>; - 确实要拿走所有权:
#[string] String; - 热路径上确定输入是 ASCII/Latin-1(例如协议帧、路径名):
#[string(onebyte)] Cow<[u8]>,代价是非 Latin-1 输入直接TypeError。编译测试见 test_cases/sync/string_onebyte.rs:
#[op2(fast)]
fn op_string_onebyte(#[string(onebyte)] s: Cow<[u8]>) -> u32 {
s.len() as _
}
2.3 V8 句柄类型
| 支持 | Rust 参数类型 | Fastcall | V8 |
|---|---|---|---|
| X | &v8::Value |
X | any |
| X | &v8::**V8** |
X | V8(泛指 String/Object/Function 等具体 v8 类型) |
| X | v8::Local<v8::Value> |
X | any |
| X | v8::Local<v8::**V8**> |
X | V8 |
表中 &v8::**V8** / v8::Local<v8::**V8**> 是通配行,代表任意具体 v8 类型(如 &v8::String、v8::Local<v8::Object> 等)都被支持。这一机制可以从校验逻辑中确认:mod.rs 的 parse_md 函数在解析时检测到 **V8** 占位符,会将其展开为 String、Object、Function、... 四种具体类型,逐一构造 fn op_test(x: 类型) 跑宏展开验证。
2.4 自定义类型转换(三种 trait 路径)
| 支持 | Rust 参数类型 | Fastcall | V8 | 备注 |
|---|---|---|---|---|
| X | FromV8Scopeless |
any | 任何实现 deno_core::convert::FromV8Scopeless 的类型 |
|
| X | #[scoped] FromV8Type |
any | 任何实现 deno_core::convert::FromV8 的类型。⚠️ 可能较慢 |
|
| X | #[scoped] (Tuple, Tuple) |
any | 任何实现 deno_core::convert::FromV8 的类型。⚠️ 可能较慢 |
|
| X | #[serde] SerdeType |
any | ⚠️ 可能较慢。遗留写法,不推荐,请改用 FromV8 trait 与宏 |
|
| X | #[serde] (Tuple, Tuple) |
any | ⚠️ 可能较慢。遗留写法,不推荐 |
这与 op2/README.md "Argument conversion" 一节的描述对应:非 fast op 的参数默认走 FromV8Scopeless trait——该 trait 不需要 v8 scope 即可完成转换,因此对许多类型更高效;需要访问 scope 的类型显式加 #[scoped] 改走 FromV8:
fn op_xyz(#[scoped] arg: MyFromV8Type) -> X {}
注意:三类自定义转换(FromV8Scopeless、#[scoped]、#[serde])均不支持 fastcall(矩阵 Fastcall 列留空),因为它们无法在 V8 快速路径的扁平参数列表中表达。
2.5 缓冲区类型:anybuffer / arraybuffer / buffer
这是矩阵中最庞大、也最需要注意安全语义的一组。
#[anybuffer](任意 ArrayBuffer 或 ArrayBufferView)——表中未标 "Supported X",即生成器可接受该写法,但尚未列入编译测试覆盖集合:
| Rust 参数类型 | Fastcall | V8 | 备注 |
|---|---|---|---|
#[anybuffer] &mut [u8] |
X | ArrayBuffer, ArrayBufferView (resizable=true,false) | ⚠️ 若 V8 被重入调用,JS 可能修改 slice 内容 |
#[anybuffer] &[u8] |
X | ArrayBuffer, ArrayBufferView (resizable=true,false) | ⚠️ 同上 |
#[anybuffer] *mut u8 |
X | ArrayBuffer, ArrayBufferView (resizable=true,false) | ⚠️ 同上;且 V8 在 fastcall 中对空数组的处理是总是传 null |
#[anybuffer] *const u8 |
X | ArrayBuffer, ArrayBufferView (resizable=true,false) | ⚠️ 同上;空数组总是传 null |
#[arraybuffer](严格限定 ArrayBuffer):
| 支持 | Rust 参数类型 | Fastcall | V8 | 备注 |
|---|---|---|---|---|
| X | #[arraybuffer] &mut [u8] |
X | ArrayBuffer (resizable=true,false) | ⚠️ 重入时 JS 可能修改 slice |
| X | #[arraybuffer] &[u8] |
X | ArrayBuffer (resizable=true,false) | ⚠️ 同上 |
| X | #[arraybuffer] *mut u8 |
X | ArrayBuffer (resizable=true,false) | ⚠️ 同上;空数组在 fastcall 中传 null |
| X | #[arraybuffer] *const u8 |
X | ArrayBuffer (resizable=true,false) | ⚠️ 同上;空数组在 fastcall 中传 null |
| X | #[arraybuffer(copy)] Vec<u8> |
X | ArrayBuffer (resizable=true,false) | 安全,但强制拷贝 |
| X | #[arraybuffer(copy)] Box<[u8]> |
X | ArrayBuffer (resizable=true,false) | 安全,但强制拷贝 |
| X | #[arraybuffer(copy)] bytes::Bytes |
X | ArrayBuffer (resizable=true,false) | 安全,但强制拷贝 |
#[buffer](严格限定 TypedArray):
| 支持 | Rust 参数类型 | Fastcall | V8 | 备注 |
|---|---|---|---|---|
| (未列 X) | #[buffer] &mut [u8] |
X | UInt8Array (resizable=true,false) |
⚠️ 重入时 JS 可能修改 slice |
| (未列 X) | #[buffer] &[u8] |
X | UInt8Array (resizable=true,false) |
⚠️ 同上 |
| (未列 X) | #[buffer] *mut u8 |
X | UInt8Array (resizable=true,false) |
⚠️ 同上;空数组传 null |
| (未列 X) | #[buffer] *const u8 |
X | UInt8Array (resizable=true,false) |
⚠️ 同上;空数组传 null |
| X | #[buffer(copy)] Vec<u8> |
X | UInt8Array (resizable=true,false) |
安全,但强制拷贝 |
| X | #[buffer(copy)] Box<[u8]> |
X | UInt8Array (resizable=true,false) |
安全,但强制拷贝 |
| X | #[buffer(copy)] bytes::Bytes |
X | UInt8Array (resizable=true,false) |
安全,但强制拷贝 |
| X | #[buffer] &mut [u32] |
X | UInt32Array (resizable=true,false) |
⚠️ 重入时 JS 可能修改 slice |
| X | #[buffer] &[u32] |
X | UInt32Array (resizable=true,false) |
⚠️ 同上 |
| X | #[buffer(copy)] Vec<u32> |
X | UInt32Array (resizable=true,false) |
安全,但强制拷贝 |
| X | #[buffer(copy)] Box<[u32]> |
X | UInt32Array (resizable=true,false) |
安全,但强制拷贝 |
| (未列 X) | #[buffer] V8Slice |
X | ArrayBufferView (resizable=false) | ⚠️ JS 可能修改来自 buffer 的 slice |
| (未列 X) | #[buffer(detach)] V8Slice |
X | ArrayBufferView (resizable=true,false) | 安全 |
| (未列 X) | #[buffer] V8ResizableSlice |
X | ArrayBufferView (resizable=true) | ⚠️ JS 可能修改 slice |
| (未列 X) | #[buffer] JsBuffer |
X | ArrayBufferView (resizable=false) | ⚠️ JS 可能修改来自 buffer 的 slice |
| X | #[buffer(detach)] JsBuffer |
ArrayBufferView (resizable=true,false) | 安全 | |
| (未列 X) | #[buffer(unsafe)] bytes::Bytes |
X | ArrayBufferView (resizable=false) | ⚠️ JS 可能修改 buffer 内容 |
| (未列 X) | #[buffer(detach)] bytes::Bytes |
X | ArrayBufferView (resizable=true,false) | 安全 |
缓冲区选型核心逻辑:
- 借用形式(
&[u8]、&mut [u8]、裸指针、V8Slice、#[buffer] JsBuffer)零拷贝,但 Rust 拿到的只是对 JS 堆内存的引用——只要 op 体内可能重新进入 V8(比如触发回调),JS 就可以改写这些数据,矩阵对每一行都标了 ⚠️; (copy)形式(Vec<u8>/Box<[u8]>/bytes::Bytes)强制拷贝,语义安全且支持 fastcall,代价是一次内存复制;(detach)形式在转换时把 JS 侧 ArrayBuffer detach(后续 JS 访问会抛异常),从而保证 Rust 独占数据,矩阵标注为 "Safe";(unsafe)形式(如#[buffer(unsafe)] bytes::Bytes)零拷贝且不 detach,是性能与风险的最极端组合,只在确认 JS 不会重入修改时使用;- 裸指针 + fastcall 组合有一个特殊边界:V8 对空数组的处理导致 fastcall 下总是传
null,Rust 侧必须容错处理空指针。
2.6 外部指针与运行时上下文参数
| 支持 | Rust 参数类型 | Fastcall | V8 | 备注 |
|---|---|---|---|---|
| X | *const std::ffi::c_void |
X | External |
|
| X | *mut std::ffi::c_void |
X | External |
|
| X | &OpState |
X | 从 JS 侧无对应值,由运行时注入 | |
| X | &mut OpState |
X | 同上 | |
| X | Rc<RefCell<OpState>> |
X | 同上 | |
| X | &JsRuntimeState |
X | 仅在 deno_core 内部可用 |
External 是 V8 用于携带不透明 C 指针值的类型,op2 允许把 JS 传入的 External 直接映射为 c_void 指针;OpState 三个变体则是 op 框架自动注入的运行时状态容器(存放资源表、扩展状态等),不占用 JS 侧参数位。
三、这张矩阵在仓库中如何被维护与校验
理解矩阵的生成与校验机制,是判断"表里写的"与"实际支持的"是否一致的关键。以下事实均可在仓库中定位:
-
每行都是可编译的。mod.rs 中的
parse_md函数跳过表头两行后逐行解析:对每行取出 Rust 类型列,若以#[attr]开头则拆出属性与裸类型,拼出形如fn op_test(#[attr] x: Type) {}的函数体(**V8**行会展开为 String/Object/Function 等具体类型),随后真实调用generate_op2完成一次宏展开——展开失败测试直接报错。也就是说矩阵不是"文档声称支持",而是"宏确实能为它生成代码"。 -
README 参数表由本文件渲染生成。
test_valid_args_md会把 valid_args.md 渲染成 HTML 表格,插入 op2/README.md 的<!-- START ARGS -->与<!-- END ARGS -->分隔符之间,然后断言渲染结果与 README 现存内容逐字节相等;设置环境变量UPDATE_EXPECTED后测试会反向把渲染结果写回 README。test_valid_retvals_md对返回类型表 valid_retvals.md 做同样的事(分隔符为<!-- START RV -->/<!-- END RV -->,且额外为支持 async 的行生成async fn op_test() -> Type再跑一遍)。 -
类型枚举与矩阵同源。signature.rs 定义了宏解析参数时使用的
NumericArg枚举(bool/i8…f64/isize/usize外加__SMI__、__VOID__两个占位符)与V8Arg枚举(Value、External、Object、ArrayBuffer、各 TypedArray、BigIntObject、Function等),二者分别对应矩阵的 "Rust" 列与 "V8" 列;NumericArg::v8_array_type还给出了整数/浮点参数与 V8 typed array 的映射(如u32 → Uint32Array、u64 → BigUint64Array),这解释了矩阵中#[buffer] &mut [u32]对应UInt32Array的实现依据。 -
行为回归用例齐全。test_cases/sync/ 目录下有
smi.rs、string_onebyte.rs、string_ref.rs、string_cow.rs、buffers.rs、buffers_copy.rs、bigint.rs、from_v8.rs、serde_v8.rs等编译测试文件,分别钉住矩阵中各特殊参数的宏展开形态,防止后续重构悄悄改变某一行的语义。
四、实操选型清单
综合矩阵 Notes 列与 op2/README.md 的说明,编写 op 参数时可按下面的决策路径:
- 数值:32 位整数/浮点直接用,免费获得 fastcall;64 位一律
#[bigint](或返回值侧用#[number]限制在安全整数区间,见 valid_retvals.md);资源句柄用#[smi] ResourceId,注意 JS 侧永远看到有符号值。 - 字符串:热路径优先
&str→Cow<str>→String三档按"是否需要持有、能否借用"选择;确定 Latin-1 输入才用#[string(onebyte)]。牢记任何字符串参数都至少有一次 Latin-1 → UTF-8 拷贝,fastcall 下&str/Cow<str>走栈缓冲零分配。 - 缓冲区:需要 JS 保持可读选
(copy)或(detach);追求零拷贝用借用形式,但要接受"重入可被改写"的契约;fastcall 下裸指针形态记得处理空数组 = null 的边界。 - 自定义结构:优先实现
FromV8Scopeless(免 scope、更快);必须访问 scope 才降级到#[scoped] FromV8;#[serde]属于遗留路径,矩阵中明确标注 "Legacy & not recommended"。 - 任意值:需要透传用
&v8::Value或v8::Local<v8::Value>,两者均支持 fastcall。
五、相关文档
- op2/README.md:op2 宏总览,含字符串拷贝原理、fallible op、async op 的 eager/lazy/deferred 语义、fastcall 注解(
fast/nofast/fast(op_XYZ))与 CppGC 对象支持; - valid_retvals.md:与本文对应的返回值支持矩阵(注意返回值侧
#[number]要求结果落在Number.MIN_SAFE_INTEGER与Number.MAX_SAFE_INTEGER区间内); - signature.rs:宏签名解析实现,
NumericArg/V8Arg枚举所在; - test_cases/:sync/async 两类编译测试,矩阵每一行语义的行为级佐证。
atomcodeClaude Code 的开源替代方案。连接任意大模型,编辑代码,运行命令,自动验证 — 全自动执行。用 Rust 构建,极致性能。 | An open-source alternative to Claude Code. Connect any LLM, edit code, run commands, and verify changes — autonomously. Built in Rust for speed. Get StartedRust0622
Hy4-previewHy4 preview 是由腾讯混元团队研发的新一代混合专家(MoE)旗舰模型。模型总参数量 770B,每个 token 激活 49B,主干共包含78层,第一层采用标准 FFN,其余 77 层均为 MoE 结构,每层包含 256 个路由专家与 1 个共享专家,每个 token 激活 top-8 路由专家及共享专家。主干之外原生内置 1 层 MTP(总参数量 10B,激活 0.7B)以支持投机解码。Python00
GLM-5.3GLM-5.3 与 GLM-5.2 使用相同的基座模型——所有提升均来自后训练。与 GLM-5.2 相比,它在复杂编程和长程任务上的表现显著提升。Jinja00
GLM-5.3-FlashGLM-5.3-Flash (320B-A18B),是GLM-5系列的首个原生多模态模型。320B总参数,能力超过GLM-5.2Jinja00
Spark-X2.5-4BSpark-X2.5-4B 旨在让强大的 AI 更实用、更高效、更易获得。在广泛日常任务中表现强劲,涵盖对话、写作、翻译、推理、编码、工具调用以及智能体工作流,并在同等规模的开源模型中取得领先成绩。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00
Spark-X2.5-1.7BSpark-X2.5-1.7B 旨在让强大的 AI 更加实用、高效且易于获取。这些模型在广泛的日常任务中表现出色,涵盖对话、写作、翻译、推理、编程、工具调用和智能体工作流,并在同等规模的开源模型中取得领先结果。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00