My Painful Ride with VSCode Source Code


前件

出于某些原因,从2021年一月底开始,我的工作就变成了阅读vscode代码。需求方的要求是希望最终能够自由定制vscode。我当时就提出了异议,说要是希望自由定制,不如直接基于monaco从头开始做,这样要快而方便得多。当然,我这种普通的小虾米的意见是不会有人听的;我最终真的被迫去读它的代码了。现在项目中关于vscode的部分差不多可以算是结束了,于是大致讲一下个人的经历。

My Painful Ride

编译与打包

需求方需要的是可以发行的安装包,所以必须先确定生成安装包的流程。vscode在github上的repo的wiki确实有提到如何编译打包,但是在我的mac mini上会报什么heap allocation failed之类的奇怪错误,于是我不得不去阅读具体的构建脚本,然后自己重新写了一个。所幸这工作并不难:在vscode的构建脚本里,打包过程的不同阶段都已经单独封装好了,我只需要逐个调用执行这些阶段任务即可。

Facade of Shitshow

得到能够确定性地生成安装包的方法之后,接下来的任务便是对具体代码的解析和theory building。也许会有人觉得,对于理解vscode内部工作原理,vscode的插件api是一个好的起始点。从现在回头看,这简直是个过于天真的想法:实际上,这么做真的不比直接读代码、赌内部的类和函数的行为正如预料来得快。vscode的插件运行在单独的extension host进程,对于插件而言,vscode只是内部对这个host暴露出来的api所描述的那一块内容。用OOP设计模式的话术来说,这大概该叫「facade pattern」;但是这facade的设计又时常不跟内部的任何一个地方相似,这就使得试图从extension host的代码推断出内部行为这种事情变得困难。当然,反过来看,这在某种意义上也可以视作某种内部对相关部分的抽象的不同程度的失败的征兆:如果面向插件的接口可以设计得如此简单,那么为什么面向内部的接口会比它复杂,是(无论是否由过去的legacy设计而来)设计上的失误,还是另有目的?

Idiom Soup of Hell

在理解代码的整个过程中最让人绝望的是,VSCode代码内部已经形成了大量的有着不同所属的层次的习语:

  • 有概念层次的习语,比如说Command、Action和Service具体指的是什么东西的抽象,这些抽象在VSCode整体内有什么样的功用等;
  • 有具体做法层次的习语,比如说在renderer进程里给workbench添加command需要做什么操作,定义的action要怎么在别的地方使用等等;
  • 有代码组织层次的习语,比如说新的控件该怎么添加,内置插件该怎么添加,该怎么将新添加的东西挂到另外某个index文件里让它生效等;
  • 有前提和适用范围层次的习语,比如说VSCode禁止直接从CommandsRegistry中把命令抽出来调用所以必须要想办法弄一个CommandService实例等;

这些习语(1)根本没有文档而且(2)很难单纯通过看代码的方式推断出来;不仅如此,有的时候,明明是类似的东西,却分别使用了两套(或以上)不同的习语,这也会对从具体的代码中抽出习语造成阻碍。没有完全搞懂这些在其中工作和navigate所必须的习语(再加上deadline迫近)的结果,就是不得不自己重新搞一套1。比如说,为了能够在electron的主进程和renderer进程之间传输一些renderer无法获取但是又需要呈现的数据,我在VSCode自己对electron ipc的封装之外自己又单独写了一套。可能会有人说,如果我想要搞明白它的行为,我应该去看它的测试代码;然而有的时候这么做也没有用,因为:

  • 测试时常是针对单个组件的测试,难以推断这些组件在整个系统之中与别的组件的交互行为。(我不是测试人,不知道测试对这个问题有什么高大上的理论,这只是个人直观的感受。)
  • 有时连推断出对应的slot**本身**的完整的行为都做不到,比如说测试用例中给出的例子跟实际的例子相差太远,结果基于测试用例得出的推断在实际情况下根本毫无用处。

就是因为有这一类的经历,我才会一直认为,代码就是代码,它是一种artifact,不是也绝对不会是它自己的文档或者设计;从文档/设计到代码的转化过程中,一定会出现重要信息的遗失。所有「阅读代码」的需求,都由对设计和行为的不知情所产生。当然一定有人要说,我在这方面的痛苦单纯只是因为我没有理解它的设计;但这正是我要说的,就是因为代码不是设计,所以我才没有理解。另外一些人可能会说,「写测试」这个操作本身是没有问题的,这些人不懂得怎么写好的测试用例,所以才会给阅读代码的人造成这样的困难;可是我要说,在这个问题上讨论测试用例的好坏完全是没有找准对象:为什么不直接把设计和spec**本身**作为测试的描述给写下来?

有时候写错代码还会有非常另类的行为发生:编译可以正常编译,打包能够顺利打包,但是运行时就会发生那一类很容易就能知道很严重的错误(比如说整个workbench加载失败没有任何显示)。被这种错误多次殴打过后,我得出了一个heuristic:大部分的时候,这都是因为引用到了不该引用的文件。VSCode代码内部有很多有两套(或以上)的几近一模一样的东西(尤其是Monaco的源码也在这里面,为了支撑它standalone,有很多东西是单独给它写了一套的),而使用自动import补全的时候,很容易就会不小心生成这样的引用。比如说有这么一次,在给之前为VSCode增加的一个控件实现一个打开文件夹的功能的时候,不小心引用到了`vs/editor`里的内容;最后的结果是,运行的时候Command Palette里除了编辑器专有的命令(剪切粘贴一类)之外什么都没有,而且无论是main process还是renderer都不会有任何错误显示。为了找出原因,花了将近一整天的时间。关于这种问题还有一点让人深恶痛绝的是,在macOS上通过上面说过的那种方式构建出来的包,在workbench加载失败时是无法打开Electron的开发者工具的。如果是主进程那还可以读终端输出,要是是renderer的话……那就祈祷上帝保佑你能够直接通过自己先前做的修改判断具体是什么东西出了差错罢。

「大家都来参与吧」型开源和「你看看就算了」型开源

现在的所谓开源运动是好是坏先且不论2;我想,开源的代码项目,大抵都可以放在「大家都来参与吧」和「你看看就算了」两种极端模式构建成的连续区间里。前者非常inclusive,文档写好写满,一般的程序员很容易就能开始做贡献;而另一端则是前者的相反。绝大多数开源项目(包括我自己的很多东西在内),大抵都应该归类在后者。之所以会有这个想法,是因为某日在知乎上看到一个回答,里面说:

……曾经有个博士给 chromium 邮件组写信,说其研究方向是软件工程,你们是否有结构性文档,譬如 UML 结构图等,如果没有我可以试试。然后开发组无情的拒绝了,理由是:1 软件迭代太快,结构变更频繁,这种外部文档没有任何价值,会误导之后的人。2 能参与开发的人无需这样的文档,没有能力的有文档也无法参与。这个小故事说明三点:1 学术界的UML分析工具相对开源领域已经非常过时了3;2 代码中蕴藏知识4无可替代;3 外部文档迟早会和代码脱节,更新代价稍高。

虽然我是没法确认确有其事,但是如果谷歌觉得他们真的需要新人给Chromium贡献代码,也许根本就不会说出「能参与开发的人无需这样的文档,没有能力的有文档也无法参与」这样的话。也许他们内部有带看repo的培训过程,也许他们有专人回答关于repo的任何问题;但是即使有,谷歌之外的人也是无从体验的。说真的,「只要代码写的足够好就可以不需要注释或文档」这种胡扯是从什么时候开始流行起来的?「需不需要」暂且不论——信誓旦旦地说着这种话而不写文档的人,他们对自己的能力的“足够好”真的就这么有自信吗?

Footnotes:

1

那感觉像在原本的codebase上手动从0生成寄生虫或肿瘤,体验真的算不上愉快。

2

有人认为现在「开源」已经变成了大公司给自己从技术和生态上赋能,进而控制和压迫所有中小公司和开发者的阴谋。

3

从技术的层面看,UML最大的问题是它的抽象跟代码处在同一级别,跟代码一样是artifact而不是design,所以在源码分析工具大量出现之后,UML就没有生存空间了。当然谁都知道从别的层面看它最大的问题是什么。对于某些人而言那大概真的是个令人怀念的好时代罢,无论是借助这种东西摇身一变成为“编程宗师”的人,还是真的把这些人当作宗师崇拜的人,无论是哪个蠢货,只要有一张嘴吹牛有一只手画UML,就能成为什么OOP架构师什么software consultant大把大把地赚钱。真希望哪一天这个行业能将这些人全部扫进垃圾堆里。

4

代码不蕴含知识。设计蕴含知识,取舍蕴含知识,代码不蕴含知识。代码中蕴含的叫技术债。


© Sebastian Higgins 2021 All Rights Reserved.
Content on this page is distributed under the CC BY-NC-SA 4.0 license unless further specified.
Last update: 2021.7.8

Back