Node.js と .NET をつなぐ Edge.js を使う上でのつまずきポイント

  • 10
    いいね
  • 0
    コメント

Edge.js って何

Edge.js: .NET and Node.js in-process
Node.js と .NET をいい感じに結びつけてくれるすごいライブラリ.
JavaScript のオブジェクトを .NET 環境に投げてそれを .NET 側で加工して戻したり,その逆が出来たりするので,
ロジックを C# とかで書いて,View を Node.js を使ってる Electron とかで作るみたいなことが出来て嬉しくなれます.

現在 Electron から ASP.NET Core のアプリを起動して,その View を Electron で見るみたいなことを挑戦しています(実際は上手くいってない)

こんな風に Web からネイティブ環境を触りやすくなるので,開発の幅が広がる面白い技術なんじゃ無いかと思います.

今回はそんな Edge.js を使う時,インストール時は mac ユーザの方がちょっと辛い思いをしたり,運用時 .NET Core を使ってという時ちょっと困ったことがあったので,そのことを中心に紹介いたします.

インストール方法

Windows

Windows でのインストールは特につまずく点は無いと思います.
t-koyama 氏の [Node.js] Edge.js を触ってみた が参考になるでしょう.

Linux(Ubuntu)

Edge.js のリポジトリ の Dockerfile を参考にインストール

macOS

macでの環境構築が非常に面倒だったので,こちらについては詳しく説明を行います.
前提の環境として

  • Node.js 6.5.0
  • Mono 4.6.2 x64
  • .NET Core(1.0.0-preview2-003121) - optional だけどオススメ

を想定.

基本的には Edge.js の公式ドキュメント の通り進めれば良い.
公式ドキュメントでは Mono 4.2.4 が必須環境とされていますが,とある理由 により正常に動作しないため, 現在の Stable の Xamarin Studio で必須とされている Mono 4.6.2 を使用します.
現在 Stable は 4.8.0 になったためビルドに失敗します.
フォークした私のリポジトリのもので現在は解消されています mono4.8.0対応版
Visual Studio for Mac や alpha 版の Xamarin Sudio などを使用していて,Mono 4.8.0 が入っていて,Current がそちらを向いている時があるので,その時は

PKG_CONFIG_PATH=/Library/Frameworks/Mono.framework/Versions/4.6.2/lib/pkgconfig \
npm install edge

として 4.6.2 の Mono Runtime を対象にするようにします.Mono 4.8.0 (4.8.0.344以前) はヘッダ(mono-dl.h)が足りなくてビルドが通らないので注意.(Revert コミットがあって 344 以降はどうも動きそうな気がします)
またこの時 dotnet --version が 1.0.0-rc4-004616 以降だとビルドツールが MSBuild に変わっているためビルドが通らなくなるので,私の過去の記事 などのように一時的に dotnet の参照を 003121 などに向くようにしておくといいでしょう.
昔のバージョンとか持ってないという人は アーカイブ からダウンロードしてインストールしましょう.

上記の工程は Mono も .NET Core も両方使いたいという方向けで,無理してどちらの環境も揃えなくてもエラーを無視して進めればどちらかの環境は使えるようにはなります.

実際に使ってみる

基本的には Edge.js の samples 以下が普通に実行できるのでそれを試してみればいいのですが,.NET Core を使う予定の人はここでつまずきポイントが発生します.

.NET Core を使うことを明示するためにフラグの指定が必要で, EDGE_USE_CORECLR=1 というフラグが必要になります.

EDGE_USE_CORECLR=1 node index.js

デバッグ時は EDGE_DEBUG=1 も追加すると詳しく実行時の状況を知ることが出来るので併用をオススメします(めちゃくちゃログが流れて鬱陶しいですが).

これで .NET Core でも問題なく動く...ということにはならず,最小のサンプルコードであるような


var edge = require('edge');

var helloWorld = edge.func(function () {/*
    async (input) => { 
        return ".NET Welcomes " + input.ToString(); 
    }
*/});

helloWorld('JavaScript', function (error, result) {
    if (error) throw error;
    console.log(result);
});

は動作しますが, samples/110_clr_instance.js のようなクラスを作ったりするようなものになると(環境にもよるが?)動作しなくなります.
原因としては実行時に参照するライブラリが不足したりバージョンが異なったものをロードしてしまうこと...だとは思いますが,詳しくは調べていません.

このようにクラスを作れない状況は実際のアプリケーションを開発する上では非常に頭の痛い状況です.
ですが解決策が無いというわけではないので,以下に記載する方法で解消していきます.

公式ドキュメントに,ある制限のもと作成した .NET Core アプリであれば Edge.js 上で動かせる(意訳)とあるので,その手法を試してみましょう.

  1. project.json の frameworks で netcoreapp1.0 が指定されている
  2. Microsoft.NETCore.DotNetHostMicrosoft.NETCore.DotNetHostPolicy が dependencies に含まれている
  3. buildOptions の emitEntryPointtrue になっている
  4. 書かれていないけれども,Edge.js が dependencies に含まれている

これを満たしていればクラス定義のあるような .NET Core アプリでも動作が可能となります.

それでは上記の条件を満たしたプロジェクトを作成してみます.
適当なディレクトリで dotnet new でプロジェクトを作成して,上記の条件を満たすよう project.json を編集します.
その後スクリプトとして動作させた場合失敗した samples/110_clr_instance.js の C# の部分を移植し以下のように書き換えた cs ファイルを作成します.

using System;
using System.Threading.Tasks;
// 毎回 Func<object, Task<object>> と書くのが億劫なのでエイリアスを...
using JsAPI = System.Func<object, System.Threading.Tasks.Task<object>>;

namespace Embtest
{
    public class Startup
    {
        // emitEntryPoint を true にしているのでとりあえず入れなくてはならない
        public static void Main() {}
        public async Task<object> Invoke(int startingSalary)
        {
            var person = new Person(startingSalary);
            return new
            {
                getSalary = (JsAPI)(async (i) => {
                    return person.Salary;
                }),
                giveRaise = (JsAPI)(async (amount) => {
                        person.GiveRaise((int)amount);
                        return person.Salary;
                })
            };
        }
    }

    public class Person
    {
        public int Salary { get; private set; }
        public Person(int startingSalary)
        {
            this.Salary = startingSalary;
        }
        public void GiveRaise(int amount)
        {
            this.Salary += amount;
        }
    }
}

最後にこのプロジェクトを dotnet publish という風にタイプして Publish します.
mac の場合は dotnet publish -r osx.10.11-x64 といった感じでしょうか.
そうすると ${ProjectRoot}/bin/Debug/netcoreapp1.0/publish というディレクトリに プロジェクト名.dll プロジェクト名.deps.json のようなファイルが格納されていると思います.

それではこの dll を使うように JavaScript 側を変更していきます.

// Overview of edge.js: http://tjanczuk.github.com/edge

var edge = require('edge');

var createPerson = edge.func({
    assemblyFile: 'DLLへのパス',
    typeName: 'Embtest.Startup', // 最初に実行される Invoke が含まれる場所の明記,指定しない場合 DLL名.Startup 内の methodName を探す
    methodName: 'Invoke' // 実際は Invoke でなくても,Func<object,Task<object>> 型ならここで名前を指定すればそれを呼んでくれる
});
var person = createPerson(120, true);
console.log(person.getSalary(null, true));
console.log(person.giveRaise(20, true));
console.log(person.getSalary(null, true));

ここで記した DLLへのパス は,dotnet publish で publish した dll へのパスを記述します.
そして実行前に EDGE_APP_ROOT という環境変数に .deps.json が含まれるディレクトリである ${ProjectRoot}/bin/Debug/netcoreapp1.0/publish を設定しましょう.

ここまで設定して EDGE_USE_CORECLR=1 node index.js とタイプすると無事クラス定義のあるものでも .NET Core で動作させることが可能となりました.

まとめ

各プラットフォームで Edge.js を動作させるための方法を(一部手抜きしながらも)紹介した.
.NET Core を使う時は環境変数だったり,スクリプトによってはビルドしたほうがよかったりする.

番外編のつらいところリスト

本当は Mono をビルドしてちゃんと x64 環境対象にしたら動くかなんて調べる気は一切なかったのですが,
気になってしまいビルドの方法も何度か試して検証で非常に時間がかかってしまいました.(最初 submodule 関連をちゃんとやってなくてダメだった)
ゴミ箱 mac じゃなくて 包丁 mac などを使っている人は寝る前に行うなどしないとコーヒータイムが増えるので注意.(MBP2015Early i5 2.7GHz で make -j8 で回して 30~40 分ぐらい?) -> 結局動かなかったので無駄な時間を過ごした

Mono4.2.4だと実際は上手くいかない

記載した手順で実際に手を動かし,ビルド時のログを眺めているとこんな怪しいエラー文を見ることができる.

clang: warning: libstdc++ is deprecated; move to libc++ with a minimum deployment target of OS X 10.9
ld: warning: ignoring file /Library/Frameworks/Mono.framework/Versions/4.2.4/lib/libmono-2.0.dylib, file was built for i386 which is not the architecture being linked (x86_64): /Library/Frameworks/Mono.framework/Versions/4.2.4/lib/libmono-2.0.dylib

Edge.js では Mono ランタイムが無い環境でも Mono を実行できるように Mono Embedded を活用しているが,Edge.js の Required にあったように,対象とする Mono は x64 であり,記載したダウンロードリンクからダウンロードしたものは i386 向け(?)のものとなっている.
そのため x64 向けの Mono を Compiling Mono on Mac OS X の手順に沿ってビルドして,PKG_CONFIG_PATH を自分でビルドした Mono に向けてあげれば解消すると思ったが,
ビルドは通るが,実行時にセグフォで落ちるという状態になってしまう.

参考までに Mono x64 のビルド方法を以下に記す.公式ドキュメントをそのままなぞるだけだとビルドは通らないので注意.

cd 適当な作業ディレクトリ
git clone --recursive https://github.com/mono/mono.git
cd mono
./autogen.sh --prefix=${インストールしたいディレクトリとか} --disable-nls
# 実際 Mono が入ってるならこれは不要.mcs コマンドが使えない環境の場合これを実行
make get-monolite-latest
make
make install

ASP.NET CoreのViewRenderingエンジンが動いてくれない?

ログを見てもなかなか解消しない問題.
dotnet publish で依存ライブラリを丸ごとディレクトリに投げているのになぜ...
favicon などは参照できているのでパスの問題ではないしなんなんだー