【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与线程绑定,不能跨线程使用
相关推荐
hackeroink1 小时前
【2024版】最新推荐好用的XSS漏洞扫描利用工具_xss扫描工具
前端·xss
迷雾漫步者3 小时前
Flutter组件————FloatingActionButton
前端·flutter·dart
向前看-3 小时前
验证码机制
前端·后端
燃先生._.4 小时前
Day-03 Vue(生命周期、生命周期钩子八个函数、工程化开发和脚手架、组件化开发、根组件、局部注册和全局注册的步骤)
前端·javascript·vue.js
高山我梦口香糖5 小时前
[react]searchParams转普通对象
开发语言·前端·javascript
m0_748235245 小时前
前端实现获取后端返回的文件流并下载
前端·状态模式
m0_748240256 小时前
前端如何检测用户登录状态是否过期
前端
black^sugar6 小时前
纯前端实现更新检测
开发语言·前端·javascript
寻找沙漠的人7 小时前
前端知识补充—CSS
前端·css