ToB企服应用市场:ToB评测及商务社交产业平台

标题: Fireasy3 揭秘 -- 代码编译器及适配器 [打印本页]

作者: 守听    时间: 2023-3-13 23:53
标题: Fireasy3 揭秘 -- 代码编译器及适配器
目录

  代码编译器是将一段源代码(C#或VisualBasic)编译成程序集,它的工作方式与 Emit 不一样。从 .net standard 开始,代码编译器就采用了 Roslyn 来编译源代码,前几篇文章里提到的 SourceGenerator 也正是基于此。
  代码编译器使用的场景也很多,比如公式解析器,还有 CodeBuilder 里的架构扩展和属性扩展等等。
  定义一个通用的编译器接口,实现不同语言的代码编译。如下:
  1.     /// <summary>
  2.     /// 代码编译器接口。
  3.     /// </summary>
  4.     public interface ICodeCompiler
  5.     {
  6.         /// <summary>
  7.         /// 编译代码生成一个程序集。
  8.         /// </summary>
  9.         /// <param name="source">程序源代码。</param>
  10.         /// <param name="options">配置选项。</param>
  11.         /// <returns>由代码编译成的程序集。</returns>
  12.         Assembly? CompileAssembly(string source, ConfigureOptions? options = null);
  13.     }
复制代码
  ConfigureOptions 主要提供了编译的相关配置,比如输出的程序集路径,引用的程序集等等。如下:
  1.     /// <summary>
  2.     /// 配置参数。
  3.     /// </summary>
  4.     public class ConfigureOptions
  5.     {
  6.         /// <summary>
  7.         /// 获取或设置输出的程序集。
  8.         /// </summary>
  9.         public string? OutputAssembly { get; set; }
  10.         /// <summary>
  11.         /// 获取或设置编译选项。
  12.         /// </summary>
  13.         public string? CompilerOptions { get; set; }
  14.         /// <summary>
  15.         /// 获取附加的程序集。
  16.         /// </summary>
  17.         public List<string> Assemblies { get; private set; } = new List<string>();
  18.     }
复制代码
  Roslyn 提供了不同的语法树解析适配器,C# 和 VB.Net 分别对应 CSharpSyntaxTree 及 VisualBasicSyntaxTree。下面使用 CSharpSyntaxTree 来实现 C# 代码的编译。
  1.     /// <summary>
  2.     /// CSharp 代码编译器。无法继承此类。
  3.     /// </summary>
  4.     public sealed class CSharpCodeCompiler : ICodeCompiler
  5.     {
  6.         /// <summary>
  7.         /// 编译代码生成一个程序集。
  8.         /// </summary>
  9.         /// <param name="source">程序源代码。</param>
  10.         /// <param name="options">配置选项。</param>
  11.         /// <returns>由代码编译成的程序集。</returns>
  12.         public Assembly? CompileAssembly(string source, ConfigureOptions? options = null)
  13.         {
  14.             options ??= new ConfigureOptions();
  15.             var compilation = CSharpCompilation.Create(Guid.NewGuid().ToString())
  16.                 .AddSyntaxTrees(CSharpSyntaxTree.ParseText(source))
  17.                 .AddReferences(MetadataReference.CreateFromFile(typeof(object).Assembly.Location))
  18.                 .AddReferences(options.Assemblies.Select(s => MetadataReference.CreateFromFile(s)))
  19.                 .WithOptions(new CSharpCompilationOptions(OutputKind.DynamicallyLinkedLibrary, optimizationLevel: OptimizationLevel.Release));
  20.             if (!string.IsNullOrEmpty(options.OutputAssembly))
  21.             {
  22.                 var result = compilation.Emit(options.OutputAssembly);
  23.                 if (result.Success)
  24.                 {
  25.                     return Assembly.Load(options.OutputAssembly);
  26.                 }
  27.                 else
  28.                 {
  29.                     ThrowCompileException(result);
  30.                     return null;
  31.                 }
  32.             }
  33.             else
  34.             {
  35.                 using var ms = new MemoryStream();
  36.                 var result = compilation.Emit(ms);
  37.                 if (result.Success)
  38.                 {
  39.                     return Assembly.Load(ms.ToArray());
  40.                 }
  41.                 else
  42.                 {
  43.                     ThrowCompileException(result);
  44.                     return null;
  45.                 }
  46.             }
  47.         }
  48.         private void ThrowCompileException(EmitResult result)
  49.         {
  50.             var errorBuilder = new StringBuilder();
  51.             foreach (var diagnostic in result.Diagnostics.Where(diagnostic =>
  52.                         diagnostic.IsWarningAsError ||
  53.                         diagnostic.Severity == DiagnosticSeverity.Error))
  54.             {
  55.                 errorBuilder.AppendFormat("{0}: {1}", diagnostic.Id, diagnostic.GetMessage());
  56.             }
  57.             throw new CodeCompileException(errorBuilder.ToString());
  58.         }
  59.     }
复制代码
  有了 C# 编译器的实现,但是又不想在公共库(Fireasy.Common)中实现 VB.Net,毕竟目前来说主流语言还是 C#,使用 VB.NET 的场景不是太多。但是你又得考虑这些语言,那该怎么办呢?
  一个很明智的做法就是使用管理器,如下,定义一个 ICodeCompilerManager 接口:
  1.     /// <summary>
  2.     /// 提供代码编译器管理的接口。
  3.     /// </summary>
  4.     public interface ICodeCompilerManager
  5.     {
  6.         /// <summary>
  7.         /// 注册指定语言类型的代码编译器类型。
  8.         /// </summary>
  9.         /// <typeparam name="TCompiler"></typeparam>
  10.         /// <param name="languages">语言。</param>
  11.         void Register<TCompiler>(params string[] languages) where TCompiler : ICodeCompiler;
  12.         /// <summary>
  13.         /// 创建代码编译器。
  14.         /// </summary>
  15.         /// <param name="language">语言。</param>
  16.         /// <returns></returns>
  17.         ICodeCompiler? CreateCompiler(string language);
  18.     }
复制代码
  管理器提供了注册和创建实例的方法,其实原理很简单,使用一个字典来管理语言和编译器类型即可,如下:
  1.     /// <summary>
  2.     /// 缺省的代码编译器管理器。
  3.     /// </summary>
  4.     public class DefaultCodeCompilerManager : ICodeCompilerManager
  5.     {
  6.         private readonly Dictionary<string, Type> _languageMappers = new(new StringIgnoreCaseComparer());
  7.         private class StringIgnoreCaseComparer : IEqualityComparer<string>
  8.         {
  9.             public bool Equals(string x, string y)
  10.             {
  11.                 return string.Compare(x, y, true) == 0;
  12.             }
  13.             public int GetHashCode(string obj)
  14.             {
  15.                 return obj?.GetHashCode() ?? 0;
  16.             }
  17.         }
  18.         /// <summary>
  19.         /// 初始化 <see cref="DefaultCodeCompilerManager"/> 类新实例。
  20.         /// </summary>
  21.         public DefaultCodeCompilerManager()
  22.         {
  23.             Register<CSharpCodeCompiler>("csharp", "c#");
  24.         }
  25.         /// <summary>
  26.         /// 注册指定语言类型的代码编译器类型。
  27.         /// </summary>
  28.         /// <typeparam name="TCompiler"></typeparam>
  29.         /// <param name="languages">语言。</param>
  30.         public void Register<TCompiler>(params string[] languages) where TCompiler : ICodeCompiler
  31.         {
  32.             foreach (var language in languages)
  33.             {
  34.                 _languageMappers.AddOrReplace(language, typeof(TCompiler));
  35.             }
  36.         }
  37.         /// <summary>
  38.         /// 创建代码编译器。
  39.         /// </summary>
  40.         /// <param name="language">语言。</param>
  41.         /// <returns></returns>
  42.         public ICodeCompiler? CreateCompiler(string language)
  43.         {
  44.             if (_languageMappers.TryGetValue(language, out var compilerType))
  45.             {
  46.                 return Activator.CreateInstance(compilerType) as ICodeCompiler;
  47.             }
  48.             return null;
  49.         }
  50.     }
复制代码
  然后将其在 AddFireasy 调用时,注册到 IServiceCollection 里。如下:
  1.         /// <summary>
  2.         /// 添加框架的基本支持。
  3.         /// </summary>
  4.         /// <param name="services"><see cref="IServiceCollection"/> 实例。</param>
  5.         /// <param name="configure">配置方法。</param>
  6.         /// <returns></returns>
  7.         public static SetupBuilder AddFireasy(this IServiceCollection services, Action<SetupOptions>? configure = null)
  8.         {
  9.             services.AddSingleton<ICodeCompilerManager>(new DefaultCodeCompilerManager());
  10.             var options = new SetupOptions();
  11.             //省略后面的代码
  12.             return builder;
  13.         }
复制代码
  这样,要任何时候都可以使用注入的方式,获取到代码编译器了。那么,VB.NET 代码编译的实现,可以单独创建一个项目(称之为实现库),来实现代码编译器的接口,注意需要从 Nuget 里安装 Microsoft.CodeAnalysis.VisualBasic。如下:
  1.     /// <summary>
  2.     /// VisualBasic 代码编译器。无法继承此类。
  3.     /// </summary>
  4.     public class VisualBasicCodeCompiler : ICodeCompiler
  5.     {
  6.         /// <summary>
  7.         /// 编译代码生成一个程序集。
  8.         /// </summary>
  9.         /// <param name="source">程序源代码。</param>
  10.         /// <param name="options">配置选项。</param>
  11.         /// <returns>由代码编译成的程序集。</returns>
  12.         public Assembly? CompileAssembly(string source, ConfigureOptions? options = null)
  13.         {
  14.             options ??= new ConfigureOptions();
  15.             var compilation = VisualBasicCompilation.Create(Guid.NewGuid().ToString())
  16.                 .AddSyntaxTrees(VisualBasicSyntaxTree.ParseText(source))
  17.                 .AddReferences(MetadataReference.CreateFromFile(typeof(object).Assembly.Location))
  18.                 .AddReferences(options.Assemblies.Select(s => MetadataReference.CreateFromFile(s)))
  19.                 .WithOptions(new VisualBasicCompilationOptions(OutputKind.DynamicallyLinkedLibrary, optimizationLevel: OptimizationLevel.Release));
  20.             if (!string.IsNullOrEmpty(options.OutputAssembly))
  21.             {
  22.                 var result = compilation.Emit(options.OutputAssembly);
  23.                 if (result.Success)
  24.                 {
  25.                     return Assembly.Load(options.OutputAssembly);
  26.                 }
  27.                 else
  28.                 {
  29.                     ThrowCompileException(result);
  30.                     return null;
  31.                 }
  32.             }
  33.             else
  34.             {
  35.                 using var ms = new MemoryStream();
  36.                 var result = compilation.Emit(ms);
  37.                 if (result.Success)
  38.                 {
  39.                     return Assembly.Load(ms.ToArray());
  40.                 }
  41.                 else
  42.                 {
  43.                     ThrowCompileException(result);
  44.                     return null;
  45.                 }
  46.             }
  47.         }
  48.         private void ThrowCompileException(EmitResult result)
  49.         {
  50.             var errorBuilder = new StringBuilder();
  51.             foreach (var diagnostic in result.Diagnostics.Where(diagnostic =>
  52.                         diagnostic.IsWarningAsError ||
  53.                         diagnostic.Severity == DiagnosticSeverity.Error))
  54.             {
  55.                 errorBuilder.AppendFormat("{0}: {1}", diagnostic.Id, diagnostic.GetMessage());
  56.             }
  57.             throw new CodeCompileException(errorBuilder.ToString());
  58.         }
  59.     }
复制代码
  再添加一个 服务部署器,将 VB.NET 语言的编译器注册到 ICodeCompilerManager 的单例里去,如下:
  1. [assembly: ServicesDeploy(typeof(VisualBasicServicesDeployer))]
  2. namespace Fireasy.Data.DependencyInjection
  3. {
  4.     /// <summary>
  5.     /// 服务部署。
  6.     /// </summary>
  7.     public class VisualBasicServicesDeployer : IServicesDeployer
  8.     {
  9.         void IServicesDeployer.Configure(IServiceCollection services)
  10.         {
  11.             var manager = services.GetSingletonInstance<ICodeCompilerManager>();
  12.             manager!.Register<VisualBasicCodeCompiler>("vb");
  13.         }
  14.     }
  15. }
复制代码
  这样,项目里如果需要使用 VB.NET 语言编译器,只需要引用该实现库,而不会侵入和破坏公共库,再如有其他的语言,都可以使用此种方法进行扩展。
  代码编译器的使用就变得很简单了,如下:
  1.         /// <summary>
  2.         /// 使用c#源代码
  3.         /// </summary>
  4.         [TestMethod]
  5.         public void TestCompileAssembly()
  6.         {
  7.             var source = @"
  8. public class A
  9. {
  10.     public string Hello(string str)
  11.     {
  12.         return str;
  13.     }
  14. }";
  15.             var codeCompilerManager = ServiceProvider.GetService<ICodeCompilerManager>();
  16.             var codeCompiler = codeCompilerManager!.CreateCompiler("csharp");
  17.             var assembly = codeCompiler!.CompileAssembly(source);
  18.             var type = assembly!.GetType("A");
  19.             Assert.IsNotNull(type);
  20.         }
  21.         /// <summary>
  22.         /// 使用vb源代码
  23.         /// </summary>
  24.         [TestMethod]
  25.         public void TestCompileAssemblyUseVb()
  26.         {
  27.             var source = @"
  28. Public Class A
  29.     Public Function Hello(ByVal str As String) As String
  30.         Return str
  31.     End Function
  32. End Class";
  33.             var codeCompilerManager = ServiceProvider.GetService<ICodeCompilerManager>();
  34.             var codeCompiler = codeCompilerManager!.CreateCompiler("vb");
  35.             var assembly = codeCompiler!.CompileAssembly(source);
  36.             var type = assembly!.GetType("A");
  37.             Assert.IsNotNull(type);
  38.         }
复制代码
  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 。
扫码加入QQ群:
扫码加入微信群(3月20日前有效):

免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作!




欢迎光临 ToB企服应用市场:ToB评测及商务社交产业平台 (https://dis.qidao123.com/) Powered by Discuz! X3.4