
Deno FFI 深度解析Deno.dlopen 双快慢路径与 JIT 蹦床的设计原理【免费下载链接】denoA modern runtime for JavaScript and TypeScript.项目地址: https://gitcode.com/GitHub_Trending/de/denoDeno 的 FFI 模块deno_ffi让 JavaScript/TypeScript 可以直接加载 C 动态库并调用其中导出的函数其核心卖点是以纳秒级开销实现“接近原生代码”的调用速度。本文基于仓库中的 ext/ffi/README.md 及其配套源码完整梳理Deno.dlopen的类型系统、符号加载调用链、V8 Fast API 优化路径与 fallback 路径的双轨设计、非阻塞调用与回调机制并给出可直接运行的基准测试方法。一、deno_ffi 是什么一个专注动态库调用的 FFI 扩展ext/ffi/README.md 对模块的定位一句话概括This crate implements dynamic library ffi.——deno_ffi负责实现动态库dylib/so/dll层面的外部函数接口。它由 Rust 实现ext/ffi/下的 8 个.rs文件加一个懒加载 JS 层00_ffi.js组成通过deno_core::extension!宏注册进运行时。从 lib.rs 的扩展声明可以确认几个关键事实仅支持 64 位平台源码开头有硬约束#[cfg(not(target_pointer_width 64))] compile_error!(platform not supported)并用编译期断言校验指针宽度为 8 字节lib.rs#L38-L45属于 unstable 特性pub const UNSTABLE_FEATURE_NAME: str ffi即使用Deno.dlopen需要显式开启--unstable-ffiOps 面共注册了 30 个 op覆盖四类能力——库加载op_ffi_load、op_ffi_get_static、函数调用op_ffi_call_ptr、op_ffi_call_nonblocking、op_ffi_call_ptr_nonblocking、指针内存读写op_ffi_read_u8…op_ffi_cstr_read等十余个、回调管理op_ffi_unsafe_callback_create/ref/close与 turbo 调试op_ffi_get_turbocall_target。二、README 的性能主张及其来源ext/ffi/README.md 的 Performance 一节给出了三条核心性能主张极低开销Deno FFI 调用开销极低原文引用约 1ns M1 16GB RAM性能与原生代码相当perform on par with native code两条路径Deno.dlopen会为每个符号同时生成一条optimized path和一条fallback path。优化路径在 V8 决定优化该函数时通过 Fast API 触发fallback 路径处理 Fast API 不支持的类型如函数回调并实现针对意外参数类型的完整错误处理JIT 蹦床优化调用进入一个 JIT 编译的trampoline蹦床函数将 Fast API 值直接翻译为符号调用参数README 将编译速度归因于tinycc。需要指出的是从当前源码看蹦床机器码的实际生成器已换成 Cranelift见下文第四节这一点以 turbocall.rs 为准平台限制README 明确目前优化路径仅支持 Linux 和 MacOS。三、JS 层 API 全貌Deno.dlopen 与四个 Unsafe 类所有面向用户的 API 定义在 00_ffi.js 末尾的模块导出中dlopen、UnsafeCallback、UnsafeFnPointer、UnsafePointer、UnsafePointerView。3.1 符号表定义与 dlopen 入口dlopen(path, symbols)将路径经pathFromURL转换为文件系统路径后构造DynamicLibrary00_ffi.js#L568-L570。符号表的每条定义对应 Rust 侧的ForeignFunction结构dlfcn.rs#L90-L101const dylib Deno.dlopen(./libfoo.so, { // 基本形式键名即符号名 add_u32: { parameters: [u32, u32], result: u32 }, // 完整形式可重定向符号名、标记非阻塞、标记可选 sleep_ms: { name: usleep, // 可选库内真实符号名 parameters: [u32], result: void, nonblocking: true, // 可选在后台线程执行返回 Promise optional: true, // 可选符号不存在时置 null 而不报错 }, // 静态变量带 type 字段的条目按静态变量处理 VERSION: { type: u32 }, });DynamicLibrary构造器00_ffi.js#L457-L566随后按条目分派带type的走op_ffi_get_static读静态变量nonblocking: true的生成异步包装函数其余使用 Rust 侧op_ffi_load返回的同步函数。库通过dylib.close()释放。3.2 原生类型系统Rust 侧的类型枚举在 symbol.rs#L7-L28类型字符串含义JS 侧接受的实参 / 返回形态void无返回不传参返回undefinedbool布尔booleanu8/i8/u16/i16/u32/i32整数无符号/有符号整数 Numberu64/i64/usize/isize64 位整数BigInt 或 Number 均可f32/f64浮点Numberbuffer缓冲区指针null、ArrayBuffer或ArrayBufferViewpointer/function裸指针 / 函数指针null或ExternalDeno.UnsafePointer产出的指针{ struct: [...] }结构体ArrayBuffer或ArrayBufferView按字段顺序与 C ABI 布局其中u64/i64/usize/isize的BigInt 或 Number双支持在解析函数里可以直接看到ffi_parse_u64_arg 先尝试 BigInt、再回退 Number注释解释了顺序考量——BigInt 罕见且 Fast API 不支持故在慢速路径优先检查。buffer参数的解析逻辑见 parse_buffer_arg检查顺序ArrayBuffer → ArrayBufferView → null。3.3 结构体JS 自己算大小和内存对齐结构体类型没有原生布局支持JS 层的 getTypeSizeAndAlignment 按 C ABI 规则递归计算每个结构体的字节大小与对齐基础类型查表bool/u8/i8 为 1u16/i16 为 2u32/i32/f32 为 4u64/i64/f64/pointer/buffer/function/usize/isize 为 8并对循环结构体抛TypeError。这个大小有两个用途调用返回结构体的同步函数时预分配输出缓冲区UnsafeFnPointer手动调用时同理。四、符号加载调用链op_ffi_load 里发生了什么op_ffi_loaddlfcn.rs#L144-L247是整个 FFI 的入口 op其执行步骤权限检查permissions.check_ffi_partial_with_path(path)即--allow-ffi可带path限定在此强制打开库dlopen2::raw::Library::open完成实际的dlopen/LoadLibraryWindows 下还专门实现了带库路径参数的FormatMessageW错误格式化format_error解析符号对每个ForeignFunction用lib.symbol::*const c_void()取函数地址optional: true的符号查不到时直接写入null而跳过否则抛带符号名的DlfcnError::RegisterSymbol建立 libffi CIF用libffi::middle::Cif::new把参数/返回类型NativeType→libffi::middle::Type转换见 symbol.rs#L30-L66编译为调用信息连同函数指针打包进Symbol生成 JS 函数同步符号经make_sync_fn创建绑定函数然后返回[resourceId, 符号对象]二元数组。Symbol与可选的Turbocall一起装进 cppgc 管理的FunctionDatadlfcn.rs#L249-L256作为函数模板的 data 携带——这样 Rust 侧状态的生命周期由 V8 的函数 GC 管理。五、性能核心optimized 与 fallback 双路径这正是 ext/ffi/README.md 主张的机制源码对应关系如下。5.1 兼容性与蹦床编译make_sync_fn 对每个符号先做兼容性判断let turbocall if turbocall::is_compatible(symbol) { match turbocall::compile_trampoline(symbol) { Ok(trampoline) Some(turbocall::make_template(symbol, trampoline)), Err(e) { log::warn!(Failed to compile FFI turbocall: {e}); None } } } else { None }; // ... let func if let Some(overloads) overloads { builder.build_fast(scope, overloads) // 优化路径挂 Fast API } else { builder.build(scope) // fallback普通 JS 回调 };兼容性条件很简单turbocall.rs#L42-L48返回类型不是 struct且参数中没有 struct。struct 参数/返回值走 fallback 的sync_fn_impldlfcn.rs#L309-L337因为 struct 需要最后一个 TypedArray 参数作为输出缓冲区、并做大小校验。5.2 Cranelift 生成的可执行蹦床compile_trampolineturbocall.rs#L62-L365为每个符号生成一段包装机器码用cranelift::prelude构建三个签名wrapper_sigV8 调用的入口参数类型按 Fast API 约定展开为 i32/i64/f32/f64 等、target_sig真正调用 C 函数的平台 ABI 签名、raise_sig错误上报wrapper 入口先做参数收窄如 i32 收窄为 i8/i16对buffer参数调用turbocall_ab_contents从 V8 值提取裸指针若类型非法返回isize::MAX哨兵则跳转错误块经turbocall_raise抛InvalidBufferType异常返回值做符号/零扩展后返回编译产物校验verify_function、优化后写入memmap2::MmapMut并make_exec()变成可执行内存包装为Trampolineturbocall.rs#L359-L364make_templateturbocall.rs#L407-L451把蹦床地址与CFunctionInfo含每个参数的CTypeInfo、Int64Representation::BigInt打包成 V8 的CFunction交build_fast注册为 Fast API 重载。此后 V8 在优化状态下可以直接以 C ABI 调用蹦床完全绕过 op 边界与 JS 调用栈。源码注释还记录了一个重要的 Apple silicon 修正V8 在 arm64 上按 AAPCS64 把栈参数打进 8 字节槽而 Darwin 默认 ABI 按自然对齐读取会导致读到垃圾值因此 macOS aarch64 上 wrapper 被强制使用SystemV调用约定而内部调用用户 C 函数的签名仍用平台默认turbocall.rs#L76-L93。这与 README 优化路径仅支持 Linux 和 MacOS 的平台限定互为印证。另外仓库保留了调试钩子设置环境变量DENO_UNSTABLE_FFI_TRACE_TURBO1后蹦床会记录被 turbo 调用的符号名JS 侧可通过getTurbocallTarget()底层为op_ffi_get_turbocall_targetturbocall.rs#L495-L517查询用于验证某次调用确实走了 Fast API 快路径。5.3 fallback 路径的调用与错误处理没有 turbo 重载时函数体是sync_fn_impl若返回类型为 struct取最后一个 TypedArray 参数作为输出缓冲区out_buffer_as_ptrir.rs#L129-L136再进入ffi_call_synccall.rs#L83——逐个参数调用ir.rs中的ffi_parse_*_arg解析为NativeValue联合ir.rs#L161-L178经libffi的ffi_call发起真正的 C 调用再按声明类型把NativeValue转回 V8 值NativeValue::to_v8其中 64 位整数转 BigInt、指针转External、null 指针转 JS null。参数类型不符时抛出 IRError 中定义的细粒度 TypeError如Invalid FFI u8 type, expected unsigned integer这就是 README 所说fallback 路径实现 Fast API 不支持的完整错误处理。六、非阻塞调用把 C 调用挪到后台线程在符号定义中声明nonblocking: true后调用返回 Promise。JS 层包装见 00_ffi.js#L509-L541struct 返回值时自动分配输出 buffer 并在 promise 完成后 resolve 为该 buffer。Rust 侧由op_ffi_call_nonblocking/op_ffi_call_ptr_nonblocking处理用deno_core::unsync::spawn_blocking把ffi_call丢到阻塞线程池从而不卡住事件循环。有两个值得注意的安全细节GC 保护BackingStoreHolder 在解析 buffer/struct 参数前保留其BackingStore的共享引用防止 V8 在后台线程仍持有裸指针期间回收底层内存禁止用户可 resize 的 bufferholder.push检测到is_resizable_by_user_javascript()时抛ResizableBackingStore错误——共享可调整大小的存储其数据指针在异步调用期间无法保证稳定。注意 dlfcn.rs 中的注释turbo 优化目前不适用于非阻塞调用nonblocking符号在op_ffi_load阶段不生成 turbo 函数统一走 op 通道。七、静态变量与函数回调7.1 静态变量Foreign Static符号表中带type字段的条目如VERSION: { type: u32 }由op_ffi_get_staticstatic.rs#L29-L37处理按符号名取地址后按类型read_unaligned读内存。限制明确void报InvalidTypeVoidstruct报InvalidTypeStructpointer/function/buffer类型读出的是External指针值。optional: true时符号不存在返回null而非报错。JS 侧还会先拦截type: void并抛 TypeError00_ffi.js#L474-L501。7.2 UnsafeCallback把 JS 函数暴露给 CUnsafeCallback00_ffi.js#L394-L453将 JS 回调包装为可传给 C 的函数指针const c new Deno.UnsafeCallback( { parameters: [u32, u32], result: u32 }, (a, b) a b, ); dylib.symbols.register_handler(c.pointer); // 传给 C c.close(); // 必须显式释放否则 C 侧悬垂Rust 侧 callback.rs 用libffi::middle::Closure创建真实 C 函数指针并注册为资源C 端线程被回调时CallbackInfo记录thread_id若不在原线程则通过V8CrossThreadTaskSpawner把参数序列化后调度回 isolate 线程执行 JS再取回返回值实现线程安全的回调测试见 tests/ffi/testdata/thread_safe_test.ts。ref()/unref()控制回调是否阻止 Deno 进程退出内部 ref 一个 op promiseUnsafeCallback.threadSafe()构造即自动 ref。构造函数还硬性禁止nonblocking定义回调必须同步。7.3 裸指针工具Deno.UnsafePointer.of(value)/.value(ptr)/.offset(ptr, n)/.equals/.create在 buffer 与 C 指针之间互转Pointer类型参数只接受External或 null见 ffi_parse_pointer_argDeno.UnsafeFnPointer.call(...)对裸函数指针按定义手动调用struct 返回值时自动分配输出 buffer00_ffi.js#L280-L332Deno.UnsafePointerView指针内存读取全家桶——getBool/getUint8…getFloat64/getPointer/getCString/getArrayBuffer/copyInto全部映射到 lib.rs 中注册的op_ffi_read_*/op_ffi_cstr_read/op_ffi_get_bufop可用于把 C 返回的pointer解析为字符串或拷贝到 JS 缓冲区。八、运行官方 FFI 基准测试README 给出的基准命令为target/release/deno bench --allow-ffi --allow-read --unstable-ffi ./tests/ffi/tests/bench.js需要说明两点适用前提其一命令假设已构建出target/release/deno且当前需要--unstable-ffi解锁 FFI其二以当前仓库为准基准脚本实际位于 tests/ffi/testdata/bench.jsREADME 中的旧路径tests/ffi/tests/bench.js已不存在运行前需先构建对应的test_ffi测试动态库基准脚本从Deno.execPath()同级目录加载libtest_ffi.{dylib,so,dll}见 bench.js#L4-L10。该基准覆盖了性能文档关心的全部维度所有标量类型的无参/有参调用nop_*/return_*、u64的 Number 与 BigInt 两种传参方式、buffer参数hash、C 字符串读取ffi_stringUnsafePointerView.getCString、26 参数的大参数列表调用nop_many_parameters、上述全部的nonblocking变体以及UnsafePointer.of/value与各UnsafePointerView#get*操作本身的开销——对照同步与非阻塞两组的差值即可直观看到 op 通道与 Fast API 路径的成本对比。更多类型层面的行为验证可参考 tests/ffi/testdata/ffi_types.ts 与 tests/ffi/testdata/test.js。九、总结与延伸阅读deno_ffi的设计可以概括为三层JS 声明层符号表定义 Unsafe 工具类、op 通道层权限、资源表、libffi 调用、错误处理、以及一层按符号动态生成的机器码快路径Cranelift 蹦床 V8 Fast API。这条声明一次、热路径零 JS 开销的路径正是 README 性能主张的实现来源而 fallback 路径保证了 struct、回调等复杂场景的正确性与可诊断性。继续深入时可按此脉络阅读源码扩展注册与 op 清单ext/ffi/lib.rs库加载与函数生成ext/ffi/dlfcn.rsFast API 蹦床ext/ffi/turbocall.rs参数解析与值转换ext/ffi/ir.rs、ext/ffi/call.rs类型系统ext/ffi/symbol.rs静态变量ext/ffi/static.rs回调ext/ffi/callback.rsJS APIext/ffi/00_ffi.js测试与基准tests/ffi/testdata/bench.js【免费下载链接】denoA modern runtime for JavaScript and TypeScript.项目地址: https://gitcode.com/GitHub_Trending/de/deno创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考