【10分钟学会】通过 Node-API 实现Native 和TS交互

什么是 Node.js​

JavaScript 诞生于 1995 年,几乎是和互联网同时出现;Node.js 诞生于 2009 年,比 JavaScript 晚了 15 年左右。

Node.js 不是一门新的编程语言,也不是一个 JavaScript 框架,它是一套 JavaScript 运行环境,用来支持 JavaScript 代码的执行。用编程术语来讲,Node.js 是一个 JavaScript 运行时(Runtime,可以类比于Java的JRE,Java Runtime Environment)。

Node.js 之前,JavaScript 只能运行在浏览器中,作为网页脚本使用,为网页添加一些特效,或者和服务器进行通信。有了 Node.js 以后,JavaScript 就可以脱离浏览器,像其它编程语言一样直接在计算机上使用,再也不受浏览器的限制了。换句话说,Node.js 几乎完全抛弃了浏览器,自己从头构建了一套全新的 JavaScript 运行时。

Node.js 运行时主要由 V8 引擎标准库本地模块组成。

  • v8引擎:是 JavaScript 解释器,它负责解析和执行 JavaScript 代码(类比于Java的JVM)。
  • 标准库:提供了一套优雅的 JavaScript 接口给开发人员,并且要保持接口在不同平台(操作系统)上的一致性。
  • 本地模块:Node.js 集成了众多高性能的开源库,它们使用 C/C++ 语言实现。比如 libuvnmpOpenSSL

注:谷歌公司在 Chrome 浏览器中集成了一种名为"V8"的 JavaScript 引擎(也即 JavaScript 解释器),它能够非常快速地解析和执行 JavaScript 代码。V8 引擎使用 C++ 语言编写,可以独立运行,也可以嵌入到任何其它 C++ 程序中。谷歌公司将 V8 引擎甚至整个 Chrome 浏览器都开源了,任何人都可以免费地将 V8 应用到自己的项目中。

什么是 TypeScript

TypeScript,简称TS,是指添加了类型系统的 JavaScript。TS一种由微软开发的自由和开源的编程语言,它是 JavaScript 的一个超集,扩展了 JavaScript 的语法,是一种面向对象的语言,更适合开发大型复杂应用程序。

TS代码经过编译,最终会产物是.js代码。TS 只会在编译时对类型进行静态检查,如果发现有错误,编译的时候就会报错 ​。而在运行时,与普通的 JS 文件一样,不会对类型进行检查。

什么是 ArkTS

ArkTS是HarmonyOS优选的主力应用开发语言。它在TypeScript的基础上,匹配ArkUI框架,扩展了声明式UI、状态管理等相应的能力,是TypeScript的超集,底层对接ArkTS引擎。

什么是 Node-API

Node-API(以前称为 N-API)是用于封装JavaScript能力为Native插件的API,独立于底层JavaScript,并作为Node.js的一部分。Node-API可以去除底层的JavaScript引擎(如V8引擎等)的差异,提供一套稳定的接口。

HarmonyOS的Native API组件对Node-API的接口进行了重新实现,底层对接了ArkJS等引擎。当前支持Node-API标准库中的部分接口

Node-API 基本使用

线程约束

ark引擎会对js对象线程使用进行保护,使用不当会引起应用crash,因此需要遵循如下原则:

  • napi接口只能在js线程使用。
  • env与线程绑定,不能跨线程使用。
  • native侧js对象与线程所持有的env绑定,即native侧js对象只能在创建时的线程使用

注册模块

  • 步骤1:编写特定函数,在 so加载时触发"注册"动作
C 复制代码
// 准备模块加载相关信息,将上述Init函数与本模块名等信息记录下来。
static napi_module demoModule = {
    .nm_version =1,
    .nm_flags = 0,
    .nm_filename = nullptr,
    .nm_register_func = Init,
    .nm_modname = "entry", 
    .nm_priv = ((void*)0),
    .reserved = { 0 },
};

// 打开so时,该函数将自动被调用,使用上述demoModule模块信息,进行模块注册相关动作。
extern "C" __attribute__((constructor)) void RegisterHelloModule(void)
{
    napi_module_register(&demoModule);
}

注意:

  1. 建议: nm_modname 的值应该与so命名一致,且大小写也一致。如,so的名称为libentry.so,那么 nm_modname 的值应为 entry
  2. 规则: nm_register_func 对应的函数需要加上static,防止与其他so里的符号冲突
  3. 规则: __attribute__((constructor))修饰的函数的函数名需要确保不与其他模块的重复
  • 步骤2:编辑注册信息 在 nm_register_func 中指定的函数中补充注册信息,建立 TS Call Native 的入口
C 复制代码
EXTERN_C_START
// Init将在exports上挂上Add/NativeCallArkTS这些native方法,此处的exports就是开发者import之后获取到的ArkTS对象。
static napi_value Init(napi_env env, napi_value exports)
{
     // 函数描述结构体
    // 以Add为例
    // 第一个参数"add"为暴露给TS层的方法名称
    // 第三个参数"Add"为Native方法
    // 注册多个 Native 方法,继续追加数组即可
    napi_property_descriptor desc[] = {
        { "add", nullptr, Add, nullptr, nullptr, nullptr, napi_default, nullptr },
        { "nativeCallArkTS", nullptr, NativeCallArkTS, nullptr, nullptr, nullptr, napi_default, nullptr },
        // 略...
    };
    // 在exports这个ArkTS对象上,挂载native方法。
    napi_define_properties(env, exports, sizeof(desc) / sizeof(desc[0]), desc);
    return exports;
}
EXTERN_C_END
  • 步骤3:编辑声明文件,方便 TS层对 Native 层的调用 在项目中存在 index.d.ts 声明文件。在该文件中按照如下规则将Native层暴露的接口进行声明
ts 复制代码
// 声明规则:
// export 表示导出,add 是 函数名
// a b 是 参数,类型是 number
// =>number 表示返回值类型是 number
export const add: (a: number, b: number) => number;

注:d.ts 文件的作用仅仅是用来声明。如果没有在该文件中声明,TS层强制调用,也是允许的。只是在没有声明的情况下,IDE不会弹出提示

详见官方demo

TS Call Native

  • 步骤1:在TS的模块中添加依赖

编辑TS 模块下的 oh-package.json5 文件

json5 复制代码
{
  "license": "",
  "devDependencies": {},
  "author": "",
  "name": "entry",
  "description": "Please describe the basic information.",
  "main": "",
  "version": "1.0.0",
  "dependencies": {
    // 指向 entry Native 模块的配置文件的文件夹,并将其命名为 libentry.so
    "libentry.so": "file:./src/main/cpp/types/libentry"
  }
}
  • 步骤2:在 TS 代码中引入 entry Native 模块
TS 复制代码
// 此处的 libentry.so 即为 配置文件中引入的依赖
// 并将其命名为 testNapi 
import testNapi from 'libentry.so';
  • 步骤3:调用
TS 复制代码
// 使用 testNapi 调用 Native 方法
testNapi.add(2, 3);

注意:由于 entry Native 模块配置文件夹中存在 d.ts 文件,所以编写代码时 IDE 可以弹出调用 add() 方法的提示

  • 步骤4:Native 响应

根据前一个大步骤中补充的注册信息,TS 层调用 add() 方法,会调用到Native 层的 Add() 当中

C 复制代码
// env 为当前运行上下文,与线程绑定
// TS 层 传递的参数都在 info 中
static napi_value Add(napi_env env, napi_callback_info info) {
    size_t argc = 2;
    napi_value args[2] = {nullptr};
    // 从 info 中获取 传递的参数
    napi_get_cb_info(env, info, &argc, args, nullptr, nullptr);

    napi_valuetype valuetype0;
    napi_typeof(env, args[0], &valuetype0);

    napi_valuetype valuetype1;
    napi_typeof(env, args[1], &valuetype1);

    // 将 TS 参数 number 类型转换为 C/C++ 的 double 类型
    double value0;
    napi_get_value_double(env, args[0], &value0);

    double value1;
    napi_get_value_double(env, args[1], &value1);

    napi_value sum;
    int32_t ret = test2(1, 2);
    // 构造返回值,并将结果保存到的 napi_value 中
    napi_create_double(env, ret, &sum);

    // 返回 napi_value ,传递给 TS 层
    return sum;
}

Native Call TS

Native 调用 TS 的Function 是项目中十分常见的行为。但是调用过程又存在着诸多限制。 下面主要介绍两种情况下,Native 是如何调用 TS Function的

首先,我们需要定义一个 TS Function。那么,我们的目标就是能够从Native的逻辑中成功触发 testFunction() 方法。

TS 复制代码
// 定义一个名为 testFunction() 的TS 层方法
function testFunction(a: number): number {
    // TODO print something

    // return value
    return 1024;
}

在主线程发起调用

  • 步骤1:准备env

可以将主线程 env保存起来。比如,在上述的 注册模块 的步骤中,将主线程的env保存至全局变量

C 复制代码
EXTERN_C_START static napi_value Init(napi_env env, napi_value exports) {
    napi_property_descriptor desc[] = {
        {"add", nullptr, Add, nullptr, nullptr, nullptr, napi_default, nullptr},
        // 略...
    };
    napi_define_properties(env, exports, sizeof(desc) / sizeof(desc[0]), desc);

    // 保存主线程 env
    g_env = env;

    return exports;
}
EXTERN_C_END

注:env 与线程绑定,所以该 env 只能在主线程中使用。请勿跨线程使用

  • 步骤2:准备 TS 层的方法

Node-API 无法像 JNI 一样 通过某些 find 方法查找TS层类或者方法(也许是没有找到,欢迎大家补充)。 因此需要设计一个接口,主动将 TS 层的方法传递到Native层,然后保存起来使用。

C 复制代码
// 定义一个TS 层Function的全局引用
napi_ref js_fun_ref;

// 设计一个Native接口
void *Napi_Register(void *env, void *info) {
    // 1. 获取参数信息
    // 参数个数与 TS 层 传入的要严格对应
    int32 const argc = 1;
    napi_value argv[argc] = {NULL};
    napi_get_cb_info(env, info, &argc, argv, NULL, NULL);

    // napi_value 类型的 argv[0] 即为 TS 层Function的引用

    // 保存 TS 层Function 的引用
    napi_create_reference(env, argv[0], 1,  &js_fun_ref);

    // 不再使用时,调用 napi_delete_reference() 销毁引用...略
    // 不再使用时,调用 napi_delete_reference() 销毁引用...略
    // 不再使用时,调用 napi_delete_reference() 销毁引用...略

    napi_value ret;
    napi_create_int32(env, 0, &ret);
    return ret;
}

TS层主动调用一下 Napi_Register() 方法,将 待调用的TS方法传递给Native层

TS 复制代码
// 将 testFunction 方法注册到native层
testNapi.Napi_Register(testFunction);
  • 步骤3:在Native层 Call TS层的方法 准备好 env 和 TS层方法的引用以后,就可以Call了
C 复制代码
// 取出 TS 层Function的值
napi_value js_fun = NULL;
napi_get_reference_value(g_env, js_fun_ref, &js_fun);

// 准备 入参
size_t argc = 1;
napi_value argv[1];
napi_create_int32(env, 48000, &argv[0]);

// 关键: Call
// 最后一个参数表示 TS Function 的返回值。此处略...
napi_status callRet = napi_call_function(env, NULL, js_callback, argc, argv, NULL);
if (callRet != napi_ok) {
    // LOGE("NAPI real callback failed: %d", callRet); 
}

在Native子线程发起调用

ark引擎会对js对象线程使用进行保护,napi接口只能在js线程使用,使用不当会引起应用crash。但是在实际开发中,更多的场景是创建几个子线程去执行任务。那么此时在子线程中该如何调用TS层的方法呢?

  • 步骤1:准备一个Native层的 threadsafe 方法 Native 子线程对TS Function发起请求,会调度到这个方法中执行。该方法所在的线程是 主线程。只有在主线程中才能发起对 TS 的调用
C 复制代码
static void Napi_ThreadSafe_TestFunction(napi_env env, napi_value js_callback, void *context, void *data)
{
    // 参考前面的思路,通过 napi_call_function() 方法对 TS Function 直接发起调用
    // 此处不再赘述...
}
  • 步骤2:准备 TS 层的方法 同样地,需要将TS Function 注册到Native层。但不同的是,Native层拿到Function以后,需要为其创建一个可以多线程调用的持久引用。 该引用可以将Native层子线程的请求异步调度到主线程,然后在主线程中发起对TS Function 的调用。这意味着:子线程发起的请求永远是异步的。
C 复制代码
// 定义一个TS 线程安全Function的全局引用
napi_threadsafe_function funHandle;

// 设计一个Native接口
void *Napi_Register(void *env, void *info) {
    // 1. 获取参数信息
    // 参数个数与 TS 层 传入的要严格对应
    int32 const argc = 1;
    napi_value argv[argc] = {NULL};
    napi_get_cb_info(env, info, &argc, argv, NULL, NULL);

    // napi_value 类型的 argv[0] 即为 TS 层Function的引用
    status = napi_create_threadsafe_function(env, argv[0], NULL, 'testFunction', 0, 1, NULL, NULL, NULL, Napi_ThreadSafe_TestFunction, funHandle);
    if (status != napi_ok) {
        LOGE("Failed to create threadsafe_function");
    }


    // 不再使用时,调用 napi_release_threadsafe_function() 销毁 funHandle 引用...略
    // 不再使用时,调用 napi_release_threadsafe_function() 销毁 funHandle 引用...略
    // 不再使用时,调用 napi_release_threadsafe_function() 销毁 funHandle 引用...略

    napi_value ret;
    napi_create_int32(env, 0, &ret);
    return ret;
}
  • 步骤3:发起调用 在子线程中,根据业务需要,在合适的实际可以发起对TS Function 的调用。
C 复制代码
// funHandle:上一个步骤生成的 线程安全Function的全局引用
// param:传递到步骤1指定函数中的参数指针
// napi_tsfn_nonblocking :如果队列已满,调用应该阻塞
napi_status status = napi_call_threadsafe_function(funHandle, param, napi_tsfn_nonblocking);
if (status != napi_ok) {
    LOGE("napi_call_threadsafe_function failed. status = %d", status);
}

通过以上3个步骤,可以实现Native 子线程对TS Function的调用。 在此过程中,涉及到线程切换。所以十分有必要对每一个步骤执行时所在的线程进行梳理:

  • 首先,请求发起是发生在步骤3。所以执行 napi_call_threadsafe_function() 方法是在Native的子线程。在一般情况下,调用该方法后,子线程不会阻塞(即,不会等待主线程TS Function执行的结果),继续执行自己的任务。
  • 然后,主线程根据自己的任务队列,调度执行到步骤1中声明的Native方法。在该方法中,会通过 napi_call_function() 方法同步执行 TS Function,并且可以拿到TS Function的返回值。注意,此时是同步调用
  • 最后,步骤2中的 注册 过程,是由TS层调度过来的。一般发生在主线程。注意,env与线程绑定,不能跨线程使用
相关推荐
加班是不可能的,除非双倍日工资2 小时前
css预编译器实现星空背景图
前端·css·vue3
wyiyiyi2 小时前
【Web后端】Django、flask及其场景——以构建系统原型为例
前端·数据库·后端·python·django·flask
gnip3 小时前
vite和webpack打包结构控制
前端·javascript
excel3 小时前
在二维 Canvas 中模拟三角形绕 X、Y 轴旋转
前端
阿华的代码王国3 小时前
【Android】RecyclerView复用CheckBox的异常状态
android·xml·java·前端·后端
一条上岸小咸鱼3 小时前
Kotlin 基本数据类型(三):Booleans、Characters
android·前端·kotlin
Jimmy3 小时前
AI 代理是什么,其有助于我们实现更智能编程
前端·后端·ai编程
ZXT4 小时前
promise & async await总结
前端
Jerry说前后端4 小时前
RecyclerView 性能优化:从原理到实践的深度优化方案
android·前端·性能优化
画个太阳作晴天4 小时前
A12预装app
linux·服务器·前端