什么是G#
G#是我在过去几个月里构思出来的一种新的程序设计语言。其目的是生成类型安全的代码,这些代码能够在编译时或运行时被注入(Inject)到一个代码基(Code Base)中。其语法是C# 2.0的一个超集。和其他代码生成技术与工具(如CodeSmith,一种伟大的工具/语言)不同,G#并不打算生成用作起始点(Starting Point)或用于消费(Consumption)的代码。取而代之,G#使用了面向方面的程序设计(AOP)技术来向客户代码中注入代码。我们会快速地介绍一下AOP,因为它对很多开发者来说还是崭新的。
AOP
AOP或称面向方面的软件开发(AOSD)于1997年在Xerox Parc创建,是一种相对先进的软件典范(Paradigm)。其思想很简单,通过使开发者每次只关注一个问题域来降低软件开发的复杂性。换句话说,人们在尝试解决一个业务问题(比如在互联网上销售产品)时无需考虑安全、线程、登录、数据访问和其他领域的问题。这被称为关注点的分离(Separation of Concerns)。通过分离这些领域或者方面,某一特殊方面的专家可以开发能够解决该方面问题的最好的解决方案,因此开发者无需再去掌握所有的行业。这样就有望产生健壮并且功能完善的软件,因为开发者只需做一名“软件问题域”的专家。
AOP通过定义方面(也就是一组行为)来开始,然后将代码注入到适当的方法中去。每个代码注入点都被称作是一个结合点(Join Point)。让我们以安全为例。“所有的输入都是邪恶的”是安全界的一条曼特罗(Mantra,咒语)。对抗这一难题的一种做法是,要求所有的开发者编写代码时都要在使用数据之前检查是否有恶意的输入。开发者们很可能会开发一个辅助方法用来解决这一问题,然后所有的开发者都会在他们的代码中简单地调用这个辅助方法。AOP可以解决这一问题,它抽取这些相同的辅助方法并创建一个方面,然后将其注入到需要对用户输入进行检查的地方。这个过程称为编排(Weaving)。我们没有简单地定义一个将会收到“邪恶输入”方面的位置列表,而是定义了将要使用的一组标准(Criteria)。既然是这样,我们就希望除方面之外能够注入所有带有参数的公共属性、方法和构造器。比起创建一个列表,这样做的好处是开发者们无需再凭借他们的记忆来将需要对输入进行检查的方法添加到列表中。
相对于你所熟悉的AOP语言如AspectJ,G#并没有单独的编排文件:编排被集成到了语法当中。对于大多数程序员来说,别人可以将代码注入到他们的代码基之中,这无疑是一种容易引起恐慌的建议。为了解决这一问题,G#包含了一个用来处理这一问题的安全模型,并且允许程序员来控制哪些人可以注入代码以及可以注入什么样的代码,这将放在后面进行讨论。在我们深入之前先来看一些基础要素:
基础
public class Client
{
public Client()
{
Messenger(“Hello World”);
}
private void Messenger(string message)
{
Console.WriteLine(message);
}
}
public generator Rename
{
static generation ChangeIt : target Client.Messenger(string message)
{
pre
{
string oldMessage = message;
message = “Hello G#”;
}
post
{
message = oldMessage;
}
}
}
尽管这个例子没有任何用途,但它演示了G#的大量特性。首先,Client类使用了标准的C#语法——这在G#中是有效的,它只是简单地向控制台输出了消息“Hello World”。这个类定义下面是G#中新增的语言构造,称作生成器(Generator)。现在只需认为生成器是所有用于定义“如何生成代码”的代码的容器即可,这和类(Class)类似。Rename是这个生成器的名字,就好像Client是类的名字一样。接下来定义了一个名为ChangeIt的生成(Generation)。生成和方法类似,每次调用它都会执行一些动作,不同的是在调用生成的时候会通常产生代码。注意ChangeIt有一个目标(Target),在这里是来自Client类的Messenger方法。目标可以是任何(语言)构造,并且还可以包括通配符和正则表达式来指定一组项目作为目标。这表示由该生成所发出(Emit)的所有代码都将被注入到Messenger方法中。关键字pre规定了其后面花括号中定义的所有代码都将被注入到Messenger方法体中定义的代码之前。关键字post规定了其后面花括号中定义的所有代码都将被注入到Messenger方法体中定义的代码之后。因为用关键字static标记了这个生成,因此代码的实际注入是编译过程的一部分,理解这一点很重要。程序员将无法看到Messenger方法的变化,除非使用ildasm或Reflector来检查Messenger方法。此外还有一个目前还只是梦想的特性,就是能够生成动态的Region,这样在Visual Studio .NET中就能打开它来检查生成器都在客户环境中生成了哪些代码。稍后我们将讨论其他类型的生成。
private void Messenger(string message)
{
// From ChangeIt pre block.
string oldMessage = message;
// From ChangeIt pre block.
message = “Hello G#”;
// From the Messenger method body.
Console.WriteLine(message);
// From ChangIt post block.
message = oldMessage;
}
这个方法因此将向控制台打印“Hello G#”,然后再将message字符串改回最初传入的消息。注意在.NET中字符串是不可变的,因此实际上是不能改变一个字符串所包含的内容的。因此通过在post块中将message改回初始的消息以保护Messenger方法外的“Hello World”消息并不是必须的,但是对于在Messenger方法体中执行的任何代码来说,后置的注入代码都是很重要的。这里出现的一个逻辑问题是,在后置条件(Post Condition)之后,Messenger方法体中的代码究竟什么时候执行呢?这个问题完美地引出了下一节。
生成器的继承
我们上面的例子表明,生成器就是生成的包容器,但是其中还可以包含类能够包含的所有成员(如方法、属性、域、事件等等)。此外可见性和其他修饰符如virtual也可以用于生成。因此,生成器是面向对象的,并且可以彼此继承。这样做的原因和类类似:这允许基生成器定义一个基本的注入行为,并由子生成器定义更多的特殊的行为。
public class Client
{
protected string message;
public Client()
{
this.message = “Hello World”;
Messenger(this.message);
}
private void Messenger(string message)
{
onsole.WriteLine(message);
}
}
public generator Base
{
protected virtual generation ChangeIt : target Client.Messenger(*)
{
pre
{
string message = “Hello G#”;
}
post
{
this.message = message;
}
}
}
public generator Sub : Base
{
protected override generation ChangeIt : target Client.Messenger(string message)
{
pre
{
base.pre();
message = capture.message;
}
post
{
capture.message = message;
base.Post();
}
}
}
下面给出了发出的Messenger方法。我们来分解一下这些代码。Sub生成器从Base生成器派生而来,并且重写了“基类”中的“方法”ChangeIt。“基类”中使用星号(*)定义了一个目标,它可以被任何参数取代,这意味着它的目标可以是Client类中Messenger的所有重载形式。稍后我们将介绍定义目标的细节。凭经验就可以知道一个基本的规则是,在重写的生成中必须为目标指定更多的特性。在代码的另外一部分中,我们使用了关键字base来访问基生成器的pre和post,因此我们可以决定是在Base生成器发出代码之前还是之后发出Sub生成器的代码。
private void Messenger(string message)
{
// Base
string capture.message = “Hello G#”;
// Sub
message = capture.message;
Console.WriteLine(message);
// Sub
capture.message = message;
// Base
this.message = capture.message;
}
捕获
关键字capture用于引用在同一个生成的作用域中定义的变量,即使这个变量定义在基生成器中。能够访问这些变量的原因是,所有生成的代码都将位于相同的作用域中。在访问被捕获(Capture)的变量时,关键字capture并不是必需的,但这里的Messenger方法使用了同名的变量,在这种情况下,就需要关键字capture来解决混淆问题。变量message定义在Base生成器的ChangeIt生成中,而其目标Messenger方法中也有可能定义同名的参数,因为我们在定义中使用了星号(*)通配符。这种请况很可能发生,因为生成中可以定义局部变量,并且稍后在其目标方法的重载中也可以定义同名的局部变量。如果G#不对其采取行动的话,当目标方法中定义了和生成中的局部变量同名的变量时,就会引发一个编译错误。
分节符
为了指出如何发出代码,G#提供了能够通过执行代码来取代发出代码。这通过“§”符号来实现,该符号称作分节符(Section Sign)。该符号在Times New Roman字体中是这样的:§,而在Courier New字体(译注:原文是Courier字体,这里为了同一代码格式使用了Courier New字体,两者非常相似)中是这样的:§。当在代码中放置了§的时候,其后的代码将被执行,而不是被发出:
pre
{
§ for(int i = 0; i < 10; i++)
§ {
Console.WriteLine(i);
§ }
}
绿色高亮的代码在编译期间将被执行而不是被发出。从这个pre块发出的代码是这样的:
Console.WriteLine(0);
Console.WriteLine(1);
Console.WriteLine(2);
Console.WriteLine(3);
Console.WriteLine(4);
Console.WriteLine(5);
Console.WriteLine(6);
Console.WriteLine(7);
Console.WriteLine(8);
Console.WriteLine(9);
Console.WriteLine(10);
注意当这几行代码被发出时,“i”被它的整数值取代了。G#知道如何注入基本类型如int和float的值,但他无法发出类或其他自定义的复杂类型。如果§后跟了一个方法,该方法的返回值类型必须是基本类型、void或emit,如果是其他类型,则编译过程将会破坏返回的所有东西。我们将在下一节里解释关键字emit。我从来没有见过哪个键盘上有§符号,不过可以通过定义组合快捷键来产生这个符号,我选择“Ctrl+l”(小写的L)来在Word里输出这个符号,并且在Visual Studio .NET中为这个快捷键组合写了一个宏来输出这个符号。
关键字emit
我们已经讨论了如何使用关键字pre和post来发出代码,但G#中有更丰富的方法来指定如何以及在哪里发出代码。其中一种方法就是像使用pre和post那样使用关键字emit:
emit
{
Console.WriteLine(“Hello G#”);
}
代码“Console.WriteLine(“Hello G#”);”会在哪里发出?它将在其基生成的emit块中发出。[(That reminds be of the definition of a normal)]OK,那么pre和post实际上也是emit块,只不过它们定义了发出代码的位置(方法体的前面和方法体的后面)。对于上面的代码片断,我们需要提供一个上下文环境来说明一下这些代码是在哪里发出的。
...
pre
{
§ Counter();
}
...
void Counter()
{
emit
{
Console.WriteLine(“The emit keyword in action”);
}
}
当一个带有该pre块的生成被编译时,它会调用Counter方法,因为Counter()的前面有§符号。在Counter方法中,关键字emit用于注入对Console.WriteLine的调用。emit块将会用块中的代码来取代对Counter()的调用。一个方法中emit块的数量没有任何限制,并且可以在emit块中使用§。
此外,emit只是对G#框架(G# Framework)中定义的Emit类型的一个映射,因此我们可以创建emit的实例。
pre
{
§ DisplayParts();
}
...
public emit DisplayParts()
{
emit partOne, partTwo;
partOne
{
§ Injector(partTwo);
Console.WriteLine(“Part One”);
§ partTwo.Emit();
}
return partOne.Emit();
}
private void Injector(emit target)
{
target
{
Console.WriteLine(“Injection...”);
}
}
在上面的代码片断中,我们在DisplayParts生成的定义中创建了两个emit对象partOne和partTwo。然后我们使用partOne加花括号定义了一个emit块。花括号之间的所有代码都将被发出到partOne的局部存储(Local Store)中,当我们在partOne对象上调用Emit方法时,将会返回这个局部存储。最后,注意该代码段的pre块中调用了返回值类型为emt的DisplayParts。[Since the emitted code is not caught it is emitted into the pre block.]
目标
我们已经探讨了当以一个方法为目标时如何使用关键字pre和post,但除此之外,G#还定义了一些关键字以使用其他语言构造作为目标。下面的表格给出了其他能够发出代码的关键字和它们的描述。为这些关键字指定目标构造时也可以使用通配符,参见后面的示例:
关键字 描述
class 注入目标命名空间中所有的类
namespace 注入目标命名空间中所有的命名空间
set | get 注入目标所定义的所有set和get区域
generator 注入目标所定义的所有生成器
generation 注入目标所定义的所有生成
property 注入目标所定义的所有属性
method 注入目标所定义的所有方法
public generator Base
{
protected virtual generation ChangeClient : target Client
{
property public string *
{
get
{
post
{
Console.WriteLine(value);
}
}
set
{
pre
{
Console.WriteLine(value);
}
}
}
method (public | protected) * Cl*(*)
{
Console.WriteLine(“Cl* Method Targeted”);
}
}
}
这里我们注入了所有类型为string而名字任意的属性。我们还在get访问器中使用了关键字value,该关键字在G#中表示由目标代码的get访问器所返回的值。在这里使用pre和post与在方法中的用法无异。接下来的关键字method定义了我们将要注入的所有公共的和受保护的方法,其中两个星号(*)分别表示返回值类型任意并且方法的名字是以“Cl”开头、后跟任意多个任意的字符。(译注:实际上是3个星号,后面括号里那个表示该方法能够带任意多的参数。)在名字中还可以使用“英镑($)”符号作为通配符,表示任意的一个字符。注意到这一点很重要:Client类中所有满足约束条件的成员都会被注入。
自适应生成
第二种生成的类型是自适应生成(Adaptive Generation),只是简单地把一个生成前面的关键字static换成adaptive。自适应生成在运行时生成并且注入代码,因此它可以检查对象的状态以指导生成。
比起静态生成,自适应生成的优势在于第三方也可以提供生成框架和组件。第三方开发者可以通过创建幻象目标(Phantom Target)来以他们一无所知的代码基作为目标。幻象目标并不存在于生成框架或目标框架中。当开发者希望使用一个第三方的生成器时,他们可以加入幻象的命名空间、类、方法并将生成的代码重定位到他们的代码基中适当的位置。 public class Client
{
protected string message;
public Client()
{
this.message = “Hello World”;
Messenger(this.message);
}
public string Message
{
get
{
return this.message;
}
}
private void Messenger(string message)
{
Console.WriteLine(message);
}
}
// Phantom Target
namespace ThirdParty.Security
{
public adaptive generator Input : target Client
{}
}
程序集:
// Third Party generator
public generator Security
{
protected adaptive generation CheckInput
: target ThirdParty.Security.Input
{
property public string *
{
get
{
pre
{
value = ValidateInput(value);
}
}
}
method public * *(all string *(input))
{
pre
{
input = ValidateInput(input);
}
}
}
}
在上面的代码中,我们定义了一个Client类、一个第三方生成器Security和一个幻象目标命名空间ThirdParty.Security。类和幻象目标被定义在一个程序集中,而第三方生成器在另外一个程序集中提供。第三方定义了所有类型为string的公共属性在返回之前都要调用ValidateInput方法。它还定义了所有返回值类型为string的公共方法在执行任何代码前都要对其类型为string的参数调用ValidateInput。G#中的关键字all表示对于作用域内所有符合标准的参数都要做这件事情。星号(*)表示参数的名字可以是任意的,我们必须将想要引用的实参的名字放在圆括号中,以告诉编译器我们正在使用这个名字,但我们不希望将它作为标准的一部分。
现在的CLR能够在运行时动态地注入IL代码,这发生在程序集加载时,通过Profiler API完成。然而这种途径还存在着一系列的安全问题,因为它禁用了CAS,因此还需要深入的研究才能找到一种切实可行的解决方案。我们将在下面描述这是如何完成的。 CAS和注入特性
现在已经有望解决注入代码所引发的安全问题了。G#的安全模型能够确保只有你希望他注入代码的人才能注入代码,并且这些代码只能限制在你所允许的代码访问安全(CAS,Code Access Security)许可中。通过使用元数据,你可以声明你授予注入代码的权限。这仍需要定义一种语法并加入建议[Still need to define this syntax and open to suggestions.]。所有包含生成器和生成的程序集都必须被赋予一个强密钥,然后为目标程序集添加一个带有该公共密钥记号的Injector特性。只有在Injector中指出了强密钥的程序集才能运行和注入代码。
总结
代码生成为我们提供了各种可能性,我们希望G#能够发展成为一个泛型的、类型安全的代码生成语言。