Momentum Firmware JS 引擎数据类型详解:mJS 的 string、number、foreign、ArrayBuffer 与 DataView 全解析
【免费下载链接】Momentum-Firmware🐬 Feature-rich, stable and customizable Flipper Firmware项目地址: https://gitcode.com/GitHub_Trending/mo/Momentum-Firmware
mJS 是内置于 Momentum Firmware(基于 Flipper Zero 的固件)中的轻量级 JavaScript 引擎,用于运行用户编写的 JS 脚本应用(.js)。本文以 documentation/js/js_data_types.md 为核心,逐一剖析 mJS 支持的 10 类数据类型,并结合 lib/mjs 下的引擎源码与 applications/system/js_app/examples/apps/Scripts 中的真实示例,讲清每种类型的语义、底层表示与实战用法。读完本文,你将能准确判断在固件脚本中何时用 Object、何时用 Array、如何用 ArrayBuffer/DataView 做二进制数据处理,并理解 foreign 指针类型在 JS 与 C 边界间的桥梁作用。
一、mJS 数据类型总览
原文档给出的 mJS 常用数据类型清单如下:
| 数据类型 | 说明 |
|---|---|
string | 单字节字符序列,不支持 UTF8 |
number | 数值 |
boolean | 布尔值 |
foreign | C 函数或数据指针 |
undefined | 未定义值 |
null | 空值 |
Object | 带命名字段的数据结构 |
Array | 特殊类型对象,所有项都有索引且类型相等 |
ArrayBuffer | 原始数据缓冲区 |
DataView | 提供访问 ArrayBuffer 内容的接口 |
从引擎内部视角看,这些类型被划分为"原始类型"与"Object 的类别"两大类。在 lib/mjs/mjs_core_public.h 的enum mjs_type中可以看到引擎对类型的官方分组:
- 原始类型(Primitive types):
MJS_TYPE_UNDEFINED、MJS_TYPE_NULL、MJS_TYPE_BOOLEAN、MJS_TYPE_NUMBER、MJS_TYPE_STRING、MJS_TYPE_FOREIGN、MJS_TYPE_ARRAY_BUF、MJS_TYPE_ARRAY_BUF_VIEW - Object 的不同类别:
MJS_TYPE_OBJECT_GENERIC(普通对象)、MJS_TYPE_OBJECT_ARRAY(数组)、MJS_TYPE_OBJECT_FUNCTION(函数)
值得注意:ArrayBuffer与DataView在枚举中归属于原始类型分组,而Array与Function属于 Object 类别——这与 ECMAScript 中"函数和数组都是对象"的认知一致,也解释了为何 mJS 中Array的每一项都要求拥有索引、且类型相等。
二、string:单字节字符序列(无 UTF-8)
mJS 的字符串是单字节字符序列,每个字符恰好占 1 个字节,不提供 UTF-8 多字节编码支持。这意味着中文等多字节字符无法被 mJS 字符串直接正确表示,开发脚本时应避免在字符串中嵌入非 ASCII 字符。
引擎内部为字符串实现了精细的分级存储策略。在 lib/mjs/mjs_core_public.h 中定义了多种字符串标签:
MJS_TAG_STRING_I:内联字符串,长度 < 5 字节,直接嵌入值中,零额外分配MJS_TAG_STRING_5:内联字符串,长度恰好 5 字节MJS_TAG_STRING_O:自有字符串(owned string),引擎持有其内存所有权MJS_TAG_STRING_F:外来字符串(foreign string),指向外部数据,不拷贝MJS_TAG_STRING_C:字符串分片(chunk)MJS_TAG_STRING_D:字典字符串(dictionary string),用于属性名等可共享的短串
这种"短串内联、长串外存"的设计(对应 lib/mjs/mjs_string.h 中embed_string等实现)是 mJS 面向内存受限嵌入式环境的核心优化:5 字节以内的短字符串不产生堆分配,避免频繁触发垃圾回收。
mJS 为字符串提供了toLowerCase、toUpperCase、slice、indexOf、charCodeAt等方法(见 lib/mjs/mjs_string.h),日常脚本处理文本足够使用。
三、number:IEEE 754 双精度浮点与 NaN-packing
mJS 的number遵循 IEEE 754 双精度浮点格式(8 字节):1 位符号位、11 位指数、52 位尾数。与标准 JavaScript 一致,NaN、Infinity等特殊值同样存在。
mJS 在底层实现上采用了著名的NaN-packing技术:既然NaN的指数位全为 1,mJS 就借用NaN的载荷区来"打包"所有 JS 值。在 lib/mjs/mjs_core_public.h 中有完整说明:
11111111|1111tttt|vvvvvvvv|vvvvvvvv|vvvvvvvv|vvvvvvvv|vvvvvvvv|vvvvvvvv NaN marker |type| 48-bit placeholder for values: pointers, strings- 前 12 位固定为
0xfff(NaN 标记) - 4 位
type标签,标识值的具体类型 - 低 48 位存放实际载荷(指针、字符串偏移等)
typedef uint64_t mjs_val_t;定义了引擎中所有 JS 值的统一表示(lib/mjs/mjs_core_public.h)。由于 64 位平台上指针实际只有 48 位有效地址空间,指针可以安全塞入载荷区。标签定义见 lib/mjs/mjs_core_public.h,共使用 32 种可能标签中的一部分,例如:
MJS_TAG_OBJECT、MJS_TAG_FOREIGN、MJS_TAG_UNDEFINED、MJS_TAG_BOOLEANMJS_TAG_ARRAY、MJS_TAG_FUNCTION、MJS_TAG_NULLMJS_TAG_ARRAY_BUF(ArrayBuffer)、MJS_TAG_ARRAY_BUF_VIEW(DataView)
C 侧创建与读取数字的 API 为mjs_mk_number()与mjs_get_double()、mjs_get_int()、mjs_get_int32()(见 lib/mjs/mjs_primitive_public.h)。其中mjs_get_int()会丢弃小数部分,而mjs_get_int32()保证返回 32 位有符号整数——在固件脚本中做位运算或与硬件寄存器打交道时,建议使用后者语义一致的写法。
四、boolean、undefined 与 null
这三个是 mJS 的基础标量类型,语义与标准 JavaScript 一致:
- boolean:只有
true和false两个值。C 侧通过mjs_mk_boolean()创建、mjs_get_bool()读取、mjs_is_boolean()判断(lib/mjs/mjs_primitive_public.h)。 - undefined:表示"未定义",通常是未初始化变量或函数未返回值。对应宏
MJS_UNDEFINED(lib/mjs/mjs_primitive_public.h)。 - null:表示"空值",对应宏
MJS_NULL。注意 mJS 保留了mjs_mk_null()兼容接口,但官方注释已标记其废弃,推荐直接使用MJS_NULL宏(lib/mjs/mjs_primitive_public.h)。
在 ArrayBuffer/DataView 的读写边界上,undefined还被用作"越界哨兵":例如 DataView 按索引取值越界时,mjs_dataview_get()会返回MJS_UNDEFINED(见 lib/mjs/mjs_array_buf.c),这与标准 JS 中越界读取返回undefined的行为保持一致。
五、foreign:C 函数与数据指针的桥接
foreign是 mJS 中最具嵌入式特色的类型:它表示一个C 函数指针或 C 数据指针。其创建 API 有两个:
mjs_mk_foreign(mjs, ptr):打包数据指针(lib/mjs/mjs_primitive_public.h)mjs_mk_foreign_func(mjs, fn):打包函数指针(lib/mjs/mjs_primitive_public.h),宏MJS_MK_FN(fn)是其便捷写法
读取侧对应mjs_get_ptr(),判断侧为mjs_is_foreign()(lib/mjs/mjs_primitive_public.h)。
原文档明确指出 foreign 的语义边界:JS 代码不能对 foreign 值做任何有用的操作,只能在属性中持有它并传来传去,它表现得像一个没有属性的密封对象。这正是 foreign 的设计初衷——它纯粹是 C 侧注册的 JS 内置函数(如print、delay等)在引擎内部的载体,JS 脚本一般不会直接接触到 foreign 值。
源码中还给出了一个重要的存储告诫:由于 foreign 依赖 48 位地址空间,在需要存放正好sizeof(void*)字节、且sizeof(void*) >= 8的原始数据时,不要用 foreign,请改用字节数组(ArrayBuffer)(lib/mjs/mjs_primitive_public.h)。这条建议直接指向下一节的主角。
六、Object 与 Array:数据结构的两类载体
- Object:带命名字段的数据结构,对应
MJS_TYPE_OBJECT_GENERIC。脚本中用字面量{a: 1, b: "x"}创建,C 侧用mjs_mk_object()、mjs_set()、mjs_get()操作。字段名通常走字典字符串(MJS_TAG_STRING_D)路径,重复出现的属性名可共享同一份存储。 - Array:
MJS_TYPE_OBJECT_ARRAY。原文档强调其特殊性——所有项都有索引,且类型相等。这意味着 mJS 的数组在实现上比标准 JS 数组更严格、也更紧凑,适合存放同构数据序列(例如一组温度读数、一组 RGB 值)。C 侧 API 见 lib/mjs/mjs_array.h 与mjs_array_public.h,支持mjs_array_get、mjs_array_push_internal(push)、mjs_array_splice(splice)等常规操作。
需要留意:正因数组要求"元素类型相等",混入不同类型元素的写法在 mJS 中可能触发类型错误,编写脚本时应尽量保证数组元素的同质性。
七、ArrayBuffer:原始二进制缓冲区
ArrayBuffer是 mJS 提供的原始数据缓冲区,对应标签MJS_TAG_ARRAY_BUF(lib/mjs/mjs_core_public.h),类型为MJS_TYPE_ARRAY_BUF。它不与任何具体数据类型绑定,只是若干字节的连续内存。
在引擎内部,所有 ArrayBuffer 的数据存放在一个统一管理的mbuf(动态字节缓冲)中(mjs->array_buffers),每个缓冲区前有一个 varint 编码的长度头。创建与读取的 C API 为mjs_mk_array_buf()与mjs_array_buf_get_ptr()(lib/mjs/mjs_array_buf_public.h)。为避免频繁扩容,每次分配还会多预留MJS_ARRAY_BUF_RESERVE(默认 100)字节(lib/mjs/mjs_array_buf.c)。
mJS 为 ArrayBuffer 提供了slice(start, end)方法:校验参数个数(0~2 个)、起始/结束位置合法性后,从源缓冲区切出一段并返回新的 ArrayBuffer(实现见 lib/mjs/mjs_array_buf.c)。仓库自带的示例脚本完整演示了这一用法:
let arr_1 = Uint8Array([0, 1, 2, 3, 4, 5, 6, 7, 8, 9]); print("len =", arr_1.buffer.byteLength); let arr_2 = Uint8Array(arr_1.buffer.slice(2, 6)); print("slice len =", arr_2.buffer.byteLength); for (let i = 0; i < arr_2.buffer.byteLength; i++) { print(arr_2[i]); }(完整文件见 array_buf_test.js)
八、DataView:类型化视图与类型化数组
DataView(标签MJS_TAG_ARRAY_BUF_VIEW)提供访问 ArrayBuffer 内容的类型化接口,对应原文档所述"provides interface for accessing ArrayBuffer contents"。通过它可以把同一段缓冲区解释为不同宽度的整数序列。
引擎内置了 6 种元素类型,定义在 lib/mjs/mjs_array_buf_public.h 的mjs_dataview_type_t中:
| 类型 | 元素宽度 | 说明 |
|---|---|---|
MJS_DATAVIEW_U8 | 1 字节 | 无符号 8 位整数 |
MJS_DATAVIEW_I8 | 1 字节 | 有符号 8 位整数 |
MJS_DATAVIEW_U16 | 2 字节 | 无符号 16 位整数 |
MJS_DATAVIEW_I16 | 2 字节 | 有符号 16 位整数 |
MJS_DATAVIEW_U32 | 4 字节 | 无符号 32 位整数 |
MJS_DATAVIEW_I32 | 4 字节 | 有符号 32 位整数 |
元素宽度映射在mjs_dataview_get_element_len()中实现(lib/mjs/mjs_array_buf.c)。这 6 种视图在 JS 全局作用域下对应构造器:Uint8Array、Int8Array、Uint16Array、Int16Array、Uint32Array、Int32Array,注册于mjs_init_builtin_array_buf()(lib/mjs/mjs_array_buf.c)。
DataView构造器支持三种入参形式(见 lib/mjs/mjs_array_buf.c):
- 传入现有 ArrayBuffer:在已有缓冲区上建立视图(
mjs_mk_dataview_from_buf); - 传入数字长度:自动分配等长的新 ArrayBuffer;
- 传入普通数组:按元素类型逐个拷贝数组元素到新缓冲区。
读取与写入分别由mjs_dataview_get()/mjs_dataview_set()完成,且二者都会做越界检查——按索引读取越界返回undefined,写入越界则返回MJS_TYPE_ERROR(lib/mjs/mjs_array_buf.c)。视图内部通过_t字段记录元素类型、buffer字段引用底层 ArrayBuffer(lib/mjs/mjs_array_buf.c)。此外,从 ArrayBuffer 建立视图时要求缓冲区长度必须是元素宽度的整数倍,否则报MJS_BAD_ARGS_ERROR。
九、在 Flipper 脚本中的实战模式
官方文档在 documentation/js/ReadMe.md 中特别提示:documentation/js目录下的说明为手工维护,可能存在滞后,权威依据是 TypeScript 类型定义(applications/system/js_app/packages/fz-sdk下的.d.ts文件)与applications/system/js_app/examples/apps/Scripts下的示例脚本。结合示例,可以看到 ArrayBuffer/Uint8Array 在固件脚本中的三类典型用途:
1. 外设收发缓冲(i2c/spi)在 i2c.js 与 spi.js 中,读写数据均以Uint8Array(data_buf)形式构造缓冲,注释明确写道"也可以直接传Uint8Array([0x00, 0x00, ...])作为写参数",即字节数组既是写入参数也是读取结果的载体。
2. 串口数据视图(uart)uart_echo.js 中let data_view = Uint8Array(rx_data);将收到的原始字节流包装为类型化视图,再逐元素访问——这正是 DataView 语义(读写 ArrayBuffer 内容的接口)的直接体现。
3. BLE 广播包构造(blebeacon)blebeacon.js 中blebeacon.setData(Uint8Array(packet))用Uint8Array打包广播数据,对应类型定义 blebeacon/index.d.ts 中setData(data: Uint8Array)的签名。此外 gui.js 中defaultData: Uint8Array([0x11, 0x22, ...])表明 GUI 组件的字节属性同样接受类型化数组。
4. 存储 API 的缓冲参数在 C 侧模块 js_storage.c 中,参数校验信息显示存储接口的写参数"expected string or ArrayBuffer"——即ArrayBuffer与string一样可作为文件写入的数据来源,这说明 ArrayBuffer 已深度融入固件 JS API 的参数体系。
十、总结
mJS 的 10 类数据类型构成了 Flipper 脚本世界的完整类型体系:string(单字节、无 UTF-8)与number(IEEE 754 双精度)处理基础数据,boolean/undefined/null表达逻辑与空值,foreign在 JS 与 C 之间传递指针,Object/Array组织结构化数据(数组要求元素类型相等),而ArrayBuffer+DataView组合则提供了面向外设通信的原始二进制处理能力。理解这套类型系统,尤其是"NaN-packing 统一表示所有值""字符串短串内联""ArrayBuffer 集中缓冲管理"三个底层设计,将帮助你在 Momentum Firmware 上写出更高效、更符合引擎预期的 JS 应用。
进一步阅读:引擎源码位于 lib/mjs,类型化数组完整实现见 mjs_array_buf.c 与 mjs_array_buf_public.h;脚本示例见 examples/apps/Scripts/Examples;权威类型声明见 packages/fz-sdk。
【免费下载链接】Momentum-Firmware🐬 Feature-rich, stable and customizable Flipper Firmware项目地址: https://gitcode.com/GitHub_Trending/mo/Momentum-Firmware
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考