文档

简介

Native Shell(简称 NSH)运行用 Rust 或 TSX 编写的原生界面,它们跑在同一个用 Rust 编写的引擎上,并用每个平台自己的界面显示。你可以完全用 Rust 编写应用,编译成原生代码,不带 JavaScript 虚拟机;可以编写 TSX 模块,由原生应用在运行时加载,运行在它掌控的沙箱里;也可以两者结合,在 Rust 组件之上用 TSX 编写页面。NSH 是为由 AI 生成、动态下发的原生界面而设计的:模型写出一个小的 TSX 模块,应用以原生方式显示它。同一套机制也让原生应用整体具备扩展能力——插件、功能、整个页面都可以在发布之后下发。

NSH 同时运行在手机和桌面上:iOS(UIKit)、Android(Android 视图)、macOS(AppKit)、Windows(用 Direct2D 绘制的 Win32 窗口)和 Linux(GTK 4),也运行在 GPUI 应用和 Web 上。每个平台都有自己的原生后端,同一个应用可以在所有这些平台上运行(平台)。

#为什么做 NSH

首要目标:由 AI 生成的界面。 应用里的助手可以用一个界面而不是一段文字来回答。模型写出一个小的 TSX 模块(一个表单、一张图表、一张预订卡片),服务器检查、构建并签名后,应用下载并在 NshAppView 中显示:是原生界面,运行在沙箱里,只能做你授予的事,而且不需要更新应用。NSH 围绕这个场景设计,整体架构都是 AI 友好的:

  • 模型擅长的格式。 一个模块就是一个 TSX 文件,用到信号、少量宿主元素和 Tailwind 类——都是模型在 Web 上早已熟悉的词汇,不需要构建配置,也无需 NPM。
  • 精确的、自动生成的契约。 每个元素、属性、样式键和模块都有从 Rust 源码生成的 TypeScript 声明(.nsh/types),生成的代码可以做类型检查,也可以把准确的 API 交给模型,而不是让它去猜。本文档也以纯文本形式提供给模型(llms.txt、llms-full.txt)。
  • 下发之前就能验证。 nsh test 可以无界面、不需要设备地运行模块,在其上执行操作步骤,报告错误和它生成的视图;构建错误附带代码定位。生成流程可以编译、运行、读取结果再重试。编译器也能作为库在进程内运行(nsh_bundler::compile_module),甚至在浏览器里运行(在线试用)。
  • 不可信也能安全运行。 生成的代码就是不可信代码。每个模块运行在独立的 QuickJS 虚拟机里,受内存、调用时长和节点数限制,只能使用宿主授予的能力,并以签名包的形式到达、先经应用校验。
  • 原生,却无需写原生代码。 产出的是平台自己的界面——UIKit、Android 视图、AppKit、GTK 4,或用 Direct2D 绘制的 Win32——拥有平台的文本、输入和无障碍能力,但模型从不需要写 Swift、Kotlin、Java 或 Rust,应用也不需要发新版本。
从提示词到原生界面:模型写出 TSX 模块;服务器构建、检查并签名;跨过信任边界后,应用校验包,在设定了限制和能力的沙箱里运行,并用平台自己的界面显示。 从提示词到原生界面:模型写出 TSX 模块;服务器构建、检查并签名;跨过信任边界后,应用校验包,在设定了限制和能力的沙箱里运行,并用平台自己的界面显示。

其次:扩展原生应用,动态下发界面。 同一套机制适用于任何模块,不只是生成的模块。应用在需要显示模块的地方嵌入一个 NshAppView(iOS)、NshView(Android)或 AppView(macOS、Windows、Linux),NSH 在其中运行模块:

  • 发布之后再下发。 模块是应用在运行时下载的签名包(.nshpkg)。应用在运行它之前,会用自己信任的密钥校验签名,并检查吊销列表(签名包)。改动无需发新版本,也无需重新提交应用商店审核。
  • 宿主始终掌控。 每个模块运行在独立的沙箱虚拟机中,受内存、时间和节点数限制,只能访问宿主授予的能力:网络主机、网页、状态栏(沙箱与能力)。多个模块可以同时运行,没有全局单例。
  • 与应用其余部分浑然一体。 模块用平台自己的界面渲染,文本、输入、无障碍和滚动都是系统自己的。宿主还可以注册自己的原生视图——地图、播放器、设计系统组件——供模块使用(原生组件)。

再次:完整的应用,用 Rust 或 TSX 编写。 nsh package 把应用打成可安装的 iOS、Android、macOS、Windows、Linux 或 Web 应用。用 Rust 编写的应用拥有 Rust 的类型系统、crates.io 生态以及自身代码的原生性能,在任何平台上都不带 JavaScript 虚拟机(用 Rust 开发);TSX 应用运行在预编译的运行时上。快速开始会分别创建一个。

#为什么在 GPUI Shell 之外还要做 Native Shell

长桥(Longbridge)把 NSH 作为手机客户端的插件层来建设,桌面客户端也需要同样的一层。GPUI Shell 让 GPUI 桌面应用可以用 JavaScript 扩展,最初的想法也是把它用到手机上。但它并不适合,因为它的模块由 GPUI 自己在 GPU 上绘制,而不是使用平台的视图:

  • 系统能力都得重新实现。 嵌在原生页面里的自绘界面,没有平台的文本选区,也没有选区弹出的菜单(拷贝、分享、翻译、查询、全选),更没有系统输入法、无障碍,以及每一代 iOS 带来的新系统视觉效果。在 GPU 渲染器里复刻这些是一项永远做不完的大工程,而且复刻品总会和真正的系统行为走样。
  • 手机团队得接手一套新的 UI 技术栈。 要在现有手机客户端里大规模运营一层自绘界面,iOS 和 Android 团队就得接手一个陌生的渲染器,以及它在各种系统版本和设备上的兼容性问题和缺陷。

因此 NSH 用每个平台自己的界面来渲染模块:iOS 上用 UIKit,Android 上用 Android 视图,macOS 上用 AppKit,Windows 上用 Direct2D 绘制的 Win32 窗口,Linux 上用 GTK 4。同一种模块格式和同一个运行时可以运行在所有这些宿主上。

两者的定位不同:GPUI Shell 用于扩展 GPUI 桌面应用;Native Shell 面向每个平台上的原生应用,手机和桌面一视同仁。

#一个应用

同一个计数器,分别用 Rust 和 TSX 编写:

use nsh_core::ui::prelude::*;

#[component]
pub fn Counter() -> impl IntoElement {
    let count = signal(0);
    view()
        .flex_1()
        .items_center()
        .justify_center()
        .gap_3()
        .child(text(move || count.get().to_string()).text_5xl().font_weight(FontWeight::BOLD))
        .child(
            view()
                .rounded_lg()
                .bg(rgb(0x2563eb))
                .px_5()
                .py_3()
                .active(|s| s.opacity(0.6))
                .on_press(move || count.update(|n| *n += 1))
                .child(text("Tap").text_lg().text_color(white())),
        )
}

视图(view()、<View>)在 iOS 上变成 UIView,在 Android 上变成 ViewGroup,在 macOS 上变成 NSView,在 Linux 上变成 GTK 控件,文本变成原生文本视图,按钮的按压反馈(.active(..)、activeStyle)在主线程上完成。点击时,count.update 只修改一个信号,只有读取它的那段文本会更新:组件函数不会重新执行。Rust 应用从头到尾都是原生代码;在 TSX 应用中,只有模块自己的代码是 JavaScript。

TSX 模块也是原生应用在运行时加载的东西。宿主应用只需几行原生代码就能显示它(iOS、Android、macOS、Windows、Linux)。在 iOS 上:

let app = NshAppView()                         // 一个 UIView:可以放在任何地方
try app.grantCapabilities(#"{ "network": { "hosts": ["api.example.com"] } }"#)
let info = try await app.loadPackage(at: downloadedFile, trust: trust)   // 先校验,再运行

模块调用的一切——响应式图、渲染器、布局、文本,以及它导入的每个模块——都是用 Rust 实现的原生代码。

#如何运行

工具链只有一个 nsh 可执行文件。用 Rust 编写的应用是一个 crate:nsh 为每个目标构建链接了你的 crate 的运行器,它的根组件直接在引擎上运行。TSX 应用(模块)是一组带入口的 .tsx / .ts 文件,入口通常是 App.tsx:

  1. 构建。 nsh 打包模块的各个文件(借助 rolldown 和 Oxc),并用自己的编译器编译 JSX。对 nsh 与 nsh/* 的导入保留为导入:运行时以原生方式提供它们,因此 bundle 里只有你的代码。bundle 会被编译为 QuickJS 字节码。
  2. 运行。 在设备上,引擎线程用 QuickJS 执行字节码。编译后的 JSX 直接调用原生渲染器;一次 JavaScript 执行轮次中的变更被收集起来,由 Taffy(flexbox 与 CSS grid)完成布局,再以每帧一批视图操作的形式交给主线程。
  3. 渲染。 主线程把这批操作应用到原生视图上。文本在引擎线程上测量和排版一次(Apple 平台用 CoreText,Android 用 StaticLayout,Windows 用 DirectWrite,Linux 用 Pango),主线程只负责绘制。

Rust 应用从第 2 步起走同样的路径,只是由它的组件以原生方式运行,取代 QuickJS。在 TSX 应用中,JavaScript 只描述界面、处理事件,解释器里的工作量很小。解析样式、解析 Tailwind 类名、差异比较、布局和文本处理都在 Rust 中完成。

#你能得到什么

  • Rust 或 TSX。 用 Rust 编写的应用,带有类型化的样式和编译期检查,不带 JavaScript 虚拟机;用 TSX 编写运行时下发的页面和模块;或者两者结合,由 TSX 导入你的 Rust 组件、模块和类(用 Rust 开发)。
  • 嵌入。 NshAppView(Swift)、NshView(Java/Kotlin)和桌面端的 AppView(AppKit、GTK、Win32)可以把模块放进原生应用,数量不限(iOS、Android、macOS、Linux、Windows)。
  • 签名、可吊销的下发。 Ed25519 签名的包,发布密钥由离线根密钥认证,支持吊销列表(签名包)。
  • 由宿主配置的沙箱。 在 iOS 和 Android SDK 中,每个视图单独设置虚拟机限制和授予的能力(沙箱与能力)。
  • 模块可以使用宿主的视图。 用 Swift、Java 或 Rust 注册原生组件(原生组件)。
  • 原生视图。 嵌入原生地图、WebView 或视频播放器没有层级问题,无障碍使用平台自己的树,文本输入是系统的输入。
  • 细粒度响应式。 与 Solid 类似的信号,在原生层实现。组件只运行一次,只有读取了变化信号的表达式会重新执行。
  • 样式与 Tailwind,在原生层解析:flexbox、CSS grid、阴影、渐变、滤镜、过渡(样式、Tailwind)。
  • 基础元素与基础组件,外观由应用决定。 应用用基础元素(View、Text、Input、List 等)配合样式和 Tailwind 类绘制界面,并以 nsh/base 为依托,这些无样式的基础组件负责行为、状态和无障碍:导航栈和 modal、安全区域、系统提示框、键盘和状态栏。其上带样式的组件库由应用自己或社区提供(三层结构)。桌面布局用基础元素加上 desktop={{…}} 属性和 platform.select 搭建(平台特定代码)。
  • 单一可执行文件的工具链。 一个 nsh 可执行文件负责构建模块、生成类型声明、以热模块替换的方式提供开发服务、打包和签名。

#运行平台

平台状态后端说明
iOS已支持(iOS 15.0+)UIKit真机和模拟器;通过 NativeShell.xcframework 嵌入(iOS)
Android已支持(API 28,Android 9+)Android 视图arm64-v8a 和 x86_64;通过 nsh.aar 嵌入(Android)
macOS已支持(macOS 14+)AppKit独立应用;通过 AppView 嵌入(macOS)
Windows已支持(Windows 10 1809+)Win32、Direct2D独立应用;通过 AppView 嵌入(Windows)
Linux已支持(GTK 4.14+)GTK 4独立应用;通过 AppView 嵌入(Linux)
GPUI(macOS、Windows、Linux)已支持GPUI通过 AppView 嵌入 GPUI 和 GPUI Kit 应用(GPUI)
Web已支持(现代浏览器)HTML静态网站;在网页中用 createPlayer 或 iframe 嵌入(Web)
Mac Catalyst用于开发UIKitnsh run 在 Mac 上打开的 iPhone 尺寸窗口,用来查看模块在手机上的样子
一个模块,多种宿主:同一个 bundle 和 Rust 运行时,运行在 iOS、Android、Linux、macOS AppKit 和 Windows Win32 / Direct2D、三个桌面平台上的 GPUI,以及用 HTML 绘制的网页。 一个模块,多种宿主:同一个 bundle 和 Rust 运行时,运行在 iOS、Android、Linux、macOS AppKit 和 Windows Win32 / Direct2D、三个桌面平台上的 GPUI,以及用 HTML 绘制的网页。

#与其他方案的比较

在应用里显示发布之后才到来的界面,常见做法有 WebView、用 JSON 描述的服务端驱动 UI,以及小程序容器;构建完整应用则有 React Native、Flutter、Electron 和 Tauri 这类跨平台框架。WebView 渲染的是网页,NSH 模块渲染的是原生界面;服务端驱动的协议只能重新组合内置组件,NSH 模块有自己的状态和逻辑;而且它运行在宿主用代码配置的沙箱里。方案对比逐一做了比较。

#下一步

  • 安装和快速开始:安装 nsh,用 Rust 或 TSX 编写第一个应用,并在每个平台上运行。
  • 用 Rust 开发:用 Rust 编写的应用,以及在其上使用 TSX。
  • 平台:各平台支持什么,以及如何在 iOS、Android、macOS、Windows 和 Linux 上运行、打包和嵌入。
  • 签名包和沙箱与能力:下发模块,并决定它们能做什么。
  • JSX 与组件和响应式:其余一切都建立在这两个概念之上,Rust 与 TSX 皆然。