本文正在参加星光计划3.0–夏日挑战赛
摘要: 本文主要讲述Node.js中的N-API模块,并以一个简单的例子展示如何实现JS->N-API->C/C++调用过程。
技术选型:
UI开发选择C位的eTS,逻辑实现选择C/C++,API调用选择NAPI。OpenHarmonyOS JS运行时与native框架之间的交互实现方式有三种,分别是:JSI机制、JNI机制、NAPI机制。OpenHarmony 源码解析之JavaScript API框架(NAPI) 今天咱们就来探探eTS+napi的实现。 eTS开发介绍: TS ArkUI-基于TS扩展的声明式开发范式-方舟开发框架(ArkUI)-UI-开发-HarmonyOS应用开发
napi开发介绍: Native API在应用工程中的使用指导
案例: HarmonyOS系统Codelabs技术指导_华为HarmonyOS源代码 - HarmonyOS应用开发官网
新建项目
1.打开DevEco Studio,点击 Create Project创建一个新工程,如果你之前已经创建过工程,则点击File -> New -> New project
2.进入选择ability template界面,选择Native C++。
3.进入配置工程界面,选择默认,然后点击Finish。
代码结构解读
本节只对核心代码进行讲解,整个工程的代码结构如下:
文件说明如下:
├── cpp: // C++代码区
│ ├── types: // 接口存放文件夹
│ │ └── libentry
│ │ ├── index.d.ts // 接口文件,供ets调用
│ │ └── package.json // 接口注册配置文件
│ ├── CmakeLists.txt // Cmake编译配置文件
│ └── hello.cpp // C++源代码
└── ets // ets代码区
└── MainAbility
├── pages
| └──index.ets // 主页面
└── app.ets // Ability,提供对Ability生命周期、上下文环境等调用管理
eTS调用C++方法流程
index.ets中引入动态库,调用动态库中的add方法。
import testNapi from "libentry.so"
...
Text(this.message)
...
.onClick(() => { console.log("Test NAPI 2 + 3 = " + testNapi.add(2, 3)); }
eTS与C/C++的衔接处就是N-API。
N-API是什么
N-API是一个C风格的API,使用C/C++语言编写,是Node.js提供的一套用于开发Native(C/C++)模块的接口。N-API是Node.js 8.0.0新增的实验性特性,作为Node.js本身的一部分被引入,在Node.js 10.0.0正式启用。
遇见N-API
接下来将使用上面生成的demo作为示例介绍N-API。首先N-API的调用顺序为:模块注册->属性定义->函数实现。
函数实现
我们需要定义Add
方法,并返回求和的结果。文件的开头我们需要引入node_api.h
头文件。然后我们需要实现一个回调函数,其定义为:
typedef napi_value (*napi_callback)(napi_env env, napi_callback_info info);
napi_callback
是一个函数指针,其接受类型分别为napi_env
以及napi_callback_info
的两个参数,并返回类型为napi_value
的值。Add
方法中涉及到的几个类型定义及其用途如下:
-
napi_value
类型是一个用于表示Javascript对象的指针 -
napi_env
类型用于存储Javascript虚拟机的上下文 -
napi_callback_info
类型传递调用时的上下文信息
我们定义的Add
方法如下:
static napi_value Add(napi_env env, napi_callback_info info)
{
size_t requireArgc = 2;
size_t argc = 2; // 参数数量
napi_value args[2] = {nullptr}; // 声明参数数组
napi_get_cb_info(env, info, &argc, args , nullptr, nullptr); // 从info中获取传入的参数并依次放入参数数组中
napi_valuetype valuetype0;
napi_typeof(env, args[0], &valuetype0); // 对第一个js参数类型进行判定
napi_valuetype valuetype1;
napi_typeof(env, args[1], &valuetype1); // 对第二个js参数类型进行判定
double value0;
napi_get_value_double(env, args[0], &value0); // 将第一个传入参数转化为double类型
double value1;
napi_get_value_double(env, args[1], &value1); // 将第二个传入参数转化为double类型
napi_value sum;
napi_create_double(env, value0 + value1, &sum); // 对传入参数进行计算
return sum;
}
在上面的实现中可以看到参数由JS类型转化为C/C++类型,求和后结果又由C/C++类型转化为JS类型。N-API提供Javascript与Native(C/C++)数据类型一对一相互转换功能,具体实现在 /foundation/ace/napi/native_engine/native_api.cppnative_api.cpp。
另外上面调用的函数都返回一个napi_status
类型的值,当其值为napi_ok
时代表函数执行成功。napi_status
是一个用于指示N-API中状态的枚举类型,其值可参考napi_status。
属性定义
方法实现后我们需要实现模块初始化函数。模块初始化函数需要满足下列函数定义:
typedef napi_value (*napi_addon_register_func)(napi_env env, napi_value exports);
在模块的初始化中,我们定义模块需要暴露的方法及属性。函数如下所示:
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);
return exports;
}
EXTERN_C_END
在我们的的初始化函数中,需要在模块的exports
对象中定义方法Add
的属性。在定义属性之前,我们需要创建一个napi_property_descriptor
类型的属性描述符,该类型的定义如下:
typedef struct {
const char* utf8name;
napi_value name;
napi_callback method;
napi_callback getter;
napi_callback setter;
napi_value value;
napi_property_attributes attributes;
void* data;
} napi_property_descriptor;
对于本文示例中需要使用的属性值描述如下所示,关于napi_property_descriptor
的更多描述可参考napi_property_descriptor。
-
utf8name
:UTF-8编码的字符序列 -
name
:由Javascript对象表示的字符串或者Symbolutf8name
以及name
只能二选一,其代表属性的名称。 -
method
:将该属性设置为一个返回值为Javascript对象的方法(回调函数) -
attributes
:属性的行为控制标志,示例中使用了默认的napi_default
值,更多描述可参考napi_property_attributes
我们这里主要设置了utf8name
以及method
属性。
在创建属性描述符后,便需要将其在模块的exports
对象中定义,使Javascript代码能够访问。对象属性的定义使用了napi_define_properties
函数,它可以快速的为一个对象定义指定数量的属性。该函数定义为:
napi_status napi_define_properties(napi_env env,
napi_value object,
size_t property_count,
const napi_property_descriptor *properties
);
-
env
:JSVM环境上下文 -
object
:需要定义属性的Javascript对象 -
property_count
:属性数量 -
properties
:属性描述符数组
同样,napi_define_properties
也返回了一个napi_status
类型的值表示函数调用成功与否。
模块注册
最后,我们只需注册一下模块就可以了。
static napi_module demoModule = {
.nm_version =1,
.nm_flags = 0,
.nm_filename = nullptr,
.nm_register_func = Init,
.nm_modname = "hello",
.nm_priv = ((void*)0),
.reserved = { 0 },
};
extern "C" __attribute__((constructor)) void RegisterHelloModule(void)
{
napi_module_register(&demoModule);
}
到此为止,我们的hello
模块便编写完成了。
结束语
对于开发Node.js Native扩展模块的小伙伴,官方提供了一个自动化工具,让底层开发者只关注底层业务逻辑即可:NAPI框架生成工具