Modula-3 简介

作者:Geoff Wyant

假设您想要为 Linux 开发一个大型、复杂的应用程序。该应用程序将是多进程的,可能是分布式的,并且肯定需要有一个 GUI。您希望相当快速地构建此应用程序,并且希望它相对没有错误。

您可能会问自己的首要问题之一是“我应该使用什么编程语言和环境?” C 可能是个不错的选择,但可能不适合这个项目。它的扩展性不如您期望的那么好,而且 C 的多进程/分布式编程工具也不到位。您可能会考虑 C++,但该语言相当复杂。此外,您和其他人从过去的经验中发现,相当多的时间都花在了调试微妙的内存管理问题上。

还有一种替代方案,即数字设备公司系统研究中心 (SRC) 的 Modula-3 编程系统。Modula-3 是一种现代的、模块化的、面向对象的语言。该语言具有垃圾回收、异常处理、运行时类型、泛型以及对多线程应用程序的支持。该语言的 SRC 实现具有本机代码编译器;增量的、分代的、保守的、多线程的垃圾回收器(呼!);最小的重新编译系统;调试器;丰富的库集;对构建分布式应用程序的支持;分布式的面向对象脚本语言;以及,最后,用于分布式应用程序的图形用户界面构建器。简而言之,是上述应用程序类型的理想环境。此外,该系统以源代码形式免费提供,并且还提供预构建的 Linux 二进制文件。

本文的其余部分将介绍该语言的相关特性,并概述库和工具。

Modula-3 语言的基础知识

Modula-3 语言的主要目标之一是简单易懂,但又适合构建大型、健壮、长寿命的应用程序和系统。语言设计过程是一个整合而非创新的过程;也就是说,目标是整合来自几种不同语言的思想,这些思想已被证明对构建大型复杂系统很有用。

Modula-3 的特性

您可以将 Modula-3 视为从 Pascal 开始并重新发明它,使其适合实际的系统开发。从类似 Pascal 的基础开始,集成了被认为编写实际应用程序所必需的特性。这些特性大致分为两个领域:使语言更适合构建大型系统的特性,以及使“机器级”编程成为可能的特性。实际应用程序需要这两者。

支持大型系统开发

Modula-3 中有几个特性支持大型系统的构建。首先是将接口与实现分离。这允许系统随着实现的演进而演变,而不会影响这些接口的客户端;没有人依赖于您如何实现某些东西,只依赖于您实现什么。只要“什么”保持不变,“如何”就可以根据需要进行更改。

其次,它提供了一个简单的单继承对象系统。关于多重继承 (MI) 的正确模型是什么,存在相当多的争议。我构建了广泛使用多重继承的系统,并为支持 MI 的语言实现了编程环境。经验告诉我,MI 会极大地复杂化一种语言(在概念上和实现方面),并且还会复杂化应用程序。

Modula-3 对对象的定义特别简单。在 Modula-3 中,对象是堆上的记录,带有关联的方法套件。对象的数据字段定义状态,而方法套件定义行为。Modula-3 语言允许将对象的状态隐藏在实现模块中,而只有行为在接口中可见。这与 C++ 不同,在 C++ 中,类定义列出了成员数据和成员函数。C++ 模型向全世界公开了本质上是私有信息(即状态)的信息。使用 Modula-3 对象,应该私有的内容可以真正做到私有。

Modula-3 中最重要的特性之一是垃圾回收。垃圾回收真正实现了健壮、长寿命的系统。如果没有垃圾回收,您需要定义关于谁拥有存储空间的约定。例如,如果我向您传递一个指向结构的指针,您是否允许将该指针存储在某处?如果是这样,将来谁负责释放该结构?是你还是我?程序员最终会采用诸如显式使用引用计数之类的约定来确定何时可以安全地释放存储空间。不幸的是,程序员不太擅长遵守约定。最终结果是程序出现存储泄漏,或者同一块存储空间被错误地用于两个不同的目的。此外,在错误情况下,可能很难释放存储空间。在 C 中,如果正在展开的过程没有机会清理,则 longjmp 可能会导致存储空间丢失。C++ 中的异常处理也存在相同的问题。一般来说,在面对失败时手动回收存储空间非常困难。在语言中加入垃圾回收消除了所有这些问题。更好的是,SRC 实现的 Modula-3 提供的垃圾回收器具有出色的性能。这是多年生产使用和调整的结果。

大多数现代系统和应用程序都具有某种形式的异步性。当然,所有基于 GUI 的应用程序本质上都是异步的。基于 GUI 的应用程序的输入由用户驱动。多进程和多机器应用程序本质上也是异步的。鉴于此,令人惊讶的是,很少有语言为管理并发提供任何支持。相反,它们“将其留给程序员”。通常,程序员通过使用计时器和信号处理程序来做到这一点。虽然这种方法对于相当简单的应用程序来说已经足够了,但随着应用程序复杂性的增加,或者当应用程序使用两个不同的库时,它很快就会崩溃,这两个库都试图以自己的方式实现并发。如果您曾经使用 Xt 或 Motif 编程,那么您就会意识到嵌套事件循环的问题。需要某种标准的并发机制。

Modula-3 提供了这样一个用于创建线程的标准接口。此外,该语言本身还包括对管理锁的支持。SRC 实现中提供的标准库都是线程安全的。Trestle 是一个提供 X 接口的库,它不仅是线程安全的,而且本身也使用线程在后台执行长时间的操作。使用基于 Trestle 的应用程序,您可以创建一个线程来执行一些可能长时间运行的操作,以响应鼠标按钮的单击。此线程在后台运行,而不会占用用户界面。这比尝试使用信号处理程序和计时器完成相同的事情要简单得多,也更不容易出错。

泛型接口和模块是重用的关键。主要用途之一是定义容器类型,例如堆栈、列表和队列。它们允许容器对象独立于所包含实体的类型。因此,只需要定义一个“Table”接口,然后实例化该接口以提供所需的“Table”类型,无论是整数表、浮点表还是其他类型的表。Modula-3 泛型比 C++ 参数化类型更简洁,但提供了大致相同的灵活性。

支持机器级编程

从 C 中吸取的重要教训之一是,有时实际系统需要基本上在机器级别进行编程。这种能力已很好地集成到 Modula-3 中。

任何标记为不安全的模块都可以完全访问依赖于机器的操作,例如指针算术、不受约束的内存分配和释放以及依赖于机器的算术。这些功能在 Modula-3 I/O 系统的实现中得到了利用。I/O 系统的最低级别被编写为大量使用依赖于机器的操作,以消除瓶颈。

此外,可以导入现有的(非 Module-3)库。许多现有的 C 库广泛使用依赖于机器的操作。这些可以作为“不安全”的接口导入。然后,可以在这些接口之上构建更安全的接口,同时仍然允许需要这些库的应用程序访问这些库的不安全特性。

Modula-3 编程系统

您有多少次避免在 C/C++ 系统中更改基本头文件,因为您不想重新编译整个世界?您有多少次重组头文件,不是因为这是正确的事情,而是因为您需要在每次更改后减少重新编译的次数?

Modula-3 的 SRC 实现对此问题有一个相当优雅的解决方案。如果接口中的某个项发生更改,则只会重新编译依赖于该特定项的单元。也就是说,依赖关系是按项记录的,而不是按接口文件记录的。这意味着每次进行一组更改后,重新编译的次数要少得多。

m3gdb 是 GDB 的一个版本,经过修改后可以理解和调试 Modula-3 程序。m3gOb 的一个优点是它理解 M3 线程,并允许您在调试问题时从一个线程切换到另一个线程。

同样令人兴奋的是 Siphon。Siphon 是一组服务器和工具,用于支持多站点开发。基本思想是您可以创建一组包。一个包只是源代码文件和文档的集合。Siphon 提供了一个简单的模型,用于检出和检入包。检出一个包会锁定它,以便其他人无法检出和修改内容。这听起来可能不是很令人兴奋。Siphon 所做的令人兴奋的事情是在包被检入后自动将修改后的文件传播到其他站点。它以这样一种方式做到这一点,即包永远不会处于“半途而废”的状态;即部分源文件已被复制,但尚未全部复制。此外,它是在面对失败的情况下做到这一点的。多站点开发真正有趣的部分之一是确保每个人都拥有最新的源文件副本。当通信链路可能中断时,这一点尤其困难。Siphon 为您解决了所有这些问题。如果您参与多站点开发,像 Siphon 这样的系统可以为您节省大量工作。顺便说一句,Siphon 不仅限于 Modula-3 源代码文件。它可以管理任何类型的源代码或文档文件。

一种好的、简单的面向对象语言是一个不错的起点,但这本身可能不足以提供足够的动力来考虑一种新语言。真正的生产力来自于存在良好的可重用库。这是 SRC Modula-3 系统的真正优势之一。它提供了一大套“工业强度”库。这些库中的大多数是多年使用和改进的结果。它们的文档记录比大多数商业库都好或更好。

Libm3

Libm3 是 Modula-3 的主力库。它是 Modula-3 相当于 libc(标准 C 库),但它要丰富得多。

Libm3 为 I/O 定义了一组抽象类型;这些类型称为“readers”和“writers”。Readers 和 writers 提供了一个抽象接口,用于写入“streams”。Streams 表示缓冲的输入和输出。Stdin、stdout 和 stderr 表示大多数程序员熟悉的流。streams 包的设计旨在使添加新型流变得容易。

除了标准的 I/O 流之外,还可以打开文件流和文本流(即,字符字符串上的流)。还有一组用于非缓冲 I/O 的抽象。除了 File 类型之外,还有 Terminal 和 Pipe。Fmt 接口提供了 C 的 printf 的类型安全版本。C 程序中错误的一个重要来源是将一种类型的数据传递到 printf 中,但试图将其格式化为另一种类型的数据。Fmt 接口的设计旨在具有 printf 的灵活性,但又不引入其问题。

Libm3 还将一组简单的“容器”类型定义为泛型接口。基本容器类型包括表、列表和序列。表是关联索引数组。列表类型是熟悉的“lisp”样式列表。序列是一个整数(实际上是 CARDINAL)寻址的数组,其大小可以增长。

最后,Libm3 提供了一种称为 Pickles 的简单持久性机制。编写代码以将复杂的数据结构转换为某种磁盘格式以及从某种磁盘格式转换出来,既繁琐又容易出错。许多程序员除非绝对必要,否则不会这样做。使用 Pickle 包,您不再需要编写这种代码。由于运行时知道内存中每个对象的布局,因此它可以使用此信息来遍历一组结构并将它们从流中读取或写入流中。程序员不必编写特定于对象的代码来将对象写入流,尽管如果知道更好的表示形式,他或她可以这样做。例如,哈希表的程序员可以选择在表的大小低于某个大小时写出单个条目。

Trestle 和 VBTKit

大多数用户界面 (UI) 都是事件处理程序、计时器和信号的混乱组合。这是因为它们需要处理在任意时间传入的用户输入,它们需要处理刷新屏幕,并且它们必须确保长时间运行的操作不会导致应用程序的窗口冻结。所有这些约束使得使用传统语言和库开发用户界面非常困难。

SRC Modula-3 实现提供了一个名为 Trestle 的 UI 库。关于 Trestle 最值得注意的是,它是高度并发的。它被编写为广泛使用线程并在多线程环境中使用。

这大大简化了用户界面的开发,因为您不再需要处理事件循环。事件循环本质上是“穷人的多线程”。由于语言和库支持一流的线程,因此可以使用这些线程来代替。如果与按钮关联的操作可能需要很长时间,则该操作只需派生一个线程来处理大部分操作。此线程可以任意调用 Trestle 以使用新结果更新屏幕。Trestle 通过明智地使用锁来保护自身。

Trestle 提供了两种类型的对象:图形对象(例如路径和区域)以及一组基本的用户界面对象。这些用户界面对象被称为“VBT”。它们在 Trestle 中扮演的角色与 X intrinsics 在 X 工具包世界中扮演的角色相同。它们定义了如何在不同的“widgets”之间分配屏幕空间。Trestle 在其基本 UI 项目集中提供了一组简单的按钮和菜单。

VBTKit 是一个构建在 Trestle 之上的更高级别的工具包。VBTKit 提供了更广泛的 UI 对象种类。它还提供了类似 Motif 的 3-D “外观和感觉”(在彩色显示器上)。相同的接口可以在单色显示器上使用而无需更改,但没有适当的视觉外观。VBTKit 提供了通常的滚动条、按钮、菜单项、数字 I/O 对象等等。

FormsVBT 和 FormsEdit

FormsVBT 是一个用户界面管理系统 (UIMS),其结构为库,并且构建在 VBTKit 之上。FormsVBT 提供了一种简单的语言来描述用户界面的布局,以及该语言的事件解释器。布局语言遵循“boxes and glue”模型。Boxes 包含一组 UI 对象。VBox 将这些对象排列在垂直显示中,而 HBox 将它们排列在水平显示中。Glue 用于强制视觉显示中项目之间留出一定的空间。正如您可能期望的那样,boxes 可以任意嵌套。

FormsVBT 库允许您指定回调来处理输入。您编写的 FormsVBT 规范指定了用户界面的“语法”;您编写的事件处理程序提供了“语义”。

FormsEdit 是一个构建在 FormsVBT 之上的简单 UI 创建工具。它以图形方式和源代码文本形式读取和显示以 FormsVBT 语言编写的 UI 规范。它还允许交互式修改源代码。

m3tk

有时您只需要为项目开发一些特定于语言的工具。问题是很少有语言实现为您提供任何支持来做到这一点。很多时候,您拥有的只是一个公共领域的 YACC 语法,您必须对其进行修改,然后从那里开始构建。这就是 m3 tk 的用武之地。它提供了一个完整的工具包,用于解析 M3 源代码文件;以及生成和操作 M3 源代码的抽象语法树表示。因此,可以相对容易地构建 M3 特定工具。事实上,网络对象(见下文)桩生成器就是使用它构建的。

网络对象

分布式系统在 90 年代迅速普及。大多数语言对分布式编程几乎不提供或不提供支持。大多数分布式应用程序仍然直接构建在套接字之上,或者使用提供简单流或 RPC 接口的库。这些库与语言的集成度很差,并且在语言和分布式系统之间引入了严重的障碍。

网络对象是 Modula-3 的 SRC 实现中的一种设施,它允许 Modula-3 对象跨地址空间和机器导出。使用网络对象,程序无法分辨它正在操作的对象是它在自己的地址空间中创建的对象,还是在另一个地址空间中创建和存在的对象。这为开发分布式应用程序提供了非常强大的机制。

要将对象类型转换为网络对象,该对象必须(直接或间接)从 NetObj.T 类型继承。该对象不能包含任何数据字段。然后,包含声明的接口通过一个名为桩编译器的工具运行。这将生成处理网络交互所需的所有代码。这就是允许对象在网络中传递所需的全部内容。非常简单。下面是一个网络对象的示例。它定义了一个名为“File”的接口,该接口定义了对文件的操作,以及该接口的实现。

INTERFACE File;
TYPE T = NetObj.T OBJECT
METHODS
getChar(): CHARACTER;
putChar(c: CHARACTER);
END;
END File;
INTERFACE FileServer;
IMPORT File;
TYPE T = NetObj.T OBJECT
METHODS
create(name: Text): File.T
open(name: Text): File.T
END;
END FileServer;

上面的代码定义了两种类型:File.T,它是一个具有两个方法的对象,用于获取和放置单个字符;以及 FileServer.T,一个管理文件对象的对象。某处的服务器定义了这些抽象类型的具体实现。

MODULE FileServerImpl;
IMPORT File, FileServer;
TYPE FileImpl = File.T
...state for a file...
OVERRIDES
getChar := GetChar;
putChar := PutChar;
END;
TYPE FileServerImpl = FileServer.T OBJECT
...state for a file server...
OVERRIDES
create := Create;
open := 0pen;
END;
VAR
fileServer := NEW(FileServerImpl);
BEGIN
NetObj.Export("FileServer",
END;
MODULE FileServerClient;
IMPORT File, FileServer;
VAR
fileServer := NetObj.Import("FileServer file");
file := fileServer.Create("someFile");
BEGIN
file.putChar('a');
END FileServerClient;

在上面的代码中,FileServerImpl 创建了一个文件服务器的实例并将其放入名称服务器中。(NetObj.Export 调用执行此操作。)模块 FileServerClient(将在不同的地址空间或机器中运行)导入文件服务器实现。这会将有效的 Modula-3 对象返回给客户端。从那时起,客户端就像调用本地方法一样调用它的方法。然后,它创建一个 File 对象,并开始向其中添加字符。

如果您使用 SunRPC 或 DCE 进行过任何开发,您将立即体会到这比在这些系统中的任何一个之上进行编程要简单得多。网络对象在范围上与这些系统相似,但它与编程模型紧密集成,而不是作为集成不良的附件。

在网络对象之上构建了两个有趣的系统。第一个是 Obliq,它是一种面向对象的分布式脚本语言。Obliq 可以调用现有的 Modula-3 包。您还可以创建 Obliq 对象并将它们交给在其他机器上运行的其他程序。Obliq 在范围上类似于 Telescript 或 TCL-DP(带有分布式编程扩展的 TCL)。另一个系统是 Visual Obliq,可以将其视为“用于 Modula-3 的分布式 Visual Basic”。它包括一个交互式的图形应用程序构建器。回调由 Obliq 脚本处理。这使其成为用于原型设计和构建分布式应用程序的非常强大的工具。它也可以用作有趣的协作应用程序的基础。

Modula-3 的一些个人经验

我们的小组使用 Modula-3 大约六个月了,尽管我从 1989 年左右就参与其中。我们的小组由经验丰富的 C/C++ 程序员组成。其中两人自 1.2 版本以来就参与了 C++,我们两人从事 C/C++ 编程环境的实现工作。

我们使用 Modula-3 的经验完全是积极的。小组成员认为,与使用 C++ 相比,该语言、库和支持工具使我们效率更高。这些库的质量更高,文档记录比许多商业库更好。为了完成给定的任务,我们编写的代码比以前少得多,并且我们相信代码的质量更高。我们将此归因于两件事。首先是语言干净简洁;理解如何完成某件事所需的脑力劳动要少得多。第二个促成因素是更多地使用库。我们首先查看标准库是否提供或接近提供的功能,而不是编写某些功能。大多数时候,我们都会找到足够接近的东西,可以将其作为起点。

在更个人的层面上,我很少见到一种语言、工具和库集如此巧妙地结合了简单性、优雅性和强大功能。

结论

Modula-3 和 SRC 的实现为开发 Linux 应用程序提供了极好的基础。它是一个旨在应对 90 年代编程挑战的系统。该语言干净、简单且功能强大。提供的库几乎是无与伦比的。对分布式编程的支持是可用的最佳支持之一。

考虑 Modula-3 和 SRC 实现的一种方法是将“类似 NeXTStep”的环境带到 Linux。它们都从一种简单的面向对象语言(尽管 M3 更安全、更强大)开始,并在其之上构建有用且复杂的库。当然,Modula-3 的优势在于它可以免费获得并在 Linux 上运行!

Geoff Wyant (geoff.wyant@east.sun.com) 是 SunMicrosystems 实验室的分布式系统研究员。在过去,他曾为 C++ 构建编程环境,从事分布式文件系统和 RPC 系统的工作,并破解操作系统内核。

加载 Disqus 评论