踩坑记录运行时加载与部署阶段八大疑难杂症【开源鸿蒙PC三方库】

欢迎加入开源鸿蒙 PC 社区:https://harmonypc.csdn.net/

开源仓库地址:

包括 SONAME 不匹配、rpath 配置、强制签名、动态库权限异常、OpenMP 运行时缺失、libc++ 版本兼容等等。

在这里插入图片描述

坑 1:编译通过,运行时 Cannot read property xxx of undefined

现象

DevEco Studio 里集成了一个三方 .so,编译阶段一切正常,Build 无报错。但部署到真机后,ArkTS 层调用 native 方法时报:

TypeError: Cannot read property xxx of undefined

通过 hilog 看日志,实际错误是:

dlopen failed: library "libplacebo.so.362" not found

根因

.so 文件内部有一个 SONAME 字段,是动态链接器(dlopen)用来识别它的「内部名字」。如果文件名是 libplacebo.so,但 SONAME 是 libplacebo.so.362,运行时 dlopen 会按 SONAME 去找 libplacebo.so.362——而这个名字的文件不存在,于是加载失败。前端因为没有拿到 native 模块,返回 undefined。

排查

llvm-readelf -d 检查 SONAME:

# DevEco SDK 自带的
llvm-readelf -d libplacebo.so | grep SONAME
# 输出类似:Library soname: [libplacebo.so.362]

解决

用 Python 在二进制层面把 SONAME 字符串替换掉,必须保持字节长度不变——用 \x00 补齐空缺:

# SONAME 从 libplacebo.so.362(18字节)改成 libplacebo.so(13字节)+ 5个\x00
data = open('libplacebo.so', 'rb').read()
data = data.replace(b'libplacebo.so.362\x00', b'libplacebo.so\x00\x00\x00\x00\x00')
open('libplacebo.so', 'wb').write(data)

改完再用 llvm-readelf -d 确认 SONAME 已变。

如果不想改二进制,也可以把文件名改成 SONAME 对应的名字——把 libplacebo.so 重命名成 libplacebo.so.362。但这样下游 CMake 工程里 find_library 可能找不到,我一般选前者。


坑 2:运行时 cannot open shared object file,但文件明明在

现象

产物在设备上部署后,运行时报找不到 .so,但你去目录下 ls,文件分明在。设了 LD_LIBRARY_PATH 也不管用。

根因

两层原因可能叠加:

第一层LD_LIBRARY_PATH 在鸿蒙上经常不生效——这和鸿蒙的文件系统权限策略有关。有些场景下程序加载器不会读这个环境变量。

第二层:即使 LD_LIBRARY_PATH 生效了,鸿蒙解压 tar 包后会把 .so 文件权限强制改成 770,动态链接器加载时因为权限不够失败。

解决

针对第一层:用 $ORIGIN rpath 替代 LD_LIBRARY_PATH。在构建时把 rpath 注入到二进制里:

# CMake: 
set(CMAKE_BUILD_RPATH "$ORIGIN")

# Meson:
-Dc_link_args="-Wl,-rpath,\$ORIGIN"

# 链接器直接传:
-Wl,-rpath,'$ORIGIN'

$ORIGIN 表示「可执行文件自己所在的目录」。注入后,程序会自动到自己的同级目录找依赖库,不依赖任何环境变量。部署时把程序和依赖 so 放一起就行。

针对第二层不要用鸿蒙系统的 tar 解压,用 Python 的 tarfile 模块代劳,解完顺便修复权限:

import tarfile, os

with tarfile.open("xxx.tar.gz") as t:
    t.extractall(".")

# 修复 .so 文件权限
for root, dirs, files in os.walk("."):
    for f in files:
        if f.endswith(".so") or ".so." in f:
            os.chmod(os.path.join(root, f), 0o755)

另外,可以把依赖 so 统一打包进 libexec/ 目录——这个目录在鸿蒙解压后能保持 755 权限,天然规避了 770 bug。


坑 3:鸿蒙强制签名 —— binary-sign-tool 这道坎

现象

产物拷到设备上,执行时报权限拒绝或直接无响应,甚至什么错误信息都没有。

根因

鸿蒙系统要求所有可执行文件和 .so 都必须经过 binary-sign-tool 签名。没签名的二进制,dlopen 直接拒绝加载,不会有友好提示。

解决

手动签:

binary-sign-tool sign -inFile my_binary -outFile my_binary -selfSign "1"
chmod +x my_binary

自动签(嵌入构建流程):在 CMakeLists.txt 里添加 POST_BUILD 步骤:

add_custom_command(TARGET my_target POST_BUILD
    COMMAND binary-sign-tool sign 
        -inFile $<TARGET_FILE:my_target> 
        -outFile $<TARGET_FILE:my_target> 
        -selfSign "1"
)

在 lycium 框架里,签名一般在 archive() 阶段执行,确保打包进 HNP 的产物已是签名状态。

一个很容易忽略的细节:不仅主程序要签,它依赖的每一个第三方 .so 都要签。漏掉任何一个,运行时就会静默失败。


坑 4:真机运行时报 Error loading shared library libomp.so

现象

G’MIC、TNN 这类开了 OpenMP 并行的库,产物在真机上运行时报:

Error loading shared library libomp.so: No such file or directory

根因

开启了 OpenMP 之后,产物运行时会动态依赖 libomp.so(OpenMP 运行时库)。鸿蒙系统默认不带这个库。

解决

两个选择,场景决定取舍:

选择 A:关掉 OpenMP(适合命令行批处理工具,G’MIC 就是典型)

# CMake 项目
-D ENABLE_OPENMP=OFF

# Meson 项目  
-Dopenmp=disabled

优势是产物体积更小、不引入额外运行时依赖。代价是损失多核加速。

选择 B:把 libomp 也交叉编译过来打包(适合推理框架,TNN 就是典型)

如果必须保留并行加速,就需要额外适配 libomp 库,编出鸿蒙 arm64 版本的 libomp.so,和主程序一起打包分发。工作量大一些,但对 AI 推理这类场景是值得的。

决策思路:先问「这个并行加速对我这个场景是不是刚需」。不是刚需就选 A,省时省力。是刚需再走 B。


坑 5:libsha.so: No such file —— 动态库在设备上找不到

现象

鸿蒙设备上运行一个依赖 libsha.so 的工具,报找不到 libsha.so,但你在 lib 目录下确认这个文件存在。

根因

这个坑和坑 2 不同——问题不在权限或 rpath,而在于动态链接的部署心智成本:带动态库部署时,除了主程序和 libsha.so 本体,还要带上它的软链接链(libsha.so → libsha.so.1 → libsha.so.1.0.0),以及正确设置 rpath 让加载器能找到它。

解决

对于体积小的库(如 SHA),全部静态链接,彻底消灭运行时依赖:

# CMakeLists.txt 里
add_executable(sha256sum sha256sum.c)
target_link_libraries(sha256sum PRIVATE sha_static)  # 链静态库

部署时一个文件带走,不需要任何 .so 陪伴。这是「小库」的最佳策略。

对于体积大的库(如 FFmpeg、TNN),必须动态链接时,参考坑 2 的 rpath + tarfile 方案。


坑 6:libmpv.so.2.5.0 软链接不生效

现象

TNN、mpv 这类带复杂版本号的 so,部署到鸿蒙设备后,运行时找不到对应版本号的文件。

根因

标准的部署套路是 libmpv.so → libmpv.so.2 → libmpv.so.2.5.0(三级软链接)。但鸿蒙系统的 tar 解压不会自动创建软链接——它看到的只是 libmpv.so.2.5.0 这一个真实文件,libmpv.solibmpv.so.2 这两级链接丢了。而运行时链接器按 soname(如 libmpv.so.2)去加载,找不到就失败。

解决

方法一:手动在设备上创建软链接(用 ln -s),适配部署脚本里多做一步。

方法二(更稳妥):直接把真实文件重命名为 soname 需要的名字:

# soname 是 libmpv.so.2,那就把文件命名成 libmpv.so.2
mv libmpv.so.2.5.0 libmpv.so.2

这比修 SONAME(坑 1)更简单——因为是部署层面的操作,不需要动 llvm-readelf


坑 7:strings libTNN.so | grep "0.1.0" 找不到版本号 —— 编出来的版本不对

现象

交叉编译完成,file 确认架构是 ARM aarch64,但在设备上功能异常或行为不对。用 strings 检查产物里的版本字符串,发现不是预期的版本。

根因

交叉编译时,版本信息通常是通过 CMake 变量(如 PROJECT_VERSION)或构建时生成的 config.h 注入的。如果构建脚本没有正确传递版本,或者 CMake 的版本信息来自 git describe 但在 git shallow clone 场景下不可用,产物里就会缺失或错误。

解决

三条校验链,确保版本正确:

# 1. file —— 查架构
file libTNN.so.0.1.0.0
# 期望:ELF 64-bit LSB shared object, ARM aarch64 ...

# 2. od —— 查 ELF 魔数
od -N 4 libTNN.so.0.1.0.0
# 期望开头:0000000 042577 043524(即 \x7fELF)

# 3. strings + grep —— 查版本字符串
strings libTNN.so.0.1.0.0 | grep -E "0\.[0-9]+\.[0-9]+"
# 期望能匹配到预期版本号

三招组合:架构对(file)、文件合法(od 魔数)、版本对(strings),缺一不可。这是我在 mpv、TNN 这类「不是直接能 --version 的可执行文件」上反复使用的验收方法。


坑 8:libc++ 版本差异 —— 交叉编译产物在真机上 std::ranges::copy 找不到

现象

C++20 项目(如 Crashpad)交叉编译通过,但真机运行时崩溃或部分功能异常。具体表现可能是调用某个使用了 std::ranges 的函数时崩溃。

根因

OHOS SDK 的 libc++ 版本,对 C++20 的部分 std::rangesconcept<=> 等特性支持不完整。编译阶段,编译器(Clang)认识这些语法,能通过语法检查;但链接时链的是 SDK 提供的 libc++.so,而它可能没有实现某些符号,或者行为有差异。

解决

策略 A:编译阶段就检测出来。在 CMake 里打开尽可能多的警告和错误检查:

if(OHOS)
    # 把 C++ 标准不兼容问题提前暴露在编译阶段
    target_compile_options(... PRIVATE -Wno-sign-compare)
endif()

策略 B:如果编译阶段漏过了,运行时出问题,回到源码把不兼容的用法替换为兼容写法:

// C++20 ranges(可能不被 OHOS libc++ 完全支持)
std::ranges::copy(source, dest);

// 改为 C++17 兼容写法
std::copy_n(source.begin(), source.size(), dest);

这种替换虽然看起来「退步」了,但保证了在所有受支持的 C++17 平台上都能正常工作——包括鸿蒙。

策略 C:升级 OHOS SDK 版本。新版本的 SDK 通常会对 libc++ 做更完善的 C++20 支持。但要注意 SDK 升级可能引入其他兼容性问题,值得在 CI 里自动化对比新旧版本的表现。


小结

这八个运行时坑,按根因可以归为四类:

类别 包含的坑 核心解法
动态库加载 坑 1(SONAME)、坑 2(rpath)、坑 5(动态依赖)、坑 6(软链接) llvm-readelf -d 体检 + $ORIGIN rpath + Python tarfile 解压
鸿蒙平台机制 坑 3(强制签名) 嵌入 POST_BUILD / archive 自动签
运行时库缺失 坑 4(libomp)、坑 8(libc++ 版本) 关掉或带包,分场景决策
产物校验 坑 7(版本不对) file + od + strings 三招验收

和上一篇编译阶段的坑对比,运行时坑的特点更「隐蔽」——很多问题没有明显的错误提示,表现为静默失败或部分功能异常。正因如此,建立一套**「编译通过后必须走真机验证 + file/od/strings 核对」**的习惯,比学会解决单个坑更重要。

基本覆盖了鸿蒙 PC 三方库适配从「开始编译」到「设备跑通」全链路的高频问题。如果你在某一步卡住了,先来这两篇里按序号对号入座,大概率能找到对应的根因和标准解法。

Logo

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

更多推荐