Technik
Die Grenze, an der beide Seiten kompilieren und eine von beiden lügt
Über eine C-ABI hinweg ist eine falsch geschriebene Funktion kein Übersetzungsfehler, und eine falsch typisierte sieht nicht einmal nm. Eine 32-Bit-Deklaration über einem 64-Bit-Wert machte aus 23347 MB plötzlich 2,8 GB — plausibel, und um das Achtfache daneben.
Aktualisiert 2026-09-12Gemessen 2026-09-11
Zwei Sprachen, die sich auf einer C-ABI treffen, haben keinen gemeinsamen Compiler, also gibt es nichts, was prüft, ob sie sich einig sind. Jede Seite erklärt, was sie von der anderen zu bekommen glaubt, beide kompilieren, und die Uneinigkeit — falls es eine gibt — wird zu einer Eigenschaft der Laufzeit.
Das weiß man im Abstrakten. Weniger offensichtlich ist, wie verschieden die beiden Arten des Scheiterns sind, denn die eine ist bloß lästig und die andere ist wirklich gefährlich.
Ein falscher Name scheitert spät, aber er scheitert
Deklarieren Sie runlix_generat, wo die Bibliothek runlix_generate exportiert, und niemand
protestiert. Auf fast allen Plattformen ist die Symbolauflösung faul: Die Bibliothek wird geladen,
das Programm startet, und der Aufruf scheitert beim ersten Mal, wenn man dorthin kommt. Was ein
Pfad sein kann, den Sie erst am zweiten Tag betreten.
Lästig, aber ehrlich. Wenn es passiert, nennt der Fehler das Symbol, und Sie korrigieren einen Tippfehler.
Die Korrektur, die skaliert, ist nicht Aufmerksamkeit, sondern ein Test. Lesen Sie die Symboltabelle der gebauten Bibliothek und vergleichen Sie sie mit den Deklarationen, in beide Richtungen:
- Jedes Symbol, das der Aufrufer aufzurufen erklärt, muss von der Bibliothek exportiert werden;
- jedes externe Symbol, das die Bibliothek zu wollen erklärt, muss in dem existieren, was Sie dazulinken.
Beide Hälften zählen. Wir hatten eine Zeit lang die erste und nicht die zweite, und die fehlende Hälfte war genau die, die einen echten Defekt gefunden hätte.
Ein falscher Typ scheitert überhaupt nicht
Das ist der interessante Fall.
Deklarieren Sie den richtigen Namen mit dem falschen Typ, und die Prüfung der Symboltabelle geht
durch, denn eine Symboltabelle kennt Namen und keine Signaturen. nm wird Ihnen sagen, dass
runlix_memoria existiert. Es wird Ihnen nicht sagen, dass die eine Seite 64 Bit zu schreiben
glaubt und die andere 32 zu lesen.
Hier der gemessene Fall. Die Engine meldet die von MLX empfohlene Speicherobergrenze als u64,
geschrieben über einen Ausgabeparameter. Die C#-Seite deklarierte den Parameter als out uint —
32 Bit.
Beide kompilieren. Nichts stürzt ab. Der Wert kommt auf seine unteren 32 Bit gekürzt zurück.
Die echte Obergrenze auf der Maschine lag bei 23347 MB. Angekommen sind 2,8 GB.
Sehen Sie sich an, was das schlimm macht. Es ist nicht null, was offensichtlich falsch wäre. Es ist nicht riesig, was offensichtlich falsch wäre. Es ist eine Zahl, die genau wie die Speicherobergrenze eines kleineren Macs aussieht — also hat der nachgelagerte Code sie geglaubt, sein eigenes Budget danach bemessen und angefangen, Modelle zu verdrängen, für die reichlich Platz gewesen wäre. Um das Achtfache daneben, und vollkommen plausibel.
Warum die üblichen Verteidigungen das nicht fangen
Der Compiler kann es nicht. Jede Seite hat gegen ihre eigene Deklaration kompiliert, und beide waren in sich stimmig.
Der Linker kann es nicht. Der Name passte.
nm kann es nicht. Signaturen stehen in der Symboltabelle nicht.
Tests können es oft nicht, und dabei lohnt sich das Verweilen. Ein Test, der prüft, ob der Wert «vernünftig» ist, geht durch: 2,8 GB ist vernünftig. Ein Test, der prüft, ob er größer als null ist, geht durch. Ein Test, der einen bestimmten Wert prüfte, hätte es gefangen — aber eine bestimmte Speicherobergrenze können Sie nicht prüfen, weil sie sich von Maschine zu Maschine unterscheidet.
Gefunden hat es das absichtliche Nebeneinanderlegen der beiden Deklarationen, als Aufgabe. Das heißt: Ein Mensch hat beide gelesen und es bemerkt. Das ist keine befriedigende Antwort, also lautet die Frage, wie man das wiederholbar macht.
Woraus Wiederholbarkeit besteht
Drei Dinge, in aufsteigender Reihenfolge von Mühe und Wirkung.
Erzeugen Sie die eine Seite aus der anderen. Kommen die Deklarationen aus einer einzigen Quelle, können sie nicht uneinig sein. Für fast jedes Sprachpaar gibt es Werkzeuge, und ist die Grenze klein genug, tut es auch das Erzeugen von Hand aus einer gemeinsamen Beschreibung. Das ist die eigentliche Korrektur.
Prüfen Sie den ganzen Umlauf, wo Sie können. Manche Werte lassen sich mit einer unabhängigen Quelle vergleichen — eine Speicherobergrenze kann man grob mit dem vergleichen, was das System angibt, und ein Faktor acht lässt auch eine großzügige Prüfung bequem scheitern. Nicht alles hat so eine Quelle, aber mehr, als man denkt.
Halten Sie die Grenze klein und langweilig. Jede Funktion ist eine Stelle, an der die beiden Deklarationen auseinanderdriften können. Eine Grenze aus fünfzehn Funktionen liest man in einer Sitzung; eine aus zweihundert nicht, und die, die niemand liest, sind genau die, wo diese Sache lebt.
Wir tun das Dritte, wir haben inzwischen die erste Hälfte der Symbolprüfung in beide Richtungen, und die Speicherobergrenze hat eine unabhängige Plausibilitätsprüfung. Die Uneinigkeit über den Typ dagegen wurde durch Lesen gefunden.
Wie die Symbolprüfung in der Praxis aussieht
Es ist weniger Arbeit, als es klingt, und es lohnt sich, es auszuschreiben, weil die naheliegende Umsetzung nur das halbe Handwerk erledigt.
Lesen Sie die exportierten Symbole der gebauten Bibliothek — nm -gU auf macOS, nm -D auf
Linux oder das Äquivalent Ihrer Plattform — und aus derselben Ausgabe die undefinierten. Dann
vergleichen Sie mit zwei Listen, die Sie ohnehin haben: den externen Funktionen, die der Aufrufer
deklariert, und den externen Funktionen, die die Bibliothek zu wollen erklärt.
Der erste Vergleich fängt einen Tippfehler auf der Aufruferseite. Der zweite fängt einen Tippfehler auf der Bibliotheksseite und außerdem etwas Interessanteres: eine Abhängigkeit, die Sie zu linken glaubten und nicht linken. Ausgelassen wird die zweite Hälfte, und sie ist es, die hier ein echtes Problem gefunden hat.
Zwei Einzelheiten unterscheiden eine Prüfung, die wirkt, von einer, die für immer durchgeht. Nehmen Sie die Symbolliste aus dem gebauten Artefakt und nicht aus dem Quelltext: Den Quelltext zu analysieren reproduziert genau die Deklarationen, die Sie prüfen wollen, und ist damit von Bauart her mit sich selbst einig. Und lassen Sie sie in demselben Job laufen, der baut, nicht als getrennten manuellen Schritt — eine Prüfung, an die jemand denken muss, ist eine Prüfung, die genau in der Woche aussetzt, in der sie gebraucht wird.
Was trotzdem ungedeckt bleibt
Über die Grenzen sollte man klar sein, denn der Sinn dieser Notiz ist ja, dass die üblichen Verteidigungen Dinge durchlassen.
Die Symbolprüfung deckt die Anordnung von Structs nicht ab. Wenn beide Seiten sie deklarieren und eine ein Feld hinzufügt, passen die Namen weiterhin und die Größen nicht. Es ist dasselbe Versagen mit größerem Radius, und die einzige echte Verteidigung ist, das Struct aus einer einzigen Definition zu erzeugen.
Sie deckt weder die Aufrufkonvention noch die Lebensdauern ab. Wer alloziert, wer freigibt, ob ein Zeiger den Aufruf überlebt: Nichts davon steht in einer Symboltabelle, und nichts davon steht auch nur in einer Signatur. Es lebt in Kommentaren und in Disziplin — was ein weiteres Argument für eine Grenze ist, die klein genug ist, um sie im Kopf zu behalten.
Der andere Grund, die Grenze klein zu halten
Er gehört erwähnt, weil er dieselbe Entscheidung aus einer anderen Richtung gestützt hat: Unsere C-Grenze ist eine Schnittstelle mit Nachfragen und nicht mit Rückrufen. Der Aufrufer fragt nach dem nächsten Token; nichts ruft in den verwalteten Teil zurück.
Diese Wahl wurde getroffen, damit kein fremder Thread zu einem Zeitpunkt, den niemand gewählt hat, in eine Laufzeit mit Speicherbereinigung hineinläuft. Aber sie hat einen zweiten Effekt, der sich als wichtiger erwiesen hat: Eine Grenze mit Nachfragen hat weniger Funktionsformen, keine Funktionszeiger, die sie überqueren, und keine Frage danach, wem ein Rückruf gehört. Weniger Deklarationen, die einig bleiben müssen, ist dieselbe Eigenschaft wie weniger Stellen, an denen man sich irren kann.
Die Kurzfassung
- Eine externe Funktion mit verstümmeltem Namen ist kein Build-Fehler. Die Auflösung ist faul.
- Eine mit falschem Typ sieht nicht einmal
nm, weil Symboltabellen Namen halten, keine Signaturen. - Gemessen:
out uintüber einemu64machte aus einer Obergrenze von 23347 MB 2,8 GB — gekürzt, plausibel, um das Achtfache daneben. - Prüfen Sie Symbole in beide Richtungen, erzeugen Sie die Deklarationen aus einer einzigen Quelle, wo Sie können, und halten Sie die Grenze klein genug, dass jemand sie ganz lesen kann.
Gemessen am 11. September 2026, an einer C-ABI zwischen Rust und .NET. Nichts davon ist auf dieses Paar beschränkt.