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

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

  • ArkTS/JS侧:实现C++方法的调用。代码比较简单,import一个对应的so库后,即可调用C++方法。
  • Native侧:.cpp文件,实现模块的注册。需要提供注册lib库的名称,并在注册回调方法中定义接口的映射关系,即Native方法及对应的JS/ArkTS接口名称等。
    此处以在ArkTS/JS侧实现add()接口、在Native侧实现Add()接口,从而实现跨语言交互为例,呈现使用Node-API进行跨语言交互的流程。

一、创建Native C++工程

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

创建工程后工程结构可以分两部分,cpp部分和ets部分,工程结构具体介绍可见C++工程目录结构。

1.1 C++工程目录结构(Stage模型)

C++ Stage模型支持API Version 10以上版本,支持使用ArkTS+C++进行开发,其工程目录结构如下图所示。

  • entry:应用模块,编译构建生成一个HAP。
    • src > main > cpp > types:用于存放C++的API接口描述文件
    • src > main > cpp > types > libentry > index.d.ts:描述C++ API接口行为,如接口名、入参、返 参数等。
    • src > main > cpp > types > libentry> oh-package.json5:配置.so三方包声明文件的入口及包
    • src > main > cpp > CMakeLists.txt:CMake配置文件,提供CMake构建脚本。
    • src > main > cpp > napi_init.cpp:定义C++ API接口的文件。
    • src > main > ets:用于存放ArkTS源码。
    • src > main > resources:用于存放应用所用到的资源文件,如图形、多媒体、字符串、布局文件等。 关于资源文件的详细说明请参考资源分类与访问
资源目录 资源文件说明
base>element 包括字符串、整型数、颜色、样式等资源的json文件。每个资源均由json格式进行定义,例如: boolean.json:布尔型 color.json:颜色 float.json:浮点型 intarray.json:整型数组 integer.json:整型 pattern.json:样式 plural.json:复数形式 =strarray.json:字符串数组 string.json:字符串值。
base>media 多媒体文件,如图形、视频、音频等文件,支持的文件格式包括:.png、.gif、.mp3、.mp4等。
rawfile 用于存储任意格式的原始资源文件。rawfile不会根据设备的状态去匹配不同的资源,需要指定文件路径和文件名进行引用。
    • src > main > module.json5:Stage模块配置文件,主要包含HAP的配置信息、应用在具体设备上的配置信息以及应用的全局配置信息。具体请参考module.json5配置文件。
    • build-profile.json5:当前的模块信息、编译信息配置项,包括buildOption、targets配置等。
    • hvigorfile.ts:模块级编译构建任务脚本。
  • build-profile.json5:应用级配置信息,包括签名、产品配置等。
  • hvigorfile.ts:应用级编译构建任务脚本。

二、Native侧方法的实现

2.1 设置模块注册信息

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

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

c 复制代码
// 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 = nullptr,
    .reserved = {0},
};

// 加载so时,该函数会自动被调用,将上述demoModule模块注册到系统中。
extern "C" __attribute__((constructor)) void RegisterDemoModule() { 
    napi_module_register(&demoModule);
 }

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

2.2 模块初始化

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

c 复制代码
// entry/src/main/cpp/napi_init.cpp
EXTERN_C_START
// 模块初始化
static napi_value Init(napi_env env, napi_value exports) {
    // ArkTS接口与C++接口的绑定和映射
    napi_property_descriptor desc[] = {
        // 注:仅需复制以下两行代码,Init在完成创建Native C++工程以后在napi_init.cpp中已配置好。
        {"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

2.3 在index.d.ts文件中,提供JS侧的接口方法

c 复制代码
// 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;

2.4 在oh-package.json5文件中将index.d.ts与cpp文件关联起来

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

2.5 在CMakeLists.txt文件中配置CMake打包参数

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

set(NATIVERENDER_ROOT_PATH ${CMAKE_CURRENT_SOURCE_DIR})

include_directories(${NATIVERENDER_ROOT_PATH}
                    ${NATIVERENDER_ROOT_PATH}/include)

# 添加名为entry的库
add_library(entry SHARED napi_init.cpp)
# 构建此可执行文件需要链接的库
target_link_libraries(entry PUBLIC libace_napi.z.so)

2.6 实现Native侧的CallNative以及NativeCallArkTS接口。具体代码如下:

c 复制代码
// 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++的方法。

c 复制代码
// 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() {
        // 第一个按钮,调用add方法,对应到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%')
  }
}

四、完整示例

4.1 效果图

4.2 代码

Index.d.ts

// entry/src/main/cpp/types/libentry/index.d.ts

c 复制代码
export const add: (a: number, b: number) => number;
export const callNative: (a: number, b: number) => number;
export const nativeCallArkTS: (cb: (a: number) => number) => number;

napi_init.cpp

// entry/src/main/cpp/napi_init.cpp

c 复制代码
#include "napi/native_api.h"

static napi_value Add(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);

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

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

    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 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++接口的绑定和映射
EXTERN_C_START
static napi_value Init(napi_env env, napi_value exports)
{// 模块初始化
    napi_property_descriptor desc[] = {// ArkTS接口与C++接口的绑定和映射
        { "add", nullptr, Add, nullptr, nullptr, nullptr, napi_default, nullptr },
         {"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

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 },
};

extern "C" __attribute__((constructor)) void RegisterEntryModule(void)
{
    napi_module_register(&demoModule);
}

oh-package.json5

// entry/src/main/cpp/types/libentry/oh-package.json5

c 复制代码
{
  "name": "libentry.so",
  "types": "./Index.d.ts",
  "version": "1.0.0",
  "description": "Please describe the basic information."
}

CMakeLists.txt

c 复制代码
# the minimum version of CMake.
cmake_minimum_required(VERSION 3.5.0)
project(TestNativeC)

set(NATIVERENDER_ROOT_PATH ${CMAKE_CURRENT_SOURCE_DIR})

if(DEFINED PACKAGE_FIND_FILE)
    include(${PACKAGE_FIND_FILE})
endif()

include_directories(${NATIVERENDER_ROOT_PATH}
                    ${NATIVERENDER_ROOT_PATH}/include)
# 添加名为entry的库
add_library(entry SHARED napi_init.cpp)
# 构建此可执行文件需要链接的库
target_link_libraries(entry PUBLIC libace_napi.z.so)

Index.ets

c 复制代码
import { hilog } from '@kit.PerformanceAnalysisKit';
import nativeModule from 'libentry.so';

@Entry
@Component
struct Index {
  @State message1: string = '使用Node-API实现跨语言交互开发流程';
  @State message2: string = 'Test Node-API callNative result: ';
  @State message3: string = 'Test Node-API nativeCallArkTS result: ';

  build() {
    Row() {
      Column({space: 10}) {
        Text(this.message1)
          .fontSize(20)
          .fontWeight(FontWeight.Bold)
          .onClick(() => {
            hilog.info(0x0000, 'testTag', 'Test NAPI 2 + 3 = %{public}d', nativeModule.add(2, 3));
          })

        // 第一个按钮,调用add方法,对应到Native侧的CallNative方法,进行两数相加。
        Text(this.message2)
          .fontSize(20)
          .fontWeight(FontWeight.Bold)
          .onClick(() => {
            this.message2 += nativeModule.callNative(2, 3);
          })
        // 第二个按钮,调用nativeCallArkTS方法,对应到Native的NativeCallArkTS,在Native调用ArkTS function。
        Text(this.message3)
          .fontSize(20)
          .fontWeight(FontWeight.Bold)
          .onClick(() => {
            this.message3 += nativeModule.nativeCallArkTS((a: number)=> {
              return a * 2;
            });
          })
      }
      .width('100%')
    }
    .height('100%')
  }
}

4.3 注意事项

安装HAP时提示"code:9568347 error: install parse native so failed"错误,或者运行时候提示"TypeError:Cannot read property xxx of undefined"错误
该问题是可能是由于设备支持的Abi类型与C++工程中配置的Abi类型不匹配导致,请通过如下步骤进行解决。

从4.1.3.400版本开始,abiFilters字段缺省配置为"arm64-v8a",将默认只编译arm64-v8a一种Abi,若设备不支持64位Abi,构建出的包将无法运行在设备上,请根据设备支持的Abi,在build-profile.json5中的buildOption/externalNativeOptions内手动配置abiFilters的值。

c 复制代码
// HarmonyOS工程
"buildOption": {
  "externalNativeOptions": {
    "abiFilters": ["arm64-v8a", "x86_64"]
  },
}
// OpenHarmony工程
"buildOption": {
  "externalNativeOptions": {
    "abiFilters": ["arm64-v8a", "x86_64", "armeabi-v7a"]
  },
}

4.4 通用场景

  1. 将设备与DevEco Studio进行连接。
  2. 打开命令行终端,并进入hdc目录:DevEco Studio安装目录/sdk/default/openharmony/toolchains。
  3. 执行如下命令,查询设备支持的Abi列表,返回结果为default/armeabi/arm64-v8a/x86/x86_64中的一个或多个Abi类型。
c 复制代码
hdc shell
param get const.product.cpu.abilist

五、Node-API的约束限制

5.1 SO命名规则

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

5.2 注册建议

  • nm_register_func对应的函数(如上述Init函数)需要加上static,防止与其他so里的符号冲突。
  • 模块注册的入口,即使用__attribute__((constructor))修饰的函数的函数名(如上述RegisterDemoModule函数)需要确保不与其它模块重复。

5.3 多线程限制

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

  • Node-API接口只能在JS线程使用。
  • Native接口入参env与特定JS线程绑定只能在创建时的线程使用。
相关推荐
SuperHeroWu72 小时前
【HarmonyOS 5】鸿蒙检测系统完整性
华为·harmonyos·模拟器·系统完整性·越狱设备
京东云开发者2 小时前
Taro on Harmony :助力业务高效开发纯血鸿蒙应用
harmonyos
前端付豪3 小时前
2、ArkTS 是什么?鸿蒙最强开发语言语法全讲解(附实操案例)
前端·后端·harmonyos
zhujiaming3 小时前
鸿蒙端应用适配使用开源flutter值得注意的一些问题
前端·flutter·harmonyos
前端付豪3 小时前
8、鸿蒙动画开发实战:做一个会跳舞的按钮!(附动效示意图)
前端·后端·harmonyos
前端付豪3 小时前
3、构建你的第一个鸿蒙组件化 UI 页面:实现可复用的卡片组件(附实战代码)
前端·后端·harmonyos
前端付豪3 小时前
7、打造鸿蒙原生日历组件:自定义 UI + 数据交互(附实操案例与效果图)
前端·后端·harmonyos
北海zx5 小时前
HarmonyNext:如何在鸿蒙上录屏后进行音视频编码
harmonyos
别说我什么都不会5 小时前
【仓颉三方库】音视频开发—— ijkplayer-ffi
harmonyos
王二蛋与他的张大花8 小时前
HarmonyOS运动开发:如何监听用户运动步数数据
harmonyos