.NET大牛之路 • 王亮@精緻碼農 • 2021.07.09
維基百科對編譯器的解釋是:編譯器是一種程式,它將某種程式語言編寫的原始碼(原始語言)轉換成另一種程式語言(目標語言)。編譯是從原始碼(通常為高階語言)到能直接被計算機或虛擬機器執行的目的碼(通常為低階語言或機器語言)的翻譯過程。
在 .NET 平臺中,在執行模型的不同階段有兩個不同的編譯器:一個叫 Roslyn 編譯器,負責把 C# 和 VB 程式碼編譯為程式集;另一個叫 RyuJIT 編譯器,負責把程式集中的 IL(中間語言) 程式碼編譯為機器碼。
本文先介紹 Roslyn 編譯器。我們不必深入研究它的工作原理,但要了解它的工作機制,要知道它可以用來做什麼事情。
最初 C# 語言的編譯器是用 C++ 編寫的,後來微軟推出了一個新的用 C# 自身編寫的編譯器:Roslyn,它屬於自舉編譯器。
所謂自舉編譯器就是指,某種程式語言的編譯器就是用該語言自身來編寫的。自舉編譯器的每個版本都是用該版本之前的版本來編譯的,但它的第一個版本必須由其它語言編寫的編譯器來編譯,比如 Roslyn 的第一個版本是由 C++ 編寫的編譯器來編譯的。很多程式語言發展成熟後都會用該語言本身來編寫自己的編譯器,比如 C# 和 Go 語言。
在 .NET 平臺,Roslyn 編譯器負責將 C# 和 VB 程式碼編譯為程式集。
大多數現有的傳統編譯器都是“黑盒”模式,它們將原始碼轉換成可執行檔案或庫檔案,中間發生了什麼我們無法知道。與之不同的是,Roslyn 允許你通過 API 訪問程式碼編譯過程中的每個階段。
它的工作機制是管道式的,整個工作管道包含四個階段,每個階段都是一個獨立的模組,每個模組都提供了相應的 API。整合開發環境(IDE)可以利用這些 API 提供方便的工具以提高開發效率,如程式碼高亮、智慧提示、重構工具、效能分析工具等。此外,通過 Roslyn,開發者可以在自己的程式中使用編譯器,將編譯器作為一種服務來使用。
下圖描繪了 Roslyn 工作管道的各個階段和各階段對應的 API,以及各 API 可為 IDE 提供的對應功能:
Parser(解析)階段,根據語言語法對原始碼進行解析,將原始碼轉換為層次化的標記集合,形成語法樹。語法樹 API 用於在原始碼編輯器中格式化、著色和程式碼大綱。
Declaration(宣告)階段,分析所有引用和匯入的元資料,形成層次化的符號表。在編輯器和物件瀏覽器中的
Navigation To
特性使用這個 API。Bind(繫結)階段,對標記集合和符號表進行匹配。編輯器中的
Find All References
、Rename
、Quick Info
和Extract Method
等特性使用這個 API。Emit(生成)階段,生成 IL 託管模組,將一個或多個 IL 託管模組和嵌入資源合併成程式集。編輯器中的
Edit and Continue
利用這個特性完成一次新的編譯。
Roslyn 是少數幾個讓你有機會觀察所有編譯階段和中間結果的編譯器之一,它提供的這些 API 可以為語言服務實現豐富的功能。例如,程式碼高亮使用語法樹,物件瀏覽器使用分層符號表。
下面我們來做一個簡單的示例,利用 Roslyn 提供的 API 來動態生成程式碼。
建立一個控制檯應用程式 ConsoleApp
,編輯 Program.cs
的程式碼如下:
using System;
namespace ConsoleApp
{
partial class Program
{
static void Main(string[] args)
{
HelloFrom("Generated Code");
Console.ReadKey();
}
static partial void HelloFrom(string name);
}
}
再建立一個 .NET Standard 類庫,取名 MyGenerator
,並新增兩個 NuGet 包,專案檔案內容如下:
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>netstandard2.0</TargetFramework>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.CodeAnalysis.Analyzers" Version="3.3.2" />
<PackageReference Include="Microsoft.CodeAnalysis.CSharp" Version="3.10.0" />
</ItemGroup>
</Project>
然後在 MyGenerator
專案中新增一個 Generator.cs
檔案,程式碼如下:
using Microsoft.CodeAnalysis;
namespace MyGenerator
{
[Generator]
public class Generator : ISourceGenerator
{
public void Initialize(GeneratorInitializationContext context)
{
}
public void Execute(GeneratorExecutionContext context)
{
// find the main method
var mainMethod = context.Compilation.GetEntryPoint(context.CancellationToken);
// build up the source code
string source = $@"
using System;
namespace {mainMethod.ContainingNamespace.Name}
{{
public static partial class {mainMethod.ContainingType.Name}
{{
static partial void HelloFrom(string name)
{{
Console.WriteLine($""Generator says: Hi from '{{name}}'"");
}}
}}
}}
";
// add the source code to the compilation
context.AddSource("generatedSource", source);
}
}
}
這裡的 source
是我們的動態組裝的程式碼,在實際應用中還可以從資料庫或文字中讀取程式碼片段。
最後在 ConsoleApp
專案中引用 MyGenerator
類庫,並參照如下程式碼設定 OutputItemType
和 ReferenceOutputAssembly
屬性:
<ItemGroup>
<ProjectReference Include="..\MyGenerator\MyGenerator.csproj"
OutputItemType="Analyzer"
ReferenceOutputAssembly="false" />
</ItemGroup>
執行 ConsoleApp
,可以看到控制檯輸出如下內容:
Roslyn 的功能非常強大,這個示例只是演示了 Roslyn 的一個非常簡單的功能和用途。
Roslyn 不只是一個編譯器,還是一個現成的框架,它使得在 .NET 平臺上建立自己的語言服務變得更加容易。你可以使用 Roslyn 編譯器的 API 在 .NET 平臺上開發一個完整的應用程式,甚至建立你自己的 IDE、編寫你自己的編譯器、直譯器或分析器來編譯和執行你自己的程式語言。