ARTICLE DETAIL

资讯详情

深耕编程入门与网站建设的一线实战洞察。

GPUI Shell 深度指南:用 JavaScript 扩展 Rust GPUI 应用的脚本运行时与插件体系

GPUI Shell 深度指南:用 JavaScript 扩展 Rust GPUI 应用的脚本运行时与插件体系 GPUI Shell 深度指南用 JavaScript 扩展 Rust GPUI 应用的脚本运行时与插件体系【免费下载链接】gpui-kitRust GUI components for building fantastic cross-platform desktop application by using GPUI.项目地址: https://gitcode.com/GitHub_Trending/gp/gpui-kitgpui-shell仓库中位于 crates/shell是 gpui-kit 提供的一个脚本运行时它让一个已经用 Rust 和 GPUI 写好的桌面应用可以用 JavaScript 进行扩展——插件优先其次才支持用 JavaScript 独立编写整个应用。本文基于 website/shell/index.md 展开并结合 engine.md、capabilities.md、state.md、dock.md 等配套文档与 crates/shell/src/lib.rs 等源码讲清它的设计动机、脚本与宿主的职责边界、性能与体积成本、安全模型以及脚本如何变成界面这一核心机制的完整链路。读完你会理解为什么说脚本只负责描述一次界面之后的每一帧都由 Rust 重放以及如何在自己的 GPUI 应用里挂载一个脚本 View。为什么需要 gpui-shell一次编译之后全部用脚本扩展一个普通的 Rust GPUI 应用每加一个面板、一个侧边工具或一段业务逻辑都要重新编译、重新分发二进制第三方想贡献一个面板只能 fork 整个仓库。gpui-shell要解决的就是这件事宿主应用只编译和发布一次之后新的界面与逻辑以脚本形式加载进同一个进程——不重编译、不重新分发二进制、贡献者也不需要 fork。它有两个目标优先级明确首要目标插件扩展。宿主应用构建一次运行时决定脚本可以触达什么脚本在同一进程内画出真实界面。次要目标用 JavaScript 写整个应用。CLI 可以独立运行一个应用目录这本身是一条可用路径同时也是插件的开发方式先把脚本跑成独立应用再挂载进宿主。需要特别澄清的是它不是 Electron也不是 Tauri。没有 WebView、没有 DOM、没有 HTML/CSS、没有浏览器内核、也没有 Node.js。脚本从不渲染——它只描述一次界面之后 Rust 在每一帧把这份描述重放成真实的 GPUI 元素走的是与基于gpui-base的 Rust 应用完全相同的元素模型与 GPU 渲染管线。JavaScript 在这里是应用层而不是渲染层这正是一次重绘不花任何 JavaScript、接入整个运行时只增加约 13.5 MiB 二进制体积详见 engine.md的原因。两个目标建立在同一条拆分之上gpui-shell直接构建在gpui-base之上QuickJS 运行在宿主自己的线程上。宿主构建运行时并授予脚本权限脚本在同一进程内画出真实界面。Rust 保留渲染、布局、文本编辑、虚拟化、焦点、浮层和一切系统能力脚本拥有组合、呈现和业务逻辑。三十行代码的完整示例一个带样式的计数器文档用一个计数器示例说明这套 API 的形态——View类来自gpui-kit布局与组件来自gpui-base样式全部是链式方法import { View } from gpui-kit; import { v_flex, Button } from gpui-base; export default class Counter extends View { init() { this.count 0; } render(cx) { return v_flex() .size_full() .items_center() .justify_center() .gap(20) .bg(cx.theme().colors.background) .child( div() .text_3xl() .text_color(cx.theme().colors.foreground) .child(${this.count}), ) .child( Button.new(increment) .h(32) .px(14) .items_center() .justify_center() .bg(cx.theme().colors.primary) .text_color(cx.theme().colors.primary_foreground) .rounded(6) .on_click((_event, cx) { this.count 1; cx.notify(); }) .child(Increment), ); } }这个示例已经包含了这套运行时的大部分约定init只跑一次、用来初始化跨帧状态render返回一个元素、只在 View 失效时运行而非每帧样式方法使用 Rust 侧的snake_case拼写items_center、on_click、text_color并直接从cx.theme().colors读取语义主题色没有任何自更新机制——改完状态必须显式调用cx.notify()。完整的开发流程check、types、--watch热重载、CLI 命令参考见 getting-started.md。为什么插件优先一次决策如何塑造整个运行时crates/base/src/dock已经拥有了插件系统所需的一半纯数据的布局、用名字从持久化文件里重建面板的PanelRegistry以及跟随面板一起保存的serde_json::Value。缺的另一半是——面板的实现必须编译进宿主二进制不 fork 就没人能贡献。gpui-shell补上的正是这一半。插件优先不是一句定位口号它是下述一系列设计决策的根源。若只面向独立脚本应用这些决策每一项都会走另一条路决策为什么它源于插件Capabilities::default()是空集由宿主授予插件是别人写的代码授权必须是宿主的决定而不是插件在自身 manifest 里的自我声明每个插件独立的Policy卸载时取消所有携带它的任务多个插件共享一个运行时授权不能在插件之间互相渗漏脚本故障是可恢复的异常宿主进程存活一个坏掉的插件不应拖垮整个应用重绘重放 Snapshot绝不进入 VM帧预算由宿主负责插件的 JavaScript 不能压在上面HostModule把宿主自己的 Rust 借给脚本只有脚本运行在宿主内时才有意义——独立应用没有宿主可借Dock 面板在卸载后仍保留位置与状态插件会被安装和移除面板要回到原来的位置带着它原来的东西基础层不提供任何表现层所以脚本拥有全部表现插件必须长得像宿主的一部分这需要控制每一个像素独立脚本应用只用到其中很少一部分。它得到的是迭代速度——热重载、check、生成的gpui-kit.d.ts——所以它排在第二位它是插件被开发和验证的地方而不是这个运行时的目的本身。文本编辑、语法高亮、LSP、虚拟化和动效采样都留在 Rust。这是一条职责划分线而不是对脚本能力的限制宿主拥有一切必须贴近 GPU 和系统的能力因此插件永远不会成为应用性能或稳定性的变量。⚠️注意插件是目标但还不是完整的接口插件机制之下的机器已经构建并测试——manifest 解析与发现、加载与卸载、每插件策略与数据目录。脚本目前可以贡献面板并绘制 dock 的 chromeDockArea、dock_area(...)和DockArea.register_panel都是公开的含脚本面板的布局可以跨重启存活。仍然缺失的是其余贡献注册表gpui.command、gpui.keymap、授权 UI以及使用PluginManager的 CLI。今天能端到端跑通的是独立路径包含 dock。见 dock.md。架构脚本描述宿主渲染脚本从不持有 GPUI 元素。它记录一份描述——构建链中的每一次调用都把一条操作写进一个 arena元素描述竞技场Rust 在需要帧时把这些操作重放成真实元素。布局、绘制、命中测试、滚动、IME 和文本编辑都留在 Rust从不回调脚本。引擎是设计的一个参数而不是组成部分。目前只有 QuickJS 一个实现但接缝之上的所有东西——arena、materializer、调用作用域、样式表、主题、能力模型、浮层宿主、热重载——在源码中都不指名任何 VM。关于引擎接缝的完整讨论为什么存在、测量方法、接缝两侧各有什么见 engine.md。从仓库源码看引擎也确实被隔离在一个 cargo feature 后面crates/shell/Cargo.toml 中default [quickjs]引擎依赖通过 git 修订版本锁定这与文档描述的引擎可替换设计一致。能力一个完整的应用层而不是一组控件脚本拿到的是一个 Rust 应用在gpui-base上能拿到的一切元素与布局、链接与控件、基于语义主题令牌的流畅样式面、通过init/render/cx.notify()管理的 View 状态、保留的宿主状态如文本输入的 rope 与选区、对话框、sheet 与 toast、异步任务、原生转场与弹簧动画以及受门控的文件系统、存储、剪贴板、进程、HTTP、TCP 和 WebSocket 能力面。围绕这些能力的是开发工具链--watch保存即热重载gpui-shell.json在代码运行前声明身份与最小权限能力生成的gpui-kit.d.ts向编辑器或模型描述整个 APIcheck在应用运行前报告错误。gpui-kit.d.ts可以放进.gitignore——它是生成的。性能脚本不在帧路径上render不会每帧运行一次。它把界面描述进一份 Snapshot快照在下次cx.notify()之前每一次重绘都在 Rust 里重放这份 Snapshot。指针划过按钮、光标闪烁、列表滚动、原生转场或弹簧推进——这些都不运行 JavaScript。运行时把两类事件分开计数画廊的 Shell storycargo run -- shell把两个计数器同时显示在屏幕上界面正在做什么每秒帧数每秒 JavaScript 运行次数重绘且 JavaScript 读取的内容没有变化600价格每 50 ms 变动6019帧数属于显示JavaScript 次数属于数据。第二行里其余的 41 帧都在重放一份已经存在的描述。因此成本按用户动作支付而不是按帧支付。在一个 443 节点的面板上运行render并把整个界面记录进 Snapshot 耗时 1.1 ms只在状态变化时支付之后的每一帧耗时 1.3 ms那是渲染本身——把 Snapshot 变成元素、布局、绘制里面没有 JavaScript。每帧成本没有 Snapshot1.1 msJS 渲染 1.3 msRust 渲染2.4 ms/帧渲染有 Snapshot1.3 ms面板变大也不会改变这一点。engine.md 中的基准覆盖到 8,403 个节点任何规模的帧都不运行 JavaScript并且最小的规模在每次 CI 构建上都会被断言。详细的成本分解——描述成本脚本→Snapshot、物化成本Snapshot→GPUI 元素、完整缓存重绘成本以及 240–340 ns/次记录调用的 FFI 边界成本——都记录在 engine.md 中。性能的完整推论按 View 拆分边界、cx.notify()的正确姿势、帧率与呈现延迟的区别、如何读计数器见 performance.md。体积接入一个脚本运行时 13.5 MiB一个真正运行脚本应用的宿主发布体积为26.1 MiB的二进制常驻内存81 MiB其中包含 QuickJS 和整个标准运行时。去掉该依赖同样的应用只增加13.5 MiB 二进制和 14 MiB 内存。这个数字是常量而不是比例体积是它五倍的组件画廊也只增加同样的 13.5 MiB。实测环境是一台 MacBook ProM3、8 核、24 GB帧数和运行次数来自 Shell story毫秒数来自基准的 release 构建二进制与内存数字来自examples/hello_world与gpui-shellCLI 的 release 构建。二进制和内存花在哪里、为什么这个常量无法再压缩hyper、rustls、ring和解释器本身见 engine.md。安全默认一无所有语言被裁剪到与之匹配Capabilities::default()是空集——没有文件访问、没有存储、没有剪贴板、没有进程执行、没有网络。宿主在加载 View 之前决定授权View 一生都保持该授权fs表面上每一条路径都经过一个解析器任何落到已授权根目录之外的东西都会被拒绝。宿主侧对应的公开函数是 crates/shell/src/lib.rs 中的set_capabilities文档注释明确写着nothing is permitted until this is called。在授权之下沙箱还裁剪语言本身因为一个 VM 最终要托管多个插件eval和全部四个函数构造器被移除内置原型被冻结一个插件无法为另一个插件改动Object.prototype模块解析被限制在应用目录内堆256 MiB、解释器栈1 MiB和单次调用时间render内 50 ms都有上限。那个时间上限是一个catch块吞不掉的中断这一点由测试保证。完整的默认拒绝清单、manifest 写法、fs/storage/process/net各面以及沙箱资源上限表见 capabilities.md。值得单独列出的是 manifest 的形态。一个目录靠gpui-shell.json被识别manifest 是惰性数据——发现阶段只读取身份、可选的版本元数据、Git 依赖和请求的权限不执行入口模块。它识别id、name、version、shell-version、entry、dependencies和capabilities只有id、name、entry是必需的{ id: com.example.quotes, name: Quotes, version: 1.0.0, shell-version: 0.6.0, entry: main.js, dependencies: { omarchy-ui: huacnlee/omarchy-ui }, capabilities: { fs: { read: [${pluginDir}], write: [${dataDir}] }, network: { hosts: [stream.example.com], http: [ { scheme: https, host: api.example.com, methods: [GET], path_prefixes: [/v1/] } ] }, storage: true, clipboard: { read: false, write: true }, process: { exit: false } } }该块中每一项在缺省时都默认拒绝唯一例外是storage默认授予——写storage: false即可拒绝。每个拒绝都会给出可操作的修复提示例如filesystem read is not granted; declare capabilities.fs.read in the manifest而不是模糊的报错。宿主侧授予能力的 Rust API 形态如下摘自 capabilities.mdgpui_kit::shell::set_capabilities( Capabilities::new() .read_roots([application_root.clone()]) .write_roots([data_directory.clone()]) .storage(true) .exit(true), );脚本如何变成界面三次消费带来的三个推论GPUI 的元素是使用时被消费的值RenderOnce::render以self按值接收、.child()按值接收其子元素、View 每次重绘都要重建整棵元素树。因此 JavaScript 对象永远不可能是一个 GPUI 元素——它没有东西可以握住。所以脚本不构建元素它描述元素。构建链中的每一次调用都在元素描述 arena 里记录一条操作脚本持有的对象只是一个指向该 arena 的整数索引。当 GPUI 要求 View 渲染时Rust 把记录的操作重放成真实元素、交给 GPUI然后清空 arena。布局、绘制、命中测试、滚动和 IME 永远不会回到脚本。三个推论直接由此而来每个都有专门页面元素是单次使用的。描述在一次渲染结束后就没了所以一个被存起来的元素在下次使用时抛错而不是画出意外的东西。见 elements.md。交给调用的cx属于那次调用。它携带一个代数generation与活着的调用栈对照检查跨await保留的cx会报出明确错误而不是触碰一个已死的栈帧。见 state.md。回调属于注册它们的这次 render。下次 render 会整体替换它们这正是脚本闭包不会在宿主里累积的原因。见 elements.md。三者都源于把一个脚本绑定到会消费值的元素模型这件事本身。表现层属于脚本大多数脚本层会给脚本一组现成的控件让它排列。这里没有现成控件可给因为下面的层同样没有。gpui-base的控件不携带任何视觉样式。Rust 里的Button::new(save)没有内边距、没有背景、没有圆角、没有尺寸——这就是契约。JavaScript 绑定原样保留Button.new(save)不写样式就只画它的子元素。推论就是重点因为基础层不提供表现层脚本拥有全部表现——每一种颜色、每一像素间距、每个 hover 状态、每个圆角。这与 Rust 应用选择gpui-base而非gpui-component时做的取舍完全相同区别在于在这里这个取舍发生在一个你可以保存并立刻看到结果的文件里中间不需要cargo build。作为回报脚本得到的是整个应用层改一个按钮的圆角不需要回到 Rust。完整的样式文法无参反射方法 vs 手工绑定的 57 个参数化方法、长度与颜色语法、语义令牌、状态样式、原生动效见 styling.md。它适合放在哪里给现有 GPUI 应用加插件支持——首要场景。插件在宿主进程内运行权限由宿主逐项授予、从零开始。产品扩展不再意味着 fork 或发新版本界面和业务逻辑以脚本形式交付无需重编译或重新分发二进制出故障的插件表现为可恢复的错误而不是拖垮宿主。在gpui-shell上用 JavaScript 写完整应用——次要场景。整个应用层元素、样式、View 状态、浮层和系统 API都在而渲染、文本编辑、虚拟化和每一帧动画都留在 Rust。这也是插件在被挂载进宿主之前编写和验证的地方。它坐在哪里分层与定位JavaScript application main.js · Views · styles · business logic │ import { … } from gpui-kit ▼ gpui-shell engine seam · element descriptions · call scope style table · theme tokens · capabilities ShellRoot (dialogs, sheet, toasts) · scheduler │ ▼ gpui-base behavior · state · infrastructure (no style) │ ▼ gpui elements · styling · rendering · GPU · platformgpui-shell与gpui-component是并排关系而非上下关系两者都是gpui-base的消费者都提供 Base 没有的表现层。gpui-component用 Rust 提供一套完整自洽的表现gpui-shell提供的是让脚本提供自己表现层的机器。当前状态M0 里程碑该 crate 处于里程碑M0一个可行性基线而非稳定接口。它没有发布到 crates.io脚本 API 预期会变化。本文档所述的内容都存在且可用缺失的部分在其对应页面上有明确标注。设计文档位于 docs/gpui-shell.mdcrate 本体位于 crates/shell。继续深入配套文档导航页面覆盖内容Getting started运行示例、最小应用、check与typesExamples仓库中的两个应用以及可以从它们复制什么Elements构造器、child/children/when、元素为什么单次使用Styling流畅样式面、长度、颜色令牌与状态样式State and Viewsinit/render、cx.notify()、保留状态、异步Overlays对话框、sheet、toasts 与阶段规则Capabilitiesgpui-shell.json、默认拒绝、文件系统、存储、进程与网络 APIDependenciesShell 包如何构成、manifest 如何命名与锁定、编辑器得到什么类型Hosting完整的 Rust 侧挂载、刷新、指标、退出、热重载HostModule把宿主自己的 Rust 借给脚本以及纯数据边界Dock and Panels脚本 View 作为可停靠面板、你为它画的 chrome、重启后保留什么Performance脚本的成本失效 vs 描述规模、View 作为边界、计数器The engine seamQuickJS、接缝为何存在、区分脚本成本与帧成本的测量【免费下载链接】gpui-kitRust GUI components for building fantastic cross-platform desktop application by using GPUI.项目地址: https://gitcode.com/GitHub_Trending/gp/gpui-kit创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
返回列表