Native Addonを作ってElectronで使ってみた


# はじめに

[Electron触ってみようの会](http://aim.connpass.com/event/19395/)でやってみたことのまとめです。
当日は全然時間が足りなかったので持ち帰ってから完成させました。

とりあえずElectronは初めてでしたが、せっかくなので普通のWebアプリではできないNative Addonを自分で作って使ってみようと思いました。
Native AddonとはC++などのネイティブ言語で実装されたNode.jsモジュールです。

Electronのバージョンは0.32です。
今回作成したソースコードは https://github.com/likr/20150915electron に置いています。

# Native Addonを作る

何か重めの処理をさせようということで、雑に[マンデルブロ集合](https://ja.wikipedia.org/wiki/%E3%83%9E%E3%83%B3%E3%83%87%E3%83%AB%E3%83%96%E3%83%AD%E9%9B%86%E5%90%88)を計算してみます。
Canvasと連携することを想定して、JavaScript側からビットマップを表す`Uint8ClampedArray`を受け取り、ピクセルごとに色を決める計算を行うようにします。

C++のソースコード全体は以下の通りです。
マンデルブロ集合の計算を行う部分と、JavaScriptのオブジェクトからポインタの取り出しを行っている部分、モジュールの登録を行う部分で成り立っています。
パラメータは決め打ちで、エラー処理などもすっ飛ばしています。

```mandelbrot.cc
#include <node.h>

using namespace v8;

void runMandelbrot(unsigned char* array, int width, int height, double pixel_size, double x0, double y0) {
  for (int j = 0; j < height; ++j) {
    for (int i = 0; i < width; ++i) {
      double cr = x0 + pixel_size * i;
      double ci = y0 + pixel_size * j;
      double zr = 0.0f;
      double zi = 0.0f;
      double zrzi, zr2, zi2;
      int k;
      for(k = 0; k < 256; k++) {
          zrzi = zr * zi;
          zr2 = zr * zr;
          zi2 = zi * zi;
          zr = zr2 - zi2 + cr;
          zi = zrzi + zrzi + ci;
          if(zi2 + zr2 >= 2.0) {
              break;
          }
      }
      if(k > 255){
          k = 255;
      }

      int index = j * width + i;
      array[4 * index] = array[4 * index + 1] = array[4 * index + 2] = 255 - k;
      array[4 * index + 3] = 255;
    }
  }
}


void Method(const FunctionCallbackInfo<Value>& args) {
  Local<Uint8ClampedArray> array = args[0].As<Uint8ClampedArray>();
  unsigned char* ptr = (unsigned char*)array->Buffer()->GetContents().Data();
  runMandelbrot(ptr, 300, 200, 0.01, -2.0, -1.0);
}

void init(Local<Object> exports) {
  NODE_SET_METHOD(exports, "mandelbrot", Method);
}

NODE_MODULE(addon, init)
```

Native AddonのAPIは何度か大幅な変更が入っていて、Electronで使用されているnode.jsのバージョンと利用するAPIを合わせる必要があります。
Electron 0.32ではio.js 3.3.0が使われているので、C++の実装もそれに合わせて行っています。
C++のコードは[io.js Documentation](https://iojs.org/api/addons.html)と[V8 API Reference Guide](https://v8docs.nodesource.com/io.js-3.0/)を参考にしながら書きました。

Native Addonは`node-gyp`を使ってビルドすることができます。
今回は、以下のような設定ファイルになります。

```binding.gyp
{
  "targets": [
    {
      "target_name": "mandelbrot",
      "sources": ["mandelbrot.cc"]
    }
  ]
}
```

`node-gyp`コマンドを実行するだけでビルドができます。

```console
$ node-gyp
```

ビルドに成功すると`./build/Release/mandelbrot`にモジュールが配置され、以下のように通常のモジュールと同じように使うことができます。。

```test-mandelbrot.js
var mandelbrot = require('./build/Release/mandelbrot');
var b = new Uint8Array(300 * 200 * 4);
mandelbrot.mandelbrot(b);
console.log(b);
```

しかし、残念ながらこのままではElectronから読み込むことができないので、以下のようにElectron向けにビルドをします。
他にもelectron-rebuildを使う方法などがあります。([Using Native Node Modules](https://github.com/atom/electron/blob/master/docs/tutorial/using-native-node-modules.md))

```console
$ HOME=~/.electron-gyp node-gyp rebuild --target=0.32.0 --arch=x64 --dist-url=https://atom.io/download/atom-shell
```

# Electronから呼び出す

Electronアプリを作っていきます。
`package.json`を作成し、[Quick Start](http://electron.atom.io/docs/v0.32.0/tutorial/quick-start/)を参考にし、以下のような`main.js`を作りました。

```main.js
var app = require('app');
var BrowserWindow = require('browser-window');

require('crash-reporter').start();

var mainWindow = null;

app.on('window-all-closed', function() {
  if (process.platform != 'darwin') {
    app.quit();
  }
});

app.on('ready', function() {
  mainWindow = new BrowserWindow({width: 800, height: 600});
  mainWindow.loadUrl('file://' + __dirname + '/index.html');
  mainWindow.on('closed', function() {
    mainWindow = null;
  });
});
```

以下が画面側のコードです。
ボタンを押すとマンデルブロ集合を計算した結果をCanvasに描画します。

```html:index.html
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>Native Mandelbrot</title>
</head>
<body>
<div>
  <button onclick="run()">Run</button>
</div>
<div>
  <canvas id="screen" width="300" height="200"></canvas>
</div>
<script>
var remote = require('remote');
var mandelbrot = require('./build/Release/mandelbrot');

function run() {
  var canvas = document.getElementById('screen');
  var context = canvas.getContext('2d');
  var imageData = context.createImageData(300, 200);
  mandelbrot.mandelbrot(imageData.data);
  context.putImageData(imageData, 0, 0);
}
</script>
</body>
</html>
```


Native Addonの使い方は`test-mandelbrot.js`と特に変わっていません。
`imageData.data`がピクセルを表す`Uint8ClampedArray`です。
Native Addon内の処理が終わったら`context.putImageData`でCanvasに描画します。
Native AddonはMain Processだけでしか使えないのかと思いきやRender Processでも`require`してしまえば普通に使えるんですね。

実行してボタンを押すとこんな感じになります。

![kobito.1442388864.608266.png](https://qiita-image-store.s3.amazonaws.com/0/4249/20b789e8-2373-0504-210a-d9213c086e65.png "kobito.1442388864.608266.png")


# まとめ

ElectronもNative Addonもどちらも意外と簡単にできました。
Native Addonで科学技術計算などのJavaScriptでは追いつかないような重い処理をさせながら、UIをWebベースで作るといったことも実現できそうでした。