Esta página foi traduzida automaticamente e pode conter erros. Ler o original em inglês

Documentação

Introdução

Native Shell (NSH, para abreviar) é um framework de UI nativa para todas as plataformas. Você cria um app uma vez, com um único modelo de app (componentes, signals, estilos e a mesma API em todos os lugares), e o NSH o executa em iOS, Android, macOS, Windows, Linux e na web, sempre como um app daquela plataforma: feito com as próprias views, editado com o próprio sistema de texto, usado do jeito que seus usuários esperam. Escreva o app inteiro em Rust, escreva telas em TSX, ou combine os dois em um único engine. Sobre o mesmo engine, apps que você já publicou podem receber novas UIs em tempo de execução, escritas pela sua equipe ou geradas por um modelo, como módulos assinados, em um sandbox configurado pelo host.

#O que significa nativo

Nativo no NSH tem duas camadas: os fundamentos da plataforma e a interação da plataforma.

Os fundamentos da plataforma. Cada plataforma tem um backend próprio, construído sobre a UI que a plataforma já tem:

Plataforma Views Texto Entrada de texto
iOS UIKit CoreText Os campos de texto e as views de texto do sistema
Android Views do Android StaticLayout EditText
macOS AppKit, Core Animation CoreText NSTextField, NSTextView
Windows Janelas Win32 desenhadas com Direct2D DirectWrite O editor do NSH, com os métodos de entrada do sistema (IMM32)
Linux GTK 4 Pango Text e TextView do GTK
Web Elementos HTML estilizados com CSS As fontes do navegador Os inputs do navegador

A seleção de texto e seu menu, os métodos de entrada, a acessibilidade e a rolagem vêm da plataforma, e o que uma nova versão do SO traz para eles chega ao app junto. Componentes nativos (um mapa, uma web view, um player de vídeo, qualquer view que o host registre) são mais uma view na mesma hierarquia, com layout, recorte e rolagem iguais aos das demais (Componentes nativos).

A interação da plataforma. Os componentes que o NSH fornece em nsh/base se comportam como os usuários de cada dispositivo esperam, então o app os escreve uma única vez:

Desktops (e navegadores desktop) Celulares e tablets
Menu Um popup junto ao gatilho; os submenus se abrem em cascata ao lado do item Uma sheet vinda da parte inferior; um submenu substitui os itens do seu menu
ContextMenu Um clique com o botão direito o abre no ponteiro Um dedo mantido pressionado no gatilho o abre como uma sheet
Tooltip Exibido quando o ponteiro repousa sobre o gatilho Exibido quando o app o abre
Popover Um clique fora o fecha e alcança o que está sob o ponteiro Um toque fora o fecha

Páginas, diálogos, o teclado e a barra de status seguem a mesma ideia: as pilhas de navegação fazem push e pop com o movimento de cada plataforma, e o botão voltar, o Escape e o gesto de voltar deslizando fazem o que fazem em cada uma (Navegação).

#Do celular ao desktop e à web

Multiplataforma no NSH significa que cada dispositivo recebe o app que espera, e não a mesma tela ampliada ou reduzida:

  • Um único modelo de app. Os componentes, o estado, a lógica de negócio e as telas do app são compartilhados; cada backend os mapeia para a sua plataforma.
  • Layouts que se remodelam. Flexbox e CSS grid têm o layout calculado pelo Taffy em todas as plataformas, e os breakpoints do Tailwind (md:, lg:) e o tamanho da janela (window() em Rust) mudam a estrutura, não só o tamanho: um app de chat mostra a barra lateral, as conversas e as mensagens lado a lado em uma janela larga, e uma tela de cada vez em um celular (Layout, Tailwind).
  • A entrada que cada dispositivo espera. Ponteiro, hover, cliques com o botão direito, atalhos e foco no desktop; toques, gestos, teclado virtual e navegação entre páginas nos celulares. Quando um app quer algo próprio para cada plataforma, as props desktop={{…}} e platform.select permitem isso (Código específico de plataforma).
  • Verificado em todos os alvos. nsh test executa os testes do app em modo headless, e os mesmos testes em janelas macOS, Windows, Linux e GPUI, em um simulador de iOS, em um dispositivo Android ou em um navegador, com a entrada da própria plataforma (Testes e tracing).

Pense em um app como um cliente de chat. Seu desenvolvedor o constrói no desktop com nsh run, abre-o em um simulador de iOS com nsh run --target ios para verificar o layout de celular e depois em um navegador com nsh run --target web, sempre com o mesmo projeto, e nsh package gera os apps instaláveis e o site. Uma única pessoa engenheira consegue carregar o que antes exigia uma equipe por plataforma, enquanto o backend é construído em paralelo.

#Rust em primeiro lugar

O NSH é construído em Rust, e Rust é uma linguagem de primeira classe para escrever apps com ele:

  • Apps inteiros em Rust. UI, lógica e componentes são Rust, compilados para código nativo em todas as plataformas, com a posse (ownership), os tipos, as threads e o ecossistema do crates.io do Rust. A computação roda em velocidade nativa, e o processamento de dados grandes, o parsing e o trabalho de sistema ficam na linguagem mais adequada para eles (Building with Rust).
  • TSX como segundo caminho. O TSX serve para telas escritas rapidamente, plugins, apps pequenos e UI entregue em tempo de execução. Ele roda no mesmo engine que o Rust, com as mesmas primitivas, os mesmos componentes base e os mesmos signals.
  • Os dois juntos. Um app Rust pode receber telas em TSX, e as telas em TSX importam os componentes, módulos e classes Rust do app.
  • Uma única toolchain. Um único executável nsh cria, compila, verifica tipos, executa com hot reload, testa, empacota e assina apps para todos os alvos. Ele traz seu próprio bundler de TSX e seu próprio verificador de TypeScript, de modo que quem vem de Rust, Swift, Kotlin ou C++ instala o Rust e o nsh, e já pode trabalhar (Instalação, CLI).

#Um app

O mesmo contador, em Rust e em TSX:

use nsh_core::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())),
        )
}

Uma view (view(), <View>) se torna um UIView no iOS, um ViewGroup no Android, um NSView no macOS, um widget GTK no Linux, uma view retida desenhada com Direct2D no Windows e um elemento HTML na web; um texto se torna uma view de texto nativa, e o feedback de pressão do botão (.active(..), activeStyle) roda na thread principal. Quando você toca, count.update altera um signal, e apenas o texto que o lê é atualizado: a função do componente não roda de novo. O app Rust é código nativo do começo ao fim; no app TSX, apenas o código do próprio módulo é JavaScript.

#Views retidas

O NSH é retido (retained): cada elemento é uma view que a plataforma mantém, e uma atualização altera apenas o que mudou.

  • Atualizações de granularidade fina. Signals como os do Solid, implementados nativamente: os componentes rodam uma vez, e uma mudança reexecuta apenas as expressões que leem o signal alterado.
  • Só o que mudou cruza threads. As mudanças de um turno têm o layout calculado em conjunto e são entregues à thread principal como um único lote de operações de view por frame; estilos e frames inalterados não são enviados, e as views em que nada foi tocado permanecem como estão.
  • As animações rodam nativamente. Animações de timing, spring e decay, gestos e valores vinculados à rolagem são avaliados no relógio de frames da plataforma, sem chamar o JS a cada frame (Animação).

Isso serve às telas de que a maioria dos apps é feita (chats, e-mail, formulários, listas, documentos, configurações, uma resposta de IA chegando em streaming), em que poucas views mudam por vez e o resto espera. Essas telas ficam quase ociosas entre as atualizações, e a rolagem e as animações sustentam 120 FPS em telas ProMotion. No benchmark de desempenho do NSH, uma tabela de cotações com 500 linhas atualizada 1.000 vezes por segundo usa 18% de um núcleo no Linux com NSH Rust, contra 20% para a mesma tela escrita diretamente no GPUI Fast. A computação pesada pertence ao Rust: o TSX roda no QuickJS, um interpretador, enquanto um módulo Rust roda em velocidade nativa mesmo quando chamado pelo TSX; ordenar 100.000 linhas leva cerca de 3 ms dessa forma, como em Rust puro (Desempenho).

#Como ele roda

A toolchain é um único binário nsh. Um app é escrito em Rust, em TSX ou em ambos, e cada um segue seu próprio caminho até o mesmo engine Rust.

#Apps Rust

Um app Rust é um crate, construído inteiramente com Rust e Native, sem runtime de JavaScript:

  1. Build. nsh compila o runner de cada alvo (o app iOS, Android, macOS, Windows, Linux ou web) com o seu crate vinculado. Componentes, estado, handlers e o engine são compilados juntos em código nativo.
  2. Execução. O componente raiz do app roda na thread do engine como Rust. O runner não tem engine de JavaScript: o QuickJS não faz parte de um app Rust em nenhuma plataforma.

#Apps TSX

Um app TSX (um módulo) é um conjunto de arquivos .tsx / .ts com uma entrada, geralmente App.tsx:

  1. Build. nsh agrupa os arquivos do módulo em um bundle (com rolldown e Oxc) e compila o JSX com seu próprio compilador. Os imports de nsh e nsh/* permanecem como imports: o runtime os fornece nativamente, de modo que o bundle contém apenas o seu código. O bundle é compilado para bytecode do QuickJS.
  2. Execução. A thread do engine executa o bytecode no QuickJS, uma VM por view. O JSX compilado chama o renderizador nativo diretamente, e o JavaScript apenas descreve a UI e trata eventos; o parsing de estilos, a resolução de classes do Tailwind, o diff, o layout e o texto acontecem todos em Rust.

#A diferença

App Rust App TSX
Escrito em Rust TSX (TypeScript e JSX)
Compilado para Código nativo, vinculado ao runner de cada alvo Bytecode do QuickJS, carregado pelo runtime
Engine de JavaScript Nenhum QuickJS, uma VM por view
Durante o desenvolvimento (nsh run) Recompilado e reiniciado a cada salvamento Atualizações a quente a cada salvamento
Após o lançamento Vai junto com o app Também entregue em tempo de execução como pacotes assinados, em um sandbox
Serve para Apps inteiros, computação pesada, trabalho de sistema Telas escritas rapidamente, plugins, UI gerada por IA

Os dois se combinam: um app Rust pode receber telas em TSX, e as telas em TSX importam seus componentes, módulos e classes Rust; um app assim vincula o QuickJS para executá-las (Building with Rust).

#O engine compartilhado

A partir do engine, os dois caminhos são iguais. As mudanças são coletadas por turno, têm o layout calculado com o Taffy (flexbox e CSS grid) e são entregues à thread principal como um único lote de operações de view por frame; a thread principal aplica o lote às views nativas. O texto é medido e disposto uma única vez na thread do engine (CoreText nas plataformas Apple, StaticLayout no Android, DirectWrite no Windows, Pango no Linux, as métricas de fonte do navegador na web) e apenas desenhado na thread principal.

Na web, o mesmo engine roda na página: o navegador exibe elementos HTML estilizados com CSS, faz a rolagem deles, seleciona o texto e trata a entrada, e nsh package --target web publica o app como um site estático cujos arquivos são nomeados pelo conteúdo, pronto para qualquer CDN (Web).

#UI dinâmica e UI gerada por IA

O NSH começou como uma forma de os apps nativos receberem UI depois do lançamento, e esse continua sendo um de seus principais usos. Um app incorpora um NshAppView (iOS), NshView (Android), AppView (apps macOS, Windows, Linux e GPUI) ou createPlayer (uma página web) no ponto onde um módulo deve aparecer, e o NSH executa módulos TSX dentro dele:

  • Entregue após o lançamento. Um módulo é um pacote assinado (.nshpkg) que o app baixa em tempo de execução. O app verifica a assinatura com as chaves em que confia e com uma lista de revogação antes de executá-lo (Pacotes assinados). O app host distribui o runtime; as equipes criam módulos com base na API dele e os entregam no próprio ritmo.
  • O host mantém o controle. Cada módulo roda em sua própria VM QuickJS, com limites de memória, de tempo de chamada e de nós, e acessa apenas o que o host concede: hosts de rede, páginas web, a barra de status (Sandbox e capacidades). Vários módulos rodam lado a lado.
  • Tem a aparência do restante do app. Os módulos são renderizados com a UI própria da plataforma, e o host pode registrar suas próprias views nativas (um mapa, um player, um componente de design system) para os módulos usarem (Componentes nativos).

O mesmo mecanismo permite que um app responda com uma interface. Um modelo escreve um pequeno módulo TSX (um formulário, um gráfico, um cartão de reserva), seu servidor o verifica, compila e assina, e o app o exibe de forma nativa:

From a prompt to native UI: a model writes a TSX module; your server builds, checks and signs it; across the trust boundary, the app verifies the package, runs it in a sandbox with the limits and capabilities it set, and shows it with the platform's own UI. From a prompt to native UI: a model writes a TSX module; your server builds, checks and signs it; across the trust boundary, the app verifies the package, runs it in a sandbox with the limits and capabilities it set, and shows it with the platform's own UI.

No iOS, a parte do host são poucas linhas:

let app = NshAppView()                         // a UIView: place it anywhere
try app.grantCapabilities(#"{ "network": { "hosts": ["api.example.com"] } }"#)
let info = try await app.loadPackage(at: downloadedFile, trust: trust)   // verified, then run

#Amigável para IA

O NSH foi projetado para software escrito com IA, tanto apps inteiros quanto telas geradas:

  • Um modelo de app para todas as plataformas. Um modelo aprende uma única forma de escrever UI, e o resultado roda em celulares, desktops e na web; o NSH adapta a interação a cada plataforma, de modo que nada precisa ser gerado seis vezes.
  • Rust ou TSX, conforme a tarefa. Rust para apps inteiros e módulos críticos em desempenho, TSX para telas, plugins e UI gerada sob demanda, tudo em um único engine.
  • Um contrato preciso e gerado. Cada elemento, prop, chave de estilo e módulo tem declarações TypeScript geradas a partir do código-fonte em Rust (.nsh/types), e as APIs em Rust são tipadas, de modo que o código gerado é verificado por um compilador. Esta documentação é servida em texto simples para modelos (llms.txt, llms-full.txt).
  • Uma única linha de comando para todo o ciclo. Um agente cria o app, o compila, o executa em um destino, roda seus testes, lê o resultado, corrige e empacota, tudo por meio de nsh. O nsh test roda em modo headless, sem dispositivo, e relata erros e as views produzidas; o compilador também roda no mesmo processo (nsh_bundler::compile_module) e no navegador (Playground).
  • Seguro para executar quando não é confiável. O código gerado e entregue em tempo de execução é código não confiável: ele chega assinado, roda em uma VM com limites e acessa apenas as capacidades que o host concede.

#Onde roda

Plataforma Requer Backend Incorporado com
iOS iOS 15.0+ UIKit NativeShell.xcframework
Android Android 9+ Android views nsh.aar
macOS macOS 14+ AppKit AppView
Windows Windows 10 1809+ Win32, Direct2D AppView
Linux GTK 4.14+ GTK 4 AppView
Web Navegadores modernos HTML createPlayer, iframe
GPUI Desktops GPUI AppView (apps GPUI, GPUI Kit)
Mac Catalyst macOS UIKit Para desenvolvimento (--target catalyst)
One module, many hosts: the same bundle and Rust runtime on iOS, Android, Linux, macOS with AppKit and Windows with Win32 / Direct2D, GPUI on all three desktops, and web pages drawn with HTML. One module, many hosts: the same bundle and Rust runtime on iOS, Android, Linux, macOS with AppKit and Windows with Win32 / Direct2D, GPUI on all three desktops, and web pages drawn with HTML.

#Native Shell e GPUI Shell

A Longbridge desenvolve o NSH para seus próprios clientes. A primeira necessidade era uma camada de plugins para o cliente mobile, e o GPUI Shell, que torna os apps desktop GPUI extensíveis em JavaScript, foi o ponto de partida. Em celulares, uma UI desenhada pelo próprio GPUI teria que reconstruir o que a plataforma já faz (a seleção de texto e seu menu, com Copiar, Compartilhar, Traduzir e Buscar, os métodos de entrada, a acessibilidade e os materiais visuais que cada versão do iOS traz), e cada equipe mobile teria que adotar uma nova stack de UI. Por isso, o NSH renderiza com a UI própria de cada plataforma, e deixou de ser uma camada de plugins para se tornar um framework para apps inteiros em todas as plataformas.

Os dois têm posicionamentos diferentes. O GPUI Shell estende aplicativos desktop GPUI; o Native Shell é um framework para apps nativos em celulares, desktops e na web, e seu backend GPUI incorpora apps NSH em apps GPUI e GPUI Kit (GPUI).

#Como se compara

Para apps inteiros, o NSH se situa entre frameworks multiplataforma como React Native, Flutter, Qt, GPUI, Slint, Electron e Tauri; para UI entregue após o lançamento, entre web views, UI orientada pelo servidor e containers de mini programas. Sua escolha é a UI e a interação próprias de cada plataforma sob um único modelo de app, com Rust como linguagem do app e TSX ao lado. Comparação de abordagens examina cada uma.

#Próximos passos