鸿蒙中级课程笔记8—Native适配开发

鸿蒙中Node-API与NAPI区别:Node-API 是标准,NAPI(Native API) 是具体实现,但在鸿蒙(HarmonyOS)语境下,它们通常指向同一个东西。

Node-API(原名 N-API) 是 Node.js 提供的一个用于构建本地插件(Native Addons) 的 API 层。它允许开发者用 C、C++ 或 Rust 等系统编程语言编写可直接被 Node.js 调用的模块。

一、HarmonyOS Node-API简介

场景介绍

HarmonyOS Node-API是基于Node.js 18.x LTS的Node-API规范扩展开发的机制,为开发者提供了ArkTS/JS与C/C++模块之间的交互能力。它提供了一组稳定的、跨平台的API,可以在不同的操作系统上使用。

本文中如无特别说明,后续均使用Node-API指代HarmonyOS Node-API能力。

说明

HarmonyOS Node-API与Node.js 18.x LTS的Node-API规范的接口异同点,详见Node-API参考文档

一般情况下HarmonyOS应用开发使用ArkTS/JS语言,但部分场景由于性能、效率等要求,比如游戏、物理模拟等,需要依赖使用现有的C/C++库。Node-API规范封装了I/O、CPU密集型、OS底层等能力并对外暴露C接口,使用C/C++模块的注册机制,向ArkTS/JS对象上挂载属性和方法的方式来实现ArkTS/JS和C/C++的交互。主要场景如下:

  • 系统可以将框架层丰富的模块功能通过Node-API的模块注册机制对外暴露ArkTS/JS的接口,将C/C++的能力开放给应用的ArkTS/JS层。

  • 应用开发者也可以选择将一些对性能、底层系统调用有要求的核心功能用C/C++封装实现,再通过ArkTS/JS接口使用,提高应用本身的执行效率。

Node-API的组成架构

图1 Node-API的组成架构

  • Native Module:开发者使用Node-API开发的模块,用于在ArkTS侧导入使用。

  • Node-API:实现ArkTS与C/C++交互的逻辑。

  • ModuleManager:Native模块管理,包括加载、查找等。

  • ScopeManager:管理napi_value的生命周期。

  • ReferenceManager:管理napi_ref的生命周期。

  • NativeEngine:ArkTS引擎抽象层,统一ArkTS引擎在Node-API层的接口行为。

  • ArkCompiler ArkTS Runtime:ArkTS运行时。

Node-API的关键交互流程

图2 Node-API的关键交互流程

ArkTS和C++之间的交互流程,主要分为以下两步:

  1. 初始化阶段 :当ArkTS侧在import一个Native模块时,ArkTS引擎会调用ModuleManager加载模块对应的so及其依赖。首次加载时会触发模块的注册,将模块定义的方法属性挂载到exports对象上并返回该对象。后续再次import时从缓存中查找Native模块信息并返回,不会进行多次加载。

  2. 调用阶段:当ArkTS侧通过上述import返回的对象调用方法时,ArkTS引擎会找到并调用对应的C/C++方法。

上图中ArkTS引擎和ModuleManager都是由系统管理的,开发者仅需要关注ArkTS模块与Native模块。ArkTS与Native模块间的调用时序图如下所示

二、常用的Node-API数据类型和接口

常用数据类型

napi_value

napi_value是一个C的结构体指针,表示一个ArkTS/JS对象的引用。在Native侧代码中,用于承载ArkTS侧任意类型的数据,开发者不需要感知不同的ArkTS侧数据类型,统一都用napi_value表示。例如ArkTS中的number、string等各种数据类型都可以用napi_value表示。退出native方法后,napi_value将失效。

napi_env

一般作为Native侧函数的参数来使用,在ArkTS侧调用Native侧函数时,默认会传递napi_env和napi_callback_info这两个参数信息。注意:native提供给ArkTS的函数必须有且只有这两个参数。

napi_env用于表示Node-API执行时的上下文,可以传递给函数中的Node-APl接口。napi_env与ArkTS线程绑定,ArkTS线程退出后, napi_env将失效。

  • napi_env与ArkTS/JS线程的上下文环境绑定,每一个napi_env都持有独立的运行时上下文环境,当ArkTS/JS线程退出之后,相应的napi_env将不再有效。

  • 禁止缓存napi_env,禁止在不同线程间传递napi_env。

napi_callback_info

一般作为Native侧函数的参数来使用,在ArkTS侧调用Native侧函数时,默认会传递napi_env和napi_callback_info这两个参数信息。注意:native提供给ArkTS的函数必须有且只有这两个参数。

napi_callback_info保存了ArkTS侧的传入的所有参数,用于传递给napi_get_cb_info()函数获取ArkTS侧入参信息。

napi_status

是一个枚举数据类型,表示Node-API接口返回的状态信息,主要用于判断Node-APl接口调用是否有异常。每当调用一个Node-API函数,都会返回该值,表示操作成功与否的相关信息。

每当调用一个Node-API函数,都会返回该值,表示操作成功与否的相关信息。举例如下:

TypeScript 复制代码
typedef enum {
    napi_ok,//默认从0开始。若返回结果非0,则调用异常。
    napi_invalid_arg,
    napi_object_expected,
    napi_string_expected,
    napi_name_expected,
    napi_function_expected,
    napi_number_expected,
    napi_boolean_expected,
    napi_array_expected,
    napi_generic_failure,
    napi_pending_exception,
    napi_cancelled,
    napi_escape_called_twice,
    napi_handle_scope_mismatch,
    napi_callback_scope_mismatch,
    napi_queue_full,
    napi_closing,
    napi_bigint_expected,
    napi_date_expected,
    napi_arraybuffer_expected,
    napi_detachable_arraybuffer_expected,
    napi_would_deadlock, /* unused */
    napi_no_external_buffers_allowed,
    napi_cannot_run_js
} napi_status;

常用NAPI接口

napi_get_cb_info

napi_get_cb_info:从给定的napi_callback_info对象中获取ArkTS侧传入参数的详细信息,如参数和this对象。

转换napi_value类型为C/C++类型相关接口

HarmonyOs Node-APl提供了一系列的接口用于将napi_value类型参数转化为C/C++标准数据类型。相关类型接口较多下面只列举部分基础数据类型相关接口。

转换C/C++类型为napi_value类型相关接口

除了将napi_value类型参数转化为C/C++标准数据类型相关接口外,HarmonyOS Node-APl还提供了一些将C/C++标准数据类型参数转换为napi_value类型参数的接口。相关类型接口较多,下面只列举部分基础数据类型相关接口。

三、Node-API开发流程

使用Node-API实现跨语言交互,首先需要按照Node-API的机制实现模块的注册和加载等相关动作。

  • ArkTS/JS侧:实现C++方法的调用,通过import所需的so库后,可以调用C++方法。

  • Native侧:.cpp文件,实现模块的注册。需要提供注册lib库的名称,并在注册回调方法中定义接口的映射关系,即Native方法及对应的JS/ArkTS接口名称等。

此处以在ArkTS/JS侧调用callNative()接口、在Native侧实现加法操作的CallNative()接口,从而实现跨语言交互为例,呈现使用Node-API进行跨语言交互的流程。

创建Native C++工程

1、在DevEco Studio中New > Create Project ,选择Native C++模板,点击Next ,选择API版本,设置好工程名称,点击Finish,创建得到新工程。

创建工程后工程结构可以分两部分,cpp目录部分和ets目录部分,工程结构如下图,详细介绍可见C++工程目录结构

上图进一步说明:

index.d.ts中定义了C++侧需要暴露在ArkTS侧的接口,ArkTS侧后续使用就是调用这里面定义的接口;

CMakeLists.txt中定义了编译的模块名、依赖的文件,包括最终生成是静态库还是动态库;

总的实现流程如下图

Native侧方法的实现

设置模块 注册信息

ArkTS侧import native模块时,会加载其对应的so。加载so时,首先会调用napi_module_register方法,将模块注册到系统中,并调用模块初始化函数。

napi_module_register方法入参是napi_module对象,它有两个关键属性:一个是.nm_register_func,定义模块初始化函数;另一个是.nm_modname,定义模块的名称,也就是ArkTS侧引入的so库的名称,模块系统会根据此名称来区分不同的so。

TypeScript 复制代码
// entry/src/main/cpp/napi_init.cpp

// 准备模块加载相关信息,将上述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 RegisterDemoModule() {
    napi_module_register(&demoModule);
}

注:以上代码无须复制,创建Native C++工程以后在napi_init.cpp代码中已配置好。

官网视频课程对代码的说明如下:

模块初始化:Native接口映射

实现ArkTS接口与C++接口的绑定和映射。

TypeScript 复制代码
// entry/src/main/cpp/napi_init.cpp
//定义Native侧函数,用于后续实现业务功能
static napi_value CallNative(napi_env env, napi_callback_info info)
{
    //...待实现
}

static napi_value NativeCallArkTS(napi_env env, napi_callback_info info)
{
    //...待实现
}

EXTERN_C_START
// 模块初始化
static napi_value Init(napi_env env, napi_value exports) {
    // ArkTS接口与C++接口的绑定和映射
    napi_property_descriptor desc[] = {
        // 注:Init函数结构在Native C++工程创建时自动实现,开发者仅需实现以下两行代码。
        //下面两行代码将Native侧的函数映射到ArkTS侧方法上。其中第一个参数是在ArkTS侧暴露的接回名,第三个参数是native侧的方法名
        {"callNative", nullptr, CallNative, nullptr, nullptr, nullptr, napi_default, nullptr},
        {"nativeCallArkTS", nullptr, NativeCallArkTS, nullptr, nullptr, nullptr, napi_default, nullptr}
    };
    // 在exports对象上挂载CallNative/NativeCallArkTS两个Native方法
    napi_define_properties(env, exports, sizeof(desc) / sizeof(desc[0]), desc);
    return exports;
}
EXTERN_C_END

定义ArkTS侧接口:在index.d.ts文件中,描述提供TS侧的方法。

TypeScript 复制代码
// entry/src/main/cpp/types/libentry/index.d.ts
export const callNative: (a: number, b: number) => number;
export const nativeCallArkTS: (cb: (a: number) => number) => number;

定义ArkTS侧接口:在oh-package.json5文件中将index.d.ts与cpp文件关联起来。

TypeScript 复制代码
// entry/src/main/cpp/types/libentry/oh-package.json5
{
  "name": "libentry.so",
  "types": "./index.d.ts",
  "version": "",
  "description": "Please describe the basic information."
}

模块构建配置:在CMakeLists.txt文件中配置CMake构建脚本

主要关注以下几个配置:

TypeScript 复制代码
# entry/src/main/cpp/CMakeLists.txt
cmake_minimum_required(VERSION 3.4.1)
project(MyApplication2)

set(NATIVERENDER_ROOT_PATH ${CMAKE_CURRENT_SOURCE_DIR})

//设置头文件目录,一般使用默认配置即可。默认配置整个cpp目录和cpp下include目录
include_directories(${NATIVERENDER_ROOT_PATH}
                    ${NATIVERENDER_ROOT_PATH}/include)

# 配置最终编译生成的产物名称、产物类型、以及编译所依赖的文件。
# 第一个参数是编译的模块名称,这里写了entry,最后编译出来的产物就是libentry.so;
# 第二个参数设置编译生成的产物类型,SHARED表示编译为动态库;
# 第三个参数配置参与编译的所有cpp文件,由于上述步骤中的业务功能、接回映射以及模块注册信息都在napi_init.cpp文件中,没有新建其他cpp文件,所以这里只配置了napi_init.cpp文件。。
add_library(entry SHARED napi_init.cpp)
# 配置编译当前模块需要依赖(链接)的库
target_link_libraries(entry PUBLIC libace_napi.z.so)

如果希望代码工程最终可以在模拟器上运行,还需要在模块级的build-profle,json5文件中通过配置项abilFilters,配置模拟器对应的架构信息x86_64

实现Native侧的CallNative以及NativeCallArkTS接口

TypeScript 复制代码
// entry/src/main/cpp/napi_init.cpp
static napi_value CallNative(napi_env env, napi_callback_info info)
{
    size_t argc = 2;
    // 声明参数数组
    napi_value args[2] = {nullptr};

    // 获取传入的参数并依次放入参数数组中
    napi_get_cb_info(env, info, &argc, args, nullptr, nullptr);

    // 依次获取参数
    double value0;
    napi_get_value_double(env, args[0], &value0);
    double value1;
    napi_get_value_double(env, args[1], &value1);

    // 返回两数相加的结果
    napi_value sum;
    napi_create_double(env, value0 + value1, &sum);
    return sum;
}

static napi_value NativeCallArkTS(napi_env env, napi_callback_info info)
{
    size_t argc = 1;
    // 声明参数数组
    napi_value args[1] = {nullptr};

    // 获取传入的参数并依次放入参数数组中
    napi_get_cb_info(env, info, &argc, args, nullptr, nullptr);

    // 创建一个int,作为ArkTS的入参
    napi_value argv = nullptr;
    napi_create_int32(env, 2, &argv);

    // 调用传入的callback,并将其结果返回
    napi_value result = nullptr;
    napi_call_function(env, nullptr, args[0], 1, &argv, &result);
    return result;
}

ArkTS侧调用C/C++方法实现

ArkTS侧通过import引入Native侧包含处理逻辑的so来使用C/C++的方法。

TypeScript 复制代码
// entry/src/main/ets/pages/Index.ets
// 通过import的方式,引入Native能力。
import nativeModule from 'libentry.so'

@Entry
@Component
struct Index {
  @State message: string = 'Test Node-API callNative result: ';
  @State message2: string = 'Test Node-API nativeCallArkTS result: ';
  build() {
    Row() {
      Column() {
        // 第一个按钮,调用callNative方法,对应到Native侧的CallNative方法,进行两数相加。
        Text(this.message)
          .fontSize(50)
          .fontWeight(FontWeight.Bold)
          .onClick(() => {
            this.message += nativeModule.callNative(2, 3);
            })
        // 第二个按钮,调用nativeCallArkTS方法,对应到Native的NativeCallArkTS,在Native调用ArkTS function。
        Text(this.message2)
          .fontSize(50)
          .fontWeight(FontWeight.Bold)
          .onClick(() => {
            this.message2 += nativeModule.nativeCallArkTS((a: number)=> {
                return a * 2;
            });
          })
      }
      .width('100%')
    }
    .height('100%')
  }
}

Node-API的约束限制

SO命名规则

导入使用的模块名和注册时的模块名大小写保持一致,如模块名为entry,则so的名字为libentry.so,napi_module中nm_modname字段应为entry,ArkTS侧使用时写作:import xxx from 'libentry.so'。

注册建议

  • nm_register_func对应的函数(如上述Init函数)需要加上static,防止与其他so里的符号冲突。

  • 模块注册的入口,即使用__attribute__((constructor))修饰的函数的函数名(如上述RegisterDemoModule函数)需要确保不与其它模块重复。

多线程限制

每个引擎实例对应一个ArkTS线程,实例上的对象不能跨线程操作,否则会引起应用crash。使用时需要遵循如下原则:

  • Node-API接口只能在ArkTS线程使用。
  • Native接口入参env与特定ArkTS线程绑定,只能在创建该env的线程使用。
  • 使用Node-API接口创建的数据需在env完全销毁前进行释放,避免内存泄漏。此外,在napi_env销毁后访问/使用这些数据,可能会导致进程崩溃。

代码调试设备选择

建议开发者优先使用真机进行代码调试,若无真机或者真机无权限则可使用模拟器进行调试,模拟器调试中遇到的问题详见bm工具

开发者不要使用预览器进行功能调试,预览器的主要功能是调试界面组件,若用于功能调试可能会出现如下报错:

  • TypeError: undefined is not callable

部分常见错误用法已增加维测手段覆盖,详见使用Node-API接口产生的异常日志/崩溃分析

四、25.9鸿蒙化中C++库使用整理

简介

鸿蒙除了支持使用ArkTS开发外,开发者还可以通过Node-API实现ArkTS与C/C++(Native)的跨语言交互能力。参考ArkTS跨语言交互官方指南、NDK开发(包括NDK开发导读创建NDK工程构建NDK工程代码开发调试和性能分析硬件兼容性)。备注:NDK 的全称是 Native Development Kit(原生开发工具包)。

HarmonyOS的Node-API是基于Node.js社区版本的扩展实现,但与原生Node-API并不完全兼容。

开发者可参考使用Node-API进行跨语言开发流程,基于Node-API支持的数据类型接口进行Native能力的开发和封装,并通过在ArkTS侧导入Native模块的方式实现跨语言调用。

Node-API扩展能力接口提供了增强功能,支持更灵活的ArkTS交互和自定义对象创建。开发者可结合Node-API的扩展能力进行功能扩展,并参考Node-API开发规范Node-API常见问题进行跨语言功能开发。

下面整理两种调用C++的方式,流程如下:

一、生成har包供外部module调用

参考使用Node-API实现跨语言交互开发流程

1、创建Native C++类型module

2、CMakeLists.txt配置

默认的CMakeLists.txt脚本中添加了编译所需的源代码、头文件以及三方库,开发者可根据实际工程添加自定义编译参数、函数声明、简单的逻辑控制等。核心代码说明如下:

add_library(entry SHARED napi_init.cpp) # 添加名为entry的库

target_link_libraries(entry PUBLIC libace_napi.z.so) # 构建此可执行文件需要链接的库

参考:native工程实现流程举例native工程举例Native工程举例

3、build-profile.json5

模块级build-profile.json5文件中externalNativeOptions参数配置NDK工程C/C++文件编译参数,可以通过path指定CMake脚本路径、arguments配置CMake参数、cppFlags配置C++编译器参数、abiFilters配置编译架构等。举例如下:

TypeScript 复制代码
"externalNativeOptions": {

  "path": "./src/main/cpp/test/CMakeLists.txt",

  "arguments": "",

  "cppFlags": "",

  "abiFilters": [

    "arm64-v8a",

    "x86_64"

  ]

}

4、oh-package.json5

文件配置依赖将index.d.ts与cpp文件关联起来。

5、napi_init.cpp

1)、设置模块信息;2)、模块初始化;注意这个文件可以自定义为想要的name。

本文件的设置步骤如下(参考:native工程实现流程举例):

1)、本文件中对外方法的大致流程:调用napi_XXX方法从ArkTS获取参数,然后调用C++方法,然后将结果通过napi_XXX返回给ArkTS。比如napi_get_cb_info 用于在Native(C/C++)代码中,获取从ArkTS(或JavaScript)侧调用时传入的参数、this 对象等信息

2)、通过napi_define_properties在exports对象上挂载 上一步 中实现的Native方法。如napi_define_properties(env, exports, sizeof(desc)/sizeof(desc[0]), desc);

3)、通过napi_module_register方法将导出的模块完成注册

6、index.d.ts

文件中声明提供给TS侧的方法。

  1. ArkTs调用C/C++模块的方法

在oh-package.json5文件中通过dependencies配置依赖库

在ets文件从so中导入C/C++的方法,如import { getRandomTestData, yyy } from 'libxxx.so';

二、将代码植入其他现有module中使用

Native C++类型module中,TS可以调用native代码,因此其他module中也可以直接调用。步骤如下:

1、按照"生成har包供外部module调用"模块步骤创建Native C++类型module,name=test。C++代码创建了test目录存放,原因:为了支持多个so模块,如果是只有一个可以直接放到src/main/cpp下,无需分子目录。

2、在现有xxx模块的src/main目录下新建一个cpp的目录,然后将步骤1中native module中test目录下的cpp目录拷贝到src/main/cpp/目录下,并改名为test。

3、从"生成har包供外部module调用"中的第2步开始逐步检查并配置

相关推荐
一起养小猫3 小时前
Flutter for OpenHarmony 实战:天气预报应用UI设计与主题切换
jvm·数据库·spring·flutter·ui·harmonyos
AI视觉网奇3 小时前
ue 模拟说话
笔记·学习·ue5
孞㐑¥3 小时前
算法—链表
开发语言·c++·经验分享·笔记·算法
BlackWolfSky3 小时前
鸿蒙中级课程笔记7—给应用添加通知
笔记·华为·harmonyos
xqqxqxxq3 小时前
结构体(Java 类)实战题解笔记(持续更新)
java·笔记·算法
Gain_chance3 小时前
27-学习笔记尚硅谷数仓搭建-数据仓库DWD层介绍及其事务表(行为)相关概念
大数据·数据仓库·笔记·学习
zyxqyy&∞3 小时前
HCIP--MPLS-VPN--1
网络·华为·hcip
子春一4 小时前
Flutter for OpenHarmony:构建一个 Flutter 速记本应用,深入解析可编辑列表、滑动删除与实时笔记管理
笔记·flutter
摘星编程4 小时前
React Native鸿蒙版:KeyboardInteractive交互监听
react native·交互·harmonyos