ARTICLE DETAIL

建站实战干货

来自一线的建站与推广经验沉淀,每一条都经过真实交付验证。

workerd 的 JSG Rust 过程宏指南:用 `jsg_macros` 为 Cloudflare Workers 运行时零样板绑定 JavaScript API

2026/9/16 17:58:01 拓冰建站 浏览量
workerd 的 JSG Rust 过程宏指南:用 `jsg_macros` 为 Cloudflare Workers 运行时零样板绑定 JavaScript API workerd 的 JSG Rust 过程宏指南用jsg_macros为 Cloudflare Workers 运行时零样板绑定 JavaScript API【免费下载链接】workerdThe JavaScript / Wasm runtime that powers Cloudflare Workers项目地址: https://gitcode.com/GitHub_Trending/wo/workerd导读jsg-macros 是 workerdCloudflare Workers 的 JavaScript/Wasm 运行时中负责 Rust ↔ JavaScript 绑定的过程宏 crate它为 JSGJavaScript Glue类型系统提供了#[jsg_resource]、#[jsg_method]、#[jsg_property]、#[jsg_oneof]等一整套属性宏用于把 Rust 类型与方法声明式地暴露给 V8。阅读完本文你将掌握如何声明一个可被new构造、带实例/静态方法、属性访问器与 GC 追踪语义的 JSG 资源类型理解宏在底层生成的jsg::Type/jsg::ToJS/jsg::FromJS/jsg::Traced实现并能结合仓库源码与测试用例在自己的绑定代码中正确选用每个宏。背景JSG 与过程宏的分工workerd 的 JSG 绑定层在 src/rust/jsg 与 src/workerd/jsg 两处提供运行时支持jsg::Lock、jsg::Rc、jsg::Member、jsg::PropertyKind等而jsg_macros只负责编译期代码生成。两个 crate 是分离的从 lib.rs 顶部的注释可以看到过程宏 crate 甚至不能链接 CXX-bridge 运行时 crate因此 resource.rs 中需要维护一个与运行时jsg::PropertyKind平行的编译期镜像枚举Prototype/Instance/Inspect仅在生成 token 时映射到运行时的jsg::PropertyKind。Crate 布局文件内容lib.rs所有宏的公共入口 —— 纯分发器resource.rs#[jsg_resource]在 struct 与 impl block 上的代码生成trace.rsGC trace 代码生成 —— 字段分类与trace()方法体生成utils.rs共享辅助函数extract_named_fields、snake_to_camel、is_lock_ref等整个 crate 在 lib.rs 声明了#![forbid(unsafe_code)]所有unsafe均发生在宏生成的调用方代码中例如从 V8 FFI 指针还原FunctionCallbackInfo。#[jsg_struct]把纯数据结构投影为普通 JS 对象#[jsg_struct]为普通的 Rust 结构体生成jsg::Struct、jsg::Type、jsg::ToJS、jsg::FromJS四个实现并附带一个空的jsg::Traced实现纯数据无 GC 边。关键语义见 lib.rs 的生成逻辑只有pub字段会被投影进 JavaScript 对象非pub字段在ToJS::to_js的字段赋值循环中被直接跳过从 JS 对象还原时FromJS::from_js每个 pub 字段通过obj.get(lock, name)读取缺失即抛TypeError: Missing property ...入参校验非对象输入抛TypeError: Expected object but got typename MyName覆盖的是Type::class_name()返回的元数据字符串用于类型诊断/报错并非定义一个 JS 类——定义 JS 类需要#[jsg_resource]。#[jsg_struct] pub struct CaaRecord { pub critical: f64, pub tag: String, pub value: String, } #[jsg_struct(name CustomName)] pub struct MyRecord { pub value: String, }仓库真实使用示例dns.rs 与 url.rs 中大量采用#[jsg_struct]声明 DNS/URL 相关的纯数据记录。#[jsg_method]为资源生成 V8FunctionCallback#[jsg_method]把一个#[jsg_resource]impl 块内的方法编译为一个extern C fn形式的 V8 回调命名规则fn_name_callback。其行为规则实例方法带self/mut self接收者注册在原型上静态方法无接收者注册在构造函数上返回类型为ResultT, E时Err会自动通过lock.throw_exception(err.into())抛成 JS 异常见 lib.rsRustsnake_case名称自动转为 JScamelCase可用#[jsg_method(name jsName)]覆盖第一个类型化参数可以是mut Lock/mut jsg::Lock宏会直接把回调的 isolate lock 传进去不占 JS 参数位。utils.rs 的is_lock_ref精确匹配mut Lock裸导入与mut jsg::Lock全限定路径两种写法其单元测试覆盖了Lock、mut String、Lock等负例。参数解包统一走T as jsg::FromJS::from_js(mut lock, args.get(i))失败即抛异常返回对str这类引用类型FromJS返回的是拥有的String宏会补一个借用lib.rs。实例方法回调中还有一个值得注意的防御逻辑lib.rs虽然方法注册时会带上 V8Signature让 V8 在回调前就抛TypeError: Illegal invocation但属性访问器#[jsg_property]/#[jsg_inspect_property]复用同一回调生成代码却不带签名因此恶意输入如Object.getOwnPropertyDescriptor(proto, p).get.call({})可能带着错误 receiver 抵达回调。此时宏通过WrappableRc::from_js解析资源失败则显式抛出Illegal invocation的TypeError而不是让catch_panic以内部错误终止执行。#[jsg_resource] impl DnsUtil { // 实例方法 —— obj.parseCaaRecord(…) #[jsg_method] pub fn parse_caa_record(self, record: String) - ResultCaaRecord, jsg::Error { … } // 实例方法 —— obj.getName() #[jsg_method] pub fn get_name(self) - String { … } // 静态方法 —— DnsUtil.create(…) #[jsg_method] pub fn create(name: String) - Resultjsg::RcSelf, jsg::Error { … } }#[jsg_resource]声明 JSG 资源类型#[jsg_resource]是绑定层的核心宏可同时作用于 struct 与 impl blocklib.rs 先尝试按ItemImpl解析失败则按DeriveInput处理作用于 struct时生成jsg::Typeclass_name()返回name JSName覆盖值默认 Rust 类型名jsg::ToJS把Self包进jsg::Rc::new(self)再转 JS见 resource.rsjsg::FromJSResultType jsg::RcSelf完全委托给jsg::RcSelf as jsg::FromJSjsg::Traced自动合成trace()见下文 GC 章节jsg::GarbageCollectedmemory_name()返回一个指向只读数据段的CStrC 侧可零分配构造kj::StringPtr见 resource.rs。作用于 impl block时生成jsg::Resource::members()扫描并注册全部#[jsg_method]、#[jsg_property]、#[jsg_inspect_property]、#[jsg_constructor]与#[jsg_static_constant]成员生成顺序为构造函数 → 方法 → 属性 → 常量resource.rs。#[jsg_resource] pub struct DnsUtil { cache: HashMapString, jsg::RcCacheEntry, // 自动 trace name: String, // 纯数据tracer 忽略 } #[jsg_resource] impl DnsUtil { #[jsg_method] pub fn lookup(self, host: String) - ResultString, jsg::Error { … } #[jsg_method] pub fn create() - Self { … } }注意#[jsg_resource]impl 块要求 self 类型是简单路径类型impl MyResource否则直接产出编译错误resource.rs。#[jsg_constructor]定义new MyClass(…)#[jsg_constructor]标记一个静态方法无self接收者、返回Self作为 JS 构造函数每个 impl block只允许一个构造函数多写一个直接compile_error!resource.rs带self或返回值不是Self都会被validate_constructor拒绝并给出编译错误resource.rs仓库配套单测覆盖了这三种情形可选的首参数mut Lock会被传入 isolate lock 且不占 JS 参数位extract_constructor_params会跳过它见 resource.rs没有构造函数时new MyClass()会抛Illegal constructor与 C JSG 行为一致生成的构造回调把资源包进jsg::Rc::new(resource)后调用rc.attach_to_this(mut args)挂接到this。#[jsg_resource] impl Greeting { #[jsg_constructor] fn constructor(message: String) - Self { Self { message } } } // JS: let g new Greeting(hello);#[jsg_static_constant]暴露数字常量把 Rustconst暴露为同时挂在构造函数与其原型上的只读 JS 属性等价于 CJSG_STATIC_CONSTANT。常量名原样使用、不做 camelCase 转换Rust 与 JS 的常量惯例都是UPPER_SNAKE_CASE且只支持数值类型i8..i64、u8..u64、f32、f64见 lib.rs。它本质是标记宏——本身原样透传 item真正的注册由外层#[jsg_resource]在 impl block 上完成lib.rs注册为jsg::Member::StaticConstant { name, value: jsg::ConstantValue::from(Self::CONST) }。#[jsg_resource] impl WebSocket { #[jsg_static_constant] pub const CONNECTING: i32 0; #[jsg_static_constant] pub const OPEN: i32 1; } // JS: WebSocket.CONNECTING 0 / instance.OPEN 1#[jsg_property]属性访问器getter/setter#[jsg_property([placement,] [name ...] [, readonly])]把方法注册为资源的访问器属性placement 可选prototype或instance默认prototype。解析逻辑在 resource.rs 的parse_jsg_property_args同时写instance和prototype属于冲突放置会报编译错误未知参数也直接报错。prototype默认属性挂在原型链上Object.keys()不含它空数组但prop in obj为true可被子类覆盖等价于 CJSG_PROTOTYPE_PROPERTY/JSG_READONLY_PROTOTYPE_PROPERTY注意虽然不可枚举进Object.keys()但 prototype 属性本身是可枚举的for...in能遍历到这与 CregisterPrototypeProperty使用v8::PropertyAttribute::None的行为对齐——测试 resource_properties.rs 中有专门用例验证 enumerable 与for...in行为。instance每个实例上的自有属性Object.keys()包含它hasOwnProperty()返回true不可被子类覆盖等价于 CJSG_INSTANCE_PROPERTY/JSG_READONLY_INSTANCE_PROPERTY访问器描述符get/set键而非数据描述符无value/writable。绝大多数场景优先用prototype。自有属性访问器会阻止 minor-GC 对对象的回收并抑制部分 V8 优化原文档明确给出的性能建议。命名规则与 setter 检测方法名必须以get_getter或set_setter开头否则编译错误resource.rs前缀被剥离剩余部分做snake_case→camelCase因此get_foo_bar/set_foo_bar都映射为 JS 属性fooBar以set_开头的方法自动注册为 setter缺省 setter或显式readonly即只读属性严格模式下写入只读属性抛TypeErrorreadonly在编译期断言不存在同时被注解的同名 setter——若readonly用在 setter 上、或只读属性又配了 setter、或同组出现重复 getter/setter都会compile_error!见 resource.rs 的validate_and_emit_propertyname ...显式覆盖 JS 属性名此时方法名前缀剥离逻辑仍生效例如get_somethingname myProp→myPropCompat flag开启spec_compliant_property_attributes时按 Web IDL §3.7.6 设置 getter 的.length 0、setter 的.length 1、getter 的.name get name、setter 的.name set name。属性分组按(js_name, PropertyKind)键合并并保留源码声明顺序注册prop_groups_find_or_insert见 resource.rs。use std::cell::{Cell, RefCell}; use jsg_macros::{jsg_resource, jsg_property}; #[jsg_resource] struct Counter { value: Cellf64, id: RefCellString, kind: String } #[jsg_resource] impl Counter { // 原型属性 —— 读写setter 由 set_ 前缀自动识别 #[jsg_property(prototype)] pub fn get_value(self) - jsg::Number { jsg::Number::new(self.value.get()) } #[jsg_property(prototype)] pub fn set_value(self, v: jsg::Number) { self.value.set(v.value()); } // 原型属性 —— 只读显式 readonly #[jsg_property(prototype, readonly)] pub fn get_label(self) - String { counter.into() } // 原型属性 —— 显式 JS 名覆盖 #[jsg_property(prototype, name myProp)] pub fn get_something(self) - String { x.into() } // 实例属性 —— 读写自有属性 #[jsg_property(instance)] pub fn get_id(self) - String { self.id.borrow().clone() } #[jsg_property(instance)] pub fn set_id(self, v: String) { *self.id.borrow_mut() v; } // 实例属性 —— 只读 名覆盖 #[jsg_property(instance, name tokenKind, readonly)] pub fn get_kind(self) - String { self.kind.clone() } } // JS: obj.value 7; obj.value 7 // 原型读写 // obj.label // counter只读 // obj.myProp // x // Object.keys(obj) // [id, tokenKind]实例属性 // obj.hasOwnProperty(id) // true // obj.kind // TypeError: 只读测试佐证resource_properties.rs 对上述语义做了系统验证——包括 prototype 属性不进Object.keys()、in运算符可见、非自有属性、for...in可见、跨实例独立、与普通方法及#[jsg_static_constant]共存、多词snake_case的 camelCase 转换get_first_name→firstName、原始 Rust 方法名不暴露typeof p.getFirstName undefined、只读属性严格模式抛错/宽松模式静默忽略、OptionT返回值映射为 nullish 等。#[jsg_inspect_property]仅调试可见的符号属性#[jsg_inspect_property]把方法注册为 inspect 属性等价于 CJSG_INSPECT_PROPERTY注册为jsg::Member::Property { kind: PropertyKind::Inspect, .. }见 lib.rs同样复用jsg_method的回调生成getter 挂在一个唯一符号v8::Symbol下对普通属性访问完全不可见字符串键查找、Object.keys()、Object.getOwnPropertyNames()都看不到它通过node:util的inspect()与console.log()呈现inspect 属性恒为只读——对set_*方法注解它属于编译错误resource.rsname ...设置inspect()输出的符号描述否则方法名做snake_case→camelCase无前缀剥离因为没有 setter 概念运行时在原型上维护一个kResourceTypeInspect字典字符串名 → 唯一符号node:util依赖它遍历输出。use jsg_macros::{jsg_resource, jsg_inspect_property}; #[jsg_resource] struct ReadableStream { state: String } #[jsg_resource] impl ReadableStream { // 在 util.inspect() 输出中显示为 [state]: readable #[jsg_inspect_property] pub fn state(self) - String { self.state.clone() } // 显式符号描述 #[jsg_inspect_property(name streamState)] pub fn get_debug_state(self) - String { format!(state{}, self.state) } } // JS: typeof stream.state // undefined字符串键不可见 // Object.keys(stream) // [] // // util.inspect(stream) 通过符号展示该属性测试佐证inspect_property_not_accessible_by_string_key、inspect_property_not_in_object_keys、inspect_property_not_in_own_string_names验证了不可见性inspect_property_registered_under_a_symbol验证符号注册inspect_properties_dict_exists_on_prototype与inspect_properties_dict_contains_property_name验证kResourceTypeInspect字典。#[jsg_oneof]Rust 版本的联合类型#[jsg_oneof]为联合枚举生成jsg::Type和jsg::FromJS是 C JSGkj::OneOf…的 Rust 等价物lib.rs每个变体必须是单字段元组变体内部类型需实现jsg::Typejsg::FromJS违反则编译错误变体按声明顺序使用精确类型匹配try_from_js_exact逐一尝试第一个命中即返回全部不匹配时抛TypeError错误信息列出所有期望类型Expected one of [class_name, ...] but got type_name生成is_exact时对各变体做 OR 合并#(#is_exact_checks)||*枚举的class_name()返回stringify!(枚举名)。#[jsg_oneof] #[derive(Debug, Clone)] enum StringOrNumber { String(String), Number(jsg::Number), } #[jsg_resource] impl MyResource { #[jsg_method] pub fn process(self, value: StringOrNumber) - String { match value { StringOrNumber::String(s) format!(string: {s}), StringOrNumber::Number(n) format!(number: {}, n.value()), } } }测试佐证jsg_oneof.rs 用四个#[jsg_oneof]枚举StringOrNumber、NumberOrString、StringOrBool、ThreeTypes验证了字符串/数字/布尔匹配、StringOrNumber引用参数、null/undefined拒绝、变体声明顺序决定匹配优先级42在 String 优先的枚举中命中 string 分支以及 TypeError 消息包含全部期望类型。垃圾回收jsg::Traced的自动合成与手动接管#[jsg_resource]作用于 struct 时自动合成impl jsg::Traced for MyType { fn trace(self, visitor) { ... } }impl jsg::GarbageCollected for MyType { fn memory_name(self) - ... }生成的trace()方法体在 trace.rs 中有意不检查字段类型对每个命名字段直接委托jsg::Traced::trace(self.field, visitor)并要求所有字段实现Traced——GC 类型天然实现它非 GC 类型则以 no-op 实现兜底从而保证默认全量追踪、逐字段委托的安全性。jsg::Traced::trace(self.field_a, visitor); jsg::Traced::trace(self.field_b, visitor);这套设计意味着追踪行为完全 trait 驱动无 GC 边的类型使用 no-op 实现容器/包装类型递归到内部值。支持的字段形态字段类型追踪行为jsg::RcT强 GC 边 ——visitor.visit_rcjsg::v8::GlobalT双模式强/追踪 ——visitor.visit_global支持循环回收jsg::WeakT不追踪—— 不保持目标存活OptionT/jsg::NullableT存在时委托给TVecT、HashMapK,V、BTreeMapK,V、HashSetT、BTreeSetT递归委托给包含的值CellT/std::cell::CellT通过as_ptr()读取后委托在单线程、非重入的 GC 追踪下安全纯数据 / 基本类型 /#[jsg_struct]类型no-opTraced其他任意T: Traced使用T自身的实现Cell…变体是必需的Traced::trace只接收self构造后还需要变更的追踪字段必须靠内部可变性Cell/RefCell承载。一个覆盖全部形态的典型资源use std::cell::Cell; use std::collections::HashMap; #[jsg_resource] pub struct EventRouter { // 强边 —— 子对象在整个 GC 生命周期内保持存活 handlers: HashMapString, jsg::RcHandler, // 条件追踪 fallback: Optionjsg::RcHandler, // 构造后设置的内部可变回调双模式 Global 允许回调闭包 // 引用本资源自己的 JS 包装时进行循环回收 on_error: CellOptionjsg::v8::Globaljsg::v8::Value, // 弱引用 —— 不保持目标存活 parent: jsg::WeakEventRouter, // 纯数据 —— no-op Traced name: String, }jsg::v8::GlobalT与循环回收jsg::v8::GlobalT采用与 Cjsg::V8RefT相同的强↔追踪双模式父资源持有至少一个强 RustRc时 V8 句柄保持强引用一旦所有Rc被丢弃、仅剩 JS 包装维持资源存活visit_global会把句柄降级为v8::TracedReferencecppgc 可沿其追踪——从而让资源存了回调、回调闭包捕获了资源自身的 JS 包装这类反向引用环在下一次 full GC 中被检测并回收。手动追踪custom_trace默认的逐字段Traced行为不够时用#[jsg_resource(custom_trace)]抑制自动生成的Traced实现并手写。宏仍会生成jsg::GarbageCollected含memory_name、jsg::Type、jsg::ToJS、jsg::FromJS见 resource.rscustom_trace标志由 utils.rs 的has_custom_trace_flag解析支持与name ...组合使用。#[jsg_resource(custom_trace)] pub struct DynamicResource { slots: VecOptionjsg::RcHandler, } impl jsg::Traced for DynamicResource { fn trace(self, visitor: mut jsg::GcVisitor) { for slot in self.slots { if let Some(ref h) slot { visitor.visit_rc(h); } } } }也可以把手写Traced放在一个中间辅助类型上再由资源字段引用它struct EventHandlers { on_message: Optionjsg::v8::Globaljsg::v8::Value, on_error: Optionjsg::v8::Globaljsg::v8::Value, } impl jsg::Traced for EventHandlers { fn trace(self, visitor: mut jsg::GcVisitor) { self.on_message.trace(visitor); self.on_error.trace(visitor); } } #[jsg_resource] pub struct MySocket { handlers: EventHandlers, name: String, }在仓库中继续深入宏的完整使用文档src/rust/jsg-macros/README.md本文主体宏清单总览见 lib.rs。JSG 运行时Lock、Rc、Member、PropertyKind、Traced、GarbageCollected等 trait 的定义与 CXX bridgesrc/rust/jsg 与 src/rust/jsg/README.mdC 侧 JSG 定义在 src/workerd/jsg。属性宏与 oneof 的行为测试含本文引用的全部 JS 语义断言resource_properties.rs 与 jsg_oneof.rs。真实业务绑定示例#[jsg_struct]/#[jsg_resource]的实践用法dns.rs 与 url.rs。JSG 的类型设计原则与 C JSG 的对应关系docs/reference/detail/type-design.mdRust 侧编码规范见 src/rust/AGENTS.md。补充说明#[jsg_property]与#[jsg_inspect_property]在宏层面都是标记宏其回调生成完全复用#[jsg_method]的实现lib.rs差异只体现在外层#[jsg_resource]把它们收集为Member::Property { kind: Prototype/Instance/Inspect, getter_callback, setter_callback }而非Member::Method——这也解释了为何属性 getter/setter 回调中必须内置Illegal invocation防御它们注册时没有 V8Signature的保护。【免费下载链接】workerdThe JavaScript / Wasm runtime that powers Cloudflare Workers项目地址: https://gitcode.com/GitHub_Trending/wo/workerd创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考