鸿蒙OS Native API

预计阅读时间3 分钟 314 views

在 HarmonyOS 中,N-API (Native API) 为开发者提供了一种使用 C/C++ 编写高性能原生模块并与 JavaScript/TypeScript 代码交互的方式。N-API 提供的接口与 Node.js 的 N-API 相一致,方便开发者将已有的 Node.js 模块移植到 HarmonyOS 平台。

相比传统的 JS bridge 方式,N-API 可以提供更高的性能和更低的调用开销,并且更容易与其他原生库集成。

N-API 支持列表: 链接

开发流程

  1. 打开 DevEco Studio,点击 “File” -> “New” -> “Create Project”。
  2. 选择 “Native C++” 模板,创建一个新工程。
  3. 工程创建完成后,在 “entry/src/main” 目录下找到 “cpp” 文件夹,该文件夹用于存放 C/C++ 代码。

在 ArkTS/TS/JS 代码中,可以使用 import 语句引入 native 侧编译生成的 .so 动态库文件,例如 import hello from ‘libhello.so’,然后就可以通过 hello 对象调用 C/C++ 中定义的函数了。

基本功能

N-API 接口可以实现 ArkTS/TS/JS 和 C/C++ 之间的交互,这里以 HelloWorld 工程的两个例子说明:

  1. 提供一个名为 Add 的 native 方法,ArkTS 侧调用该方法并传入两个 number,native 方法将这两个 number 相加并返回到 ArkTS 侧。
  2. 提供一个名为 NativeCallArkTS 的 native 方法,ArkTS 侧调用该方法并传入一个 ArkTS function,native 方法中调用这个 ArkTS function,并将其结果返回 ArkTS 侧。

以此来介绍:

  1. ArkTS 侧如何调用到 C++ 侧方法。
  2. C++ 侧如何调用到 ArkTS 侧方法。

下面给出了工程中的:

  1. entry\src\main\cpp\hello.cpp: 包含 native 侧逻辑。
  2. entry\src\main\ets\pages\index.ets: 包含 ArkTS 侧逻辑。
  3. entry\src\main\cpp\types\libentry\index.d.ts: 包含 native 侧暴露给 ArkTS 侧接口的声明。

同时给出了注解,工程中其余部分均与 native 默认工程相同。

// entry\src\main\cpp\hello.cpp

// 引入 N-API 相关头文件。
#include "napi/native_api.h"

// 开发者提供的 native 方法,入参有且仅有如下两个,开发者不需进行变更。
// napi_env 为当前运行的上下文。
// napi_callback_info 记录了一些信息,包括从 ArkTS 侧传递过来参数等。
static napi_value Add(napi_env env, napi_callback_info info) {
    // 期望从 ArkTS 侧获取的参数的数量,napi_value 可理解为 ArkTS value 在 native 方法中的表现形式。
    size_t argc = 2;
    napi_value args[2] = {nullptr};

    // 从 info 中,拿到从 ArkTS 侧传递过来的参数,此处获取了两个 ArkTS 参数,即 arg[0] 和 arg[1]。
    napi_get_cb_info(env, info, &argc, args, nullptr, nullptr);

    // 将获取的 ArkTS 参数转换为 native 信息,此处 ArkTS 侧传入了两个 number,这里将其转换为 native 侧可以操作的 double 类型。
    double value0;
    napi_get_value_double(env, args[0], &value0);

    double value1;
    napi_get_value_double(env, args[1], &value1);

    // native 侧的业务逻辑,这里简单以两数相加为例。
    double nativeSum = value0 + value1;

    // 此处将 native 侧业务逻辑处理结果转换为 ArkTS 值,并返回给 ArkTS。
    napi_value sum;
    napi_create_double(env, nativeSum, &sum);
    return sum;
}

static napi_value NativeCallArkTS(napi_env env, napi_callback_info info) {
    // 期望从 ArkTS 侧获取的参数的数量,napi_value 可理解为 ArkTS value 在 native 方法中的表现形式。
    size_t argc = 1;
    napi_value args[1] = {nullptr};

    // 从 info 中,拿到从 ArkTS 侧传递过来的参数,此处获取了一个 ArkTS 参数,即 arg[0]。
    napi_get_cb_info(env, info, &argc, args, nullptr, nullptr);

    // 创建一个 ArkTS number 作为 ArkTS function 的入参。
    napi_value argv = nullptr;
    napi_create_int32(env, 10, &argv);

    napi_value result = nullptr;
    // native 方法中调用 ArkTS function,其返回值保存到 result 中并返到 ArkTS 侧。
    napi_call_function(env, nullptr, args[0], 1, &argv, &result);

    return result;
}

EXTERN_C_START

// Init 将在 exports 上挂上 Add/NativeCallArkTS 这些 native 方法,此处的 exports 就是开发者 import 之后获取到的 ArkTS 对象。
static napi_value Init(napi_env env, napi_value exports) {
    // 函数描述结构体,以 Add 为例,第三个参数 "Add" 为上述的 native 方法,
    // 第一个参数 "add" 为 ArkTS 侧对应方法的名称。
    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

// 准备模块加载相关信息,将上述 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);
}
// entry\src\main\ets\pages\index.ets

import hilog from '@ohos.hilog';

// 通过 import 的方式,引入 native 能力。
import entry from 'libentry.so' 

@Entry
@Component
struct Index {
  build() {
    Row() {
      Column() {
        // 第一个按钮,调用 add 方法,对应到 native 侧的 Add 方法,进行两数相加。
        Button('ArkTS call C++')
          .fontSize(50)
          .fontWeight(FontWeight.Bold)
          .onClick(() => {
            hilog.isLoggable(0x0000, 'testTag', hilog.LogLevel.INFO);
            hilog.info(0x0000, 'testTag', 'Test NAPI 2 + 3 = %{public}d', entry.add(2, 3));
          })
        // 第二个按钮,调用 nativeCallArkTS 方法,对应到 native 的 NativeCallArkTS,在 native 中执行 ArkTS function。
        Button('C++ call ArkTS')
          .fontSize(50)
          .fontWeight(FontWeight.Bold)
          .onClick(() => {
            hilog.isLoggable(0x0000, 'testTag', hilog.LogLevel.INFO);
            let ret = entry.nativeCallArkTS((value) => {
              return value * 2;
            });
            hilog.info(0x0000, 'testTag', 'Test NAPI nativeCallArkTS ret = %{public}d', ret);
          })
      }
      .width('100%')
    }
    .height('100%')
  }
}
// entry\src\main\cpp\types\libentry\index.d.ts

// native 侧暴露给 ArkTS 侧接口的声明。
export const add: (a: number, b: number) => number;
export const nativeCallArkTS: (a: object) => number;

开发建议

注册建议

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

so 命名规则

so 命名必须符合以下规则:

  • 每个模块对应一个 so
  • 如模块名为 hello,则 so 的名字为 libhello.sonapi_module 中 nm_modname 字段应为 hello,大小写与模块名保持一致,应用使用时写作:import hello from 'libhello.so'

JS 对象线程限制

ArkCompiler 会对 JS 对象线程进行保护,使用不当会引起应用 crash,因此需要遵循如下原则:

  • N-API 接口只能在 JS 线程使用。
  • env 与线程绑定,不能跨线程使用。native 侧 JS 对象只能在创建时的线程使用,即与线程所持有的 env 绑定。

头文件引入限制

在引入头文件时,需引入 “napi/native_api.h“,否则会出现 N-API 接口无法找到的编译报错。

分享此文档

鸿蒙OS Native API

或复制链接

本页目录