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线程绑定只能在创建时的线程使用。
相关推荐
别说我什么都不会8 小时前
ohos.net.http请求HttpResponse header中set-ccokie值被转成array类型
网络协议·harmonyos
码是生活9 小时前
鸿蒙开发排坑:解决 resourceManager.getRawFileContent() 获取文件内容为空问题
前端·harmonyos
鸿蒙场景化示例代码技术工程师9 小时前
基于Canvas实现选座功能鸿蒙示例代码
华为·harmonyos
小脑斧爱吃鱼鱼10 小时前
鸿蒙项目笔记(1)
笔记·学习·harmonyos
鸿蒙布道师11 小时前
鸿蒙NEXT开发对象工具类(TS)
android·ios·华为·harmonyos·arkts·鸿蒙系统·huawei
zhang10620911 小时前
HarmonyOS 基础组件和基础布局的介绍
harmonyos·基础组件·基础布局
马剑威(威哥爱编程)11 小时前
在HarmonyOS NEXT 开发中,如何指定一个号码,拉起系统拨号页面
华为·harmonyos·arkts
GeniuswongAir12 小时前
Flutter极速接入IM聊天功能并支持鸿蒙
flutter·华为·harmonyos
90后的晨仔16 小时前
鸿蒙ArkUI框架中的状态管理
harmonyos
别说我什么都不会1 天前
OpenHarmony 5.0(API 12)关系型数据库relationalStore 新增本地数据变化监听接口介绍
api·harmonyos