在 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'"
- 13. 进阶: dlopen 方式
-
- 14. 总结
-
文章目录
-
- @[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'"
- 13. 进阶: dlopen 方式
- 14. 总结
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()),你有两个选择:
- 用 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, "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-v8a、x86_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 错误。解决方案:
- 以管理员权限运行 DevEco Studio 或终端
- 或者手动创建目录:
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 未执行/失败。
解决:
- 检查
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) { ... }
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 注意事项
- 只能加载 C 接口(
extern "C"导出的函数),C++ 名称修饰后的符号无法用 dlsym 获取 - 必须使用沙箱路径,不能使用真实文件系统路径
- 使用完毕后应调用
dlclose()释放 - 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' → 调用
关键要点
- nm_modname 一致性:C++ 的
nm_modname、CMake 的add_library目标名、ArkTS 的import 'xxx.so'三者必须对应 - 类型声明必须带 .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/ - 头文件是否放在
entry/src/main/cpp/include/ - NAPI 桥接代码中
extern "C"声明的函数名是否与 SO 导出符号一致 -
nm_modname是否与 CMake target 名和 import 名对应 -
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)