Roslyn 入門:使用 .NET Core 版本的 Roslyn 編譯並執行跨平臺的靜態的源碼

Roslyn 是微軟爲 C# 設計的一套分析器,它具有很強的擴展性。以至於我們只需要編寫很少量的代碼便能夠編譯並執行我們的代碼。

作爲 Roslyn 入門篇文章之一,你將可以通過本文學習如何開始編寫一個 Roslyn 擴展項目 —— 編譯一個類,然後執行其中的一段代碼。


本文是 Roslyn 入門系列之一:

我們希望做什麼?

是否有過在編譯期間修改一段代碼的想法呢?

我曾經在 生成代碼,從 T 到 T1, T2, Tn —— 自動生成多個類型的泛型 一文中提到過這樣的想法,在這篇文章中,我希望只編寫泛型的一個參數的版本 Demo<T>,然後自動生成 2~16 個參數的版本 Demo<T1, T2>, Demo<T1, T2, T3>Demo<T1, T2, ... T16>。不過,在那篇文章中,我寫了一個應用程序來完成這樣的事情。我在另一篇文章 如何創建一個基於命令行工具的跨平臺的 NuGet 工具包 中說到我們可以將這樣的應用程序打包成一個 NuGet 工具包。也就是說,利用這兩種不同的技術,我們可以製作一個在編譯期間生成多個泛型的 NuGet 工具包。

不過,這樣的生成方式不夠通用。今天我們想生成泛型,明天我們想生成多語言類,後天我們又想生成代理類。能否做一種通用的方式來完成這樣的任務呢?

於是,我想到可以使用 Roslyn。在項目中編寫一段轉換代碼,我們使用通用的方式去編譯和執行這段代碼,以便完成各種各樣日益增加的類型轉換需求。具體來說,就是 使用 Roslyn 編譯一段代碼,然後執行它

準備工作

與之前在 Roslyn 入門:使用 Roslyn 靜態分析現有項目中的代碼 中的不同,我們這次無需打開解決方案或者項目,而是直接尋找並編譯源代碼文件。所以(利好消息),我們這回可以使用 .NET Core 跨平臺版本的 Roslyn 了。所以爲了充分有跨平臺特性,我們創建控制檯應用 (.NET Core)

新建項目
▲ 千萬不要吐槽相比於上一個入門教程來說,這次的界面變成了英文

安裝必要的 NuGet 包

這次不需要完整的 .NET Framework 環境,也不需要打開解決方案和項目這種重型 API,所以一個簡單的 NuGet 包足矣:

安裝 Microsoft.CodeAnalysis.CSharp

準備一份用於編譯和執行代碼文件

我直接使用 生成代碼,從 T 到 T1, T2, Tn —— 自動生成多個類型的泛型 這篇文章中的例子。把其中最關鍵的文件拿來用於編譯和生成試驗。

using System.Linq;
using static System.Environment;

namespace Walterlv.Demo.Roslyn
{
    public class GenericGenerator
    {
        private static readonly string GeneratedAttribute =
            @"[System.CodeDom.Compiler.GeneratedCode(""walterlv"", ""1.0"")]";

        public string Transform(string originalCode, int genericCount)
        {
            if (genericCount == 1)
            {
                return originalCode;
            }

            var content = originalCode
                // 替換泛型。
                .Replace("<out T>", FromTemplate("<{0}>", "out T{n}", ", ", genericCount))
                .Replace("Task<T>", FromTemplate("Task<({0})>", "T{n}", ", ", genericCount))
                .Replace("Func<T, Task>", FromTemplate("Func<{0}, Task>", "T{n}", ", ", genericCount))
                .Replace(" T, Task>", FromTemplate(" {0}, Task>", "T{n}", ", ", genericCount))
                .Replace("(T, bool", FromTemplate("({0}, bool", "T{n}", ", ", genericCount))
                .Replace("var (t, ", FromTemplate("var ({0}, ", "t{n}", ", ", genericCount))
                .Replace(", t)", FromTemplate(", {0})", "t{n}", ", ", genericCount))
                .Replace("return (t, ", FromTemplate("return ({0}, ", "t{n}", ", ", genericCount))
                .Replace("<T>", FromTemplate("<{0}>", "T{n}", ", ", genericCount))
                .Replace("(T value)", FromTemplate("(({0}) value)", "T{n}", ", ", genericCount))
                .Replace("(T t)", FromTemplate("({0})", "T{n} t{n}", ", ", genericCount))
                .Replace("(t)", FromTemplate("({0})", "t{n}", ", ", genericCount))
                .Replace("var t =", FromTemplate("var ({0}) =", "t{n}", ", ", genericCount))
                .Replace(" T ", FromTemplate(" ({0}) ", "T{n}", ", ", genericCount))
                .Replace(" t;", FromTemplate(" ({0});", "t{n}", ", ", genericCount))
                // 生成 [GeneratedCode]。
                .Replace("    public interface ", $"    {GeneratedAttribute}{NewLine}    public interface ")
                .Replace("    public class ", $"    {GeneratedAttribute}{NewLine}    public class ")
                .Replace("    public sealed class ", $"    {GeneratedAttribute}{NewLine}    public sealed class ");
            return content.Trim();
        }

        private static string FromTemplate(string template, string part, string seperator, int count)
        {
            return string.Format(template,
                string.Join(separator, Enumerable.Range(1, count).Select(x => part.Replace("{n}", x.ToString()))));
        }
    }
}

這份代碼你甚至可以直接複製到你的項目中,一定是可以編譯通過的。

編譯這份代碼

使用 Roslyn 編譯一份代碼是非常輕鬆愉快的。寫出以下這三行就夠了:

var syntaxTree = CSharpSyntaxTree.ParseText("那份代碼的全文內容");
var compilation = CSharpCompilation.Create("assemblyname", new[] { syntaxTree },
        options: new CSharpCompilationOptions(OutputKind.DynamicallyLinkedLibrary));
var result = compilation.Emit(ms);

好吧,其實我是開玩笑的,這三行代碼確實能夠跑通過,不過得到的 result 是編譯不通過的結局。爲了能夠在多數情況下編譯通過,我寫了更多的代碼:

using System;
using System.IO;
using System.Linq;
using System.Reflection;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp;

namespace Walterlv.Demo.Roslyn
{
    class Program
    {
        static void Main(string[] args)
        {
            // 大家都知道在代碼中寫死文件路徑是不對的,不過,我們這裏是試驗。放心,我會改的!
            var file = @"D:\Development\Demo\Walterlv.Demo.Roslyn\Walterlv.Demo.Roslyn.Tests\GenericGenerator.cs";
            var originalText = File.ReadAllText(file);
            var syntaxTree = CSharpSyntaxTree.ParseText(originalText);
            var type = CompileType("GenericGenerator", syntaxTree);
            // 於是我們得到了編譯後的類型,但是還不知道怎麼辦。
        }

        private static Type CompileType(string originalClassName, SyntaxTree syntaxTree)
        {
            // 指定編譯選項。
            var assemblyName = $"{originalClassName}.g";
            var compilation = CSharpCompilation.Create(assemblyName, new[] { syntaxTree },
                    options: new CSharpCompilationOptions(OutputKind.DynamicallyLinkedLibrary))
                .AddReferences(
                    // 這算是偷懶了嗎?我把 .NET Core 運行時用到的那些引用都加入到引用了。
                    // 加入引用是必要的,不然連 object 類型都是沒有的,肯定編譯不通過。
                    AppDomain.CurrentDomain.GetAssemblies().Select(x => MetadataReference.CreateFromFile(x.Location)));

            // 編譯到內存流中。
            using (var ms = new MemoryStream())
            {
                var result = compilation.Emit(ms);

                if (result.Success)
                {
                    ms.Seek(0, SeekOrigin.Begin);
                    var assembly = Assembly.Load(ms.ToArray());
                    return assembly.GetTypes().First(x => x.Name == originalClassName);
                }
                throw new CompilingException(result.Diagnostics);
            }
        }
    }
}

執行編譯後的代碼

既然得到了類型,那麼執行這份代碼其實毫無壓力,因爲我們都懂得反射(好吧,我假裝你懂反射)。

var transformer = Activator.CreateInstance(type);
var newContent = (string) type.GetMethod("Transform").Invoke(transformer,
    new object[] { "某個泛型類的全文,假裝我是泛型類 Walterlv<T> is a sb.", 2 });

執行完之後,裏面的 Walterlv<T> 真的變成了 Walterlv<T1, T2, T3, T4, T5, T6, T7, T8, T9, T10, T11, T12, T13, T14, T15, T16> 啊。說明成功執行。

下面進入高階模式

作爲入門篇,我纔不會進入高階模式呢!如果你想實現如本文開頭所說的更通用的效果,歡迎發動你的大腦讓想象力迸發。當然,如果你確實想不出來,歡迎在下方評論,我將盡快回復。

參考資料

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章