Resposta rápida: Kotlin/Native e JNI não são substitutos diretos. Use Kotlin/Native com cinterop quando o código Kotlin compila sem JVM para iOS, macOS, Linux, Windows ou outros alvos nativos. Use JNI quando uma aplicação Kotlin/JVM — principalmente Android com NDK — precisa chamar uma biblioteca C ou C++. Em um projeto Kotlin Multiplatform, é possível usar Kotlin/Native no iOS e JNI no Android para acessar a mesma biblioteca nativa.
| Cenário | Escolha mais comum | Motivo |
|---|---|---|
| Android com biblioteca C/C++ ou NDK | JNI | O app executa sobre ART/JVM e a fronteira nativa do Android é JNI. |
| iOS em um projeto Kotlin Multiplatform | Kotlin/Native + cinterop | O código Kotlin compila para binário nativo e interopera com APIs C/Objective-C. |
| CLI ou serviço sem JVM em Linux/macOS/Windows | Kotlin/Native + cinterop | Gera executável nativo e não exige uma JVM instalada. |
| Backend Kotlin/JVM com biblioteca nativa existente | JNI ou FFM API | JNI é compatível com versões antigas; a Foreign Function & Memory API é a opção moderna em JDKs recentes. |
| Biblioteca compartilhada entre Android e iOS | Ambos | JNI no androidMain; cinterop no iosMain ou em source sets nativos. |
Kotlin/Native e JNI resolvem problemas diferentes
A comparação faz sentido porque as duas tecnologias conectam Kotlin a código nativo, mas elas ficam em arquiteturas distintas:
- Kotlin/Native é um backend do compilador Kotlin. Ele transforma Kotlin em código de máquina para um alvo nativo, sem depender da JVM.
- JNI (Java Native Interface) é uma ponte entre a JVM e código nativo. O Kotlin continua compilando para bytecode JVM; somente algumas chamadas atravessam a fronteira para C ou C++.
Portanto, migrar de JNI para Kotlin/Native não é apenas trocar uma API de interoperabilidade. Isso também pode significar mudar o runtime, as bibliotecas disponíveis, o modelo de implantação e o alvo do projeto.
Para uma visão mais ampla de compartilhamento entre plataformas, leia o guia de Kotlin Multiplatform e o tutorial prático de Kotlin Multiplatform.
Comparação direta: Kotlin/Native vs JNI
| Critério | Kotlin/Native com cinterop | JNI com Kotlin/JVM |
|---|---|---|
| Runtime principal | Binário nativo | JVM ou ART no Android |
| Alvos típicos | iOS, macOS, Linux, Windows | Android e aplicações JVM |
| Interoperabilidade principal | C; também Objective-C nos alvos Apple | C e C++ por meio de uma camada JNI |
| Geração de bindings | cinterop lê headers e arquivo .def | Métodos external, headers e implementação JNI |
| Acesso ao ecossistema Java | Não nos alvos nativos | Sim |
| Distribuição | Executável ou framework nativo | JAR/APK mais bibliotecas .so, .dll ou .dylib |
| Memória | Objetos Kotlin geridos pelo runtime Native; memória C exige disciplina | Objetos JVM geridos pelo GC; recursos nativos exigem disciplina |
| Portabilidade da integração | Configuração por alvo nativo | Binário por arquitetura e sistema operacional |
| Melhor encaixe | KMP/iOS, desktop e executáveis sem JVM | Android NDK e sistemas Kotlin/JVM legados ou atuais |
Como funciona o cinterop no Kotlin/Native
O cinterop gera declarações Kotlin a partir de headers C. Uma integração normalmente possui um arquivo .def com os headers, opções do compilador e opções do linker.
# src/nativeInterop/cinterop/minha_lib.def
headers = minha_lib.h
package = br.dev.kotlin.minhalib
compilerOpts.linux_x64 = -I/usr/local/include
linkerOpts.linux_x64 = -L/usr/local/lib -lminha_lib
A configuração pode ser associada a um alvo no build.gradle.kts:
kotlin {
linuxX64 {
compilations.getByName("main") {
cinterops {
val minhaLib by creating {
defFile(project.file(
"src/nativeInterop/cinterop/minha_lib.def"
))
}
}
}
}
}
Depois da geração dos bindings, funções e tipos C ficam disponíveis no código Kotlin. Operações com ponteiros e memória nativa usam APIs de kotlinx.cinterop e são marcadas como experimentais quando necessário.
import br.dev.kotlin.minhalib.*
import kotlinx.cinterop.ExperimentalForeignApi
@OptIn(ExperimentalForeignApi::class)
fun somarComBibliotecaNativa(a: Int, b: Int): Int {
return minha_lib_somar(a, b)
}
O ganho de ergonomia é claro quando a biblioteca expõe uma API C pequena e estável. Porém, cinterop não transforma automaticamente uma API C++ complexa em uma API Kotlin amigável. Para C++, a prática comum é criar uma camada C com funções extern "C" e tipos opacos.
Como funciona JNI com Kotlin/JVM
No JNI, o Kotlin declara um método external e carrega uma biblioteca compartilhada:
class CalculadoraNativa {
companion object {
init {
System.loadLibrary("calculadora")
}
}
external fun somar(a: Int, b: Int): Int
}
A implementação C ou C++ recebe um JNIEnv, usado para trabalhar com objetos e tipos da JVM:
#include <jni.h>
JNIEXPORT jint JNICALL
Java_br_dev_kotlin_CalculadoraNativa_somar(
JNIEnv *env,
jobject instancia,
jint a,
jint b
) {
return a + b;
}
Em Android, essa biblioteca costuma ser compilada pelo NDK com CMake ou ndk-build, gerando um arquivo .so para cada ABI suportada, como arm64-v8a e x86_64.
O JNI é mais verboso porque precisa representar classes, métodos, arrays, strings, exceções e referências da JVM em uma API C. Em compensação, ele permite manter o restante da aplicação no ecossistema JVM, incluindo bibliotecas Android e Java já adotadas pelo projeto.
Android: Kotlin/Native substitui JNI?
Não para o caso normal de um aplicativo Android. O código Kotlin de um app Android executa sobre ART, e a integração oficial com código C/C++ do NDK passa por JNI. Kotlin/Native possui alvos nativos relacionados ao Android, mas isso não transforma cinterop em substituto geral da camada JNI de um app Android convencional.
Se a sua equipe mantém uma biblioteca C++ usada em Android e iOS, uma arquitetura frequente é:
Biblioteca C/C++ compartilhada
├── Android: Kotlin/JVM -> JNI -> biblioteca nativa
└── iOS: Kotlin/Native -> wrapper C/cinterop -> biblioteca nativa
A API comum do projeto pode esconder essas diferenças com expect e actual:
// commonMain
expect class HashNativo() {
fun calcular(bytes: ByteArray): ByteArray
}
// androidMain: implementação chama JNI
actual class HashNativo {
actual external fun calcular(bytes: ByteArray): ByteArray
}
No iosMain, a implementação actual chama os bindings gerados por cinterop. Assim, o código de negócio compartilhado não precisa conhecer a ponte usada em cada plataforma.
Desempenho: evite números universais
Não existe um valor confiável e universal para dizer que uma chamada Kotlin/Native custa “X nanossegundos” e JNI custa “Y”. O resultado muda com processador, compilador, ABI, runtime, aquecimento da JVM, tipo convertido, tamanho do buffer e forma de acesso à memória.
O que pode ser afirmado com segurança:
- chamadas JNI têm um custo de travessia entre código gerenciado e nativo;
- conversões de
String, arrays e objetos podem custar mais que uma chamada com tipos primitivos; - Kotlin/Native chama uma API C sem atravessar a JVM, mas ainda pode haver conversão, cópia ou alocação;
- fazer milhares de chamadas pequenas costuma ser pior do que enviar um lote de trabalho em uma única chamada;
- copiar um buffer grande pode dominar o tempo total, independentemente da tecnologia escolhida.
Para código sensível a latência, crie um benchmark representativo do seu dispositivo e da sua biblioteca. No Android, use Macrobenchmark ou medição nativa adequada; na JVM, JMH ajuda a evitar erros comuns de microbenchmark. Compare também consumo de memória, tamanho do binário e tempo de inicialização, não apenas a duração de uma chamada.
Memória e segurança
As duas opções exigem cuidado na fronteira nativa.
No Kotlin/Native
O runtime gerencia objetos Kotlin, mas não assume automaticamente a posse de toda memória retornada por C. É preciso conhecer o contrato da biblioteca: quem aloca, quem libera e por quanto tempo um ponteiro permanece válido. memScoped é útil para alocações temporárias:
@OptIn(ExperimentalForeignApi::class)
fun lerValor(): Int = memScoped {
val saida = alloc<IntVar>()
biblioteca_preencher(saida.ptr)
saida.value
}
Não retorne um ponteiro para uma área temporária criada dentro de memScoped. Ao sair do bloco, aquela memória deixa de ser válida.
No JNI
O GC gerencia objetos Java/Kotlin, mas não libera automaticamente um malloc, handle de sistema ou recurso criado pela biblioteca C++. Também é necessário controlar referências locais e globais, liberar arrays obtidos da JVM conforme a API usada e converter exceções nativas em erros compreensíveis no lado Kotlin.
Erros nas duas abordagens podem causar vazamentos, corrupção de memória ou encerramento do processo. Bindings gerados reduzem boilerplate, mas não tornam uma API de ponteiros intrinsecamente segura.
Build, depuração e manutenção
Kotlin/Native tende a oferecer uma integração mais declarativa com bibliotecas C, mas cada alvo pode precisar de headers, flags e binários próprios. Em projetos Apple, também entram no fluxo frameworks, Xcode e arquiteturas de simulador e dispositivo.
JNI tem mais cerimônia, especialmente ao manipular objetos JVM. No Android, porém, a combinação NDK, CMake, LLDB e empacotamento por ABI é madura e bem documentada. O maior risco de manutenção aparece quando nomes JNI são escritos manualmente e ficam acoplados ao pacote e à classe Kotlin. RegisterNatives ou ferramentas de geração de bindings podem reduzir esse acoplamento.
Em ambos os casos, mantenha a fronteira pequena:
- exponha operações de alto nível em vez de centenas de funções mínimas;
- prefira tipos simples, buffers e handles opacos;
- documente propriedade e tempo de vida da memória;
- converta erros nativos em resultados ou exceções consistentes;
- teste cada arquitetura usada em produção.
E a Foreign Function & Memory API do Java?
Para backend e desktop em JVMs recentes, a Foreign Function & Memory API (FFM API) é a alternativa moderna ao JNI. Ela permite chamar funções e manipular memória fora da JVM com uma API Java padronizada, que Kotlin/JVM também pode consumir.
Ela não torna JNI obsoleto em todos os ambientes. Projetos presos a JDKs antigos, bibliotecas com integração JNI pronta e aplicativos Android ainda possuem restrições diferentes. Antes de adotar FFM, confirme a versão mínima de Java do produto e a compatibilidade das ferramentas de build e implantação.
Para aplicações Kotlin server-side que não precisam de código nativo, manter tudo na JVM costuma ser mais simples. Veja também o guia de Kotlin na AWS Lambda para um exemplo de implantação backend.
JNA e JavaCPP: alternativas ao JNI manual na JVM
Entre “escrever JNI à mão” e “sair da JVM”, existem duas bibliotecas intermediárias comuns em projetos Kotlin/JVM:
- JNA (Java Native Access) permite chamar a biblioteca C a partir de uma simples interface Kotlin/Java, sem gerar código nativo nem manter nomes JNI. É o caminho mais rápido para colocar uma integração no ar em servidores e ferramentas de desktop, mas cada chamada passa por mapeamento dinâmico e costuma ser mais lenta que uma chamada JNI direta quando as chamadas são pequenas e frequentes.
- JavaCPP gera automaticamente a camada JNI a partir de headers C/C++ durante o build. É a base do ecossistema Bytedeco (OpenCV, FFmpeg e outros) e tende a valer a pena quando a biblioteca nativa é grande.
| Ferramenta | Boilerplate nativo | Custo por chamada | Encaixe típico |
|---|---|---|---|
| JNI manual | Alto (headers + external) | Mais baixo | Android NDK e controle fino |
| JNA | Nenhum | Mais alto (mapeamento dinâmico) | Protótipos e integrações simples na JVM |
| JavaCPP | Gerado no build | Próximo do JNI | Bibliotecas C++ grandes no ecossistema JVM |
No Android, o caminho convencional continua sendo NDK com JNI — ou uma camada gerada sobre ele —, porque JNA não é projetado para o ambiente do ART e adiciona peso desnecessário ao APK. Para um backend Kotlin/JVM que só precisa consumir uma .so existente, avalie JNA ou JavaCPP antes de escrever JNI à mão, e a FFM API quando o JDK mínimo do projeto permitir.
Qual escolher? Checklist de decisão
Escolha Kotlin/Native com cinterop quando:
- o alvo é iOS, macOS, Linux, Windows ou outro ambiente sem JVM;
- você está implementando a camada nativa de um projeto Kotlin Multiplatform;
- a dependência oferece uma API C estável ou pode receber um wrapper C;
- gerar um executável nativo faz parte do requisito de distribuição.
Escolha JNI quando:
- o produto é um app Android que usa NDK;
- a aplicação precisa continuar em Kotlin/JVM;
- existe uma biblioteca JNI madura que já resolve o problema;
- você precisa acessar uma biblioteca C/C++ sem reescrever a aplicação para outro runtime.
Considere FFM API quando:
- o projeto é JVM, não Android;
- a versão mínima do JDK oferece a API necessária;
- você quer uma integração nova sem manter o boilerplate tradicional de JNI.
Antes de adicionar qualquer ponte nativa, verifique se uma biblioteca Kotlin ou Java pura já atende ao caso. Código nativo aumenta a matriz de builds, dificulta a observabilidade e pode converter um erro recuperável em crash do processo.
Perguntas frequentes
Kotlin/Native é mais rápido que JNI?
Não há uma resposta universal. Kotlin/Native evita a travessia da JVM ao chamar C, mas o desempenho total depende do trabalho executado, das conversões e das cópias de memória. Para operações grandes, o custo da ponte pode ser irrelevante; para chamadas mínimas e repetidas, ele pode importar. Faça benchmark no alvo real.
Posso usar JNI no iOS?
JNI pressupõe uma JVM, portanto não é a integração normal de um aplicativo iOS. Em Kotlin Multiplatform para iOS, use Kotlin/Native e as opções de interoperabilidade disponíveis para C e Objective-C.
cinterop funciona diretamente com C++?
A interoperabilidade direta é centrada em C. Para uma API C++, crie normalmente um wrapper C com extern "C", escondendo classes e templates atrás de funções e handles opacos.
Preciso de JNI para todo código C no Android?
Para chamar C/C++ a partir do Kotlin/JVM de um app Android, sim, existe uma fronteira JNI — mesmo que uma biblioteca ou gerador esconda parte dela. Código inteiramente nativo pode se comunicar de outras formas dentro do NDK, mas a entrada a partir de Kotlin/Java passa por JNI.
Um projeto pode usar Kotlin/Native e JNI ao mesmo tempo?
Sim. Esse é um desenho comum em Kotlin Multiplatform: a implementação Android usa JNI, enquanto a implementação iOS usa Kotlin/Native com cinterop. A camada commonMain expõe uma API única para o restante do aplicativo.
Veredito
A pergunta correta não é “qual tecnologia vence?”, mas “em qual runtime meu código Kotlin executa?”. Em Android ou Kotlin/JVM, JNI continua sendo a ponte relevante para C/C++. Em iOS, desktop nativo e outros alvos sem JVM, Kotlin/Native com cinterop é o encaixe natural. Em uma base multiplataforma, as duas tecnologias podem cooperar atrás da mesma API comum.
Se a decisão envolve compartilhar um aplicativo inteiro, compare também Kotlin Multiplatform vs Flutter. Se o objetivo principal é segurança de memória no componente nativo, a comparação entre Kotlin e Rust ajuda a avaliar outra arquitetura possível.