在鸿蒙应用开发中,经常会遇到需要复用已有 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文件
│   ├── src/main/
│   │   ├── cpp/
│   │   │   ├── include/                ← ② 头文件
│   │   │   │   ├── MediaInfo/
│   │   │   │   │   ├── MediaInfo.h
│   │   │   │   │   └── MediaInfo_Const.h
│   │   │   │   └── MediaInfoDLL/
│   │   │   │       └── MediaInfoDLL.h
│   │   │   ├── types/
│   │   │   │   └── libmediainfo/       ← ③ 类型声明
│   │   │   │       ├── 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

如果 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)

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_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, "CharSet", "UTF-8");

    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函数: 按字段获取媒体信息
static napi_value GetMediaInfoByField(napi_env env, napi_callback_info info)
{
    size_t argc = 4;
    napi_value args[4] = {nullptr};
    napi_get_cb_info(env, info, &argc, args, nullptr, nullptr);

    // 解析: filePath, streamKind, streamNumber, field
    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);

    double streamKind = 0;
    napi_get_value_double(env, args[1], &streamKind);
    double streamNumber = 0;
    napi_get_value_double(env, args[2], &streamNumber);

    size_t fieldLen = 0;
    napi_get_value_string_utf8(env, args[3], nullptr, 0, &fieldLen);
    char *field = new char[fieldLen + 1];
    napi_get_value_string_utf8(env, args[3], field, fieldLen + 1, &fieldLen);

    napi_value result = nullptr;

    void *handle = MediaInfo_New();
    if (handle == nullptr) {
        napi_create_string_utf8(env, "MediaInfo_New() returned null",
                               NAPI_AUTO_LENGTH, &result);
        delete[] filePath;
        delete[] field;
        return result;
    }

    MediaInfo_Option(handle, "CharSet", "UTF-8");

    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;
        delete[] field;
        return result;
    }

    // streamKind: 0=General, 1=Video, 2=Audio, 3=Text
    const char *value = MediaInfo_Get(handle, (size_t)streamKind,
                                      (size_t)streamNumber, field, 1, 0);
    std::string valueStr = (value != nullptr) ? value : "";

    MediaInfo_Close(handle);
    MediaInfo_Delete(handle);

    napi_create_string_utf8(env, valueStr.c_str(), valueStr.length(), &result);
    delete[] filePath;
    delete[] field;
    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},
        {"getMediaInfoByField", nullptr, GetMediaInfoByField,
         nullptr, nullptr, nullptr, napi_default, nullptr},
    };
    napi_define_properties(env, exports, sizeof(desc) / sizeof(desc[0]), desc);
    return exports;
}
EXTERN_C_END

// ⚠️ nm_modname 必须与 import 时使用的名字对应
static napi_module demoModule = {
    .nm_version = 1,
    .nm_flags = 0,
    .nm_filename = nullptr,
    .nm_register_func = Init,
    .nm_modname = "libmediainfo",    // → 编译出 liblibmediainfo.so
                                     // → ArkTS import from 'libmediainfo.so'
    .nm_priv = ((void *)0),
    .reserved = {0},
};

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

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

这是最容易踩坑的地方。三者的命名必须保持对应:

C++ 侧 CMake 侧 ArkTS 侧
nm_modname = "libmediainfo" add_library(libmediainfo SHARED ...) import xxx from 'libmediainfo.so'

规律:

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

踩坑警告:如果 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(libmediainfo SHARED mediainfo_napi.cpp)

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

# 链接三方SO库和系统库
target_link_libraries(libmediainfo 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 等,由构建系统自动设置。


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/libmediainfo/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/libmediainfo/oh-package.json5

{
  "name": "libmediainfo.so",    // ⚠️ 必须加 .so 后缀!
  "version": "1.0.0",
  "main": "index.d.ts",
  "types": "index.d.ts"
}

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


9. Step 6: 配置模块依赖

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

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

然后必须执行

cd entry/
ohpm install

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

Windows 下 symlink 权限问题

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

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

10. Step 7: ArkTS 侧调用

10.1 导入模块

import libmediainfo from 'libmediainfo.so';

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

10.2 选择文件并调用

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

import libmediainfo from 'libmediainfo.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_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);
    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);
}

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);

    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;
    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, "CharSet", "UTF-8");
    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);

    napi_create_string_utf8(env, infoStr.c_str(), infoStr.length(), &result);
    delete[] filePath;
    return result;
}

static napi_value GetMediaInfoByField(napi_env env, napi_callback_info info)
{
    size_t argc = 4;
    napi_value args[4] = {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);

    double streamKind = 0;
    napi_get_value_double(env, args[1], &streamKind);
    double streamNumber = 0;
    napi_get_value_double(env, args[2], &streamNumber);

    size_t fieldLen = 0;
    napi_get_value_string_utf8(env, args[3], nullptr, 0, &fieldLen);
    char *field = new char[fieldLen + 1];
    napi_get_value_string_utf8(env, args[3], field, fieldLen + 1, &fieldLen);

    napi_value result = nullptr;
    void *handle = MediaInfo_New();
    if (handle == nullptr) {
        napi_create_string_utf8(env, "MediaInfo_New() returned null",
                               NAPI_AUTO_LENGTH, &result);
        delete[] filePath; delete[] field;
        return result;
    }

    MediaInfo_Option(handle, "CharSet", "UTF-8");
    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; delete[] field;
        return result;
    }

    const char *value = MediaInfo_Get(handle, (size_t)streamKind,
                                      (size_t)streamNumber, field, 1, 0);
    std::string valueStr = (value != nullptr) ? value : "";
    MediaInfo_Close(handle);
    MediaInfo_Delete(handle);

    napi_create_string_utf8(env, valueStr.c_str(), valueStr.length(), &result);
    delete[] filePath; delete[] field;
    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},
        {"getMediaInfoByField", nullptr, GetMediaInfoByField,
         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 = "libmediainfo",
    .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(libmediainfo SHARED mediainfo_napi.cpp)

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

target_link_libraries(libmediainfo 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;
export const getMediaInfoByField: (filePath: string, streamKind: number,
    streamNumber: number, field: 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) { ... }

13. 进阶: 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

14. 总结

核心流程回顾

三方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. 类型声明必须带 .so 后缀oh-package.json5 中的 name 和 dependencies key 都要带 .so
  3. 文件路径必须走沙箱:Native 侧只能访问应用沙箱路径,Picker 选择的文件要先拷贝
  4. 运行时依赖要齐全:用 llvm-readelf -d 检查三方 SO 的 NEEDED 列表
  5. Windows 环境注意 symlink 权限:ohpm install 可能需要管理员权限

快速检查清单

  • SO 文件架构是否与目标设备匹配(arm64-v8a)
  • SO 文件是否放在 entry/libs/arm64-v8a/
  • 头文件是否放在 entry/src/main/cpp/include/
  • NAPI 桥接代码中 extern "C" 声明的函数名是否与 SO 导出符号一致
  • nm_modname 是否与 CMake target 名和 import 名对应
  • 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

作为“人工智能6S店”的官方数字引擎,为AI开发者与企业提供一个覆盖软硬件全栈、一站式门户。

更多推荐