编程达到一个高的境界就是自制脚本语言,通过这可以精通编程里面的高深的技术,如编译原理、语言处理器、编译器与解释器,这些都是代表一个程序员实力的技术。
每个程序员都有实现属于自己编程语言的梦想,说其是梦想,原因是实现的难度很大......这种情况一直持续到《自制编程语言》的出现。
《自制编程语言》
郑钢 著
本书讲的是纯粹的技术“干货”,符合郑钢老师一贯的写作风格,这是他静心写出来的东西,内容满满,很值得阅读。滴滴系统部技术高级总监于晓声说:“很高兴能成为本书的首批读者,也很高兴能为本书写推荐序。”
刚拿到本书手稿时,从书名上我意识到这是对我胃口的书。果然,整书阅读以后,收获颇多。如今程序员的开发成本已经很低了,项目中有各种成熟的框架和库可供选择和使用,但还有人能静下心来研究编译器这么底层的技术,实属难得。本书犹如一把火炬,点燃了技术人内心对开发的热情。
依稀记得2010年年初在百度与郑钢初次见面的情景,那时他工作之余的时间基本都用在向各个技术专家请教、讨论各类技术问题上,他是我带过的人中最勤奋的人之一。时间荏苒,一分耕耘一分收获,看到他今天的成长,尤感欣慰。
本书讲述了一门脚本语言(sparrow)的开发过程,这是一本“步步为营”式的书籍,延续了他编写《操作系统真象还原》的风格,手把手地教读者从零实现一门语言,从原理到实践每一步都有实际的代码和详尽的原理说明,通过运行书中各小节中的代码,读者可以很轻松地掌握各个细节,因此本书的学习曲线并不陡峭,甚至很平坦。
另外,值得欣喜的是,本书所编写的脚本语言并不是用Java、C++等入门难度略大的语言实现的,而是用C语言,这是我们学习编程的基础语言。也就是说,本书并不需要专业的开发经验即可上手学习。另外,在实现过程中并未用到复杂的库函数或系统调用,可以负责地说,本书已经将学习成本降到最低。
C语言是一种面向过程的语言,如何用一种面向过程的语言去实现一种面向对象的语言很有意思。另外,PHP和Perl语言虽然也实现了类,但它们其实是一种面向过程的语言,并不是纯粹的面向对象语言,而sparrow语言是一种纯粹的面向对象语言,它在设计之初就采用对象的方式来处理脚本语言中类的成员和方法,这仿佛让我们看到了面向对象编程语言的基因。
众所周知,当今最流行的脚本语言应属Python,Python也是用C语言实现的,也许你很好奇Python的内部原理,但是想到它有将近 4 万行的源代码时,也许甚至不想看它的源程序了。那么研读本书中的sparrow语言会是一种更好的选择,其源码不足7100行,阅读过程轻松愉快,但可以学到Python这种语言的实现原理。
对于脚本语言来说,两个重要方面就是垃圾回收和运行环境。垃圾回收就是我们平时所说的GC(Garbage Collection)。有了GC,程序员不需要手工释放所分配的对象,可以使精力专注于业务逻辑而不用担心内存泄漏问题。
在sparrow语言中同样实现了GC,通过此部分代码你可以看到GC 的原理,以及哪些对象才能被回收。 运行时环境就是脚本语言中的虚拟机,即VM(如Java语言的JVM也是一种VM)。
脚本语言是通过虚拟机才能运行的,如何把编译器生成的操作码转换为实际的代码行为,这里面的工作对大多数人来说很神秘。相信各位在源码中一探究竟之后会发现:GC和VM这两个神秘的黑盒子不过如此。
另外,也许程序员最感兴趣的就是线程,关于线程在用户态下是如何实现的、线程如何实现调度,本书将告诉你答案。总之,但凡涉猎,开卷有益。
为什么创作这本书?
很多读者看了我写的《操作系统真象还原》(一本一步步编写操作系统的书)书后,纷纷来信,要求我再写一本自制编程语言的书。这也在情理之中,对于很多计算机从业者来说,操作系统和编译器几乎是两座无法逾越的大山,其难度之大,令很多人员望而生畏。最终,在读者的鼓励下,一冲动就答应了写作本书,其实我很“后悔”做出这样的决定。
为什么后悔呢?因为写书代价很大。
首先,写书相当累,占用很多精力。其次,占用自己学习的时间,在当今个人进步缓慢就算退步的时代,自己没有提升技术会很恐慌。
再次,精力全放在写书上会影响家庭、影响工作。
最后,还要负责解答许多问题,确实很累。而且,这一次可是在创造编程语言,难度系数太高了,不亚于开发一个操作系统,甚至我父母都劝我:小刚,你都多大了还写书,好好过日子、踏实上班就行了。但是,我最后还是决定写本书。
下面是我跨过重重拦阻创作本书的动机。
1 有梦想,有远方
既然写书代价那么大,那我为什么还要“明知山有虎,偏向虎山行”呢?因为我就是奔着“老虎”去的,没有老虎的山就没有探险的乐趣。
2 有难度才有价值
每次遇到一件很难的工作时,我先是“痛苦”,然后随之而来的就是“兴奋”,因为这意味着我要进步。也许读者会说,一定会进步吗?也许99%会失败。
同样一件事,每个人对它的态度都不同,懦夫看到的是:99%会失败,别干了。勇士看到的是:还有1%成功的机会,干吧!
只要不放弃(注意,不是坚持),一定会成功,成功只是时间长短的问题。
3 人生的意义
人生最大的遗憾是“壮志未酬”。如果你是天才,请将自己的才华“挥霍”得一滴不剩,直到触碰到自己智力上的天花板,这样才甘心。如果你是大力士,请努力在奥运赛场上为国争光,直到累得站不起来,这样才甘心。
这正是我写本书的信仰。
学习很累并且无止境,但是多知道一些就会有多一些的欣喜。本着“把自己的知识多掏点给大家”的诚意,本书依然从第0章开始,相对《操作系统真象还原》来说,本书的语言不再那么活泼(啰唆)了,毕竟编译器的开发难度略小于开发操作系统,没必要穿插一些“过渡”的话题。
本书一步步地实现了一种称为sparrow的编程语言,它是用虚拟机运行的,因此最后还要实现一个虚拟机。sparrow语言是用C语言编写的,学习的难度较低,实现的代码不长,希望大家在学习的旅途中愉快。
为什么读这本书?
本书是一本专门介绍自制编程语言的图书,书中深入浅出地讲述了如何开发一门编程语言,以及运行这门编程语言的虚拟机。
本书主要内容包括:脚本语言的功能、词法分析器、类、对象、原生方法、自上而下算符优先、语法分析、语义分析、虚拟机、内建类、垃圾回收、命令行及调试等技术。
本书适合程序员阅读,也适合对编程语言原理感兴趣的计算机从业人员学习。
成功的基石不是坚持,而是“不放弃”
人们常说,坚持是成功的“前提”。我说,既然只是前提,这说明坚持也未必会成功。要想成功,人们需要的是成功的“基石”,而不是“前提”,这个基石就是3个字:不放弃。
大部分读者都觉得开发一门编程语言是很难的事,甚至想都不敢想,我担心你也有这个想法,所以特意用这种方式先和你说说心里话:这本书你买都买了,多少发挥点价值才对得起买书的钱,谁的钱也不是白来的。
首先,我并不会为了鼓励大家而大言不惭地说开发语言“其实不难”“很容易”之类的话,相反,这个方向确实很难,而且就应该很难,我想这也正是吸引你的地方,没有难度哪来的价值,“其实不难、很容易”之类的话是对大家上进心的不尊重。
其次,只有在“我也认为很难”的前提下才能保证大部分的朋友能看懂本书。你看,在普通人眼里从A到D,需要有B和C的推理过程,一个步骤都不能少,在天才眼里,A到D是理所应当的事,不需要解释得太清楚,天才认为B和C都是废话,明摆着的事不需要解释。而我不是天才,所以我会把B和C解释清楚。
回到开头的话,为什么说成功的基石不是“坚持”而是“不放弃”呢?这两个词有啥区别?也许有读者说,不放弃就是做着喜欢的事,让自己爱上学习技术。个人觉得这有点不对了,我觉得我更喜欢吃喝玩乐,因为那是生物的本能,选择技术的原因只是我没那么讨厌它,它是我从众多讨厌的事物中选择的最不讨厌的东西。
放弃是为了减少痛苦,坚持是带着痛苦继续前行。“坚持”是个痛苦的词,但凡靠坚持来做的事情必然建立在痛苦之上,而痛苦就会使人产生放弃的念头,这是生物的本能。用“坚持”来“鼓励”自己硬着头皮干,其实已经输了一半,自己认为痛苦的事很难干下去,干不下去的原因是遇到困难时头脑里有“放弃”的念头,如果把这个念头去掉,那么,只要活着,成功无非是时间长短的问题。这个念头其实就是心理预期,“提前”做好心理预期很重要。
总之,不要给自己“可以放弃”的念头,不要让“可以放弃”成为一种选项,把这个选项去掉,那么,只剩下成功。
你懂编程语言的“心”吗
先来猜猜这是什么?
它是一种人人必不可少,拥有多种颜色、多种外形的物品。
它是一种质地柔软,可使人免受风寒,给予人们温暖的日常物品。
它是一种使人更加美丽,更受年轻女性欢迎的物品。
它是一种用纽扣、拉链或绳带绑定到身体上的物品。
猜到了吗?其实这是对“衣服”的描述。由于我们都知道什么是衣服,因此我们认为以上4种描述都是正确的,通过“免受风寒”这4个字便有可能想到是衣服。但对于没见过衣服的人,比如刚出生的小孩儿,他肯定还是不懂,甚至不知道什么是纽扣。
什么是编程语言呢?以下摘自百度百科。
(1)“编程语言"(programming language),是用来定义计算机程序的形式语言。它是一种被标准化的交流技巧,用来向计算机发出指令……
(2)编程语言的描述一般可以分为语法及语义。语法是说明编程语言中,哪些符号或文字的组合方式是正确的,语义则是对于编程的解释……
(3)编程语言俗称“计算机语言”,种类非常多,总的来说可以分成机器语言、汇编语言、高级语言三大类。程序是计算机要执行的指令的集合,而程序全部都是用我们所掌握的语言来编写的……
就像刚才我对衣服的描述,以上的3个概念,懂的人早已经懂了,不懂的人还是不懂,回答显得很“鸡肋”。因为对于编程语言的理解并不在语言本身,而是在编译器,编译器是编程语言的“心”,而我们很少有人像了解衣服那样了解编译器,因此对于我们大多数人来说只是熟悉了语言的语法,仅仅是“会用”而已。
那什么是编程语言呢?无论我用多少文字都不足以表述精准与全面,因为语言的本质就是编译器,等你了解编译器后,答案自在心中。目前我只能给出同样“鸡肋”的答案—编程语言是编译器用来“将人类思想转换为计算机行为”的语法规则。
编程语言的来历
世界上本没有编程语言,有的只是编译器。语言本身只是一系列的语法规则, 这个规则对应的“行为”才是我们编程的“意图”,因此从“规则”到“行为”解析便是语言的本质,这就是编译器所做的工作。
估计大伙儿都知道,如果想输出字符串,在PHP语言中可以用语句echo,在C语言中使用printf函数,在C++中使用cout,这说明不同的规则对应相同的行为,因此语言规则的多样性只是迷惑人的外表,而本质的行为都是一样的,万变不离其宗。
并不是“打印”功能就一定得是print、out等相关的字眼儿,那是编译器的设计者为了用户使用方便(当然也是为了他自己设计方便)而采用了大伙儿有共识的关键字,避免不必要的混乱。
语言一定要用更底层的语言来编写吗
有这个疑问并不奇怪,比如:
(1)Python是用C写的,C较Python来说更适合底层执行。
(2)C代码在编译后会转换为更底层的汇编代码给汇编器,再由汇编器将汇编代码转换为机器码。
因此给人的感觉是,一种语言必须要用更底层的语言来实现,其实这是个误解。C只是起初是用汇编语言写的,因为在C语言之前只有汇编语言和机器语言。人总是懒惰的,肯定是挑最方便的用,汇编语言好歹是机器语言的符号化,因此相对来说更好用一些,所以只好用汇编来编写C语言,等第一版C语言诞生后,他们就用C语言来写了。
什么?用C来编写C?有些读者内心就崩溃了,似乎像是陷入了死循环。其实这根本不是一回事,因为起作用的并不是C语言,而是C编译器。语言只是规则,编译器产生的行为才是最关键的,编译器就是个程序,C代码只是它的文本输入。用C来编写C,这就是自举,假如编译器是用别的语言写的,也许你心里就好受一些了。
其实只要所使用的语言具有一定的写文件功能就能够写编译器,为什么这么说呢?因为编译器本身是程序,程序本身是由操作系统加载执行的,操作系统识别程序的格式后按照格式读取程序中的段并加载到内存,最后使程序计数器(寄存器pc或ip)跳到程序入口,该程序就执行了。
因此用来编写编译器的语言只要具有一定程度的写文件的能力即可,比如至少要具有形同seek的文件定位功能,这可用于按照不同格式的协议在不同的偏移处写入数据,因此用Python是可以写出C编译器的。在这之前我写过《操作系统真象还原》一书,里面的第0章第0.17小节“先有的语言还是先有的编译器,第1个编译器是怎么产生的”,详细地说明C编译器是如何自举的,下面我把它贴过来。
首先肯定的是先有的编程语言,哪怕这个语言简单到只有一个符号。先是设计好语言的规则,然后编写能够识别这套规则的编译器,否则若没有语言规则作为指导方向,编译器的编写将无从下笔。第1个编译器是怎么产生的,这个问题我并没有求证,不过可以谈下自己的理解,请大伙儿辩证地看。
这个问题属于哲学中鸡生蛋,蛋生鸡的问题,这种思维回旋性质的本源问题经常让人产生迷惑。可是现实生活中这样的例子太多了,具体如下。
(1)英语老师教学生英语,学生成了英语老师后又可以教其他学生英语。
(2)写新的书需要参考其他旧书,新的书将来又会被更新的书参考,就像本书编写过程一样,要参考许多前辈的著作。
(3)用工具可以制造工具,被制造出来的工具将来又可以制造新的工具。
(4)编译器可以编译出新的编译器。
这种自己创造自己的现象,称为自举。
自举?是不是自己把自己举起来?是的,人是不能把自己举起来的,这个词很形象地描述了这类“后果必须有前因”的现象。
以上前3个举的都是生活例子,似乎比第4个更容易接受。即使这样,对于前3个例子大家依然会有疑问:
(1)第一个会英语的人是谁教的?
(2)第一本书是怎样产生的?
(3)第一个工具是如何制造出来的?
其实看到第(2)个例子大家就可能明白了。世界上的第一本书,它的知识来源肯定是人的记忆,通过向个人或群众打听,把大家都认同的知识记录到某个介质上,这样第一本书就出生了。此后再记录新的知识时,由于有了这本书的参考,不需要重新再向众人打听原有知识了,从此以后便形成了书生书的因果循环。
从书的例子可以证明,本源问题中的第一个,都是由其他事物创建出来的,不是自己创造的自己。
就像先有鸡还是先有蛋一样,一定是先有的其他生命体,这个生命体不是今天所说的鸡。伴随这个生命体漫长的进化中,突然有一天具备了生蛋的能力(也许这个蛋在最初并不能孵化成鸡,这个生命体又经过漫长的进化,最终可以生出能够孵化成鸡的蛋),于是这个蛋可以生出鸡了。过了很久之后,才有的人类。人一开始便接触的是现在的鸡而不知道那个生命体的存在,所以人只知道鸡是由蛋生出来的。
很容易让人混淆的是编译C语言时,它先是被编译成汇编代码,再由汇编代码编译为机器码,这样很容易让人误以为一种语言是基于一种更底层的语言的。似乎没有汇编语言,C语言就没有办法编译一样。拿gcc来说,其内部确实要调用汇编器来完成汇编语言到机器码的翻译工作。因为已经有了汇编语言编译器,那何必浪费这个资源不用,自己非要把C语言直接翻译成机器码呢,毕竟汇编器已经无比健壮了,将C直接变成机器码这个难度比将C语言翻译为汇编语言大多了,这属于重新造*的行为。
曾经我就这样问过自己,PHP解释器是用C语言写的,C编译器是用汇编语言写的(这句话不正确),汇编语言是谁写的呢?后来才知道,编译器gcc其实是用C语言写的。乍一听,什么?用C语言写C编译器?自己创造自己,就像电影《超验骇客》一样。当时的思维似乎陷入了死循环一样,现在看来这不奇怪。其实编译器用什么语言写是无所谓的,关键是能编译出指令就行了。
编译出的可执行文件是要写到磁盘上的,理论上,某个进程,无论其是不是编译器,只要其关于读写文件的功能足够强大,可以往磁盘上写任意内容,都可以生成可执行文件,直接让操作系统加载运行。想象一下,用Python写一个脚本,功能是复制一个二进制可执行文件,新复制出来的文件肯定是可以执行的。那Python脚本直接输出这样的一个二进制可执行文件,它自然就是可以直接执行的,完全脱离Python解释器了。
编译器其实就是语言,因为编译器在设计之初就是先要规划好某种语言,根据这个语言规则来写合适的编译器。所以说,要发明一种语言,关键是得写出与之配套的编译器,这两者是同时出来的。最初的编译器肯定是简单、粗糙的,因为当时的编程语言肯定不完善,顶多是几个符号而已,所以难以称之为语言。只有功能完善且符合规范,有自己一套体系后才能称之为语言。
不用说,这个最初的编译器肯定无法编译今天的C语言代码。编程语言只是文本,文本只是用来看的,没有执行能力。最初的编译器肯定是用机器码写出来的。这个编译器能识别文本,可以处理一些符号关键字。随着符号越来越多,不断地去改进这个编译器就是了。
以上的符号说的就是编程语言。后来这个编译器支持的关键字越来越多了,也就是这个编译器支持的编程语言越发强大了,可以写出一些复杂的功能的时候,干脆直接用这个语言写个新的编译器,这个新的编译器出生时,还是需要用旧的编译器编译出来的。
只要有了新的编译器,之后就可以和旧的编译器说拜拜了。发明新的编译器实际上就是能够处理更多的符号关键字,也就是又有新的开发语言了,这门语言可以是全新的也可以是最初的语言,这取决于编译器的实现。这个过程不断持续,不断进化,逐渐才有了今天的各种语言解释器,这是个迭代的过程。
图 0-1
图0-1在网络上非常火,它常常与励志类的文字相关。起初看到这个雕像在雕刻自己时,我着实被感动了,感受到的是一种成长之痛。今天把它贴过来的目的是想告诉大家,起初的编译器也是功能简单,不成规范的,然而经过不断自我“雕刻”,它才有了今天功能的完善。
下面的内容我参考了别人的文章,由于找不到这位大师的署名,只好在此先献上我真挚的敬意,感谢他对求知者的奉献。
要说到C编译器的发展,必须要提到这两位大神—C语言之父Dennis Ritchie和Ken Thompson。Dennis和Ken在编程语言和操作系统的深远贡献让他们获得了计算机科学的最高荣誉,Dennis和Ken于1983年赢得了ACM图灵奖。
编译器是靠不断学习,不断积累才发展起来的,这是自我学习的过程。下面来看看他们是如何让编译器长大的。
我们都知道转义字符,转义字符是以\开头的多个字符,通常表示某些控制字符,它们通常是不可键入的,也就是这些字符无法在键盘上直接输入,比如\n表示回车换行,\t表示tab。由于以\开头的字符表示转义,因此要想表示\字符本身,就约定用\来转义自己,即\\表示字符\。转义字符虽然表示的是单个字符的意义,在编译器眼里转义字符是多个字符组成的字符串,比如\n是字符\和n组成的字符串。
起初的C编译器中并没有处理转义字符,为叙述方便,我们现在称之为旧编译器。如果待编译的代码文件中有字符串\\,这在旧编译器眼里就是\\字符串,并不是转义后的单个字符\。为了表明编译器与作为其输入的代码文件的关系,我们称“作为输入的代码文件”为应用程序文件。尽管被编译的代码文件是实现了一个编译器,而在编译器眼里,它只是一个应用程序级的角色。例如,gcc –c a.c中,a.c就是应用程序文件。
现在想在编译器中添加对转义字符的支持,那就需要修改旧编译器的源代码,假设旧编译器的源代码文件名为compile_old.c。被修改后的编译器代码,已不属于旧编译器的源代码,故我们命名其文件名为compile_new_a.c,图0-2是修改后的内容。
代码compile_new_a.c
图 0-2
其中,函数next()的功能是返回待处理文本(即被编译的源码文件)中的下一字符,强调一下是“单个字符”,并不是记法分析中的单词(即token)。
用旧编译器将新编译器的源代码compile_new_a.c编译,生成可执行文件,该文件就是新的编译器,我们取名为新编译器_a。为了方便理清它们的关系,将它们列入表0-1中。
表 0-1
这下编译出来的新编译器_a可以编译含有转义字符\\的应用程序代码了,也就是说,待编译的文件(也就是应用程序代码)中,应该用\\来表示\。而单独的字符\在新编译器_a中未做处理而无法通过编译。所以此时新编译器_a是无法编译自己的源代码compile_new_a.c的,因为该源文件中只是单个\字符,新编译器_a只认得\\。先更新它们的关系,见表0-2。
表 0-2
也就是说,现在新编译器_a,无法编译自己的源文件compile_new_a.c,只有旧编译器才能编译它。再说一下,新编译器_a无法正确编译自己的源文件compile_new_a.c的原因是,compile_new_a.c中\字符应该用转义字符的方式来引用,即所有用\的地方都应该替换为\\。再回头看一下新编译器_a的源代码compile_new_a.c,它只处理了字符串\\,单个\没有对应的处理逻辑。下面修改代码,将新修改后的代码命名为compile_new_b.c,如图0-3所示。
代码compile_new_b.c
图 0-3
其实compile_new_b.c只是更新了转义字符的语法,这是新编译器_a所支持的新的语法,下面还是以新编译器_a来编译新的编译器。
用新编译器_a编译此文件,将生成新编译器_b,将新的关系录入到表0-3中。
表 0-3
继续之前再说一下:用编译器去编译另一编译器的源码,也许有的读者觉得很费解,其实你把“被编译的编译器源码”当成普通的应用程序源码就特别容易理解了。上面的编译器代码compile_new_b.c,其第3、6、7行的字符串\\被新编译器_a处理后,会以单字符\来代替(这是新编译器_a源码中return语句的功能),因此最终处理完成后的代码等同于代码compile_new_a.c。现在想加上换行符\n的支持,如图0-4所示。
图 0-4
由于现在编译器还不认识\n,故这样做肯定不行,不过可以用其ASCII码来代替,将其命名为compile_new_c.c,如图0-5所示。
代码compile_new_c.c
图 0-5
用新编译器_a来编译compile_new_c.c,将生成新编译器_c,新编译器_c的代码相当于代码compile_new_c.c中所有\\被替换为\后的样子,如表0-4所列,暂且称之为代码compile_new_c1.c,如图0-6所示。
代码compile_new_c1.c
图 0-6
表 0-4
最后再修改compile_new_c.c为compile_new_d.c,将10用\n替代,如图0-7所示。
代码compile_new_d.c
图 0-7
用新编译器_c编译compile_new_d.c,生成新编译器d,将直接识别\n。同理,新编译器d的代码相当于代码compile_new_d.c中,所有字符串\\被替换为字符\、字符\n被替换为数字10后的样子,即等同于代码compile_new_c1.c,如表0-5所列。
编译器经过这样不断地训练,功能越来越强大,不过占用的空间也越来越大了。
表 0-5
《自制编程语言》
郑钢 著
本书全面从脚本语言和虚拟机介绍开始,讲解了词法分析的实现、一些底层数据结构的实现、符号表及类的结构符号表,常量存储,局部变量,模块变量,方法存储、虚拟机原理、运行时栈实现、编译的实现、语法分析和语法制导自顶向下算符优先构造规则、调试、查看指令流、查看运行时栈、给类添加更多的方法、垃圾回收实现、添加命令行支持命令行接口。
《今日互动》
你最想从这本书中学到什么?为什么?截止时间7月27日17时,留言+转发本活动到朋友圈,小编将抽奖选出2名读者赠送纸书2本,文末留言点赞最多的自动获得图书1本(参与活动直达微信端编程达到一个高的境界就是自制脚本语言)
长按二维码,可以关注我们哟
每天与你分享IT好文。
在“异步图书”后台回复“关注”,即可免费获得2000门在线视频课程
文章转载自公众号
点击阅读原文:直接购书哦
阅读原文