【FFI】N-API的JS堆对象生命周期管理

N-APIJS堆对象生命周期管理

N-APINode API的简写,同时也是nodejsJS VM(链)接入原生模块.node文件的应用程序二进制接口(i.e. ABI)。借助N-API引入的抽象隔离,升级nodejs运行时(虚拟机)

  • 【编译】不要求对原生扩展模块重新编译 — 为nodejs的不同版本分别准备不同的原生模块build真的好麻烦。

  • 【运行】不导致原生模块程序崩溃 — 精读每一版changelogs清单和微调原生模块源码更耗时费力。

N-API开放接口在nodejs 10+后才逐步稳定,和成为nodejs c-addon的主流编程标准。

不久前,我有机会在工程实践中独立完成“给node-webkit容器编写原生扩展模块的”程序开发任务。虽然扩展模块自身的业务处理逻辑很简单 — 馁馁的“胶水”代码,但其涉及到了跨越多个FFI接口调用的JS对象缓存处理。初版程序缓存不住JS堆内存中的变量值,因为JS VMGC总是在FFI接口调用的间隙回收由原生模块缓存的JS对象和导致程序崩溃。由此,我特意“死磕”C/C++ addons with Node-API厂方文档,在解决工程难题的同时汇总实践收获写下此文。

文章以名词解释统一术语理解开篇,以对比不同版本ABI标准引题,以技术细节展开讨论为依据,最后向读者图文并茂地描述我个人创新的实践方案。

名词解释

nodejs c-addon

nodejs原生扩展模块。所谓“原生”是相对JS模块而言的。它必须由【系统编程语言C / Cpp / Rust】编写,并经由nodejs开放接口N-API

  1. 接入nodejsJS VM,并

  2. nodejs交换数据·互操作。

为了文字简练,下文也将其记作为addon

nodejs c-addonCommonjs Module在科技树上处于相同的生态位,和对“上游”调用端的JS业务代码呈现一致的调用方式。

JS堆对象

它既包括由JS程序自身构造的对象实例,也包含由系统程序从addon内调用N-API接口(比如,napi_create_object())实例化的JS对象。它们都

  1. 被保存在JS VM内存中,和

  2. Rust内存中的napi_value可修改原始指针引用。

N-API引用计数

它是指向JS堆对象的“FFI引用计数”智能指针(后文有图,应该会更直观些)。其

  1. 被保存于JS VM内存中,和

  2. Rust内存中的napi_ref可修改原始指针引用。即,addonRust程序拿到的是指向了“智能指针”的“指针”。

  3. 被用于阻止JS VMGC回收正活跃于addon端的JS堆对象。这就赋予了 @Rustacean 从JS VM外部干预JS对象生命周期的能力。React Native可都做不到这一点。

WASM垫片程序

它既包括由wasm-bindgen-cli生成的JS垫片程序文件,也包含由wasm-bindgen crate导出的Rust开发框架。正是js <-> Rust两端垫片程序的协同配合,JS堆对象才几乎被“投影为”Rust所有权(栈)变量。比如,JS堆对象的wasm_bindgen::JsValue(智能指针)结构体就比nj_sys::napi_value可修改原始指针更能发挥Rust类型系统与Borrow / Drop Checker对程序正确性的保障力。没有“黑魔法”,满眼都是对垫片程序开发迭代的工作量

WASM vs. N-API堆对象生命周期管理策略

简单地讲,生命周期策略的差异取决于【垫片程序】的“薄/厚”。因为WASM应用场景多(包括但不限于:网页、nodejswasm-runtime独立虚拟机),社区关注度高,wasm-bindgen工具链迭代速度快,所以,wasm <-> js垫片程序就“厚”。JS堆对象向Rust的“投影”就更像【智能指针】,而不是“裸奔的”原始指针。WebAssembly工作组甚至规划将垫片程序逐步“固化”至wasm-runtime内(比如,TC39弱引用提案与引用类型提案等)以完备核心功能。工作量到位自然对接平滑!这不是黑魔法,而是真金白银的血汗努力。

相反,nodejs c-addon的应用场景就要少得多了。所以,技术社区鲜有热情面向N-API开放接口编写功能丰富的addon <-> js垫片程序。于是,@Rustacean 不得不直面

  1. “裸奔的”原始指针

  2. 简陋的Rust Bindings — 与C头文件概念对等的Rust语言项

  3. “安慰剂”式的编程工具。因为缺乏了js垫片程序的协同呼应,几个Rust宏也只是杯水车薪,能“糖”的内容很少。

  4. 转移更多精力从【业务逻辑实现】至【FFI编程】,并与各种FFI技术细节做“斗争”。赶快补课内存布局理论知识去吧!

具体地讲,在Rust - WASM程序上下文中,披上了“智能指针”马甲的JS堆对象几乎完全“锈化”了。@Rustacean 可忽视JS VM垃圾收集器的干扰和:

  1. static全局缓存JS堆对象。而不必担心活跃于addonJS堆对象会被JS VMGC回收。

  • 相对FFI函数的单次调用执行周期,延长JS堆对象的生命周期。

{ .. }块作用域限定JS堆对象,按需释放不再访问的变量值,提高内存利用效率。就有局部变量的函数而言,这可明显地降低JS堆内存占用的瞬时峰值。

  • 相对FFI函数的单次调用执行周期,缩短JS堆对象的生命周期

另一方面,N-API没有功能面面俱到的垫片程序。所以,@Rustacean 做不到仅凭Rust基本语法项就对FFI另一端的JS堆对象执行【全局缓存】或【块作用域】按需回收的程序处理。甚至(重点来了),即便JS端代码刻意保留了已FFI导出堆对象的引用,addon端(栈内存)所持有的原始指针依旧会,在FFI函数执行之后,丢失其原本指向的值和成为“野”指针。我怀疑JS VM就算没有回收也至少挪动了被导出JS堆对象的内存位置。由此,@Rustacean 需要在addon业务代码中额外实现部分本该由垫片程序完成的“公共服务”功能,包括但不限于:

  1. 徒手维护N-API引用计数智能指针,以“锁住”JS堆对象不被JS VMGC回收 — 延长JS堆对象的生命周期。

  2. 调用N-API程序接口构造可层叠嵌套的作用域【块】 — 缩短JS堆对象的生命周期。

这的确是一次接触底层“自己动手丰衣足食”的机会,但绝对不是什么令人愉快的开发体验。千言万语汇聚一张图(左侧WASM,右侧nodejs c-addon)促成读者思绪的豁然开朗:

6c4f3e2aeb5d533cff76cb1ad829c5d0.png

N-API JS堆对象生命周期管理的技术细节

addonJS堆对象生命周期的管理分为如下三种情况(看图吧,一图抵千词):

32922512076e2f7e9acc84f031f96ecd.png

由上图可见,真实数据被保存于JS端(堆)内存中。Rust端(栈)内存仅持有随时可能失效的原始指针。所以,@Rustacean 需要调用特定的N-API接口,远程操控JS堆对象的活跃周期。但是,N-API接口并不易用。这表现为...

N-API引用计数智能指针不智能

  1. 没有RAII Guard对活跃引用数量的自动跟踪。@Rustacean 还需书面编写N-API接口调用和人工增减引用个数跟踪引用复本数量 — 这是传统的缺陷产出“大户”。

  2. 引用数量意味着GC回收。@Rustacean 还需显式地析构掉N-API【引用计数】智能指针实例,才能促使被“持久化于内存”的JS堆对象接受GC回收。否则,内存泄漏!具体作法请参见如下伪码

    use ::nj_sys::{napi_delete_reference, napi_reference_unref};
    use ::node_bindgen::core::napi_call_result;
    let result = Box::into_raw(Box::new(u32::MAX));
    // 1. 将引用计数值减一
    napi_call_result!(napi_reference_unref(
        <N-API 调用上下文>,
        <N-API 引用计数·智能指针>,
        result // 引用计数减一之后的结果数值
    )).unwrap();
    let result = unsafe { Box::from_raw(result) };
    // 2. 判断减一后的最新引用计数值是否已经归零。
    if *result == 0 { // 当且仅当不再有任何 N-API 引用复本还指向该 JS 堆对象时,
        // 3. 显式地释放引用计数智能指针实例。
        napi_call_result!(napi_delete_reference( // 这一步是必须的。要不然,内存就漏了!
            <N-API 调用上下文>,
            <N-API 引用计数·智能指针>
        )).unwrap();
    }
  3. 只有四类JS堆对象支持N-API引用计数。它们分别是

  • napi_object — ECMAScript规范中的Object

  • napi_function — ECMAScript规范中的Function

  • napi_symbol — ECMAScript规范中的Symbol

  • napi_external — 类似于ECMAScript中的Blob,专门引用进程外的某种“黑盒opaque”资源。

若多个N-API引用计数指针实例(注:不是引用复本)都指向同一个JS堆对象,那么只有当全部N-API引用计数指针实例都被napi_delete_reference()处理后,“持久化于内存”的JS堆对象才被允许GC回收。

可逃逸作用域与作用域提升不实用

在上图中的(普通)作用域napi_handle_scope禁止其内部的JS堆对象溢出作用域,和向外传值。即,普通作用域是“多入无出”的。

【可逃逸作用域napi_escapable_handle_scope】有限松绑了这条限制。它允许作用域像函数一样向外输出一个且仅一个值,而输出形式不是Rust块表达式【返回值】,而是JS堆对象【作用域·提升handle promoting】。类比JS动态语言的【变量提升variable hoisting】,

  • 相同点:块内声明的变量可从块外引用和访问

  • 不同点:【可逃逸作用域】有且只有一个块内声明的变量可从块外被访问。否则,程序崩溃。

所以,可逃逸作用域是“多入单出”的面向实用有限放开。再看图吧,一图抵千词!

2dd416a3a60b28508d9016169bb4aa62.png

在作用域层叠嵌套的场景下,这绝对是“盛产”缺陷的泥沼。@Rustacean 需要从程序设计之初就努力避免从Rust端远程管理JS变量的作用域。最好从产品架构上,多用addon构建【业务组件】,少封装【功能模块】,从根本上规避Rust <-> JS复杂互操作出现

智能化N-API引用计数 — “二段式”引用计数优化法

相比于最低也需要【过程宏】作为抽象工具才能描述清楚的JS堆对象作用域N-API引用计数智能化改造还是有捷径可走的。

简单地讲,将对引用复本数量变化的跟踪任务委托给遵循RAII with Guard设计模式的智能指针std::rc::Rc<napi_ref>处理。然后,addon业务实现代码仅需负责

  1. 【始】调用napi_create_reference() 接口,构造一个单复本引用计数指针实例,锁住JS堆对象不被GC回收。

  2. 【末】调用napi_reference_unref()napi_delete_reference()接口,清空引用复本与析构唯一的引用计数指针实例,解锁GC回收JS堆对象。

接着看图,依旧一图抵千词!

b3ee7d565f1db7ce37578d2e49979273.png

于是,整个设计方案的“难点”就聚焦于:

  1. 监听智能指针std::rc::Rc<napi_ref>的引用复本清空事件,并

  2. 在事件处理函数内,调用napi_reference_unref()napi_delete_reference()接口通知VM GC回收JS堆对象。

难点不难,因为Newtypes设计模式允许 @Rustacean

  1. std::rc::Rc<napi_ref>AOP编程。以

  2. “拦截+重写”std::rc::Rc<napi_ref>的析构函数<Rc as Drop>::drop(&mut self)。于是,

  3. 在每个引用复本的析构处理后,都重新统计剩余引用复本的数量。最后,

  4. 没有剩余引用复本了,就立即调用N-API接口napi_reference_unref()napi_delete_reference()

文章写得再自恰也不如呈现一段既注释丰富又可独立运行的参考实现[例程]来得清晰明白。整个例程由四个部分组成:

  1. 模块nj_sys模拟nj_sys crate的部分导出项,因为nj_sys crate并没有入选playground.orgtop 100热门依赖包榜单。

  2. 模块napi_rc包含了对智能指针std::rc::Rc<napi_ref>AOP封装。

  3. 函数napi_export_method()模仿nodejs c-addonFFI导出函数。

  4. 入口函数main()模仿JS程序调用Rust-FFI函数napi_export_method()

“二段式”引用计数优化方案的裨益

  1. 【程序性能】将FFI调用次数减少至一个常量3

  2. 【代码健壮性】将引用复本的数量跟踪任务从易错的人工完成转为机器自动完成。addon业务代码仅需关注引用复本的个数归零事件。

结束语

关于nodejs c-addon技术方向,我这次仅准备了上述偏【编程】内容与大家分享。其实,交叉编译与动态库链接也是一项可以聊出些许深度的话题。比如,如何做到“从一个工程,一个分支,一套Rust程序同时编译出三版.node链接库文件,以分别适用于nodejs / nwjs / electron三款应用程序容器”的呢?。哎!无处不是“黑科技” — 从条件编译,至编译时修改链接目标。在我输出下一篇相关主题的文章前,感兴趣的读者不防率先品鉴我的另一个github工程request-window-attention寻找答案,和给我的工程点个star

创作不易,值得(文章)点赞,(github工程)点star,和(两者都)转发。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:/a/278707.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

SpringCloud(H版alibaba)框架开发教程之Hystrix——附源码(4)

参考博客&#xff1a;https://www.cnblogs.com/cjsblog/p/9391819.html https://blog.csdn.net/tongtong_use/article/details/78611225 Hystrix介绍 在微服务场景中&#xff0c;通常会有很多层的服务调用。如果一个底层服务出现问题&#xff0c;故障会被向上传播给用户。我们…

AI赋能金融创新:技术驱动的未来金融革命

人工智能&#xff08;AI&#xff09;作为一种技术手段&#xff0c;正逐渐改变金融行业的方方面面。从风险管理到客户体验&#xff0c;从交易执行到反欺诈&#xff0c;AI带来了许多创新和机遇。本文将探讨AI在金融领域的应用和其赋能的金融创新。 金融领域一直以来都面临着复杂的…

从方程到预测:数学在深度学习中的作用

图片来源 一、说明 深度学习通常被认为是人工智能的巅峰之作&#xff0c;它的成功很大程度上归功于数学&#xff0c;尤其是线性代数和微积分。本文将探讨深度学习与数学之间的深刻联系&#xff0c;阐明为什么数学概念是该领域的核心。 二、数学框架 从本质上讲&#xff0c;深度…

Visual Studio 配置DLL

我们在用Visual Studio进行开发时&#xff0c;如果没有正确配置DLL&#xff0c;就会出现类似“丢失***.dll”的错误。DLL配置有哪些方法&#xff1f; 1、手动复制 将dll文件拷贝到生成的.exe所在的文件夹里 2、配置环境 在右键属性->配置属性->调试->环境&#xf…

使用Commons JXPath简化XML/JSON处理

第1章&#xff1a;引言 咱们都知道&#xff0c;在现代软件开发中&#xff0c;处理XML和JSON数据几乎是家常便饭。这两种格式广泛应用于配置文件、数据交换、API响应等领域。不过&#xff0c;要手动解析和操作它们&#xff0c;有时候真是让人头大。 当你面对一堆复杂的XML或JS…

目标检测-One Stage-SSD

文章目录 前言一、SSD的网络结构和流程二、SSD的创新点总结 前言 根据前文目标检测-Two Stage-YOLOv1可以看出YOLOv1的主要缺点是&#xff1a; 每个格子针对目标框的回归是不加限制的&#xff0c;导致目标的定位并不是很精准和Faster RCNN等先进Two Stage算法相比&#xff0c…

轻松实现iphone截图传电脑

目录 摘要 引言 用户登录工具和连接设备 生成截图 摘要 本篇博文介绍了克魔助手这款工具&#xff0c;解决了iPhone与Windows系统下图片传输的烦恼。通过连接同一Wi-Fi&#xff0c;使用克魔助手轻松实现了iPhone截图传输到电脑上的便捷操作。用户只需简单地下载并安装克魔助…

【机器学习】深度学习概论(二)

五、受限玻尔兹曼机&#xff08;Restricted Boltzmann Machine&#xff0c;RBM&#xff09; 5.1 RBM介绍 示例代码&#xff1a; Python 编写了一个简单的 RBM 实现&#xff0c;并用一些假数据训练了它。然后&#xff0c;他展示了如何用 RBM 来解释用户的电影偏好&#xff0c;以…

Cocos3D项目中fbx模型转gITF模型和glb模型

1.npm安装&#xff1a;先按照npm哈 npm install --save fbx2gltf -g 2. 到指定目录 cd C:\Program Files\nodejs\node_global\node_modules\fbx2gltf\bin\Windows_NT cmd命令行界面进入node_modules\fbx2gltf文件下的bin文件&#xff0c;然后根据平台选择进入相应目录&#…

mac node基本操作

1 查看所有版本 npm view node versions输出 2 查看已经安装的版本 n list3 安装指定版本 sudo -E n 16.0.04 切换版本 sudo n 16.0.05 查看版本 node -v

ssm基于java的网上手机销售系统论文

摘 要 现代经济快节奏发展以及不断完善升级的信息化技术&#xff0c;让传统数据信息的管理升级为软件存储&#xff0c;归纳&#xff0c;集中处理数据信息的管理方式。本网上手机销售系统就是在这样的大环境下诞生&#xff0c;其可以帮助管理者在短时间内处理完毕庞大的数据信息…

代码随想录刷题笔记(DAY3)

今日总结&#xff1a;虽然之前刷过链表&#xff0c;但这次做的是有些费力的&#xff0c;也有了更深的理解。整理完今天的 Vue 笔记就睡。。。 DAY 3 01. 移除链表元素&#xff08;No. 203&#xff09; 题目链接&#xff1a;https://leetcode.cn/problems/remove-linked-list-…

复数值神经网络可能是深度学习的未来

一、说明 复数这种东西,在人的头脑中似乎抽象、似乎复杂,然而,对于计算机来说,一点也不抽象,不复杂,那么,将复数概念推广到神经网络会是什么结果呢?本篇介绍国外的一些同行的尝试实践,请我们注意观察他们的进展。

详解“量子极限下运行的光学神经网络”——相干伊辛机

量子计算和量子启发计算可能成为解答复杂优化问题的新前沿&#xff0c;而经典计算机在历史上是无法解决这些问题的。 当今最快的计算机可能需要数千年才能完成高度复杂的计算&#xff0c;包括涉及许多变量的组合优化问题&#xff1b;研究人员正在努力将解决这些问题所需的时间缩…

大数据学习(29)-Spark Shuffle

&&大数据学习&& &#x1f525;系列专栏&#xff1a; &#x1f451;哲学语录: 承认自己的无知&#xff0c;乃是开启智慧的大门 &#x1f496;如果觉得博主的文章还不错的话&#xff0c;请点赞&#x1f44d;收藏⭐️留言&#x1f4dd;支持一下博主哦&#x1f91…

苹果电脑Dock栏优化软件 mac功能亮点

hyperdock mac是一款Dock优化软件&#xff0c;hyperdock支持使用窗口自动排列功能&#xff0c;您可以直接通过将窗口拖拉至屏幕上方来快速最大化至全屏&#xff0c;又或者拖动到左右来进行左分屏和右分屏。而且Dock优化软件还有一个特色便是对Dock的强大管理哪里能力&#xff0…

RISC Zero zkVM Host Guest 101

1. 引言 在RISC Zero zkVM应用程序中&#xff0c;host为运行RISC Zero zkVM的机器&#xff08;或系统&#xff09;。host为不可信agent&#xff0c;负责设置zkVM环境和处理执行过程中的输入输出。 host程序&#xff08;代码&#xff09;&#xff0c;是指&#xff1a; zkVM应…

设计模式-Java版本

文章目录 前言设计原则单一职责原则开闭原则里氏替换原则迪米特法则接口隔离原则依赖倒置原则 设计模式构建类型工厂模式抽象工厂建造者模式原型模式单例模式 结构型适配器模式桥接模式组合模式装饰器模式代理模式外观模式享元模式 行为模式责任链模式命令模式迭代器模式中介模…

SONiC和ONL所依赖的Debian版本说明

Debian 的最新几个版本 下一代 Debian 正式发行版的代号为 trixie — 测试&#xff08;testing&#xff09;版 Debian 12 (bookworm) — 当前的稳定&#xff08;stable&#xff09;版 Debian 11 (bullseye) — 当前的旧的稳定&#xff08;oldstable&#xff09;版 Debian 10&a…

PPT录制视频的方法,轻松提升演示效果!

在现代工作和学习中&#xff0c;ppt是一种常见的演示工具&#xff0c;而将ppt转化为视频形式更能方便分享和传播。本文将介绍两种ppt录制视频的方法&#xff0c;每一种方法都将有详细的步骤和简要的介绍&#xff0c;通过这些方法&#xff0c;你可以轻松将ppt制作成视频&#xf…