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árioEscolha mais comumMotivo
Android com biblioteca C/C++ ou NDKJNIO app executa sobre ART/JVM e a fronteira nativa do Android é JNI.
iOS em um projeto Kotlin MultiplatformKotlin/Native + cinteropO código Kotlin compila para binário nativo e interopera com APIs C/Objective-C.
CLI ou serviço sem JVM em Linux/macOS/WindowsKotlin/Native + cinteropGera executável nativo e não exige uma JVM instalada.
Backend Kotlin/JVM com biblioteca nativa existenteJNI ou FFM APIJNI é compatível com versões antigas; a Foreign Function & Memory API é a opção moderna em JDKs recentes.
Biblioteca compartilhada entre Android e iOSAmbosJNI 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érioKotlin/Native com cinteropJNI com Kotlin/JVM
Runtime principalBinário nativoJVM ou ART no Android
Alvos típicosiOS, macOS, Linux, WindowsAndroid e aplicações JVM
Interoperabilidade principalC; também Objective-C nos alvos AppleC e C++ por meio de uma camada JNI
Geração de bindingscinterop lê headers e arquivo .defMétodos external, headers e implementação JNI
Acesso ao ecossistema JavaNão nos alvos nativosSim
DistribuiçãoExecutável ou framework nativoJAR/APK mais bibliotecas .so, .dll ou .dylib
MemóriaObjetos Kotlin geridos pelo runtime Native; memória C exige disciplinaObjetos JVM geridos pelo GC; recursos nativos exigem disciplina
Portabilidade da integraçãoConfiguração por alvo nativoBinário por arquitetura e sistema operacional
Melhor encaixeKMP/iOS, desktop e executáveis sem JVMAndroid 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:

  1. exponha operações de alto nível em vez de centenas de funções mínimas;
  2. prefira tipos simples, buffers e handles opacos;
  3. documente propriedade e tempo de vida da memória;
  4. converta erros nativos em resultados ou exceções consistentes;
  5. 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.
FerramentaBoilerplate nativoCusto por chamadaEncaixe típico
JNI manualAlto (headers + external)Mais baixoAndroid NDK e controle fino
JNANenhumMais alto (mapeamento dinâmico)Protótipos e integrações simples na JVM
JavaCPPGerado no buildPróximo do JNIBibliotecas 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.