しばやん雑記

ASP.NET とメイドさんが大好きなフリーランスのプログラマーのブログ

OWIN を使って IronPython と IronRuby をホスティングしてみる

OWIN - Open Web Interface for .NET とは何か? - しばやん雑記 で調べた OWIN ですが、これを使えば .NET 以外の言語でも動かせるんじゃないかと思いました。

例えば IronPython や IronRuby は .NET から操作可能なホスティング API を持っているので、これを使って直接ランタイムを叩けば似非 Fast CGI みたいなこと出来るんじゃね?ということで実際に試してみました。

まずは NuGet を使って必要なパッケージをインストールします。

Install-Package Microsoft.Owin.Hosting -pre
Install-Package Microsoft.Owin.Host.HttpListener -pre

Install-Package IronRuby
Install-Package IronPython

最初に IronRuby をインストールしないと DLR のバージョンが IronPython のが新しいので動かなくなります。

まずはハンドラ以外を作成しておきます。基本的には前回のコードをほぼ同じですが、RythonHandler と RubyHandler を Use メソッドで呼び出しています。この二つのクラスはこれから作成していきます。

class Program
{
    static void Main(string[] args)
    {
        using (WebApplication.Start<Startup>("http://localhost:8080/"))
        {
            Console.ReadKey();
        }
    }
}

public class Startup
{
    public void Configuration(IAppBuilder app)
    {
        app.Use(typeof(PythonHandler));
        app.Use(typeof(RubyHandler));
    }
}

これでホスティングする準備はできたので、ハンドラを作っていきます。ちなみに IronPython と IronRuby は DLR 上に構築されているので、基本的なホスティング API は共通となっています。同じような処理を二つ作りたくないので、共通のクラスを作って継承で Python と Ruby のそれぞれのハンドラを用意したいと思います。

今回は IronPython と IronRuby とは CGI を使ってやり取りすることにします。実際にはいろんな変数を用意しないといけないんですが、今回はとりあえず出力が出来ればいいので、ヘッダーの出力周りだけを適当に実装しました。*1

public abstract class ScriptingHandler
{
    protected ScriptingHandler(ScriptEngine scriptEngine, Func<IDictionary<string, object>, Task> next)
    {
        _next = next;
        _scriptEngine = scriptEngine;
    }

    private readonly Func<IDictionary<string, object>, Task> _next;
    private readonly ScriptEngine _scriptEngine;

    private const string RootDirectory = @"...";

    public Task Invoke(IDictionary<string, object> environment)
    {
        var requestPath = (string)environment["owin.RequestPath"];

        if (requestPath.EndsWith("/"))
        {
            requestPath += "index" + _scriptEngine.Setup.FileExtensions[0];
        }

        var requestExtension = Path.GetExtension(requestPath);

        if (!_scriptEngine.Setup.FileExtensions.Contains(requestExtension))
        {
            return _next(environment);
        }

        var sourcePath = RootDirectory + requestPath;

        if (!File.Exists(sourcePath))
        {
            return _next(environment);
        }

        environment["owin.ResponseStatusCode"] = 200;

        var stream = new MemoryStream();

        _scriptEngine.Runtime.IO.SetOutput(stream, Encoding.UTF8);

        _scriptEngine.ExecuteFile(sourcePath);

        stream.Seek(0, SeekOrigin.Begin);

        using (var reader = new StreamReader(stream, Encoding.UTF8))
        using (var writer = new StreamWriter((Stream)environment["owin.ResponseBody"]))
        {
            var responseHeaders = (IDictionary<string, string[]>)environment["owin.ResponseHeaders"];

            // ヘッダー解析
            while (true)
            {
                var line = reader.ReadLine();

                if (string.IsNullOrEmpty(line))
                {
                    break;
                }

                var tokens = line.Split(':');

                responseHeaders.Add(tokens[0], new[] { tokens[1].Trim() });
            }

            return writer.WriteAsync(reader.ReadToEnd());
        }
    }
}

一応リクエストされたパスの拡張子やファイルの存在チェックをしてからランタイムでコンパイルして実行しています。CGI では標準出力に HTML などが出力されるので、ScriptEngine の設定を弄ってメモリ上に書きだすようにしています。

後は Python と Ruby のハンドラを作るだけです。こっちは説明要らないと思いますので飛ばします。

public class PythonHandler : ScriptingHandler
{
    public PythonHandler(Func<IDictionary<string, object>, Task> next)
        : base(Python.CreateEngine(), next)
    {
    }
}

public class RubyHandler : ScriptingHandler
{
    public RubyHandler(Func<IDictionary<string, object>, Task> next)
        : base(Ruby.CreateEngine(), next)
    {
    }
}

実行環境の準備は完了したので、実際に実行される Python と Ruby のコードを準備します。と言っても、以下のような非常に単純な Hello world なコードです。

print "Content-Type: text/html\n";
print "Hello IronPython!";
print "Content-Type: text/html\n\n";
print "Hello IronRuby!";

それぞれ Index.py と Index.rb という名前で保存して、ブラウザで表示を確認してみました。

f:id:shiba-yan:20130304214527p:plain

ちゃんと /index.py と /index.rb でメッセージが表示されていますね。割と気合を入れた割に実用性が皆無なネタでした。

*1:空行の前をヘッダー、後をコンテンツとする簡単なやつ