面向C++语言的外部模块系统--Sin of Sloth

2020-06-24 09:24:55

不久前,约翰·普莱斯和我为C语言发明了一个外部模块系统。它对我们很有效,但从未流行起来。也许这会对你们中的一些人有用。

Sloth是一个雄心勃勃的项目(可供2到3个人使用)的一部分,该项目名为Popshop(我在我的软件成功帖子中提到了这一点)。

这一切开始时都是无害的-为什么我不编写一个Lucid解释器呢?

一个相反的论点是:我已经有了一个,正如我在之前的帖子中已经提到的那样。这主要是A·福斯蒂尼的作品,他是我的第一个学生,也是博士后。它运行得很好,拥有1985年的Lucid书中使用的所有功能,以及出色的错误处理和UNIX逃逸功能,您还想要什么呢?

但我有更大的抱负。在Lucid这本书出版后,我突然意识到,同样的原则也可以应用于类似的项目,例如,三维电子表格。

福斯蒂尼的翻译帮不上忙。它(事实证明是明智的)是一个单一目的的程序,而且引导起来很笨拙。Faustini已经练习了我所说的今天的编程-产生了一些立即有用的东西,如果与明天的项目无关的话。

我发誓我不会再犯同样的--在我看来--错误。我将为未来编程,并产生通用的可重用代码,这些代码可以用于未来的内涵编程项目;包括一些我甚至还没有想过的代码。

我不知道那会有多难。我最终不得不实现像Sloth这样的整套工具,这些工具需要的工作量和独创性与我脑海中的任何未来项目一样多。我滑下了过度设计的滑坡路。

但我就是不能让自己暂时写作,也不能为浮斯蒂尼翻译机写出一个单一目的的继任者。事实证明,这并不是一个好的职业选择。

我开始计划所有项目之母,很快就明白了软件可以分成许多独立的组件。

一个统一的主题是,所有未来的“可交付成果”(如电子表格)都将基于POP-2的数据类型和语法,就像plucid(Faustini实现的语言)一样。(这就是“Popshop”这个名字的由来。)。

POP-2已经消亡很久了,但加入了一些聪明的想法。它有数字、字符串、单词(原子)和嵌套列表,基本上与LISP相同。但是它使用了用户友好的中缀语法,就像pLucid和(我计划的)其他交付件一样。

因此,显然我们需要一个实现POP-2数据类型的组件(我们称其为模块)。以及用于输入和输出这些数据类型的单独组件。以及用于POP-2中缀表达式的词汇和句法分析的模块。

该计划是将表达式编译成在抽象机上运行的中间语言。所以我们需要编译器和抽象机器解释器的模块。

然后是用于导出(需求驱动评估)、用于仓库(缓存)、用于仓库存储管理、用于…的模块。不胜枚举。

在某种程度上,我们意识到我们将处理几十个模块。这听起来可能不是很多,但当你们只有三个人(我、约翰·普莱斯和学生马丁·戴维斯)时就是如此。每一个都应该是可单独编译的,并且都有一个.o文件以保持最新。仅Makefile(S)一项就会很大。

因此,我们决定将该过程自动化,结果是SLOTH。它消除了makefile,允许单独编译,并使.o文件保持最新。它生成并执行复杂的c编译器命令。这对我们来说非常有效。

基本思想是让模块成为一个目录(扩展名为.m),内部包含定义模块的各种文件(例如,过程定义或启动代码)。Sloth将使用这些文件生成自己的文件,并将它们存储在.m目录中。特别是,它将生成并维护最新的.o文件。

然后,要“编译”一个模块,需要使用“link module”命令LKM。如果foo是一个模块,那么LKM foo将更新foo的.o文件以及foo直接或间接依赖的所有模块的.o文件,启动一个大型编译,并生成一个名为foo的可执行文件。

通常,最大也是最重要的文件是proc.i文件。它包含过程定义和模块外不需要的其他声明(例如类型声明)。

模块还有一个var.i文件,其中包含其他模块在模块本身外部所需的变量声明。类似地,对于外部需要的宏、typedefs和其他声明,有一个fine.i文件。

每个模块都有一个初始化代码的文件体.I,在加电时执行一次。

在某种意义上,大多数模块需要其他更原始的模块可用。因此,每个模块都有一个文件导入,其中列出了这些模块的名称(不带.m扩展名)。模块不能直接或间接导入自身。

用户通常不会进入.m目录并直接操作这些文件。相反,它们使用简单的命令:例如,vmdfoo在foo.m目录中显示fine.i文件,而mmpfoo调用编辑器来修改foo.m中的proc.i文件。

sloth提供用于配置应用程序和生成可执行文件的单个命令。这是LKM(链接模块)命令。调用LKM foo将启动幕后精细计算,并在foo.m内创建许多文件。

但是,这对用户是透明的。暂停后,LKM终止,并在调用它的目录中保留一个可执行文件foo。

如果出于好奇心,你想知道懒惰是如何工作的,我建议你参考开头链接的那篇论文。在此期间,我将简要介绍一下(如果不完整的话)。

第一步是生成由foo直接或间接导入的所有模块的列表;生成可执行foo所需的所有模块。换句话说,这是植根于foo的导入关系的传递闭包。该列表存储在SLOTH生成的USELLIST文件中。

文件在用户列表中的显示顺序至关重要。简而言之,它首先是最原始的。更准确地说,如果模块X直接导入模块Y,那么Y必须出现在模块X之前(某处)。Sloth通过对导入关系执行拓扑排序来生成uselist。sloth直接或间接计算foo导入的每个文件的使用列表,而不仅仅是foo本身的使用列表。

下一步是安排foo的用法列表上每个模块的.o文件都是最新的(我将跳过一些细节)。一旦最新的.o文件可用,Sloth就会启动编译,生成可执行文件foo。它创建了一个简短的.c文件,其过程main由对foo的用法列表的powerup例程(body.I文件)的调用组成,最原始的首先调用。最后一个被调用的是Foo本身的第一个Body I.。

请注意,这不一定涉及大量编译。可能是Uselist上的大多数模块已经具有最新的.o文件,因为它们已经在另一个模块栏的Uselist上,并且已经为其调用了LKM栏。有了SLOTH,应用程序可以共享编译后的代码和源代码。

早些时候,我们遇到了似乎别无选择,只能允许模块相互导入的情况--这是一种不可接受的情况。在这些情况下,模块A调用模块B中的过程以及模块B调用模块A中的过程似乎是不可避免的。

考虑从空闲列表中存储分配存储的模块。当空闲列表耗尽时,它会执行标记并清除垃圾收集-这是通常的安排。

如果模块STA需要动态存储,它显然需要导入sto并调用sto中返回新单元的过程。但另一方面,当空闲列表耗尽时,它需要调用sta中的一个过程,该过程标记sta不准备放弃的所有存储。一个圆形。

解决方案是将对sta的回调设为匿名,这样sto就不必导入sta。我们向sto提供了我们称为jobjar的东西,即一组函数指针,以及一个将函数指针p添加到作业JAR的过程addjob(P)。在sta的powerup代码中,我们调用addjob(P),其中p是指向标记存储STA要保留的函数的指针。

然后,当自由列表用完时,sto首先标记它知道的所有信息,然后遍历作业JAR,调用其中的所有过程。这标记了导入sto的所有模块所需的所有存储空间(假设它们在初始化时都根据需要调用了addjob)。

类似的情况多次出现。例如,假设模块mch实现抽象机。机器的工作原理是将程序与POP-2单词相关联,在表格中将单词与函数指针配对。例如,如果模块MAT执行矩阵命令,则MAT导入mch。但机器必须知道矩阵命令及其名称。

解决方案是向MCH提供过程NEW COMMAND(W,P),该过程NEW COMMAND(W,P)接受字W和函数指针P,并将W添加到与P相关联的命令表。MAT中的POWER UP代码将所有MAT命令添加到MCH中的指令表。

懒惰最初是为了配置Pascal(!)。代码,直到我们意识到没有办法实现可扩展模块,因为Pascal没有等价的函数指针。

有一个版本的Sloth,称为Lemur,它处理版本控制,但我将在稍后描述它。

懒惰是可用的,如果你们中的任何人在博客上认为这可能是有用的。它目前没有被使用或维护,但是我们有标准C的源代码(不,我们没有愚蠢到把它结构成Sloth模块)。懒惰模块不如Python模块方便。Python模块具有大多数相同的功能(例如,按正确的顺序导入模块),没有第三方应用程序带来的不便。开发懒惰换Python是没有意义的。

另一方面,信不信由你,C语言仍然被广泛使用,如果你正在使用它,你会想要模块化。也许斯劳思可以帮助你把几十年前的技术更新几十年。