はじめに
今回はRuntimeでcsファイルをRoslynで解析をしてリフレクションで実行する方法を紹介したいと思います。
概要
.NET Compiler Platform SDK(通称Roslyn)は、C#で開発されたC#コンパイラープラットフォームを指します。オープンソースなので、誰でもコードが見れますしプルリクも送れます。
The Roslyn .NET compiler provides C# and Visual Basic languages with rich code analysis APIs.
// DeepL翻訳
Roslyn .NETコンパイラーは、C#およびVisual Basic言語に豊富なコード解析APIを提供します。
この新しいコンパイラーは、開発コードネーム「Roslyn」(アメリカ ワシントン州の地名が由来)として開発され、 最終的なプロダクト名は「.NET Compiler Platform となりました。 IDE との連携や第三者による IDE 拡張まで見越したものなので、単なるコンパイラーではなく、「プラットフォーム」という呼称が付いています。 (ただ、今でも俗称としてはコードネームそのままな「Roslyn」の方がよく使われます。)
[雑記] .NET Compiler Platform - C# によるプログラミング入門 | ++C++; // 未確認飛行 C
最近だとRoslyn AnalyzerやSourceGeneratorがC#界隈だとホットな話題かと思いますが、それもRoslynの機能の一部です。
インストール
NuGetよりMicrosoft.CodeAnalysis.CSharpをインストールします。

コマンドだと以下で入れられます。
$ dotnet add package Microsoft.CodeAnalysis.CSharp
今回の流れ
RoslynのCompiler APIsを利用して、.csを動的に読み込んでコンパイル、リフレクションでメソッドを実行してみます。
1. .csに対して構文解析を行い、構文木(SyntaxTree)を生成
2. SyntaxTreeと依存先のアセンブリを設定したCSharpCompilationを生成
3. CSharpCompilation.Emitを実行して、コンパイルされたソースコードをStreamに書き込み、コンパイル結果を示すEmitResultを取得
4. Streamからアセンブリをロードし、リフレクションでメソッド呼び出しを行う
構文木を生成する
テキストから構文木(SyntaxTree)を生成するには、CSharpSyntexTree.ParseTextメソッドを利用します。
CSharpSyntaxTree.ParseText Method (Microsoft.CodeAnalysis.CSharp) | Microsoft Learn
string sourceCode = """ namespace Sample { public class SampleClass { public static string Execute() { return "Hello, World!"; } } } """; // sourceCodeから構文木(SyntaxTree)を生成 SyntaxTree syntaxTree = CSharpSyntaxTree.ParseText(sourceCode);
具体的にどのようなSyntexTreeが形成されるかは、SharpLabを見ると一目瞭然です。SharpLabを開いた後に、ResultsをSyntex Treeに設定します。

SharpLab上で青い3つの四角のアイコンがついているものはNodeを表していて、黄色い1つの四角のアイコンがついているものはTokenを表しています。またグレーの横棒みたいなアイコンはTriviaを指していて、空白、コメント、プリプロセッサ ディレクティブなど重要でないものが格納されています。
Node: 青い3つの四角アイコンToken: 黄色い1つの四角アイコン, 黄色い丸ぽちアイコン(keyword)Trivia: グレーの横棒アイコン
他にもちょこちょこ記号がありますが、おそらく色で統一されてるのかなと。(要調査)
Syntax Nodes・Syntax Tokens・Syntax Triviaに関しての詳細は以下を参照してください。
github.com
// sourceCodeから構文木(SyntaxTree)を生成 SyntaxTree syntaxTree = CSharpSyntaxTree.ParseText(sourceCode); // SyntaxTreeのルートを取得 // 中身はSharpLab(https://sharplab.io/)で確認してみると分かりやすい // SharpLab上で青い3つの四角のアイコンがついているものは、Nodeを表す // SharpLab上で黄色い1つの四角のアイコンがついているものは、Tokenを表す CompilationUnitSyntax root = syntaxTree.GetCompilationUnitRoot(); // CompilationUnit Console.WriteLine(root.Kind()); // rootの子であるNode // NamespaceDeclaration Console.WriteLine(string.Join(", ", root.ChildNodes().Select(x => x.Kind()))); // rootの子であるToken // EndOfFileToken Console.WriteLine(string.Join(", ", root.ChildTokens().Select(x => x.Kind()))); // NamespaceDeclarationの子であるNode // IdentifierName, ClassDeclaration Console.WriteLine(string.Join(", ", root.ChildNodes()[0].ChildNodes().Select(x => x.Kind()))); // NamespaceDeclarationの子であるToken // NamespaceKeyword, OpenBraceToken, CloseBraceToken Console.WriteLine(string.Join(", ", root.ChildTokens[0].ChildTokens().Select(x => x.Kind()))); // SyntexTree全体に対して含まれるNode // NamespaceDeclaration, IdentifierName, ClassDeclaration, MethodDeclaration, PredefinedType, ParameterList, Block, ReturnStatement, StringLiteralExpression Console.WriteLine(string.Join(", ", root.DescendantNodes().Select(x => x.Kind()))); // SyntexTree全体に対して含まれるToken // NamespaceKeyword, IdentifierToken, OpenBraceToken, PublicKeyword, ClassKeyword, IdentifierToken, OpenBraceToken, PublicKeyword, StaticKeyword, StringKeyword, IdentifierToken, OpenParenToken, CloseParenToken, OpenBraceToken, ReturnKeyword, StringLiteralToken, SemicolonToken, CloseBraceToken, CloseBraceToken, CloseBraceToken, EndOfFileToken Console.WriteLine(string.Join(", ", root.DescendantTokens().Select(x => x.Kind()))); // SyntexTree全体に対して含まれるNode + Token // NamespaceDeclaration, NamespaceKeyword, IdentifierName, IdentifierToken, OpenBraceToken, ClassDeclaration, PublicKeyword, ClassKeyword, IdentifierToken, OpenBraceToken, MethodDeclaration, PublicKeyword, StaticKeyword, PredefinedType, StringKeyword, IdentifierToken, ParameterList, OpenParenToken, CloseParenToken, Block, OpenBraceToken, ReturnStatement, ReturnKeyword, StringLiteralExpression, StringLiteralToken, SemicolonToken, CloseBraceToken, CloseBraceToken, CloseBraceToken, EndOfFileToken Console.WriteLine(string.Join(", ", root.DescendantNodesAndTokens().Select(x => x.Kind()))); // Triviaは空白、コメント、およびプリプロセッサ ディレクティブなど、コードを通常に理解するためにはさほど重要ではないソース テキストの部分 // WhitespaceTrivia, EndOfLineTrivia, EndOfLineTrivia, WhitespaceTrivia, SingleLineCommentTrivia, EndOfLineTrivia, WhitespaceTrivia, WhitespaceTrivia, WhitespaceTrivia, EndOfLineTrivia, WhitespaceTrivia, EndOfLineTrivia, WhitespaceTrivia, SingleLineCommentTrivia, EndOfLineTrivia, WhitespaceTrivia, WhitespaceTrivia, WhitespaceTrivia, WhitespaceTrivia, EndOfLineTrivia, WhitespaceTrivia, EndOfLineTrivia, WhitespaceTrivia, WhitespaceTrivia, EndOfLineTrivia, WhitespaceTrivia, EndOfLineTrivia, WhitespaceTrivia, EndOfLineTrivia Console.WriteLine(string.Join(", ", root.DescendantTrivia().Select(x => x.Kind())));
また情報を取り出す際にはSharpLabに表示されているKindを参考にしながら、専用の〇〇Syntaxを利用していくのが定番の手法です。
// TODO: 書きたかった内容と微妙にコードがズレてるので後で書き換える // sourceCodeから構文木(SyntaxTree)を生成 SyntaxTree syntaxTree = CSharpSyntaxTree.ParseText(sourceCode); // SyntaxTreeのルートを取得 CompilationUnitSyntax root = syntaxTree.GetCompilationUnitRoot(); // 専用の〇〇Syntexに型変換をする // MemberDeclarationSyntaxからNamespaceDeclarationSyntaxに型変換 var syntex = (NamespaceDeclarationSyntax)root.Members[0]; // Sample (=namespaceの名前) Console.WriteLine(syntex.Name);
Syntax Treeの走査
CSharpSyntaxWalkerを用いることでSyntax Treeの中で条件に会うようなノードを見つけ出すことができます。
CSharpSyntaxWalker Class (Microsoft.CodeAnalysis.CSharp) | Microsoft Learn
class MethodCollector : CSharpSyntaxWalker { public ICollection<MethodDeclarationSyntax> Usings { get; } = new List<MethodDeclarationSyntax>(); public override void VisitMethodDeclaration(MethodDeclarationSyntax node) { Usings.Add(node); } }
var sourceCode = """ namespace Sample { public class SampleClass { public static string Execute() { return "Hello, World!"; } public static string Hoge() { return "Hoge"; } } } """; // sourceCodeから構文木(SyntaxTree)を生成 SyntaxTree syntaxTree = CSharpSyntaxTree.ParseText(sourceCode); // SyntaxTreeのルートを取得 CompilationUnitSyntax root = syntaxTree.GetCompilationUnitRoot(); // 定義したMethodCollectorを利用 var collector = new MethodCollector(); collector.Visit(root); // Execute, Hoge Console.WriteLine(string.Join(", ", collector.Usings.Select(x => x.Identifier.ToString())));
C#のバージョン指定
CSharpSyntexTree.ParseTextメソッドの引数にCSharpParseOptionsを渡すことで、Syntex Treeを構築する際のC#のバージョンを指定したりすることができます。
// C#12を指定 CSharpParseOptions options = CSharpParseOptions.Default.WithLanguageVersion(LanguageVersion.CSharp12); // sourceCodeから構文木(SyntaxTree)を生成 SyntaxTree syntaxTree = CSharpSyntaxTree.ParseText(sourceCode, options);
ちなみにCSharpParseOptions.Defaultは単にnew CSharpParseOptions()をしたときと同じ内容です。
public CSharpParseOptions( LanguageVersion languageVersion = LanguageVersion.Default, DocumentationMode documentationMode = DocumentationMode.Parse, SourceCodeKind kind = SourceCodeKind.Regular, IEnumerable<string>? preprocessorSymbols = null) : this(languageVersion, documentationMode, kind, preprocessorSymbols.ToImmutableArrayOrEmpty<string>(), (IReadOnlyDictionary<string, string>) ImmutableDictionary<string, string>.Empty) { }
コンパイル
CSharpCompilation.Createメソッド + CSharpCompilation.Emitメソッドを利用することでコンパイルを実行できます。
// sourceCodeから構文木(SyntaxTree)を生成 SyntaxTree syntaxTree = CSharpSyntaxTree.ParseText(sourceCode, options); // コンパイル準備 // Createメソッドの引数にAssemblyNameを指定する CSharpCompilation compilation = CSharpCompilation.Create("Sample") // stringの定義が含まれているアセンブリを依存先として設定する .AddReferences(MetadataReference.CreateFromFile(typeof(string).Assembly.Location)) // コンパイル後はDLLにする(ConsoleAppにしたりとか設定できる) .WithOptions(new CSharpCompilationOptions(OutputKind.DynamicallyLinkedLibrary)) // syntaxTreeをコンパイル対象に設定 .AddSyntaxTrees(syntaxTree); // コンパイル実行 using (MemoryStream stream = new MemoryStream()) { EmitResult emitResult = compilation.Emit(stream); if (!emitResult.Success) { throw new ArgumentException("Compile error occured."); } Console.WriteLine("コンパイル成功!"); }
CSharpCompilation.Createにてコンパイル対象のSyntaxTreeの設定と依存先のアセンブリの設定などを設定します。
次にCompilation.Emitにてコンパイルした結果のソースコードはStreamに書き込まれます。今回はMemoryStreamなのでメモリです。
またEmitResultにてコンパイルの結果成功したのかどうかなどが書き込まれます。
MetadataReferenceで依存先のアセンブリを追加
コンパイルするコードが他のアセンブリに依存している場合は、それをCSharpCompilation.Createの際に設定してあげる必要があります。上のサンプルではstring・Consoleに依存しているため、MetadataReference.CreateFromFile(typeof(string).Assembly.Location)を設定しています。
これを適切に設定しないとコンパイルができないので注意してください。
CSharpCompilationOptionsでコンパイル後のアセンブリの種類を設定
CSharpCompilationOptionsを用いてコンパイル後のアセンブリの種類を設定します。
// Microsoft.CodeAnalysisより一部抜粋 public enum OutputKind { ConsoleApplication = 0, WindowsApplication = 1, DynamicallyLinkedLibrary = 2, NetModule = 3, WindowsRuntimeMetadata = 4, WindowsRuntimeApplication = 5, }
コンパイル結果を調べる
コンパイルの後、その中身を取り出してみたいと思います。
using (var stream = new MemoryStream()) { var emitResult = compilation.Emit(stream); foreach (var diagnostic in emitResult.Diagnostics) { var pos = diagnostic.Location.GetLineSpan(); var location = "(" + pos.Path + "@Line" + (pos.StartLinePosition.Line + 1) + ":" + (pos.StartLinePosition.Character + 1) + ")"; Console.WriteLine($"[{diagnostic.Severity}, {location}] {diagnostic.Id}, {diagnostic.GetMessage()}"); } if (!emitResult.Success) { throw new ArgumentException("Compile error occured."); } stream.Seek(0, SeekOrigin.Begin); var assembly = AssemblyLoadContext.Default.LoadFromStream(stream); // 読み込んだコードを実行 var classType = assembly.GetType("Sample.SampleClass"); var methodType = classType.GetMethod("Execute"); var instance = Activator.CreateInstance(classType); Console.WriteLine(methodType.Invoke(instance, null)); }
また前回はEmitResult.Successしか確認していませんでしたが、EmitResult.Diagnosticsを用いることで、コンパイル中に発生したエラー情報を取り出すことができます。
最後にStreamに書き込まれたデータからアセンブリをロードします。注意点として、Stream.Seekでストリームの読み取り位置を最初に戻してあげてください。
stream.Seek(0, SeekOrigin.Begin); var assembly = AssemblyLoadContext.Default.LoadFromStream(stream);
あとはリフレクションで情報を取ってこれます。
Unityで扱う場合
ちなみにUnityではUPMでcom.unity.code-analysisを指定することでRoslynを同様に利用することができるようになります。