Electron Advent Calendar 2015の2日目のエントリです
ElectronのRemoteモジュール使ってハマったけど最終的にハマらなくなった話
最初は最近新設されたAPIでも叩いて遊ぼうと思って, session.enableNetworkEmulation とか触ってみたのですが, 特に記事にするほどの知見も得られなかったので, 少し前にElectronでハマった話を書こうと思います.
ハマった内容
今年(2015年)の秋頃の話で, その時に使ってたElectronのversionはv0.31.0でした.
Renderer Processから読み込まれる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 にいます.
コードをざっくり読むと, こいつ自身は以下を実行していることがわかります.
- IPCで使うメタデータ と RendererProcess上でのJavaScriptオブジェクト相互変換
- IPC呼び出し
今回のケースの、「remote.getCurrentWindow()
で取得したオブジェクトにぶら下がってるメソッドの実行」であれば, https://github.com/atom/electron/blob/v0.35.0/atom%2Frenderer%2Fapi%2Flib%2Fremote.coffee#L86 が概要箇所ですね.
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 にハンドラが記載されています.
rpc-server.coffeeには, IPC経由でRenderer Processから受信した引数のメタデータをBrowser Process側で戻す処理が含まれます. 今回のケースでは, 関数を引数として送信しているので, そこに辺りを付けて変更履歴をおっていくと, 62d15953ffea6932622c771daba56b408a311fee にてそれっぽい変更がなされてました.
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
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が入ったのも10月末ぐらいの話で(報告者は僕ではないです. 気づいた時点で面倒がらずにissue立てるべきでした), remoteが枯れてるか、というとまだまだ微妙なのかもしれません.
remoteに限った話ではないですが,,,
- まずはきちんとコード読んでみる
- issue立てれば結構早く対応してくれそう
ってことですね.
明日は @enjoycoding さんです!