近日,Rust和Swift資深專家Aria Beingessner發(fā)布的一篇文章《C 不再是一種編程語言》在Hacker News上引起了熱烈討論。
原文鏈接:
https://gankra.github.io/blah/c-isnt-a-language/
Hacker News評論區(qū):
https://news.ycombinator.com/item?id=30704642
Aria和朋友Phantomderp在“對C ABI接口感到非常失望并試圖修復(fù)上”達(dá)成了高度一致。
但在失望的原因上,Aria與朋友各自持不同意見。那具體產(chǎn)生了哪些分歧呢?為什么會提出C不再是一種編程語言的觀點(diǎn)呢?筆者對原文進(jìn)行了編譯:
整理 | 于軒
出品 | 程序人生 (ID:coder _life)
Phantomderp試圖從原生上改善使用C本身作為編程語言的條件,而Aria則希望改善使用C以外的任何語言條件。
這時候大家就會產(chǎn)生疑問了,這個問題和C有什么關(guān)系?
Aria表示:如果C真的是一種編程語言,那就和它無關(guān)。不幸的是,它并不是。這不是說數(shù)十億種實現(xiàn)方式和失敗的層次結(jié)構(gòu),導(dǎo)致它的定義方式非常糟糕的事實,而是C被提升到一個具有威望和權(quán)力的角色,它的統(tǒng)治是絕對和永恒的。C是編程的通用語言,我們都必須學(xué)C,因此C不再只是一種編程語言,它成了每一種通用編程語言都需要遵守的協(xié)議。
這實際有點(diǎn)像是關(guān)于整個“C是一個不可捉摸的實現(xiàn)定義混亂” 。但僅因為它讓我們不得不使用這個協(xié)議,這就變成了一個更大的噩夢。
外部功能接口
下面一起來談?wù)劶夹g(shù)問題。假如你已經(jīng)完成了你的新語言BAppyscript的設(shè)計,對Bappy Paws/Hooves/Fins有一流的支持。這是一種神奇的語言,將徹底改變cats、sheep、和sharks的編程方式。
但現(xiàn)在需要讓它真正做一些有用的事情。比如接受用戶的輸入,或者輸出,或者字面上的任何可觀察之類的東西。如果你想讓該語言編寫的程序與主流操作系統(tǒng)兼容,那就需要與操作系統(tǒng)的界面進(jìn)行交互。聽說linux上的一切都“只是一個文件”,所以一起在Linux上打開一個文件吧!
OPEN(2)
NAME
open, openat, creat - open and possibly create a file
SYNOPSIS
#include <fcntl.h>
int open(const char *pathname, int flags);
int open(const char *pathname, int flags, mode_t mode);
int creat(const char *pathname, mode_t mode);
int openat(int dirfd, const char *pathname, int flags);
int openat(int dirfd, const char *pathname, int flags, mode_t mode);
/* Documented separately, in openat2(2): */
int openat2(int dirfd, const char *pathname,
const struct open_how *how, size_t size);
Feature Test macro Requirements for glibc (see
feature_test_macros(7)):
openat:
Since glibc 2.10:
_POSIX_C_SOURCE >= 200809L
Before glibc 2.10:
_ATFILE_SOURCE
這是Bappyscript,不是C,那Linux的Bappyscript接口在哪里?
你說Linux中沒有Bappyscript接口是什么意思?好吧,當(dāng)然是因為這是一種全新的語言,但你會添加一個,對嗎?那這時你就會發(fā)現(xiàn),你好像必須使用他們給的東西。
你將需要某種接口,讓語言能夠調(diào)用外部的函數(shù),就像外部函數(shù)接口FFI。然后你發(fā)現(xiàn)Rust也有C FFI,Swift也有,甚至Python也有。
你會發(fā)現(xiàn),每個人都必須學(xué)會C才能與主流的操作系統(tǒng)對話,然后當(dāng)需要相互對話時,大家突然都用起了C。所以…為什么不直接用C來相互對話呢?
現(xiàn)在C就變成了一種編程通用語言,不僅是一種編程語言,它還是一種協(xié)議了。
與C對話包括哪些內(nèi)容?
很明顯,基本上每種語言都必須學(xué)會與C進(jìn)行對話,而且這種語言絕對是非常明確的。
"對話 "C是什么意思?它意味著以C頭文件的形式獲得接口類型和功能的描述,并以某種方式:
-
匹配這些類型的布局
-
用鏈接器做一些事情,將函數(shù)的符號解析為指針
-
用適當(dāng)?shù)腁BI來調(diào)用這些函數(shù)(比如把a(bǔ)rgs放在正確的寄存器中)
那么,這里就有幾個問題:
-
你實際上不能寫一個C解析器
-
C實際上沒有ABI,甚至沒有定義的類型布局
實際上無法解析一個C頭文件
Aria曾斷言解析C基本上是不可能的,但有人說其實有很多工具可以讀取C頭文件,比如rust-bindgen。事實果真如此嗎?其實不然。
bindgen使用libclang來解析C和C++頭文件。要修改bindgen搜索libclang的方式,請參閱clang-sys文檔。關(guān)于bindgen如何使用libclang的更多細(xì)節(jié),請參閱bindgen用戶指南。
任何花費(fèi)大量時間試圖快速解析C(++)頭文件的人都會很快放棄,然后讓一個C(++)編譯器來做這件事。請記住,有意義地解析C頭文件不僅僅是解析:你還需要解決#includes、typedefs和macros的問題!所以現(xiàn)在不僅要實現(xiàn)所有相關(guān)功能,還要實現(xiàn)所有平臺的頭文件解析邏輯,并且還需要想方設(shè)法找到DEFINED!
就拿Swift來說,它在C互操作和資源方面擁有絕對優(yōu)勢,它是由蘋果開發(fā)的一門編程語言,有效取代了Objective-C,成為在其平臺上定義和使用系統(tǒng)API的主要語言。在這樣做的過程中,它比其他任何人都更進(jìn)一步實現(xiàn)了ABI穩(wěn)定性和設(shè)計概念。
它也是Aria見過的最支持FFI的語言之一。它可以本地導(dǎo)入(Objective-)C(++)頭文件,并產(chǎn)生一個漂亮的本地Swift接口,其類型在邊界自動 "橋接 "到它們的Swift對等項(由于類型具有相同的ABI,所以通常是透明的)。
Swift也是由蘋果公司中許多構(gòu)建和維護(hù)Clang和LLVM的人開發(fā)。這些人都是C及其衍生品方面的世界頂級專家。Doug Gregor就是其中之一,他曾表達(dá)了對C FFI的看法:
所有這些都是Swift內(nèi)部使用Clang來處理 C(++) ABI的原因。這樣一來,我們就不會去追著Clang增加的每一個影響ABI的新屬性。
可以看出,即使是Swift也不想花時間解析C(++)頭文件。那么,如果你絕對不想讓C編譯器在編譯時解析和解決頭文件,你該怎么做呢?
你需要手工翻譯!int64_t
? 還是寫i64. long
…?什么是long?
C實際上沒有ABI
好吧,這沒有什么好驚訝的:C語言中的整數(shù)類型,為了 “可移植性”而被設(shè)計成搖擺不定的大小,實際上大小也是不穩(wěn)定的。我們可以認(rèn)為CHAR_BIT很奇怪,但這也不能幫助我們了解long
的大小和對齊方式。
有人說每個平臺都有標(biāo)準(zhǔn)化的調(diào)用約定和ABI,確實有,而且它們通常定義了C中關(guān)鍵原語的布局(并且有些不只是用C類型來定義調(diào)用約定,這里側(cè)眼于AMD64 SysV)。
還有一個棘手的問題:架構(gòu)并沒有定義ABI,操作系統(tǒng)也是。我們必須在一個特定的目標(biāo)三元組上全力以赴,比如 “x86_64-pc-windows-gnu”(不要和 "x86_64-pc-windows-msvc "混淆)。經(jīng)過測試,一共有176個三元組。
> rustc --print target-list
aarch64-apple-darwin
aarch64-apple-IOS
aarch64-apple-ios-macabi
aarch64-apple-ios-sim
aarch64-apple-tvos
...
armv7-unknown-linux-musleabi
armv7-unknown-linux-musleabihf
armv7-unknown-linux-uclibceabihf
...
x86_64-uwp-windows-gnu
x86_64-uwp-windows-msvc
x86_64-wrs-vxworks
>_
這實在是有太多ABI了,因為測試中甚至沒有用到所有不同的調(diào)用約定,如stdcall vs fastcall或aapcs vs aapcs-vfp。
但至少所有這些ABI和調(diào)用約定之類的東西,都可以一種方便使用的機(jī)器可讀格式獲得。至少主流的C編譯器在特定目標(biāo)三元組的ABI上達(dá)成了一致! 當(dāng)然有一些奇怪的jank C編譯器,但Clang和GCC不是:
> abi-checker --tests ui128 --pairs clang_calls_gcc gcc_calls_clang
...
Test ui128::c::clang_calls_gcc::i128_val_in_0_perturbed_small passed
Test ui128::c::clang_calls_gcc::i128_val_in_1_perturbed_small passed
Test ui128::c::clang_calls_gcc::i128_val_in_2_perturbed_small passed
Test ui128::c::clang_calls_gcc::i128_val_in_3_perturbed_small passed
Test ui128::c::clang_calls_gcc::i128_val_in_0_perturbed_big failed!
test 57 arg3 field 0 mismatch
caller: [30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 3A, 3B, 3C, 3D, 3E, 3F]
callee: [38, 39, 3A, 3B, 3C, 3D, 3E, 3F, 40, 41, 42, 43, 44, 45, 46, 47]
Test ui128::c::clang_calls_gcc::i128_val_in_1_perturbed_big failed!
test 58 arg3 field 0 mismatch
caller: [30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 3A, 3B, 3C, 3D, 3E, 3F]
callee: [38, 39, 3A, 3B, 3C, 3D, 3E, 3F, 40, 41, 42, 43, 44, 45, 46, 47]
...
392 passed, 60 failed, 0 completely failed, 8 skipped
上面是Aria在Ubuntu 20.04 x64上運(yùn)行的FFI abi-checker,她在這個相當(dāng)重要的、表現(xiàn)良好的平臺上測試了一些非常無聊的情況。結(jié)果發(fā)現(xiàn),一些整數(shù)參數(shù)在兩個由Clang和GCC編譯的靜態(tài)庫之間按值傳遞失敗了!
Aria發(fā)現(xiàn),Clang和GCC甚至不能就Linux x64上_int128
的ABI達(dá)成一致。
Aria本來是為了檢查rustc中的錯誤,沒想到會在一個重要的、常用的ABI上發(fā)現(xiàn)兩大主流C編譯器的不一致。
試圖馴服C
Aria認(rèn)為,可怕的是對C頭文件進(jìn)行語義解析,只能由該平臺的C編譯器來完成。即使C編譯器告訴了你類型和如何理解注釋,但實際上你仍然不知道所有內(nèi)容的大小/對齊/慣例。那如何與這些亂七八糟的東西進(jìn)行互操作呢?Aria提供了兩種選擇。
第一個選擇是完全投降,將你的語言與C進(jìn)行靈魂綁定,這可以是以下任何一種:
-
用C(++)編寫你的編譯器/運(yùn)行時,這樣它就可以用C了
-
讓你的 "codegen "直接發(fā)出C(++),這樣用戶無論如何都需要一個C編譯器
-
將你的編譯器建立在一個成熟的主要C編譯器(Clang或GCC)之上
但上面這些也只能讓你走這么遠(yuǎn),因為除非你的語言真的暴露了unsigned long long
,否則你將繼承C的巨大可移植性混亂。
這就讓我們想到了第二個選擇:撒謊、欺騙和偷竊。
如果這一切是無論如何都無法避免的災(zāi)難,你還不如開始手工翻譯類型和接口定義到你的語言中,基本上就是我們每天在Rust中所做的事情。比如,人們使用rust-bindgen和friends自動化處理一些事,但很多時候,定義會被檢查或手工調(diào)整。因為人們不想浪費(fèi)時間,去嘗試Phantomderp的定制C構(gòu)建系統(tǒng)可移植地工作。
在Rust中,Linux x64上的intmax_t
是什么?
pub type intmax_t = i64;
在Nim中,Linux x64上的long long
是什么?
clonglong {.importc: "long long", nodecl.} = int64
很多代碼已經(jīng)完全放棄將C保持在循環(huán)中,開始對核心類型的定義進(jìn)行硬編碼。畢竟,它們顯然只是平臺ABI的一部分!他們要改變intmax_t
的大小嗎?這顯然是一個破壞ABI的變化!
那phantomderp正在研究的又是什么?
我們討論過為何
intmax_t
不能被改變,因為如果我們從long long
(64位整數(shù))改為_int128_t
(128位整數(shù)),某個地方的二進(jìn)制會失控使用錯誤的調(diào)用約定/返回約定。但有沒有一種方法,如果代碼選擇了它或其他東西,我們可以為較新的應(yīng)用程序升級函數(shù)調(diào)用,而讓舊應(yīng)用程序保持不變?讓我們編寫一些代碼,測試一下透明別名可以幫助ABI的想法。
Aria提出了她的疑問:編程語言如何處理這種變化?如何指定與哪個版本的 intmax_t
互操作?如果你有一些C頭文件提到intmax_t
,它使用的是哪個定義?
在此討論具有不同ABI的平臺的主要機(jī)制是目標(biāo)三元組。你知道什么是目標(biāo)三元組嗎?你知道基本上涵蓋了過去20年里所有主流桌面/服務(wù)器Linux發(fā)行版的 x86_64-unknown-linux-gnu
包括什么嗎?現(xiàn)在,雖然表面上可以針對這個目標(biāo)進(jìn)行編譯,并得到一個在所有這些平臺上都能“正常工作”的二進(jìn)制文件,但Aria不相信有些程序會被編譯成intmax_t
大于int64_t
。
任何試圖做出這種改變的平臺都會成為一個新的x86_64-unknown-linux-gnu2
目標(biāo)三元組嗎?如果任何針對x86_64-unknown-linux-gnu
編譯的東西都被允許在上面運(yùn)行,這難道還不夠嗎?
在不破壞ABI的情況下更改簽名
"那又怎樣,C永遠(yuǎn)不會再有進(jìn)步嗎?"不!但也是!因為他們提供了糟糕的設(shè)計。
老實說,進(jìn)行ABI兼容的修改是一種藝術(shù)形式。這種藝術(shù)的一部分就是準(zhǔn)備工作。具體來說,如果你準(zhǔn)備好了,做出不破壞ABI的修改就會容易得多。
正如phantomderp的文章所指出的,像glibc( g
是 x86_64-unknown-linux-gnu
中的 gnu
)早就明白了這一點(diǎn),并使用符號版本化這樣的機(jī)制來更新簽名和API,同時為任何針對舊版本編譯的人保留舊版本。
因此,如果你有 int32_t my_rad_symbol(int32_t)
,你告訴編譯器將其導(dǎo)出為 my_rad_symbol_v1
,那么任何根據(jù)這個頭文件進(jìn)行編譯的人,都會在他們的代碼中寫上 my_rad_symbol
,但針對 my_rad_symbol_v1
鏈接。
然后當(dāng)你決定實際上應(yīng)該使用int64_t
時,你可以把int64_t my_rad_symbol(int64_t)
作為my_rad_symbol_v2
,但保留舊的定義作為my_rad_symbol_v1
。任何針對較新版本頭文件進(jìn)行編譯的人都會高興地使用v2符號,而針對舊版本進(jìn)行編譯的人則繼續(xù)使用v1!
但是你仍然有一個兼容性的問題:任何用新頭文件編譯的人都不能與庫的舊版本進(jìn)行鏈接,庫的V1版本根本沒有V2符號!因此,如果你想獲得熱門的新功能,你就要接受與舊系統(tǒng)的不兼容。
不過這并不是什么大問題,它只是讓平臺供應(yīng)商感到難過,因為沒有人能夠立即使用他們花了這么多時間做的東西。你不得不推出一個閃亮的新功能,然后讓大家等待它變得足夠普遍和成熟。但為了人們愿意依賴它并中斷對舊平臺的支持(或者愿意為它實施動態(tài)檢查和回退)時,你必須坐等幾年。
如果你真的想讓人們立即升級,那就要談?wù)撓蚯凹嫒莸膯栴}。這讓舊版本的東西以某種方式與他們沒有概念的新功能一起工作。
在不破壞ABI的情況下更改類型
那除了可以改變一個函數(shù)的簽名,還可以改變類型布局嗎?Aria表示,這取決于你是如何暴露類型的。
C真正奇妙的一個特點(diǎn)是,它可以讓你區(qū)分一個已知布局的類型和一個未知布局的類型。如果你只在C頭文件中前向聲明一個類型,那么任何與之交互的用戶代碼都不被“允許”知道該類型的布局,并且必須一直在指針后面不透明地處理它。
所以你可以做一個像MyRadType* make_val
和use_val(MyRadType*)
的API,然后使用同樣的符號版本技巧來暴露make_val_v1
和 use_val_v1
符號,任何時候你想改變這個布局,你就在所有與該類型交互的東西上增加版本。類似地,你在MyRadTypeV1
、MyRadTypeV2
和一些類型定義中保留了一些,以確保人們使用“正確”的類型。這樣就可以在不同的版本之間改變類型的布局。
如果多個東西建立在你的庫之上,然后開始用不透明類型相互交談,壞事就會發(fā)生:
-
lib1: 制作一個API,接受
MyRadType*
并調(diào)用use_val
-
lib2:調(diào)用
make_val
并將結(jié)果傳遞給lib1
如果lib1和lib2針對庫的不同版本進(jìn)行了編譯,那么make_val_v1
就會被輸入到use_val_v2
中!你有兩個選擇來處理這個問題:
1.說這是被禁止的,責(zé)備那些無論如何都要這么做的人,然后傷心
2.以一種向前兼容的方式設(shè)計MyRadType
,這樣混合就可以了
常見的前向兼容技巧包括:
-
保留未使用的字段供未來版本使用
-
MyRadType的所有版本都有一個共同的前綴,可以讓你“檢查”你所使用的版本
-
擁有自定大小的字段,以便舊版本可以“跳過”新的部分
案例研究:MINIDUMP_HANDLE_DATA
微軟是這種向前兼容的大師,甚至可以實現(xiàn)在架構(gòu)之間保持布局兼容。Aria最近正在處理的一個例子是Minidumpapiset.h
中的
MINIDUMP_HANDLE_DATA_STREAM。
這個API描述了一個有版本的值列表。該列表以這種類型開始:
typedef struct _MINIDUMP_HANDLE_DATA_STREAM {
ULONG32 SizeOfHeader;
ULONG32 SizeOfDescriptor;
ULONG32 NumberOfDescriptors;
ULONG32 Reserved;
} MINIDUMP_HANDLE_DATA_STREAM, *PMINIDUMP_HANDLE_DATA_STREAM;
其中:
-
SizeOfHeader
是
MINIDUMP_HANDLE_DATA_STREAM本身的大小。如果他們需要在最后增加更多的字段,那也沒關(guān)系,因為舊版本可以使用這個值來檢測頭的“版本”,也可以跳過任何他們不知道的字段。 -
SizeOfDescriptor
是數(shù)組中每個元素的大小。這讓你知道你有什么 "版本 "的元素,并跳過任何你不知道的字段。 -
NumberOfDescriptors
是數(shù)組長度 -
Reserved
是一些額外的內(nèi)存,無論如何他們決定保留在頭文件中(Minidumpapiset.h非常謹(jǐn)慎,從不在任何地方進(jìn)行填充,因為填充字節(jié)有未指定的值,而且它是一種序列化的二進(jìn)制文件格式。我希望他們添加這個字段是為了使結(jié)構(gòu)的大小是8的倍數(shù),這樣就不會有任何關(guān)于數(shù)組元素在標(biāo)題之后是否需要填充的問題。這是在認(rèn)真對待兼容性!)
而事實上,微軟實際上有理由使用這種版本方案,并定義了兩個版本的數(shù)組元素:
typedef struct _MINIDUMP_HANDLE_DESCRIPTOR {
ULONG64 Handle;
RVA TypeNameRva;
RVA ObjectNameRva;
ULONG32 Attributes;
ULONG32 GrantedAccess;
ULONG32 HandleCount;
ULONG32 PointerCount;
} MINIDUMP_HANDLE_DESCRIPTOR, *PMINIDUMP_HANDLE_DESCRIPTOR;
typedef struct _MINIDUMP_HANDLE_DESCRIPTOR_2 {
ULONG64 Handle;
RVA TypeNameRva;
RVA ObjectNameRva;
ULONG32 Attributes;
ULONG32 GrantedAccess;
ULONG32 HandleCount;
ULONG32 PointerCount;
RVA ObjectInfoRva;
ULONG32 Reserved0;
} MINIDUMP_HANDLE_DESCRIPTOR_2, *PMINIDUMP_HANDLE_DESCRIPTOR_2;
// The latest MINIDUMP_HANDLE_DESCRIPTOR definition.
typedef MINIDUMP_HANDLE_DESCRIPTOR_2 MINIDUMP_HANDLE_DESCRIPTOR_N;
typedef MINIDUMP_HANDLE_DESCRIPTOR_N *PMINIDUMP_HANDLE_DESCRIPTOR_N;
這些結(jié)構(gòu)的實際細(xì)節(jié)不是很有趣,除了:
-
他們只是通過在末尾添加字段來改變它
-
有一個“最新版本”的類型定義
-
保留了一些也許再次Padding(填充)(RVA是一個ULONG32)
這是一個堅不可摧的向前兼容的龐然大物。它們對填充非常小心,它甚至在32位和64位之間有相同的布局 (這實際上是非常重要的,因為你希望一個架構(gòu)上的minidump處理器能夠處理來自每個架構(gòu)的minidump)。
案例研究:jmp_buf
Aria對這種情況不是很熟悉,但在研究歷史上的glibc中斷時,她在LWN上看到了一篇很棒的文章:《glibc s390 ABI中斷》,她假設(shè)它是準(zhǔn)確的。
事實證明,glibc曾經(jīng)破解過類型的ABI,至少在s390上。根據(jù)這篇文章的描述,它是混亂的。
特別是他們改變了setjmp/longjmp使用的保存狀態(tài)類型的布局,即jmp_buf
。現(xiàn)在,他們知道這是一個破壞ABI的變化,所以他們做了負(fù)責(zé)任的符號版本化的事情。
但jmp_buf
并不是一個不透明的類型,其他東西都在內(nèi)聯(lián)地存儲這個類型的實例,比如Perl的運(yùn)行時間。不用說,這個相對晦澀的類型已經(jīng)滲透到許多二進(jìn)制文件中去了,最終的結(jié)論是,Debian的所有東西都需要重新編譯!
這篇文章甚至討論了將libc版本升級以應(yīng)對這種情況的可能性:
在像debian這樣的混合ABI環(huán)境中,SO名稱碰撞導(dǎo)致兩個libc被加載并爭奪相同的符號命名空間,而解析(以及因此選擇ABI)則由ELF插值和范圍規(guī)則決定。這真是一場噩夢。這可能是一個比告訴大家重建并繼續(xù)生活更糟糕的解決方案。
真的能改變intmax_t嗎?
在Aria看來,不完全是。就像jmp_buf
一樣,它不是一個不透明的類型,這意味著它被內(nèi)聯(lián)到大量的隨機(jī)結(jié)構(gòu)中,被認(rèn)為具有大量其他語言和編譯器的特定表示,并且可能是大量公共接口的一部分。而這些接口并不在libc、Linux,甚至不在發(fā)行版維護(hù)者的控制之下。
當(dāng)然,libc可以適當(dāng)?shù)厥褂梅柊姹炯记蓙硎蛊銩PI與新的定義兼容,但改變像 intmax_t
這樣的基本數(shù)據(jù)類型的大小,是在一個平臺的大生態(tài)系統(tǒng)中尋求混亂。
Aria希望被證明自己是錯誤的,但據(jù)她所知,做出這樣的改變需要一個新的目標(biāo)三元組,并且不允許任何為舊ABI構(gòu)建的二進(jìn)制/庫在這個新三元組上運(yùn)行。當(dāng)然有人可以做這些工作,但Aria并不羨慕任何這樣做的發(fā)行版。
即使如此,面臨的還有x64的int問題:這是一個非常基本的類型,而且長期以來一直是這種大小,無數(shù)的應(yīng)用程序可能對它有奇怪的無法察覺的假設(shè)。這就是為什么int在x64上是32位的,盡管它應(yīng)該是64位的:int是32位的時間太長了,以至于完全無望將軟件更新到新的大小,盡管它是一個全新的架構(gòu)和目標(biāo)三元組。
Aria再次希望自己是錯的,但是人們有時犯的錯誤如此嚴(yán)重,以至于根本無法挽回。如果C語言是一種獨(dú)立的編程語言?當(dāng)然可以去做。但它不是,它是一個協(xié)議,還是我們必須使用的糟糕的協(xié)議。
就算C征服了世界,但也許它再也得不到好東西了。