- Mã chỉ tuân theo tiêu chuẩn ISO C là rất hiếm; trong thực tế, các codebase C thường phụ thuộc vào những phần mở rộng phi tiêu chuẩn để bổ sung tính năng và lách các khoảng trống khác nhau giữa trình biên dịch và thư viện
- Một trình biên dịch C hữu dụng phải xử lý được cả các system header như
<stdio.h>, nhưng glibc tạo ra rào cản với các phần mở rộng GNU và những giả định như __attribute__((packed)), #include_next
- Logic byteswapping của SDL có thể chọn inline assembly nếu có macro ISA, nên ngay cả các trình biên dịch không phải GCC·clang cũng có thể bị yêu cầu hỗ trợ các phần mở rộng kiểu GCC
- Cách OpenBSD và Gnulib xử lý
extern inline khiến tính tương thích của ngữ nghĩa inline trở nên phức tạp do khác biệt giữa C99 và GCC, các nhánh theo nền tảng và điều kiện _FORTIFY_SOURCE
- Các trình biên dịch C nhỏ phải chọn giữa patch upstream, patch downstream, giành được các guard riêng, hoặc giả lập tương thích GCC; việc mở rộng các macro kiểm tra tính năng có vẻ là hướng đi tốt hơn
Rào cản đầu tiên do header của glibc tạo ra
- Để trở thành một trình biên dịch C hữu dụng, nó phải có thể tiền xử lý và phân tích cú pháp các header của thư viện C hệ thống; nếu không xử lý được
<stdio.h> thì ngay cả hello world cũng khó mà biên dịch qua
- Trong môi trường GNU/Linux, rào cản đó dẫn thẳng tới glibc
- glibc xác định các phần mở rộng được hỗ trợ bằng cách kiểm tra các macro được trình biên dịch định nghĩa sẵn trong sys/cdefs.h, tệp được hầu như mọi header libc gián tiếp include
- Với các phần mở rộng không được hỗ trợ, nó xử lý bằng cách loại bỏ các định nghĩa liên quan, nhưng ngay cả logic tương thích này trên thực tế cũng có thể bị lỗi
-
struct epoll_event và __attribute__((packed))
struct epoll_event trong sys/epoll.h của Linux là một packed struct dùng GNU __attribute__((packed))
- Thuộc tính này làm thay đổi layout của struct trên 64-bit, nên nếu bỏ qua thì ABI sẽ bị hỏng
- Chỉ triển khai
__attribute__((packed)) trong trình biên dịch thôi vẫn chưa đủ
- Trong
sys/cdefs.h có đoạn mã định nghĩa __attribute__(xyz) thành macro rỗng nếu trình biên dịch không phải GCC, clang hoặc tcc
- Kết quả là các trình biên dịch khác, dù có hỗ trợ thuộc tính packed, vẫn có thể bị header glibc loại bỏ thuộc tính đó
- Cũng có thể phản biện rằng header
epoll là dành riêng cho Linux nên khó áp nguyên xi tiêu chí tính di động theo chuẩn C
-
limits.h và #include_next
- Một số header C như
stddef.h, stdint.h, limits.h, float.h là cần thiết cả với các triển khai freestanding nên trình biên dịch phải tự cung cấp
- POSIX yêu cầu
limits.h phải định nghĩa cả các hằng số dành riêng cho POSIX ngoài các hằng số chuẩn C, nên cần có limits.h theo nền tảng đặt chồng lên limits.h của trình biên dịch
<limits.h> của glibc sẽ tự định nghĩa trực tiếp các giá trị limits.h ANSI nếu không phải GNU C, còn trong môi trường GCC thì nó dùng #include_next <limits.h> để lấy header của trình biên dịch
- Cấu trúc này giả định rằng
limits.h builtin dành riêng cho GCC sẽ định nghĩa một số macro nhất định, đồng thời phụ thuộc vào phần mở rộng #include_next
- clang cũng phải lách cấu trúc này
Phát hiện tính năng của SDL và vấn đề inline assembly
- Các hàm byteswapping trong
SDL_endian.h dùng compiler builtin hoặc inline assembly khi có thể, và đến bước cuối mới thay bằng triển khai phép toán bit thông thường
- Logic phát hiện hoạt động theo thứ tự đại khái như sau
- Nếu là GCC hoặc clang và có
__has_builtin(__builtin_bswapX) thì dùng builtin
- Nếu là MSVC 8.0 trở lên thì dùng intrinsic
#pragma của MSVC
- Nếu các macro theo ISA như
__x86_64__ được định nghĩa thì dùng inline assembly
- Nếu không thì dùng triển khai phép toán bit thông thường
- Nếu một trình biên dịch không phải GCC hay clang định nghĩa các predefined macro theo ISA vì lý do hợp lý, thứ tự này sẽ gây vấn đề
- Ngay cả khi trình biên dịch đó có cung cấp builtin bswap và toán tử đặc biệt
__has_builtin, logic vẫn có thể cố dùng inline assembly kiểu GCC
- Kết quả là cấu trúc này ngầm kỳ vọng cả trình biên dịch không xác định cũng hỗ trợ inline assembly theo phong cách GCC
OpenBSD libc và sự rối rắm của extern inline
- Một số header của OpenBSD chứa các định nghĩa hàm inline để trình biên dịch tùy chọn sử dụng khi tối ưu hóa
- Các hàm này được định nghĩa bằng macro
__only_inline, và nếu trình biên dịch thực sự không inline chúng thì phải thay bằng symbol bên ngoài
- Tức là cần các hàm inline có extern linkage
-
Khác biệt ngữ nghĩa inline giữa C99 và GCC
inline được quy định trong C99, nhưng hành vi chuẩn lại xung đột với hành vi GCC phi tiêu chuẩn từ trước C99
- Với định nghĩa inline trong header, phải dùng
extern inline cùng thân hàm, và trong trường hợp đó nó sẽ không phát sinh hàm exported thực sự
- Trong translation unit, để export định nghĩa hàm thì phải khai báo chỉ với
inline
- Ý nghĩa của
inline cũng khác nhau giữa C++ và C
- Sự khác biệt này được trình bày chi tiết trong bài viết của Youtao Guo
-
__only_inline của OpenBSD
- OpenBSD phụ thuộc vào GCC inline semantics
- Để che đi khác biệt giữa các phiên bản GCC, macro
__only_inline trong sys/cdefs.h dùng __attribute__ tường minh trên GCC mới để chỉ định ngữ nghĩa inline gnu89 cũ
- Với các trình biên dịch không phải GNU,
__only_inline được định nghĩa thành linkage static
- Kết quả là hàm có thể bị khai báo và định nghĩa với linkage xung đột, dẫn tới hỏng biên dịch
-
Cách lách bằng _ANSI_LIBRARY
- OpenBSD tôn trọng macro
_ANSI_LIBRARY
- Nếu định nghĩa macro này, nó sẽ bỏ qua hoàn toàn việc dùng các định nghĩa
__only_inline dễ gây hỏng trong các header chuẩn như signal.h
- Dù không có được phiên bản tối ưu, ít nhất việc build vẫn chạy được
-
Mã tương thích extern inline của Gnulib
- Mã tương thích
extern inline của Gnulib cũng xuất hiện khi build Guile và nano
- extern-inline.m4 chứa các nhánh điều kiện phức tạp để xử lý những cách triển khai hỏng và kỳ quặc của góc cạnh này trong C
- Các điều kiện phản ánh khác biệt giữa Apple, DragonFly, FreeBSD, GCC, clang, PCC, HP cc, PGI, SunPro C,
_FORTIFY_SOURCE, __GNUC_STDC_INLINE__, __GNUC_GNU_INLINE__
Giả định về clang trong bionic của Android
- bionic là libc của Android, và các header của nó giả định clang mạnh hơn cả GCC
- Header của bionic dùng nhiều phần mở rộng dành riêng cho clang như
_Nonnull, _Null_unspecified để phục vụ nullability checks
- Những macro như vậy không khó để vô hiệu hóa bằng
#define trên dòng lệnh
- Vấn đề này lộ ra khi dùng Termux để biến điện thoại Android thành môi trường phát triển aarch64 native
_Null_unspecified còn được gọi là __BIONIC_COMPLICATED_NULLNESS, và định nghĩa liên quan nằm trong sys/cdefs.h của bionic
Những lựa chọn mà một trình biên dịch C nhỏ phải đối mặt
- Mã chỉ tuân theo tiêu chuẩn ISO C là điều hiếm gặp trong thực tế, và nhiều codebase C phụ thuộc vào hành vi phi tiêu chuẩn cùng các phần mở rộng ngôn ngữ
- Sự phụ thuộc này không chỉ đến từ việc thêm tính năng mà còn từ quá trình lách bug và khoảng trống khác nhau giữa từng trình biên dịch và thư viện
- Các codebase muốn hỗ trợ nhiều môi trường thường dựa vào kiểm tra của preprocessor và các guard, nhưng cách này rất dễ gãy và khó xử lý
- Khi viết một trình biên dịch C như antcc, các vấn đề tương thích như vậy liên tục lộ ra
- Khi nhiều dự án mã nguồn mở phụ thuộc vào các phần mở rộng và hành vi phi tiêu chuẩn theo từng trình biên dịch ngay cả cho những việc không thật sự bắt buộc, gánh nặng thích ứng cho các trình biên dịch thay thế sẽ tăng mạnh
- Đồng thời cũng khó có thể yêu cầu mọi lập trình viên phải kiểm thử mã C của họ trên nhiều trình biên dịch, bao gồm cả những trình biên dịch nhỏ và ít tên tuổi
- Bản thân tính di động của C vốn đã đủ khó rồi
- Từ góc nhìn của người viết trình biên dịch, có bốn lựa chọn khả dĩ
- Cố gắng gửi patch lên upstream để sửa các điểm không tương thích
- Trở nên đủ nổi tiếng để các nhà phát triển thêm kiểm tra
#ifdef chuyên biệt và các bài test cơ bản
- Xử lý ở downstream, rồi phát hành patch hoặc patch riêng
- Giả làm một phiên bản GCC cụ thể và triển khai các phần mở rộng tương ứng
- Patch upstream có vẻ như là một cuộc chiến khó thắng, còn patch downstream là cách dễ nhất
- Nếu muốn hỗ trợ nhiều codebase cho người dùng và nhà phát triển với mức rối loạn tối thiểu, giả lập tương thích GCC là phương án thực tế nhưng chi phí triển khai rất lớn
- clang định nghĩa
__GNUC__=4, __GNUC_MINOR__=2, __GNUC_PATCHLEVEL__=1 để tuyên bố tương thích với GCC 4.2.1
- Hiện nay clang gần như là một mục tiêu hỗ trợ riêng, nhưng đã từng cần tới nỗ lực lớn, bao gồm cả patch cho hai phía dự án, chỉ để làm cho Linux kernel biên dịch được bằng clang
Macro GCC và bài toán chạy theo
- Cách giả làm GCC cũng có vấn đề riêng
- Nhiều codebase chỉ kiểm tra
#ifdef __GNUC__ mà không kiểm tra phiên bản, rồi dùng luôn các phần mở rộng GNU mới
- Trong trường hợp đó, trình biên dịch thay thế sẽ phải liên tục chạy theo
- Đây cũng là một trong các lý do clang hỗ trợ những phần mở rộng GNU mới hơn 4.2.1 nhưng vẫn không tăng giá trị của macro
__GNUC__
- Bối cảnh liên quan có trong thảo luận của LLVM về việc tăng minor version của
__GNUC__ trong clang
Hướng đi tốt hơn và tình trạng hiện tại
- Lý tưởng nhất là macro kiểm tra tính năng nên được dùng rộng rãi hơn thay vì guard theo từng trình biên dịch và kiểm tra phiên bản
- Các macro kiểm tra tính năng hữu ích gồm
__has_builtin, __has_feature, __has_attribute
- Những macro chuẩn như
__STDC_NO_VLA__ cũng có thể được dùng nhiều hơn
- Trong thế giới *NIX hiện nay, dù tốt hay xấu, thế song mã gần như độc quyền của GCC/clang là trạng thái mặc định
- Việc phát triển các trình biên dịch C nhỏ, độc lập vẫn đang tiếp diễn
1 bình luận
Ý kiến trên Lobste.rs
(tác giả trình biên dịch kefir) Vấn đề
__attribute__trong<sys/cdefs.h>theo kinh nghiệm của tôi thuộc hàng đau đầu nhất. Nó làm hỏng epoll, các cấu trúc packed thông dụng, constructor và khả năng hiển thị symbol, nên tôi đã phải kèm header monkeypatch này vào kefirKhông lý tưởng, nhưng có lẽ là cách thực tế nhất, và trên thực tế đã cho phép loại bỏ phần lớn các bản vá tùy biến trong test suite bên ngoài
Một kiểu thất bại khác là mã thay thế bị lỗi. Một số dự án cố phát hiện trình biên dịch để điều chỉnh hành vi, nhưng vì thiếu kiểm thử trên trình biên dịch thay thế nên mã fallback đầy lỗi hoặc không được bảo trì tử tế. Từ góc nhìn của người làm trình biên dịch, chuyện này còn khó chịu hơn nhiều so với việc thất bại ngay với thông báo “trình biên dịch không được hỗ trợ”. Ví dụ, bạn phải tự debug những lỗi biên dịch sai kỳ quặc như độ rộng
typedefsố nguyên không khớp giữa chương trình và thư viện biên dịch sẵn$TERMthànhxterm-256colorđể giả làm xterm thì đủ thứ sẽ hỏngTôi thật sự không biết phải giải quyết thế nào. Chẳng lẽ cuối cùng chỉ còn cách để dự án của chúng ta phổ biến và nổi tiếng đủ mức. Dễ quá nhỉ!
Tôi cũng có lẽ đã vài lần gặp phải những lỗi biên dịch sai kỳ quặc do fallback phát hiện trình biên dịch không được quản lý tử tế, và nó thực sự rất phiền
Tôi chủ yếu phát triển cproc trên linux-musl nên không biết glibc vô hiệu hóa
__attribute__trên các trình biên dịch khác, nhưng thực sự đây là tình huống khá tệ. Chú thích có nói rằng việc dùng attribute có thể bị bỏ qua mà vẫn ổn, nhưng không tính đến việc phần lớn mã ứng dụng gián tiếp includesys/cdefs.hvà có thể dùng những attribute không thể bỏ quaNgoài
packed,alignedvàconstructorcũng được dùng rất phổ biếnTôi tự hỏi chuyện này có được báo trên issue tracker nào chưa. Có vẻ phần lớn việc dùng attribute trong cdefs.h đã được bảo vệ bằng
__glibc_has_attribute, nên tôi cũng thắc mắc việc vô hiệu hóa__attribute__trên diện rộng thực sự đạt được gì, và liệu có thể bỏ nó đi khôngNhững tính năng mà header libc dùng nhưng trình biên dịch không có cách tốt để biểu thị hỗ trợ hay không cũng là vấn đề. Đây là các tính năng không lộ ra theo kiểu
__has_attributehay__has_builtin; ví dụ tôi nghĩ đến là nhãn__asm__. NetBSD dùng nó để đổi tên symbol, và nếu không có__GNUC__hay__PCC__thì sẽ#error. Tuy vậy, ngoài việc cứ để nó thử rồi thất bại nếu không hỗ trợ, tôi cũng không biết nên đề xuất gì hơnTôi cũng đã gặp vấn đề liên quan đến
__builtin_va_list. Có những libc khi không có__GNUC__thì định nghĩava_listlàvoid *, hoặc thậm chí có các định nghĩa xung đột. Chuyện này cũng không thể kiểm tra bằng__has_builtin. Có thể__has_builtin(__builtin_va_arg)là một phép kiểm đủ tốt, nhưng tôi không rõ làm sao khiến phía macOS sửa điều này__attribute__trong/usr/include/sysvà/usr/include/bits, và thấy có nhiều chỗ dùng không được bảo vệ. Chủ yếu là__format__,__aligned__,__noreturn__, nên các chỗ đó cũng cần được sửaNhìn chung glibc có vẻ không ưu tiên khả năng tương thích với các trình biên dịch không phải GCC, nên tôi không chắc họ có chấp nhận những bản vá như vậy không. Đầu năm nay sau khi nâng cấp hệ thống, glibc đã thêm cách dùng
__SIZE_TYPE__không được bảo vệ trong header Linux, khiến trình biên dịch của tôi không thể biên dịch một số dự án. Tôi đã báo lỗi nhưng đến giờ vẫn chưa được sửa, và cuối cùng tôi phải thêm các macro định nghĩa sẵn kiểu__X_TYPE__để khớp với GCCVới vấn đề nhãn
__asm__, tôi cũng chưa nghĩ ra lời giải nào tốt. Tuy vậy, nếu việc đổi tên bằng asm thật sự cần thiết 100% cho hoạt động, thì có lẽ cứ để nó thử và thất bại sẽ tốt hơn là kiểm tra trình biên dịch__builtin_va_listthì khá nghiêm trọng. Tôi đã nghĩ__has_builtin(__builtin_va_list)sẽ hoạt động, nhưng hình như không phải vậy