在鸿蒙应用开发中,经常会遇到需要复用已有 C/C++ 三方库的场景。这些库可能是在 Linux/Android 平台上编译的,也可能专门为 HarmonyOS 编译。无论是哪种情况,ArkTS 代码都无法直接调用 C/C++ 函数——必须通过 NAPI(Node-API) 机制搭建一座桥梁。本文以 libmediainfo 为例,从零开始讲解如何在鸿蒙应用中集成和使用 C/C++ 三方动态库。

更多交流学习,欢迎加入开源鸿蒙PC社区https://harmonypc.csdn.net/

欢迎在PC社区平台申请新建项目https://atomgit.com/OpenHarmonyPCDeveloper
在这里插入图片描述


文章目录

1. 背景与概述

在鸿蒙应用开发中,经常会遇到需要复用已有 C/C++ 三方库的场景。这些库可能是在 Linux/Android 平台上编译的,也可能专门为 HarmonyOS 编译。无论是哪种情况,ArkTS 代码都无法直接调用 C/C++ 函数——必须通过 NAPI(Node-API) 机制搭建一座桥梁。

libmediainfo 简介

MediaInfo 是一个开源的媒体文件信息解析库,可以获取视频、音频文件的编码格式、码率、分辨率等详细信息。它提供了 C++ 类接口和 C 函数接口,我们以它为例演示完整的集成流程。

假设我们已经有了:

  • libmediainfo.so:编译好的 ARM64 动态库
  • MediaInfo.h 等头文件

目标:在 ArkTS 应用中调用 MediaInfo_Open()MediaInfo_Inform() 等函数来解析媒体文件。

本文对应的demo项目地址https://atomgit.com/qq8864/LibMediaInfoDemo


2. 两种集成方式对比

在 HarmonyOS 中引用三方 SO 库有两种方式:

对比项 直接链接(推荐) dlopen 动态加载
原理 CMake 编译时链接 SO,运行时自动加载 运行时通过 dlopen() 手动加载
代码量 少,声明 extern 函数即可 多,需定义函数指针、管理句柄
编译检查 链接期验证符号是否存在 无编译期检查,运行时才发现缺失
依赖传递 CMake 自动处理依赖链 需手动确保所有依赖 SO 存在
适用场景 SO 库导出了 C 接口符号 需运行时决定加载哪个 SO
ArkTS调用 import lib from 'xxx.so' 直接调用 需传递沙箱路径给 Native

本文采用直接链接方式,这也是 HarmonyOS 官方推荐的做法。


3. 项目结构设计

一个完整的 NAPI 集成项目,目录结构如下:

LibMediaInfoDemo/
├── AppScope/
│   └── app.json5
├── entry/
│   ├── libs/
│   │   └── arm64-v8a/
│   │       ├── libmediainfo.so         ← ① 三方SO文件
│   │       ├── libtinyxml2.so          ← ① 三方SO的依赖库
│   │       ├── libzen.so
│   │       └── libc++_shared.so        ← ① C++运行时库
│   ├── src/main/
│   │   ├── cpp/
│   │   │   ├── include/                ← ② 头文件
│   │   │   │   ├── MediaInfo/
│   │   │   │   │   ├── MediaInfo.h
│   │   │   │   │   └── MediaInfo_Const.h
│   │   │   │   └── MediaInfoDLL/
│   │   │   │       └── MediaInfoDLL.h
│   │   │   ├── types/
│   │   │   │   └── mediainfo_napi/     ← ③ 类型声明
│   │   │   │       ├── index.d.ts
│   │   │   │       └── oh-package.json5
│   │   │   ├── mediainfo_napi.cpp      ← ④ NAPI桥接代码
│   │   │   └── CMakeLists.txt          ← ⑤ CMake构建配置
│   │   ├── ets/
│   │   │   ├── entryability/
│   │   │   │   └── EntryAbility.ets
│   │   │   └── pages/
│   │   │       └── Index.ets           ← ⑥ UI页面
│   │   ├── resources/
│   │   └── module.json5
│   ├── build-profile.json5             ← ⑦ 含externalNativeOptions
│   └── oh-package.json5                ← ⑧ 含SO依赖声明
├── build-profile.json5
└── oh-package.json5

标注说明

  • ① ② 是你的三方库文件,直接放入对应位置
  • ③ ④ ⑤ 是你需要编写的核心代码
  • ⑥ 是 ArkTS UI 层
  • ⑦ ⑧ 是项目配置

4. Step 1: 创建工程与放置文件

4.1 创建工程

在 DevEco Studio 中创建 “Native C++” 模板工程,或使用空模板后手动添加 C++ 支持。

4.2 放置 SO 文件

libmediainfo.so 及其依赖库放入 entry/libs/arm64-v8a/ 目录。此目录下的 SO 文件会被构建系统自动打包到 HAP 中,最终安装到设备的沙箱目录。

entry/libs/arm64-v8a/
├── libmediainfo.so      ← 主库
├── libtinyxml2.so       ← 依赖库
├── libzen.so
└── libc++_shared.so     ← C++运行时

重要:三方库的依赖库也必须放入此目录!用 llvm-readelf -d xxx.so | grep NEEDED 查看依赖。

如果 SO 文件还有其他架构版本(如 x86_64),也放入对应子目录即可。

4.3 放置头文件

将头文件放入 entry/src/main/cpp/include/ 目录下,保持原有的目录结构:

entry/src/main/cpp/include/
├── MediaInfo/
│   ├── MediaInfo.h
│   ├── MediaInfo_Const.h
│   └── ...
└── MediaInfoDLL/
    ├── MediaInfoDLL.h
    └── ...

4.4 确认 SO 架构

在集成前,务必确认 SO 文件的架构与目标设备匹配:

# Linux/Mac
file libmediainfo.so
# 输出: ELF 64-bit LSB shared object, ARM aarch64, ...

# Windows (用Python查看)
python -c "f=open('libmediainfo.so','rb'); m=f.read(20); print(m[4:6].hex(), m[18:20].hex())"
# ARM64输出: 0201 b700   (0xB7 = AArch64)
# x86_64输出: 0201 3e00  (0x3E = x86_64)

4.5 SO 文件签名说明

HAP 应用中 SO 文件不需要单独签名!

在 HarmonyOS 中有两种开发场景:

场景 签名要求 说明
HAP 应用开发 不需要单独签名 HAP 整体包签名会覆盖内部所有 .so 文件
系统级原生工具开发 必须单独签名 可执行文件、核心 .so 库都直接暴露给内核,必须用 binary-sign-tool 签名

本文是 HAP 应用开发场景,不需要对 SO 文件单独签名。构建系统会自动对整个 HAP 包签名。


5. Step 2: 编写 NAPI 桥接层

这是整个集成的核心——编写一个 C++ 文件,将三方库的 C/C++ 函数"包装"成 NAPI 接口,让 ArkTS 可以调用。

5.1 核心思路

ArkTS 调用 getMediaInfo(filePath)
         ↓
NAPI 桥接层 mediainfo_napi.cpp
  ├── napi_get_value_string_utf8()  解析ArkTS参数
  ├── MediaInfo_New()               调用三方库创建实例
  ├── MediaInfo_Option()            初始化库(设置版本等)
  ├── MediaInfo_Open()              调用三方库打开文件
  ├── MediaInfo_Inform()            调用三方库获取信息
  ├── MediaInfo_Close/Delete()      调用三方库释放资源
  └── napi_create_string_utf8()     返回结果给ArkTS

5.2 确认可用的 C 接口

直接链接方式要求三方库导出 C 接口符号(即 extern "C" 修饰的函数)。libmediainfo 的 C 接口定义在 MediaInfoDLL.h 中,主要包括:

void*  MediaInfo_New();
void   MediaInfo_Delete(void* handle);
size_t MediaInfo_Open(void* handle, const char* file);
void   MediaInfo_Close(void* handle);
const char* MediaInfo_Inform(void* handle, size_t reserved);
const char* MediaInfo_Option(void* handle, const char* option, const char* value);
const char* MediaInfo_Get(void* handle, size_t streamKind, size_t streamNumber,
                          const char* parameter, size_t infoKind, size_t searchKind);
size_t MediaInfo_Count_Get(void* handle, size_t streamKind, size_t streamNumber);

如果三方库只导出了 C++ 类接口(如 MediaInfo::Open()),你有两个选择:

  1. 用 dlopen + dlsym 方式加载(但同样只能调用 C 接口)
  2. 修改三方库源码,添加 extern "C" 包装函数,重新编译

5.3 编写桥接代码

// mediainfo_napi.cpp
#include <cstdio>
#include <cstring>
#include <string>
#include "hilog/log.h"
#include "napi/native_api.h"

#undef LOG_DOMAIN
#undef LOG_TAG
#define LOG_DOMAIN 0x0000
#define LOG_TAG "LibMediaInfoDemo"

// 声明三方库的C接口
extern "C" {
    void *MediaInfo_New();
    void MediaInfo_Delete(void *handle);
    size_t MediaInfo_Open(void *handle, const char *file);
    void MediaInfo_Close(void *handle);
    const char *MediaInfo_Inform(void *handle, size_t reserved);
    const char *MediaInfo_Option(void *handle, const char *option, const char *value);
    const char *MediaInfo_Get(void *handle, size_t streamKind, size_t streamNumber,
                              const char *parameter, size_t infoKind, size_t searchKind);
}

// NAPI函数: 获取完整媒体信息
static napi_value GetMediaInfo(napi_env env, napi_callback_info info)
{
    // 1. 解析参数 - 从ArkTS获取文件路径字符串
    size_t argc = 1;
    napi_value args[1] = {nullptr};
    napi_get_cb_info(env, info, &argc, args, nullptr, nullptr);

    size_t filePathLen = 0;
    napi_get_value_string_utf8(env, args[0], nullptr, 0, &filePathLen);
    char *filePath = new char[filePathLen + 1];
    napi_get_value_string_utf8(env, args[0], filePath, filePathLen + 1, &filePathLen);

    napi_value result = nullptr;

    // 2. 调用三方库
    void *handle = MediaInfo_New();
    if (handle == nullptr) {
        napi_create_string_utf8(env, "MediaInfo_New() returned null",
                               NAPI_AUTO_LENGTH, &result);
        delete[] filePath;
        return result;
    }

    // ⚠️ 重要:初始化库(必须设置版本信息)
    MediaInfo_Option(handle, "Info_Version", "0.7.0.0;MyApp;1.0.0");
    MediaInfo_Option(handle, "CharSet", "UTF-8");
    MediaInfo_Option(handle, "Internet", "No");  // 禁用网络连接

    size_t openResult = MediaInfo_Open(handle, filePath);
    if (openResult == 0) {
        napi_create_string_utf8(env, "Failed to open file",
                               NAPI_AUTO_LENGTH, &result);
        MediaInfo_Delete(handle);
        delete[] filePath;
        return result;
    }

    const char *informResult = MediaInfo_Inform(handle, 0);
    std::string infoStr = (informResult != nullptr) ? informResult : "";

    MediaInfo_Close(handle);
    MediaInfo_Delete(handle);

    // 3. 返回结果给ArkTS
    napi_create_string_utf8(env, infoStr.c_str(), infoStr.length(), &result);
    delete[] filePath;
    return result;
}

// 注册NAPI模块
EXTERN_C_START
static napi_value Init(napi_env env, napi_value exports)
{
    napi_property_descriptor desc[] = {
        {"getMediaInfo", nullptr, GetMediaInfo,
         nullptr, nullptr, nullptr, napi_default, nullptr},
    };
    napi_define_properties(env, exports, sizeof(desc) / sizeof(desc[0]), desc);
    return exports;
}
EXTERN_C_END

// ⚠️ nm_modname 命名规则:避免与三方库名冲突!
static napi_module demoModule = {
    .nm_version = 1,
    .nm_flags = 0,
    .nm_filename = nullptr,
    .nm_register_func = Init,
    .nm_modname = "mediainfo_napi",    // 不要用 "libmediainfo",会与三方库冲突!
                                       // → 编译出 libmediainfo_napi.so
                                       // → ArkTS import from 'libmediainfo_napi.so'
    .nm_priv = ((void *)0),
    .reserved = {0},
};

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

5.4 nm_modname 命名规则(非常重要!)

这是最容易踩坑的地方。NAPI 模块名不要与三方库名相同,否则会导致符号冲突!

C++ 侧 CMake 侧 ArkTS 侧 说明
nm_modname = "mediainfo_napi" add_library(mediainfo_napi SHARED ...) import xxx from 'libmediainfo_napi.so' ✅ 正确
nm_modname = "libmediainfo" add_library(libmediainfo SHARED ...) import xxx from 'liblibmediainfo.so' ❌ 名字混乱

规律:

  • CMake 编译生成的 SO 文件名 = lib + target_name + .so
  • nm_modname 的值 = CMake 的 target_name
  • ArkTS import 名 = lib + nm_modname + .so

踩坑警告

  1. 不要让 NAPI 模块名与三方库名相同(如三方库叫 libmediainfo.so,NAPI 模块就叫 mediainfo_napi
  2. 如果 nm_modname = "entry"(很多模板默认值),会导致 is not callable 运行时错误!

6. Step 3: 配置 CMakeLists.txt

cmake_minimum_required(VERSION 3.4.1)
project(LibMediaInfoDemo)

set(NATIVERENDER_ROOT_PATH ${CMAKE_CURRENT_SOURCE_DIR})

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

# 编译我们自己的NAPI桥接SO
add_library(mediainfo_napi SHARED mediainfo_napi.cpp)

# 添加头文件搜索路径
target_include_directories(mediainfo_napi PRIVATE
    ${CMAKE_CURRENT_SOURCE_DIR}/include
    ${CMAKE_CURRENT_SOURCE_DIR}/include/MediaInfo
    ${CMAKE_CURRENT_SOURCE_DIR}/include/MediaInfoDLL
)

# 链接三方SO库和系统库
target_link_libraries(mediainfo_napi PUBLIC
    # ⚠️ 三方SO库路径: 从cpp目录回溯3级到entry,再进入libs/<架构>/
    ${NATIVERENDER_ROOT_PATH}/../../../libs/${OHOS_ARCH}/libmediainfo.so
    ace_napi.z       # NAPI框架库(必须)
    hilog_ndk.z      # 日志库
)

路径解析说明

CMakeLists.txt 位于:  entry/src/main/cpp/
回溯 ../../../:       entry/
进入 libs/${OHOS_ARCH}/: entry/libs/arm64-v8a/
最终:                 entry/libs/arm64-v8a/libmediainfo.so

${OHOS_ARCH} 是 CMake 变量,值为 arm64-v8ax86_64 等,由构建系统自动设置。

6.1 注意事项

HAP 应用中 SO 文件不需要单独签名,构建系统会自动对整个 HAP 包签名。


7. Step 4: 配置构建选项

entry/build-profile.json5 中添加 externalNativeOptions

{
  "apiType": "stageMode",
  "buildOption": {
    "externalNativeOptions": {
      "path": "./src/main/cpp/CMakeLists.txt",
      "arguments": "",
      "cppFlags": ""
    }
  },
  "targets": [
    { "name": "default" },
    { "name": "ohosTest" }
  ]
}

这告诉构建系统去执行 CMake 构建,生成我们的 NAPI 桥接 SO。


8. Step 5: 编写类型声明

ArkTS 是强类型语言,必须为 NAPI 模块提供 TypeScript 类型声明文件。

8.1 index.d.ts

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

export const getMediaInfo: (filePath: string) => string;
export const getMediaInfoByField: (filePath: string, streamKind: number, streamNumber: number, field: string) => string;

8.2 oh-package.json5

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

{
  "name": "libmediainfo_napi.so",    // ⚠️ 必须加 .so 后缀!且与 nm_modname 对应
  "version": "1.0.0",
  "main": "index.d.ts",
  "types": "index.d.ts"
}

踩坑记录oh-package.json5 中的 name 必须是 libmediainfo_napi.so 格式(带 .so 后缀),而不是 mediainfo_napi。否则 ArkTS 编译器找不到模块!


9. Step 6: 配置模块依赖

entry/oh-package.json5 中声明依赖:

{
  "name": "entry",
  "version": "1.0.0",
  "dependencies": {
    "libmediainfo_napi.so": "file:src/main/cpp/types/mediainfo_napi"   // ⚠️ key也必须带.so后缀
  }
}

然后必须执行

cd entry/
ohpm install

这会在 entry/oh_modules/libmediainfo_napi.so/ 目录下生成符号链接,ArkTS 编译器才能找到类型声明。

Windows 下 symlink 权限问题

在 Windows 上 ohpm install 可能报 EPERM: operation not permitted, symlink 错误。解决方案:

  1. 以管理员权限运行 DevEco Studio 或终端
  2. 或者手动创建目录
    entry/oh_modules/libmediainfo_napi.so/
    └── index.d.ts   ← 从 types/mediainfo_napi/index.d.ts 复制过来
    

10. Step 7: ArkTS 侧调用

10.1 导入模块

import libmediainfo from 'libmediainfo_napi.so';

注意 import 名必须与 lib + nm_modname + .so 一致。

10.2 选择文件并调用

由于 HarmonyOS 的安全机制,Native 侧无法直接访问用户通过 Picker 选择的文件。需要先将文件拷贝到应用沙箱,再传沙箱路径给 Native:

import libmediainfo from 'libmediainfo_napi.so';
import { picker } from '@kit.CoreFileKit';
import { fileIo as fs } from '@kit.CoreFileKit';
import { common } from '@kit.AbilityKit';
import { hilog } from '@kit.PerformanceAnalysisKit';

@Entry
@Component
struct Index {
  @State mediaInfoText: string = 'Click button to select a media file';
  @State selectedFilePath: string = '';
  @State isLoading: boolean = false;

  private context: common.UIAbilityContext =
    this.getUIContext().getHostContext() as common.UIAbilityContext;

  private async pickMediaFile() {
    try {
      // 1. 配置文件选择器
      let documentSelectOptions = new picker.DocumentSelectOptions();
      documentSelectOptions.maxSelectNumber = 1;
      let documentPicker = new picker.DocumentViewPicker(this.context);

      // 2. 拉起文件选择
      let uris: Array<string> = await documentPicker.select(documentSelectOptions);
      if (uris && uris.length > 0) {
        // 3. 拷贝到沙箱(Native侧只能访问沙箱路径)
        let file = fs.openSync(uris[0], fs.OpenMode.READ_ONLY);
        let destPath = `${this.context.filesDir}/${file.name}`;
        fs.copyFileSync(file.fd, destPath);
        fs.closeSync(file);

        this.selectedFilePath = destPath;
        this.analyzeFile(destPath);
      }
    } catch (err) {
      hilog.error(0x0000, 'Demo', 'pick failed: %{public}s', JSON.stringify(err));
    }
  }

  private analyzeFile(filePath: string) {
    this.isLoading = true;
    try {
      // 4. 调用NAPI函数(底层调用libmediainfo的C接口)
      let result = libmediainfo.getMediaInfo(filePath);
      this.mediaInfoText = result || 'No info returned';
    } catch (err) {
      this.mediaInfoText = `Error: ${JSON.stringify(err)}`;
    }
    this.isLoading = false;
  }

  build() {
    Column() {
      Button(this.isLoading ? 'Analyzing...' : 'Select Media File')
        .onClick(() => this.pickMediaFile())

      Scroll() {
        Text(this.mediaInfoText)
          .fontFamily('monospace')
          .fontSize(13)
      }
      .layoutWeight(1)
    }
    .width('100%').height('100%')
  }
}

10.3 调用流程图

用户点击"选择文件"
      ↓
ArkTS: picker.DocumentViewPicker.select()
      ↓
获取URI → fs.copyFileSync() 拷贝到沙箱
      ↓
ArkTS: libmediainfo.getMediaInfo(sandboxPath)
      ↓
NAPI: GetMediaInfo()
  ├── napi_get_value_string_utf8() → 获取文件路径
  ├── MediaInfo_New()              → 创建实例
  ├── MediaInfo_Option()           → 初始化库(设置版本)
  ├── MediaInfo_Open(handle, path) → 打开文件
  ├── MediaInfo_Inform(handle, 0)  → 获取信息
  ├── MediaInfo_Close(handle)      → 关闭文件
  ├── MediaInfo_Delete(handle)     → 释放实例
  └── napi_create_string_utf8()    → 返回结果
      ↓
ArkTS: 显示结果文本

11. 完整代码

mediainfo_napi.cpp

#include <cstdio>
#include <cstring>
#include <string>
#include "hilog/log.h"
#include "napi/native_api.h"

#undef LOG_DOMAIN
#undef LOG_TAG
#define LOG_DOMAIN 0x0000
#define LOG_TAG "LibMediaInfoDemo"

extern "C" {
    void *MediaInfo_New();
    void MediaInfo_Delete(void *handle);
    size_t MediaInfo_Open(void *handle, const char *file);
    void MediaInfo_Close(void *handle);
    const char *MediaInfo_Inform(void *handle, size_t reserved);
    const char *MediaInfo_Option(void *handle, const char *option, const char *value);
    // Buffer API
    size_t MediaInfo_Open_Buffer_Init(void *handle, unsigned long long fileSize, unsigned long long fileSizeFinal);
    size_t MediaInfo_Open_Buffer_Continue(void *handle, const unsigned char *buffer, size_t bufferSize);
    size_t MediaInfo_Open_Buffer_Finalize(void *handle);
}

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

    // 1. 安全校验:确保有参数且必须是字符串
    if (argc < 1) {
        napi_throw_type_error(env, nullptr, "Requires 1 argument");
        return nullptr;
    }
    napi_valuetype valuetype;
    napi_typeof(env, args[0], &valuetype);
    if (valuetype != napi_string) {
        napi_throw_type_error(env, nullptr, "Argument must be a string");
        return nullptr;
    }

    size_t filePathLen = 0;
    napi_get_value_string_utf8(env, args[0], nullptr, 0, &filePathLen);
    char *filePath = new char[filePathLen + 1];
    napi_get_value_string_utf8(env, args[0], filePath, filePathLen + 1, &filePathLen);

    napi_value result = nullptr;
    std::string debugInfo;

    // 2. 在 NAPI 层通过 fopen 打开并读取文件内容到内存
    FILE *fp = fopen(filePath, "rb");
    if (fp == nullptr) {
        debugInfo = "ERROR: Cannot open file via fopen: " + std::string(filePath);
        napi_create_string_utf8(env, debugInfo.c_str(), debugInfo.length(), &result);
        delete[] filePath;
        return result;
    }
    
    // 获取文件大小
    fseek(fp, 0, SEEK_END);
    long fileSize = ftell(fp);
    fseek(fp, 0, SEEK_SET); // 关键:移回文件开头准备读取
    
    debugInfo = "File: " + std::string(filePath) + "\n";
    debugInfo += "Size: " + std::to_string(fileSize) + " bytes\n\n";

    // 申请内存并将文件读入 Buffer
    char *fileBuffer = new (std::nothrow) char[fileSize];
    if (fileBuffer == nullptr) {
        debugInfo += "ERROR: Memory allocation failed for file buffer";
        napi_create_string_utf8(env, debugInfo.c_str(), debugInfo.length(), &result);
        fclose(fp);
        delete[] filePath;
        return result;
    }

    size_t bytesRead = fread(fileBuffer, 1, fileSize, fp);
    fclose(fp); // 读取完毕,提前关闭文件句柄

    if (bytesRead != (size_t)fileSize) {
        debugInfo += "ERROR: Failed to read complete file into memory";
        napi_create_string_utf8(env, debugInfo.c_str(), debugInfo.length(), &result);
        delete[] fileBuffer;
        delete[] filePath;
        return result;
    }

    // 3. 初始化 MediaInfo 实例
    void *handle = MediaInfo_New();
    if (handle == nullptr) {
        debugInfo += "ERROR: MediaInfo_New() failed";
        napi_create_string_utf8(env, debugInfo.c_str(), debugInfo.length(), &result);
        delete[] fileBuffer;
        delete[] filePath;
        return result;
    }

    // 基础配置
    MediaInfo_Option(handle, "CharSet", "UTF-8");
    MediaInfo_Option(handle, "Internet", "No");

    // 获取版本号
    const char *versionInfo = MediaInfo_Option(handle, "Info_Version", "");
    debugInfo += "MediaInfo Library Version: " + std::string((versionInfo != nullptr && strlen(versionInfo) > 0) ? versionInfo : "Unknown") + "\n\n";
    
    debugInfo += "=== Calling MediaInfo Buffer API ===\n";

    // 4. 【核心改动】使用 MediaInfo 的 Buffer 专属三步走 API
    // 第一步:初始化 Buffer 模块 (传入文件预期大小,后一个参数通常传 0)
    size_t initResult = MediaInfo_Open_Buffer_Init(handle, (unsigned long long)fileSize, 0);
    
    // 第二步:将内存中的文件流送给 MediaInfo 解析
    // 这一步会返回一个比特位状态(通常 >0 表示库还需要更多数据,0 表示解析完毕或终止)
    size_t continueResult = MediaInfo_Open_Buffer_Continue(handle, (const unsigned char*)fileBuffer, bytesRead);
    
    // 第三步:通知 MediaInfo 缓冲区流已结束,要求其固化解析结果
    size_t finalizeResult = MediaInfo_Open_Buffer_Finalize(handle);

    debugInfo += "Buffer_Init result: " + std::to_string(initResult) + "\n";
    debugInfo += "Buffer_Continue result: " + std::to_string(continueResult) + "\n";
    debugInfo += "Buffer_Finalize result: " + std::to_string(finalizeResult) + "\n\n";

    // 5. 照常提取结构化文本信息
    // 用宽字符指针去接
    const wchar_t *informResultW = (const wchar_t*)MediaInfo_Inform(handle, 0);
    
    // 将宽字符转为标准的 std::string (UTF-8) 
    std::string infoStr = "";
    if (informResultW != nullptr) {
        // 借助标准库或者使用系统的转换方法将 wchar_t* 转成普通 char* 的 std::string
        std::wstring wstr(informResultW);
        infoStr = std::string(wstr.begin(), wstr.end()); // 仅适用于纯 ASCII,若有中文需用 wstring_convert
    }

    // 释放 MediaInfo 资源
    MediaInfo_Close(handle);
    MediaInfo_Delete(handle);

    // 6. 组装结果并清理本地临时内存
    if (infoStr.empty()) {
        debugInfo += "WARNING: Inform returned empty string. (Does your .so library include this format parser?)\n";
    } else {
        debugInfo += "=== SUCCESS ===\n\n";
        debugInfo += infoStr;
    }

    napi_create_string_utf8(env, debugInfo.c_str(), debugInfo.length(), &result);
    
    // 必须释放动态申请的内存,严防内存泄漏
    delete[] fileBuffer; 
    delete[] filePath;
    
    return result;
}

EXTERN_C_START
static napi_value Init(napi_env env, napi_value exports)
{
    napi_property_descriptor desc[] = {
        {"getMediaInfo", nullptr, GetMediaInfo,
         nullptr, nullptr, nullptr, napi_default, nullptr},
    };
    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 = "mediainfo_napi",
    .nm_priv = ((void *)0),
    .reserved = {0},
};

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

CMakeLists.txt

cmake_minimum_required(VERSION 3.4.1)
project(LibMediaInfoDemo)

set(NATIVERENDER_ROOT_PATH ${CMAKE_CURRENT_SOURCE_DIR})
if(DEFINED PACKAGE_FIND_FILE)
    include(${PACKAGE_FIND_FILE})
endif()

add_library(mediainfo_napi SHARED mediainfo_napi.cpp)

target_include_directories(mediainfo_napi PRIVATE
    ${CMAKE_CURRENT_SOURCE_DIR}/include
    ${CMAKE_CURRENT_SOURCE_DIR}/include/MediaInfo
    ${CMAKE_CURRENT_SOURCE_DIR}/include/MediaInfoDLL
)

target_link_libraries(mediainfo_napi PUBLIC
    ${NATIVERENDER_ROOT_PATH}/../../../libs/${OHOS_ARCH}/libmediainfo.so
    ace_napi.z
    hilog_ndk.z
    dl
)

index.d.ts

export const getMediaInfo: (filePath: string) => string;

真机运行效果截图:

在这里插入图片描述

12. 常见问题与踩坑记录

踩坑1: “Cannot find module ‘xxx’ or its corresponding type declarations”

场景:ArkTS 编译期报错,找不到 NAPI 模块。

根因:oh-package.json5 中的依赖配置不正确,或 ohpm install 未执行/失败。

解决

  1. 检查 entry/oh-package.json5 中 dependencies 的 key 是否为 xxx.so 格式
  2. 检查 types 目录下的 oh-package.json5 的 name 是否也为 xxx.so
  3. 执行 ohpm install 并确认 entry/oh_modules/xxx.so/index.d.ts 存在
  4. Windows 下如果 symlink 失败,手动复制 index.d.ts 到 oh_modules

踩坑2: C++编译 “use of undeclared identifier ‘LOG_APP’”

场景:使用 OH_LOG_ERROR(LOG_APP, ...) 编译报错。

根因:hilog 头文件中 LOG_APP 的定义在某些 SDK 版本中需要特定的 include 路径。

解决

#include "hilog/log.h"       // 确保引入此头文件
#undef LOG_DOMAIN
#undef LOG_TAG
#define LOG_DOMAIN 0x0000
#define LOG_TAG "YourTag"

踩坑3: 运行时 “dlopen failed” 或 SO 加载失败

场景:编译成功,但运行时加载 SO 库失败。

根因:三方 SO 库有未满足的运行时依赖。

解决

  1. 用 NDK 工具查看依赖:
    llvm-readelf -d libmediainfo.so | grep NEEDED
    
  2. 如果输出包含 libzen.solibz.so 等非系统库,需要将它们也放入 entry/libs/arm64-v8a/

踩坑4: 运行时 “xxx is not callable”

场景:调用 NAPI 导出的函数时报 TypeError。

根因nm_modname 冲突。多个 SO 库使用了相同的 nm_modname,系统只加载了第一个。

解决:确保 nm_modname 全局唯一,建议用包名格式如 com.vendor.libname

踩坑5: Windows 下 ohpm install symlink EPERM

场景:执行 ohpm install 时报权限错误。

根因:Windows 创建符号链接需要管理员权限或开发者模式。

解决

  • 方案1:以管理员身份运行 DevEco Studio/终端
  • 方案2:手动将 types/xxx/index.d.ts 复制到 entry/oh_modules/xxx.so/index.d.ts

踩坑6: 文件选择后传路径给Native,Native打不开文件

场景:通过文件选择器获取路径后传给 Native,fopen() 返回 NULL。

根因:Picker 返回的是 URI(如 file://...),不是文件系统路径。Native 侧只能访问沙箱路径。

解决:先将文件拷贝到沙箱,再传沙箱路径:

let file = fs.openSync(uri, fs.OpenMode.READ_ONLY);
let destPath = `${context.filesDir}/${file.name}`;
fs.copyFileSync(file.fd, destPath);
fs.closeSync(file);
// 传 destPath 给 Native

踩坑7: ArkTS “Use explicit types instead of ‘any’, ‘unknown’”

场景:ArkTS 编译报 arkts-no-any-unknown 错误。

根因:ArkTS 禁止使用 anyunknown 类型。

解决:为所有变量指定明确类型,catch 块中不要标注类型:

// 错误
catch (err: any) { ... }

// 正确
catch (err) { ... }

踩坑8: 真机运行 “Load native module failed”

场景:编译成功,模拟器正常,真机运行报错。

根因:通常是 SO 库依赖缺失或架构不匹配,而非签名问题。

解决

  1. 检查 SO 库的所有依赖是否都已放入 entry/libs/arm64-v8a/
  2. 检查 SO 库架构是否与真机匹配(arm64-v8a)
  3. llvm-readelf -d xxx.so | grep NEEDED 查看依赖

注意:HAP 应用中 SO 文件不需要单独签名,HAP 整体包签名会覆盖内部所有文件。

踩坑9: nm_modname 与三方库名冲突

场景:三方库名为 libmediainfo.so,NAPI 模块 nm_modname = "libmediainfo",导致符号冲突。

根因:NAPI 模块名与三方库名相同,导致链接时符号冲突。

解决:使用不同的名字,如 nm_modname = "mediainfo_napi"


13. SO库调试经验

13.1 调试流程总览

编译成功 → 运行失败
    ↓
1. 检查 nm_modname 是否正确
    ↓
2. 检查 import 语句是否与 nm_modname 对应
    ↓
3. 检查 oh-package.json5 配置
    ↓
4. 检查 SO 文件是否签名(真机)
    ↓
5. 检查 SO 依赖是否完整
    ↓
6. 检查三方库是否正确初始化
    ↓
7. 添加详细日志定位问题

13.2 检查 SO 库依赖

使用 NDK 提供的 llvm-readelf 工具:

# 查看 SO 库依赖
llvm-readelf -d libmediainfo.so | grep NEEDED

# 输出示例:
# 0x0000000000000001 (NEEDED)   Shared library: [libzen.so]
# 0x0000000000000001 (NEEDED)   Shared library: [libz.so]
# 0x0000000000000001 (NEEDED)   Shared library: [libtinyxml2.so]
# 0x0000000000000001 (NEEDED)   Shared library: [libc++_shared.so]

将所有非系统库放入 entry/libs/arm64-v8a/ 目录。

13.3 检查 SO 库架构

# 查看 SO 库架构
llvm-readelf -h libmediainfo.so | grep Machine

# 输出示例:
# Machine:                           AArch64

确保与目标设备架构一致(真机通常是 AArch64,模拟器可能是 x86_64)。

13.4 SO 文件签名说明

HAP 应用中 SO 文件不需要单独签名!

场景 签名要求 说明
HAP 应用开发 不需要单独签名 HAP 整体包签名会覆盖内部所有 .so 文件
系统级原生工具开发 必须单独签名 可执行文件、核心 .so 库都直接暴露给内核,必须用 binary-sign-tool 签名

本文是 HAP 应用开发场景,构建系统会自动对整个 HAP 包签名。

13.5 添加调试日志

在 NAPI 桥接层添加详细日志:

#include "hilog/log.h"

#undef LOG_DOMAIN
#undef LOG_TAG
#define LOG_DOMAIN 0x0000
#define LOG_TAG "NativeDebug"

static napi_value GetMediaInfo(napi_env env, napi_callback_info info)
{
    OH_LOG_INFO(LOG_APP, "=== GetMediaInfo called ===");

    // 解析参数
    size_t filePathLen = 0;
    napi_get_value_string_utf8(env, args[0], nullptr, 0, &filePathLen);
    char *filePath = new char[filePathLen + 1];
    napi_get_value_string_utf8(env, args[0], filePath, filePathLen + 1, &filePathLen);

    OH_LOG_INFO(LOG_APP, "File path: %{public}s", filePath);

    // 检查文件是否存在
    FILE *fp = fopen(filePath, "rb");
    if (fp == nullptr) {
        OH_LOG_ERROR(LOG_APP, "ERROR: Cannot open file!");
        // ...
    }
    fseek(fp, 0, SEEK_END);
    long fileSize = ftell(fp);
    fclose(fp);
    OH_LOG_INFO(LOG_APP, "File size: %{public}ld bytes", fileSize);

    // 调用三方库
    void *handle = MediaInfo_New();
    OH_LOG_INFO(LOG_APP, "MediaInfo_New: %{public}p", handle);

    const char *version = MediaInfo_Option(handle, "Info_Version", "0.7.0.0;MyApp;1.0.0");
    OH_LOG_INFO(LOG_APP, "MediaInfo Version: %{public}s", version != nullptr ? version : "null");

    size_t openResult = MediaInfo_Open(handle, filePath);
    OH_LOG_INFO(LOG_APP, "MediaInfo_Open result: %{public}zu", openResult);

    // ...
}

13.6 分步测试策略

当遇到问题时,采用分步测试策略:

// 测试1:NAPI 模块是否加载成功
static napi_value TestNapi(napi_env env, napi_callback_info info)
{
    napi_value result;
    napi_create_string_utf8(env, "NAPI module loaded successfully!", NAPI_AUTO_LENGTH, &result);
    return result;
}

// 测试2:三方库是否可以创建实例
static napi_value TestMediaInfo(napi_env env, napi_callback_info info)
{
    void *handle = MediaInfo_New();
    std::string msg = (handle != nullptr) ? "MediaInfo_New OK" : "MediaInfo_New FAILED";
    
    if (handle) {
        const char *version = MediaInfo_Option(handle, "Info_Version", "0.7.0.0;MyApp;1.0.0");
        msg += "\nVersion: " + std::string(version != nullptr ? version : "null");
        MediaInfo_Delete(handle);
    }
    
    napi_value result;
    napi_create_string_utf8(env, msg.c_str(), msg.length(), &result);
    return result;
}

// 测试3:三方库是否可以打开文件
static napi_value TestOpenFile(napi_env env, napi_callback_info info)
{
    // ... 完整测试逻辑
}

在 ArkTS 侧添加测试按钮:

Column() {
  Button('Test 1: NAPI Module')
    .onClick(() => {
      let result = libmediainfo.testNapi();
      this.resultText = result;
    })

  Button('Test 2: MediaInfo Init')
    .onClick(() => {
      let result = libmediainfo.testMediaInfo();
      this.resultText = result;
    })

  Button('Test 3: Open File')
    .onClick(() => {
      // ...
    })
}

13.7 常见错误诊断

错误信息 可能原因 解决方案
Load native module failed SO 未签名或签名错误 签名所有 SO 文件
dlopen failed: library not found SO 文件未打包 检查 entry/libs/ 目录
dlopen failed: symbol not found 依赖库缺失 llvm-readelf -d 检查依赖
xxx is not callable nm_modname 冲突 使用唯一的 nm_modname
Cannot find module 类型声明缺失 检查 oh-package.json5 配置

13.8 三方库初始化问题

某些三方库需要正确的初始化才能工作。以 MediaInfo 为例:

// ❌ 错误:未设置版本信息
void *handle = MediaInfo_New();
MediaInfo_Option(handle, "CharSet", "UTF-8");  // 只设置字符集
MediaInfo_Open(handle, filePath);  // 可能失败

// ✅ 正确:设置完整的初始化参数
void *handle = MediaInfo_New();
MediaInfo_Option(handle, "Info_Version", "0.7.0.0;MyApp;1.0.0");  // 必须设置!
MediaInfo_Option(handle, "CharSet", "UTF-8");
MediaInfo_Option(handle, "Internet", "No");
MediaInfo_Open(handle, filePath);  // 正常工作

验证方法:调用 MediaInfo_Option(handle, "Info_Version", ...) 后检查返回值:

  • 正常:返回类似 MediaInfoLib - v23.10 的版本字符串
  • 异常:返回空字符串或异常字符(如 O),说明库有问题

14. 进阶: dlopen 方式

当直接链接不可行时(如 SO 库符号冲突、需运行时决定加载),可使用 dlopen 方式:

Native 侧代码

#include <dlfcn.h>

typedef void* (*MediaInfo_New_Fn)();
typedef size_t (*MediaInfo_Open_Fn)(void*, const char*);
// ... 其他函数指针

static void* g_handle = nullptr;
static MediaInfo_New_Fn g_mediaInfo_New = nullptr;
// ... 其他全局指针

static bool LoadLibrary(const char* soPath) {
    g_handle = dlopen(soPath, RTLD_LAZY);
    if (!g_handle) return false;

    g_mediaInfo_New = (MediaInfo_New_Fn)dlsym(g_handle, "MediaInfo_New");
    // ... 加载其他符号
    return true;
}

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

    // 参数1: 文件路径  参数2: SO库沙箱路径
    // ... 解析参数 ...

    if (!LoadLibrary(soPath)) {
        // 返回错误信息
    }

    // 使用函数指针调用
    void* mi = g_mediaInfo_New();
    // ...
}

ArkTS 侧

let bundleCodeDir = this.getUIContext().getHostContext()!.bundleCodeDir;
let abiPath = deviceInfo.abiList === 'x86_64' ? 'x86_64' : 'arm64-v8a';
let soPath = `${bundleCodeDir}/libs/${abiPath}/libmediainfo.so`;

let result = libmediainfo.getMediaInfo(filePath, soPath);

dlopen 注意事项

  1. 只能加载 C 接口(extern "C" 导出的函数),C++ 名称修饰后的符号无法用 dlsym 获取
  2. 必须使用沙箱路径,不能使用真实文件系统路径
  3. 使用完毕后应调用 dlclose() 释放
  4. dlopen 具有命名空间隔离,只能加载应用包目录下的 SO

15. 总结

核心流程回顾

三方SO + 头文件
     ↓
放置到 entry/libs/ 和 entry/src/main/cpp/include/
     ↓
编写 NAPI 桥接层(extern "C" 声明 + napi 包装函数)
     ↓
CMakeLists.txt 链接三方SO + 配置 externalNativeOptions
     ↓
编写 index.d.ts 类型声明 + oh-package.json5(name必须带.so后缀)
     ↓
entry/oh-package.json5 添加依赖 + ohpm install
     ↓
ArkTS: import xxx from 'xxx.so' → 调用

关键要点

  1. nm_modname 一致性:C++ 的 nm_modname、CMake 的 add_library 目标名、ArkTS 的 import 'xxx.so' 三者必须对应
  2. nm_modname 唯一性:不要与三方库名相同,避免符号冲突
  3. 类型声明必须带 .so 后缀oh-package.json5 中的 name 和 dependencies key 都要带 .so
  4. 文件路径必须走沙箱:Native 侧只能访问应用沙箱路径,Picker 选择的文件要先拷贝
  5. 运行时依赖要齐全:用 llvm-readelf -d 检查三方 SO 的 NEEDED 列表
  6. 三方库正确初始化:某些库需要特定的初始化参数才能工作
  7. Windows 环境注意 symlink 权限:ohpm install 可能需要管理员权限

快速检查清单

  • SO 文件架构是否与目标设备匹配(arm64-v8a)
  • SO 文件是否放在 entry/libs/arm64-v8a/
  • SO 文件的所有依赖是否都已放入 libs 目录
  • 头文件是否放在 entry/src/main/cpp/include/
  • NAPI 桥接代码中 extern "C" 声明的函数名是否与 SO 导出符号一致
  • nm_modname 是否与 CMake target 名和 import 名对应
  • nm_modname 是否与三方库名不同(避免冲突)
  • build-profile.json5 是否配置了 externalNativeOptions
  • types 目录下的 oh-package.json5 name 是否带 .so 后缀
  • entry/oh-package.json5 dependencies key 是否带 .so 后缀
  • 是否执行了 ohpm installoh_modules 下有对应类型声明
  • 是否检查了三方 SO 的运行时依赖
  • 是否正确初始化了三方库(如设置版本信息)

更多交流学习,欢迎加入开源鸿蒙PC社区https://harmonypc.csdn.net/

Logo

讨论HarmonyOS开发技术,专注于API与组件、DevEco Studio、测试、元服务和应用上架分发等。

更多推荐