代碼編譯器是將一段源代碼(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 分別對應 CSharpSyntaxTree
及 VisualBasicSyntaxTree
。下麵使用 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日前有效):