ぷろじぇくと、みすじら。

雑なHTTPサーバーをC#で書いてみる

Created at:

人生のうち一度はHTTPサーバーを書くという話をしたら意外とそうでもなくてあれー?ってなったのですが、それはそれとして。

「HTTPサーバーには人生の大切な事が全てつまっている」と言った人がいるかどうかは定かではないですが、 雑なサーバーを書いてみるのはまあまあ簡単で理解も進んで面白いのでおすすめですよ、というわけで書いてみましょうという話。

アプリにWebサーバーを組み込みたくなったりして雑なHTTPサーバーを書いてみることというのがたまにあります。 そんな時、現代ではどの言語やプラットフォームでもパッケージやライブラリであっという間にサーバーを組み込めるので自分で書くことはないかもしれません。

ですが今時だとArduinoのような省メモリであることを必要とされる環境でもHTTPサーバーが必要になったとき、 ほぼ固定機能のHTTPサーバーを実装してAPIやWebページからコントロールできるように…ということもよくあると思いますので経験や知識は無駄ではないと思います。多分。

この記事ではC#で書きますが、お好みのプラットフォームなどで書いてみるとよいかと思います。

対象読者

雑なHTTPサーバーとは

おおよそこんな感じ。

要するにお遊びなのでRFCとかも読まないし実際適当です。ほとんどのブラウザーなどからおおよそアクセスして何か表示できればよい程度を目指します。

ただし外部に出ると死ぬ(セキュリティーとか)ような感じもあるのでまじめに使う前提ではありません。

流れとHTTPについて

流れについて一応雑に説明すると…

  1. HTTPサーバー: クライアントからの接続を待ち受ける
  2. HTTPクライアント: サーバーへ接続する
  3. HTTPサーバー: クライアントからの接続を受け入れる
  4. HTTPクライアント: サーバーへリクエストを送信する
  5. HTTPサーバー: クライアントからのリクエストを解釈する
  6. HTTPサーバー: レスポンスをクライアントへ送信する
  7. HTTPクライアント: レスポンスを解釈する

となっています。ですのでHTTPサーバー部分を作るので以下の部分を作ればよいことになります。

  1. HTTPサーバー: クライアントからの接続を待ち受ける
  2. HTTPサーバー: クライアントからの接続を受け入れる
  3. HTTPサーバー: クライアントからのリクエストを解釈する
  4. HTTPサーバー: レスポンスをクライアントへ送信する

HTTPについてですが、ご存知の通りリクエストとレスポンスというメッセージを送受信するプロトコルです。

リクエストメッセージはこんな感じです。

POST / HTTP/1.0[CR][LF]
User-Agent: Nantoka/1.0[CR][LF]
X-Nantoka: Kantoka[CR][LF]
[CR][LF]
Body

CR+LFで区切られていて、CR+LFだけの行が来るまでがヘッダー部分、それ以降がボディ部分となっていてあったりなかったりする部分です。

レスポンスメッセージはこんな感じ。おおよそ同じです。一行目(スタート行)がちょっと違いますがそれ以外の形は同じです。

HTTP/1.0 200 OK[CR][LF]
Server: Nantoka/1.0[CR][LF]
X-Hauhau: Maumau[CR][LF]
[CR][LF]
Body

詳しくはRFC 7230をご覧ください(HTTP 1.1ですが)。実は折り返せたりして(obsoleteだけど)、本当にまじめに実装しようとすると結構面倒だったりします。

サーバーを書いてみる

と、雑な説明をしたところでサーバーを書いてみます。

クライアントからの接続を待ち受ける

まずは「クライアントからの接続を待ち受ける」部分を作ってみます。

クライアントからの接続というのはTCP接続を待ち受けるいわゆるLISTENするということで一般的にはSocketを使ったりします。 とはいえSocket直接というのはプリミティブすぎるので.NET FrameworkにはTcpListenerクラスという便利クラスが用意されているのでそれを使うと簡単です。

var tcpListener = new TcpListener(IPAddress.Loopback, 12345);
tcpListener.Start();

// Startメソッドはブロッキングではなくて、そのまま終わってしまうので適当に止めておく
Console.ReadLine();

接続待ち受けを開始するにはTcpListenerをどのIPアドレスとポートで待ち受けるか指定して作って、Startメソッドを呼び出すだけです。

ここではLoopback(127.0.0.1と::1、いわゆるlocalhost)のポート12345で待ち受ける指定にしています。 Loopbackにしておくと自身のコンピューターからしか接続できない状態となりますが、Loopback以外にはAny(何でも)や特定のIPアドレス(イーサネットアダプタのアドレス)などを指定できます。

そして実行してブラウザーから http://localhost:12345/ にアクセスすると……接続できませんとも言わずに何も起きないはずです。

クライアントからの接続を受け入れる

TcpListenerで待ち受ける状態になっても、「クライアントからの接続を受け入れる」処理がないのでクライアントはまだ待たされている状態です。

というわけで受け入れる処理を書きます。AcceptTcpClientAsyncメソッドを呼び出すことで受け入れが行われ、クライアントとの接続であるTcpClientクラスのインスタンスが返ってきます。 返ってきたTcpClientはIDisposableを実装しているのでusingしておきます。Disposeすればクライアントとの接続を切断できます。

var tcpListener = new TcpListener(IPAddress.Loopback, 12345);
tcpListener.Start();

using (var tcpClient = await tcpListener.AcceptTcpClientAsync())
{
    // 接続元を出力しておく
    Console.WriteLine(tcpClient.Client.RemoteEndPoint);
}

// Startメソッドはブロッキングではなくて、そのまま終わってしまうので適当に止めておく
Console.ReadLine();

このコードを実行してブラウザーから http://localhost:12345/ にアクセスすると……多分すぐにアクセスできませんでしたというメッセージが表示されるようになるはずです。

これでデータの流れはありませんが接続の「待ち受け」→「受け入れ」→「切断」の一連ができました。

しかし、この実装では1回しか受け入れられないのでもう一度アクセスすると突き刺さります。ということで何度も受け入れられるように無限ループにしておきます。

var tcpListener = new TcpListener(IPAddress.Loopback, 12345);
tcpListener.Start();

while (true)
{
    using (var tcpClient = await tcpListener.AcceptTcpClientAsync())
    {
        // 接続元を出力しておく
        Console.WriteLine(tcpClient.Client.RemoteEndPoint);
    }
}

クライアントからのリクエストを解釈する(フリをする)

次はリクエストを読み出します。が、とりあえずいったん読んで解釈したフリをすることにします。

ほとんどのブラウザーやユーザーエージェントのリクエストはGETリクエストであって、返したものをそのまま表示したり解釈したりするので最悪読み捨てても大丈夫だったりします。

クライアントとのデータの送受信はTcpClientのGetStreamメソッドで得られるStreamのインスタンスを利用します。このStreamを読めばクライアントから受信、書き込めばクライアントへ送信ということです。もちろんStreamなのでStreamReader/StreamWriterを通すと簡単に読み書きできます。

StreamReaderを作ったら、ReadLineAsyncメソッドでヘッダー部分の終わり、つまりCR+LFのみの行(=空行)まで読み込みます。 その際、読み込んだ行をコンソールなどにデバッグ目的で適当に書き出しておきます。

var tcpListener = new TcpListener(IPAddress.Loopback, 12345);
tcpListener.Start();

while (true)
{
    using (var tcpClient = await tcpListener.AcceptTcpClientAsync())
    using (var stream = tcpClient.GetStream())
    using (var reader = new StreamReader(stream))
    using (var writer = new StreamWriter(stream))
    {
        // 接続元を出力しておく
        Console.WriteLine(tcpClient.Client.RemoteEndPoint);

        // ヘッダー部分を全部読む
        string line;
        do
        {
            line = await reader.ReadLineAsync();
            // 読んだ行を出力しておく
            Console.WriteLine(line);
        } while (!String.IsNullOrWhiteSpace(line));
    }
}

このコードを実行してブラウザーから http://localhost:12345/ にアクセスしてみると、コンソールなどに以下のようなブラウザーのリクエストが出力されるはずです。

GET / HTTP/1.1
Accept: text/html, application/xhtml+xml, image/jxr, */*
Accept-Language: en-US,en;q=0.7,ja;q=0.3
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/46.0.2486.0 Safari/537.36 Edge/13.14251
Accept-Encoding: gzip, deflate
Host: localhost:12345
Connection: Keep-Alive

とりあえず今のところはこれでよいということにします。

レスポンスをクライアントへ送信する

いよいよクライアントに対してレスポンスを返します。

レスポンスは1行目がステータスライン(ステータスコードとかを含む行)、その後ヘッダー部分、ボディ部分を送信すれば出来上がりです。つまりStreamWriterに対して書き込んでいくだけの簡単なお仕事です。

200なステータスで、Content-Typeが指定されていて、ボディがあれば大体受け入れてもらえます。というわけでこんな感じのレスポンスを返すということにします。

HTTP/1.0 200 OK
Content-Type: text/plain; charset=UTF-8

Hello! Konnichiwa!

これを今までのコードに組み込むとこうなります。

var tcpListener = new TcpListener(IPAddress.Loopback, 12345);
tcpListener.Start();

while (true)
{
    using (var tcpClient = await tcpListener.AcceptTcpClientAsync())
    using (var stream = tcpClient.GetStream())
    using (var reader = new StreamReader(stream))
    using (var writer = new StreamWriter(stream))
    {
        // 接続元を出力しておく
        Console.WriteLine(tcpClient.Client.RemoteEndPoint);

        // ヘッダー部分を全部読んで捨てる
        string line;
        do
        {
            line = await reader.ReadLineAsync();
            // 読んだ行を出力しておく
            Console.WriteLine(line);
        } while (!String.IsNullOrWhiteSpace(line));

        // レスポンスを返す
        // ステータスライン
        await writer.WriteLineAsync("HTTP/1.0 200 OK");
        // ヘッダー部分
        await writer.WriteLineAsync("Content-Type: text/plain; charset=UTF-8");
        await writer.WriteLineAsync(); // 終わり
        // これ以降ボディ
        await writer.WriteLineAsync("Hello! Konnichiwa! @ " + DateTime.Now); // 動的感を出す
    }
}

そしてこのコードを実行してブラウザーから http://localhost:12345/ にアクセスしてみます。"Hello! Konnichiwa!"と表示されたら成功です。

というわけで超雑なHTTPサーバーができました。なんと32行。この通り、データを返すだけならばとても簡単ですね。あとはこれをベースにいじって遊んでみるとよいのではないでしょうか。

まとめ

HTTPサーバーを書いてみると、クライアントとサーバー、接続やリクエスト、レスポンスといったものを少し身近に感じることができるようになると思います。 一度書いていじってみるのは個人的にはとてもおすすめなのです。Extraとしてちょっといじったものを載せておきます。

Extra: リクエストを解釈する

ヘッダー部分を読み捨てましたが、ヘッダー部分(とリクエストライン)を解釈するともっとそれっぽくなります。

リクエストラインやヘッダー部分にはリクエストパスやリクエストに関する情報が含まれています。

var tcpListener = new TcpListener(IPAddress.Loopback, 12345);
tcpListener.Start();

while (true)
{
    using (var tcpClient = await tcpListener.AcceptTcpClientAsync())
    using (var stream = tcpClient.GetStream())
    using (var reader = new StreamReader(stream))
    using (var writer = new StreamWriter(stream))
    {
        // ヘッダー部分を全部読んでおく
        var requestHeaders = new List<string>();
        while (true)
        {
            var line = await reader.ReadLineAsync();
            if (String.IsNullOrWhiteSpace(line))
            {
                break;
            }
            requestHeaders.Add(line);
        }

        // 一行目(リクエストライン)は [Method] [Path] HTTP/[HTTP Version] となっている
        var requestLine = requestHeaders.FirstOrDefault();
        var requestParts = requestLine?.Split(new[] { ' ' }, 3);
        if (!requestHeaders.Any() || requestParts.Length != 3)
        {
            await writer.WriteLineAsync("HTTP/1.0 400 Bad Request");
            await writer.WriteLineAsync("Content-Type: text/plain; charset=UTF-8");
            await writer.WriteLineAsync();
            await writer.WriteLineAsync("Bad Request");
            return;
        }

        // 接続元を出力しておく
        Console.WriteLine("{0} {1}", tcpClient.Client.RemoteEndPoint, requestLine);

        // パス
        var path = requestParts[1];
        if (path == "/")
        {
            // / のレスポンスを返す
            await writer.WriteLineAsync("HTTP/1.0 200 OK");
            await writer.WriteLineAsync("Content-Type: text/plain; charset=UTF-8");
            await writer.WriteLineAsync();
            await writer.WriteLineAsync("Hello! Konnichiwa! @ " + DateTime.Now); // 動的感を出す
        }
        else if (path == "/hauhau")
        {
            // /hauhau のレスポンスを返す
            await writer.WriteLineAsync("HTTP/1.0 200 OK");
            await writer.WriteLineAsync("Content-Type: text/plain; charset=UTF-8");
            await writer.WriteLineAsync();
            await writer.WriteLineAsync("Hauhau!!");
        }
    }
}

パスが取れれば、ルーティングのテーブルを作って振り分けて…とかもやってみたくなりますね。

Extra: 複数リクエストをさばく

今までの実装は async/await を使ってはあったもののリクエストを受けて流す部分は1つなので並列で処理を実行することができません。 例えばある一つのリクエストの処理に時間がかかっている間はほかのリクエストを受け入れることができなくなるということです。

そこで.NET FrameworkにはTaskという便利なものがあるので空いているスレッドに処理を回すことができます。

var tcpListener = new TcpListener(IPAddress.Loopback, 12345);
tcpListener.Start();

while (true)
{
    var tcpClient = await tcpListener.AcceptTcpClientAsync();

    // 接続を受け入れたら後続の処理をTaskに放り込む
    Task.Run(async () =>
    {
        using (var stream = tcpClient.GetStream())
        using (var reader = new StreamReader(stream))
        using (var writer = new StreamWriter(stream))
        {
            // ヘッダー部分を全部読んでおく
            var requestHeaders = new List<string>();
            while (true)
            {
                var line = await reader.ReadLineAsync();
                if (String.IsNullOrWhiteSpace(line))
                {
                    break;
                }
                requestHeaders.Add(line);
            }

            // 一行目(リクエストライン)は [Method] [Path] HTTP/[HTTP Version] となっている
            var requestLine = requestHeaders.FirstOrDefault();
            var requestParts = requestLine?.Split(new[] { ' ' }, 3);
            if (!requestHeaders.Any() || requestParts.Length != 3)
            {
                await writer.WriteLineAsync("HTTP/1.0 400 Bad Request");
                await writer.WriteLineAsync("Content-Type: text/plain; charset=UTF-8");
                await writer.WriteLineAsync();
                await writer.WriteLineAsync("Bad Request");
                return;
            }

            // 接続元を出力しておく
            Console.WriteLine("{0} {1}", tcpClient.Client.RemoteEndPoint, requestLine);

            // パス
            var path = requestParts[1];
            if (path == "/")
            {
                // / のレスポンスを返す
                await writer.WriteLineAsync("HTTP/1.0 200 OK");
                await writer.WriteLineAsync("Content-Type: text/plain; charset=UTF-8");
                await writer.WriteLineAsync();
                await writer.WriteLineAsync("Hello! Konnichiwa! @ " + DateTime.Now); // 動的感を出す
            }
            else if (path == "/hauhau")
            {
                // 遅い処理をシミュレートするマン
                await Task.Delay(5000); // 5秒

                // /hauhau のレスポンスを返す
                await writer.WriteLineAsync("HTTP/1.0 200 OK");
                await writer.WriteLineAsync("Content-Type: text/plain; charset=UTF-8");
                await writer.WriteLineAsync();
                await writer.WriteLineAsync("Hauhau!!");
            }
        }
    });
}

まじめにやるときにはTaskと接続の管理とかが必要ですが、こんな感じで並列化ができます。

ちなみに~Async(非同期メソッド)の場合、IO待ちの際にスレッドを手放すことが多くなるので効率よく処理できるようになります。

ご報告

Created at:

ガルパンはいいぞ

DNS-SD(Bonjour)をUniversal Windows Platformから利用してサービスを探す

Created at:

Windows 10からDNS-SD/mDNS、いわゆるAppleのBonjourをネイティブサポートするようになりました。 要するに何ができるかというとIPアドレスやホスト名を知らなくても同じネットワークにいるデバイスとサービスを探すことができる仕組みです。

例えば、iTunesが他のコンピューターのiTunes 共有ライブラリを見つけるといったことに利用されています。そのちょっと便利な機能をUWPのAPIを通して利用できるようになっています。

アプリからDNS-SDでサービスを探す

さて、どうやって使うかですがまあまあ簡単で、通常のデバイスを列挙するのと同様にDeviceInformation クラスを利用します……が、列挙しようと思ってFindAllAsyncメソッドを使うと何も返ってこず(awaitすると進まない)、暫くするとエラーが出ます。

この挙動が正しいのかどうかよくわかりませんが、ともかく何やらうまく動きません。

そこでDeviceInformation.CreateWatcher メソッドを利用します。 このメソッドはデバイスがネットワークに追加されたり削除されたりというのを監視する方法です。この方法でならば列挙できます。

Watcherを作ってStartメソッドを呼び、監視を始めるとデバイスが見つかりAddedイベントが呼び出されますのでそこで適当に情報を保持しておけばよいです。 ちなみに監視して列挙終了時の EnumerationCompleted イベントが発生しないので完了を待つというのはやめた方がいいでしょう(多分それがFindAllAsyncが返ってこない原因な気がします)。

とりあえず、ネットワークにあるAndroid TV(正確にはAndroid TVが持つリモコン用サービス)を列挙するソースコードはこんな感じなります。

using Windows.Devices.Enumeration;

// ...

// DNS-SDのサービス名
var serviceName = "_androidtvremote._tcp";
// DNS-SDを利用する
var protocolId = "{4526e8c1-8aac-4153-9b16-55e86ada0e54}";
// デバイス検索文字列
var aqsFilter = $"System.Devices.AepService.ProtocolId:={protocolId} AND System.Devices.Dnssd.Domain:=\"local\" AND System.Devices.Dnssd.ServiceName:=\"{serviceName}\"";
// 取得するプロパティ
var properties = new[] {
    "System.Devices.Dnssd.HostName", // ホスト名 (自称)
    "System.Devices.IpAddress", // IPアドレスの配列(string[])
};

var watcher = DeviceInformation.CreateWatcher(aqsFilter, properties, DeviceInformationKind.AssociationEndpointService);
watcher.Added += (sender, args) =>
{
    Debug.WriteLine("Added: " + args.Id);
    foreach (var prop in args.Properties)
    {
        var value = prop.Value is string[] ? (String.Join(", ", (string[])prop.Value)) : prop.Value + "";
        Debug.WriteLine($"  - {prop.Key}: {value}");
    }
};
watcher.Updated += (sender, args) =>
{
    Debug.WriteLine("Updated: " + args.Id);
    foreach (var prop in args.Properties)
    {
        var value = prop.Value is string[] ? (String.Join(", ", (string[])prop.Value)) : prop.Value + "";
        Debug.WriteLine($"  - {prop.Key}: {value}");
    }
};
watcher.Start();

実行するとこんな感じにホスト名やIPアドレスなどの情報が取れます。

DnsSd#KJ-55X8500C._androidtvremote._tcp.local#0
  - System.ItemNameDisplay: KJ-55X8500C
  - System.Devices.DeviceInstanceId: 
  - System.Devices.Icon: 
  - System.Devices.GlyphIcon: 
  - System.Devices.InterfaceEnabled: 
  - System.Devices.IsDefault: 
  - System.Devices.PhysicalDeviceLocation: 
  - System.Devices.Aep.CanPair: False
  - System.Devices.Aep.IsPaired: False
  - {A35996AB-11CF-4935-8B61-A6761081ECDF} 18: 
  - {A35996AB-11CF-4935-8B61-A6761081ECDF} 13: 
  - System.Devices.Dnssd.HostName: Android.local
  - System.Devices.IpAddress: 192.168.1.100

アプリはこの情報をもとにサーバー(サービス)に接続すれば、相手の名前をあらかじめ知っていなくてもアクセスできる、というわけです。

手順をおさらいすると、DeviceWatcherを作るCreateWatcherメソッドの引数に渡すAQS文字列と呼ばれる検索フィルター文字列に、 DNS-SDプロトコル({4526e8c1-8aac-4153-9b16-55e86ada0e54})を使うことを指定して、さらにDNS-SDのサービス名(_daap.tcpなど)を指定して、検索を実行すれば見つかるという形になっています。例えばサービス名に_daap.tcpを指定するとネットワーク上のiTunesの共有ライブラリを探すことができます。

ちなみにこのUniversalなAPIはいわゆる従来のデスクトップアプリケーションからも呼び出すことができます。

制限事項

得られたホスト名(.localで終わる名前)をHttpClientなどに渡しても名前解決されますが、たまに失敗するのでIPアドレスを使った方がいいかもしれません。

なお、どうやら試した範囲ではサービス名を指定せずにネットワークに存在するサービスをすべて列挙する、ということはできないようです。System.Devices.Dnssd.ServiceNameを指定しないで検索すると何も返ってこないことに注意が必要です。

また、Hyper-Vが有効の場合にも動作しないという話もあります。

ChakraCoreをビルドしてC#から使うはじめの一歩

Created at:

ついにChakraCoreが公開されたので、早速ビルドしてC#からとりあえずハローしてみます。

ChakraCore is 何

とその前にChakraCoreのおさらいです。

まずChakraCoreはInternet ExplorerやEdgeで使われているJavaScriptエンジン ChakraのWindows固有の機能を外したライブラリです。要するにV8とかJavaScriptCoreみたいなものです。

オープンソースになった後のロードマップが公開され、いろいろ書いてあります。

ChakraCoreのメリット

従来のChakraはWindowsまたはInternet Explorerと共にバージョンが管理されていました。 つまり現在、ChakraにはInternet Explorer 9, 10, 11(Windows Vista/7/2008/2008 R2)、Windows 8.1、10、10 Version 1511というバリエーションがあることになり、 それをアプリケーションに組み込んで利用したい場合にバージョンの差異が発生することになります。

そこでChakraCoreはWindowsやブラウザから分離したJavaScriptのエンジンとなったので、 アプリケーションに独立して組み込むことができるようになり、環境ごとのバージョンの差異に悩まされないで済むようになります。

そもそも従来のWindowsではWindows Scripting Hostを経由してエンジンを組み込むことができましたが、APIもCOMベースでJavaScriptのエンジンを使うということ以外を考慮していたりして使いづらく、エンジンも古いのでモダンなJavaScriptを動かすことができなかったという話もあります。

ChakraCoreをビルドする

2016年1月14日現在、WindowsサポートのみなのでGitHubのリポジトリをcloneし、BuildディレクトリにあるChakraCore.slnをVisual Studio 2015で開いてビルドすればよいです。

と、簡単そうな気がしたのですが日本語環境(CP932がデフォルト環境)ではソースコードがUnicodeじゃないよというWarning(C4819)が出て、WarningがErrorとして扱われてビルドが失敗します。幸い数は少ないのでBOM付きUTF-8で適当に保存しなおしましょう。

ビルドすると Build\VcBuild\bin\x64_debug などに ChakraCore.dll が生成されます。

ChakraCoreをC#から使う

ChakraCoreを無事ビルドできたらC#からChakraCoreを使ってみます。

ChakraCoreはJSRT API(JavaScript Runtime API)というAPIを持っているのでそれを利用してChakraCoreの機能を呼び出します。ちなみにJSRTはChakraCoreよりも以前からWindows 10以降(またはInternet Explorer 11以降)ではChakraを利用する手段として提供されています。

詳しくはMSDNのJavaScript ランタイムのホスト処理に書いてあります。

ただしJSRTは.NET Framework向けのAPIではないアンマネージドなAPIなため、P/Invokeでの呼び出しを行う必要があります。 幸いにもGitHubにChakra-Samples@Microsoftというリポジトリがあり、JSRTのC#のバインディングがすでに作られているのでこれを使います(自分で書こうとするとだいぶ大変です)。

というわけでChakra-Samplesをcloneやダウンロードしておきます。Chakra-SamplesにはHello WorldやHostingサンプルがあるのでまあそれでいいという話もあるのですが、最小限の構成を作ってみることにします。

準備

まず初めに適当にコンソールアプリケーションのプロジェクトを作ります。

プロジェクトを作ったら先ほどビルドしたChakraCore.dllをプロジェクトに追加し、プロパティでビルド時にコピーされるように設定します。

ちなみに普通にプロジェクトを作るとPrefer 32bitということで32bitプロセスとして起動されるのでx86版DLLをコピーしておきましょう。

次にChakra-SamplesからChakraCore Samples\JSRT Hosting Samples\C#\ChakraCoreHostにあるHostingフォルダをプロジェクトにコピーします。これがJSRTのバインディングです。

プロジェクト構成

ChakraCoreを実行してみる

というわけでまずはランタイム(=実行エンジン)を作り、実行用のコンテキストを作って実行に備えます。

// ランタイムを作る
JavaScriptRuntime runtime;
Native.JsCreateRuntime(JavaScriptRuntimeAttributes.None, null, out runtime);

// 実行コンテキストを作る
var context = runtime.CreateContext();

// 現在のスレッドの実行コンテキストをセットする
Native.JsSetCurrentContext(context);

次に実際にJavaScriptのコードを実行します。

// 実行するスクリプト
var script = @"
    class Greeter {
        hello() { return 'コンニチハ!'; }
    }

    new Greeter().hello();
";

// スクリプトのソースの位置を記録するためのコンテキスト
var currentSourceContext = JavaScriptSourceContext.FromIntPtr(IntPtr.Zero);
// スクリプトを実行する
JavaScriptValue result;
Native.JsRunScript(script, currentSourceContext++, "", out result);

JsRunScriptメソッドはランタイムもコンテキストも渡さず、現在のスレッドのコンテキストから良しなに実行するので少し不思議な感じがしますね。

正常に実行できれば戻り値がresult変数に格納されます。戻り値はJavaScriptの値となっているのでそこからさらにCLRオブジェクトへ変換します。ToStringメソッドがお手軽に変換してくれるので任せましょう。

// 戻り値をJavaScriptの値からCLRのStringに変換する
// ちなみにConvertToStringメソッドはJavaScriptのStringなので注意。
var resultString = result.ToString(); // Native.JsStringToPointer + Marshal.PtrToStringUni

// 出力
Console.WriteLine(resultString);

最後はお片付けです。

// 後片付け
Native.JsSetCurrentContext(JavaScriptContext.Invalid);
runtime.Dispose(); // IDisposableなのでusingでもいい

というわけでここまでのコードをまとめるとこんな感じ。

実行すると「コンニチハ!」という出力が出てくるかと思います。割とお手軽ですね。

なお実行してBadImageFormatExceptionAn attempt was made to load a program with an incorrect format. (Exception from HRESULT: 0x8007000B)と言われたらそれはx86/x64のDLLを間違えている可能性があります。

まとめ

もちろんエラーハンドリングや.NETからJavaScript側へオブジェクトの公開などもありますがとりあえず今回はここまでです。

意外と簡単に動かせるようになっているのでアプリケーションに組み込み用途にはいいかなと思うものの、ホストとChakraCoreとのやり取りが増えると割と高度なことを必要とするのでちょっと難易度高いかもという感じもします。

Visual Studio CodeのシンタックスハイライトをWebページで使う

Created at:

Visual Studio Codeのエディタ部分はVisual Studio Codeがリリースされるよりも前からいろいろなところで使われていて、 最初はVisual Studio Online "Monaco"のエディタ部分として公開されていました。

それから徐々にMicrosoftの中での利用範囲が広がりTypeScriptのPlaygroundやWinJSのPlayground、 ちょっと変わったところではInternet Explorer/EdgeのF12開発者ツールの中でも使われていたりします。

そして月日は流れて、Visual Studio Codeがオープンソースになってついにエディタ部分(Monaco Editor)が公開されました。

ということでそれを使う方法を調べていたのですが、たまたまシンタックスハイライトの機能が備わっているのを発見したので単体でも使えるようにするライブラリを作りました。

mayuki/Mimosa - GitHub

使い方

GitHubのReleasesに一式を固めたものがあるのでそれをとってきて適当に展開します。

<script src="/shared/js/mimosa/vs/loader.js"></script>
<script src="/shared/js/mimosa/mimosa.min.js"></script>
<script>
    require.config({
        baseUrl: '/shared/js/mimosa/', // MimosaとMonacoの入っているディレクトリへのパス
    });

    // 自動で pre.vs[data-lang] な要素を探してシンタックスハイライトを適用する
    Mimosa.initialize();
</script>

こんな感じでMimosaを読み込ませるものをページに書いておきます。 loaderはVSCode Loaderなのですが、これがほかのrequire機構と被ると何が起きるのかは謎です。

そして以下のように対象となるソースコードを pre 要素にして vs クラス (とスタイルのために monaco-editor)をつけておきます。 あと data-lang 属性に言語名を指定する必要があるのでそれも指定します。 対応している言語名はcss, html, javascript,...csharp, bat, powershell, ...といった感じです。

<pre data-lang="csharp" class="monaco-editor vs">
class A
{
    public async Task<int> Hoge()
    {
        await Task.Delay(1000);
        return 10;
    }
}</pre>

そうすると以下のような感じでシンタックスハイライトが適用されます。

class A
{
    public async Task<int> Hoge()
    {
        await Task.Delay(1000);
        return 10;
    }
}

ちなみに vs-dark クラスをつけておくと黒背景になじむカラーリングになります。

class A
{
    public async Task<int> Hoge()
    {
        await Task.Delay(1000);
        return 10;
    }
}

備考

まとめ

どうぞご利用ください。

次回はMonaco Editorを使う方法について書く予定があるとかないとか(ビルド方法とかはそっちで…)。