ARTICLE DETAIL

资讯详情

深耕网站视觉设计与运营推广的一线实战洞察。

Deno op 层桥 serde_v8:Rust 与 V8/JS 值双向编码的实现与最佳实践

Deno op 层桥 serde_v8:Rust 与 V8/JS 值双向编码的实现与最佳实践 Deno op 层桥 serde_v8Rust 与 V8/JS 值双向编码的实现与最佳实践【免费下载链接】denoA modern runtime for JavaScript and TypeScript.项目地址: https://gitcode.com/GitHub_Trending/de/deno本篇技术指南以 libs/serde_v8/README.md 为核心结合仓库源码深入讲解serde_v8如何在 Deno 的 opoperation层中充当 Rust 与 V8/JS 值之间的序列化桥梁你将掌握to_v8/from_v8这对核心 API 的完整用法、编写高性能 op 时的避坑实践以及递归深度限制、键字符串内化、零拷贝 magic 类型等源码级实现细节。定位Deno op 层的核心编码组件serde_v8为(rusty_)v8值提供 Serde 支持即对 V8 引擎句柄值进行编码/解码。根据 README 的描述它的设计目标是提供一个表达力强、且接近最大效率的编码层在 Rust 与 v8/js 值之间建立双射bijection。它是Deno op 层的核心组件负责编码/解码所有非 buffer 类的值。从源码结构看这一点可以直接得到印证Deno 核心运行时在多个位置直接调用该库例如 libs/core/ops_builtin_v8.rs 中用serde_v8::to_v8将错误信息、流 ID 等 Rust 值传给 JS 侧libs/core/runtime/bindings.rs 中用serde_v8::from_v8把 JS 侧传回的参数反序列化为 Rust 类型。换言之每一次Deno.core.op(...)跨语言调用时参数的转换底层走的都是这条链路。包版本与特性开关定义在 libs/serde_v8/Cargo.toml 中当前版本 0.320.0[features] default [v8] quickjs [v8/quickjs, serde_v8_utilities/quickjs] v8 [v8/v8, serde_v8_utilities/v8] [dependencies] deno_error.workspace true num-bigint.workspace true serde.workspace true smallvec { workspace true, features [union] } thiserror.workspace true v8.workspace true值得注意的是它同时提供v8与quickjs两个 feature通过v8crate 的特性切换同一套 serde 实现可以运行在 V8 之上也可以运行在 QuickJS 之上这是 Deno 探索轻量引擎deno_isolate场景的基础。核心 APIto_v8 与 from_v8serde_v8天然融入 serde 生态。如果你用过serde或serde_json其 API 会非常熟悉。它暴露两个关键函数libs/serde_v8/lib.rs 中的公开导出函数方向类比to_v8rust → v8类似serde_json::to_stringfrom_v8v8 → rust类似serde_json::from_strfrom_v8_cachedv8 → rust带 KeyCache结构键优化的from_v8变体除这两个函数外库还导出了Serializer、Deserializer、Error/Result、KeyCache以及一整族 magic 零拷贝类型AnyValue、BigInt、JsBuffer、ToJsBuffer、ByteString、DetachedBuffer、StringOrBuffer、U16String、V8Slice、V8Sliceable、ExternalPointer、GlobalValue。完整 Quickstart 示例仓库自带一个可直接运行的示例 libs/serde_v8/examples/basic.rs演示了从零初始化 V8 平台到完成若干次 v8 → rust 反序列化的完整流程use serde::Deserialize; #[derive(Debug, Deserialize)] struct MathOp { pub a: u64, pub b: u64, pub operator: OptionString, } fn main() { // 1. 初始化 V8 平台与 Isolate let platform v8::new_default_platform(0, false).make_shared(); v8::V8::initialize_platform(platform); v8::V8::initialize(); { let isolate mut v8::Isolate::new(v8::CreateParams::default()); v8::scope!(handle_scope, isolate); let context v8::Context::new(handle_scope, Default::default()); let scope mut v8::ContextScope::new(handle_scope, context); // 在 V8 中执行 JS 源码拿到一个 v8::Value fn execs( scope: mut v8::PinScopes, _, src: str, ) - v8::Locals, v8::Value { let code v8::String::new(scope, src).unwrap(); let script v8::Script::compile(scope, code, None).unwrap(); script.run(scope).unwrap() } // 2. 基本类型JS 数字 - u64 let v exec(scope, 32); let x32: u64 serde_v8::from_v8(scope, v).unwrap(); println!(x32 {x32}); // 3. 结构体JS 对象 - Rust struct多余的 key 被忽略 let v exec(scope, ({a: 1, b: 3, c: ignored})); let mop: MathOp serde_v8::from_v8(scope, v).unwrap(); println!(mop {{ a: {}, b: {}, operator: {:?} }}, mop.a, mop.b, mop.operator); // 4. 集合JS 数组 - Vecu64 let v exec(scope, [1,2,3,4,5]); let arr: Vecu64 serde_v8::from_v8(scope, v).unwrap(); println!(arr {arr:?}); // 5. 字符串数组 - VecString let v exec(scope, [hello, world]); let hi: VecString serde_v8::from_v8(scope, v).unwrap(); println!(hi {hi:?}); // 6. 浮点f64 let v: v8::Localv8::Value v8::Number::new(scope, 12345.0).into(); let x: f64 serde_v8::from_v8(scope, v).unwrap(); println!(x {x}); } // 7. 所有 isolate 销毁后安全地释放 V8 资源 // SAFETY: all isolates have been destroyed unsafe { v8::V8::dispose(); } v8::V8::dispose_platform(); }这个示例覆盖了from_v8的典型场景整数、带Option字段的结构体缺失字段operator会被反序列化为NoneJS 对象中多余的c字段被忽略、整型数组、字符串数组、浮点数。to_v8则是对称方向任何实现了serde::Serialize的 Rust 值在持有mut v8::PinScope的前提下均可编码为v8::Localv8::Value。反序列化器的递归深度保护libs/serde_v8/de.rs 中的Deserializer并非简单的值转换器它内置了安全防护。源码中明确写道// Maximum nesting depth permitted while deserializing a V8 value. serde_v8 // recurses on the Rust stack for each nested container, so without a bound a // deeply nested object or array (cheaply built from untrusted JS) would // overflow the stack and abort the process. We return an error instead once // this depth is reached. Matches the default used by serde_json. const RECURSION_LIMIT: usize 128;要点serde_v8对每个嵌套容器都在 Rust 调用栈上递归若无上限一个由不可信 JS 廉价构造出的深层嵌套对象就能打爆栈、导致进程 abort因此每个Deserializer持有remaining_depth预算默认 128与serde_json的默认值一致每下降一层就checked_sub(1)耗尽后返回Error::RecursionLimitExceeded而不是崩溃此外from_v8还会把数字按类型细分处理is_uint32→deserialize_u32、is_int32→deserialize_i32、否则 →deserialize_f64以兼容松散类型的serde_json语义整数反序列化宏deserialize_signed!/deserialize_unsigned!甚至会尝试把v8::BigInt当作整数值转换失败则返回Error::ExpectedInteger。错误类型全部集中在 libs/serde_v8/error.rs包括ExpectedBoolean、ExpectedInteger、ExpectedString、ExpectedArray、ExpectedMap、ExpectedEnum、ExpectedObject、ExpectedBuffer、LengthMismatch、RecursionLimitExceeded、V8Exception、ResizableBackingStoreNotSupported等并通过deno_error::JsError#[class(type)]标记使得跨 V8 边界抛出时能生成正确的 JS 错误类型。编写高性能 op 的最佳实践README 的 Best practices 一节给出了三条在 Deno 生态中编写 op 时应遵循的经验准则值得逐条理解其背后的性能机理1. 优先使用原生 Rust 结构体/元组/基本类型避免serde_json::Value中转虽然serde_v8兼容serde_json::Value但要记住serde_json::Value本质上是一个弱类型值类似嵌套的 HashMap。编写 op 时建议直接使用 rust 的 struct/tuple 或基本类型因为映射到serde_json::Value会带来额外开销导致 op 变慢。2. 避免不必要的包装如果某个 op 只接收一个单键 struct除非近期打算扩展字段否则直接把它解包成普通值传参。少一层嵌套就少一次对象/属性访问的编解码开销。3. 用 Rust 单元类型()代替空对象返回值不要通过Ok(json!({}))返回无值而应把返回类型改成 Rust 单元类型()并返回Ok(())——serde_v8会将其高效编码为 JS 的null。从 de.rs 的deserialize_any实现看ValueType::Null确实会走deserialize_unit分支二者是精确对应的。序列化侧枚举变体的 tagged 编码libs/serde_v8/ser.rs 中的Serializer负责 rust → v8 方向。其中有一个专门的VariantSerializer用于把其他序列化器包装为枚举 tagged 变体形式/// Wraps other serializers into an enum tagged variant form. /// Uses {Variant: ...payload...} for compatibility with serde-json. pub struct VariantSerializera, b, c, i, S { inner: S, scope: ScopePtra, b, c, i, variant: static str, }也就是说Rust 的enum会被编码为与 serde_json 一致的{VariantName: payload}对象形式实现了与 serde 生态在枚举表示上的互通end方法通过v8::Object::with_prototype_and_properties构造无原型对象减少垃圾对象开销。字符串键内化v8_struct_key与 KeyCache结构体字段名在编码为 V8 对象属性键时会反复出现字符串的创建与去重是热点。libs/serde_v8/keys.rs 给出了当前策略pub fn v8_struct_keys, i( scope: v8::PinScopes, i, field: static str, ) - v8::Locals, v8::String { // Internalized v8 strings are significantly faster than normal v8 strings // since v8 deduplicates re-used strings minimizing new allocations v8::String::new_from_utf8( scope, field.as_ref(), v8::NewStringType::Internalized, ).unwrap() }核心思想是使用Internalized内化V8 字符串V8 会对重复使用的内化字符串自动去重从而最小化分配。源码注释还量化了放弃 external string 的原因当前未去重的 external string不走 KeyCache比去重后的 internalized string 慢约 2.5 倍因为它在 V8 眼里是全新字符串需要重新哈希。同文件中的KeyCache结构键到v8::Globalv8::String的哈希池目前标注为#[allow(dead_code, reason experiment)]属于实验性实现尚未在from_v8/to_v8主路径启用——这与 README TODO 中Experiment with KeyCache to optimize struct keys一条对应。不过from_v8_cached入口de.rs已经预留它接收一个mut KeyCache并在Deserializer中持有引用供高频 op 在解码重复结构键时命中缓存。magic 模块跨边界的零拷贝与特殊类型libs/serde_v8/magic/mod.rs 定义了serde_v8中最具工程价值的部分——一组实现 serdeSerialize/Deserialize的魔法类型覆盖普通 JSON 语义无法表达或代价过高的场景类型文件用途JsBuffer/ToJsBufferbuffer.rs把ArrayBuffer/TypedArray零拷贝地映射为 Rustmut [u8]避免整块 buffer 拷贝ByteStringbytestring.rslatin1 编码的紧凑字符串V8 内部字符串表示省去 UTF-8 转换U16Stringu16string.rs直接对应 V8 内部 UTF-16 表示的字符串DetachedBufferdetached_buffer.rs允许 Rust 侧获取已 detach 的 buffer 所有权StringOrBufferstring_or_buffer.rs统一处理 JS 侧字符串或二进制两类输入的 op 参数V8Slice/V8Sliceablev8slice.rs对ArrayBuffer内存的安全切片抽象ExternalPointer/GlobalValueexternal_pointer.rs、global_value.rs通过 external pointer 在 JS 值中携带 Rust 侧指针/全局引用AnyValueany_value.rs可自由在双向间透传的任意 V8 值Value的 serde 包装BigIntbigint.rs基于num-bigint的 JS BigInt 互转这些类型正是 README 所说编码/解码所有非 buffer 值中buffer 值的对应处理路径普通标量/结构走 serde 泛型路径buffer 走 magic 类型的零拷贝路径二者共同构成 op 参数编解码的全集。测试覆盖在 libs/serde_v8/tests/magic.rs、de.rs 与 ser.rs 中。值分类ValueType 与 Payload 的雏形libs/serde_v8/payload.rs 定义了ValueType枚举用于把v8::Value细分为Null、Bool、Number、BigInt、String、Array、ArrayBuffer、ArrayBufferView、Object九类Deserializer::deserialize_any正是基于这个分类分派到对应的deserialize_*方法。该文件顶部还留有一条 TODO 注释——也许添加一个持有 scope 与 v8::Value 的 Payload 类型让它自身实现 Deserialize——与 README TODO 中的Payload类型一项呼应说明这一设计仍处演进中从源码结构看payload.rs目前主要承担值分类职责。README TODO当前实现与已知演进方向README 末尾的 TODO 列表是理解该库当前成熟度的一张路线图对照源码可以逐项验证其现状Experiment with KeyCache to optimize struct keysKeyCache已存在于 keys.rs但标记为 dead code 实验from_v8_cached已提供接入点。Experiment with external v8 stringskeys.rs 中保留了 external string 的注释代码与 ~2.5x 的实测结论。Explore json-stringifier.cc fast-paths for arrays数组序列化仍走通用 serde 路径。Improve tests to test parity withserde_json已有 tests/ser.rs、tests/de.rs 双向测试兼容serde_json语义如枚举 tagged 形式、整数/浮点细分是设计目标。Consider aPayloadtype thats deserializable by itself见上文 payload.rs 注释尚未落地。Ensure we return errors instead of panicking on.unwrap()s主路径已改为返回Error如递归超限返回RecursionLimitExceeded但该目标仍在持续清理中。小结serde_v8虽然代码体量不大但它是理解 Deno op 层架构的一把钥匙任何 JS 侧调用Deno.core.op的参数在跨越 V8 边界时都经由from_v8反序列化为强类型 Rust 值返回值再经to_v8序列化回 JS。掌握它的三条最佳实践——直接用原生 Rust 类型而非serde_json::Value、避免单键包装结构、用()代替空对象——再理解其递归深度保护128 层上限、内化字符串键优化与 magic 零拷贝类型就具备了阅读乃至编写 Deno 扩展 op 的底层基础。【免费下载链接】denoA modern runtime for JavaScript and TypeScript.项目地址: https://gitcode.com/GitHub_Trending/de/deno创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
返回列表