← 返回公开文章

iOS 版 Emacs App:可行路线、硬限制与实现工作量

/research/ios-emacs-app-feasibility.md

做 iOS 版 Emacs App,第一步应先选边界,UI 反而不是最先决定的部分:是“App Store 上架的本地 GNU Emacs”、 “可侧载的本地 iPad Emacs”、 “远程 Emacs 前端”,还是“越狱设备上的 CLI Emacs”。这四条路的工作量和风险完全不同。按现在能查到的资料,最现实的本地路线是侧载的 iPad App:Emacs 核心在 App 内运行,先用终端网格渲染,逐步补文件、键盘、配置和少量 in-process 能力;直接做 App Store 版完整 GNU Emacs,法律、审核和技术限制都会撞墙。

已经能看到的现状

官方 GNU Emacs 现在已经有 Android 图形端口,Emacs 源码里有 Android window-system port 和交叉编译支持;这说明“把 Emacs 搬到移动 OS”不是概念上不可能,但 Android 给的进程、文件和分发条件比 iOS 宽得多。[4][5]

iPad 侧载方向已经有很直接的先例:mclearc/emacs-ipad 声称 GNU Emacs 作为 iPad App 本地运行,不需要 shell、终端 App 或远程服务器;当前实现用 SwiftTerm 渲染终端式网格,能在真机上启动、读写 Files 授权目录、加载 config.el,但项目也明确说它不是 App Store App,发行路线是侧载。[9]

这个先例给出了很重要的工程边界:它当前可用的是 terminal build,不是原生 GUI backend;README 明确列出无 subprocess、无 PTY、无 .eln native compilation,Magit/LSP/vterm/compile-mode/非 elisp org-babel 等都不在当前能力内。[9][11]

“真正原生 iOS GUI Emacs”还没有公开可直接复用的 backend。emacs-ipad 的 native GUI 调研结论是:没有公开的非越狱 iOS native GUI Emacs backend,没有 iosterm 或 UIKit redisplay backend;如果做,Android 的 androidterm.c 和 macOS 的 nsterm.m 只能作为结构模板。[14]

还有两类旁路:一类是 Blink 这种 iOS SSH/Mosh 终端,适合连远程机器跑 Emacs,但它不是本地 Emacs;另一类是 iSH,用用户态 x86 emulator 在 iOS 上跑 Linux shell,但性能、JIT 和 App Store 政策都限制它成为“完整本地 GNU Emacs App”的主路线。[18][19][20]

四条路线怎么选

路线 A:App Store 上架的本地 GNU Emacs

这条最不建议作为第一目标。Apple Review Guidelines 要求 App 自包含,不能读写容器外数据,也不能下载、安装或执行会引入/改变 App 功能的代码;这会直接影响 MELPA 式包安装、用户动态扩展、外部工具链和脚本执行。[1]

JIT/可写可执行内存也不是普通 iOS App 可以随意拿的能力。Apple 的公开 JIT entitlement 文档面向 macOS Hardened Runtime,描述的是 MAP_JIT 下可写且可执行内存;iOS/iPadOS 面向第三方的 JIT 能力主要出现在欧盟替代浏览器引擎 entitlement 里,并带有浏览器、安全更新、测试套件等要求,不适合拿来给 Emacs native-comp 或通用解释器开后门。[2][3]

GNU Emacs 又是 GPLv3-or-later 代码。现有 emacs-ipad 项目因此选择分发源码和未签名 IPA,让用户用自己的 Apple 身份签名侧载,并明确说避开 App Store 是为了避免 App Store 条款和 GPL 自由修改/运行修改版之间的冲突。[9][21]

可行替代方案:如果必须 App Store,上架目标应改成“远程 Emacs 客户端”或“类 Emacs 编辑器”,不要把 GNU Emacs 本体和 MELPA 式扩展系统打包成 App Store 产品。

路线 B:非越狱、侧载、本地 terminal Emacs

这是最现实的 MVP。核心做法是把 GNU Emacs 交叉编译进 iPadOS App,禁掉 X/NS/native-comp 等桌面路径,用 Swift/UIKit 包一层 App shell,再用 SwiftTerm/Metal 或等价终端网格显示 Emacs 的 TTY 输出。[9][10]

这条路的产品定位要克制:它可以成为本地写作、Org、轻量编辑、Dired、Eshell、基本 VC 的工具;不要承诺桌面 Emacs 的 CLI 生态、LSP、Magit、vterm、compile-mode、任意包原生模块都能工作。[11]

技术方案已经比较清楚:先做 terminal grid,而不是一开始写 native GUI;Emacs 主循环跑在 worker thread,UIKit 主线程负责窗口、键盘、文件选择器和渲染,中间用 C/Objective-C bridge 做事件和 frame-buffer 更新。[10]

路线 C:非越狱、本地 native GUI Emacs

这是长期路线。它的目标并非把终端画得更漂亮,而是写一个 iOS window-system backend,让 Emacs redisplay 直接画到 UIKit/Metal view,类似 Android 的 androidterm.c 或 macOS 的 nsterm.m 在各自平台扮演的角色。[5][14]

难点在于它要进入 Emacs redisplay、frame、font、input method、selection、clipboard、scroll、mouse/pointer、IME composition、HiDPI/rotation/scene lifecycle 等内部接口;没有现成 iosterm 可 fork,只能参考 Android/macOS 端口重新实现。[14]

可行方案是把它放到 v2/v3:先用 terminal build 验证 Emacs core、文件、键盘、配置、侧载和保存,再在已有 App shell 上逐步替换 redisplay backend。

路线 D:越狱 / Procursus CLI Emacs

越狱路线技术上反而更像传统 Unix:shogi-dojo/emacs-ios 的计划就是为 rootless Procursus 做 GNU Emacs 30.2 CLI .deb,目标是 iOS 15+ 越狱设备、ncurses terminal Emacs、正常 subprocess 和 PTY,不做 UIKit、原生 GUI、App Store 或非越狱支持。[17]

这条适合做底层移植验证,不适合作为普通用户产品,因为用户前提变成“有越狱设备和 Procursus 环境”。

真要做,需要完成哪些工作

1. 明确发行和许可证策略

如果基于 GNU Emacs 本体,默认按 GPLv3-or-later 处理整个派生 App;侧载路线需要提供源码、patch、构建说明和 unsigned IPA,让安装者自己签名,这正是 emacs-ipad 选择的结构。[9][21]

如果目标是 App Store,建议改做远程客户端或 clean-room 编辑器:前者不分发 GNU Emacs 本体,后者只能实现 Emacs 风格键位/少量 Lisp 兼容层,不能声称就是 GNU Emacs。

2. 建交叉编译工具链

需要在 macOS/Xcode 上为 arm64 iPadOS 交叉编译 Emacs C core 和必要依赖。Emacs 的传统构建依赖 configure、GNU make、host-side 构建工具和一堆可选库;Android port 已经把“移动端交叉编译 Emacs”这条路打通过,但 iOS 仍要单独适配 SDK、codesign、bundle、sandbox 和 UIKit 入口。[4][5]

实际工程上要拆成两段:host 侧生成 autoload、bytecode、Unicode 表、DOC 等构建产物;target 侧编出 iOS 可运行的 Emacs core,再把它嵌进 App bundle 或 framework。越狱 CLI 方案甚至需要在真机上生成 portable dump,因为 target binary 不能直接在 Mac 上运行。[17]

3. 选择启动模型:pdump 还是 bundled .elc

桌面 Emacs 常见构建流程会涉及 portable dump;iOS 非越狱侧载 App 上,最稳的路线是绕开运行时 dump,采用 bytecode-only + bundled .elc/.el tree 启动。emacs-ipad README 明确说它替换了 portable-dump 路径,改用 bundled .elc 启动。[9]

如果坚持 pdumper,则要像越狱 CLI 计划那样,在连接的 iPhone/iPad 目标环境上生成 dump,记录 fingerprint,再打包回产物;这对普通侧载 App 不是好的 MVP 路径。[17]

4. 做 iOS App shell 和 Emacs bridge

App shell 负责 UIWindowScene、生命周期、键盘、document picker、clipboard、pointer/scroll/rotation/Stage Manager,以及渲染 surface。emacs-ipad 的结构是三层:UIKit shell、C/Objective-C bridge、Emacs worker thread;主线程不阻塞,Emacs 主循环在单独 worker thread。[10]

bridge 的本质是把 iOS 事件翻译成 Emacs 输入,把 Emacs 更新翻译成终端网格或将来的 GUI frame 更新。这个部分不是“套个 WebView”能解决的,因为 Emacs 的 redisplay、minibuffer、输入法、快捷键和窗口重排都要保持一致。

5. 先做 terminal renderer,再考虑 native GUI

MVP 应该复用 SwiftTerm 这类终端渲染组件,先让 TTY Emacs 可用。这样可以避开 Emacs redisplay backend 的大坑,把第一阶段工作集中在启动、输入、字体、颜色、窗口尺寸、文件访问和状态保存。[9][10]

native GUI backend 的技术方案是:以 Android port 的 androidterm.c 为骨架、以 macOS nsterm.m 的 Cocoa/输入/字体/选择经验为参照,写一个 UIKit/Metal 的 iosterm。这属于新的 window-system port,不是小修小补。[5][14]

6. 处理文件系统和 Files App

iOS 文件访问不是桌面 POSIX 目录任意扫。基本盘是 App 自己的 Documents/Library;访问外部文件或文件夹要通过 document picker 拿 security-scoped URL,并持久化 bookmark;每次读写要包在 startAccessingSecurityScopedResource() / stopAccessingSecurityScopedResource() 中。[1][13]

可行方案是:让 ~/.emacs.d/ 和默认工作目录放在 App container;外部项目通过 Files 选择文件夹授权;在 Emacs 里维护“已授权目录”虚拟根,让 find-file、Dired 和路径补全只在这些根内工作。[13]

7. 重新定义 subprocess 能力

这是最大技术断层。Emacs Lisp 程序可以创建同步/异步 subprocess,很多桌面功能都建立在 call-process、make-process、PTY、signals、filters/sentinels 上。[8]

iOS 非越狱 sandbox 下不能按传统方式执行另一个 Mach-O;emacs-ipad 的 subprocess dispatcher 文档把问题说得很直白:exec* 和 posix_spawn 会被 sandbox 拒绝,fork 即便返回,没有 exec 也没实际用途,所以每个 shell-out 点都要改成 in-process function。[12]

解决方案要避免“模拟一个完整 Unix”的大坑,改成列白名单:grep/find/git/ssh/hunspell/SQLite/tree-sitter/GnuTLS 这类能力编成静态库或签名 framework,通过 dispatcher 暴露给 Emacs;需要 PTY、job control、真正信号的功能直接判为不支持。[11][12]

8. 给 Git、搜索、拼写、网络功能找 in-process 替代

Git 是典型例子:Magit 大量调用 git CLI,iOS 上跑不了;emacs-ipad 选择用 libgit2/libegit2 思路做 vc-git 后端,覆盖 status、diff、log、commit、branch 等一组较小但实用的同步操作。[11][15]

搜索也类似:不要指望用户装 ripgrep;可以把 grep/find 类功能编进 App,或做基于 tree-sitter / SQLite index 的 in-process 搜索。邮件同步不要走 mbsync/mu,而是用 Emacs 网络 primitive 走 Gnus/IMAP 这种不依赖外部二进制的路径。[11]

9. 限制 package manager 和 native modules

Emacs native compilation 依赖 GCC、binutils 和 libgccjit;native-compiled .eln 还跟系统/机器强相关,放到 iOS 上会同时碰到 JIT/code signing/工具链不可用问题。[6]

Emacs dynamic module 本质是 shared library;桌面上可以加载 .so/.dylib/.dll,但 iOS 下可行边界应收缩为“随 App 打包、同团队签名、版本固定”的动态库或直接静态链接。[7][11]

因此 MELPA 体验要降级:第一阶段只支持 pure Elisp 包,最好通过 Documents/vendored 或 curated package bundle 加载;任何 native module 都需要重新构建 App。不要承诺“手机上 package-install 一切照旧”。

10. 做键盘、输入法和移动交互

Emacs 在 iPad 上能不能用,很大程度取决于键盘层。要完整处理硬件键盘 modifier、Esc/Ctrl/Meta、Cmd 快捷键冲突、软件键盘 accessory bar、中文输入法 marked text、选择/复制/粘贴、触控滚动、pointer、Stage Manager resize 和旋转。[9][10]

这部分没有神奇方案,只能实作和测试。terminal MVP 可以先保证 ASCII、快捷键和基本中文输入;原生 GUI 阶段再把 text input、composition、selection 和 glyph hit-testing 做细。

难点与可用技术方案

推荐架构

第一版建议做“本地侧载 terminal Emacs”,不要一开始冲 App Store 或 native GUI:

UIKit / Swift App shell
  - UIWindowScene / Stage Manager / rotation
  - keyboard + accessory bar + clipboard
  - UIDocumentPicker + security-scoped bookmarks
  - SwiftTerm/Metal terminal grid

C / Objective-C bridge
  - event queue: key / resize / file picker result / lifecycle
  - output queue: terminal cells / status / file access request
  - optional dispatcher registry for in-process commands

Emacs core worker
  - arm64 iPadOS build, bytecode-only
  - bundled lisp + config.el + pure-elisp vendored packages
  - no native-comp, no unrestricted subprocess, no PTY

这套架构的好处是边界清楚:Emacs core 是真的,iOS shell 是平台适配层,terminal renderer 是可替换的第一阶段 UI;后续 native GUI 可以在同一 App shell 上另开 backend,而不是推倒重来。[9][10][14]

分阶段工作量

P0:复现实有路线,锁定基线

先 fork 或复现 emacs-ipad 的构建:macOS + Xcode + xcodegen,真机侧载,确认 Emacs 能启动、键盘能输入、config.el 能加载、文件能读写。[9][16]

验收标准:真机打开 App,M-x version、find-file、保存文件、基础中文/英文输入、旋转/分屏 resize、重新打开后状态稳定。

P1:可用编辑器 MVP

完善 terminal grid 的颜色、字体、窗口尺寸、复制粘贴、软件键盘辅助栏、Files 授权目录、Dired、basic Org、autosave/session persistence。

验收标准:能作为离线写作和轻量代码编辑器使用;明确告诉用户哪些桌面功能没有。

P2:补“无 subprocess”后的关键能力

实现 dispatcher 白名单:grep/find、libgit2/VC、SQLite、tree-sitter、hunspell、必要网络库。不要先追 Magit/LSP/vterm;先覆盖每天真的会用的少数动作。[11][12][15]

验收标准:不用外部二进制也能搜索项目、看 git diff/log/status、commit 小改动、处理 basic Org/Markdown/code 文件。

P3:native GUI 调研实现

等 terminal 版证明产品有价值,再写 iosterm:frame lifecycle、font/glyph shaping、redisplay、scroll、selection、clipboard、IME、menus/toolbars、HiDPI/rotation/scene lifecycle。Android port 和 macOS NS port 是主要参考,但不能直接移植。[5][14]

验收标准:不经 terminal 网格,Emacs redisplay 直接绘制到 UIKit/Metal;至少能覆盖 text frame、minibuffer、selection、keyboard input 和 resize。

P4:分发和维护

侧载发行需要 release unsigned IPA、源码、patch、依赖版本、构建脚本和签名说明;同时要接受免费 Apple ID 7 天刷新、付费账号一年证书、AltStore/SideStore/Sideloadly 等工具差异。[16]

如果以后想进入 App Store,建议拆成另一个产品:远程 Emacs 前端,或者不包含 GNU Emacs 本体的类 Emacs 编辑器。不要把侧载 GNU Emacs 的技术路线强行改成 App Store 路线。

粗略判断

如果基于现有 emacs-ipad 路线继续做,做出“能自用的侧载 terminal Emacs”是可行工程,不是科研项目;一个熟悉 iOS + C/Emacs internals 的人,数月级可以推进到可用。若从零开始,至少要准备半年级工作量,因为真正难的是交叉编译、启动模型、键盘输入、文件授权和 subprocess 替代,而不是画一个编辑器窗口。

做“完整桌面 Emacs 体验”在非越狱 iOS 上不可行,至少不应承诺。iOS sandbox 会迫使它成为一个受限但真实的本地 Emacs:强在编辑、Org、纯 Elisp、少量 in-process 能力;弱在 CLI 生态、LSP、PTY、native module、native-comp 和外部工具链。[11]

做“原生 GUI Emacs”有技术路线,但工作量明显更大:它是一个新的 Emacs window-system backend,而不是把 terminal renderer 换成 Metal canvas。最稳的顺序是先 terminal 版跑通真实用户工作,再投入 native GUI backend。[10][14]

Sources

[1] https://developer.apple.com/app-store/review/guidelines — Apple App Review Guidelines [2] https://developer.apple.com/support/alternative-browser-engines — Apple alternative browser engines in the EU [3] https://developer.apple.com/documentation/bundleresources/entitlements/com.apple.security.cs.allow-jit — Apple allow-jit entitlement [4] https://www.gnu.org/software/emacs/manual/html_node/emacs/Android-Environment.html — GNU Emacs Manual: Android Environment [5] https://github.com/emacs-mirror/emacs/commit/c71a520d1da636a722cf87b46534ca3b5aafbc7b — Emacs Android window-system port commit [6] https://www.gnu.org/software/emacs/manual/html_node/elisp/Native-Compilation.html — GNU Emacs Lisp Reference: Native Compilation [7] https://www.gnu.org/software/emacs/manual/html_node/elisp/Dynamic-Modules.html — GNU Emacs Lisp Reference: Dynamic Modules [8] https://www.gnu.org/software/emacs/manual/html_node/elisp/Processes.html — GNU Emacs Lisp Reference: Processes [9] https://codeberg.org/mclearc/emacs-ipad/raw/branch/main/README.md — mclearc/emacs-ipad README [10] https://codeberg.org/mclearc/emacs-ipad/raw/branch/main/ARCHITECTURE.md — mclearc/emacs-ipad Architecture [11] https://codeberg.org/mclearc/emacs-ipad/raw/branch/main/docs/feature-budget.md — mclearc/emacs-ipad Feature budget [12] https://codeberg.org/mclearc/emacs-ipad/raw/branch/main/docs/subprocess-dispatcher.md — mclearc/emacs-ipad Subprocess dispatcher [13] https://codeberg.org/mclearc/emacs-ipad/raw/branch/main/docs/file-access.md — mclearc/emacs-ipad File access [14] https://codeberg.org/mclearc/emacs-ipad/raw/branch/main/docs/native-gui-backend-study.md — mclearc/emacs-ipad Native GUI backend study [15] https://codeberg.org/mclearc/emacs-ipad/raw/branch/main/docs/vc-shim.md — mclearc/emacs-ipad vc-git shim [16] https://codeberg.org/mclearc/emacs-ipad/raw/branch/main/SIDELOAD.md — mclearc/emacs-ipad Sideloading [17] https://raw.githubusercontent.com/shogi-dojo/emacs-ios/main/PLAN.md — shogi-dojo/emacs-ios PLAN [18] https://raw.githubusercontent.com/blinksh/blink/master/README.md — Blink Shell README [19] https://ish.app — iSH home [20] https://ish.app/blog/ish-jit-and-eu — iSH JIT and EU [21] https://git.savannah.gnu.org/cgit/emacs.git/plain/COPYING?h=emacs-30 — GNU Emacs COPYING / GPLv3 text