你每天用 Python、Ruby、PHP、Lua 写代码,但你可能不知道——它们最早的"第一行实现",几乎都是用 C 语言写的。
C 语言不只是一门编程语言,它更像整个软件世界的"地基"。过去五十多年里,无数新语言选择用 C 来写下自己的第一个版本。这篇文章就按时间顺序,把这些语言一个个讲清楚:它们哪年诞生、由谁创造、最早为什么要用 C 实现,以及它们和 C 到底是什么关系。
先看一张总表
| | | | |
|---|
| | | 第一个编译器用 PDP-11 汇编写,1973 年自举 | |
| | | | |
| | Aho、Weinberger、Kernighan(贝尔实验室) | | |
| | | | |
| | | | |
| | | Cfront 把 C++ 翻译成 C(Cfront 自身用 C++ 自举) | |
| | | | |
| | | | |
| | | | |
| | | | |
| | Ross Ihaka、Robert Gentleman(奥克兰大学) | | |
| | Ierusalimschy、de Figueiredo、Celes(巴西 PUC-Rio) | | |
| | | | |
| | | | |
| | | | |
| | Griesemer、Pike、Thompson(Google) | | |
下面我们一个个细说。
一、故事要从 C 自己说起(1972)
要讲"哪些语言用 C 实现",得先讲 C 自己是怎么来的——因为这里有个经典的"先有鸡还是先有蛋"问题。
1970 年代初,贝尔实验室的 Ken Thompson 为了玩他自己写的游戏《星际旅行》(Space Travel),在一台 PDP-7 小型机上搞出了 Unix 的雏形,以及一门叫B的语言。B 语言继承自更早的 BCPL,但它是无类型的,在 PDP-11 上新的字节寻址能力面前显得力不从心。
1972 年,Dennis Ritchie 在 B 的基础上做了关键改进:加入了类型系统,这门新语言被命名为C。
重点来了:第一个 C 编译器并不是用 C 写的。Ritchie 最初用 PDP-11 汇编语言写了一个 C 编译器。但到了 1973 年,C 语言已经足够成熟,Ritchie 用 C 重写了 C 编译器本身——这就是所谓的"自举"(bootstrapping)。同年,Unix 内核用 C 进行了大规模重写。
这件事意义深远:
- C 证明了一门高级语言可以既强大又高效,足以用来写操作系统;
- C 编译器可以用 C 自己来写,意味着只要在新机器上实现一个 C 编译器,所有 C 代码就都能跑;
- 于是 C 成了一把"通用钥匙",后来所有想做新语言的人,第一反应都是:先用 C 把它写出来。
这就是后面所有故事的源头。
二、贝尔实验室的"小语言"传统:sed 与 awk
C 在贝尔实验室生根发芽后,实验室的大神们很快养成了一个习惯:用 C 写各种专注于单一任务的"小语言"(little languages)。其中影响最大的两个,就是 sed 和 awk。
sed(1974)
sed是"stream editor"(流编辑器)的缩写,由 Lee E. McMahon 在 1973—1974 年间开发。它的思想来自交互式编辑器 ed,但 sed 是非交互式的:你给它一串编辑命令,它就逐行处理文本流,非常适合用在管道里。
最早的 sed 随 1979 年的 Version 7 Unix 广泛传播,整个程序只有大约1600 行 C 代码。别看它小,sed 有自己的变量、循环、条件跳转和标签,是一门图灵完备的小语言。后来的 Perl 从 sed 这里借鉴了大量正则和文本处理思想。
awk(1977)
如果说 sed 擅长"编辑",那awk擅长的就是"提取和报告"。它的名字来自三位作者姓氏首字母:Alfred Aho、PeterWeinberger、BrianKernighan——这三位都是贝尔实验室的传奇人物(Kernighan 后来还和 Ritchie 合写了那本著名的《C 程序设计语言》)。
awk 同样用 C 写成,随 Version 7 Unix 发布。它把文本自动按列切分成字段,用类似 C 的语法写规则,是有史以来第一门把"正则表达式""关联数组"这些特性带给大众的语言。后来的 Perl、Python、Ruby 身上都有 awk 的影子。
和 C 的关系:sed 和 awk 都是用 C 实现的领域特定语言(DSL),语法也明显模仿 C。它们证明了"用 C 写一个解释器来跑一种新语言"是可行且高效的路子。
三、C shell:名字就叫"C"的 shell(1978)
1978 年,加州大学伯克利分校的研究生Bill Joy(后来 Sun 公司的联合创始人,vi 编辑器也是他写的)在 BSD Unix 上写出了csh。
csh 之所以叫"C shell",有两层含义:一是它的语法(判断、循环、表达式)刻意模仿 C 语言,让 C 程序员用起来顺手;二是它本身就是用 C 写的。csh 引入了命令历史、别名、作业控制等今天看来理所当然的功能,后来的 tcsh 就是它的增强版。
和 C 的关系:用 C 实现,语法也向 C 看齐。它是 Unix shell 家族里和 C 绑定最深的一个。
四、给 C 加上面向对象:Objective-C(1984)
1980 年代初,Brad Cox 和 Tom Love 在 ITT 公司接触到了 Smalltalk,被它的面向对象思想震撼。但 Smalltalk 太慢、太封闭,而他们日常用的是 C 和 Unix。于是 Cox 想:能不能把 Smalltalk 的消息传递机制,安到 C 身上?
1981 年前后,Cox 写出了一个叫OOPC(Object-Oriented Pre-Compiler)的预编译器,它把带有 Smalltalk 式方括号语法的代码翻译成标准 C 代码,再交给已有的 C 编译器编译。1984 年,这门语言以Objective-C之名正式发布。
Objective-C 是 C 的严格超集——任何合法的 C 程序都是合法的 Objective-C 程序。它后来被 Steve Jobs 的 NeXT 公司采用,再随 NeXT 技术回到苹果,成为 macOS 和 iOS 二十多年的主力开发语言,直到 Swift 出现。
和 C 的关系:Objective-C 就是 C 加上面向对象层。最早的实现是一个"输出 C 代码"的预编译器,等于把 C 编译器当成了自己的"后端"。
五、C++:和 C 血缘最近的语言(1985)
说到 C 的"后代",C++ 是绕不开的。
1979 年,Bjarne Stroustrup 在贝尔实验室开始做一个叫"C with Classes"的项目,初衷是把 Simula 的类和面向对象特性带到 C 里。1983 年语言正式更名为C++(名字里的"++"就是 C 的自增运算符)。
C++ 的第一个编译器叫Cfront,1985 年发布。这里有一个需要特别说明的细节:
- Cfront 的工作方式是把 C++ 代码翻译成纯 C 代码,然后用平台上现成的 C 编译器编译——这和 Objective-C 的思路如出一辙,都是"蹭"C 编译器的可移植性;
- 但 Cfront 本身是用 C++ 写的(准确说是用"C with Classes"写的),所以它是一个自举编译器,而不是用 C 实现的。
所以严格来说,C++ 不属于"最早用 C 实现"的语言,但它和 C 的关系是所有语言里最深的:它是 C 的超集,它的第一个编译器把 C 当作输出目标,它的语法、类型、内存模型全部建立在 C 之上。把它请出这篇文章是不可能的,所以特别放在这里说明。
和 C 的关系:C 的直接继承者。Cfront 输出 C 代码,但 Cfront 自身用 C++ 自举——这是一个有趣的边界情况。
六、脚本语言大爆发:Perl、Tcl、Bash
1980 年代末,随着 Unix 普及,大家需要比 shell 更强大、又比 C 更灵活的"胶水语言"。一批用 C 实现的脚本语言集中诞生了。
Perl(1987)
Larry Wall是个语言学家兼程序员,他在 1987 年 12 月 18 日把 Perl 1.0 发布到了 Usenet 新闻组。Perl 融合了 C、sed、awk、shell 的特性,正则表达式和文本处理能力极强,口号是"做一件事不止一种方法"(There's more than one way to do it)。
Perl 的解释器完全用 C 写成(核心大约 15 万行 C 代码),这让它在当年的各种 Unix 平台上都能轻松编译运行。整个 1990 年代到 2000 年代初,Perl 是 Web CGI 编程和系统管理的王者。
Tcl(1988)
John Ousterhout在伯克利做集成电路 CAD 工具时发现:每个工具都在发明自己一套乱七八糟的命令语言。他决定做一门统一的、可嵌入的命令语言,这就是Tcl(Tool Command Language),1988 年春天诞生。
Tcl 解释器用 C 写成,以库的形式提供,任何 C 程序都能把 Tcl 嵌进去当脚本层。后来 Ousterhout 又给它配了 GUI 工具包Tk,Tcl/Tk 组合一度成为跨平台 GUI 开发的热门选择。今天 Python 自带的 tkinter,本质上就是 Tk 的 Python 封装。
Bash(1989)
GNU 项目需要一个自由的 Unix shell,Brian Fox在 1989 年写出了Bash(Bourne Again SHell,双关"重生")。Bash 完全用 C 实现,兼容 sh,又加入了命令补全、历史、数组、作业控制等大量功能,如今是绝大多数 Linux 发行版和 macOS 的默认 shell。
和 C 的关系:这三位都是"C 实现的解释器"的典范。它们用 C 保证性能和可移植性,又在 C 之上提供了更高层的抽象——这成了后来几乎所有脚本语言的标准范式。
七、互联网时代的"四大解释器":Python、Ruby、PHP、Lua
1990 年代,互联网和万维网爆发,用 C 写解释器、再用高级语言写业务,成了最主流的开发模式。下面四门语言,至今仍是各自领域的巨头。
Python(1991)
Guido van Rossum1989 年圣诞节期间在荷兰 CWI 研究所开始写 Python,1991 年 2 月发布了 0.9.0 版本。Python 的参考实现CPython从第一天起就是用 C 写的——这也是"CPython"名字里那个"C"的由来。
Python 语法受 ABC 和 Modula-3 影响,但花括号、缩进之外的很多底层约定(整数、字符串、运算符)都能看到 C 的影子。更重要的是,CPython 提供了一套完整的C API,允许用 C 写扩展模块,这让 Python 能轻松"粘住"各种高性能 C/C++ 库(NumPy 就是典型例子)。
Ruby(1995)
日本程序员松本行弘(Matz)想要一门"比 Perl 更面向对象、比 Python 更有表达力"的语言。1995 年 12 月,他公开发布了 Ruby。
Ruby 的官方实现叫MRI(Matz's Ruby Interpreter),也叫CRuby,顾名思义是用 C 写的。它有自己的虚拟机,但底层是 C。Ruby 的语法混合了 Perl、Smalltalk 和 C 的风格,后来 Ruby on Rails 框架让它在 Web 开发领域大红大紫。
PHP(1995)
PHP 的起源特别接地气。1994 年,丹麦裔加拿大程序员Rasmus Lerdorf想用几个小程序统计"谁看了我的在线简历"。他先用 Perl 写了个 CGI 包装器,嫌慢,又用 C 重写了一遍——这就是 PHP 的雏形。
1995 年 6 月 8 日,他以"Personal Home Page Tools(PHP Tools)1.0"之名发布到 Usenet。根据 PHP 官方手册的记载,PHP 最早的形态就是一组用 C 写的 CGI 二进制程序。后来它加进了表单处理(FI,Form Interpreter),演变成 PHP/FI,再由 Andi Gutmans 和 Zeev Suraski 重写引擎,发展成今天的 PHP。它的语法大量借鉴 C、Perl,是整个 LAMP 时代的 Web 后端之王。
Lua(1993)
1993 年,巴西里约热内卢天主教大学(PUC-Rio)的三位研究者 Roberto Ierusalimschy、Luiz Henrique de Figueiredo 和 Waldemar Celes,需要给巴西石油公司的应用软件配一门可嵌入的脚本语言。当时巴西有严格的计算机硬件软件进口壁垒,他们必须自己造。
这门语言就是Lua(葡萄牙语"月亮"的意思,因为它的前身叫 SOL,"太阳")。Lua 从诞生起就坚持一个原则:解释器用纯 ANSI C 写成,严格遵循 C 标准,不依赖任何平台特性。这让 Lua 的可移植性达到了极致——它能跑在从大型服务器到嵌入式设备、甚至游戏主机的几乎任何平台上。《魔兽世界》用 Lua 写插件,Redis、Nginx、游戏引擎(Cocos2d、LÖVE)都把 Lua 当嵌入式脚本。Lua 的 C API 设计得极其干净,被公认为"C 嵌入脚本语言"的教科书。
和 C 的关系:这四门语言的官方解释器/虚拟机都是用 C 实现的,并且都提供了 C 扩展接口。它们语法上不同程度地借鉴 C,运行时则把 C 当作"底座"。可以说,C 负责快,它们负责好用。
八、R:统计学家的 C 语言利器(1993)
1990 年代初,新西兰奥克兰大学的Ross Ihaka和Robert Gentleman想实现一门统计计算语言。他们参考了贝尔实验室的 S 语言,做出了一个开源实现,这就是R。
R 于 1993 年首次公开亮相。它的核心运行时用C 和 Fortran写成(统计计算圈有大量经典的 Fortran 数值库,这是历史原因),并提供了 C/Fortran 扩展接口。今天 R 是数据分析和生物统计领域最流行的语言之一,底层大量调用 C 编写的高性能计算代码。
和 C 的关系:核心用 C(加 Fortran)实现,通过 C 接口复用大量数值计算库。
九、JavaScript:十天写出来的 C 语言引擎(1995)
1995 年,网景公司(Netscape)想让网页"动起来"。工程师Brendan Eich受命设计一门浏览器脚本语言,他花了大约 10 天就写出了原型(当时叫 Mocha,后改名 LiveScript,最终蹭 Java 的热度定名 JavaScript)。
JavaScript 的第一个引擎(也就是后来开源的SpiderMonkey的前身)是 Eich用 C 写的。这个引擎包含编译器、解释器、反编译器和垃圾回收器,全部是 C 代码。虽然后来 SpiderMonkey 逐步迁移到了 C++ 和 Rust,但 1995 年随 Netscape Navigator 2.0 发布的那第一个版本,确实是 C 的产物。
JavaScript 的语法(花括号、for/while、运算符)刻意模仿 Java/C,好让当时的 C/Java 程序员觉得眼熟;但它的函数式内核其实更像 Scheme。今天 JavaScript 是世界上使用最广泛的语言之一,而它的第一个引擎,和这篇文章里其他语言一样,建立在 C 之上。
和 C 的关系:第一个 JS 引擎用 C 实现;语法表层借鉴 C。注意:现代的 V8 引擎是 C++ 写的,但那是后来的事。
十、Go:C 语言祖师爷晚年用 C 写的新语言(2009)
时间快进到 2007 年,Google 的三位大牛——Robert Griesemer、Rob Pike,以及当年和 Ritchie 一起发明 C 和 Unix 的 Ken Thompson——对 C++ 开发大型服务端程序的痛深感不满,决定设计一门新语言:简单、并发、编译快。这就是Go。
这里有一个让很多人意外的事实:Go 的初代编译器gc是 Ken Thompson 用 C 写的。2008 年 1 月,Thompson 开始动手,编译器用 C 编写,用 Yacc/Bison 做语法解析,连命名都带着 Plan 9 的老传统(5c/6c/8c 对应 ARM/x86-64/x86)。Go 在 2009 年 11 月 10 日正式开源发布。
从 2009 年一直到Go 1.4(2014 年),Go 的编译器、汇编器、链接器全都是 C 代码。到了Go 1.5(2015 年 8 月),Go 团队把编译器和运行时全部用 Go 重写,完成了自举——从此 Go 不再依赖 C 编译器。Go 官方在 2015 年的技术演讲"Why was it in C? Bootstrapping."(为什么当初用 C?为了自举。)里,把这件事讲得明明白白。
Go 的语法同样深受 C 影响:花括号、分号省略、指针、基本类型,甚至 Ken Thompson 本人就是 C 的创造者之一。Go 可以说是 C 语言"直系精神传人"在 21 世纪的回响。
和 C 的关系:初代编译器由 C 语言之父之一 Ken Thompson 用 C 写成,2015 年自举;语法和设计哲学深受 C 影响。
十一、遗珠:那些同样用 C 写成、值得一提的语言
除了上面的"主角",还有不少语言的第一个版本也是用 C 实现的,限于篇幅这里简列:
- m4(1977):Brian Kernighan 写的宏处理语言,用 C 实现,至今是 Unix/Linux 构建系统(autoconf)的基石。
- KornShell / ksh(1983):David Korn 在贝尔实验室写的 shell,用 C 实现,融合了 csh 和 sh 的优点,是很多商业 Unix 的默认 shell。
- Zsh(1990):Paul Falstad 写的 shell,用 C 实现,功能强大,如今是 macOS 默认 shell 和 Oh My Zsh 的基础。
- Guile(1993):GNU 项目的官方扩展语言,Scheme 的一种实现,解释器用 C 写成,设计目标就是嵌入到 C 程序里。
- Plan 9 家族:Newsqueak(1988)、Alef(1995)、Limbo(1995):Rob Pike 等人在贝尔实验室用 C 写的并发语言,它们的通道(channel)思想后来直接启发了 Go。
- Vala(2006):语法像 C#,但编译成 C 代码再用 C 编译器编译,编译器本身也用 C 写,主要用于 GNOME 生态。
- Wren、Janet(2010 年代):两门较新的嵌入式脚本语言,都坚持用纯 C 实现解释器,延续了 Lua 的哲学。
十二、边界情况:这些语言和 C 关系很深,但最早不是用 C 实现的
为了严谨,有几个"名气很大、容易被误以为是 C 实现"的语言需要单独说明:
- C++(1985):如前所述,Cfront 把 C++ 翻译成 C,但 Cfront 本身是用 C++ 自举的。
- Java(1995):语法大量借鉴 C/C++,早期的 Sun JVM 用 C 和 C++ 编写;但 Java 编译器 javac 很早就用 Java 自身实现了,属于"虚拟机用 C/C++、编译器自举"的混合情况。
- C#(2000):.NET 的 CLR 运行时主要用 C++ 写,C
编译器 Roslyn 则用 C# 自举。
- Objective-C:最早是"输出 C 的预编译器",但后来的主流实现(GCC/Clang 模块)是 C/C++ 工程。它的特殊之处前面已经讲过。
- Swift(2014):Swift 编译器基于 LLVM,主要用 C++ 写成,不是 C。
- Rust(2010):Rust 的第一个编译器是用 OCaml 写的(叫 rustboot),后来用 Rust 自举。
- Zig(2016):Zig 的第一个编译器用 C++ 写成,2022—2023 年才完成用 Zig 自举。
- Nim(2008):最早的 Nim 编译器是用 Object Pascal(Delphi) 写的,后来用 Nim 重写。
- D(2001):Walter Bright 最早的 DMD 编译器前端用 C++ 写,后来才迁移到 D。
- Julia(2012):核心用 C++ 和 Scheme 写,底层用 LLVM,不是纯 C。
- MATLAB(1984):Cleve Moler 最早的 MATLAB 是用 Fortran 写的,后来才用 C 重写。
- Erlang(1986):Joe Armstrong 最早的 Erlang 解释器是用 Prolog 写的,后来的 BEAM 虚拟机才是 C 实现。
这些语言有的语法像 C,有的运行时依赖 C 的生态,但它们的"第一行实现"并不是 C,所以没有进入正文的主表。
十三、为什么偏偏是 C?
讲完这五十多年的故事,你可能会问:为什么这么多语言的第一个版本,都不约而同地选择用 C 来写?
原因可以归结为四点:
1. C 是"可移植的汇编"。C 离硬件足够近,能直接操作内存和指针,性能和汇编一个量级;但它又比汇编高级,写起来可移植。只要目标平台有一个 C 编译器,你的新语言就能跑。在那个硬件平台五花八门的年代,这是巨大的优势。
2. C 和 Unix 一起统治了世界。从 1970 年代起,Unix(以及后来的 Linux)的内核、工具链、系统调用全是 C。用 C 写新语言,可以无缝调用整个操作系统的能力。C 不只是一门语言,它是整个软件世界的"接口标准"。
3. 自举的悖论。写一门新语言时,你还没有这门语言的编译器,所以必须用一门已有的语言来写它。而 C 几乎是所有平台上唯一"一定存在"的高级语言。Ken Thompson 后来讲 Go 为什么当初用 C 时,答案就两个词:"Bootstrapping."(为了自举。)
4. C 的接口是跨语言的"通用插头"。几乎所有语言都提供了"C 接口"(C FFI / C API),因为 C 的类型和调用约定是所有操作系统和库的事实标准。用 C 实现新语言,意味着它能直接调用全世界海量的 C 库。
结语
从 1974 年贝尔实验室里那 1600 行 C 代码的 sed,到 2009 年 Ken Thompson 用 C 敲下 Go 编译器的第一行,再到今天仍在用 C 从头实现新语言的 Wren、Janet——C 语言像一条暗河,流淌在几乎所有编程语言的脚下。
你用 Python 做数据分析、用 PHP 写网站、用 Ruby 做后端、用 Lua 写游戏插件、用 JavaScript 刷网页、用 Go 写微服务——这些语言的语法和哲学千差万别,但它们最早的"摇篮",都是同一种语言。
C 已经 54 岁了。很多语言宣称要取代它,但讽刺的是,这些"取代者"自己,往往还是用 C 养大的。
这大概就是 Dennis Ritchie 留给世界最深的烙印:C 不只是一门编程语言,它是整个数字世界的地基。
参考来源:Dennis Ritchie《The Development of the C Language》、Bjarne Stroustrup 关于 C++ 历史的论文、Go 官方博客与 2015 年"Go in Go"技术演讲、PHP 官方手册、Lua HOPL III 论文《The Evolution of Lua》、Tcl 作者 John Ousterhout 的历史记述、Python/CPython 文档、Ruby 官方站点、Mozilla SpiderMonkey 文档、ACM 与 IEEE Computer Society 相关资料等。