OBS Studio 的鸿蒙迁移进入插件加载阶段后,遇到一个很典型的问题:libobs 的插件目录扫描在应用沙箱里静默失效,日志里连“尝试加载”都没有。这不是某个插件坏掉,而是“扫目录发现插件”的传统做法与鸿蒙安全模型不兼容。本文基于 OBS Studio 32.2.2 的鸿蒙移植分支 qingkouwei/obs-studio@harmonyos-port,验证机为 HUAWEI MatePad Edge(PC/2in1 模式)、HarmonyOS 7.0.0.109、API 26,梳理两道门和可复用的适配方法。
一、现象:obs_load_all_modules 零日志
libobs 启动时由 obs_load_all_modules() 扫描插件目录,逐个 dlopen 并注册源/编码器/输出类型。桌面版 OBS 的采集、编码、滤镜、转场等内置插件都靠这一步进场。鸿蒙上这一步没有任何日志:不是“加载失败”,而是连尝试记录都没有。原因是模块扫描用 os_opendir 遍历插件目录下的 *.so,目录打不开就静默返回;它把“目录不存在”当正常情况,失败被吞掉。前提是 blog() 已接 hilog,否则连“零日志”都观察不到。
二、第一道门:SELinux 禁止 readdir bundle libs
在桥接层直接对插件目录 opendir() 复现,errno 为:
- EACCES (Permission denied)
复制代码
目录存在、路径正确,且是应用自己的 bundle libs 目录 /data/storage/el1/bundle/libs/arm64-v8a,但应用进程没有 readdir 权限。鸿蒙对随 HAP 分发的原生库目录设置了 SELinux 策略:这些 .so 是系统托管文件,应用可以把它们作为 NAPI 模块“用”,但不能列目录“翻”。工程含义很直接:任何依赖“扫目录发现插件”的框架,在鸿蒙上都要换发现机制。libobs 的 obs_find_modules2(目录扫描+读 .so 元数据)整条路走不通,不是调权限能解决,而是模型不兼容。
三、第二道门:linker namespace 拒绝绝对路径 dlopen
绕开扫描,硬编码绝对路径逐个 dlopen:
- dlopen("/data/storage/el1/bundle/libs/arm64-v8a/harmony-capture.so", ...);
- // → dlopen failed: check ns accessible failed
复制代码
仍然失败。应用进程的动态链接器运行在受限 linker namespace 中,只允许按搜索路径规则解析库名,不允许用任意绝对路径打开未声明的库文件,这是沙箱防“加载任意代码”的核心机制之一。关键点:两道门报错完全不同,EACCES 是文件系统层,check ns accessible failed 是链接器层。排查时不要把两次失败当同一个问题,否则会误以为“改了没用”。
四、正解:裸 soname + 显式清单
第二道门留了正门:namespace 的 permitted paths 包含 bundle libs 目录,前提是按库名(soname)请求,让链接器自己搜索。裸 soname 不触发路径审计,一次通过:
- dlopen("harmony-capture.so", RTLD_NOW); // 走 namespace 搜索路径,命中
复制代码
桥接层因此改成显式清单模式:
- static const char *const kPlugins[] = {
- "harmony-capture", "harmony-audio", "harmony-camera", "harmony-vcodec",
- "obs-ffmpeg", "obs-filters", "obs-outputs", "obs-transitions",
- "obs-x264", "rtmp-services", "image-source", "text-freetype2",
- };
- for (const char *name : kPlugins) {
- snprintf(bin, sizeof(bin), "%s.so", name);
- snprintf(data, sizeof(data), "%s/obs-plugins/%s", dataDir.c_str(), name);
- obs_module_t *mod = nullptr;
- int ret = obs_open_module(&mod, bin, data); // bin=裸soname, data=资源目录
- if (ret == MODULE_SUCCESS) {
- obs_init_module(mod);
- loaded++;
- }
- }
复制代码
结果 12 个插件全部加载:4 个鸿蒙原生(采集/音频/相机/编码)+ 8 个上游原生(obs-ffmpeg、obs-filters、obs-outputs、obs-transitions、obs-x264、rtmp-services、image-source、text-freetype2)。OBS 的插件生态在鸿蒙沙箱里以显式清单方式重生。
排查决策路径可照抄:插件加载静默失败→桥接层手动 opendir 复现→EACCES→判断 SELinux 禁 readdir→改绝对路径 dlopen→check ns accessible failed→判断 linker namespace 路径审计→改裸 soname dlopen→命中 permitted paths→加载成功。两类报错分层:EACCES=文件系统层,ns failed=链接器层。
五、账单:显式清单不是最优解
显式清单是沙箱约束下的可行解,不是工程最优解。可维护性:新增插件要改 C++ 数组,自动扫描没了;失败静默:清单里某个库加载失败,不会有“目录里明明有它”的对照提示;数据目录耦合:obs-plugins/<name> 资源目录仍依赖 rawfile 暂存约定,清单和暂存两处要同步。工程化收尾方向:把插件清单挪进 rawfile 的 manifest(数据驱动,不改代码),或在编译期从 CMake 插件列表生成 C++ 数组(单一事实源)。鸿蒙沙箱不会为应用网开一面,能改的是架构假设。
六、技术影响
这两道门不是鸿蒙 bug,而是安全模型的设计表达:bundle libs 不可 readdir,表示随包代码是系统托管资产,应用是租户不是房东;linker namespace 路径审计,表示代码加载必须走声明过的领地,杜绝任意路径注入。对“应用自带插件目录、运行时扫描加载”的传统桌面软件架构,这是根本性冲突。OBS 的解法(显式清单+裸 soname)可平移到 GStreamer、Qt 插件甚至 Lua C 扩展等同类场景:任何带插件体系的开源软件上鸿蒙,第一关都在这里。 |