Les dix-huit DLL au vert sur Linux
Les builds multiplateformes n'élargissent pas seulement votre portée — ils forcent votre base de code à cesser de dépendre des faveurs silencieuses d'un seul compilateur. Chaque plateforme que vous ajoutez rend le runtime plus honnête sur celles que vous aviez déjà.
Le build Linux avait été un « réussi partiellement » connu depuis des semaines. La plupart des dix-huit DLL natives du runtime (le moteur est structuré comme une flotte de bibliothèques partagées ciblées) compilaient proprement. Quelques-unes ne le faisaient pas. Les échecs avaient la même forme à chaque fois : des erreurs de symbole non défini au moment de la liaison sur des fonctions qui existaient manifestement dans la base de code. Quiconque a travaillé sur des bibliothèques partagées Linux en C++ sait où cela mène.
Ce samedi, je me suis assis avec l’intention de terminer le travail. À la fin du samedi, les dix-huit DLL compilaient proprement et le pipeline CI Linux rapportait un vert complet.
Voici le billet sur quel était le vrai problème, à quoi ressemblait le correctif, et pourquoi je pense que construire sur trois plateformes (Linux, macOS, Windows MSVC) est une discipline qui vaut la peine d’être payée.
À quoi ressemblait l’échec
Le schéma était toujours à peu près ainsi. Un binaire de test qui se lie contre, disons, libraku_runtime.so échouerait à se lier avec des erreurs comme :
undefined reference to `raku_runtime_create_session(...)'
undefined reference to `raku_runtime_initialize(...)'
undefined reference to `raku_runtime_shutdown(...)'
Les fonctions existaient bel et bien. Elles avaient été écrites. Elles étaient dans les fichiers source. Elles avaient été compilées. Les fichiers objets les contenaient. La bibliothèque partagée, quand inspectée avec nm -D libraku_runtime.so, ne les exportait pas.
C’est le problème classique de visibilité de symboles des bibliothèques partagées Linux. Le runtime avait été construit avec le drapeau CMake -fvisibility=hidden sur ses fichiers source, ce qui est un défaut parfaitement raisonnable. L’intention de -fvisibility=hidden est de garder les symboles internes hors de la table de symboles publics de la bibliothèque partagée. Tout ce qui doit être public doit être explicitement étiqueté avec __attribute__((visibility("default"))) ou avec une macro qui s’y développe.
Le système de build du runtime avait une macro appelée RAKU_API qui était censée se développer vers le bon attribut de visibilité sur chaque plateforme. Sur Windows, RAKU_API se développait vers __declspec(dllexport) lors de la construction de la DLL et __declspec(dllimport) lors de sa consommation. Sur Linux, elle était censée se développer vers __attribute__((visibility("default"))) lors de la construction et vers rien lors de la consommation.
Le problème était que le côté Linux de la macro n’avait pas été appliqué à chaque fonction publique. Certaines fonctions avaient été étiquetées. Beaucoup ne l’avaient pas été. Celles qui n’avaient pas été étiquetées étaient cachées par le défaut -fvisibility=hidden et disparaissaient de la table de symboles publics.
À quoi ressemblait le correctif
Trois étapes, toutes mécaniques, toutes qui valent la peine d’être notées.
Un : auditer chaque en-tête d’API publique pour les étiquettes RAKU_API manquantes. Écriture d’un script qui parse chaque fichier .h de l’API publique et trouve chaque déclaration de fonction qui devrait être publique mais n’est pas étiquetée. Le script rapporte la fonction, le fichier, et la ligne. Exécution du script. Obtention d’une liste de trois cent quarante-sept fonctions à travers dix-huit DLL qui avaient besoin de l’étiquette.
Deux : balayer et ajouter l’étiquette. C’est exactement le genre de refactor mécanique qu’un agent fait bien. Le cadrage du ticket disait : « pour chaque fonction listée ci-dessous, ajoutez RAKU_API à la ligne de déclaration. Ne modifiez pas le corps de la fonction. Ne modifiez aucune autre ligne. Exécutez le build après le lot de chaque sous-système et confirmez qu’il compile toujours sur Linux. » L’agent l’a fait proprement, sous-système par sous-système. Chaque PR de sous-système était relisible seule et le build Linux devenait plus vert à chaque fusion.
Trois : une vérification CI qui empêche la régression. Ajout d’une vérification qui s’exécute au moment de la PR. La vérification construit le runtime sur Linux, exécute nm -D sur chaque .so produit, compare la liste de symboles exportés contre l’ensemble attendu déclaré dans les en-têtes d’API publique, et fait échouer la PR si un symbole attendu est manquant. C’est le genre de garde-fou qui attrapera la prochaine instance de « j’ai oublié d’étiqueter une nouvelle fonction » le jour où elle atterrit au lieu de mois plus tard.
À la fin du samedi, les dix-huit DLL exportaient leur surface publique complète. Les binaires de test se liaient. Les tests s’exécutaient. Le build Linux était vert.
Pourquoi trois plateformes valent la peine d’être payées
Une question naturelle : pourquoi s’embêter avec Linux du tout ? Le runtime cible des lunettes AR, qui exécutent un OS spécialisé qui n’est ni Linux ni Windows ni macOS de la façon dont l’est une machine de développeur. Pourquoi payer le coût multiplateforme en plus du coût de support du matériel cible réel ?
Trois raisons.
Un : l’inférence IA côté serveur et le rendu cloud se produisent sur Linux. Tout composant côté cloud d’une expérience AR (service de modèles, synchronisation d’état de monde, persistance) s’exécute sur Linux. Le runtime a des points d’ancrage que le côté cloud appelle. Ces points d’ancrage doivent compiler et s’exécuter sur Linux pour que le côté cloud s’intègre. Si le runtime est une bête uniquement Windows, le côté cloud doit soit construire une cale de communication séparée, soit exécuter le runtime sous une couche de compatibilité Linux. Aucune des deux n’est ce que je veux que les partenaires fassent.
Deux : la CI sur Linux est plus rapide et moins chère que la CI sur Windows. Chaque PR que je fusionne passe par la CI. Les runners CI Linux sont plus petits, plus rapides, et moins chers que les runners CI Windows. Plus la boucle CI est rapide, plus d’itérations les agents et moi pouvons exécuter en un samedi. Linux comme cible de build de premier ordre accélère l’ensemble du flux de travail de développement.
Trois : la discipline multiplateforme attrape des bugs. C’est la raison la plus profonde. Quand une base de code ne compile que sur une plateforme, les motifs que les développeurs utilisent sont les motifs qui fonctionnent sur cette plateforme. Les builds multiplateformes forcent les motifs à être portables : des attributs de visibilité explicites au lieu d’implicites, des largeurs de types multiplateformes explicites au lieu de « long fait 32 bits sur ce compilateur », une sémantique de threading explicite au lieu de « ça marche sur Windows ». Le correctif de visibilité de symboles d’aujourd’hui est exactement ce motif. La base de code est plus solide après le correctif qu’avant, sur chaque plateforme, parce que le correctif a rendu le contrat de visibilité explicite.
Ce que cela généralise
Quelques motifs honnêtes.
La visibilité de symboles sur Linux est une taxe que tout le monde paie une fois. La première fois qu’un projet rencontre ce problème, c’est mystérieux et frustrant. Une fois le correctif en place (la macro RAKU_API, appliquée de manière cohérente, avec un garde-fou CI), il est invisible. Le coût est la première fois. Payez-le tôt.
Les garde-fous CI pour les régressions de visibilité de symboles ne sont pas optionnels. Le genre de bug qui prend des mois à apparaître, parce qu’une fonction que personne n’utilise encore manque son étiquette, est exactement le genre de bug qu’un garde-fou CI attrape le jour où il atterrit. Mettez le garde-fou en place.
Les builds multiplateformes rendent la base de code plus honnête. Partout où une base de code dépend du comportement implicite d’une plateforme, le portage multiplateforme force le comportement à devenir explicite. Chaque fois que j’ai porté une base de code vers une nouvelle plateforme, la nouvelle plateforme a fait apparaître des bugs que la plateforme d’origine absorbait silencieusement. Les correctifs sont des améliorations sur chaque plateforme, pas seulement la nouvelle.
Ce que partenaires et bâtisseurs devraient en retenir
Si vous êtes un partenaire décidant sur quel moteur construire pour un produit AR multiplateforme, demandez à l’équipe à quoi ressemble leur matrice de build. Une équipe qui construit sur Linux, macOS, et Windows est une équipe dont la base de code a été disciplinée par les différences entre les trois plateformes. Une équipe qui construit sur une seule est une équipe dont la base de code ne l’a pas été.
Si vous êtes un développeur travaillant sur votre propre projet de bibliothèque partagée multiplateforme, l’audit de visibilité de symboles est l’audit que vous devriez exécuter aujourd’hui, pas en mars quand un test commence à échouer pour des raisons qui prennent trois jours à diagnostiquer. Exécutez nm -D sur vos bibliothèques partagées. Comparez contre vos en-têtes. Les écarts sont l’audit.
Si vous êtes un labo IA dont l’agent de codage écrit du code natif multiplateforme, la macro d’attribut de visibilité est le genre de chose dont l’agent a besoin dans ses connaissances de travail. Un agent qui écrit une nouvelle fonction publique et oublie de l’étiqueter est un agent qui vous coûte une PR de suivi. Un agent qui étiquette chaque fonction publique de manière cohérente est un agent qui se rembourse lui-même.
Bilan du samedi. Dix-huit DLL au vert sur Linux. La CI est plus rapide maintenant. La base de code est plus honnête. La matrice de build fait trois plateformes de large.
Retour à la construction.
Un runtime discipliné par chaque plateforme qu'il touche
RakuAI est le runtime spatial multiplateforme pour les lunettes AR, l'inférence cloud, et tout ce qui se trouve entre les deux — Linux, macOS, et Windows, tous au vert. Voyez pourquoi les partenaires construisent sur une fondation portable.