Rust para Aplicativos Móveis: Android e iOS com JNI, UniFFI e Tauri Mobile em 2026

Como levar Rust para apps Android e iOS em 2026: JNI, bindings nativos, UniFFI, cargo-mobile, Tauri 2 Mobile, integração com Kotlin/Swift, toolchains cross-compile, CI e quando vale a pena frente a Dart, C++ e Kotlin Multiplatform.

Por que Rust aparece cada vez mais dentro de apps Android e iOS

Durante anos a regra era simples: Android em Kotlin/Java, iOS em Swift/Objective-C, e o máximo de compartilhamento entre as duas plataformas vinha de C++ nativo ou de frameworks como Flutter e React Native. O problema é que C++ é custoso de manter e expõe falhas de memória e concorrência, enquanto as ferramentas cross-platform baseadas em linguagens gerenciadas não entregam o desempenho necessário para criptografia, parsing, sync offline, compressão e inferência de modelos em dispositivos fracos.

Rust mudou esse cenário porque oferece três coisas ao mesmo tempo: desempenho equivalente ao C++, garantias de memória e concorrência sem garbage collector, e bindings interoperáveis com Kotlin, Swift, JavaScript e Python. O resultado é um padrão arquitetural que empresas como Mozilla, Cloudflare, Signal, 1Password e Discord já usam em produção: um núcleo Rust compartilhado entre backend, web e mobile, com a UI permanecendo nativa em cada plataforma.

Para quem busca vagas Rust e quer atuar no mercado mobile, esse é um nicho em ascensão. Times brasileiros de fintech, saúde digital e mensageria já procuram engenheiras e engenheiros que saibam expor uma biblioteca Rust como SDK consumível por Kotlin e Swift. A carreira Rust em mobile ainda é pouco disputada comparada ao backend puro, o que faz do domínio de UniFFI, JNI e Tauri Mobile um diferencial concreto — visível inclusive no nosso comparativo Rust vs C++ em 2026.

Os quatro caminhos para levar Rust ao mobile em 2026

Existem quatro estratégias práticas, cada uma com custo e ganho diferentes. Escolher a certa antes de começar economiza semanas de retrabalho.

1. Núcleo compartilhado com UniFFI — A Mozilla mantém o UniFFI para gerar bindings idiomáticos a partir de uma interface declarativa. Você escreve a lógica em Rust, descreve a API em um arquivo .udl (ou usando macros proc #[uniffi::export]), e o UniFFI gera código Kotlin para Android e Swift para iOS pronto para uso. É o caminho mais limpo para novo código e o que melhor envelhece, porque isola o desenvolvedor mobile dos detalhes de FFI.

2. JNI direto no Android — Quando você precisa de controle fino sobre a JVM, ou quando integra código legado, chamar Rust via JNI continua sendo a rota. O crate jni encapsula a ligação com JNIEnv, permite registrar callbacks e manipular objetos Kotlin a partir do Rust. É mais verboso e exige cuidado com threads (a JNIEnv é por-thread), mas é o padrão para interagir com APIs da plataforma Android.

3. Tauri 2 Mobile — O Tauri, já coberto no nosso guia de GUI em Rust 2026, ganhou no Tauri 2 suporte oficial a Android e iOS. Você escreve a UI em HTML/JS (ou em frameworks como Svelte, Vue, React) e o backend em Rust, usando o WebView nativo de cada plataforma. É uma boa escolha quando a equipe já domina web e quer um app com footprint pequeno, binários leves e lógica de domínio em Rust.

4. Cross-compile manual para bibliotecas nativas — Em projetos que só precisam embutir uma .so ou .framework sem camada de alto nível, compilar diretamente com as toolchains NDK e iOS SDK resolve. É o que fazem muitos SDKs de criptografia e parsing, expostos depois via FFI bruto ou via UniFFI.

Preparando a toolchain: NDK e iOS SDK

Antes de escrever código, o ambiente precisa saber compilar para os alvos mobile. O ponto de partida é instalar os alvos Rust corretos e configurar linkers dedicados.

# Alvos Android (escolha conforme a ABI alvo)
rustup target add aarch64-linux-android armv7-linux-androideabi x86_64-linux-android i686-linux-android

# Alvos iOS
rustup target add aarch64-apple-ios aarch64-apple-ios-sim x86_64-apple-ios

Para Android, instale o Android NDK (via Android Studio ou sdkmanager) e configure o linker de cada alvo em ~/.cargo/config.toml. O caminho do NDK muda a cada versão; em 2026 a prática recomendada é usar a toolchain llvm dentro de ndk/toolchains/llvm/prebuilt/<host>/bin.

# .cargo/config.toml do projeto
[target.aarch64-linux-android]
linker = "/Android/Sdk/ndk/27.0.12077973/toolchains/llvm/prebuilt/linux-x86_64/bin/aarch64-linux-android24-clang"

[target.armv7-linux-androideabi]
linker = "/Android/Sdk/ndk/27.0.12077973/toolchains/llvm/prebuilt/linux-x86_64/bin/armv7a-linux-androideabi24-clang"

Para iOS, a integração com Xcode é mais simples porque o cargo-lipo foi incorporado ao Rust. Basta compilar com cargo build --target aarch64-apple-ios e depois agrupar as arquiteturas em um fat binary .a, que entra como framework estático no projeto Xcode.

A ferramenta cargo-ndk automatiza essa configuração no Android, aceitando o nível de API (--android-platform 24) e repassando para o cargo. Em CI, é o que mantém os builds reproduzíveis entre GitHub Actions, GitLab CI e runners locais.

UniFFI: o caminho idiomático

O UniFFI é hoje a forma mais produtiva de expor Rust para mobile. O fluxo começa com uma crate de biblioteca cuja API pública é descrita em uma interface. Veja um núcleo mínimo que criptografa e descriptografa uma mensagem — exemplo realista de uma carteira digital.

# Cargo.toml
[lib]
crate-type = ["cdylib", "staticlib", "lib"]

[dependencies]
uniffi = { version = "0.28", features = ["cli"] }

[build-dependencies]
uniffi = { version = "0.28", features = ["build"] }
// src/lib.rs
uniffi::include_scaffolding!("wallet");

pub struct MensagemCriptografada {
    pub nonce: Vec<u8>,
    pub cifrado: Vec<u8>,
}

pub fn cifrar(chave: Vec<u8>, texto: String) -> Result<MensagemCriptografada, ErroCarteira> {
    if chave.len() != 32 {
        return Err(ErroCarteira::ChaveInvalida);
    }
    // Lógica real usando, por exemplo, a crate `crypto-box` ou `aes-gcm`
    Ok(MensagemCriptografada { nonce: vec![0u8; 12], cifrado: texto.into_bytes() })
}

pub fn decifrar(chave: Vec<u8>, msg: MensagemCriptografada) -> Result<String, ErroCarteira> {
    if chave.len() != 32 {
        return Err(ErroCarteira::ChaveInvalida);
    }
    Ok(String::from_utf8(msg.cifrado).map_err(|_| ErroCarteira::ConteudoInvalido)?)
}

#[derive(Debug, thiserror::Error, uniffi::Error)]
pub enum ErroCarteira {
    #[error("chave inválida")]
    ChaveInvalida,
    #[error("conteúdo inválido")]
    ConteudoInvalido,
}

A interface em src/wallet.udl descreve os tipos expostos:

namespace wallet {
  [Throws=ErroCarteira]
  MensagemCriptografada cifrar([ByRef] sequence<u8> chave, string texto);

  [Throws=ErroCarteira]
  string decifrar([ByRef] sequence<u8> chave, MensagemCriptografada msg);
};

dictionary MensagemCriptografada {
  sequence<u8> nonce;
  sequence<u8> cifrado;
};

[Error]
enum ErroCarteira {
  "ChaveInvalida",
  "ConteudoInvalido",
};

Ao rodar cargo build --target aarch64-linux-android, o UniFFI gera o arquivo wallet.kt com classes idiomáticas em Kotlin. No iOS, cargo build --target aarch64-apple-ios produz wallet.swift. O desenvolvedor mobile simplesmente importa o arquivo no projeto e consome a API como se fosse nativa:

// Android (Kotlin)
import uniffi.wallet.cifrar
import uniffi.wallet.ErroCarteira

try {
    val msg = cifrar(chave = chave32bytes, texto = "segredo")
    saveToDisk(msg)
} catch (e: ErroCarteira) {
    Log.e("Wallet", "falha ao cifrar", e)
}
// iOS (Swift)
import wallet

do {
    let msg = try cifrar(chave: chave32bytes, texto: "segredo")
    try saveToDisk(msg)
} catch let e as ErroCarteira {
    print("falha ao cifrar: \(e)")
}

Esse desenho entrega três vantagens concretas. Primeiro, o tratamento de erro é tipado em ambas as plataformas — ErroCarteira vira uma sealed class em Kotlin e um enum em Swift. Segundo, os tipos de dados (records/dictionaries) cruzam a fronteira sem código manual de marshalling. Terceiro, o mesmo binário Rust pode ser testado em testes unitários antes de sequer tocar o lado mobile, reduzindo o ciclo de feedback.

JNI direto: controle fino da VM Android

Quando o UniFFI não cobre o caso — tipicamente em integrações com APIs específicas do Android, como Context, ContentResolver ou ciclo de vida de Activity — a rota continua sendo JNI via crate jni. O padrão é registrar uma função #[no_mangle] que recebe um JNIEnv e um JClass:

use jni::objects::{JClass, JString, JValue};
use jni::JNIEnv;

#[no_mangle]
pub extern "system" fn Java_br_rustbr_wallet_Wallet_cifrar<'local>(
    mut env: JNIEnv<'local>,
    _class: JClass<'local>,
    chave: JString<'local>,
    texto: JString<'local>,
) -> JString<'local> {
    let chave: String = env.get_string(&chave).unwrap().into();
    let texto: String = env.get_string(&texto).unwrap().into();

    let resultado = format!("cifrado:{}:{}", chave.len(), texto.len());

    env.new_string(resultado).unwrap()
}

O lado Kotlin declara a função como external dentro de um companion object e carrega a biblioteca nativa:

object Wallet {
    init { System.loadLibrary("wallet") }
    external fun cifrar(chave: String, texto: String): String
}

A atenção necessária com JNI é real: JNIEnv só é válido na thread que o recebeu, então qualquer chamada a partir de uma task Tokio precisa ser precedida por env.attach_current_thread(); strings UTF-8 precisam ser convertidas com cuidado; e exceções lançadas a partir de Kotlin tornam o estado da JVM inconsistente até a próxima chamada detectar env.exception_check(). Para código novo, UniFFI cobre 90% dos casos sem esse custo; JNI direto fica reservado para integrações profundas com o framework Android.

Tauri 2 Mobile: web UI sobre núcleo Rust

O Tauri 2 trouxe oficialmente o mobile como cidadão de primeira classe. Em vez de gerar bindings manuais, você declara comandos Rust consumíveis pelo frontend JavaScript, e o Tauri cuida do transporte entre WebView e backend nativo. Veja um projeto mínimo:

cargo install create-tauri-app
cargo create-tauri-app wallet-app
# escolha: TypeScript + Svelte, gerenciador pnpm
cd wallet-app
cargo tauri android init
cargo tauri ios init
cargo tauri android dev

No src-tauri/src/lib.rs, o comando exposto fica assim:

#[tauri::command]
fn cifrar(chave: String, texto: String) -> Result<String, String> {
    if chave.len() != 32 {
        return Err("chave deve ter 32 bytes".into());
    }
    Ok(format!("cifrado:{}:{}", chave.len(), texto))
}

#[cfg_attr(mobile, tauri::mobile_entry_point)]
pub fn run() {
    tauri::Builder::default()
        .invoke_handler(tauri::generate_handler![cifrar])
        .run(tauri::generate_context!())
        .expect("erro ao iniciar o app");
}

O frontend chama o comando pela API do Tauri:

import { invoke } from "@tauri-apps/api/core";

const resultado = await invoke<string>("cifrar", {
  chave: chave32bytes,
  texto: "segredo",
});

O ganho do Tauri Mobile é concentration de stack: o mesmo código Rust, a mesma UI web, deployando para Android, iOS, Linux, macOS e Windows. O custo é depender do WebView nativo (o que varia entre dispositivos Android e exige cuidado com versões mínimas), e o tamanho de binário maior do que uma biblioteca pura. Em 2026 ele já é uma escolha madura para apps corporativos e de produtividade, mas ainda não substitui o desenho UniFFI + UI nativa para apps com exigências visuais profundas.

CI: cross-compile reprodutível em pipelines

O calcanhar de Aquiles do Rust mobile é o CI lento. Cada arquitetura exige um build independente e, em apps reais, são quatro alvos Android mais dois iOS. As práticas que mantêm o pipeline saudável são três.

Primeiro, use cache de cargo com escopo por alvo. Ferramentas como Swatinem/rust-cache distinguem targets por sharedKey, evitando invalidar o cache inteiro quando só um alvo muda. Segundo, paralelize os alvos em jobs distintos, cada um publicando o .so/.a como artefato. Um job final de montagem junta tudo no .aar (Android) ou .xcframework (iOS) consumido pelos builds nativos. Terceiro, prefira cargo-ndk e xcodebuild com versões fixas, e versione o NDK no repositório — atualizar o NDK sem testar quebra builds silenciosamente.

Um padrão comum em SDKs brasileiros é manter um repositório separado só para a biblioteca Rust, que publica artefatos no GitHub Releases ou em um repositório Maven privado. O app Android consome via Gradle como qualquer .aar, e o app iOS via Swift Package Manager apontando para o .xcframework. Isso separa o ciclo de release da UI do ciclo de release do núcleo — fundamental quando o núcleo é compartilhado entre mobile, web e backend.

Desempenho e tamanho de binário

Levar Rust ao mobile só vale quando o ganho de desempenho ou a garantia de segurança compensam o custo de manter uma toolchain extra. Os pontos onde Rust brilha em apps reais são consistentes: criptografia ponta a ponta (reduz latência de handshake em 40-70% vs equivalente em linguagens gerenciadas), parsing de JSON/protobuf grandes em streaming, compressão de imagens e áudio, inferência de modelos pequenos com ort (ONNX Runtime), e sync offline baseado em CRDTs — tema que aprofundamos em Rust local-first e CRDT.

Para manter o tamanho do binário baixo, três práticas: ative lto = "thin" e codegen-units = 1 no Cargo.toml do release, use panic = "abort" quando o app puder travar e reiniciar (comum em mobile), e audite dependências grandes com cargo bloat. Uma biblioteca UniFFI bem ajustada entrega .so de 1-3 MB por arquitetura; um app Tauri Mobile completo costuma ficar entre 8 e 15 MB, ainda bem abaixo de equivalentes Electron.

Quando Rust no mobile faz sentido (e quando não faz)

A pergunta honesta antes de começar é: este app realmente precisa de Rust? A resposta costuma ser sim quando pelo menos um destes pontos aparece:

  • há um núcleo de lógica reutilizável entre Android, iOS e web (ou backend);
  • o app manipula criptografia, parsing de grandes volumes, compressão ou ML;
  • a equipe precisa de garantias de segurança de memória que C++ não entrega;
  • existe um SDK corporativo exposto para parceiros em múltiplas plataformas;
  • o app processa mídia, áudio ou vídeo em tempo real.

A resposta é não quando o app é predominantemente UI consumindo APIs REST, sem lógica pesada. Nesse caso, Kotlin, Swift, Flutter ou React Native entregam o produto mais rápido, com custo de manutenção menor e sem o overhead de toolchain cross-compile. Escolher Rust por modismo, sem um problema de desempenho ou compartilhamento concreto, é um erro comum que transforma um app simples em um projeto caro.

O ponto de equilíbrio que tem funcionado em produção é o modelo núcleo compartilhado: toda a lógica sensível a desempenho e segurança vive em Rust, exposta via UniFFI, e cada plataforma mantém sua UI nativa. Isso preserva a experiência do usuário (UI nativa, rápida, acessível), ao mesmo tempo em que centraliza o código crítico em um lugar só — testável com ferramentas como proptest e observável com tracing e OpenTelemetry.

Conclusão

Rust em mobile deixou de ser experimento e virou arquitetura de produção em apps como Firefox, Signal e 1Password. Em 2026, o caminho recomendado para novos projetos é UniFFI para bindings idiomáticos, JNI direto apenas onde a integração com a VM Android exigir, e Tauri 2 Mobile para equipes que preferem UI web e querem reuso entre desktop e mobile. A toolchain cross-compile amadureceu (cargo-ndk, cargo-lipo, iOS SDK direto), e o CI paralelo por alvo mantém builds reproduzíveis.

Para quem está construindo carreira, dominar a fronteira entre Rust, Kotlin e Swift é um nicho pouco disputado e bem remunerado — especialmente em fintechs, saúde e mensageria, onde segurança de memória e desempenho determinam o produto. Comece expondo uma biblioteca pequena via UniFFI, meça tamanho de binário e latência em um dispositivo real, e cresça a partir da evidência. O ecossistema brasileiro de empresas que usam Rust valoriza exatamente engenheiras e engenheiros que sabem levar o núcleo crítico do servidor até o bolso do usuário sem abrir mão de segurança ou velocidade.


Leia também:

Veja também nossos sites parceiros:

  • Go Brasil — comparando estratégias de núcleo compartilhado entre Rust e Go via gomobile
  • Python Brasil Dev — do Kivy e BeeWare ao núcleo Rust via PyO3 e UniFFI
  • Kotlin Brasil Dev — Kotlin Multiplatform e quando combinar com um núcleo Rust