it-swarm.com.de

Wann ist es sinnvoll, zuerst meine eigene Sprache in C-Code zu kompilieren?

Wann ist es beim Entwerfen einer eigenen Programmiersprache sinnvoll, einen Konverter zu schreiben, der den Quellcode in C- oder C++ - Code konvertiert, damit ich einen vorhandenen Compiler wie gcc verwenden kann, um Maschinencode zu erhalten? Gibt es Projekte, die diesen Ansatz verwenden?

38
danijar

Die Übersetzung in C-Code ist eine sehr gut etablierte Gewohnheit. Das ursprüngliche C mit Klassen (und die frühen C++ - Implementierungen, die dann Cfront genannt wurden) haben dies erfolgreich durchgeführt. Mehrere Implementierungen von LISP oder Scheme tun dies, z. Hühnerschema , Schema48 , Bigloo . Einige Leute übersetzten Prolog zu C . Und einige Versionen von Mozart (und es gab Versuche, Ocaml-Bytecode zu C ) zu kompilieren. Die künstliche Intelligenz von J.Pitrat CAIA-System wird ebenfalls gebootet und generiert den gesamten C-Code. Vala übersetzt auch in C für GTK-bezogenen Code. Queinnecs Buch LISP In Small Pieces enthält ein Kapitel über die Übersetzung nach C.

Eines der Probleme bei der Übersetzung in C ist rekursive Aufrufe . Der C-Standard garantiert nicht, dass ein C-Compiler sie ordnungsgemäß übersetzt (in einen "Sprung mit Argumenten", dh ohne Aufruf des Aufrufstapels), selbst wenn In einigen Fällen führen neuere Versionen von GCC (oder Clang/LLVM) diese Optimierung durch.

Ein weiteres Problem ist Speicherbereinigung . Einige Implementierungen verwenden nur den Boehm konservativen Garbage Collector (der C-freundlich ist ...). Wenn Sie Code sammeln möchten (wie es mehrere LISP-Implementierungen tun, z. B. SBCL), könnte dies ein Albtraum sein (Sie möchten dlclose unter Posix).

Ein weiteres Problem betrifft erstklassige Fortsetzungen und call/cc . Aber clevere Tricks sind möglich (siehe Chicken Scheme). Der Zugriff auf den Call-Stack kann viele Tricks erfordern (siehe jedoch GNU-Backtrace usw.). Orthogonal Persistenz von Fortsetzungen (d. H. Von Stapeln oder Fäden) wäre in C schwierig.

Bei der Ausnahmebehandlung müssen häufig clevere Aufrufe an longjmp etc ... gesendet werden.

Möglicherweise möchten Sie (in Ihrem ausgegebenen C-Code) entsprechende #line - Anweisungen generieren. Dies ist langweilig und erfordert viel Arbeit (Sie möchten, dass dies beispielsweise einfacher zu gdb - debuggbarem Code führt).

Meine veraltete GCC MELT lispy domänenspezifische Sprache (zum Anpassen oder Erweitern GCC ) wird in C übersetzt (tatsächlich in schlechtes C++ jetzt) . Es hat einen eigenen Generationen-Kopier-Garbage-Collector. (Sie könnten interessiert sein an Qish oder Ravenbrook MPS ). Tatsächlich ist Generations-GC in maschinengeneriertem C-Code einfacher als in handgeschriebenem C-Code (da Sie Ihren C-Code-Generator für Ihre Schreibbarriere- und GC-Maschinen anpassen).

Ich kenne keine Sprachimplementierung, die in echten C++ - Code übersetzt, dh mit einer "Garbage Collection" -Technik zur Kompilierungszeit, um C++ - Code mit viel Geld auszugeben von STL-Vorlagen und unter Berücksichtigung der RAII Redewendung. (Bitte sagen Sie, wenn Sie eine kennen).

Was heute lustig ist, ist, dass (auf aktuellen Linux-Desktops) C-Compiler möglicherweise schnell genug sind, um eine interaktive oberste Ebene zu implementieren read-eval-print-loop übersetzt in C: Sie geben C-Code aus (a Einige hundert Zeilen) Bei jeder Benutzerinteraktion werden Sie fork eine Zusammenstellung davon zu einem gemeinsam genutzten Objekt erstellen, das Sie dann dlopen verwenden würden. (MELT macht das alles fertig und es ist normalerweise schnell genug). All dies kann einige Zehntelsekunden dauern und ist für Endbenutzer akzeptabel.

Wenn möglich, würde ich empfehlen, nach C zu übersetzen, nicht nach C++, insbesondere weil die C++ - Kompilierung langsam ist. C++ hat heute jedoch einen leistungsstarken Standard Container , Ausnahmen , λ-Ausdrücke usw. usw. und wird von interessanten C++ - Bibliotheken verwendet oder benötigt oder Frameworks wie Qt , POCO , Tensorflow , und all diese Funktionen motivieren die Wahl C++ - Code in einem meiner Lieblingsprojekte namens RefPerSys zu generieren. Wenn Sie C++ dynamisch generieren, warten Sie entweder länger als eine Sekunde, bis jede generierte C++ - Datei kompiliert ist (z. B. in ein temporäres Plugin , siehe für Linux das C++ dlopen mini howto ) oder Verwenden Sie clevere Tricks (z. B. ccache und/oder vorkompilierte GCC-Header usw.) und minimieren Sie nach Möglichkeit die Gesamtmenge von #include - d material), um die C++ - Kompilierungszeit zu verkürzen.

Wenn Sie Ihre Sprache implementieren, können Sie auch einige JIT Bibliotheken wie libjit , GNU Blitz , asmjit oder sogar LLVM oder GCCJIT . Wenn Sie nach C übersetzen möchten, können Sie manchmal tinycc verwenden: Der generierte C-Code wird sehr schnell kompiliert (auch in Speicher) bis langsamer Maschinencode. Aber im Allgemeinen möchten Sie die Optimierungen nutzen, die von einem echten C-Compiler wie GCC durchgeführt wurden

Wenn Sie Ihre Sprache in C übersetzen, stellen Sie sicher, dass Sie zuerst den gesamten AST des generierten C-Codes im Speicher erstellen (dies erleichtert auch die Generierung aller zuerst Deklarationen, dann alle Definitionen und Funktionscode). Auf diese Weise können Sie einige Optimierungen/Normalisierungen vornehmen. Sie könnten auch an mehreren GCC-Erweiterungen (z. B. berechneten gotos) interessiert sein. Sie sollten wahrscheinlich vermeiden, große C-Funktionen zu generieren - z. von hunderttausend Zeilen generierten C Sie sollten sie besser in kleinere Teile aufteilen), da die Optimierung von C-Compilern mit sehr großen C-Funktionen sehr unzufrieden ist (in der Praxis und experimentell gcc -O Kompilierungszeit großer Funktionen ist proportional zum Quadrat der Funktionscodegröße). Begrenzen Sie daher die Größe Ihrer generierten C-Funktionen auf jeweils einige tausend Zeilen.

Beachten Sie, dass sowohl --- (Clang (bis LLVM ) als auch GCC = (bis libgccjit ) C & C++ - Compiler bieten eine Möglichkeit, einige für diese Compiler geeignete interne Darstellungen auszugeben. Dies ist jedoch möglicherweise schwieriger (oder nicht) als die Ausgabe von C oder C++) Code spezifisch für jeden Compiler.

Wenn Sie eine Sprache entwerfen, die in C übersetzt werden soll, möchten Sie wahrscheinlich mehrere Tricks (oder Konstrukte) haben, um eine Mischung aus C mit Ihrer Sprache zu generieren. Mein DSL2011-Artikel --- ( MELT: eine im GCC-Compiler eingebettete übersetzte domänenspezifische Sprache sollte Ihnen nützliche Hinweise geben.

55

Es ist sinnvoll, wenn die Zeit zum Generieren des vollständigen Maschinencodes die Unannehmlichkeit überwiegt, einen Zwischenschritt zum Kompilieren Ihrer "IL" in Maschinencode mit einem C-Compiler zu haben.

Typischerweise werden domänenspezifische Sprachen auf diese Weise geschrieben. Ein System auf sehr hoher Ebene wird verwendet, um einen Prozess zu definieren oder zu beschreiben, der dann in eine ausführbare Datei oder DLL kompiliert wird. Die Zeit, die benötigt wird, um eine funktionierende/gute Assembly zu erstellen, ist viel länger als das Generieren von C, und C liegt ziemlich nahe am Assembly-Code für die Leistung. Daher ist es sinnvoll, C zu generieren und die Fähigkeiten der C-Compiler-Autoren wiederzuverwenden. Beachten Sie, dass es nicht nur kompiliert, sondern auch optimiert wird - die Leute, die gcc oder llvm schreiben, haben viel Zeit damit verbracht, optimierten Maschinencode zu erstellen. Es wäre dumm zu versuchen, all ihre harte Arbeit neu zu erfinden.

Es ist möglicherweise akzeptabler, das Compiler-Backend von LLVM wiederzuverwenden, dessen IIRC sprachneutral ist. Daher generieren Sie LLVM-Anweisungen anstelle von C-Code.

8
gbjbaanb

Das Schreiben eines Compilers zum Erzeugen von Maschinencode ist möglicherweise nicht viel schwieriger als das Schreiben eines Compilers, der C erzeugt (in einigen Fällen ist dies möglicherweise einfacher), aber ein Compiler, der Maschinencode erzeugt, kann nur auf der jeweiligen Plattform ausführbare Programme erstellen, für die es wurde geschrieben; Im Gegensatz dazu kann ein Compiler, der C-Code erzeugt, möglicherweise Programme für jede Plattform erstellen, die einen C-Dialekt verwendet, den der generierte Code unterstützen soll. Beachten Sie, dass es in vielen Fällen möglich sein kann, C-Code zu schreiben, der vollständig portabel ist und sich wie gewünscht verhält, ohne Verhaltensweisen zu verwenden, die nicht durch den C-Standard garantiert werden. Code, der auf plattformgarantierten Verhaltensweisen beruht, kann jedoch möglicherweise viel schneller ausgeführt werden auf Plattformen, die diese Garantien geben, als Code, der dies nicht tut.

Angenommen, eine Sprache unterstützt eine Funktion, mit der aus vier aufeinanderfolgenden Bytes eines willkürlich ausgerichteten UInt32 Einen UInt8[] Ergeben wird, der auf Big-Endian-Weise interpretiert wird. Auf einigen Compilern könnte man den Code wie folgt schreiben:

uint32_t dat = *(__packed uint32_t*)p;
return (dat >> 24) | (dat >> 8) | ((uint32_t)dat << 8) | ((uint32_t)dat << 24));

und lassen Sie den Compiler eine Word-Ladeoperation generieren, gefolgt von einer Anweisung zum Umkehren von Bytes in Word. Einige Compiler würden den Modifikator __packed jedoch nicht unterstützen und in Abwesenheit Code generieren, der nicht funktionieren würde.

Alternativ könnte man den Code schreiben als:

return dat[3] | ((uint16_t)dat[2] << 8) | ((uint32_t)dat[1] << 16) | ((uint32_t)dat[0] << 24);

ein solcher Code sollte auf jeder Plattform funktionieren, auch auf solchen, auf denen CHAR_BITS nicht 8 ist (vorausgesetzt, dass jedes Oktett der Quelldaten in einem bestimmten Array-Element endet), aber ein solcher Code wird wahrscheinlich nicht annähernd so schnell ausgeführt wie Das wäre die nicht tragbare Version auf Plattformen, die die erstere unterstützen.

Beachten Sie, dass die Portabilität häufig erfordert, dass der Code bei Typecasts und ähnlichen Konstrukten äußerst liberal ist. Beispielsweise muss Code, der zwei vorzeichenlose 32-Bit-Ganzzahlen multiplizieren und die unteren 32 Bit des Ergebnisses ergeben möchte, für die Portabilität wie folgt geschrieben werden:

uint32_t result = 1u*x*y;

Ohne diesen 1u Könnte ein Compiler auf einem System, auf dem INT_BITS zwischen 33 und 64 lag, legitimerweise alles tun, was er wollte, wenn das Produkt von x und y größer als 2.147.483.647 war, und einige Compiler neigen dazu, solche Möglichkeiten zu nutzen .

2
supercat

Sie haben oben einige ausgezeichnete Antworten, aber angesichts der Tatsache, dass Sie in einem Kommentar die Frage "Warum möchten Sie überhaupt eine eigene Programmiersprache erstellen?" Mit "Es würde hauptsächlich zu Lernzwecken dienen" beantwortet haben: "Ich". Ich werde aus einem anderen Blickwinkel antworten.

Es ist sinnvoll, einen Konverter zu schreiben, der den Quellcode in C- oder C++ - Code konvertiert, damit Sie einen vorhandenen Compiler wie gcc verwenden können, um Maschinencode zu erhalten, wenn Sie mehr über Lexik, Syntax und semantische Analyse, als Sie etwas über Codegenerierung und -optimierung lernen!

Das Schreiben eines eigenen Maschinencodegenerators ist eine ziemlich wichtige Arbeit, die Sie durch Kompilieren in C-Code vermeiden können, wenn Sie nicht hauptsächlich daran interessiert sind!

Wenn Sie sich jedoch für das Assembly-Programm interessieren und von den Herausforderungen der Codeoptimierung auf der untersten Ebene fasziniert sind, schreiben Sie auf jeden Fall selbst einen Codegenerator für die Lernerfahrung!

1
Carson63000