Fireasy3 揭秘 -- 代碼編譯器及適配器

来源:https://www.cnblogs.com/fireasy/archive/2023/03/13/17213296.html
-Advertisement-
Play Games

代碼編譯器是將一段源代碼(C#或VisualBasic)編譯成程式集,它的工作方式與 Emit 不一樣。從 .net standard 開始,代碼編譯器就採用了 Roslyn 來編譯源代碼,前幾篇文章里提到的 SourceGenerator 也正是基於此。 ...


目錄

  • Fireasy3 揭秘 -- 依賴註入與服務發現
  • Fireasy3 揭秘 -- 自動服務部署
  • Fireasy3 揭秘 -- 使用 SourceGeneraor 改進服務發現
  • Fireasy3 揭秘 -- 使用 SourceGeneraor 實現動態代理(AOP)
  • Fireasy3 揭秘 -- 使用 Emit 構建程式集
  • Fireasy3 揭秘 -- 代碼編譯器及適配器
  • Fireasy3 揭秘 -- 使用緩存提高反射性能
  • Fireasy3 揭秘 -- 動態類型及擴展支持
  • Fireasy3 揭秘 -- 線程數據共用的實現
  • Fireasy3 揭秘 -- 配置管理及解析處理
  • Fireasy3 揭秘 -- 資料庫適配器
  • Fireasy3 揭秘 -- 解決資料庫之間的語法差異
  • Fireasy3 揭秘 -- 獲取資料庫的架構信息
  • Fireasy3 揭秘 -- 數據批量插入的實現
  • Fireasy3 揭秘 -- 使用包裝器對數據讀取進行相容
  • Fireasy3 揭秘 -- 數據行映射器
  • Fireasy3 揭秘 -- 數據轉換器的實現
  • Fireasy3 揭秘 -- 通用序列生成器和雪花生成器的實現
  • Fireasy3 揭秘 -- 命令攔截器的實現
  • Fireasy3 揭秘 -- 資料庫主從同步的實現
  • Fireasy3 揭秘 -- 大數據分頁的策略
  • Fireasy3 揭秘 -- 數據按需更新及生成實體代理類
  • Fireasy3 揭秘 -- 用對象池技術管理上下文
  • Fireasy3 揭秘 -- Lambda 表達式解析的原理
  • Fireasy3 揭秘 -- 擴展選擇的實現
  • Fireasy3 揭秘 -- 按需載入與惰性載入的區別與實現
  • Fireasy3 揭秘 -- 自定義函數的解析與綁定
  • Fireasy3 揭秘 -- 與 MongoDB 進行適配
  • Fireasy3 揭秘 -- 模塊化的實現原理

  代碼編譯器是將一段源代碼(C#或VisualBasic)編譯成程式集,它的工作方式與 Emit 不一樣。從 .net standard 開始,代碼編譯器就採用了 Roslyn 來編譯源代碼,前幾篇文章里提到的 SourceGenerator 也正是基於此。
  代碼編譯器使用的場景也很多,比如公式解析器,還有 CodeBuilder 里的架構擴展和屬性擴展等等。
  定義一個通用的編譯器介面,實現不同語言的代碼編譯。如下:

    /// <summary>
    /// 代碼編譯器介面。
    /// </summary>
    public interface ICodeCompiler
    {
        /// <summary>
        /// 編譯代碼生成一個程式集。
        /// </summary>
        /// <param name="source">程式源代碼。</param>
        /// <param name="options">配置選項。</param>
        /// <returns>由代碼編譯成的程式集。</returns>
        Assembly? CompileAssembly(string source, ConfigureOptions? options = null);
    }

  ConfigureOptions 主要提供了編譯的相關配置,比如輸出的程式集路徑,引用的程式集等等。如下:

    /// <summary>
    /// 配置參數。
    /// </summary>
    public class ConfigureOptions
    {
        /// <summary>
        /// 獲取或設置輸出的程式集。
        /// </summary>
        public string? OutputAssembly { get; set; }

        /// <summary>
        /// 獲取或設置編譯選項。
        /// </summary>
        public string? CompilerOptions { get; set; }

        /// <summary>
        /// 獲取附加的程式集。
        /// </summary>
        public List<string> Assemblies { get; private set; } = new List<string>();
    }

  Roslyn 提供了不同的語法樹解析適配器,C# 和 VB.Net 分別對應 CSharpSyntaxTreeVisualBasicSyntaxTree。下麵使用 CSharpSyntaxTree 來實現 C# 代碼的編譯。

    /// <summary>
    /// CSharp 代碼編譯器。無法繼承此類。
    /// </summary>
    public sealed class CSharpCodeCompiler : ICodeCompiler
    {
        /// <summary>
        /// 編譯代碼生成一個程式集。
        /// </summary>
        /// <param name="source">程式源代碼。</param>
        /// <param name="options">配置選項。</param>
        /// <returns>由代碼編譯成的程式集。</returns>
        public Assembly? CompileAssembly(string source, ConfigureOptions? options = null)
        {
            options ??= new ConfigureOptions();
            var compilation = CSharpCompilation.Create(Guid.NewGuid().ToString())
                .AddSyntaxTrees(CSharpSyntaxTree.ParseText(source))
                .AddReferences(MetadataReference.CreateFromFile(typeof(object).Assembly.Location))
                .AddReferences(options.Assemblies.Select(s => MetadataReference.CreateFromFile(s)))
                .WithOptions(new CSharpCompilationOptions(OutputKind.DynamicallyLinkedLibrary, optimizationLevel: OptimizationLevel.Release));

            if (!string.IsNullOrEmpty(options.OutputAssembly))
            {
                var result = compilation.Emit(options.OutputAssembly);
                if (result.Success)
                {
                    return Assembly.Load(options.OutputAssembly);
                }
                else
                {
                    ThrowCompileException(result);
                    return null;
                }
            }
            else
            {
                using var ms = new MemoryStream();
                var result = compilation.Emit(ms);
                if (result.Success)
                {
                    return Assembly.Load(ms.ToArray());
                }
                else
                {
                    ThrowCompileException(result);
                    return null;
                }
            }
        }

        private void ThrowCompileException(EmitResult result)
        {
            var errorBuilder = new StringBuilder();

            foreach (var diagnostic in result.Diagnostics.Where(diagnostic =>
                        diagnostic.IsWarningAsError ||
                        diagnostic.Severity == DiagnosticSeverity.Error))
            {
                errorBuilder.AppendFormat("{0}: {1}", diagnostic.Id, diagnostic.GetMessage());
            }

            throw new CodeCompileException(errorBuilder.ToString());
        }
    }

  有了 C# 編譯器的實現,但是又不想在公共庫(Fireasy.Common)中實現 VB.Net,畢竟目前來說主流語言還是 C#,使用 VB.NET 的場景不是太多。但是你又得考慮這些語言,那該怎麼辦呢?
  一個很明智的做法就是使用管理器,如下,定義一個 ICodeCompilerManager 介面:

    /// <summary>
    /// 提供代碼編譯器管理的介面。
    /// </summary>
    public interface ICodeCompilerManager
    {
        /// <summary>
        /// 註冊指定語言類型的代碼編譯器類型。
        /// </summary>
        /// <typeparam name="TCompiler"></typeparam>
        /// <param name="languages">語言。</param>
        void Register<TCompiler>(params string[] languages) where TCompiler : ICodeCompiler;

        /// <summary>
        /// 創建代碼編譯器。
        /// </summary>
        /// <param name="language">語言。</param>
        /// <returns></returns>
        ICodeCompiler? CreateCompiler(string language);
    }

  管理器提供了註冊和創建實例的方法,其實原理很簡單,使用一個字典來管理語言和編譯器類型即可,如下:

    /// <summary>
    /// 預設的代碼編譯器管理器。
    /// </summary>
    public class DefaultCodeCompilerManager : ICodeCompilerManager
    {
        private readonly Dictionary<string, Type> _languageMappers = new(new StringIgnoreCaseComparer());

        private class StringIgnoreCaseComparer : IEqualityComparer<string>
        {
            public bool Equals(string x, string y)
            {
                return string.Compare(x, y, true) == 0;
            }

            public int GetHashCode(string obj)
            {
                return obj?.GetHashCode() ?? 0;
            }
        }

        /// <summary>
        /// 初始化 <see cref="DefaultCodeCompilerManager"/> 類新實例。
        /// </summary>
        public DefaultCodeCompilerManager()
        {
            Register<CSharpCodeCompiler>("csharp", "c#");
        }

        /// <summary>
        /// 註冊指定語言類型的代碼編譯器類型。
        /// </summary>
        /// <typeparam name="TCompiler"></typeparam>
        /// <param name="languages">語言。</param>
        public void Register<TCompiler>(params string[] languages) where TCompiler : ICodeCompiler
        {
            foreach (var language in languages)
            {
                _languageMappers.AddOrReplace(language, typeof(TCompiler));
            }
        }

        /// <summary>
        /// 創建代碼編譯器。
        /// </summary>
        /// <param name="language">語言。</param>
        /// <returns></returns>
        public ICodeCompiler? CreateCompiler(string language)
        {
            if (_languageMappers.TryGetValue(language, out var compilerType))
            {
                return Activator.CreateInstance(compilerType) as ICodeCompiler;
            }

            return null;
        }
    }

  然後將其在 AddFireasy 調用時,註冊到 IServiceCollection 里。如下:

        /// <summary>
        /// 添加框架的基本支持。
        /// </summary>
        /// <param name="services"><see cref="IServiceCollection"/> 實例。</param>
        /// <param name="configure">配置方法。</param>
        /// <returns></returns>
        public static SetupBuilder AddFireasy(this IServiceCollection services, Action<SetupOptions>? configure = null)
        {
            services.AddSingleton<ICodeCompilerManager>(new DefaultCodeCompilerManager());

            var options = new SetupOptions();
            //省略後面的代碼

            return builder;
        }

  這樣,要任何時候都可以使用註入的方式,獲取到代碼編譯器了。那麼,VB.NET 代碼編譯的實現,可以單獨創建一個項目(稱之為實現庫),來實現代碼編譯器的介面,註意需要從 Nuget 里安裝 Microsoft.CodeAnalysis.VisualBasic。如下:

    /// <summary>
    /// VisualBasic 代碼編譯器。無法繼承此類。
    /// </summary>
    public class VisualBasicCodeCompiler : ICodeCompiler
    {
        /// <summary>
        /// 編譯代碼生成一個程式集。
        /// </summary>
        /// <param name="source">程式源代碼。</param>
        /// <param name="options">配置選項。</param>
        /// <returns>由代碼編譯成的程式集。</returns>
        public Assembly? CompileAssembly(string source, ConfigureOptions? options = null)
        {
            options ??= new ConfigureOptions();
            var compilation = VisualBasicCompilation.Create(Guid.NewGuid().ToString())
                .AddSyntaxTrees(VisualBasicSyntaxTree.ParseText(source))
                .AddReferences(MetadataReference.CreateFromFile(typeof(object).Assembly.Location))
                .AddReferences(options.Assemblies.Select(s => MetadataReference.CreateFromFile(s)))
                .WithOptions(new VisualBasicCompilationOptions(OutputKind.DynamicallyLinkedLibrary, optimizationLevel: OptimizationLevel.Release));

            if (!string.IsNullOrEmpty(options.OutputAssembly))
            {
                var result = compilation.Emit(options.OutputAssembly);
                if (result.Success)
                {
                    return Assembly.Load(options.OutputAssembly);
                }
                else
                {
                    ThrowCompileException(result);
                    return null;
                }
            }
            else
            {
                using var ms = new MemoryStream();
                var result = compilation.Emit(ms);
                if (result.Success)
                {
                    return Assembly.Load(ms.ToArray());
                }
                else
                {
                    ThrowCompileException(result);
                    return null;
                }
            }
        }

        private void ThrowCompileException(EmitResult result)
        {
            var errorBuilder = new StringBuilder();

            foreach (var diagnostic in result.Diagnostics.Where(diagnostic =>
                        diagnostic.IsWarningAsError ||
                        diagnostic.Severity == DiagnosticSeverity.Error))
            {
                errorBuilder.AppendFormat("{0}: {1}", diagnostic.Id, diagnostic.GetMessage());
            }

            throw new CodeCompileException(errorBuilder.ToString());
        }
    }

  再添加一個 服務部署器,將 VB.NET 語言的編譯器註冊到 ICodeCompilerManager 的單例里去,如下:

[assembly: ServicesDeploy(typeof(VisualBasicServicesDeployer))]

namespace Fireasy.Data.DependencyInjection
{
    /// <summary>
    /// 服務部署。
    /// </summary>
    public class VisualBasicServicesDeployer : IServicesDeployer
    {
        void IServicesDeployer.Configure(IServiceCollection services)
        {
            var manager = services.GetSingletonInstance<ICodeCompilerManager>();
            manager!.Register<VisualBasicCodeCompiler>("vb");
        }
    }
}

  這樣,項目里如果需要使用 VB.NET 語言編譯器,只需要引用該實現庫,而不會侵入和破壞公共庫,再如有其他的語言,都可以使用此種方法進行擴展。
  代碼編譯器的使用就變得很簡單了,如下:

        /// <summary>
        /// 使用c#源代碼
        /// </summary>
        [TestMethod]
        public void TestCompileAssembly()
        {
            var source = @"
public class A
{
    public string Hello(string str)
    {
        return str;
    }
}";
            var codeCompilerManager = ServiceProvider.GetService<ICodeCompilerManager>();
            var codeCompiler = codeCompilerManager!.CreateCompiler("csharp");

            var assembly = codeCompiler!.CompileAssembly(source);

            var type = assembly!.GetType("A");

            Assert.IsNotNull(type);
        }

        /// <summary>
        /// 使用vb源代碼
        /// </summary>
        [TestMethod]
        public void TestCompileAssemblyUseVb()
        {
            var source = @"
Public Class A
    Public Function Hello(ByVal str As String) As String
        Return str
    End Function
End Class";
            var codeCompilerManager = ServiceProvider.GetService<ICodeCompilerManager>();
            var codeCompiler = codeCompilerManager!.CreateCompiler("vb");

            var assembly = codeCompiler!.CompileAssembly(source);

            var type = assembly!.GetType("A");

            Assert.IsNotNull(type);
        }

  ICodeCompiler 還有幾個擴展方法,可以獲取對應的類型、方法及委托,只不過是通過反射對程式集的操作罷了。

  最後,奉上 Fireasy 3 的開源地址:https://gitee.com/faib920/fireasy3 ,歡迎大家前來捧場。

  本文相關代碼請參考:
  https://gitee.com/faib920/fireasy3/src/libraries/Fireasy.Common/Compiler
  https://gitee.com/faib920/fireasy3/src/libraries/Fireasy.CodeCompiler.VisualBasic
  https://gitee.com/faib920/fireasy3/tests/Fireasy.Common.Tests/CodeCompilerTests.cs

  更多內容請移步官網 http://www.fireasy.cn 。

作者:fireasy
出處:http://fireasy.cnblogs.com
官網:http://www.fireasy.cn
版權聲明:本文的版權歸作者與博客園共有。轉載時須註明本文的詳細鏈接,否則作者將保留追究其法律責任。 掃碼加入QQ群:
掃碼加入微信群(3月20日前有效):

您的分享是我們最大的動力!

-Advertisement-
Play Games
更多相關文章
  • IDEA: 如何導入項目模塊 以及 將 Java程式打包 JAR 詳細步驟 、 @ IDEA 導入項目模塊 Module 一. 創建一個空項目 想要導入模塊 Module ,我們需要先創建一個項目,因為 Module模塊在 IDEA 中是存在於項目下的。 這裡我們先創建一個空項目,當然已經有項目了, ...
  • 我這個人比較懶,總是喜歡把收到的重要文件,或者比較緊急的文件放到桌面,久而久之,桌面或者文件夾越來越亂 。 不知道大家是不是像我一樣的 我滴媽呀,看著就很崩潰! 之所以放在桌面上,主要是為了下次使用的時候好找 但是,其實,結果…並沒有 結果,我的馬馬~~ 反而更難找了 也不知道越亂越好找這句話是誰第 ...
  • 作者:變速風聲 鏈接:https://juejin.cn/post/7104090532015505416 前言 在開發中遇到一個業務訴求,需要在千萬量級的底池數據中篩選出不超過 10W 的數據,並根據配置的權重規則進行排序、打散(如同一個類目下的商品數據不能連續出現 3 次)。 下麵對該業務訴求的 ...
  • 在正式開始驅動開發之前,需要自行搭建驅動開發的必要環境,首先我們需要安裝`Visual Studio 2013`這款功能強大的程式開發工具,在課件內請雙擊`ISO`文件並運行內部的`vs_ultimate.exe`安裝包,`Visual Studio`的安裝非常的簡單,您只需要按照提示全部選擇預設參... ...
  • C++語言線上運行編譯,是一款可線上編程編輯器,在編輯器上輸入C++語言代碼,點擊運行,可線上編譯運行C++語言,C++語言代碼線上運行調試,C++語言線上編譯,可快速線上測試您的C++語言代碼,線上編譯C++語言代碼發現是否存在錯誤,如果代碼測試通過,將會輸出編譯後的結果。 該線上工具由IT寶庫提 ...
  • 當我們準備去殺死一個進程時,從程式設計的本身來考慮,我們應當要設計一定的保護方案來確保程式被非正常終止時,相應的計算結果也能夠被很好的保存下來。在Python中可以使用signal.signal函數來實現這樣的功能,但是如果要實現數據的保存功能,需要結合一個實際的類來實現。 ...
  • 本文介紹基於Python中ArcPy模塊,實現大量HDF格式柵格圖像文件批量轉換為TIFF格式的方法。 首先,來看看我們想要實現的需求。 在一個名為HDF的文件夾下,有五個子文件夾;每一個子文件夾中,都存儲了大量的.hdf格式的柵格遙感影像數據。 我們在其中任選一個子文件夾,來看看其中所含的文件。 ...
  • 文件上傳是一個老生常談的話題了,在文件相對比較小的情況下,可以直接把文件轉化為位元組流上傳到伺服器,但在文件比較大的情況下,用普通的方式進行上傳,這可不是一個好的辦法,畢竟很少有人會忍受,當文件上傳到一半中斷後,繼續上傳卻只能重頭開始上傳,這種讓人不爽的體驗。那有沒有比較好的上傳體驗呢,答案有的,就是 ...
一周排行
    -Advertisement-
    Play Games
  • GoF之工廠模式 @目錄GoF之工廠模式每博一文案1. 簡單說明“23種設計模式”1.2 介紹工廠模式的三種形態1.3 簡單工廠模式(靜態工廠模式)1.3.1 簡單工廠模式的優缺點:1.4 工廠方法模式1.4.1 工廠方法模式的優缺點:1.5 抽象工廠模式1.6 抽象工廠模式的優缺點:2. 總結:3 ...
  • 新改進提供的Taurus Rpc 功能,可以簡化微服務間的調用,同時可以不用再手動輸出模塊名稱,或調用路徑,包括負載均衡,這一切,由框架實現並提供了。新的Taurus Rpc 功能,將使得服務間的調用,更加輕鬆、簡約、高效。 ...
  • 本章將和大家分享ES的數據同步方案和ES集群相關知識。廢話不多說,下麵我們直接進入主題。 一、ES數據同步 1、數據同步問題 Elasticsearch中的酒店數據來自於mysql資料庫,因此mysql數據發生改變時,Elasticsearch也必須跟著改變,這個就是Elasticsearch與my ...
  • 引言 在我們之前的文章中介紹過使用Bogus生成模擬測試數據,今天來講解一下功能更加強大自動生成測試數據的工具的庫"AutoFixture"。 什麼是AutoFixture? AutoFixture 是一個針對 .NET 的開源庫,旨在最大程度地減少單元測試中的“安排(Arrange)”階段,以提高 ...
  • 經過前面幾個部分學習,相信學過的同學已經能夠掌握 .NET Emit 這種中間語言,並能使得它來編寫一些應用,以提高程式的性能。隨著 IL 指令篇的結束,本系列也已經接近尾聲,在這接近結束的最後,會提供幾個可供直接使用的示例,以供大伙分析或使用在項目中。 ...
  • 當從不同來源導入Excel數據時,可能存在重覆的記錄。為了確保數據的準確性,通常需要刪除這些重覆的行。手動查找並刪除可能會非常耗費時間,而通過編程腳本則可以實現在短時間內處理大量數據。本文將提供一個使用C# 快速查找並刪除Excel重覆項的免費解決方案。 以下是實現步驟: 1. 首先安裝免費.NET ...
  • C++ 異常處理 C++ 異常處理機制允許程式在運行時處理錯誤或意外情況。它提供了捕獲和處理錯誤的一種結構化方式,使程式更加健壯和可靠。 異常處理的基本概念: 異常: 程式在運行時發生的錯誤或意外情況。 拋出異常: 使用 throw 關鍵字將異常傳遞給調用堆棧。 捕獲異常: 使用 try-catch ...
  • 優秀且經驗豐富的Java開發人員的特征之一是對API的廣泛瞭解,包括JDK和第三方庫。 我花了很多時間來學習API,尤其是在閱讀了Effective Java 3rd Edition之後 ,Joshua Bloch建議在Java 3rd Edition中使用現有的API進行開發,而不是為常見的東西編 ...
  • 框架 · 使用laravel框架,原因:tp的框架路由和orm沒有laravel好用 · 使用強制路由,方便介面多時,分多版本,分文件夾等操作 介面 · 介面開發註意欄位類型,欄位是int,查詢成功失敗都要返回int(對接java等強類型語言方便) · 查詢介面用GET、其他用POST 代碼 · 所 ...
  • 正文 下午找企業的人去鎮上做貸後。 車上聽同事跟那個司機對罵,火星子都快出來了。司機跟那同事更熟一些,連我在內一共就三個人,同事那一手指桑罵槐給我都聽愣了。司機也是老社會人了,馬上聽出來了,為那個無辜的企業經辦人辯護,實際上是為自己辯護。 “這個事情你不能怪企業。”“但他們總不能讓銀行的人全權負責, ...