1 poin oleh GN⁺ 17 jam lalu | 1 komentar | Bagikan ke WhatsApp
  • Kode yang hanya mengikuti standar ISO C itu jarang, dan codebase C di dunia nyata bergantung pada ekstensi nonstandar untuk menambah fitur serta mengatasi celah pada tiap kompiler dan pustaka
  • Kompiler C yang berguna harus bisa memproses header sistem seperti <stdio.h>, tetapi glibc membuat hambatan melalui ekstensi GNU dan asumsi seperti __attribute__((packed)) dan #include_next
  • Logika byteswapping di SDL dapat memilih inline assembly jika ada makro ISA, sehingga kompiler selain GCC·clang pun bisa dituntut mendukung ekstensi gaya GCC
  • Penanganan extern inline di OpenBSD dan Gnulib membuat kompatibilitas semantik inline menjadi rumit karena perbedaan makna antara C99 dan GCC, percabangan per platform, serta kondisi _FORTIFY_SOURCE
  • Kompiler C kecil harus memilih antara patch upstream, patch downstream, menyiapkan guard khusus, atau meniru kompatibilitas GCC, dan perluasan makro pengujian fitur tampak sebagai arah yang lebih baik

Hambatan pertama yang dibuat header glibc

  • Untuk menjadi kompiler C yang berguna, ia harus mampu memproses dan mengurai header pustaka C sistem, dan jika tidak bisa menangani <stdio.h>, akan sulit bahkan untuk meloloskan hello world
  • Di lingkungan GNU/Linux, hambatan ini berujung pada glibc
  • glibc menentukan ekstensi yang didukung dengan memeriksa makro yang telah didefinisikan kompiler dalam sys/cdefs.h, yang secara tidak langsung disertakan oleh hampir semua header libc
  • Ekstensi yang tidak didukung ditangani dengan menghapus definisi terkait, tetapi logika kompatibilitas ini sendiri pada praktiknya bisa rusak
  • struct epoll_event dan __attribute__((packed))

    • struct epoll_event di sys/epoll.h Linux adalah packed struct yang menggunakan GNU __attribute__((packed))
    • Atribut ini mengubah layout struktur pada 64-bit, sehingga jika diabaikan maka ABI akan rusak
    • Tidak cukup jika kompiler hanya mengimplementasikan __attribute__((packed))
    • Di sys/cdefs.h, ada kode yang mendefinisikan __attribute__(xyz) sebagai makro kosong jika kompiler bukan GCC, clang, atau tcc
    • Akibatnya, kompiler lain bisa kehilangan atribut packed di header glibc meskipun sebenarnya mendukung atribut tersebut
    • Karena header epoll khusus Linux, ada juga argumen bahwa standar portabilitas C tidak mudah diterapkan mentah-mentah di sini
  • limits.h dan #include_next

    • Beberapa header C seperti stddef.h, stdint.h, limits.h, dan float.h dibutuhkan bahkan dalam implementasi freestanding, sehingga harus disediakan oleh kompiler
    • POSIX menuntut limits.h mendefinisikan konstanta khusus POSIX selain konstanta C standar, sehingga dibutuhkan limits.h khusus platform di atas limits.h milik kompiler
    • <limits.h> milik glibc langsung mendefinisikan nilai limits.h ANSI jika bukan GNU C, dan di lingkungan GCC mengambil header kompiler dengan #include_next <limits.h>
    • Struktur ini mengasumsikan bahwa builtin limits.h khusus GCC mendefinisikan makro tertentu, dan juga bergantung pada ekstensi #include_next
    • clang juga harus mengakali struktur ini

Deteksi fitur SDL dan masalah inline assembly

  • Fungsi byteswapping di SDL_endian.h memakai builtin kompiler atau inline assembly jika memungkinkan, dan sebagai jalan terakhir diganti dengan implementasi operasi bit biasa
  • Logika deteksinya kira-kira berjalan dalam urutan berikut
    • Jika GCC atau clang dan ada __has_builtin(__builtin_bswapX), gunakan builtin
    • Jika MSVC 8.0 atau lebih baru, gunakan intrinsic MSVC #pragma
    • Jika makro spesifik ISA seperti __x86_64__ terdefinisi, gunakan inline assembly
    • Selain itu, gunakan implementasi operasi bit biasa
  • Urutan ini menjadi masalah jika kompiler yang bukan GCC atau clang secara masuk akal mendefinisikan predefined macro spesifik ISA
  • Sekalipun kompiler tersebut menyediakan builtin bswap dan operator khusus __has_builtin, secara logika ia bisa tetap mencoba memakai inline assembly gaya GCC
  • Hasilnya, strukturnya menjadi seperti mengharapkan kompiler yang tidak dikenal juga mendukung inline assembly gaya GCC

OpenBSD libc dan kebingungan extern inline

  • Beberapa header OpenBSD memuat definisi fungsi inline yang dapat digunakan kompiler secara opsional saat optimisasi
  • Fungsi-fungsi ini didefinisikan melalui makro __only_inline, dan jika kompiler tidak benar-benar melakukan inline, harus ada pengganti berupa simbol eksternal
  • Artinya, dibutuhkan fungsi inline dengan extern linkage
  • Perbedaan makna inline di C99 dan GCC

    • inline memang ditetapkan di C99, tetapi perilaku standarnya bertabrakan dengan perilaku GCC nonstandar sebelum C99
    • Definisi inline di dalam header harus memakai extern inline bersama badan fungsi, dan dalam kasus ini tidak akan mengeluarkan exported function yang nyata
    • Di translation unit, deklarasi untuk mengekspor definisi fungsi harus hanya memakai inline
    • Makna inline juga berbeda antara C++ dan C
    • Perbedaan ini dibahas rinci dalam tulisan Youtao Guo
  • __only_inline milik OpenBSD

    • OpenBSD bergantung pada semantik inline GCC
    • Untuk menutupi perbedaan versi GCC, makro __only_inline di sys/cdefs.h menetapkan semantik inline gnu89 lama pada GCC modern dengan __attribute__ eksplisit
    • Pada kompiler non-GNU, __only_inline didefinisikan sebagai linkage static
    • Akibatnya, fungsi bisa dideklarasikan dan didefinisikan dengan linkage yang saling bertentangan lalu rusak
  • Solusi _ANSI_LIBRARY

    • OpenBSD menghormati makro _ANSI_LIBRARY
    • Jika makro ini didefinisikan, penggunaan definisi __only_inline yang bermasalah di header standar seperti signal.h diabaikan sepenuhnya
    • Versi yang dioptimalkan memang tidak didapat, tetapi setidaknya proses build tetap berjalan
  • Kode kompatibilitas extern inline milik Gnulib

    • Kode kompatibilitas extern inline di Gnulib juga muncul saat membangun Guile dan nano
    • extern-inline.m4 memuat percabangan kondisi yang rumit untuk menangani implementasi sudut semantik C ini yang aneh dan mudah rusak
    • Kondisinya mencerminkan perbedaan lingkungan seperti Apple, DragonFly, FreeBSD, GCC, clang, PCC, HP cc, PGI, SunPro C, _FORTIFY_SOURCE, __GNUC_STDC_INLINE__, dan __GNUC_GNU_INLINE__

Asumsi clang dalam bionic Android

  • bionic adalah libc milik Android, dan header-nya jauh lebih kuat mengasumsikan clang dibanding GCC
  • Header bionic banyak memakai ekstensi khusus clang seperti _Nonnull dan _Null_unspecified untuk nullability checks
  • Makro seperti ini tidak sulit dihilangkan dengan #define lewat flag baris perintah
  • Masalah ini terlihat saat memakai ponsel Android sebagai lingkungan pengembangan aarch64 native melalui Termux
  • _Null_unspecified juga disebut __BIONIC_COMPLICATED_NULLNESS, dan definisi terkait ada di sys/cdefs.h milik bionic

Pilihan yang dihadapi kompiler C kecil

  • Kode yang hanya mengikuti standar ISO C jarang ditemukan di dunia nyata, dan banyak codebase C bergantung pada perilaku nonstandar serta ekstensi bahasa
  • Ketergantungan ini muncul bukan hanya untuk fitur tambahan, tetapi juga ketika mengatasi bug dan celah yang berbeda-beda pada tiap kompiler dan pustaka
  • Codebase yang ingin mendukung banyak lingkungan bergantung pada pemeriksaan preprocessor dan guard, tetapi pendekatan ini mudah rusak dan sulit dikelola
  • Saat membuat kompiler C seperti antcc, masalah kompatibilitas seperti ini terus berulang
  • Jika banyak proyek open source bergantung pada ekstensi dan perilaku nonstandar spesifik kompiler bahkan untuk hal yang tidak esensial, beban respons untuk kompiler alternatif menjadi makin besar
  • Pada saat yang sama, juga sulit menuntut semua pengembang untuk menguji kode C mereka di banyak kompiler, termasuk kompiler kecil yang kurang dikenal
  • Portabilitas C sendiri sudah cukup sulit
  • Dari sudut pandang penulis kompiler, ada empat pilihan yang mungkin
    • Mencoba mengirim patch inkompatibilitas ke upstream
    • Menjadi cukup terkenal sehingga pengembang menambahkan pemeriksaan #ifdef khusus dan pengujian dasar
    • Menanganinya di downstream, lalu mendistribusikan patch atau patch terpisah
    • Berpura-pura sebagai versi GCC tertentu dan mengimplementasikan ekstensi yang relevan
  • Patch upstream tampak seperti pertarungan yang sulit dimenangkan, dan patch downstream adalah cara termudah
  • Untuk mendukung banyak codebase dengan kebingungan seminimal mungkin bagi pengguna dan pengembang, meniru kompatibilitas GCC terdengar realistis, tetapi beban implementasinya besar
  • clang mendefinisikan __GNUC__=4, __GNUC_MINOR__=2, dan __GNUC_PATCHLEVEL__=1 untuk mengklaim kompatibilitas dengan GCC 4.2.1
  • Kini clang hampir menjadi target dukungan tersendiri, tetapi upaya besar tetap diperlukan sampai kernel Linux bisa dikompilasi dengan clang, termasuk patch di kedua proyek

Makro GCC dan masalah mengejar ketertinggalan

  • Pendekatan berpura-pura sebagai GCC juga punya masalah
  • Banyak codebase hanya memeriksa #ifdef __GNUC__ dan bisa memakai ekstensi GCC baru tanpa memeriksa versinya
  • Dalam kasus seperti ini, kompiler alternatif harus terus mengejar ketertinggalan
  • Ini juga menjadi salah satu alasan mengapa clang, meskipun mendukung ekstensi GNU yang lebih baru daripada 4.2.1, tidak menaikkan nilai makro __GNUC__
  • Latar belakang terkait dibahas dalam diskusi LLVM tentang menaikkan minor version __GNUC__

Arah yang lebih baik dan kondisi saat ini

  • Idealnya, makro pengujian fitur harus dipakai lebih luas daripada guard khusus kompiler dan pemeriksaan versi
  • Makro pengujian fitur yang berguna antara lain __has_builtin, __has_feature, dan __has_attribute
  • Pendekatan seperti makro standar __STDC_NO_VLA__ juga bisa dipakai lebih luas
  • Saat ini di dunia *NIX, baik atau buruk, duopoli semu GCC/clang adalah kondisi dasarnya
  • Pengembangan kompiler C kecil yang independen juga terus berlanjut

1 komentar

 
GN⁺ 17 jam lalu
Komentar Lobste.rs
  • (penulis kompiler kefir) Masalah __attribute__ di <sys/cdefs.h> menurut pengalaman termasuk salah satu yang paling merepotkan. Ini merusak epoll, struktur packed yang umum, constructor, dan visibilitas simbol, sehingga kefir sampai harus dibundel bersama header monkeypatch berikut
    Memang tidak ideal, tetapi mungkin ini cara yang paling realistis, dan dalam praktiknya ini memungkinkan penghapusan sebagian besar patch kustom dari test suite eksternal
    Jenis kegagalan lain adalah kode alternatif yang bug. Beberapa proyek mencoba mendeteksi kompiler agar bisa menyesuaikan perilakunya, tetapi karena pengujian pada kompiler alternatif kurang, kode fallback sering penuh bug atau tidak terawat dengan baik. Dari sudut pandang pembuat kompiler, ini jauh lebih menjengkelkan daripada langsung gagal dengan status “kompiler tidak didukung”. Misalnya, karena harus men-debug sendiri miskompilasi aneh seperti ketidaksesuaian lebar typedef integer antara program dan pustaka yang sudah dikompilasi sebelumnya

    • Hal serupa juga terjadi di terminal. Kalau $TERM tidak disetel ke xterm-256color untuk berpura-pura menjadi xterm, banyak hal jadi rusak
      Saya benar-benar tidak tahu harus menyelesaikannya bagaimana. Pada akhirnya apakah proyek kita memang harus cukup luas dipakai dan cukup terkenal dulu ya. Mudah sekali!
    • Pendekatan header monkeypatch tampaknya juga dipakai oleh slimcc, dan terlihat seperti kompromi yang cukup masuk akal
      Saya rasa saya juga pernah beberapa kali mengalami miskompilasi aneh yang muncul karena fallback pendeteksian kompiler tidak dikelola dengan baik, dan itu memang sangat menyebalkan
  • Saya terutama mengembangkan cproc di linux-musl, jadi saya tidak tahu bahwa glibc menonaktifkan __attribute__ untuk kompiler lain, tetapi ini benar-benar situasi yang cukup buruk. Komentarnya mengatakan penggunaan attribute aman meski diabaikan, tetapi itu tidak mempertimbangkan bahwa sebagian besar kode aplikasi secara tidak langsung mengikutsertakan sys/cdefs.h dan bisa memakai attribute yang tidak boleh diabaikan
    Selain packed, aligned dan constructor juga sering dipakai
    Saya penasaran apakah ini sudah dilaporkan di issue tracker suatu tempat. Sebagian besar penggunaan attribute di dalam cdefs.h tampaknya sudah dilindungi dengan __glibc_has_attribute, jadi saya juga penasaran apa sebenarnya yang dicapai oleh penonaktifan __attribute__ secara menyeluruh, dan apakah itu bisa dihapus
    Fitur yang dipakai header libc tetapi tidak punya cara yang baik bagi kompiler untuk menyatakan dukungannya juga menjadi masalah. Ini fitur yang tidak terekspos lewat cara seperti __has_attribute atau __has_builtin; contoh yang terlintas adalah label __asm__. NetBSD memakainya untuk mengganti nama simbol, dan jika tidak ada __GNUC__ atau __PCC__, ia mengeluarkan #error. Namun saya juga tidak tahu apa yang sebaiknya diusulkan selain membiarkannya saja dicoba lalu gagal bila memang tidak didukung
    Saya juga pernah mengalami masalah terkait __builtin_va_list. Ada libc yang tanpa __GNUC__ mendefinisikan va_list sebagai void *, atau bahkan memberikan definisi yang saling bertentangan. Ini juga tidak bisa diuji dengan __has_builtin. Mungkin __has_builtin(__builtin_va_arg) bisa jadi pengujian yang cukup baik, tetapi saya tidak yakin bagaimana membuat ini diperbaiki di macOS

    • Saya cepat-cepat mencari penggunaan __attribute__ di /usr/include/sys dan /usr/include/bits, dan ada banyak penggunaan yang tidak dilindungi. Kebanyakan berupa __format__, __aligned__, __noreturn__, jadi ini juga perlu diperbaiki
      Secara umum glibc tampaknya tidak memprioritaskan kompatibilitas dengan kompiler non-GCC, jadi saya tidak yakin patch seperti itu akan diterima. Setelah upgrade sistem di awal tahun ini, glibc menambahkan penggunaan __SIZE_TYPE__ tanpa pelindung di header Linux, sehingga kompiler saya tidak bisa mengompilasi beberapa proyek. Saya sudah melaporkannya, tetapi sampai sekarang belum diperbaiki, dan akhirnya saya menambahkan makro pradefinisi bergaya __X_TYPE__ agar sesuai dengan GCC
      Untuk masalah label __asm__, saya juga tidak terpikir solusi yang bagus. Namun jika penggantian nama asm memang 100% diperlukan agar bisa berfungsi, mungkin lebih baik cukup dicoba saja dan dibiarkan gagal ketimbang melakukan pengecekan kompiler
      __builtin_va_list ini cukup serius. Saya sempat mengira __has_builtin(__builtin_va_list) akan berfungsi, tetapi ternyata tidak