C++混合开发:使用NAPI调用C/C++库(OpenCV/FFmpeg)(50)

在鸿蒙(HarmonyOS)中进行 C++ 混合开发,调用 OpenCV 或 FFmpeg 等底层 C/C++ 库,其核心技术是 NAPI (Node-API)。整体架构分为 ArkTS 前端调用、NAPI 接口桥接、C++ 业务逻辑处理以及底层三方库执行四个层级。

以下是完整的实战开发流程与代码示例:

一、 工程配置与依赖引入

在 NDK 工程中使用预构建的 C++ 库(如 FFmpeg 或 OpenCV),需要通过 CMake 进行配置。

1. 引入预构建库(以 FFmpeg 为例)

将预构建的 .so 文件放入工程的 libs 目录,并在 CMakeLists.txt 中声明并链接:

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

# 添加预构建的 FFmpeg 库
add_library(avcodec_ffmpeg SHARED IMPORTED)
set_target_properties(avcodec_ffmpeg PROPERTIES IMPORTED_LOCATION 
    ${CMAKE_CURRENT_SOURCE_DIR}/third_party/FFmpeg/libs/${OHOS_ARCH}/libavcodec_ffmpeg.so)

# 添加头文件路径
include_directories(${CMAKE_CURRENT_SOURCE_DIR}/third_party/FFmpeg/include)

# 编译 NAPI 模块并链接依赖库
add_library(entry SHARED napi_init.cpp)
target_link_libraries(entry PUBLIC libace_napi.z.so avcodec_ffmpeg)

2. 声明 ArkTS 接口

index.d.ts 中声明供 ArkTS 调用的方法,并通过 oh-package.json5 关联:

javascript 复制代码
// entry/src/main/cpp/types/libentry/index.d.ts
export const callNativeFFmpeg: (inputPath: string, outputPath: string) => number;

二、 NAPI 接口桥接与 C++ 业务实现

NAPI 的核心职责是处理 ArkTS 与 C++ 之间的数据类型转换,并将实际业务委托给 C++ 类处理。

1. NAPI 接口注册与参数解析

napi_init.cpp 中注册方法,解析 ArkTS 传入的参数,并调用 C++ 业务类:

cpp 复制代码
// napi_init.cpp
#include "napi/native_api.h"
#include "ffmpeg_utils.h" // 引入 C++ 业务逻辑头文件

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

    // 解析 ArkTS 传入的字符串参数
    char inputPath[1024], outputPath[1024];
    size_t length;
    napi_get_value_string_utf8(env, args[0], inputPath, sizeof(inputPath), &length);
    napi_get_value_string_utf8(env, args[1], outputPath, sizeof(outputPath), &length);

    // 调用 C++ 核心处理逻辑
    int result = FfmpegUtils::processVideo(inputPath, outputPath);

    // 返回结果给 ArkTS
    napi_value res;
    napi_create_int32(env, result, &res);
    return res;
}

// 模块初始化与接口映射
EXTERN_C_START
static napi_value Init(napi_env env, napi_value exports) {
    napi_property_descriptor desc[] = {
        { "callNativeFFmpeg", nullptr, CallNativeFFmpeg, nullptr, nullptr, nullptr, napi_default, nullptr }
    };
    napi_define_properties(env, exports, sizeof(desc) / sizeof(desc[0]), desc);
    return exports;
}
EXTERN_C_END

2. C++ 核心业务逻辑(以 FFmpeg 视频裁剪为例)

在 C++ 层直接调用 FFmpeg 的 API 处理多媒体数据:

cs 复制代码
// ffmpeg_utils.cpp
#include "ffmpeg_utils.h"
#include <libavformat/avformat.h>
#include <libavcodec/avcodec.h>
#include "hilog/log.h"

int FfmpegUtils::processVideo(const char* in_filename, const char* out_filename) {
    AVFormatContext *ifmt_ctx = nullptr, *ofmt_ctx = nullptr;
    int ret = 0;

    // 1. 初始化输入输出上下文
    ret = avformat_open_input(&ifmt_ctx, in_filename, 0, 0);
    if (ret < 0) return ret;
    
    ret = avformat_alloc_output_context2(&ofmt_ctx, NULL, NULL, out_filename);
    if (ret < 0) goto end;

    // 2. 创建输出流并拷贝编码参数
    for (int i = 0; i < (int)ifmt_ctx->nb_streams; i++) {
        AVStream *out_stream = avformat_new_stream(ofmt_ctx, NULL);
        avcodec_parameters_copy(out_stream->codecpar, ifmt_ctx->streams[i]->codecpar);
        out_stream->codecpar->codec_tag = 0;
    }

    // 3. 打开输出文件,写入头信息,读取帧并写入...
    ret = avio_open(&ofmt_ctx->pb, out_filename, AVIO_FLAG_WRITE);
    avformat_write_header(ofmt_ctx, NULL);
    
    // (此处省略 av_read_frame 和 av_interleaved_write_frame 的循环处理逻辑)

end:
    // 4. 释放资源
    if (ifmt_ctx) avformat_close_input(&ifmt_ctx);
    if (ofmt_ctx) avformat_free_context(ofmt_ctx);
    return ret;
}

三、PixelMap 与 OpenCV 的内存交互

在图像处理中,ArkTS 侧通常使用 PixelMap,需要在 C++ 侧将其零拷贝或安全拷贝转换为 OpenCV 的 cv::Mat 进行处理。

cpp 复制代码
// 方案:使用 OH_PixelMap_AccessPixels 获取内存地址并转换为 cv::Mat
#include <multimedia/image_framework/image_pixel_map_mdk.h>
#include <opencv2/opencv.hpp>

static napi_value AccessToMat(napi_env env, napi_callback_info info) {
    // 1. 获取 ArkTS 传入的 NativePixelMap 对象
    NativePixelMap *native = OH_PixelMap_InitNativePixelMap(env, args[0]);
    
    // 2. 获取图像基本信息(宽高、行大小等)
    struct OhosPixelMapInfos pixelMapInfos;
    OH_PixelMap_GetImageInfo(native, &pixelMapInfos);
    
    // 3. 获取内存地址并锁定
    void *pixel = nullptr;
    OH_PixelMap_AccessPixels(native, &pixel);
    
    // 4. 构造 cv::Mat (注意传入 rowSize 保证内存对齐)
    cv::Mat originMat(pixelMapInfos.height, pixelMapInfos.width, CV_8UC4, pixel, pixelMapInfos.rowSize);
    
    // 5. 使用 OpenCV 进行图像处理(如灰度转换)
    cv::Mat grayMat;
    cv::cvtColor(originMat, grayMat, cv::COLOR_BGRA2GRAY);
    
    // 6. 处理完毕后释放内存锁
    OH_PixelMap_UnAccessPixels(native);
    
    return nullptr;
}

核心注意事项

  1. 线程安全 :NAPI 的回调默认在 ArkTS 线程执行,如果 C++ 层有耗时操作(如视频编解码),务必使用 napi_create_async_work 将其放入后台线程执行,避免阻塞 UI。
  2. SONAME 规范 :引入预构建的 .so 库时,必须确保其设置了正确的 SONAME,否则在 HAP 打包运行时动态加载器可能无法找到依赖库。
  3. 内存管理:在 C++ 与 ArkTS 交互(尤其是传递 PixelMap 或 ArrayBuffer)时,务必遵循"谁申请谁释放"的原则,防止内存泄漏。

四、 工程化提效:IDE 自动化生成胶水代码

在复杂的 C++ 项目中,手动编写 napi_get_cb_infonapi_create_string_utf8 等类型转换代码极其繁琐且容易出错。DevEco Studio 提供了强大的跨语言代码编辑能力:

  • 自动生成 NAPI 框架 :在 C++ 头文件(.h / .hpp)中,将光标置于需要暴露给 ArkTS 的函数或类名上,右键选择 Generate > NAPI,IDE 会自动生成对应的胶水代码框架及 napi_init.cpp 注册逻辑。
  • 反向生成 C++ 定义 :在 ArkTS 侧的 .d.ts 接口文件中,针对未实现的 Native 函数,点击悬浮提示的 Generate native implementation,可一键生成对应的 C++ 函数定义,大幅缩短开发周期。
1、 自动生成 NAPI 框架(C++ 头文件 -> 注册与胶水代码)

假设你在 C++ 头文件中定义了一个简单的数学运算函数。

1. 编写 C++ 头文件

javascript 复制代码
// entry/src/main/cpp/test.h
#ifndef TEST_H
#define TEST_H

int add(int a, int b);
int getNumber();

#endif // TEST_H

2. 触发 IDE 自动生成

  • 首次生成 :确保当前 C++ 工程 entry > src > main > cpp 路径下没有 napi_init.cpp 文件。将光标放置在 addgetNumber 函数名上,右键选择 Generate > NAPI 。IDE 会自动生成 napi_init.cpp 文件。
  • 追加生成 :如果 napi_init.cpp 已存在,再次对 getNumber 函数右键选择 Generate > NAPI ,IDE 会自动在 napi_property_descriptor 数组中追加注册信息,并生成对应的胶水代码框架。

3. IDE 自动生成的 napi_init.cpp 代码示例

cpp 复制代码
// entry/src/main/cpp/napi_init.cpp
#include "napi/native_api.h"
#include "test.h"

// 自动生成的胶水代码框架
static napi_value NAPI_Global_add(napi_env env, napi_callback_info info) {
    // ToDo: implements the code;
    return nullptr;
}

static napi_value NAPI_Global_getNumber(napi_env env, napi_callback_info info) {
    // ToDo: implements the code;
    return nullptr;
}

EXTERN_C_START
static napi_value Init(napi_env env, napi_value exports) {
    // 自动完成接口映射与注册
    napi_property_descriptor desc[] = {
        { "NAPI_Global_add", nullptr, NAPI_Global_add, nullptr, nullptr, nullptr, napi_default, nullptr },
        { "NAPI_Global_getNumber", nullptr, NAPI_Global_getNumber, nullptr, nullptr, nullptr, napi_default, nullptr }
    };
    napi_define_properties(env, exports, sizeof(desc) / sizeof(desc[0]), desc);
    return exports;
}
EXTERN_C_END

2、 跨语言快速生成函数定义(ArkTS .d.ts -> C++ 实现)

在 ArkTS 侧声明好接口后,可以直接让 IDE 反向生成对应的 C++ 底层实现。

1. 在 ArkTS 侧声明接口

cpp 复制代码
// entry/src/main/cpp/types/libentry/Index.d.ts
export const add: (a: number, b: number) => number;
export const test: (a: number, b: number) => number;

2. 触发 IDE 自动生成

将光标悬浮在未定义的函数名(如 test)处,IDE 会提示 Declared function 'test' has no native implementation.。此时点击页面出现的红色灯泡图标 ,或者按下快捷键 Alt + Shift + Enter,选择 Generate native implementation

3. IDE 自动生成的 C++ 函数定义

IDE 会根据 .d.ts 中的类型签名,自动在对应的 C++ 文件中生成符合 NAPI 规范的函数定义:

cs 复制代码
// 自动生成的 C++ 函数定义
static napi_value Test(napi_env env, napi_callback_info info) {
    // ToDo: implements the code;
    return nullptr;
}

五、 架构解耦:C++ 类的面向对象导出

在实际业务中,底层 C++ 库往往是以"类(Class)"的形式提供能力的(如 dlca_player 播放器类)。如果仅导出全局函数,会导致状态管理混乱。NAPI 支持将 C++ 类完整导出为 ArkTS 侧的类:

  • 实例映射表管理 :在 C++ 侧维护一个全局的 std::map<int, NativeClass*> 映射表。创建实例时,返回一个 id 给 ArkTS 侧;后续 ArkTS 调用方法时,传入 id,C++ 侧通过 id 找到对应的 Native 对象并执行方法。
  • 类定义注册 :通过 napi_define_classnapi_define_properties 将 C++ 类的构造函数及成员方法(如 play, stop, setConfig)挂载到 ArkTS 的 exports 对象上,实现面向对象的跨语言调用。
1、 C++ 侧:定义类与 NAPI 桥接

1. 定义底层 C++ 类

javascript 复制代码
// MyDemo.h
class MyDemo {
public:
    MyDemo(std::string m_name);
    ~MyDemo();
    std::string name;
    int add(int a, int b);
};

2. 实现 NAPI 绑定与生命周期管理

hello.cpp 中,通过 napi_define_class 建立 ArkTS 类与 C++ 类的映射,并在构造函数中使用 napi_wrap 绑定对象:

cpp 复制代码
// hello.cpp
#include "napi/native_api.h"
#include "MyDemo.h"

// 1. ArkTS 侧的构造函数映射
static napi_value JsConstructor(napi_env env, napi_callback_info info) {
    napi_value jDemo = nullptr;
    size_t argc = 1;
    napi_value args[1] = {nullptr};
    napi_get_cb_info(env, info, &argc, args, &jDemo, nullptr);

    // 获取 ArkTS 传入的参数并创建 C++ 对象
    char name[50];
    size_t result = 0;
    napi_get_value_string_utf8(env, args[0], name, sizeof(name), &result);
    MyDemo *cDemo = new MyDemo(name);

    // 【核心】将 C++ 对象包装到 ArkTS 对象上
    // 同时注册析构回调:当 ArkTS 对象被 GC 回收时,自动销毁 C++ 对象,防止内存泄漏
    napi_wrap(env, jDemo, cDemo, [](napi_env env, void *finalize_data, void *finalize_hint) {
        MyDemo *cDemo = (MyDemo *)finalize_data;
        delete cDemo;
        cDemo = nullptr;
    }, nullptr, nullptr);

    return jDemo;
}

// 2. ArkTS 侧的成员方法映射(以 add 为例)
static napi_value JsAdd(napi_env env, napi_callback_info info) {
    size_t argc = 2;
    napi_value args[2] = {nullptr};
    napi_value jDemo = nullptr;
    napi_get_cb_info(env, info, &argc, args, &jDemo, nullptr);

    // 【核心】从 ArkTS 对象中解包出 C++ 对象指针
    MyDemo *cDemo = nullptr;
    napi_unwrap(env, jDemo, (void **)&cDemo);

    // 获取参数并调用 C++ 方法
    int value0, value1;
    napi_get_value_int32(env, args[0], &value0);
    napi_get_value_int32(env, args[1], &value1);
    int cResult = cDemo->add(value0, value1);

    napi_value jResult;
    napi_create_int32(env, cResult, &jResult);
    return jResult;
}

// 3. 模块初始化与类注册
EXTERN_C_START
static napi_value Init(napi_env env, napi_value exports) {
    // 定义类的方法映射
    napi_property_descriptor classProp[] = {
        {"add", nullptr, JsAdd, nullptr, nullptr, nullptr, napi_default, nullptr}
    };

    napi_value jDemo = nullptr;
    const char *jDemoName = "MyDemo";
    
    // 【核心】定义 ArkTS 类与 C++ 构造函数的关联
    napi_define_class(env, jDemoName, NAPI_AUTO_LENGTH, JsConstructor, nullptr,
                      sizeof(classProp) / sizeof(classProp[0]), classProp, &jDemo);
    
    // 将类挂载到 exports 对象上导出
    napi_set_named_property(env, exports, jDemoName, jDemo);
    return exports;
}
EXTERN_C_END
2、 ArkTS 侧:声明接口与调用

1. 声明 .d.ts 接口文件

为了让 ArkTS 侧能够识别并具备代码提示,需要在 index.d.ts 中声明对应的类结构:

cpp 复制代码
// index.d.ts
declare namespace testNapi {
    class MyDemo {
        constructor(name: string);
        name: string;
        add(a: number, b: number): number;
    }
}
export default testNapi;

2. 业务代码调用

在 ArkTS 业务逻辑中,可以像使用普通 TypeScript 类一样,通过 new 关键字创建 Native 对象并调用方法:

cpp 复制代码
// Index.ets
import testNapi from 'libentry.so';

// 创建 Native 对象
const demo = new testNapi.MyDemo('TestInstance');

// 调用 Native 方法
const result = demo.add(10, 20);
console.info(`Native 计算结果: ${result}`); 

六、 极致性能:纯 Native 架构与异步任务

对于性能极度敏感的场景(如实时计步器、物理引擎模拟),推荐采用**"纯 Native 架构"**:

  • 核心逻辑全下沉:将传感器订阅、数据过滤、算法计算全部放在 C++ 层完成,ArkTS 层仅作为 UI 渲染展示层。由于 C++ 做计算天然比脚本层快,且避免了频繁的跨语言通信开销,能显著提升帧率与响应速度。
  • 异步工作线程(Async Work) :NAPI 的回调默认在 ArkTS 主线程执行。对于耗时操作,必须使用 napi_create_async_work 将任务分发到后台线程池执行,处理完毕后再通过回调将结果安全地传回 ArkTS 侧,坚决杜绝阻塞 UI 线程导致的掉帧或 ANR。

七、 内存与资源安全管控

  • 指针安全传递 :在 ArkTS 与 Native 之间传递复杂的 C++ 对象时,通常将其强转为 int64_t 指针值进行传递。务必在 ArkTS 侧提供显式的 destroy 接口,在 C++ 侧清理全局映射表并释放内存,防止内存泄漏。
  • 生命周期管理 :充分利用 NAPI 的 ScopeManagerReferenceManager 来管理 napi_valuenapi_ref 的生命周期,确保在跨语言传递复杂对象(如 PixelMap、ArrayBuffer)时,底层内存不会被垃圾回收机制意外释放。