Skip to main content

脚本原生接口(SNI)

脚本原生接口(Script Native Interface,简称SNI)是一种用于在脚本中调用 C 函数的接口。

类型桥阶层(SNI Type Bridge Layer)

基本数据类型

基本数据类型有:

  • 数值(Number)
  • 布尔(Boolean)
  • 字符串(String)

SNI 直接使用 JerryScript 的 API 来处理基本数据类型。

结构体

SNI 将结构体分为两类:Value Object 与 Handle Object。

Value Object

Value Object 是一种不独立拥有底层资源的运行时对象,用于表示纯数据或配置值。

Value Object 不需要显式创建或销毁,其生命周期通常受限于一次函数调用或表达式求值过程。它在桥接层会被直接封装为 JS 对象,其成员变量会被直接映射为 JS 对象的属性。 它们不会被运行时登记或追踪,也不会参与 Realm 级别的资源管理。

Value Object 的生命周期为栈生命周期(Stack-Scoped)

栈生命周期:由 JS 引擎自动管理,当函数调用结束时,栈上的 Value Object 会被自动销毁。

Handle Object

Handle Object 是一种表示底层资源引用的运行时对象,其本身并不包含资源数据,而是作为对底层分配实体的间接访问入口。

Handle Object 必须通过显式创建获得,并支持显式销毁。 其生命周期由脚本逻辑直接控制,但运行时在 Realm 结束时提供统一的兜底回收,以防止资源泄漏。

Handle Object 的生命周期为堆生命周期(Heap-Scoped)

堆生命周期:由脚本逻辑显式创建,通过调用特定的销毁函数来释放底层资源。当所有对 Handle Object 的引用都被释放时,运行时会自动触发其销毁过程。在本系统中,堆生命周期的对象会在 Realm 结束时被统一销毁。

API 导出层(SNI API Export Layer)

API 导出层(SNI API Export Layer)负责将 API 导出给 JS Realm,使它们在脚本中可直接调用,API 是通过描述表(API Description Table)来定义的。

描述表内包括:

  • 函数
  • 枚举
  • 常量
  • 子命名空间

该层主要负责 API 结构组织。

API 描述表(API Description Table)

API 描述表是一个 C 语言结构体数组,每个元素表示一个 API 条目。

每个 API 条目包含以下字段:

  • 名称(Name)
  • 类型(Type)
  • 值(Value)

[!CAUTION]注意 数组最后一个元素中,名称字段必须为 NULL,用于标识数组结束。

API的类型

目前 SNI 支持的 API 类型有:

  • 函数:jerry_external_handler_t 类型的函数指针
  • 常量:整数常量、浮点数常量、字符串常量
  • 子条目:指向子条目的指针,用于实现命名空间的递归结构

函数 API

函数类型实际上就是 jerry_external_handler_t 类型的函数指针,您需要实现该类型的函数,用于处理 JS 调用。

该函数的参数和返回值都需要符合 JerryScript 引擎的要求,具体可以参考 jerry_external_handler_t - JerryScript 文档

常量 API

常量 API 是指在描述表中定义的整数值、浮点数常量和字符串常量,它们会被直接导出为 JS 中的常量。 它们在 C 代码中通常是枚举类型或由宏定义的常量。

子条目 API

子条目 API 是指在描述表中定义的指向子条目的指针,用于实现命名空间的递归结构。

例如:

jerry_value_t my_lv_obj_create(const jerry_call_info_t *call_info_p,
const jerry_value_t args_p[],
const jerry_length_t args_count)
{
// 参数检查
// ...
// 参数类型转换
// ...
// 执行 C 函数
// ...
// 检查返回值
// ...
// 返回创建的 Handle Object
}

const struct sni_api_entry_t lvgl_api_desc[] = {
{ "obj", SNI_ENTRY_NAMESPACE, { .sub_entries = lvgl_obj_api_desc } },
// 其他子条目...
};

const struct sni_api_entry_t lvgl_obj_api_desc[] = {
{ "create", SNI_ENTRY_FUNCTION, { .function = my_lv_obj_create } },
// 其他子条目...
};

这样,您就可以在 JS 中通过 lv.obj.create() 来调用 C 函数 my_lv_obj_create() 了。

API 的导出过程

API 导出由以下过程组成:

  1. 获取描述表代码
  2. 描述表的解析
  3. API 的挂载

获取描述表代码

描述表代码一般通过 Python 脚本生成。

例如,LVGL API的描述表代码可以通过以下命令生成:

python3 generate_lvgl_desc.py

当然,您也可以根据需求自行编写描述表代码。

描述表的解析

描述表的解析只需要调用sni_api_build()函数即可构建 API 描述表,若构建成功则会返回一个jerry_value_t类型的 JS 对象,此对象下挂载了所有 API 条目,此对象通常称为全局原生对象(Global Native Capability Object),例如lv就是一个全局原生对象。

API 的挂载

API 的挂载过程相对简单,只需要调用sni_api_mount()函数即可将 API 描述表挂载到指定的 JS Realm 中。

例如,挂载 LVGL API 描述表到指定 Realm 中可以通过以下代码实现:

jerry_value_t lvgl_api_obj = sni_api_build(lvgl_api_desc);
sni_api_mount(jerry_realm, lvgl_api_obj, "lv");

挂载成功后,JS Realm 中就可以直接调用 LVGL API 了。

例如:

lv.obj.create();