Harmony NDK 开发

1 阅读6分钟

NDK(Native Development Kit) 是鸿蒙提供的原生开发工具集,允许开发者使用 C/C++ 编写底层代码,通过跨语言调用与 ArkTS 层交互。适用于性能敏感,复用C/C++库,底层硬件操作等场景。

创建 NDK 工程

可以直接使用 DevEco Studio 模板构建 NDK 工程

image.png

创建成功后,目录如下所示:

image.png

CMakeLists.txt 是鸿蒙原生 C++ 模块的构建配置文件,CMake 工具会根据它编译生成动态库(.so文件),供鸿蒙 ArkTS 层调用,我已经逐行解释含义了,不懂得直接看注释即可。

# 声明CMake所需的最低版本
cmake_minimum_required(VERSION 3.5.0)
# 定义项目名称
project(HarmonyApplication)
# 定义变量:CMAKE_CURRENT_SOURCE_DIR 为系统内置变量,代表当前 CMakeLists.txt 所在的文件夹路径
set(NATIVERENDER_ROOT_PATH ${CMAKE_CURRENT_SOURCE_DIR})

# 判断是否定义了 PACKAGE_FIND_FILE 变量,若是则引入该文件,鸿蒙自动生成的兼容配置,用于加载依赖包的配置,开发者无需手动修改
if(DEFINED PACKAGE_FIND_FILE)
    include(${PACKAGE_FIND_FILE})
endif() # CMake 里 if 判断的结束标记,用来闭合 if 语句,CMake 不是 Java,没有大括号 {} 来圈定代码范围

# 添加头文件搜索路径:告诉 CMake,编译 C++ 代码时去这两个路径下查找头文件
include_directories(${NATIVERENDER_ROOT_PATH}
                    ${NATIVERENDER_ROOT_PATH}/include)

# 将 napi_init.cpp 编译成名为 entry 的动态库
# add_library:CMake 编译库文件的命令
# entry:最终生成的动态库名称(编译后会得到libentry.so)
# SHARED:指定生成动态共享库(鸿蒙 NAPI 必须用动态库)
# napi_init.cpp:要编译的 C++ 源文件
add_library(entry SHARED napi_init.cpp)

# 为动态库链接依赖库:让我们的动态库能调用鸿蒙 NAPI 接口,实现 C++ 与 ArkTS 的交互
target_link_libraries(entry PUBLIC libace_napi.z.so)

模块级 build-profile.json5 中 externalNativeOptions 参数是 NDK 工程 C/C++ 文件编译配置的入口

image.png

napi_init.cpp 是鸿蒙 NDK 的 “入口文件”,它是 C/C++ 代码 和 ArkTS/JS 代码之间的桥梁,没有它,ArkTS 就调用不了你的 C++ 方法。

它专门负责 3 件事:

  • 注册 Native 模块:告诉系统是一个 C++ 动态库
  • 绑定 C++ 函数:把你写的 C++ 方法暴露给 ArkTS
  • 提供调用入口:让 ArkTS 能像调用普通函数一样调用 C++
#include "napi/native_api.h"

//自定义的 C++ 方法(给 ArkTS 调用)
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;

}

//模块初始化:实现 ArkTS 接口与 C++ 接口的绑定和映射
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

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

在 cpp\types\libentry\Index.d.ts 文件中,提供 JS 侧的接口方法

export const add: (a: number, b: number) => number;

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

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

这些都是由 DevEco Studio 自动生成的,比如我们在 Index.d.ts 中定义一个方法

image.png

然后点击 Generate native implementation,它就能在 cpp 中自动生成对应的 C++ 方法和绑定

image.png

Node-API

  • napi_env:表示 Node-API 执行时的上下文,可以把它理解成 NAPI 给你的一张操作许可证 + 全套工具,所有 NAPI 函数都必须传入它。
  • napi_callback_info:代表 ArkTS 调用 C++ 函数时传递过来的所有信息,专门用来获取 ArkTS 传过来的参数。
  • napi_value:是一个C的结构体指针,表示一个 ArkTS/JS 对象的引用,可以理解为万能的数据载体,是 NAPI 统一的数据类型,可以表示字符串,数字,布尔,数组,对象,null,undefined 等等,C++ 和 ArkTS 之间传递数据只能用它,不能直接传 int,string,bool,必须包装成 napi_value。

这仨的关系,简言之:
ArkTS 调用 C++ 函数 -> 通过 info 拿到参数列表 -> 参数都是 napi_value 类型 -> 用 env 操作这些 napi_value -> 返回一个 napi_value 给 ArkTS

现在来实现一下上面定义的 NAPI_Global_getLast 方法,用来获取数组的最后一个元素。

static napi_value NAPI_Global_getLast(napi_env env, napi_callback_info info) {
    size_t argc = 1;
    napi_value args[1];
    napi_get_cb_info(env, info, &argc, args, nullptr, nullptr);
    // 判断是否为数组
    bool isArray = false;
    napi_is_array(env, args[0], &isArray);
    if (isArray) {
        // 获取数组长度
        uint32_t arrayLength = 0;
        napi_get_array_length(env, args[0], &arrayLength);
        if (arrayLength > 0) {
            // 获取最后一个元素的索引
            uint32_t lastIndex = arrayLength - 1;
            // 获取数组最后一个元素
            napi_value lastElement;
            napi_get_element(env, args[0], lastIndex, &lastElement);
            // 获取字符串长度
            size_t strLen = 0;
            napi_get_value_string_utf8(env, lastElement, nullptr, 0, &strLen);
            // 读取字符串内容
            char resultStr[1024];
            napi_get_value_string_utf8(env, lastElement, resultStr, sizeof(resultStr), nullptr);
            napi_value returnValue;
            // NAPI_AUTO_LENGTH = 让 NAPI 自动计算字符串长度,不用你手动填数字
            napi_create_string_utf8(env, resultStr, NAPI_AUTO_LENGTH, &returnValue);

            return returnValue;
        }
    }
    return nullptr;
}

常用的 Napi 方法

获取调用信息(函数入口必用)

size_t argc = 1;
napi_value args[1];
napi_get_cb_info(env, info, &argc, args, nullptr, nullptr);

类型判断

napi_is_array:判断是不是数组

bool isArray = false;
napi_is_array(env, args[0], &isArray);

napi_typeof:判断类型

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

取值

取字符串

char buf[1024];
napi_get_value_string_utf8(env, args[0], buf, sizeof(buf), nullptr);
std::string cppStr = buf;

取数字

double num;
napi_get_value_double(env, args[0], &num);

取整数

int num;
napi_get_value_int32(env, args[0], &num);

取布尔值

bool b;
napi_get_value_bool(env, args[0], &b);

创建值

// 创建数字
napi_value dNum;
napi_create_double(env, 100, &dNum);

// 创建整数
napi_value num;
napi_create_int32(env, 10, &num);

// 创建字符串
napi_value str;
napi_create_string_utf8(env, "Hello", NAPI_AUTO_LENGTH, &str);

// 创建布尔值
napi_value b;
napi_create_boolean(env, true, &b);

// 创建对象
napi_value obj;
napi_create_object(env, &obj);

// 创建数组
napi_value arr;
napi_create_array(env, &arr);

数组操作

// 获取数组长度
uint32_t len;
napi_get_array_length(env, arr, &len);

// 获取数组第 index 个元素
napi_value elem;
napi_get_element(env, arr, index, &elem);

// 设置数组第 index 个元素
napi_set_element(env, arr, index, elem);

对象操作

export const handleUser: (user: UserInfo) => UserInfo;

export interface UserInfo {
  name: string;
  age: number;
}
// ArkTS对象 → C++结构体
struct UserInfo {
    std::string name;
    int32_t age;
};


UserInfo ParseUser(napi_env env, napi_value object) {
    UserInfo info{};
    napi_value nameVal, ageVal;

    // 读取 name
    napi_get_named_property(env, object, "name", &nameVal);
    char nameBuff[64];
    size_t len;
    napi_get_value_string_utf8(env, nameVal, nameBuff, sizeof(nameBuff), &len);
    info.name = nameBuff;

    // 读取 age
    napi_get_named_property(env, object, "age", &ageVal);
    napi_get_value_int32(env, ageVal, &info.age);

    return info;
}

// C++ 结构体 -> ArkTs 对象
napi_value WrapUser(napi_env env, const UserInfo &info) {
    napi_value jsObject;
    napi_create_object(env, &jsObject);

    // 设置 name
    napi_value nameVal;
    napi_create_string_utf8(env, info.name.c_str(), NAPI_AUTO_LENGTH, &nameVal);
    napi_set_named_property(env, jsObject, "name", nameVal);

    // 设置 age
    napi_value ageVal;
    napi_create_int32(env, info.age, &ageVal);
    napi_set_named_property(env, jsObject, "age", ageVal);

    return jsObject;
}

static napi_value NAPI_Global_handleUser(napi_env env, napi_callback_info info) {
    size_t argc = 1;
    napi_value args[1];
    napi_get_cb_info(env, info, &argc, args, nullptr, nullptr);
    
    // 解析入参
    UserInfo userInfo = ParseUser(env, args[0]);
    userInfo.age += 1;
    userInfo.name = "XZJ";
    
    return WrapUser(env, userInfo);
}