O problema que ninguém conta sobre misturar linguagens
Eu comecei a usar linguagem mista em projetos reais quando precisei migrar um sistema legado de C++ para algo que não travasse a cada três meses. A resposta óbvia era escrever tudo em Rust do zero, mas isso levaria oito meses e eu tinha dois. Peguei o núcleo crítico, que era performance pura, e deixei em C++. O resto, a camada de negócio, fui escrevendo em Go. O resultado foi um sistema que rodava estável e ainda assim eu conseguia lançar features novas sem medo de quebrar a base antiga. O que todo mundo esquece de mencionar é que o custo não está na escrita do código. O custo está na integração. Duas linguagens, dois compiladores, dois conjuntos de ferramentas, dois ecossistemas de dependências. Quando uma delas atualiza e quebra compatibilidade, você perde pelo menos meio dia só descobrindo onde foi o erro.
O que linguagem mista realmente exige
A primeira coisa que você precisa resolver é a fronteira entre os idiomas. Se você estiver ligando C a Rust, a Interface Foreign Function (FFI) é o caminho. Se for Python chamando uma biblioteca C++ via pybind11, o processo é diferente mas o problema é o mesmo: como passar dados sem copiar tudo para memória e sem vazar ponteiros. Eu já perdi duas noites debugging um vazamento de memória que parecia vir do C++ mas na verdade era o wrapper Python que criava referências cíclicas quando chamava funções nativas dentro de um loop. A solução foi simples, mas não óbvia: usar PyObject_GC_UnTrack() manualmente antes de cada chamada de repetição no laço. Você não vê isso em tutorial nenhum.
Outro ponto cego: o sistema de type checking. Em linguagem mista, você perde a verificação estática nas fronteiras. Se o módulo A passa um struct com layout errado para o módulo B, o compilador não vai te avisar. O runtime que vai travar. Por isso, contratos binários bem definidos são obrigatórios. Eu uso header files com #pragma pack e gero wrappers automaticamente a partir deles com script próprio.
Cenários onde linguagem mista funciona de verdade
Performance crítica + produtividade em desenvolvimento. É o caso mais clássico. Você tem um serviço que processa milhões de requisições por segundo e precisa rodar em hardware existente. Escreve o hot path em C++ ou Rust, o resto em Python ou TypeScript. O ganho é real: em um projeto meu, migrei um módulo de compressão de imagens de Python para C++ com bindings via pybind11. O tempo de processamento caiu de 47 segundos para 3 segundos. Simples assim. O segundo cenário é legado. Você tem uma base de código envelhecida que funciona, mas ninguém quer tocar. Em vez de reescrever tudo, constrói camadas novas ao redor dela em uma linguagem moderna. Eu fiz isso com um sistema de cálculos financeiros escrito em COBOL nos anos 90. Nada a ver com modernidade, mas a precisão dos números era testada há décadas. Coloquei uma API REST em volta com Node.js e pronto. O novo código nunca precisa saber que o COBOL existe.
👉 Clique no botão abaixo para saber mais sobre o assunto!
Onde a coisa costuma dar errado
Dependência de tooling. Cada linguagem tem seu gerenciador de packages, seu formato de build, suas convenções de teste. Quando você mistura três ou mais, o pipeline de CI vira um quebra-cabeça. Eu já vi times inteiros gastarem mais tempo configurando ações no GitHub Actions do que escrevendo código de fato. Debugging cruzado é outro pesadelo. Quando um crash acontece na fronteira entre as linguagens, o stack trace pode mostrar frames de múltiplos idiomas, alguns sem symbols. Eu aprendi na prática que manter DWARF debug info ativado em todas as compilações e usar lldb com extensões para C++ e Python simultaneamente faz diferença entre perder uma hora ou vinte minutos.
A terceira armadilha é deploy. Containers ajudam muito, mas se você tiver binários específicos para arquitectura e depender de bibliotecas nativas de sistemas operacionais diferentes, o suporte a multi-arch no Docker pode ser frustrante. Eu resolvi isso usandokaniko para build sem root e mantendo uma imagem base personalizada com todas as bibliotecas necessárias embutidas.
Como começar sem sofrer
Não tente misturar tudo de uma vez. Comece com uma única fronteira. Escolha a parte do sistema que mais dói e isole ela em um módulo separado. Teste a comunicação entre as linguagens com um exemplo mínimo antes de expandir. Defina contratos claros desde o início. Documente o formato dos dados que atravessam a fronteira, quem é responsável pela alocação e deallocation, e o que acontece em caso de erro. Isso economiza semanas de debugging no futuro.
Mantenha versions das bibliotecas nativas sincronizadas. Nada pior do que uma linguagem A depender da versão 2.3 de uma library e a linguagem B depender da 2.4, causando comportamentos diferentes no mesmo código. Se o projeto for pequeno, talvez não valha a pena. Linguagem mista traz complexidade que só faz sentido quando o ganho de performance ou a reutilização de código legado justificam o esforço. Para microsserviços simples, uma única linguagem bem escolhida geralmente entrega mais valor com menos dor de cabeça.