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

Filtered by Tag: NET

Intel Edisonを買ったのでC#でも触りたいのです

Created at:

Intel Edison

最近、Intel Edisonをおすすめされたので買ってみました。

Arduinoなどと比べるとお高いのですが、その分性能も高くてSDカードのひと回り大きいぐらいのサイズで500MHzで動作するAtomとRAMが1GB、ストレージとして4GBを持ち、Wi-FiとBluetooth 4.0も持っていて技適も通っていてLinuxが動くという代物。すごい。ちなみに写真はBreakout Board Kitに乗せた状態のものです。

Edisonのセットアップは簡単で、ちょこちょこと初期設定するとsshで入れるようになり、Node.jsも標準で使えます。EdisonのピンというかIOを操作するためにMRAAというライブラリが用意されていてNode.jsのバインディングも用意されていて簡単に扱えるようになっています。

Intel Edison (Breakout Board Kit)でLチカ - Qiitaといった記事を参考にLチカをするのも簡単です。

C#を書けるようにする

やはりC#書きたいですね。LチカするにもC#でやりたい!…ということで最初はCoreCLRのネイティブコンパイルしたバイナリを持っていくか、CoreCLR自体を持っていこうと思っていたのですがそもそもLinux x86 portはまだのようなのできっとあるであろうMonoを探すことに。

探してみるとMonoのパッケージを提供している方がいらっしゃるのでありがたく利用させていただきます(EdisonのLinuxにはopkgというパッケージマネージャーが入っています)。

…と、簡単に入ります。

Monoがインストールできればあとはmonoコマンドでいろいろ実行できてこっちのものです。ちなみにMono 4.0なので少し古いのですが概ね問題ないでしょう。

C#からピンを操作できるようにする

しかしMonoからいざEdisonのGPIOのようなものを操作するにはsysfs経由 (/sys/class/gpio/)で操作する必要がありあまり使い勝手がよくありません。そこでNode.jsのようにMRAA(libmraa)をC#から使えるようにするライブラリがあれば…!と思ったので作ったのがMraaSharpです。

MraaSharpはだいぶ作りかけですが、libmraaをP/Invokeで呼び出すためMono以外に必要としません。本来であればMRAAはSWIGで各言語のバインディングを生成できるのですが、いろいろと(入れるのもビルドも)面倒なのでピュアC#で完結するものにしました。

使い方は簡単で、MraaSharp.dllを参照したうえで大よそMRAAと同じようなAPIを呼び出せます(ほとんど未実装ですが)。

using MraaSharp;

// 初期化 (mraa_init)
Mraa.Initialize();
// 情報を表示する (mraa_get_version, mraa_get_platform_name)
Console.WriteLine("Version: {0}", Mraa.Version);
Console.WriteLine("PlatformName: {0}", Mraa.PlatformName);

// ピンをGPIOの出力として開く
var gpio = new Gpio(MraaIntelEdisonMiniboard.J17_8, MraaGpioDir.Out);
Console.WriteLine("Gpio Pin: {0}", gpio.Pin);
Console.WriteLine("Gpio PinRaw: {0}", gpio.PinRaw);

while (true)
{
    gpio.Write(MraaGpioValue.High); // 出力をHIGHにする
    Thread.Sleep(1000);
    gpio.Write(MraaGpioValue.Low); // 出力をLOWにする
    Thread.Sleep(1000);
}

なおMonoをインストールするとC# ShellというMonoのC# REPLが一緒に入るので、それを起動してMraaSharpを読み込むとインタラクティブに試すことができます。Node.jsに負けてない!(MraaSharpがすべて実装済みとは言っていない)

というわけでC#でLチカできてよかったのです(1.8Vなので直接つなげない分、若干難易度高かった)。

MRAA + C#

A video posted by Mayuki Sawatari (@misuzilla) on

雑な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待ちの際にスレッドを手放すことが多くなるので効率よく処理できるようになります。

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とのやり取りが増えると割と高度なことを必要とするのでちょっと難易度高いかもという感じもします。

RoslynをT4テンプレート内で使う

Created at:

Visual StudioにはT4テンプレートというソースコードを生成する機能があるのですが、その中からRoslynを使おうというお話です。

T4テンプレートはVisual Studioと統合されていることとC#でテンプレートのコードを書いて、C#, VB, TypeScriptなどのコードを吐き出せます。

例えば…

のような感じで、アセンブリを読み込んでコードを生成するという使い方はよく見かけます(プロキシコードの生成とか)。

アセンブリを読み込んでコードを生成するということは「アセンブリがコンパイルされている」ことが前提になります。

例えばプロキシクラスみたいなものはそれで問題ないので、大体はそれでいいのですが「元になるコードと生成したコードを同一のアセンブリに含めたい」場合にはニワトリ卵というか「元になるコードをコンパイル」「コンパイル結果をもとにコード生成」「コード生成を含めてコンパイル」という手順が必要です。

もちろんリフレクションとかファクトリとか登録する式にすればコード生成いらないよね?という場合もあると思いますが、そこはまあ要件しだいというやつです。生成したいときもある、はず…。

例えばEnvDTEを避ける

そんなわけで「コンパイルしてできたアセンブリを元にコードを生成」するのではなく、「現在のコードを元にコードを生成」したいというときはどうするのかというとコードの構造を読み取るためにVisual Studioにアクセスする方法をとります。

一般的にT4テンプレートからVisual StudioにアクセスするにはEnvDTEというかっこいいオブジェクトモデルと仲良くすることになります。EnvDTEはVisual Studioの機能を公開するCOMライブラリなのなので、絶妙な使いづらさがあります。何よりEnvDTEはVisual Studioにくっついている都合、ConsoleApplicationやLINQPadなどで試しに書いてみるというのが難易度高いです。

まあサンプルコードを書く気にならない程度には面倒だということです。

そこでVisual Studio 2015以降ではコンパイラー基盤がRoslynになっているのでそれを使えばいいのではというのが今回のアイデアです。

T4の中でRoslynを使ってコードを読み込む

さてどうやってRoslynを使って読み込むのかという話ですが、残念なことにVisual Studio 2015の内部で「現在動いているRoslynのインスタンス」に直接アクセスすることはできません。

ですが、Visual Studio 2015に含まれているRoslynを使うことはできます。Roslynは C:\Program Files (x86)\Microsoft Visual Studio 14.0\Common7\IDE\PrivateAssemblies におかれているので、それらしいアセンブリを参照して名前空間をインポートします。

<#@ assembly name="$(DevEnvDir)PrivateAssemblies\Microsoft.CodeAnalysis.dll" #>
<#@ assembly name="$(DevEnvDir)PrivateAssemblies\Microsoft.CodeAnalysis.CSharp.dll" #>
<#@ assembly name="$(DevEnvDir)PrivateAssemblies\Microsoft.CodeAnalysis.Workspaces.dll" #>
<#@ assembly name="$(DevEnvDir)PrivateAssemblies\Microsoft.CodeAnalysis.Workspaces.Desktop.dll" #>
<#@ assembly name="$(DevEnvDir)PrivateAssemblies\System.Collections.Immutable.dll" #>

<#@ import namespace="Microsoft.CodeAnalysis" #>
<#@ import namespace="Microsoft.CodeAnalysis.CSharp" #>
<#@ import namespace="Microsoft.CodeAnalysis.CSharp.Syntax" #>
<#@ import namespace="Microsoft.CodeAnalysis.MSBuild" #>

これで使えるようになります!

あとは単純にRoslynを使ってソリューションを読み込んで、コードを解析すればいい感じに処理できるでしょう。

と言ってもいきなりT4でRoslynプログラミングはそれはそれで難しいのでConsoleApplicationかLINQPadで書いてみるのがよいでしょう。

T4の外でコードを書いてみる

とりあえずどこかのC#プロジェクトにあるクラスの一覧を適当に出力する、というコードを書いてみることにします。Roslynのコードの詳しいことは今回割愛しますので別途調べてください。

というわけでLINQPadやConsoleApplicationで動きそうな単純なコードは以下のようになります。

void Main()
{
    // 対象となるプロジェクトの.csprojのパス
    var projPath = @"C:\Users\Tomoyo\Documents\visual studio 2015\Projects\ConsoleApplication1\ConsoleApplication1\ConsoleApplication1.csproj";
    var classNames = GetClassNamesAsync(projPath).Result;
    classNames.Dump();
}

// Roslynを使ってプロジェクトのコードからクラスのコンストラクタ定義を引っこ抜く
async Task<List<string>> GetClassNamesAsync(string csprojPath)
{
    // MSBuildのワークスペース(環境みたいなもの)を作って、プロジェクトファイルを開く
    var workspace = MSBuildWorkspace.Create();
    var project = await workspace.OpenProjectAsync(csprojPath);

    // ソースコードをコンパイル的なことをする(出力するわけではなくて内部的なデータを作るやつ)
    // これでコードをパースした結果を得ることができるようになる
    var compilation = await project.GetCompilationAsync();

    var classNames = new List<string>();
    // シンタックスツリーをファイル単位で適当になめていく
    foreach (var syntaxTree in compilation.SyntaxTrees)
    {
        // セマンティックモデル(シンタックスツリーは文法で、それに対応する「コード的な意味」)を取得する
        var semModel = compilation.GetSemanticModel(syntaxTree);

        // シンタックスツリーからクラス定義のシンタックス(記述)を引っ張り出して、
        // セマンティックモデルに問い合わせることでクラス定義(意味)を引っ張り出して、ふにゃふにゃ処理する。
        classNames.AddRange(
            syntaxTree.GetRoot()
                .DescendantNodes()
                .OfType<ClassDeclarationSyntax>()
                .Select(x => semModel.GetDeclaredSymbol(x))
                .Select(x => x.ToDisplayString())
        );
    }

    return classNames;
}

実行結果はこんな感じ。

LINQPad

なおこのコードはMicrosoft.CodeAnalysis.CSharp.WorkspaceというNuGetパッケージをインストールすれば動くでしょう。

T4テンプレートの中で使う

で、これとT4テンプレートの中で使うには…という話ですが、先ほどのimportに加えて、適当にコピペしても大体動くはずです。例えば以下のように。

template debug="false" hostspecific="false" language="C#" #>
<#@ assembly name="System.Core" #>
<#@ assembly name="System.Runtime" #>
<#@ assembly name="System.Threading.Tasks" #>
<#@ assembly name="$(DevEnvDir)PrivateAssemblies\Microsoft.CodeAnalysis.dll" #>
<#@ assembly name="$(DevEnvDir)PrivateAssemblies\Microsoft.CodeAnalysis.CSharp.dll" #>
<#@ assembly name="$(DevEnvDir)PrivateAssemblies\Microsoft.CodeAnalysis.Workspaces.dll" #>
<#@ assembly name="$(DevEnvDir)PrivateAssemblies\Microsoft.CodeAnalysis.Workspaces.Desktop.dll" #>
<#@ assembly name="$(DevEnvDir)PrivateAssemblies\System.Collections.Immutable.dll" #>
<#@ import namespace="System.Linq" #>
<#@ import namespace="System.Text" #>
<#@ import namespace="System.Collections.Generic" #>
<#@ import namespace="System.Threading.Tasks" #>
<#@ import namespace="Microsoft.CodeAnalysis" #>
<#@ import namespace="Microsoft.CodeAnalysis.CSharp" #>
<#@ import namespace="Microsoft.CodeAnalysis.CSharp.Syntax" #>
<#@ import namespace="Microsoft.CodeAnalysis.MSBuild" #>
<#@ output extension=".txt" #>
<#
    // 対象となるプロジェクトの.csprojのパス
    var projPath = @"C:\Users\Tomoyo\Documents\visual studio 2015\Projects\ConsoleApplication1\ConsoleApplication1\ConsoleApplication1.csproj";
    var classNames = GetClassNamesAsync(projPath).Result;
#>

<# foreach (var className in classNames) {
#>- <#= className #>
<# } #>

<#+

// Roslynを使ってプロジェクトのコードからクラスのコンストラクタ定義を引っこ抜く
async Task<List<string>> GetClassNamesAsync(string csprojPath)
{
    // MSBuildのワークスペース(環境みたいなもの)を作って、プロジェクトファイルを開く
    var workspace = MSBuildWorkspace.Create();
    var project = await workspace.OpenProjectAsync(csprojPath);

    // ソースコードをコンパイル的なことをする(出力するわけではなくて内部的なデータを作るやつ)
    // これでコードをパースした結果を得ることができるようになる
    var compilation = await project.GetCompilationAsync();

    var classNames = new List<string>();
    // シンタックスツリーをファイル単位で適当になめていく
    foreach (var syntaxTree in compilation.SyntaxTrees)
    {
        // セマンティックモデル(シンタックスツリーは文法で、それに対応する「コード的な意味」)を取得する
        var semModel = compilation.GetSemanticModel(syntaxTree);

        // シンタックスツリーからクラス定義のシンタックス(記述)を引っ張り出して、
        // セマンティックモデルに問い合わせることでクラス定義(意味)を引っ張り出して、ふにゃふにゃ処理する。
        classNames.AddRange(
            syntaxTree.GetRoot()
                .DescendantNodes()
                .OfType<ClassDeclarationSyntax>()
                .Select(x => semModel.GetDeclaredSymbol(x))
                .Select(x => x.ToDisplayString())
        );
    }

    return classNames;
}
#>

で、このT4テンプレートを実行すると、以下のようにクラス名の一覧が記述されたファイルが吐き出されます。

ConsoleApplication1.Hauhau
- ConsoleApplication1.Program
- ConsoleApplication1.Hoge
- ConsoleApplication1.Fuga

とても簡単ですね。

まとめ

大抵の場合においてはアセンブリを直接読み込めばいいと思いますが、もし「現在のコードを元にプロジェクトに含めるコードを生成する」という必要がある場面では役に立つのではないでしょうか。

またこの組み合わせで書いてみるとわかるのですがLINQPadでRoslynでソースコードから情報を集約するコードを書いて、それを元にジェネレートする、という手順はテンプレートからロジックの一部を切り出して実装でき、デバッグしやすいのでとてもおすすめです。