A
Air官方
【Air 系列 · 鸿蒙 PC 适配全记录】55 / 56
■ 应用:AirWord(from LibreOffice)(AirWord)
■ 定位:LibreOffice 移植,文字处理与文档编辑
■ 上游:LibreOffice 27.2.0 alpha0+(master) | 路线:Qt5 5.15+QPA(VCL) | 版本:1.3.0
■ 开源: https://gitcode.com/Andy88/Harmony-LibreOffice
■ 市场状态:已上架 AGC 应用页: https://appgallery.huawei.com/app/6917615885742616389
■ 官网(免费下载): https://www.shunidc.cn/
————————————————————
开源鸿蒙 PC 社区: https://harmonypc.csdn.net/
PC 社区项目申请: https://atomgit.com/OpenHarmonyPCDeveloper
适配开源地址:https://gitcode.com/Andy88/Harmony-LibreOffice
环境搭建文章:https://blog.csdn.net/weixin_52908342/article/details/161343743
▍一、为什么要适配 AirWord
办公套件是桌面生态的试金石。文字、表格、演示三类文档的编辑涉及排版引擎、UNO 组件模型、字体子系统、文件格式过滤器和一整套桌面交互范式,任何一层缺失都会让"能打开文档"退化为一句空话。AirWord 选择了这个品类里体量最大的上游:LibreOffice master 快照(27.2.0.0.alpha0+,约千万行 C++/UNO 代码),目标是让 Writer、Calc、Impress 三组件以原生桌面窗口的形态运行在鸿蒙 PC 上。
工程按 Air 系列双层架构组织:ArkTS 壳提供 Ability 生命周期、欢迎页、文档标签栏、菜单栏与输入法桥,LibreOffice 全套运行库(Qt5 栈、QPA 插件与两百余个组件动态库)进入 HAP,在一个专用线程里被进程内直调拉起。当前应用版本 1.1.0,包名 com.airword.app(原内部代号 AirOffice,v10 起独立品牌避免工程部署冲突),deviceTypes 覆盖 tablet、2in1、phone,目标设备从 MateBook Pro(2in1)、MatePad 系平板到 Pura X 折叠手机,ABI 为 arm64-v8a,要求 HarmonyOS 6.1.0(API 23)以上。
与浏览器或终端不同,LibreOffice 的移植难点不在某个单一模块,而在"整体性":UNO 桥、VCL 图形层、配置系统、字体链路、IPC 管道彼此咬合,任何一环在沙箱环境下的假设失效,症状都会以启动失败或静默黑屏的形式出现。这也是 AirWord 的工程手册以"先写映射再动手、所有条目附证据"为纪律的原因。
▍二、先划清边界:不把编译通过当成适配完成
LibreOffice 桌面版构建出的 soffice.bin 假定它可以 fork 子进程、mmap 可执行内存、读取固定安装路径、弹出自己的窗口系统。这些假设在鸿蒙应用沙箱里全部不成立。AirWord 把边界表写在工程手册第一节,明确哪些桌面机制被替换、哪些功能被显式禁用。
层次 LibreOffice 桌面版 鸿蒙侧当前方案
进程与入口 soffice.bin 独立进程 + fork/exec HAP 进程内 T_lo 专用线程直调 soffice_main()(OHOS 无 fork/exec)
GUI 后端 VCL + gtk3/win32 等平台插件 VCL qt5 插件 + Qt 5.15.16 + libqohos QPA(复用 AirKrita 权威副本)
渲染路径 Skia/OpenGL 可选加速 QtSvp + cairo 纯 raster 位图,--disable-skia --disable-opengl
窗口系统 X11/Wayland/Win32 顶层窗口 XComponent SURFACE/TEXTURE + eglSwapBuffers 上屏
UNO 桥 运行时生成 vtable thunk(依赖 exec 内存) 65536×64B 静态跳板表,零运行时代码生成(P35)
菜单系统 应用内 QMenuBar 250ms 快照转 JSON → ArkTS 系统 Menu(P38)
多文档形态 SDI 多顶层窗口 QOhos_SetMultiDocMode + WPS 式文档标签栏(P43/P50)
安装布局 固定前缀 /usr/lib/libreoffice rawfile 首启部署沙箱 + rc 内 $ORIGIN 宏重写为沙箱绝对路径
功能裁剪 全功能 禁用 java/python/数据库全家/avmedia/pdfium/openssl 等三十余项
边界表里最后一行常被忽略,却最诚实:AirWord 不假装带来完整的 LibreOffice。构建配置(native/autogen.input)里显式关闭的模块清单与交付的 Writer/Calc/Impress 编辑能力共同定义了产物范围。
▍三、鸿蒙版的整体架构
整套架构的要点是"壳薄核重":ArkTS 层只做系统能力接入与可视管理,NAPI 桥 libairofficebridge.so 只链 ace_napi/native_window/hilog 三个系统库;LibreOffice 全家在 native 侧按拓扑序 dlopen,Qt 事件循环与 LO 主循环合并进同一个 T_lo 线程。
text
AirWord HAP
├── ArkTS 壳 entry/src/main/ets
│ ├── EntryAbility:splash 遮罩 → 沉浸式 → rawfile 数据部署 → notifyDatafilesReady 门控
│ ├── Index.ets:XComponent(id=airoffice_xcomp)、欢迎层、文档标签栏、菜单栏、IME 桥
│ ├── DialogWindowManager / DialogPage:Qt 模态对话框的子窗承载与救援条
│ └── 文件 Picker 桥:documentViewPicker → uri 拷沙箱中转 → TSFN 回灌
│ │ NAPI:libairofficebridge.so(entry/src/main/cpp/napi_init.cpp)
│ ▼ OnSurfaceCreated → 预绑定 NativeWindow → 拓扑序 dlopen(RTLD_GLOBAL)
│ Qt5Core→Qt5Gui→Qt5Widgets→其余 Qt5 模块→libqohos(QPA)→LO 组件库(≈235 个 .so)
│ T_lo 线程:airoffice_core 启动 → env 注入 → soffice_main(argc,argv) 阻塞主循环
│ ├── SAL_USE_VCLPLUGIN=qt5 → VCL qt5 插件 → QApplication(T_lo 即 Qt 主线程)
│ ├── QPA=ohos:raster→GLES blit→eglSwapBuffers→XComponent
│ └── 输入:NAPI 回调 → QWindowSystemInterface::handleXxx 入队异步派发
└── entry/libs/arm64-v8a(模块级整目录通道,非 hvigor 闭包)
libQt5*.so ×12 + libqohos.so + libcairo + LO 组件库等共 235 个 .so
主要目录如下:
text
AirWord/
├── PORTING.md # 工程手册:架构、决策表、P1..P96c 补丁清单、风险登记册
├── native/
│ ├── env.sh # NDK / brew LLVM / Qt5 栈路径与编译参数组装
│ ├── autogen.input # LibreOffice configure 完整参数(含禁用清单)
│ ├── build_core.sh # patch rsync → autogen → host 工具 → 交叉编译 → install
│ ├── deploy_core.sh # make install → ELF 整备 → rc 重写 → rawfile 打包
│ ├── rewrite_rc.py # $ORIGIN/${LO_LIB_DIR} 等宏改写为沙箱绝对路径
│ ├── patches/core/ # P 系列补丁权威副本(0001-00xx patch 文件)
│ ├── patches/snapshot-v4-M0/ 等 # M0/M1 阶段源码树快照
│ ├── scripts/elf_edit.py # NEEDED 去版本号 / 64K 对齐整备
│ ├── toolchain/ # 工具链修复(如 diff 劫持问题的本地链接)
│ └── langpack/ + langpack_overlay/# zh-CN .mo×32 与汉化 overlay(41 文件)
└── entry/
├── libs/arm64-v8a/ # Qt5×12 + libqohos + LO 组件库,235 个 .so
└── src/main/
├── ets/
│ ├── pages/Index.ets # 欢迎层 / 标签栏 / 菜单栏 / IME 桥 / 方向轮换
│ ├── pages/DialogPage.ets # 对话框子窗宿主页
│ ├── utils/ # DialogWindowManager / WindowStageHolder
│ └── entryability/EntryAbility.ets
├── cpp/napi_init.cpp # libairofficebridge.so 薄壳(输入/IME/光标/窗口桥)
└── resources/rawfile/ # program(rc/rdb)、share/registry、fonts、presets、user 骨架
▍四、核心适配过程
### 1. 交叉编译体系:musl 伪装与双工具链接力
LibreOffice 的 configure 不认识 ohos 平台,工程让宿主 triplet 走 --host=aarch64-unknown-linux-musl 得到 _os=Linux(configure.ac 明确接受 linux-musl),真正的 ohos 目标只出现在 clang 参数里。编译器用 brew LLVM 22 的 clang——LO master 硬要求 Clang≥18,NDK 自带 15.0.4 不可行——而 libc++ 头与库仍取自 NDK sysroot 的 15 版,保证设备运行时 ABI 一致。交叉模式下的 host 侧工具(构建机自己的 ICU、gengal 等)由 configure 自动在 CONF-FOR-BUILD 目录跑 macOS 原生子 configure;qmake/moc/rrc 则直接复用 AirKrita 的 macOS 版 Qt 5.15.16 工具,实测可跑。外部依赖除 Qt5 外全部内编(icu 走 --with-cross-build,boost 走 gbuild StaticLibrary),唯一的同名冲突 harfbuzz 以静态链入消解。M0 阶段还有一个重要反转:--enable-mergelibs=yes 生成的 151MB 巨库在 musl 加载器重定位阶段 SEGV,工程改回独立库模式,每库符号表独立,代价约 +50MB 体积。
### 2. UNO 桥的零运行时代码生成改造
UNO 桥原先在运行时生成 vtable thunk,需要可写可执行内存。工程用 execprobe 在 MateBook Pro 上实测了完整的"通道全灭矩阵":匿名 RWX mmap、mprotect 升级、文件背书 RWX、memfd exec 映射、/proc/self/mem 改写已加载页全部被拒,linker 还会拒载 RWX PT_LOAD 段——HarmonyOS NEXT 起应用域运行时代码生成被系统级 W^X 硬禁。最终方案是 P35:把 vtable thunk 改为 65536×64B 的静态跳板表,条目代码由自身地址推导出 (vtableOffset, functionIndex) 后跳转共享的 vtableSlotCall,寄存器约定与原动态 snippet 完全一致;addLocalFunctions 只填静态表地址,代码块退化为纯 RW 数据,彻底免掉 mprotect。配套的 P34 则解决了 musl 无 __clear_cache 导出导致 flushCode 直调空指针的问题,改用 CTR_EL0 行宽加内联 dc cvau/ic ivau 缓存维护指令。这一对补丁是 M0 从"秒退无栈"走到"完整窗口渲染"的转折点。
### 3. 壳层接管桌面交互:菜单、标签、对话框与选窗
LibreOffice 是 SDI 多顶层窗口模型,直接搬进移动形态会碎成管理不了的窗海。壳层按 AirScape 模式接管可视管理:core 侧每 250ms 快照 QMenuBar 生成 JSON(含稳定 id、enabled、checked、子菜单结构)经 TSFN 推给 ArkTS,用系统 bindMenu 渲染菜单栏,同时 hide() 全部应用内 QMenuBar(P38);顶层 Qt::Window 快照驱动 WPS 式文档标签栏,点击切换、✕ 关闭、"+"按钮弹出新建类型菜单(P50/P88);纯首页态用不透明欢迎卡片盖住上游起始中心,2×2 卡片的动作经菜单树叶子匹配直通 LO(P55)。指针事件走 core 侧 aoPickWindowAt 落点选窗:模态窗无条件优先、ToolTip 型弹层按包含判定、Popup 型全捕获,配合 DOWN 锁定与原生物理像素双坐标契约,解决对话框、下拉弹层、多文档混布时的点击归属(P45/P61/P80/P86)。Qt 模态对话框则由 DialogWindowManager 创建真子窗承载,失败时走重试、降级、救援条三级兜底(P96/P96c)。
### 4. 渲染与窗口生命周期:让 raster 管线持续出帧
QPA 上屏路径是纯 raster:LO 的 cairo 位图经 QWidget paint 到 QPA 的 GLES blit 再 eglSwapBuffers。真机很快暴露出一组"空闲不画"的协同缺陷:splash 析构连带销毁共享 EGL surface 导致平板冷启动永久白屏(修复为 owner 退场时注册表非空则 keep-alive);T_lo 只被输入唤醒导致定时器与首绘全停(修复为 20ms dispatcher 心跳线程调 wakeUp);LO 空闲无脏区不重绘导致首绘错过即黑屏(修复为菜单树就绪后 15 秒稳定期逐拍全量 update,P40);TEXTURE 模式下 buffer 队列被 ArkUI 无回调重建导致僵尸 surface(修复为 QOhos_InvalidateSurface 显式失效,P39)。窗口抖动期的重绘风暴再加 500ms 节流(P42)。这一组修复的共同逻辑是:不再假设桌面 WM 会替你管理 surface 与失效,壳层与 QPA 必须自己闭环。
### 5. 部署管线与全量汉化
native/deploy_core.sh 把 make install 产物整备进 HAP:ELF 去 NEEDED 版本号、64K 对齐修复、构建侧 strip、同名冲突闸门;rewrite_rc.py 把 fundamentalrc/unorc/services.rdb 里的 $ORIGIN、${LO_LIB_DIR} 宏展开为沙箱绝对路径——因为 /proc/self/exe 在 HAP 进程里指向 appspawn 产物,LO 的自动定位全部失效,只能环境变量显式指定 URE_BOOTSTRAP。汉化走三件套:zh-CN langpack 的 32 个 .mo 经 boost::locale 加载、Langpack-zh-CN.xcd 注册 InstalledLocales、41 个汉化 overlay 文件入部署 overlay 防擦除;user preset 预置 UILocale/ooLocale=zh-CN,同时关掉首跑欢迎弹窗与 TipOfTheDay(P47)、关掉自动保存杜绝崩溃恢复模态框(P49)。字体链路单独处理:rawfile 内置 fonts.conf 指向 /system/fonts 并配置 CJK 回退链,解决中文文档豆腐块。
▍五、适配中遇到的几个难点
### 难点一:光标形状的三级接力(P51→P78→P82)
鼠标光标听起来是最小的需求,实际走了三个阶段。第一版在 QPA 里实现 QPlatformCursor::changeCursor,把 Qt shape 映射为 Input_PointerStyle 后经 OH_Input_SetPointerStyle 下发,链路日志全部正常但真机指针恒为箭头——该系统 API 在真机上是 stub,rc=0 是假成功。第二阶段修复还暴露过启动时序竞态:setWindowId 早于 core dlopen,窗口 id 被静默丢弃。最终形态是 P82:core 注册 QOhos_SetCursorHandler3(style+pixmapKey+对话框 id),经 AO_RegisterCursorCallback 导出,bridge 按枚举名(值漂移免疫)去抖后走 TSFN 到 ArkTS,用 pointer.setPointerStyleSync 双通道下发并配 2 秒心跳重申(消费端勿去重),对话框光标经 DialogWindowManager 的 dialogWindowId 路由到子窗;P87 再加固为每次活取窗口 id 加读回验证。这次迭代留下两条实证纪律:API 的 rc=0 不等于视觉生效,读回验证才是落位证据;uinput 无法注入无按键 hover 移动,光标视觉终验只能物理鼠标完成。
### 难点二:dpr 舍入引发的坐标系统性错位
注入 dpr=1.9 时 Qt HighDpi 默认 Round 策略把它舍成 2.0,物理坐标对 Qt 逻辑几何整体偏移:探针实锤 (1939,1293) 落不进任何窗口,右侧 106px、底部 63px 成为点击死区,工具栏溢出箭头点不动;resize 兜底的尺寸相等判断也永不相等,最大化/还原后窗口滞留旧尺寸。修复分两步:P46 前置 PassThrough 舍入策略、P60 注入侧钳制 dpr≤2.0;P85 再改为"实测制"——g_eff_dpr 在快照拍上用首个可见窗口逻辑宽与 surface 物理宽实测得出,自愈任何舍入策略与设备差异,选窗、滚轮、resize 全部统一出口。dpr=1.90 的 MateBook 上探针验证严格相等,最大化/还原双向内容重排正常。
### 难点三:双投递输入与 IME 缓冲失步
2in1 系统为一次鼠标点击同时派发合成触摸与原生鼠标两路,可选按钮两次翻转回零表现为"点了没反应",且合成触摸坐标是 vp 单位、原生是物理像素,落点差一倍 dpr(P81,按事件源判别丢弃合成触摸)。软键盘侧,IME 提交后 TextArea 缓冲残留导致退格落在"语义空"缓冲上不产生任何回调(P84 空缓冲直通),表格场景每次触摸清空桥缓冲又永不回填,删除指令永远发不出去(P95 引入 ZWSP 哨兵位 + onWillDelete 拦截直通,编辑框文本恒定零状态翻转)。壳层键码表也曾错位:OHOS 真实键码 TAB=2049、Escape=2070 与预想不同,字母 E/F 被当 Tab 转发造成"拆字"(P96 修正键码表并停转字母)。这三个案例的共同点是症状都在用户侧表现为"输入随机失灵",必须以逐击探针日志定位到每一路事件的真实单位与去向。
### 难点四:引擎生命周期与"假活"检测
LO SDI 模型下关掉最后一个文档,soffice_main 直接返回,而 ArkTS 壳还持着最后一帧冻结画面——点击无效但不崩溃,用户看到的是"应用坏了"。工程为此建了两级检测:core 在 soffice_main 返回处推 shutdown 事件,壳层显示"文档引擎已退出·重新开始"层(P64);0 可见窗持续 3 秒且无模态时触发僵尸态恢复阶梯,先按标题找隐藏的起始中心复活,找不到才推 shutdown(P65)。重启通道本身也有坑:进程内二次调用 soffice_main 因 Qt 全局态不可重建而挂死(永久废弃),UIAbilityContext.restartApp 仅 2in1 支持,最终以 appRecovery.restartApp 应用级重启收敛。崩溃恢复模态框则从源头掐断:预置关闭自动保存,不再产生恢复数据(P49),恢复态打标与欢迎层隐藏作为纵深防御保留(P66)。
### 难点五:手机折叠形态的沉浸适配
Pura X 首次把工程带进手机形态,暴露出与平板完全不同的适配面:横屏时挖孔转到侧边,安全区需补读左右 avoid area 并内缩画布;module.json5 不写 orientation 时缺省值被系统按竖屏锁死处理,传感器已旋转而界面不动,必须显式声明 auto_rotation_restricted(P94,sceneboard 日志实锤);对话框子窗的 createSubWindowWithOptions 在手机上 Window 版重载静默 resolve undefined,整条 promise 链坍缩成无装饰死锁窗,根修为优先走 WindowStage 版重载加逐步 try/catch 与救援条兜底(P96c);直接导出 PDF 类绕过 picker 的路径在沙箱里静默无产出,壳层补了中转目录看守与菜单级重映射(P93)。v33 的 Pura X 全功能遍历(52 张截图加日志双通道)确认了三类文档创建、输入删除、标签切换、PDF 导出、保存关闭、方向轮换全链路可用。
▍六、编译、安装与启动
构建分三段:环境与参数、核心交叉编译、部署整备。native/env.sh 组装 OHOS NDK、brew LLVM 与 Qt5 栈路径;native/build_core.sh 完成 patch 同步、autogen、host 工具构建与目标编译;native/deploy_core.sh 把产物整备进 entry/libs/arm64-v8a/ 与 entry/src/main/resources/rawfile/。之后是标准 HAP 构建:
bash
source native/env.sh
native/build_core.sh
native/deploy_core.sh
export JAVA_HOME=/Applications/DevEco-Studio.app/Contents/jbr/Contents/Home
export DEVECO_SDK_HOME=/Applications/DevEco-Studio.app/Contents/sdk
hvigorw --mode module -p module=entry assembleHap --no-daemon
签名产物位于 entry/build/default/outputs/default/entry-default-signed.hap。连接设备后安装并启动:
bash
hdc install -r entry/build/default/outputs/default/entry-default-signed.hap
hdc shell aa start -a EntryAbility -b com.airword.app
两点部署纪律值得注意:native 变更必须全量构建安装,增量快速通道对 .so 无效;安装后应独立验证库指纹,磁盘满时包管理可能静默降级为旧库继续运行。
▍七、当前功能边界
已覆盖的能力(以 PORTING.md 补丁清单与 v28-v33 回归记录为据):
- Writer、Calc、Impress 三组件真机可编辑(文本输入、格式工具栏、公式栏、模板对话框、状态栏);
- 桌面级交互闭环:欢迎层四卡片、WPS 式文档标签栏、ArkTS 菜单栏(含子菜单与快捷键助记符)、Qt 模态对话框真子窗承载与救援条、保存/关闭确认全链路;
- 文件链路:系统 picker 打开/保存、导出为 PDF 全链路、中转目录看守与静默产物导出重映射、.~lock 残留清理;
- 输入全通道:物理键盘、软键盘 IME 桥(哨兵位防失步)、鼠标/触摸双路去重、滚轮与弹层路由、光标形状 ArkTS 通道;
- 稳定性机制:引擎退出检测与应用级重启、僵尸态恢复阶梯、崩溃恢复防御、多文档输入路由纠偏;
- 全量中文界面(菜单、对话框、起始中心)与 CJK 字体链路;
- 三形态适配:2in1 悬浮三键避让与 decor 隐藏、平板沉浸布局、Pura X 横竖屏旋转与安全区内缩。
尚未覆盖或有据可查的开放项:extension 同步路径(P52 两次真机失败后回退,待插桩分诊死因后恢复)、curl TLS 网络(M2 遗留,当前禁用)、M3 性能与打包体积专项(独立库模式体积代价约 +50MB)、Impress 模板框 Escape 关闭与超屏裁字(v33 遗留)、decor 在窗口 setTitle 后间歇复活(P58 定时重试兜底)、上游被禁用模块(java/python/宏/数据库引擎/多媒体/数字签名等)不在交付范围。
因此,当前版本更准确的定位是"LibreOffice 鸿蒙 PC 三组件基础办公版": Writer/Calc/Impress 的编辑、文件与中文体验已经过四类真机的全流程遍历,外围的网络能力与体积优化仍是明确的后续里程碑。
▍八、总结
AirWord 的适配对象是这批移植工程里"整体性"最强的一个:千万行 C++ 不允许逐模块讨价还价,UNO 桥、VCL、配置、字体、IPC 任何一环的桌面假设失效都会终止启动。工程的应对是把每一处失效都转化为带证据的补丁条目——从 P1 的 configure 守卫到 P96c 的对话框子窗管线,五十余个补丁构成了一份可审计的鸿蒙差异清单,其中静态跳板表、rc 宏重写、实测制 dpr、落点选窗等方案已具备跨工程复用价值。
后续工作重心按工程手册的里程碑推进:恢复 extension 同步死因的分诊插桩、补齐 curl TLS、进入 M3 的性能回归与打包体积优化,并跟进光标与滚轮类物理外设的最终人工验证。对一个把"所有条目附证据"写进纪律的工程来说,剩下的每一项都有明确的锚点和验收方式,边界之内的工作只会越来越薄。
■ 应用:AirWord(from LibreOffice)(AirWord)
■ 定位:LibreOffice 移植,文字处理与文档编辑
■ 上游:LibreOffice 27.2.0 alpha0+(master) | 路线:Qt5 5.15+QPA(VCL) | 版本:1.3.0
■ 开源: https://gitcode.com/Andy88/Harmony-LibreOffice
■ 市场状态:已上架 AGC 应用页: https://appgallery.huawei.com/app/6917615885742616389
■ 官网(免费下载): https://www.shunidc.cn/
————————————————————
开源鸿蒙 PC 社区: https://harmonypc.csdn.net/
PC 社区项目申请: https://atomgit.com/OpenHarmonyPCDeveloper
适配开源地址:https://gitcode.com/Andy88/Harmony-LibreOffice
环境搭建文章:https://blog.csdn.net/weixin_52908342/article/details/161343743
▍一、为什么要适配 AirWord
办公套件是桌面生态的试金石。文字、表格、演示三类文档的编辑涉及排版引擎、UNO 组件模型、字体子系统、文件格式过滤器和一整套桌面交互范式,任何一层缺失都会让"能打开文档"退化为一句空话。AirWord 选择了这个品类里体量最大的上游:LibreOffice master 快照(27.2.0.0.alpha0+,约千万行 C++/UNO 代码),目标是让 Writer、Calc、Impress 三组件以原生桌面窗口的形态运行在鸿蒙 PC 上。
工程按 Air 系列双层架构组织:ArkTS 壳提供 Ability 生命周期、欢迎页、文档标签栏、菜单栏与输入法桥,LibreOffice 全套运行库(Qt5 栈、QPA 插件与两百余个组件动态库)进入 HAP,在一个专用线程里被进程内直调拉起。当前应用版本 1.1.0,包名 com.airword.app(原内部代号 AirOffice,v10 起独立品牌避免工程部署冲突),deviceTypes 覆盖 tablet、2in1、phone,目标设备从 MateBook Pro(2in1)、MatePad 系平板到 Pura X 折叠手机,ABI 为 arm64-v8a,要求 HarmonyOS 6.1.0(API 23)以上。
与浏览器或终端不同,LibreOffice 的移植难点不在某个单一模块,而在"整体性":UNO 桥、VCL 图形层、配置系统、字体链路、IPC 管道彼此咬合,任何一环在沙箱环境下的假设失效,症状都会以启动失败或静默黑屏的形式出现。这也是 AirWord 的工程手册以"先写映射再动手、所有条目附证据"为纪律的原因。
▍二、先划清边界:不把编译通过当成适配完成
LibreOffice 桌面版构建出的 soffice.bin 假定它可以 fork 子进程、mmap 可执行内存、读取固定安装路径、弹出自己的窗口系统。这些假设在鸿蒙应用沙箱里全部不成立。AirWord 把边界表写在工程手册第一节,明确哪些桌面机制被替换、哪些功能被显式禁用。
层次 LibreOffice 桌面版 鸿蒙侧当前方案
进程与入口 soffice.bin 独立进程 + fork/exec HAP 进程内 T_lo 专用线程直调 soffice_main()(OHOS 无 fork/exec)
GUI 后端 VCL + gtk3/win32 等平台插件 VCL qt5 插件 + Qt 5.15.16 + libqohos QPA(复用 AirKrita 权威副本)
渲染路径 Skia/OpenGL 可选加速 QtSvp + cairo 纯 raster 位图,--disable-skia --disable-opengl
窗口系统 X11/Wayland/Win32 顶层窗口 XComponent SURFACE/TEXTURE + eglSwapBuffers 上屏
UNO 桥 运行时生成 vtable thunk(依赖 exec 内存) 65536×64B 静态跳板表,零运行时代码生成(P35)
菜单系统 应用内 QMenuBar 250ms 快照转 JSON → ArkTS 系统 Menu(P38)
多文档形态 SDI 多顶层窗口 QOhos_SetMultiDocMode + WPS 式文档标签栏(P43/P50)
安装布局 固定前缀 /usr/lib/libreoffice rawfile 首启部署沙箱 + rc 内 $ORIGIN 宏重写为沙箱绝对路径
功能裁剪 全功能 禁用 java/python/数据库全家/avmedia/pdfium/openssl 等三十余项
边界表里最后一行常被忽略,却最诚实:AirWord 不假装带来完整的 LibreOffice。构建配置(native/autogen.input)里显式关闭的模块清单与交付的 Writer/Calc/Impress 编辑能力共同定义了产物范围。
▍三、鸿蒙版的整体架构
整套架构的要点是"壳薄核重":ArkTS 层只做系统能力接入与可视管理,NAPI 桥 libairofficebridge.so 只链 ace_napi/native_window/hilog 三个系统库;LibreOffice 全家在 native 侧按拓扑序 dlopen,Qt 事件循环与 LO 主循环合并进同一个 T_lo 线程。
text
AirWord HAP
├── ArkTS 壳 entry/src/main/ets
│ ├── EntryAbility:splash 遮罩 → 沉浸式 → rawfile 数据部署 → notifyDatafilesReady 门控
│ ├── Index.ets:XComponent(id=airoffice_xcomp)、欢迎层、文档标签栏、菜单栏、IME 桥
│ ├── DialogWindowManager / DialogPage:Qt 模态对话框的子窗承载与救援条
│ └── 文件 Picker 桥:documentViewPicker → uri 拷沙箱中转 → TSFN 回灌
│ │ NAPI:libairofficebridge.so(entry/src/main/cpp/napi_init.cpp)
│ ▼ OnSurfaceCreated → 预绑定 NativeWindow → 拓扑序 dlopen(RTLD_GLOBAL)
│ Qt5Core→Qt5Gui→Qt5Widgets→其余 Qt5 模块→libqohos(QPA)→LO 组件库(≈235 个 .so)
│ T_lo 线程:airoffice_core 启动 → env 注入 → soffice_main(argc,argv) 阻塞主循环
│ ├── SAL_USE_VCLPLUGIN=qt5 → VCL qt5 插件 → QApplication(T_lo 即 Qt 主线程)
│ ├── QPA=ohos:raster→GLES blit→eglSwapBuffers→XComponent
│ └── 输入:NAPI 回调 → QWindowSystemInterface::handleXxx 入队异步派发
└── entry/libs/arm64-v8a(模块级整目录通道,非 hvigor 闭包)
libQt5*.so ×12 + libqohos.so + libcairo + LO 组件库等共 235 个 .so
主要目录如下:
text
AirWord/
├── PORTING.md # 工程手册:架构、决策表、P1..P96c 补丁清单、风险登记册
├── native/
│ ├── env.sh # NDK / brew LLVM / Qt5 栈路径与编译参数组装
│ ├── autogen.input # LibreOffice configure 完整参数(含禁用清单)
│ ├── build_core.sh # patch rsync → autogen → host 工具 → 交叉编译 → install
│ ├── deploy_core.sh # make install → ELF 整备 → rc 重写 → rawfile 打包
│ ├── rewrite_rc.py # $ORIGIN/${LO_LIB_DIR} 等宏改写为沙箱绝对路径
│ ├── patches/core/ # P 系列补丁权威副本(0001-00xx patch 文件)
│ ├── patches/snapshot-v4-M0/ 等 # M0/M1 阶段源码树快照
│ ├── scripts/elf_edit.py # NEEDED 去版本号 / 64K 对齐整备
│ ├── toolchain/ # 工具链修复(如 diff 劫持问题的本地链接)
│ └── langpack/ + langpack_overlay/# zh-CN .mo×32 与汉化 overlay(41 文件)
└── entry/
├── libs/arm64-v8a/ # Qt5×12 + libqohos + LO 组件库,235 个 .so
└── src/main/
├── ets/
│ ├── pages/Index.ets # 欢迎层 / 标签栏 / 菜单栏 / IME 桥 / 方向轮换
│ ├── pages/DialogPage.ets # 对话框子窗宿主页
│ ├── utils/ # DialogWindowManager / WindowStageHolder
│ └── entryability/EntryAbility.ets
├── cpp/napi_init.cpp # libairofficebridge.so 薄壳(输入/IME/光标/窗口桥)
└── resources/rawfile/ # program(rc/rdb)、share/registry、fonts、presets、user 骨架
▍四、核心适配过程
### 1. 交叉编译体系:musl 伪装与双工具链接力
LibreOffice 的 configure 不认识 ohos 平台,工程让宿主 triplet 走 --host=aarch64-unknown-linux-musl 得到 _os=Linux(configure.ac 明确接受 linux-musl),真正的 ohos 目标只出现在 clang 参数里。编译器用 brew LLVM 22 的 clang——LO master 硬要求 Clang≥18,NDK 自带 15.0.4 不可行——而 libc++ 头与库仍取自 NDK sysroot 的 15 版,保证设备运行时 ABI 一致。交叉模式下的 host 侧工具(构建机自己的 ICU、gengal 等)由 configure 自动在 CONF-FOR-BUILD 目录跑 macOS 原生子 configure;qmake/moc/rrc 则直接复用 AirKrita 的 macOS 版 Qt 5.15.16 工具,实测可跑。外部依赖除 Qt5 外全部内编(icu 走 --with-cross-build,boost 走 gbuild StaticLibrary),唯一的同名冲突 harfbuzz 以静态链入消解。M0 阶段还有一个重要反转:--enable-mergelibs=yes 生成的 151MB 巨库在 musl 加载器重定位阶段 SEGV,工程改回独立库模式,每库符号表独立,代价约 +50MB 体积。
### 2. UNO 桥的零运行时代码生成改造
UNO 桥原先在运行时生成 vtable thunk,需要可写可执行内存。工程用 execprobe 在 MateBook Pro 上实测了完整的"通道全灭矩阵":匿名 RWX mmap、mprotect 升级、文件背书 RWX、memfd exec 映射、/proc/self/mem 改写已加载页全部被拒,linker 还会拒载 RWX PT_LOAD 段——HarmonyOS NEXT 起应用域运行时代码生成被系统级 W^X 硬禁。最终方案是 P35:把 vtable thunk 改为 65536×64B 的静态跳板表,条目代码由自身地址推导出 (vtableOffset, functionIndex) 后跳转共享的 vtableSlotCall,寄存器约定与原动态 snippet 完全一致;addLocalFunctions 只填静态表地址,代码块退化为纯 RW 数据,彻底免掉 mprotect。配套的 P34 则解决了 musl 无 __clear_cache 导出导致 flushCode 直调空指针的问题,改用 CTR_EL0 行宽加内联 dc cvau/ic ivau 缓存维护指令。这一对补丁是 M0 从"秒退无栈"走到"完整窗口渲染"的转折点。
### 3. 壳层接管桌面交互:菜单、标签、对话框与选窗
LibreOffice 是 SDI 多顶层窗口模型,直接搬进移动形态会碎成管理不了的窗海。壳层按 AirScape 模式接管可视管理:core 侧每 250ms 快照 QMenuBar 生成 JSON(含稳定 id、enabled、checked、子菜单结构)经 TSFN 推给 ArkTS,用系统 bindMenu 渲染菜单栏,同时 hide() 全部应用内 QMenuBar(P38);顶层 Qt::Window 快照驱动 WPS 式文档标签栏,点击切换、✕ 关闭、"+"按钮弹出新建类型菜单(P50/P88);纯首页态用不透明欢迎卡片盖住上游起始中心,2×2 卡片的动作经菜单树叶子匹配直通 LO(P55)。指针事件走 core 侧 aoPickWindowAt 落点选窗:模态窗无条件优先、ToolTip 型弹层按包含判定、Popup 型全捕获,配合 DOWN 锁定与原生物理像素双坐标契约,解决对话框、下拉弹层、多文档混布时的点击归属(P45/P61/P80/P86)。Qt 模态对话框则由 DialogWindowManager 创建真子窗承载,失败时走重试、降级、救援条三级兜底(P96/P96c)。
### 4. 渲染与窗口生命周期:让 raster 管线持续出帧
QPA 上屏路径是纯 raster:LO 的 cairo 位图经 QWidget paint 到 QPA 的 GLES blit 再 eglSwapBuffers。真机很快暴露出一组"空闲不画"的协同缺陷:splash 析构连带销毁共享 EGL surface 导致平板冷启动永久白屏(修复为 owner 退场时注册表非空则 keep-alive);T_lo 只被输入唤醒导致定时器与首绘全停(修复为 20ms dispatcher 心跳线程调 wakeUp);LO 空闲无脏区不重绘导致首绘错过即黑屏(修复为菜单树就绪后 15 秒稳定期逐拍全量 update,P40);TEXTURE 模式下 buffer 队列被 ArkUI 无回调重建导致僵尸 surface(修复为 QOhos_InvalidateSurface 显式失效,P39)。窗口抖动期的重绘风暴再加 500ms 节流(P42)。这一组修复的共同逻辑是:不再假设桌面 WM 会替你管理 surface 与失效,壳层与 QPA 必须自己闭环。
### 5. 部署管线与全量汉化
native/deploy_core.sh 把 make install 产物整备进 HAP:ELF 去 NEEDED 版本号、64K 对齐修复、构建侧 strip、同名冲突闸门;rewrite_rc.py 把 fundamentalrc/unorc/services.rdb 里的 $ORIGIN、${LO_LIB_DIR} 宏展开为沙箱绝对路径——因为 /proc/self/exe 在 HAP 进程里指向 appspawn 产物,LO 的自动定位全部失效,只能环境变量显式指定 URE_BOOTSTRAP。汉化走三件套:zh-CN langpack 的 32 个 .mo 经 boost::locale 加载、Langpack-zh-CN.xcd 注册 InstalledLocales、41 个汉化 overlay 文件入部署 overlay 防擦除;user preset 预置 UILocale/ooLocale=zh-CN,同时关掉首跑欢迎弹窗与 TipOfTheDay(P47)、关掉自动保存杜绝崩溃恢复模态框(P49)。字体链路单独处理:rawfile 内置 fonts.conf 指向 /system/fonts 并配置 CJK 回退链,解决中文文档豆腐块。
▍五、适配中遇到的几个难点
### 难点一:光标形状的三级接力(P51→P78→P82)
鼠标光标听起来是最小的需求,实际走了三个阶段。第一版在 QPA 里实现 QPlatformCursor::changeCursor,把 Qt shape 映射为 Input_PointerStyle 后经 OH_Input_SetPointerStyle 下发,链路日志全部正常但真机指针恒为箭头——该系统 API 在真机上是 stub,rc=0 是假成功。第二阶段修复还暴露过启动时序竞态:setWindowId 早于 core dlopen,窗口 id 被静默丢弃。最终形态是 P82:core 注册 QOhos_SetCursorHandler3(style+pixmapKey+对话框 id),经 AO_RegisterCursorCallback 导出,bridge 按枚举名(值漂移免疫)去抖后走 TSFN 到 ArkTS,用 pointer.setPointerStyleSync 双通道下发并配 2 秒心跳重申(消费端勿去重),对话框光标经 DialogWindowManager 的 dialogWindowId 路由到子窗;P87 再加固为每次活取窗口 id 加读回验证。这次迭代留下两条实证纪律:API 的 rc=0 不等于视觉生效,读回验证才是落位证据;uinput 无法注入无按键 hover 移动,光标视觉终验只能物理鼠标完成。
### 难点二:dpr 舍入引发的坐标系统性错位
注入 dpr=1.9 时 Qt HighDpi 默认 Round 策略把它舍成 2.0,物理坐标对 Qt 逻辑几何整体偏移:探针实锤 (1939,1293) 落不进任何窗口,右侧 106px、底部 63px 成为点击死区,工具栏溢出箭头点不动;resize 兜底的尺寸相等判断也永不相等,最大化/还原后窗口滞留旧尺寸。修复分两步:P46 前置 PassThrough 舍入策略、P60 注入侧钳制 dpr≤2.0;P85 再改为"实测制"——g_eff_dpr 在快照拍上用首个可见窗口逻辑宽与 surface 物理宽实测得出,自愈任何舍入策略与设备差异,选窗、滚轮、resize 全部统一出口。dpr=1.90 的 MateBook 上探针验证严格相等,最大化/还原双向内容重排正常。
### 难点三:双投递输入与 IME 缓冲失步
2in1 系统为一次鼠标点击同时派发合成触摸与原生鼠标两路,可选按钮两次翻转回零表现为"点了没反应",且合成触摸坐标是 vp 单位、原生是物理像素,落点差一倍 dpr(P81,按事件源判别丢弃合成触摸)。软键盘侧,IME 提交后 TextArea 缓冲残留导致退格落在"语义空"缓冲上不产生任何回调(P84 空缓冲直通),表格场景每次触摸清空桥缓冲又永不回填,删除指令永远发不出去(P95 引入 ZWSP 哨兵位 + onWillDelete 拦截直通,编辑框文本恒定零状态翻转)。壳层键码表也曾错位:OHOS 真实键码 TAB=2049、Escape=2070 与预想不同,字母 E/F 被当 Tab 转发造成"拆字"(P96 修正键码表并停转字母)。这三个案例的共同点是症状都在用户侧表现为"输入随机失灵",必须以逐击探针日志定位到每一路事件的真实单位与去向。
### 难点四:引擎生命周期与"假活"检测
LO SDI 模型下关掉最后一个文档,soffice_main 直接返回,而 ArkTS 壳还持着最后一帧冻结画面——点击无效但不崩溃,用户看到的是"应用坏了"。工程为此建了两级检测:core 在 soffice_main 返回处推 shutdown 事件,壳层显示"文档引擎已退出·重新开始"层(P64);0 可见窗持续 3 秒且无模态时触发僵尸态恢复阶梯,先按标题找隐藏的起始中心复活,找不到才推 shutdown(P65)。重启通道本身也有坑:进程内二次调用 soffice_main 因 Qt 全局态不可重建而挂死(永久废弃),UIAbilityContext.restartApp 仅 2in1 支持,最终以 appRecovery.restartApp 应用级重启收敛。崩溃恢复模态框则从源头掐断:预置关闭自动保存,不再产生恢复数据(P49),恢复态打标与欢迎层隐藏作为纵深防御保留(P66)。
### 难点五:手机折叠形态的沉浸适配
Pura X 首次把工程带进手机形态,暴露出与平板完全不同的适配面:横屏时挖孔转到侧边,安全区需补读左右 avoid area 并内缩画布;module.json5 不写 orientation 时缺省值被系统按竖屏锁死处理,传感器已旋转而界面不动,必须显式声明 auto_rotation_restricted(P94,sceneboard 日志实锤);对话框子窗的 createSubWindowWithOptions 在手机上 Window 版重载静默 resolve undefined,整条 promise 链坍缩成无装饰死锁窗,根修为优先走 WindowStage 版重载加逐步 try/catch 与救援条兜底(P96c);直接导出 PDF 类绕过 picker 的路径在沙箱里静默无产出,壳层补了中转目录看守与菜单级重映射(P93)。v33 的 Pura X 全功能遍历(52 张截图加日志双通道)确认了三类文档创建、输入删除、标签切换、PDF 导出、保存关闭、方向轮换全链路可用。
▍六、编译、安装与启动
构建分三段:环境与参数、核心交叉编译、部署整备。native/env.sh 组装 OHOS NDK、brew LLVM 与 Qt5 栈路径;native/build_core.sh 完成 patch 同步、autogen、host 工具构建与目标编译;native/deploy_core.sh 把产物整备进 entry/libs/arm64-v8a/ 与 entry/src/main/resources/rawfile/。之后是标准 HAP 构建:
bash
source native/env.sh
native/build_core.sh
native/deploy_core.sh
export JAVA_HOME=/Applications/DevEco-Studio.app/Contents/jbr/Contents/Home
export DEVECO_SDK_HOME=/Applications/DevEco-Studio.app/Contents/sdk
hvigorw --mode module -p module=entry assembleHap --no-daemon
签名产物位于 entry/build/default/outputs/default/entry-default-signed.hap。连接设备后安装并启动:
bash
hdc install -r entry/build/default/outputs/default/entry-default-signed.hap
hdc shell aa start -a EntryAbility -b com.airword.app
两点部署纪律值得注意:native 变更必须全量构建安装,增量快速通道对 .so 无效;安装后应独立验证库指纹,磁盘满时包管理可能静默降级为旧库继续运行。
▍七、当前功能边界
已覆盖的能力(以 PORTING.md 补丁清单与 v28-v33 回归记录为据):
- Writer、Calc、Impress 三组件真机可编辑(文本输入、格式工具栏、公式栏、模板对话框、状态栏);
- 桌面级交互闭环:欢迎层四卡片、WPS 式文档标签栏、ArkTS 菜单栏(含子菜单与快捷键助记符)、Qt 模态对话框真子窗承载与救援条、保存/关闭确认全链路;
- 文件链路:系统 picker 打开/保存、导出为 PDF 全链路、中转目录看守与静默产物导出重映射、.~lock 残留清理;
- 输入全通道:物理键盘、软键盘 IME 桥(哨兵位防失步)、鼠标/触摸双路去重、滚轮与弹层路由、光标形状 ArkTS 通道;
- 稳定性机制:引擎退出检测与应用级重启、僵尸态恢复阶梯、崩溃恢复防御、多文档输入路由纠偏;
- 全量中文界面(菜单、对话框、起始中心)与 CJK 字体链路;
- 三形态适配:2in1 悬浮三键避让与 decor 隐藏、平板沉浸布局、Pura X 横竖屏旋转与安全区内缩。
尚未覆盖或有据可查的开放项:extension 同步路径(P52 两次真机失败后回退,待插桩分诊死因后恢复)、curl TLS 网络(M2 遗留,当前禁用)、M3 性能与打包体积专项(独立库模式体积代价约 +50MB)、Impress 模板框 Escape 关闭与超屏裁字(v33 遗留)、decor 在窗口 setTitle 后间歇复活(P58 定时重试兜底)、上游被禁用模块(java/python/宏/数据库引擎/多媒体/数字签名等)不在交付范围。
因此,当前版本更准确的定位是"LibreOffice 鸿蒙 PC 三组件基础办公版": Writer/Calc/Impress 的编辑、文件与中文体验已经过四类真机的全流程遍历,外围的网络能力与体积优化仍是明确的后续里程碑。
▍八、总结
AirWord 的适配对象是这批移植工程里"整体性"最强的一个:千万行 C++ 不允许逐模块讨价还价,UNO 桥、VCL、配置、字体、IPC 任何一环的桌面假设失效都会终止启动。工程的应对是把每一处失效都转化为带证据的补丁条目——从 P1 的 configure 守卫到 P96c 的对话框子窗管线,五十余个补丁构成了一份可审计的鸿蒙差异清单,其中静态跳板表、rc 宏重写、实测制 dpr、落点选窗等方案已具备跨工程复用价值。
后续工作重心按工程手册的里程碑推进:恢复 extension 同步死因的分诊插桩、补齐 curl TLS、进入 M3 的性能回归与打包体积优化,并跟进光标与滚轮类物理外设的最终人工验证。对一个把"所有条目附证据"写进纪律的工程来说,剩下的每一项都有明确的锚点和验收方式,边界之内的工作只会越来越薄。
楼主
8 天前