Série: Aprendendo a Programar com IA

As Dezoito DLLs Verdes no Linux

A varredura de visibilidade de símbolo que deixou o build do Linux verde.

Todas as Dezoito Verdes no Linux 347 funções marcadas, uma trava de CI, três plataformas libraku_01 libraku_02 libraku_03 libraku_04 libraku_05 libraku_06 libraku_07 libraku_08 libraku_09 libraku_10 libraku_11 libraku_12 libraku_13 libraku_14 libraku_15 libraku_16 libraku_17 libraku_18 RAKU_API __attribute__((visibility("default"))) trava nm -D: falha qualquer PR que derrube um símbolo
Dezoito bibliotecas, um contrato de visibilidade consistente, uma trava que o mantém honesto.

Builds multiplataforma não apenas ampliam seu alcance — eles forçam sua base de código a parar de depender dos favores silenciosos de um único compilador. Cada plataforma que você adiciona torna o runtime mais honesto nas que você já tinha.

O build do Linux tinha sido uma aprovação parcial conhecida por semanas. A maioria das dezoito DLLs nativas do runtime (o engine é estruturado como uma frota de bibliotecas compartilhadas focadas) tinha estado compilando limpo. Algumas não. As falhas tinham a mesma forma toda vez: erros de símbolo indefinido em tempo de linkagem em funções que obviamente existiam na base de código. Qualquer um que já trabalhou com bibliotecas compartilhadas Linux em C++ sabe para onde isso está indo.

Neste sábado me sentei com a intenção de terminar o serviço. Ao final do sábado, todas as dezoito DLLs estavam compilando limpo e o pipeline de CI do Linux estava reportando verde total.

Este é o post sobre qual era o problema real, como foi a correção, e por que acho que compilar em três plataformas (Linux, macOS, Windows MSVC) é uma disciplina que vale a pena pagar.

Como era a falha

O padrão era sempre algo assim. Um binário de teste que linka contra, digamos, libraku_runtime.so falharia ao linkar com erros como:

undefined reference to `raku_runtime_create_session(...)'
undefined reference to `raku_runtime_initialize(...)'
undefined reference to `raku_runtime_shutdown(...)'

As funções existiam. Tinham sido escritas. Estavam nos arquivos-fonte. Tinham sido compiladas. Os arquivos-objeto as continham. A biblioteca compartilhada, quando inspecionada com nm -D libraku_runtime.so, não as exportava.

Esse é o clássico problema de visibilidade de símbolo em bibliotecas compartilhadas Linux. O runtime tinha sido construído com a flag do CMake -fvisibility=hidden em seus arquivos-fonte, que é um padrão perfeitamente bom. A intenção do -fvisibility=hidden é manter símbolos internos fora da tabela de símbolo público da biblioteca compartilhada. Qualquer coisa que precise ser pública tem que ser explicitamente marcada com __attribute__((visibility("default"))) ou com uma macro que se expande para isso.

O sistema de build do runtime tinha uma macro chamada RAKU_API que deveria se expandir para o atributo de visibilidade certo em cada plataforma. No Windows, RAKU_API se expandia para __declspec(dllexport) ao construir a DLL e __declspec(dllimport) ao consumi-la. No Linux, deveria se expandir para __attribute__((visibility("default"))) ao construir e para nada ao consumir.

O problema era que o lado Linux da macro não tinha sido aplicado a toda função pública. Algumas funções tinham sido marcadas. Muitas não. As que não tinham sido marcadas estavam sendo escondidas pelo padrão -fvisibility=hidden e desaparecendo da tabela de símbolo público.

Como foi a correção

Três passos, todos mecânicos, todos que valem a pena registrar.

Um: auditar todo cabeçalho de API pública em busca de tags RAKU_API ausentes. Escrevi um script que faz parse de cada arquivo .h na API pública e encontra toda declaração de função que deveria ser pública mas não está marcada. O script reporta a função, o arquivo, e a linha. Rodei o script. Obtive uma lista de trezentas e quarenta e sete funções ao longo de dezoito DLLs que precisavam da tag.

Dois: adicionar a tag em varredura. Esse é exatamente o tipo de refatoração mecânica que um agente faz bem. O enquadramento da issue dizia: “para cada função listada abaixo, adicione RAKU_API na linha de declaração. Não modifique o corpo da função. Não modifique nenhuma outra linha. Rode o build depois do lote de cada subsistema e confirme que ainda compila no Linux.” O agente fez isso de forma limpa, subsistema por subsistema. Cada PR de subsistema era revisável por si só e o build do Linux foi ficando mais verde a cada merge.

Três: uma verificação de CI que previne regressão. Adicionei uma verificação que roda no momento do PR. A verificação constrói o runtime no Linux, roda nm -D em cada .so produzido, compara a lista de símbolos exportados contra o conjunto esperado declarado nos cabeçalhos de API pública, e falha o PR se qualquer símbolo esperado estiver ausente. Esse é o tipo de trava de segurança que vai pegar a próxima instância de “esqueci de marcar uma função nova” no dia em que ela acontece em vez de meses depois.

Ao final do sábado, todas as dezoito DLLs estavam exportando sua superfície pública completa. Os binários de teste linkaram. Os testes rodaram. O build do Linux estava verde.

Por que três plataformas vale a pena pagar

Uma pergunta natural: por que se incomodar com o Linux? O runtime tem como alvo óculos de RA, que rodam um SO especializado que não é Linux nem Windows nem macOS do jeito que uma máquina de desenvolvedor é. Por que pagar o custo multiplataforma em cima do custo de suportar o hardware alvo de verdade?

Três razões.

Um: inferência de IA do lado do servidor e renderização em nuvem acontecem no Linux. Qualquer componente do lado da nuvem de uma experiência de RA (servir modelos, sincronização de estado do mundo, persistência) está rodando no Linux. O runtime tem ganchos que o lado da nuvem chama. Esses ganchos precisam compilar e rodar no Linux para que o lado da nuvem se integre. Se o runtime é uma besta só-para-Windows, o lado da nuvem tem que ou construir uma camada de comunicação separada ou rodar o runtime sob uma camada de compatibilidade Linux. Nenhum dos dois é o que eu quero que parceiros façam.

Dois: CI no Linux é mais rápida e mais barata do que CI no Windows. Todo PR que integro roda pela CI. Executores de CI Linux são menores, mais rápidos, e mais baratos do que executores de CI Windows. Quanto mais rápido o loop de CI, mais iterações os agentes e eu conseguimos rodar em um sábado. Linux como alvo de build de primeira classe acelera o fluxo de trabalho de desenvolvimento inteiro.

Três: disciplina multiplataforma pega bugs. Essa é a razão mais profunda. Quando uma base de código só compila em uma plataforma, os padrões que os desenvolvedores buscam são os padrões que funcionam naquela plataforma. Builds multiplataforma forçam os padrões a serem portáteis: atributos de visibilidade explícitos em vez de implícitos, larguras de tipo multiplataforma explícitas em vez de “long tem 32 bits nesse compilador,” semântica de threading explícita em vez de “isso funciona no Windows.” A correção de visibilidade de símbolo de hoje é exatamente esse padrão. A base de código está mais forte depois da correção do que estava antes, em toda plataforma, porque a correção tornou o contrato de visibilidade explícito.

Para onde isso generaliza

Alguns padrões honestos.

Visibilidade de símbolo no Linux é um imposto que todo mundo paga uma vez. A primeira vez que um projeto bate nesse problema, é misterioso e frustrante. Uma vez que a correção está em vigor (a macro RAKU_API, aplicada consistentemente, com uma trava de CI), fica invisível. O custo é a primeira vez. Pague cedo.

Travas de CI para regressões de visibilidade de símbolo não são opcionais. O tipo de bug que leva meses para aparecer, porque uma função que ninguém está usando ainda está sem a tag, é exatamente o tipo de bug que uma trava de CI pega no dia em que acontece. Configure a trava.

Builds multiplataforma tornam a base de código mais honesta. Em qualquer lugar onde uma base de código depende de comportamento implícito de uma plataforma, o porte multiplataforma força o comportamento a se tornar explícito. Toda vez que portei uma base de código para uma plataforma nova, a plataforma nova revelou bugs que a plataforma original estava silenciosamente absorvendo. As correções são melhorias em toda plataforma, não só na nova.

O que parceiros e construtores devem tirar disso

Se você é um parceiro decidindo em qual engine construir para um produto de RA multiplataforma, pergunte ao time como é a matriz de build deles. Um time que constrói no Linux, macOS, e Windows é um time cuja base de código foi disciplinada pelas diferenças entre as três plataformas. Um time que constrói em uma só é um time cuja base de código não foi.

Se você é um desenvolvedor trabalhando no seu próprio projeto de biblioteca compartilhada multiplataforma, a auditoria de visibilidade de símbolo é a auditoria que você deveria rodar hoje, não em março quando um teste começa a falhar por razões que levam três dias para diagnosticar. Rode nm -D nas suas bibliotecas compartilhadas. Compare contra seus cabeçalhos. As discrepâncias são a auditoria.

Se você é um laboratório de IA cujo agente de codificação escreve código nativo multiplataforma, a macro de atributo de visibilidade é o tipo de coisa que o agente precisa ter no seu conhecimento de trabalho. Um agente que escreve uma nova função pública e esquece de marcá-la é um agente que custa a você um PR de acompanhamento. Um agente que marca toda função pública consistentemente é um agente que se paga sozinho.

Fechamento de sábado. Dezoito DLLs verdes no Linux. A CI está mais rápida agora. A base de código está mais honesta. A matriz de build tem três plataformas de largura.

De volta a construir.

Um runtime disciplinado por toda plataforma que toca

O RakuAI é o runtime espacial multiplataforma para óculos de RA, inferência em nuvem, e tudo entre eles — Linux, macOS, e Windows, todos verdes. Veja por que parceiros constroem sobre uma base portátil.

← Todos os posts