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:
- Frameworks GUI em Rust 2026
- Rust Local-First e CRDT: Colaboração em Tempo Real
- Observabilidade com OpenTelemetry em Produção
- Testes de Propriedade e Fuzzing em Rust
- Rust vs C++ em 2026
- Axum — Framework Web em Rust
- Tutorial de API REST com Axum
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