Serie: Programmieren lernen mit KI

Alle achtzehn DLLs grün auf Linux

Der Symbol-Visibility-Sweep, der den Linux-Build grün gemacht hat.

Alle achtzehn grün auf Linux 347 Funktionen getaggt, ein CI-Guard, drei Plattformen 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"))) nm -D Guard: lässt jeden PR fehlschlagen, der ein Symbol fallen lässt
Achtzehn Bibliotheken, ein konsistenter Visibility-Vertrag, ein Guard, der ihn ehrlich hält.

Cross-Platform-Builds erweitern nicht nur deine Reichweite — sie zwingen deine Codebasis, sich nicht mehr auf die stillen Gefälligkeiten eines einzigen Compilers zu verlassen. Jede Plattform, die du hinzufügst, macht die Runtime ehrlicher auf denen, die du schon hattest.

Der Linux-Build war seit Wochen ein bekannter Teilerfolg. Die meisten der achtzehn nativen Runtime-DLLs (die Engine ist als Flotte fokussierter Shared Libraries strukturiert) hatten sauber gebaut. Ein paar nicht. Die Fehlschläge hatten jedes Mal dieselbe Form: Undefined-Symbol-Fehler zur Link-Zeit bei Funktionen, die offensichtlich in der Codebasis existierten. Jeder, der mit Linux-Shared-Libraries in C++ gearbeitet hat, weiß, wohin das führt.

Diesen Samstag habe ich mich mit der Absicht hingesetzt, den Job zu Ende zu bringen. Am Ende des Samstags bauten alle achtzehn DLLs sauber, und die Linux-CI-Pipeline meldete durchgängig grün.

Dieser Post handelt davon, was das eigentliche Problem war, wie der Fix aussah, und warum ich glaube, dass Bauen auf drei Plattformen (Linux, macOS, Windows MSVC) eine Disziplin ist, die es wert ist, dafür zu bezahlen.

Wie der Fehlschlag aussah

Das Muster war immer etwa so. Eine Test-Binärdatei, die gegen z. B. libraku_runtime.so linkt, würde mit Fehlern wie diesen scheitern:

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

Die Funktionen existierten tatsächlich. Sie waren geschrieben worden. Sie waren in den Quelldateien. Sie waren kompiliert worden. Die Objektdateien enthielten sie. Die Shared Library exportierte sie, bei Inspektion mit nm -D libraku_runtime.so, nicht.

Das ist das klassische Linux-Shared-Library-Symbol-Visibility-Problem. Die Runtime war mit dem CMake-Flag -fvisibility=hidden auf ihren Quelldateien gebaut worden, was ein durchaus guter Standard ist. Die Absicht von -fvisibility=hidden ist, interne Symbole aus der öffentlichen Symboltabelle der Shared Library herauszuhalten. Alles, was öffentlich sein muss, muss explizit mit __attribute__((visibility("default"))) oder mit einem Makro, das dazu expandiert, getaggt werden.

Das Build-System der Runtime hatte ein Makro namens RAKU_API, das auf jeder Plattform zum richtigen Visibility-Attribut expandieren sollte. Auf Windows expandierte RAKU_API zu __declspec(dllexport) beim Bauen der DLL und zu __declspec(dllimport) beim Konsumieren. Auf Linux sollte es beim Bauen zu __attribute__((visibility("default"))) expandieren und beim Konsumieren zu nichts.

Das Problem war, dass die Linux-Seite des Makros nicht auf jede öffentliche Funktion angewendet worden war. Manche Funktionen waren getaggt worden. Viele nicht. Die nicht getaggten wurden vom -fvisibility=hidden-Default versteckt und verschwanden aus der öffentlichen Symboltabelle.

Wie der Fix aussah

Drei Schritte, alle mechanisch, alle das Aufschreiben wert.

Erstens: jeden öffentlichen API-Header auf fehlende RAKU_API-Tags auditieren. Ein Skript geschrieben, das jede .h-Datei in der öffentlichen API parst und jede Funktionsdeklaration findet, die öffentlich sein sollte, aber nicht getaggt ist. Das Skript meldet die Funktion, die Datei und die Zeile. Skript laufen lassen. Eine Liste von dreihundertsiebenundvierzig Funktionen über achtzehn DLLs erhalten, die das Tag brauchten.

Zweitens: das Tag im Sweep hinzufügen. Das ist genau die Art von mechanischem Refactoring, das ein Agent gut kann. Der Issue-Rahmen sagte: „Für jede unten aufgelistete Funktion füge RAKU_API zur Deklarationszeile hinzu. Ändere den Funktionskörper nicht. Ändere keine andere Zeile. Führe den Build nach jedem Batch eines Subsystems aus und bestätige, dass er auf Linux weiterhin baut.” Der Agent hat das sauber gemacht, Subsystem für Subsystem. Jeder Subsystem-PR war für sich reviewbar, und der Linux-Build wurde mit jedem Merge grüner.

Drittens: eine CI-Prüfung, die Regressionen verhindert. Eine Prüfung hinzugefügt, die zur PR-Zeit läuft. Die Prüfung baut die Runtime auf Linux, führt nm -D auf jeder erzeugten .so aus, vergleicht die exportierte Symbolliste mit der erwarteten Menge, die in den öffentlichen API-Headern deklariert ist, und lässt den PR fehlschlagen, wenn ein erwartetes Symbol fehlt. Das ist die Art von Schutzmechanismus, der die nächste Instanz von „ich habe vergessen, eine neue Funktion zu taggen” am Tag ihrer Landung abfängt statt Monate später.

Am Ende des Samstags exportierten alle achtzehn DLLs ihre vollständige öffentliche Oberfläche. Die Test-Binärdateien linkten. Die Tests liefen. Der Linux-Build war grün.

Warum drei Plattformen es wert sind, dafür zu bezahlen

Eine naheliegende Frage: Warum sich überhaupt mit Linux beschäftigen? Die Runtime zielt auf AR-Brillen ab, die ein spezialisiertes Betriebssystem laufen lassen, das weder Linux noch Windows noch macOS ist im Sinne einer Entwicklermaschine. Warum die Cross-Platform-Kosten obendrauf zu den Kosten für die tatsächliche Zielhardware zahlen?

Drei Gründe.

Erstens: serverseitige KI-Inferenz und Cloud-Rendering laufen auf Linux. Jede Cloud-seitige Komponente einer AR-Experience (Model-Serving, Weltzustand-Synchronisierung, Persistenz) läuft auf Linux. Die Runtime hat Hooks, die die Cloud-Seite aufruft. Diese Hooks müssen auf Linux bauen und laufen, damit die Cloud-Seite integrieren kann. Wenn die Runtime ein reines Windows-Tier ist, muss die Cloud-Seite entweder einen separaten Kommunikations-Shim bauen oder die Runtime unter einer Linux-Kompatibilitätsschicht laufen lassen. Keines davon ist es, was ich mir von Partnern wünsche.

Zweitens: CI auf Linux ist schneller und günstiger als CI auf Windows. Jeder PR, den ich merge, läuft durch CI. Linux-CI-Runner sind kleiner, schneller und günstiger als Windows-CI-Runner. Je schneller der CI-Loop, desto mehr Iterationen können die Agenten und ich an einem Samstag durchlaufen. Linux als erstklassiges Build-Ziel beschleunigt den gesamten Entwicklungs-Workflow.

Drittens: Cross-Platform-Disziplin fängt Bugs ab. Das ist der tiefste Grund. Wenn eine Codebasis nur auf einer Plattform baut, sind die Muster, zu denen die Entwickler greifen, die Muster, die auf dieser Plattform funktionieren. Cross-Platform-Builds zwingen die Muster, portabel zu sein: explizite Visibility-Attribute statt implizite, explizite plattformübergreifende Typbreiten statt „long ist auf diesem Compiler 32 Bit”, explizite Threading-Semantik statt „das funktioniert auf Windows.” Der heutige Symbol-Visibility-Fix ist genau dieses Muster. Die Codebasis ist nach dem Fix stärker als davor, auf jeder Plattform, weil der Fix den Visibility-Vertrag explizit gemacht hat.

Worauf sich das verallgemeinert

Ein paar ehrliche Muster.

Symbol-Visibility auf Linux ist eine Steuer, die jeder einmal zahlt. Beim ersten Mal, dass ein Projekt darauf trifft, ist es mysteriös und frustrierend. Sobald der Fix vorhanden ist (das RAKU_API-Makro, konsequent angewendet, mit einem CI-Guard), ist es unsichtbar. Die Kosten fallen beim ersten Mal an. Zahl sie früh.

CI-Guards für Symbol-Visibility-Regressionen sind nicht optional. Die Art von Bug, die Monate braucht, um ans Licht zu kommen, weil eine Funktion, die noch niemand verwendet, ihr Tag fehlt, ist genau die Art von Bug, den ein CI-Guard am Tag ihrer Landung abfängt. Setz den Guard.

Cross-Platform-Builds machen die Codebasis ehrlicher. Überall dort, wo sich eine Codebasis auf implizites Verhalten einer Plattform verlässt, zwingt der Cross-Platform-Port das Verhalten, explizit zu werden. Jedes Mal, wenn ich eine Codebasis auf eine neue Plattform portiert habe, hat die neue Plattform Bugs zutage gebracht, die die ursprüngliche Plattform still absorbiert hatte. Die Fixes sind Verbesserungen auf jeder Plattform, nicht nur auf der neuen.

Was Partner und Entwickler daraus mitnehmen sollten

Wenn du ein Partner bist, der entscheidet, auf welche Engine er für ein Cross-Platform-AR-Produkt setzt, frag das Team, wie ihre Build-Matrix aussieht. Ein Team, das auf Linux, macOS und Windows baut, ist ein Team, dessen Codebasis von den Unterschieden zwischen den drei Plattformen diszipliniert wurde. Ein Team, das auf einer baut, ist ein Team, dessen Codebasis das nicht wurde.

Wenn du an deinem eigenen Cross-Platform-Shared-Library-Projekt arbeitest, ist das Symbol-Visibility-Audit das Audit, das du heute durchführen solltest, nicht im März, wenn ein Test aus Gründen zu scheitern beginnt, die drei Tage zur Diagnose brauchen. Führ nm -D auf deinen Shared Libraries aus. Vergleiche mit deinen Headern. Die Diskrepanzen sind das Audit.

Wenn du ein KI-Labor bist, dessen Coding-Agent Cross-Platform-nativen Code schreibt: Das Visibility-Attribut-Makro ist die Art von Sache, die der Agent in seinem Arbeitswissen braucht. Ein Agent, der eine neue öffentliche Funktion schreibt und vergisst, sie zu taggen, ist ein Agent, der dich einen Folge-PR kostet. Ein Agent, der jede öffentliche Funktion konsequent taggt, ist ein Agent, der sich selbst bezahlt macht.

Samstagsabschluss. Achtzehn DLLs grün auf Linux. Die CI ist jetzt schneller. Die Codebasis ist ehrlicher. Die Build-Matrix ist drei Plattformen breit.

Zurück ans Bauen.

Eine Runtime, diszipliniert von jeder Plattform, die sie berührt

RakuAI ist die Cross-Platform-spatiale Runtime für AR-Brillen, Cloud-Inferenz und alles dazwischen — Linux, macOS und Windows, alles grün. Sieh, warum Partner auf einem portablen Fundament bauen.

← Alle Beiträge