7.4 工具支持下的DSL开发——Xtext
像ANTLR这样的语法分析器生成器,对于在更高级的抽象上开发外部DSL是一大进步。不过我们还是需要直接在文法规则中嵌入目标操作。EBNF规则没有与语义模型的生成逻辑充分分离。假如我们希望为一套文法规则实现多个语义模型,除了复制分析器本身的代码,或者通过文法规则提供的共同抽象基础去派生不同的语义模型,恐怕没有更好的办法。
Xtext是一个建立在Eclipse平台基础上的外部DSL开发框架。它提供了管理DSL完整生命周期的全套工具。Xtext集成了EMF(Eclipse Modeling Framework),并且会以组件形式管理DSL开发过程中产生的所有内容。(关于EMF框架的详情请查阅http://www.eclipse.org/emf。)
使用Xtext时,我们首先编写好语言的EBNF文法。然后EMF根据文法生成以下产物:
基于ANTLR生成的语法分析器;
语言的元模型,基于EMF的Ecore元模型语言;
一个自定义Eclipse编辑器,带有语法高亮、代码提示、代码补全功能,还为我们的模型提供一个可定制的大纲视图。
图7-9展示Xtext对我们定义的EBNF文法规则做了哪些后续处理。
图7-9 Xtext处理文本性质的文法规则过后将生成大量产物。其中Ecore元模型抽象了文法模型使用的语法,是众多制成品里的重中之重
Xtext还拥有各种可以组合使用的代码生成器,能生成满足多种需求的语义模型。本节将再次实现7.2.2节用ANTLR实现过的交易指令处理DSL。这次你会注意到,Xtext全方位的工具支持和它基于模型的开发方式,大大方便了我们管理DSL的演化。我们的实现历程将从语言的定义开始,使用一种基于EBNF的Xtext文法规则。
7.4.1 文法规则和大纲视图
Xtext的DSL文法定义沿用EBNF形式,另外有一些Xtext的附加功能点缀其间。这里不打算详细介绍Xtext的文法规则定义,这方面的信息都可以在Xtext User Guide(见7.6节文献[5])里面查到。代码清单7.6用Xtext文法规则重新定义了7.2.2节的交易指令处理DSL。
代码清单7-6 Xtext文法规则
grammar org.xtext.example.Orders
with org.eclipse.xtext.common.Terminals ➊ 重用默认的词法分析器
generate orders http://www.xtext.org/example/Orders ➋ 生成元模型
Model :
(orders += Order)*; ➌ 多值赋值
Order :
line = Line; ➍ 单值赋值
Line :
buysell = ('buy' | 'sell') security = Security
price = Price account = Account;
Security :
name = ID;
Price :
'@' (
(value = INT)
|
('limitprice' '=' (value = INT))
);
Account :
'for' value = ID;
这段代码与先前的ANTLR版本大体相似。其中一处不同点表现在开头部分,即命令Xtext生成语言元模型的部分➋。假如已经有现成的Ecore元模型,也可以让Xtext将它导入当前工作环境,令模型与文法规则的文本表述同步。7.4.2节会进一步探讨元模型的内部细节。Xtext允许将既有文法混入当前正在定义的规则➊,以此实现文法的重用,这也是一个非常有趣的特点。
Xtext根据我们的文法规则生成默认的分析树(AST)。AST生成之后,文法里的内联赋值成为语法分析器生成的各AST元素之间的联系纽带➌、➍。
除文本性的文法规则之外,Xtext还给出一个大纲视图来展现模型的树形结构。利用大纲视图,可以浏览各模型元素的。图7-10显示根据代码清单7-6定义的文法产生的大纲视图。
图7-10 大纲视图展示模型的层级结构。大纲视图展示每一条规则对应的结构。视图内的元素可以按字母顺序排列,以便查找,选择其中一个元素将打开相应的文本编辑器
图中所见即为模型的默认视图。这个视图最值得称道的特点在于,Xtext允许定制视图的几乎每一个方面。我们可以调整大纲的结构,可以让用户选择过滤部分内容,可以自定义上下文菜单等,只要覆盖默认实现即可。大纲视图是实现语言模型可视化的其中一件工具,与文法的文本表述结合起来,赋予DSL开发过程更加丰富的体验。
7.4.2 文法的元模型
在文本编辑器里写好文法规则以后,让Xtext生成语言制品,它就会根据文法生成完整的Ecore元模型(Ecore包含EMF的核心抽象定义)。我们以文本形式定义的文法,经过元模型的抽象,呈现为一种便于Xtext管理的模型样式。元模型用Ecore元类型(metatypes)来描述文法规则的各个部分。图7-11展示了Xtext文法示例所对应的元模型。
图7-11 交易指令处理DSL的元模型。文法中的每条产生式规则都返回一个Ecore模型元素,如EString
和EInt
注意,在这个元模型里用了EString
、EInt
等元类型来表示文法的AST
,它们都是Ecore提供的核心抽象。
除了元模型,生成器同时还生成用来实例化元模型的ANTLR分析器。元模型控制Xtext提供的全套工具。如需进一步了解元模型的内部细节,请参考Xtext User Guide(参考7.6节文献[5])。
所有必要的制品都生成完毕,连同生成的IDE插件也都安装好之后,就可以在编辑器里编写DSL脚本,享受语法高亮、代码提示、约束检查等智能功能。Xtext在它的信息库中建立了DSL语言完整的EMF模型,因而可以在脚本的呈现方式上增加智能化的手段。图7-12展示了DSL示例的编辑场景。
图7-12 Xtext元模型提供了一个专为编写DSL而设的优秀编辑器。图中代码补全功能提示有效的输入候选内容。从图中还可以看到另一种便利功能——语法高亮功能
现在,我们已经将语言签入了Xtext信息库中。Xtext为我们提供操作DSL语法的手段和界面,并且自动更新其Ecore模型。它还给了我们一棵默认的AST树,以及生成这棵AST的语法分析器。但对于一种实用的DSL来说,这些还不够,我们还需要一个更加精细、完善的抽象——语义模型。DSL设计者希望通过自定义的代码开发来产生语义模型,同时希望这部分代码能与语言的核心模型分离。Xtext的代码生成模板提供了这样的能力,可以妥善地将生成的语义模型与文法规则集成在一起。那么,下面就让我们继续探索Xtext这方面的功能,看看它的代码生成器要用哪些工具来为语义模型生成代码。
7.4.3 为语义模型生成代码
文法规则和元模型都准备就绪,可以编写代码生成器了。代码生成器将处理我们到目前为止所建立的模型,并生成语义模型。有时,我会希望从一套文法生成多个语义模型。以交易指令处理DSL为例,除了生成一个根据用户输入创建交易指令对象集合的Java类,我们可能还希望生成一个JSON(JavaScript Serialized Object Notation)对象集合,用来向数据库传递交易指令数据。理想状态下,这两个语义模型都不与核心的文法规则发生耦合。图7-13展示了整体架构。
图7-13 语义模型应与文法规则分离。一套文法规则可以对应多个语义模型
那么,我们就用Xtext提供的Xpand模板来生成Java代码。
1. 用Xpand模板生成代码
Xpand模板遍历由文法规则形成的AST,然后生成代码。模板需要的第一个输入是元模型,我们通过«IMPORT orders»
提供给它。下面是充当入口的主模板,它负责派发到其他子模板:
«IMPORT orders»
«DEFINE main FOR Model»
«EXPAND Orders::orders FOR this»
«ENDDEFINE»
在这段代码中,orders
是需要导入的元模型名称。每个模板都有一个名称和一个决定模板调用时机的元类型。本例中模板的名称是main
,元类型是Model
(Model
是我们在文法规则中定义的一个文法元素,Xtext将它表示为Ecore元模型中的一个元类型)。这个main
模板非常简单,它的功能仅仅是在识别到Model
符号时,派发到Orders::orders
子模板。代码清单7-7是Orders::orders
模板的定义。
代码清单7-7 生成orders
语义模型的模版
«IMPORT orders» 导入元模型
«DEFINE orders FOR Model»
«FILE "OrderProcessor.java"» 生成Java文件
import java.util.*;
import org.xtext.example.ClientOrder; 自定义Java类
public class OrderProcessor { 待生成的类
private List<ClientOrder> cos = new ArrayList<ClientOrder>();
public List<ClientOrder> getOrders() {
«EXPAND order FOREACH this.orders» 将被替换的模板
return Collections.unmodifiableList(cos);
}
}
«ENDFILE»
«ENDDEFINE»
«DEFINE order FOR Order»
cos.add(new ClientOrder("«this.line.buysell»",
"«this.line.security.name»",
«this.line.price.value»,
"«this.line.account.value»"));
«ENDDEFINE»
当文法被归约到起始符号(即Model
),语言识别结束时,代码清单7-7中定义的代码也在模板推动下生成出来。表7-4就代码生成模板的一部分特性给出了详细解释。
表7-4 Xtext的代码生成模版
特性 | 解释 |
---|---|
可以生成任意的代码。例中生成了一个Java类 | 这个类只对外提供一个方法,返回从DSL脚本解析所得的全部交易指令的集合。集合的元素是单独定义的POJO对象(ClientOrder ),与文法无关 |
在orders 模板内运用了对order 模板的内联展开 | 我们可以在产生式规则内访问文法模型的元素,并使用模板替换这些内联元素的文本。例如«this.line.price.value» 就是一个占位标记,会在DSL脚本完成分析后,被实际的单价替换掉 |
模板一切就绪,我们通过项目的上下文菜单再次运行Xtext的生成器。
2. 执行DSL脚本
假设我们要执行下列DSL脚本:
buy ibm @ 100 for nomura sell google @ limitprice = 200 for chase
这段脚本经过Xtext的处理,最终生成代码清单7-8所示的OrderProcessor.java文件。
代码清单7-8 由代码清单7-7的模版生成的类
import java.util.*;
import org.xtext.example.ClientOrder;
public class OrderProcessor {
private List<ClientOrder> cos = new ArrayList<ClientOrder>();
public List<ClientOrder> getOrders() {
cos.add(new ClientOrder("buy", "ibm", 100, "nomura"))
cos.add(new ClientOrder("sell", "google", 200, "chase"))
return Collections.unmodifiableList(cos);
}
}
按照Xtext的做法,定义文法规则和构建语义模型这两个方面可以充分分离。文法规则的定义由智能文本编辑器负责,用可定制的大纲视图作为补充。语义模型的实现则在各种代码生成器的控制之下,通过元模型与语法分析器联系在一起。
最后谈谈开发外部DSL时,像Xtext这样文本与可视化环境相混合的方式有哪些优缺点。
3. 优点和缺点(优点是主要的)
Xtext提出了一种创新的外部DSL开发方式,以其丰富的工具,增强了DSL传统的文本表示。我们仍然需要编写EBNF格式的文法规则,但在后台,Xtext不但为我们生成ANTLR语法分析器,更将我们的文法规则抽象为一个元模型。
Xtext的全套工具都置于一个围绕元模型建立起来的架构内。基于Xtext的开发方式除了增强DSL编辑能力,用它的Xpand模板机制来生成自定义代码,还能有效地分离语义模型和文法规则。
总而言之,在EMF框架支持下,Xtext提供了优异的外部DSL开发体验。只有一点需要小心,即Xtext对Eclipse平台的依赖。不过也只有IDE集成开发环境依赖Eclipse,语法分析器、元模型、序列化组件、链接组件、Xpand模板等运行时组件,都可以在任意的Java进程中使用。Xtext的种种优点使其成为我们开发外部DSL的首选框架。