アジョブジ星通信

進捗が出た頃に更新されるブログ。

DNXでC#以外の言語をコンパイルしてみよう

f:id:azyobuzin:20151118224028p:plain

DNXの可能性

DNX で C# 以外の言語を使えるのかと思って調べていたところ、 F# を動作させるためのライブラリが存在していることを発見しました。

(試してみましたが、 F# コンパイラが内部で NullReferenceException 起こしてうまくいかなかったです。)

この作者のブログには、 DNX で F# を動かしてみての感想などいろいろ書いてあるので参考にどうぞ。

で、これの何がイケてるかというと、コンパイラ部分も NuGet から取得できるということです。つまり DNX さえあればコンパイラをインストールすることなく、その場でコンパイルできてしまうんですね。夢が広がる。

よろしいならば実装(インプリメント)だ

というわけで何か試すのにいい言語はないかなぁと考えたところで、 .NET で動くほかの言語は Nemerle くらいしか触ったことなかったので Nemerleコンパイルできるようにしてみました。

完成品


rc1-final ブランチは DNX 1.0.0-rc1-final、 rc2-16177 ブランチは 1.0.0-rc2-16177 に対応しています。

NuGet パッケージは MyGet にあげてあります。

https://www.myget.org/F/azyobuzin/api/v3/index.json

をパッケージソースに追加して、 DNX 1.0.0-rc1-final ならば 1.2.0-* を、 1.0.0-rc2-16177 ならば 1.1.0-* を使用してください。

なお、 DNX の CLR 版にしか対応していないので、 CoreCLR 版から呼び出すと NullReferenceException で落ちます。 Nemerle コンパイラがポータブルじゃないからしかたないね。

IProjectCompiler を実装する

DNX は Microsoft.Dnx.Compilation.IProjectCompiler*1 を実装したクラスを使ってプロジェクトをコンパイルします。これは C# の場合でも同様ですので、 C# での実装を見てみましょう。
dnx/RoslynProjectCompiler.cs at 1.0.0-rc1 · aspnet/dnx · GitHub

雛形としてはこのようになります。

using System;
using System.Collections.Generic;
using Microsoft.Dnx.Compilation;

namespace YourNamespace
{
    public class YourProjectCompiler : IProjectCompiler
    {
        public IMetadataProjectReference CompileProject(
            CompilationProjectContext projectContext,
            Func<LibraryExport> referenceResolver,
            Func<IList<ResourceDescriptor>> resourcesResolver)
        {
            return /* Compile */ ;
        }
    }
}

RoslynProjectCompiler では、コンストラクタで大量のインターフェイスを受け取っていますが、これは依存性注入で自動的にサービスが引数に与えられます。

CompileProject メソッドで実際にコンパイルを行います。しかし、これは意味解析までを行うという意味です。というのも、ここで返す IMetadataProjectReference には EmitAssembly(string outputPath)EmitReferenceAssembly(Stream stream) といったメソッドがあるので、アセンブリの出力はこれらのメソッドが呼ばれた時に行われるのがベストです。(でも Nemerle コンパイラの中身までいじるのが面倒だったので、 NemerleDnxProvider ではファイルに出力して、読み込んだりコピーしたりするようにしました)

参照アセンブリreferenceResolver() で、マニフェストリソースは resourcesResolver() で取得できます。その他の情報は projectContext から取得してください。

なお、参照のデータは IMetadataReference なので、他のインターフェイスにキャストしないとデータを引き出せません。
dnx/MetadataReferenceExtensions.cs at 1.0.0-rc1 · aspnet/dnx · GitHub

IMetadataProjectReference を実装する

Microsoft.Dnx.Compilation.IMetadataProjectReferenceCompileProject の戻り値の型です。このインターフェイスは意味解析の結果を表します。

C# ではこのように実装されています。
dnx/RoslynProjectReference.cs at 1.0.0-rc1 · aspnet/dnx · GitHub

雛形としてはこのようになります。

using System;
using System.Collections.Generic;
using System.IO;
using System.Reflection;
using Microsoft.Dnx.Compilation;
using Microsoft.Extensions.PlatformAbstractions;

namespace YourNamespace
{
    public class YourProjectReference : IMetadataProjectReference
    {
        // 意味解析のエラー情報
        private readonly DiagnosticResult diagnostics;

        public string Name { get; } // projectContext.Target.Name

        public string ProjectPath { get; } // projectContext.ProjectFilePath

        // dnu build で呼ばれます。
        public DiagnosticResult EmitAssembly(string outputPath)
        {
            Directory.CreateDirectory(outputPath);

            // DLL ファイルを出力

            return new DiagnosticResult(success: true, diagnostics: /* エラーリスト */);
        }

        // 他のプロジェクトから参照されたときに呼ばれます。
        public void EmitReferenceAssembly(Stream stream)
        {
            if (!this.diagnostics.Success)
                // YourCompilationException は ICompilationException を実装するといいかも
                throw new YourCompilationException(this.diagnostics.Diagnostics);

            // stream にアセンブリを出力する
        }

        public DiagnosticResult GetDiagnostics() => this.diagnostics;

        public IList<ISourceReference> GetSources()
        {
            // コンパイルに使用したソースコードを返します。
            // コンパイル時に使った ISourceReference を保持しておいて返すか
            // または new Microsoft.Dnx.Compilation.SourceFileReference(string path) で作成できます。
        }

        // dnx command から実行したときに呼ばれます。
        public Assembly Load(AssemblyName assemblyName, IAssemblyLoadContext loadContext)
        {
            if (!this.diagnostics.Success)
                throw new YourCompilationException(this.diagnostics.Diagnostics);

            // return loadContext.LoadFile(string path);
            // return loadContext.LoadStream(Stream assemblyStream, Stream assemblySymbols);
        }
    }
}

アセンブリ出力をここで行う場合は、C# の実装を参考にうまいことファイルやメモリにアセンブリを出力してください。すでにファイルに出力してある場合は、ファイルのコピーなどが主な仕事になります。

使ってみよう

ProjectCompiler が完成したら、他のプロジェクトから使用してみましょう。

作成したライブラリを参照に加え、 project.json に使用するコンパイラを記述します。

"dependencies": {
  "YourLanguage": {
    "version": "1.0.0-*",
    "type": "build"
  }
},
"compiler": {
  "name": "言語名",
  "compilerAssembly": "YourLanguage",
  "compilerType": "YourNamespace.YourProjectCompiler"
},
"compile": "./**/*.extension"

これで準備は完了しました。 dnu build なり dnx command なりでテストしてみましょう。

終わり

と説明したところで、こんなこと誰もやりませんよね。でも、 DSL を簡単にコンパイルできる環境を作れる、しかも NuGet からパッケージを取得するだけ、みたいなことができるようになっているわけです。誰か面白い使い方考えてください。僕は疲れました。以上です。

*1:RC2 では名前空間が異なります。この記事では RC1 に合わせて説明していきますので、 RC2 で試す場合は適当に書き換えてください。