本技能讨论的是:如何产出一个 JVM 应用能够真正加载并随应用分发的原生共享库。嵌入与运行程序是两件不同的事:预构建的原生分发包是为独立进程准备的,而嵌入需要的是能在已映射宿主系统库的 JVM 进程内打开的库。多数预构建包做不到这一点,而且在系统已全局安装同名库的机器上,这种失败是不可见的——加载器会静默改用系统库。因此应在容器中自行构建产物,并让构建过程拒绝产生无法加载的切片。
陷阱一:预构建可移植包通常不是库。原因有两个,都可在投入一天之前快速核实。其一,文件名看似共享库的,往往就是播放器可执行文件本身,即位置无关可执行文件(PIE),Linux 加载器会直接拒绝将其作为库打开(这是按操作系统而异的事实:macOS 上同类文件可以被加载)。可用 readelf -h <file> | grep Type 与 readelf -d <file> | grep FLAGS_1 检查,出现 PIE 标志即为线索。其二,可移植包自带 C 运行时与程序加载器,这正是其可移植性的来源;但一个进程里不能有两套 C 运行时,JVM 已经映射了宿主的。readelf -d <file> | grep NEEDED 可看出它期望自带什么。
构建基线与系统库下限:在你打算支持的最老基础系统上构建,并从产物中读出下限。编译产物要求的最低系统库版本由编译它的机器决定,且这是一个下限:在较新的系统上构建,产物就无法在更老的系统上启动。不要相信构建文件中基础镜像的名称,该版本号会随镜像更新而与实际脱节。应读回实际值:objdump -T lib<name>.so.N | grep -o 'GLIBC_[0-9.]*' | sort -u -V | tail -1。并把该值对应到你所声称支持的发行版,与基础镜像记录在同一文件中,以便两者一起更新。
设置 RPATH 而非 RUNPATH:RUNPATH 只作用于携带它的对象,因此依赖的依赖无法通过它找到;RPATH 会沿整条依赖链继承,从而让一个条目覆盖整个闭包。顶层库指向子目录,同级库指向自身: patchelf --force-rpath --set-rpath '$ORIGIN/lib' "$OUT/lib<name>.so.N" for so in "$OUT"/lib/.so; do patchelf --force-rpath --set-rpath '$ORIGIN' "$so"; done 用 readelf -d <file> | grep -E 'RPATH|RUNPATH' 验证——某些 patchelf 版本默认写 RUNPATH,因此 --force-rpath 不可省略。
按名称有意排除基础系统库:把“ldd 打印的一切”都打进去是错误的闭包;再打包一份 C 运行时、数学库、C++ 运行时或程序加载器,正是当初排除可移植包的那个失败原因。应把排除列表显式写在复制循环附近,使向包中添加库成为有意的决定而非副作用。示例列表(改编自暂存脚本):SYSTEM_LIBS="libc.so.6 libm.so.6 libdl.so.2 libpthread.so.0 librt.so.1 libutil.so.1 ld-linux-x86-64.so.2 libgcc_s.so.1 libstdc++.so.6 libresolv.so.2 libz.so.1 libbz2.so.1.0 liblzma.so.5",配合 is_system() 判断函数。若宿主应用会独立使用某些广泛共享但并非严格基础系统的库,也应加入该列表——被打包副本抢占名称可能破坏宿主进程中无关的部分。
安装在加载器缓存之外的库会从闭包中静默消失。构建进本地前缀的库在裸镜像上不在缓存中,ldd 会报告“未找到”,复制循环跳过它,暂存切片看似完整却无法加载。在遍历依赖前注册前缀并刷新缓存:echo "/usr/local/lib" > /etc/ld.so.conf.d/local.conf 然后 ldconfig。暂存时用 cp -L 穿透符号链接——ldd 打印的条目通常是指向带版本文件的链接,背后空无一物的链接会在用户端造成加载失败。
裁剪嵌入路径永远无法到达的部分:如果通过软件渲染路径驱动引擎,整个 GPU/着色器栈就是不可达代码,却仍要被打包和签名;在配置阶段禁用它,可移除本处闭包中最大的单块——仅播放应用中禁用编码器同理。注意:禁用一个子系统会连带其选项(参见同级技能中的特性检测规则),而被禁用的可选音频滤镜必须是运行时代码能够在缺失时正常工作的。
在构建器内以“加载并初始化”作为门禁,两项检查任一失败即令构建失败。其一是解析检查:对每个暂存对象运行 ldd,任何“未找到”都失败。其二是用一个极小程序,以绝对路径、本地作用域打开暂存文件,解析实际调用的符号,并运行库的初始化: void *h = dlopen("/out/lib<name>.so.N", RTLD_NOW | RTLD_LOCAL); if (!h) { printf("dlopen FAILED: %s\n", dlerror()); return 1; } void *ctx = create(); // 改编:引擎自有的 create/init 组合 if (!ctx) { printf("create returned NULL\n"); return 1; } int rc = init(ctx); return rc == 0 ? 0 : 1; 在该应用实际会强制的数字区域设置状态下运行。JVM 启动时会采用用户区域设置(以空名称调用设置区域例程),因此嵌入应用必须在初始化按 C 区域解析数字的引擎之前,自行把数字类别强制回 C;冒烟测试同样设置 LC_NUMERIC=C,以便在与应用运行时保证一致的状态下检验产物。一个能解析一切却仍初始化失败的切片,正是只以“文件都在”为唯一门禁时会被发出去的东西。
验证方式(在含 mpv-natives/ 和 scripts/mpv-linux/stage.sh 的被审计仓库中运行):确认 patchelf --force-rpath 写出的确实是 RPATH 而非 RUNPATH——在副本上验证,绝不要动将要发布的文件: SO=mpv-natives/linux-x64/lib/libX11-xcb.so.1 # 你正在暂存的任意一个共享对象 cp "$SO" /tmp/rpath-test.so && chmod +w /tmp/rpath-test.so patchelf --force-rpath --set-rpath '$ORIGIN' /tmp/rpath-test.so objdump -p /tmp/rpath-test.so | grep -iE 'rpath|runpath' objdump -p mpv-natives//lib/.so* 2>/dev/null | grep RUNPATH 通过条件:副本打印出 RPATH $ORIGIN,绝不能是 RUNPATH;新暂存的目录树对第二条命令应无输出。 排除列表、本地前缀注册与加载并初始化门禁都是真实存在的具名代码,而非传闻,可用以下命令核对:grep -n "SYSTEM_LIBS=|is_system()|ld.so.conf.d|ldconfig|dlopen|mpv_initialize|LC_NUMERIC=C" scripts/mpv-linux/stage.sh。通过条件:SYSTEM_LIBS/is_system() 相邻出现;ld.so.conf.d/ldconfig 位于依赖遍历之前;冒烟测试在 LC_NUMERIC=C 下先调用 dlopen 再调用 mpv_initialize。
适用场景:需要把原生媒体引擎等库嵌入 JVM 桌面应用并随应用打包的构建流程,尤其是 Linux 与 macOS 交叉平台场景。限制:资料未提供该技能的具体安装方式、支持版本范围或授权与收费信息。