Introdução: Quando o compilador diz "não"
Você já viu aquela linha vermelha assustadora no console? Cannot inline bytecode built with JVM target 1.8 into bytecode that is being built with JVM target 1.6. Se sim — você não está sozinho. Esse erro acumulou milhares de visualizações no Stack Overflow e se tornou uma verdadeira pedra no sapato dos desenvolvedores Kotlin. Mas e se eu lhe dissesse que por trás dessa mensagem não há apenas um bug, mas toda uma história da evolução da máquina virtual Java?
Neste artigo, não vamos apenas mostrar como corrigir o erro. Vamos olhar sob o capô do compilador Kotlin, entender o que é o destino JVM, como funcionam as funções inline e por que o bytecode da versão 1.8 não quer se dar bem com a versão 1.6. Aperte os cintos — vai ser interessante. E sim, vamos escrever em Kotlin, porque não é apenas moderno, mas também incrivelmente conveniente.
O que é o destino JVM e por que isso é importante?
Para começar, vamos entender os conceitos básicos. A JVM (Java Virtual Machine) é o ambiente onde seu código Kotlin é executado. Cada versão da JVM é capaz de entender uma versão específica de bytecode. Por exemplo:
- Destino JVM 1.6 — bytecode compatível com Java 6. É uma versão antiga, mas estável.
- Destino JVM 1.8 — bytecode para Java 8. Adiciona suporte a lambdas, Stream API, novas anotações e outros recursos.
Quando você escreve kotlin { jvmTarget = "1.8" } no build.gradle.kts, você está dizendo ao compilador: "Gere bytecode que a JVM 1.8 entenderá". E se você especificar "1.6" — o bytecode será compatível com versões mais antigas.
O problema surge quando diferentes partes do seu projeto (ou bibliotecas) são compiladas com destinos JVM diferentes. O compilador Kotlin vê a incompatibilidade e entra em pânico. Isso é especialmente verdadeiro para funções inline, que em Kotlin são uma poderosa ferramenta de otimização.
Funções inline: como funcionam por baixo dos panos?
Em Kotlin, a palavra-chave inline faz com que o compilador incorpore o código da função diretamente no local da chamada. Isso difere das funções comuns, onde a chamada ocorre através da pilha. Funções inline aceleram a execução (especialmente com lambdas), mas criam um problema: o bytecode da função é copiado para cada chamada.
Imagine que você tem uma biblioteca compilada com o destino JVM 1.8. Dentro dela, há uma função inline que usa invokedynamic (instrução que surgiu no Java 7/8 para lambdas). Você adiciona essa biblioteca a um projeto com destino JVM 1.6. O compilador tenta incorporar o bytecode com invokedynamic em um contexto onde essa instrução não existe. E recebe o erro.
// Exemplo de função inline em uma biblioteca (compilada com target 1.8)inline fun performAction(action: () -> Unit) { println("Antes") action() println("Depois")}
// Seu código (compilado com target 1.6)fun main() { performAction { println("Olá!") } // ERRO: Cannot inline bytecode...}O compilador grita: "Ei, não consigo copiar este bytecode, ele usa recursos da JVM 1.8, mas meu target é 1.6!"
Por que isso acontece em projetos reais?
A causa mais comum é o conflito de versões nas dependências do Gradle. Por exemplo:
- Você usa a biblioteca Kotlin coroutines (que é compilada com target 1.8)
- Seu projeto está configurado para target 1.6 (talvez devido a um modelo desatualizado ou requisitos de compatibilidade)
- Ao compilar, o Kotlin tenta incorporar o bytecode das coroutines e encontra a incompatibilidade
Outra situação são as chamadas diretas de funções inline de diferentes módulos. Se o módulo A foi compilado com target 1.8 e o módulo B com target 1.6, e você chama uma função inline de A em B, o erro é garantido.
Como isso se parece na prática: análise de um exemplo
Vamos simular a situação. Criaremos um projeto simples em Kotlin com Gradle.
// build.gr