[.NET大牛之路 006] 瞭解 Roslyn 編譯器

精緻碼農發表於2021-08-05

.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 提供的對應功能:

來源:bit.ly/3AKnWyb

  • Parser(解析)階段,根據語言語法對原始碼進行解析,將原始碼轉換為層次化的標記集合,形成語法樹。語法樹 API 用於在原始碼編輯器中格式化、著色和程式碼大綱。

  • Declaration(宣告)階段,分析所有引用和匯入的後設資料,形成層次化的符號表。在編輯器和物件瀏覽器中的 Navigation To 特性使用這個 API。

  • Bind(繫結)階段,對標記集合和符號表進行匹配。編輯器中的 Find All ReferencesRenameQuick InfoExtract 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 類庫,並參照如下程式碼設定 OutputItemTypeReferenceOutputAssembly 屬性:

  <ItemGroup>
    <ProjectReference Include="..\MyGenerator\MyGenerator.csproj"
        OutputItemType="Analyzer"
        ReferenceOutputAssembly="false" />
  </ItemGroup>

執行 ConsoleApp,可以看到控制檯輸出如下內容:

Roslyn 的功能非常強大,這個示例只是演示了 Roslyn 的一個非常簡單的功能和用途。

Roslyn 不只是一個編譯器,還是一個現成的框架,它使得在 .NET 平臺上建立自己的語言服務變得更加容易。你可以使用 Roslyn 編譯器的 API 在 .NET 平臺上開發一個完整的應用程式,甚至建立你自己的 IDE、編寫你自己的編譯器、直譯器或分析器來編譯和執行你自己的程式語言。

相關文章