ElectronのRemoteモジュール使ってハマったけど最終的にハマらなくなった話

[Electron Advent Calendar 2015](http://qiita.com/advent-calendar/2015/electron)の2日目のエントリです

# ElectronのRemoteモジュール使ってハマったけど最終的にハマらなくなった話

最初は最近新設されたAPIでも叩いて遊ぼうと思って, session.enableNetworkEmulation とか触ってみたのですが, 特に記事にするほどの知見も得られなかったので, 少し前にElectronでハマった話を書こうと思います.

## ハマった内容
今年(2015年)の秋頃の話で, その時に使ってたElectronのversionはv0.31.0でした.

Renderer Processから読み込まれるjsに下記のようなコードを書いてました:

```js
var myHandler = () => {
  console.log('動いた!');
};

var win = require('remote').getCurrentWindow();

win.on('move', myHandler);              // イベントハンドラの登録

window.onBbeforeUnload = () => {
  win.removeListener('move', myHandler);  // イベントハンドラの解除
};
```

実際のmyHandlerの中身はもっと長ったらしいことをやっていたのですが、それは本質じゃないので割愛.

ここで言いたいのは

* BrowserWindowはEventEmitterだから, onとremoveListenerで登録/解除ができる
* remote.getCurrentWindow使えば, Renderer Process側から上記が実行できる

の部分です. 

そんでまぁ、こいつが動かない訳です. 正確にいうと, 登録したイベントハンドラは意図通りに動くものの, removeListenerによる解除が出来なかったのです.

この時はあまり時間に余裕がなかったこともあり、「プロセス間通信で関数の参照渡すようなコードが上手く動かなくても文句言えんわな...」と自分を納得させた上で, 登録/解除の処理本体をMain Process側のコードに記述して,remoteを使わないようにすることで回避しました.


## v0.35.0でもっかい試してみた
このエントリ書く中で、念のため現時点での最新versionにして、現象が再現することを確認しようと考え, v0.35.0で試したところ、今度は再現しなくなっちゃいました. 
何が起こったかというと、removeListenerでちゃんとハンドラが解除出来たわけです.

「まずい、このままではQiitaに書く内容が無くなる, 2日目にしてAdvent Calendarが歯抜けてしまう！」ってビビったので、こっから先は何故直ったかの考察を書いておきます。

## RemoteとIPCで何が行われているか

remoteモジュールの実体は[electron/atom/renderer/api/lib/remote.coffee](https://github.com/atom/electron/blob/v0.35.0/atom%2Frenderer%2Fapi%2Flib%2Fremote.coffee) にいます.
コードをざっくり読むと, こいつ自身は以下を実行していることがわかります.

* IPCで使うメタデータ と RendererProcess上でのJavaScriptオブジェクト相互変換
* IPC呼び出し

今回のケースの、「`remote.getCurrentWindow()` で取得したオブジェクトにぶら下がってるメソッドの実行」であれば, [https://github.com/atom/electron/blob/v0.35.0/atom%2Frenderer%2Fapi%2Flib%2Fremote.coffee#L86](https://github.com/atom/electron/blob/v0.35.0/atom%2Frenderer%2Fapi%2Flib%2Fremote.coffee#L86) が概要箇所ですね.

```coffee
ret = ipcRenderer.sendSync 'ATOM_BROWSER_MEMBER_CALL', meta.id, member.name, wrapArgs(arguments)
```

IPCで送信される`ATOM_BROWSER_MEMBER_CALL`なるイベントは, [electron/atom/browser/lib/rpc-server.coffee](https://github.com/atom/electron/blob/v0.35.0/atom%2Fbrowser%2Flib%2Frpc-server.coffee) にハンドラが記載されています.

rpc-server.coffeeには, IPC経由でRenderer Processから受信した引数のメタデータをBrowser Process側で戻す処理が含まれます. 今回のケースでは, 関数を引数として送信しているので, そこに辺りを付けて変更履歴をおっていくと,  [62d15953ffea6932622c771daba56b408a311fee](https://github.com/atom/electron/commit/62d15953ffea6932622c771daba56b408a311fee?diff=split) にてそれっぽい変更がなされてました.

```coffee:変更前
unwrapArgs = (sender, args) ->
  metaToValue = (meta) ->
    switch meta.type
      # 中略
      when 'function'
        rendererReleased = false
        objectsRegistry.once "clear-#{sender.getId()}", ->
          rendererReleased = true

        ret = ->
          throw new Error('Calling a callback of released renderer view') if rendererReleased
          sender.send 'ATOM_RENDERER_CALLBACK', meta.id, valueToMeta(sender, arguments)
        v8Util.setDestructor ret, ->
          return if rendererReleased
          sender.send 'ATOM_RENDERER_RELEASE_CALLBACK', meta.id
        ret
      else throw new TypeError("Unknown type: #{meta.type}")

  args.map metaToValue
```

```coffee:変更後
unwrapArgs = (sender, args) ->
  metaToValue = (meta) ->
    switch meta.type
      # 中略
      when 'function'
        rendererReleased = false
        objectsRegistry.once "clear-#{sender.getId()}", ->
          rendererReleased = true

        return rendererCallbacks[meta.id] if rendererCallbacks[meta.id]?

        ret = ->
          if rendererReleased
            throw new Error("Attempting to call a function in a renderer window
              that has been closed or released. Function provided here: #{meta.id}.")
          sender.send 'ATOM_RENDERER_CALLBACK', meta.id, valueToMeta(sender, arguments)
        v8Util.setDestructor ret, ->
          return if rendererReleased
          delete rendererCallbacks[meta.id]
          sender.send 'ATOM_RENDERER_RELEASE_CALLBACK', meta.id
        rendererCallbacks[meta.id] = ret
        ret
      else throw new TypeError("Unknown type: #{meta.type}")

  args.map metaToValue
```

関数のメタデータを戻す際に, 一度受けたことのある関数メタデータの場合は, その参照を使いまわす対応が入ったことが見てとれます.

今回の`EventEmitter.on`, `EventEmitter.removeListener` に当て嵌めると、この対応によって, この2つのメソッドに渡したハンドラが同一であることの確認がなされるようになり, 結果としてハンドラが登録が解除できるようになったわけですね.

ようやく納得できました.


## 結論

最新のElectronでは, remote経由で関数の参照渡すような利用も考慮されているので, 万一同じ悩みを持ってる人がいれば、参考にしてください.
とはいえ, この対応[issue#3229](https://github.com/atom/electron/issues/3229)が入ったのも10月末ぐらいの話で(報告者は僕ではないです. 気づいた時点で面倒がらずにissue立てるべきでした), remoteが枯れてるか、というとまだまだ微妙なのかもしれません.

remoteに限った話ではないですが,,,

1. まずはきちんとコード読んでみる
1. issue立てれば結構早く対応してくれそう

ってことですね.

明日は @enjoycoding さんです!