- Code, der sich strikt nur an den ISO-C-Standard hält, ist selten; reale C-Codebasen verlassen sich auf nicht standardisierte Erweiterungen, um Funktionen zu ergänzen und Lücken je nach Compiler und Bibliothek zu umgehen
- Ein nützlicher C-Compiler muss schon System-Header wie
<stdio.h>verarbeiten können, aber glibc errichtet durch GNU-Erweiterungen und Annahmen wie__attribute__((packed))und#include_nextHürden - Die Byteswapping-Logik von SDL kann bei vorhandenen ISA-Makros inline assembly wählen, sodass auch Compiler, die weder GCC noch clang sind, GCC-artige Erweiterungen unterstützen müssen
- Die Behandlung von
extern inlinein OpenBSD und Gnulib macht die Kompatibilität der inline-Semantik durch Unterschiede zwischen C99 und GCC-Semantik, plattformspezifische Verzweigungen und_FORTIFY_SOURCE-Bedingungen kompliziert - Kleine C-Compiler müssen zwischen Upstream-Patches, Downstream-Patches, eigenen Guards und dem Nachahmen von GCC-Kompatibilität wählen; die Ausweitung von Feature-Test-Makros scheint der bessere Weg zu sein
Die erste Hürde durch glibc-Header
- Um ein nützlicher C-Compiler zu sein, muss man Header der System-C-Bibliothek präprozessieren und parsen können; wer
<stdio.h>nicht verarbeiten kann, wird kaum schon ein Hello World kompilieren - In GNU/Linux-Umgebungen führt diese Hürde direkt zu glibc
- glibc prüft in sys/cdefs.h, das fast von allen libc-Headern indirekt eingebunden wird, Compiler-vordefinierte Makros, um zu bestimmen, welche Erweiterungen unterstützt werden
- Nicht unterstützte Erweiterungen werden behandelt, indem die betreffenden Definitionen entfernt werden, aber auch diese Kompatibilitätslogik kann in der Praxis kaputtgehen
-
struct epoll_eventund__attribute__((packed))struct epoll_eventinsys/epoll.hunter Linux ist eine packed struct, die GNU-__attribute__((packed))verwendet- Dieses Attribut verändert auf 64-Bit-Systemen das Layout der Struktur; wenn es ignoriert wird, ist die ABI kaputt
- Es reicht nicht aus, dass ein Compiler
__attribute__((packed))implementiert - In
sys/cdefs.hgibt es Code, der__attribute__(xyz)zu einem leeren Makro definiert, wenn es sich nicht um GCC, clang oder tcc handelt - Dadurch kann das packed-Attribut in glibc-Headern auch bei anderen Compilern entfernt werden, selbst wenn diese es unterstützen
- Da der
epoll-Header Linux-spezifisch ist, lässt sich ein strenger Maßstab der C-Standard-Portabilität hier nur eingeschränkt anlegen
-
limits.hund#include_next- Einige C-Header wie
stddef.h,stdint.h,limits.hundfloat.hwerden auch in freestanding Implementierungen benötigt und müssen daher vom Compiler bereitgestellt werden - POSIX verlangt, dass
limits.hzusätzlich zu den Standard-C-Konstanten auch POSIX-spezifische Konstanten definiert; deshalb wird über demlimits.hdes Compilers noch ein plattformspezifischeslimits.hbenötigt - Das
<limits.h>von glibc definiert, wenn kein GNU C vorliegt, die ANSI-limits.h-Werte direkt und holt in GCC-Umgebungen per#include_next <limits.h>den Compiler-Header - Diese Struktur setzt voraus, dass das GCC-exklusive builtin-
limits.hbestimmte Makros definiert, und hängt außerdem von der Erweiterung#include_nextab - clang muss diese Struktur ebenfalls umgehen
- Einige C-Header wie
Funktionserkennung in SDL und das Problem mit inline assembly
- Die Byteswapping-Funktionen in
SDL_endian.hverwenden nach Möglichkeit Compiler-Builtins oder inline assembly und fallen als letztes Mittel auf eine gewöhnliche Implementierung mit Bitoperationen zurück - Die Erkennungslogik arbeitet grob in dieser Reihenfolge
- Wenn GCC oder clang vorliegt und
__has_builtin(__builtin_bswapX)verfügbar ist, werden Builtins verwendet - Bei MSVC 8.0 oder neuer wird das MSVC-Intrinsic per
#pragmaverwendet - Wenn ISA-spezifische Makros wie
__x86_64__definiert sind, wird inline assembly verwendet - Andernfalls wird die allgemeine Implementierung mit Bitoperationen genutzt
- Wenn GCC oder clang vorliegt und
- Definiert ein Compiler, der weder GCC noch clang ist, aus gutem Grund ISA-spezifische vordefinierte Makros, wird diese Reihenfolge problematisch
- Selbst wenn dieser Compiler ein bswap-Builtin und den Spezialoperator
__has_builtinbereitstellt, kann die Logik trotzdem versuchen, GCC-artige inline assembly zu verwenden - Dadurch entsteht faktisch die Erwartung, dass auch ein unbekannter Compiler GCC-Stil-inline-assembly unterstützt
OpenBSD libc und die Verwirrung um extern inline
- Einige Header von OpenBSD enthalten Inline-Funktionsdefinitionen, die der Compiler bei Optimierung optional verwenden kann
- Diese Funktionen werden über das Makro
__only_inlinedefiniert; wenn der Compiler sie nicht tatsächlich inlined, muss stattdessen ein externes Symbol verwendet werden - Dafür werden also Inline-Funktionen mit externem Linkage benötigt
-
Unterschiede zwischen C99-inline und GCC-inline-Semantik
inlineist in C99 festgelegt, aber das Standardverhalten kollidiert mit dem nicht standardisierten GCC-Verhalten aus der Zeit vor C99- Eine Inline-Definition in einem Header muss zusammen mit dem Funktionskörper
extern inlineverwenden; in diesem Fall wird keine tatsächlich exportierte Funktion emittiert - In einer Translation Unit muss die Deklaration nur mit
inlineversehen sein, um die Funktionsdefinition zu exportieren - Die Bedeutung von
inlineunterscheidet sich auch zwischen C++ und C - Diese Unterschiede werden im Artikel von Youtao Guo ausführlich behandelt
-
OpenBSDs
__only_inline- OpenBSD verlässt sich auf GCC-inline-Semantik
- Um Unterschiede zwischen GCC-Versionen abzudecken, legt das Makro
__only_inlinein sys/cdefs.h bei neueren GCC-Versionen per explizitem__attribute__die ältere gnu89-inline-Semantik fest - Bei Nicht-GNU-Compilern wird
__only_inlinealsstatic-Linkage definiert - Dadurch können Funktionen mit widersprüchlichem Linkage deklariert und definiert werden und daran zerbrechen
-
Der Umweg über
_ANSI_LIBRARY- OpenBSD respektiert das Makro
_ANSI_LIBRARY - Wenn dieses Makro definiert ist, wird die problematische
__only_inline-Definition in Standard-Headern wiesignal.hvollständig weggelassen - Die optimierte Version erhält man dann zwar nicht, aber zumindest funktioniert der Build
- OpenBSD respektiert das Makro
-
Gnulibs Kompatibilitätscode für
extern inline- Der Kompatibilitätscode von Gnulib für
extern inlinetaucht auch beim Build von Guile und nano auf - extern-inline.m4 enthält komplexe bedingte Verzweigungen, um kaputte und merkwürdige Implementierungen dieses C-Eckfalls zu behandeln
- Die Bedingungen spiegeln Unterschiede zwischen Apple, DragonFly, FreeBSD, GCC, clang, PCC, HP cc, PGI, SunPro C,
_FORTIFY_SOURCE,__GNUC_STDC_INLINE__und__GNUC_GNU_INLINE__wider
- Der Kompatibilitätscode von Gnulib für
Android bionic und die Annahme von clang
- bionic ist die libc von Android, und ihre Header setzen sogar stärker auf clang als auf GCC
- Die bionic-Header verwenden für Nullability-Checks viele clang-exklusive Erweiterungen wie
_Nonnullund_Null_unspecified - Solche Makros lassen sich per Kommandozeilen-Flag nicht besonders schwer per
#defineneutralisieren - Wenn man über Termux ein Android-Smartphone als native aarch64-Entwicklungsumgebung nutzt, wird dieses Problem in den bionic-Headern sichtbar
_Null_unspecifiedwird auch__BIONIC_COMPLICATED_NULLNESSgenannt; die zugehörige Definition steht in bionics sys/cdefs.h
Vor welchen Entscheidungen kleine C-Compiler stehen
- Code, der sich strikt nur an den ISO-C-Standard hält, ist in der Praxis selten, und viele C-Codebasen hängen von nicht standardisiertem Verhalten und Spracherweiterungen ab
- Diese Abhängigkeiten entstehen nicht nur für zusätzliche Funktionen, sondern auch beim Umgehen unterschiedlicher Bugs und Lücken je nach Compiler und Bibliothek
- Codebasen, die mehrere Umgebungen unterstützen wollen, setzen auf Präprozessor-Prüfungen und Guards, aber dieser Ansatz bricht leicht und ist schwer handhabbar
- Beim Bau eines C-Compilers wie antcc treten solche Kompatibilitätsprobleme immer wieder auf
- Wenn viele Open-Source-Projekte auch in nicht essenziellen Bereichen von compilerspezifischen nicht standardisierten Erweiterungen und Verhaltensweisen abhängen, steigt die Last für alternative Compiler
- Gleichzeitig ist es schwer zu verlangen, dass alle Entwickler ihren C-Code auf mehreren Compilern testen, einschließlich kleinerer und weniger bekannter Compiler
- C-Portabilität ist schon für sich genommen schwierig genug
- Aus Sicht von Compiler-Autoren gibt es vier mögliche Wege
- versuchen, die Inkompatibilitäten Upstream zu patchen
- bekannt genug werden, damit Entwickler eigene
#ifdef-Prüfungen und grundlegende Tests hinzufügen - das Problem Downstream behandeln und Patches oder separate Patches verteilen
- sich als bestimmte GCC-Version ausgeben und die betreffenden Erweiterungen implementieren
- Upstream-Patches wirken wie ein schwer zu gewinnender Kampf, und Downstream-Patches sind der einfachste Weg
- Um viele Codebasen mit minimaler Verwirrung für Nutzer und Entwickler zu unterstützen, ist das Nachahmen von GCC-Kompatibilität realistisch, aber mit hohem Implementierungsaufwand verbunden
- clang definiert
__GNUC__=4,__GNUC_MINOR__=2und__GNUC_PATCHLEVEL__=1, um Kompatibilität zu GCC 4.2.1 zu beanspruchen - clang ist heute fast ein eigenes Unterstützungsziel, aber es war so viel Aufwand nötig, damit sich der Linux-Kernel mit clang kompilieren ließ, dass Patches in beiden Projekten erforderlich waren
GCC-Makros und das Problem des Hinterherlaufens
- Auch das Vorgeben, GCC zu sein, hat Probleme
- Viele Codebasen prüfen nur
#ifdef __GNUC__und verwenden dann womöglich neuere GCC-Erweiterungen ohne Versionsprüfung - In diesem Fall muss ein alternativer Compiler ständig hinterherlaufen
- Einer der Gründe, warum clang trotz Unterstützung für GNU-Erweiterungen, die neuer als 4.2.1 sind, den Wert seiner
__GNUC__-Makros nicht erhöht, liegt genau hier - Hintergrund dazu findet sich in der LLVM-Diskussion über die Anhebung von Clangs
__GNUC__-Minor-Version
Ein besserer Weg und der aktuelle Stand
- Idealerweise sollten statt compilerspezifischer Guards und Versionsprüfungen Feature-Test-Makros viel breiter eingesetzt werden
- Nützliche Feature-Test-Makros sind
__has_builtin,__has_featureund__has_attribute - Auch standardisierte Makros nach dem Muster von
__STDC_NO_VLA__könnten häufiger verwendet werden - In der *NIX-Welt ist ein quasi-duopolistischer Zustand von GCC und clang derzeit, im Guten wie im Schlechten, der Standard
- Die Entwicklung unabhängiger kleiner C-Compiler geht dennoch weiter
1 Kommentare
Lobste.rs-Kommentare
(Autor des kefir-Compilers) Das
__attribute__-Problem in<sys/cdefs.h>gehört meiner Erfahrung nach zu den unerquicklichsten Fällen. Es zerstört epoll, übliche packed-Strukturen, Konstruktoren und Symbolsichtbarkeit, weshalb ich am Ende diesen Monkeypatch-Header mit kefir mitliefern mussteDas ist nicht ideal, aber vermutlich der praktikabelste Ansatz, und in der Praxis konnten wir damit die meisten Custom-Patches aus externen Test-Suites entfernen
Eine andere Fehlerklasse sind fehlerhafte alternative Codepfade. Manche Projekte versuchen, den Compiler zu erkennen und sich entsprechend anzupassen, aber auf alternativen Compilern werden diese Fallback-Pfade oft zu wenig getestet, sind voller Bugs oder schlecht gepflegt. Aus Sicht eines Compiler-Autors ist das viel frustrierender, als direkt mit „nicht unterstützter Compiler“ zu scheitern. Man muss dann nämlich seltsame Fehlkompilierungen selbst debuggen, etwa Breiteninkonsistenzen bei Integer-
typedefs zwischen Programm und vorcompilierten Bibliotheken$TERMnicht aufxterm-256colorsetzt und sich als xterm ausgibt, geht alles Mögliche kaputtIch habe wirklich keine Ahnung, wie man das lösen soll. Am Ende müssen unsere Projekte wohl einfach weit genug verbreitet und bekannt werden. Ganz einfach!
Diese seltsamen Fehlkompilierungen, die durch schlecht gepflegte Compiler-Erkennungs-Fallbacks entstehen, habe ich wohl auch schon ein paar Mal erlebt, und sie sind wirklich lästig
Ich entwickle cproc hauptsächlich auf linux-musl, daher wusste ich nicht, dass glibc
__attribute__bei anderen Compilern deaktiviert, aber das ist tatsächlich eine ziemlich schlechte Situation. In den Kommentaren steht zwar, dass die Verwendung von Attributes ignoriert werden könne, berücksichtigt aber nicht, dass die meiste Anwendungscodesys/cdefs.hindirekt einbindet und dabei Attributes nutzen kann, die nicht ignoriert werden dürfenNeben
packedwerden auchalignedundconstructorhäufig verwendetIch frage mich, ob das irgendwo im Issue-Tracker gemeldet ist. Der Großteil der Attribute-Nutzung in cdefs.h scheint bereits durch
__glibc_has_attributegeschützt zu sein, deshalb frage ich mich, was die pauschale Deaktivierung von__attribute__überhaupt erreicht und ob man sie entfernen könnteProblematisch sind auch Funktionen, die libc-Header verwenden, für die Compiler ihre Unterstützung nicht sauber signalisieren können. Es sind Dinge, die sich nicht über
__has_attributeoder__has_builtinerkennen lassen; ein Beispiel, das mir einfällt, sind__asm__-Labels. NetBSD nutzt sie zum Umbenennen von Symbolnamen und wirft#error, wenn weder__GNUC__noch__PCC__gesetzt ist. Ich weiß allerdings auch nicht, was man sonst vorschlagen sollte, außer es einfach zu versuchen und bei fehlender Unterstützung scheitern zu lassenIch hatte auch Probleme rund um
__builtin_va_list. Manche libc-Implementierungen definieren ohne__GNUC__defineva_listtovoid *oder haben sogar widersprüchliche Definitionen. Auch das lässt sich nicht mit__has_builtintesten.__has_builtin(__builtin_va_arg)könnte ein hinreichend guter Test sein, aber ich weiß nicht, wie man macOS dazu bringen sollte, das zu korrigieren__attribute__in/usr/include/sysund/usr/include/bitsgesucht und viele ungeschützte Stellen gefunden. Meistens waren es__format__,__aligned__,__noreturn__, also müsste man auch diese korrigierenInsgesamt scheint glibc Kompatibilität mit Nicht-GCC-Compilern nicht besonders zu priorisieren, daher weiß ich nicht, wie wahrscheinlich es ist, dass solche Patches angenommen würden. Nach einem System-Upgrade Anfang des Jahres hat glibc in Linux-Headern ungeschützte Nutzung von
__SIZE_TYPE__hinzugefügt, wodurch mein Compiler einige Projekte nicht mehr kompilieren konnte. Ich habe das gemeldet, aber es ist noch nicht behoben, und am Ende habe ich vordefinierte Makros im Stil von__X_TYPE__ergänzt, um GCC zu entsprechenFür das Problem mit
__asm__-Labels fällt mir auch keine gute Lösung ein. Wenn das Umbenennen per asm-Name für die Funktionsfähigkeit wirklich zu 100 % nötig ist, könnte es besser sein, es einfach zu versuchen und bei Fehlern zu scheitern, statt den Compiler zu prüfen__builtin_va_listist ziemlich gravierend. Ich hätte erwartet, dass__has_builtin(__builtin_va_list)funktioniert, aber anscheinend ist das nicht der Fall