什么是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#能夠發展成為一個泛型的、類型安全的代碼生成語言。