Las dieciocho DLLs en verde en Linux
Los builds multiplataforma no solo amplían tu alcance: obligan a tu base de código a dejar de depender de los favores silenciosos de un solo compilador. Cada plataforma que añades hace que el runtime sea más honesto en las que ya tenías.
El build de Linux había sido un aprobado parcial conocido durante semanas. La mayoría de las dieciocho DLLs nativas del runtime (el motor está estructurado como una flota de bibliotecas compartidas enfocadas) se construían limpiamente. Algunas no. Los fallos tenían la misma forma cada vez: errores de símbolo indefinido en tiempo de enlace en funciones que obviamente existían en la base de código. Cualquiera que haya trabajado con bibliotecas compartidas de Linux en C++ sabe hacia dónde va esto.
Este sábado me senté con la intención de terminar el trabajo. Para el final del sábado, las dieciocho DLLs se construían limpiamente y el pipeline de CI de Linux reportaba verde completo.
Este es el post sobre cuál fue el problema real, cómo se veía la corrección, y por qué creo que construir en tres plataformas (Linux, macOS, Windows MSVC) es una disciplina que vale la pena pagar.
Cómo se veía el fallo
El patrón siempre era algo así. Un binario de prueba que se enlaza contra, digamos, libraku_runtime.so fallaba al enlazarse con errores como:
undefined reference to `raku_runtime_create_session(...)'
undefined reference to `raku_runtime_initialize(...)'
undefined reference to `raku_runtime_shutdown(...)'
Las funciones sí existían. Se habían escrito. Estaban en los archivos fuente. Se habían compilado. Los archivos objeto las contenían. La biblioteca compartida, al inspeccionarla con nm -D libraku_runtime.so, no las exportaba.
Este es el clásico problema de visibilidad de símbolos de bibliotecas compartidas de Linux. El runtime se había construido con la bandera de CMake -fvisibility=hidden en sus archivos fuente, que es un valor por defecto perfectamente bueno. La intención de -fvisibility=hidden es mantener los símbolos internos fuera de la tabla de símbolos públicos de la biblioteca compartida. Cualquier cosa que necesite ser pública tiene que etiquetarse explícitamente con __attribute__((visibility("default"))) o con una macro que se expanda a eso.
El sistema de build del runtime tenía una macro llamada RAKU_API que se suponía debía expandirse al atributo de visibilidad correcto en cada plataforma. En Windows, RAKU_API se expandía a __declspec(dllexport) al construir la DLL y a __declspec(dllimport) al consumirla. En Linux, se suponía que se expandiría a __attribute__((visibility("default"))) al construir y a nada al consumir.
El problema era que el lado de Linux de la macro no se había aplicado a todas las funciones públicas. Algunas funciones se habían etiquetado. Muchas no. Las que no se habían etiquetado quedaban ocultas por el valor por defecto -fvisibility=hidden y desaparecían de la tabla de símbolos públicos.
Cómo se veía la corrección
Tres pasos, todos mecánicos, todos vale la pena anotar.
Uno: auditar cada encabezado de API pública en busca de etiquetas RAKU_API faltantes. Escribí un script que analiza cada archivo .h de la API pública y encuentra cada declaración de función que debería ser pública pero no está etiquetada. El script reporta la función, el archivo, y la línea. Ejecuté el script. Obtuve una lista de trescientas cuarenta y siete funciones a través de dieciocho DLLs que necesitaban la etiqueta.
Dos: añadir la etiqueta en un barrido. Esto es exactamente el tipo de refactorización mecánica que un agente hace bien. El planteamiento del issue decía: “para cada función listada abajo, añade RAKU_API a la línea de declaración. No modifiques el cuerpo de la función. No modifiques ninguna otra línea. Ejecuta el build después del lote de cada subsistema y confirma que aún se construye en Linux.” El agente hizo esto limpiamente, subsistema por subsistema. Cada PR de subsistema era revisable por sí mismo y el build de Linux se volvía más verde con cada fusión.
Tres: una verificación de CI que evita la regresión. Añadí una verificación que se ejecuta en el momento del PR. La verificación construye el runtime en Linux, ejecuta nm -D en cada .so producido, compara la lista de símbolos exportados contra el conjunto esperado declarado en los encabezados de API pública, y falla el PR si falta algún símbolo esperado. Este es el tipo de barrera que atrapará la próxima instancia de “olvidé etiquetar una función nueva” el día en que aterrice, en lugar de meses después.
Para el final del sábado, las dieciocho DLLs exportaban su superficie pública completa. Los binarios de prueba se enlazaron. Las pruebas se ejecutaron. El build de Linux estaba en verde.
Por qué vale la pena pagar por tres plataformas
Una pregunta natural: ¿por qué molestarse con Linux en absoluto? El runtime apunta a gafas de AR, que ejecutan un sistema operativo especializado que no es Linux ni Windows ni macOS de la forma en que lo es una máquina de desarrollador. ¿Por qué pagar el costo multiplataforma además del costo de soportar el hardware objetivo real?
Tres razones.
Uno: la inferencia de IA del lado del servidor y el renderizado en la nube ocurren en Linux. Cualquier componente del lado de la nube de una experiencia de AR (servicio de modelos, sincronización de estado del mundo, persistencia) se ejecuta en Linux. El runtime tiene hooks que el lado de la nube llama. Esos hooks tienen que construirse y ejecutarse en Linux para que el lado de la nube se integre. Si el runtime es una bestia exclusiva de Windows, el lado de la nube tiene que construir un shim de comunicación separado o ejecutar el runtime bajo una capa de compatibilidad de Linux. Ninguna de las dos es lo que quiero que hagan los socios.
Dos: el CI en Linux es más rápido y barato que el CI en Windows. Cada PR que fusiono pasa por CI. Los runners de CI de Linux son más pequeños, más rápidos, y más baratos que los runners de CI de Windows. Cuanto más rápido es el ciclo de CI, más iteraciones pueden ejecutar los agentes y yo en un sábado. Linux como objetivo de build de primera clase acelera todo el flujo de trabajo de desarrollo.
Tres: la disciplina multiplataforma detecta bugs. Esta es la razón más profunda. Cuando una base de código solo se construye en una plataforma, los patrones que usan los desarrolladores son los patrones que funcionan en esa plataforma. Los builds multiplataforma obligan a que los patrones sean portables: atributos de visibilidad explícitos en lugar de implícitos, anchos de tipo multiplataforma explícitos en lugar de “long es de 32 bits en este compilador,” semántica de hilos explícita en lugar de “esto funciona en Windows.” La corrección de visibilidad de símbolos de hoy es exactamente ese patrón. La base de código es más sólida después de la corrección de lo que era antes, en cada plataforma, porque la corrección hizo explícito el contrato de visibilidad.
A qué se generaliza esto
Algunos patrones honestos.
La visibilidad de símbolos en Linux es un impuesto que todos pagan una vez. La primera vez que un proyecto se topa con este problema, es misterioso y frustrante. Una vez que la corrección está en su lugar (la macro RAKU_API, aplicada consistentemente, con un guardián de CI), es invisible. El costo es la primera vez. Págalo temprano.
Los guardianes de CI para regresiones de visibilidad de símbolos no son opcionales. El tipo de bug que tarda meses en salir a la luz, porque a una función que todavía nadie usa le falta su etiqueta, es exactamente el tipo de bug que un guardián de CI atrapa el día en que aterriza. Instala el guardián.
Los builds multiplataforma hacen que la base de código sea más honesta. En cualquier lugar donde una base de código dependa del comportamiento implícito de una plataforma, el port multiplataforma obliga a que el comportamiento se vuelva explícito. Cada vez que he portado una base de código a una plataforma nueva, la plataforma nueva ha sacado a la luz bugs que la plataforma original absorbía silenciosamente. Las correcciones son mejoras en cada plataforma, no solo en la nueva.
Qué deberían llevarse los socios y constructores de esto
Si eres un socio decidiendo en qué motor construir para un producto de AR multiplataforma, pregúntale al equipo cómo se ve su matriz de build. Un equipo que construye en Linux, macOS, y Windows es un equipo cuya base de código ha sido disciplinada por las diferencias entre las tres plataformas. Un equipo que construye en una sola no lo es.
Si eres un desarrollador trabajando en tu propio proyecto multiplataforma de biblioteca compartida, la auditoría de visibilidad de símbolos es la auditoría que deberías ejecutar hoy, no en marzo cuando una prueba empieza a fallar por razones que toman tres días diagnosticar. Ejecuta nm -D en tus bibliotecas compartidas. Compara contra tus encabezados. Las discrepancias son la auditoría.
Si eres un laboratorio de IA cuyo agente de codificación escribe código nativo multiplataforma, la macro de atributo de visibilidad es el tipo de cosa que el agente necesita en su conocimiento de trabajo. Un agente que escribe una función pública nueva y olvida etiquetarla es un agente que te cuesta un PR de seguimiento. Un agente que etiqueta cada función pública consistentemente es un agente que se paga a sí mismo.
Cierre del sábado. Dieciocho DLLs en verde en Linux. El CI ahora es más rápido. La base de código es más honesta. La matriz de build tiene tres plataformas de ancho.
De vuelta a construir.
Un runtime disciplinado por cada plataforma que toca
RakuAI es el runtime espacial multiplataforma para gafas de AR, inferencia en la nube, y todo lo que hay en medio: Linux, macOS, y Windows, todo en verde. Descubre por qué los socios construyen sobre una base portable.