non vorrei lavorare

ブログ名の通りです。javascript three.js mruby rust OCaml golang julialang blender

node-ffiをGoで入門してElectronを使って応用してみる

このエントリは Node.js Advent Calendar 2015の 11 日目のエントリです。

はじめに

1日目のエントリでコントリビューションの件がありました。Node.js本体への貢献はまだ出来ずにいますが、 今年は、

からのやり取りに始まり、node-ffiへ若干のコントリビュートができたました。 (まぁ、いろいろフォークされてるブランチを漁ってパッチを作っただけじゃねぇかと 言われればそれまでなんですがw)

そんなこともあり、node-ffiの紹介とその応用をテーマにエントリーを書きました。

node-ffiとは

Node.jsでC++を一切書かずに、共有ライブラリ(.so/.dll/.dylib)を呼び出すことが出来るモジュールです。

Node.jsは頻繁にネイティブモジュールのAPIが更新されるので ヘタレな、自分は

と音を上げてしまっています。でも、node-ffiを使えば、そんな悩みから開放されます。

ただし、node-ffiのREADMEにもありますが、呼び出しのオーバーヘッドが高いので、node.jsと使いたいライブラリ間を 頻繁に呼び出しあうような必要があるものは、パフォーマンスが出ないとのことです。

共有ライブラリ側にJsの関数を関数ポインタとして渡せるので、共有ライブラリ側から Js側の関数を呼び出すことも出来ます。以下のようなコードイメージとなります。

typedef int (*FUNCPTR)(char *script);

int boot(char *name, FUNCPTR cb)
{
  (*cb)("foobar");
  return 0;
}
// C側に渡す関数ポインタの定義
const funcPtr = ffi.Function('int', ['string']);

const mylib = ffi.Library('libhoge', {
  // libhogeないでJS側から使う関数を定義
  'boot': ['int', ['string', funcPtr]]
});

// Cのライブラリ側から呼び出されるコールバック関数
const onResult = function(resultVal) {
  console.log('Result is', resultVal);
  return 0;
};

// Cのライブラリの呼び出し
mylib.boot("", onResult);

Goで作った共有ライブラリをNode.jsで動かす

C/C++以外でもRustやGoなどもCから呼び出せる共有ライブラリを 出力することができるので、こららの言語で共有ライブラリを出力すれば、これも同様にNode.jsから呼び出せます。

以下は、Node.js v5.1.1を使って動かしました。

準備

node-ffiを以下の様にインストールします。 node-ffiではないことに注意です。

npm install ffi

cgoで共有ライブラリを作る

以下が参考になりました。

ライブラリのコード

package main

import (
    "C"
    "fmt"
    "os"

    "github.com/shiena/ansicolor"
)

//export colorprint
func colorprint(cstr *C.char) {
    w := ansicolor.NewAnsiColorWriter(os.Stdout)
    text := "%sforeground %s "+C.GoString(cstr) + " %s %sbackground%s\n"
    fmt.Fprintf(w, text, "\x1b[31m", "\x1b[1m", "\x1b[21m", "\x1b[41;32m", "\x1b[0m")
    fmt.Fprintf(w, text, "\x1b[32m", "\x1b[1m", "\x1b[21m", "\x1b[42;31m", "\x1b[0m")
    fmt.Fprintf(w, text, "\x1b[33m", "\x1b[1m", "\x1b[21m", "\x1b[43;34m", "\x1b[0m")
    fmt.Fprintf(w, text, "\x1b[34m", "\x1b[1m", "\x1b[21m", "\x1b[44;33m", "\x1b[0m")
    fmt.Fprintf(w, text, "\x1b[35m", "\x1b[1m", "\x1b[21m", "\x1b[45;36m", "\x1b[0m")
    fmt.Fprintf(w, text, "\x1b[36m", "\x1b[1m", "\x1b[21m", "\x1b[46;35m", "\x1b[0m")
    fmt.Fprintf(w, text, "\x1b[37m", "\x1b[1m", "\x1b[21m", "\x1b[47;30m", "\x1b[0m")
}

func main() {
}

Linux

go build -buildmode=c-shared -o libtest.so test.go 

OSX

go build -buildmode=c-shared -o libtest.dylib test.go 

Windows

go build -buildmode=c-shared -o libtest.dll test.go 

と思いきや、Windwosでは32ビットも64ビットもともに未サポートでした。。 折角、windowsでも動くカラーのコンソール出力ライブラリを見つけたのに残念です。

なので、Windows 10 64ビットでRustならば、MSVC版つかってnode-ffiで動かせそうなので、 あとで記事を書く予定です。

作成したライブラリを動かす

node-ffiでGoでつくった共有ライブラリを呼び出すには以下の様になります。

「Hello,go! from Node.js」というNode.js内の文字列がGoのライブラリ側に 渡してGo側で表示します。

const ffi = require("ffi");

const mylib = ffi.Library('libtest', {
          'colorprint': ['void', ['string']]
});

mylib.colorprint("Hello,go! from Node.js");

以下のように動かします。

node test.js

www.youtube.com

node-ffiの実用的な応用編

node-ffiが実用的に使える可能性を示すようなものが何か出来ないかと あれ考え、以下にしました。OSX限定で済みません。

スクリーンキャスト用のキー履歴表示ツールをつくる

以下の記事で紹介されている今は開発が止まってしまったMacRubyをつかったスクリーンキャスト用のキー履歴表示ツールの Node.js版を作成することにしてみます。

独自のバイナリーを作り、安全確保

キーロガーなので、単純にnode.jsで動かすと、node.js自体をアクセス許可を与える必要があり、 試したあと、設定変更しないで、忘れていると潜在的なリスクが高いので、 別のOSXのアプリケーションとして作成し、これに許可を与えるようにするべきです。

そんな用途に打って付けなのがelectron-packagerです。 指定したアプリケーション名でElectronをアプリケーション内にパッケージしてくれます。

あ、Electronはnode.jsが入っているですよ。念のため。

その証拠に

ATOM_SHELL_INTERNAL_RUN_AS_NODE=1 electron

とすれば、コマンドラインのREPLが動きます。(環境変数名がATOM_SHELLというもの今となっては 歴史を感じてしまいます。)

ただ、今回はフツーにElectronを使うことで、GUIまわりもWeb系の技法で実装します。

キーイベントを取得するCocoa APIを叩くには

NodObjCというモジュールを使います。このモジュールでNode.jsからCocoa APIが叩けるようになります。

NobObjCがnode-ffiを使っている箇所

というように、libojcを使って、C APIからObjective-CベースのCocoa APIにアクセスしています。

ブロック構文があつかえる

実は、OSXでキーイベントの取得に使うAPIはブロック構文を使うAPIであり、 開発が停止してしまったMacRubyでは出来ていたのですが、現在も開発が続いているRubyCocoaでは、ブロック構文が サポートされていない様です。そのため、前述のサイトのコードをRubyCocoaに移植?してもたぶん動かないと思います。

ちょっと話がそれましたが、NodObjCはブロック構文も使えます。

今回のコアとなるキーイベントを受取る部分のコードは以下の様になります。

const keyDownHandler = $(function(s,e){
    console.log(e);
    //console.log(e('keyCode'));
    mainWindow.webContents.send('keydn',
      JSON.stringify({
        characters: e('characters')('UTF8String'),
        flags: e('modifierFlags'),
        charactersIgnoringModifiers: e('charactersIgnoringModifiers')('UTF8String'),
        keyCode: e('keyCode')}));
},['v',['?','@']]);

$.NSEvent('addGlobalMonitorForEventsMatchingMask',
  $.NSKeyDownMask,
  'handler',
  keyDownHandler
);

成果物

www.youtube.com

まとめ

node.js側からのGoの呼び出しや、キー履歴表示ツールの事をもう少し詳細に書いても良かったのですが、 ここはNode.jsのアドベントカレンダーなので、別の機会に書くことにします。

関連記事