在 HarmonyOS/鸿蒙PC ArkTS 侧调用 C/C++ 三方库完整指南
在鸿蒙应用开发中,经常会遇到需要复用已有 C/C++ 三方库的场景。这些库可能是在 Linux/Android 平台上编译的,也可能专门为 HarmonyOS 编译。无论是哪种情况,ArkTS 代码都无法直接调用 C/C++ 函数——必须通过 NAPI(Node-API) 机制搭建一座桥梁。本文以 libmediainfo 为例,从零开始讲解如何在鸿蒙应用中集成和使用 C/C++ 三方动态库。
更多交流学习,欢迎加入开源鸿蒙PC社区:https://harmonypc.csdn.net/
欢迎在PC社区平台申请新建项目:https://atomgit.com/OpenHarmonyPCDeveloper
文章目录
-
- @[toc]
- 1. 背景与概述
-
- 2. 两种集成方式对比
- 3. 项目结构设计
- 4. Step 1: 创建工程与放置文件
-
- 5. Step 2: 编写 NAPI 桥接层
-
- 6. Step 3: 配置 CMakeLists.txt
-
- 7. Step 4: 配置构建选项
- 8. Step 5: 编写类型声明
-
- 9. Step 6: 配置模块依赖
-
- 10. Step 7: ArkTS 侧调用
-
- 11. 完整代码
-
- 12. 常见问题与踩坑记录
-
- 踩坑1: "Cannot find module 'xxx' or its corresponding type declarations"
- 踩坑2: C++编译 "use of undeclared identifier 'LOG_APP'"
- 踩坑3: 运行时 "dlopen failed" 或 SO 加载失败
- 踩坑4: 运行时 "xxx is not callable"
- 踩坑5: Windows 下 ohpm install symlink EPERM
- 踩坑6: 文件选择后传路径给Native,Native打不开文件
- 踩坑7: ArkTS "Use explicit types instead of 'any', 'unknown'"
- 踩坑8: 真机运行 "Load native module failed"
- 踩坑9: nm_modname 与三方库名冲突
- 13. SO库调试经验
-
- 14. 进阶: dlopen 方式
-
- 15. 总结
-
文章目录
-
- @[toc]
- 1. 背景与概述
- 2. 两种集成方式对比
- 3. 项目结构设计
- 4. Step 1: 创建工程与放置文件
- 5. Step 2: 编写 NAPI 桥接层
- 6. Step 3: 配置 CMakeLists.txt
- 7. Step 4: 配置构建选项
- 8. Step 5: 编写类型声明
- 9. Step 6: 配置模块依赖
- 10. Step 7: ArkTS 侧调用
- 11. 完整代码
- 12. 常见问题与踩坑记录
-
- 踩坑1: "Cannot find module 'xxx' or its corresponding type declarations"
- 踩坑2: C++编译 "use of undeclared identifier 'LOG_APP'"
- 踩坑3: 运行时 "dlopen failed" 或 SO 加载失败
- 踩坑4: 运行时 "xxx is not callable"
- 踩坑5: Windows 下 ohpm install symlink EPERM
- 踩坑6: 文件选择后传路径给Native,Native打不开文件
- 踩坑7: ArkTS "Use explicit types instead of 'any', 'unknown'"
- 踩坑8: 真机运行 "Load native module failed"
- 踩坑9: nm_modname 与三方库名冲突
- 13. SO库调试经验
- 14. 进阶: dlopen 方式
- 15. 总结
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()),你有两个选择:
- 用 dlopen + dlsym 方式加载(但同样只能调用 C 接口)
- 修改三方库源码,添加
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
踩坑警告:
- 不要让 NAPI 模块名与三方库名相同(如三方库叫
libmediainfo.so,NAPI 模块就叫mediainfo_napi)- 如果
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-v8a、x86_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 错误。解决方案:
- 以管理员权限运行 DevEco Studio 或终端
- 或者手动创建目录:
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 未执行/失败。
解决:
- 检查
entry/oh-package.json5中 dependencies 的 key 是否为xxx.so格式 - 检查 types 目录下的
oh-package.json5的 name 是否也为xxx.so - 执行
ohpm install并确认entry/oh_modules/xxx.so/index.d.ts存在 - 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 库有未满足的运行时依赖。
解决:
- 用 NDK 工具查看依赖:
llvm-readelf -d libmediainfo.so | grep NEEDED - 如果输出包含
libzen.so、libz.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 禁止使用 any 和 unknown 类型。
解决:为所有变量指定明确类型,catch 块中不要标注类型:
// 错误
catch (err: any) { ... }
// 正确
catch (err) { ... }
踩坑8: 真机运行 “Load native module failed”
场景:编译成功,模拟器正常,真机运行报错。
根因:通常是 SO 库依赖缺失或架构不匹配,而非签名问题。
解决:
- 检查 SO 库的所有依赖是否都已放入
entry/libs/arm64-v8a/ - 检查 SO 库架构是否与真机匹配(arm64-v8a)
- 用
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 注意事项
- 只能加载 C 接口(
extern "C"导出的函数),C++ 名称修饰后的符号无法用 dlsym 获取 - 必须使用沙箱路径,不能使用真实文件系统路径
- 使用完毕后应调用
dlclose()释放 - 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' → 调用
关键要点
- nm_modname 一致性:C++ 的
nm_modname、CMake 的add_library目标名、ArkTS 的import 'xxx.so'三者必须对应 - nm_modname 唯一性:不要与三方库名相同,避免符号冲突
- 类型声明必须带 .so 后缀:
oh-package.json5中的 name 和 dependencies key 都要带.so - 文件路径必须走沙箱:Native 侧只能访问应用沙箱路径,Picker 选择的文件要先拷贝
- 运行时依赖要齐全:用
llvm-readelf -d检查三方 SO 的 NEEDED 列表 - 三方库正确初始化:某些库需要特定的初始化参数才能工作
- 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.json5name 是否带.so后缀 -
entry/oh-package.json5dependencies key 是否带.so后缀 - 是否执行了
ohpm install且oh_modules下有对应类型声明 - 是否检查了三方 SO 的运行时依赖
- 是否正确初始化了三方库(如设置版本信息)
更多交流学习,欢迎加入开源鸿蒙PC社区:https://harmonypc.csdn.net/
更多推荐



所有评论(0)