ElectronのIPC

3 min read読了の目安(約3200字

ElectronのIPCの歴史はセキュリティの歴史でもあります。ゆえに、ちまたにあふれる情報のうち多くは、古いやり方を解説しているものも多いため、注意が必要です。

僕も Electron を触るのは大分久しぶりなため間違っているかもしれないので、なにか間違いがあればツッコミなどをいただけると幸いです。

最初に「今」の正しいIPCのやりかた

まず、メインプロセスでウィンドウを作成する時に、

  const mainWindow = new BrowserWindow({
    width: 800,
    height: 600,
    webPreferences: {
      nodeIntegration: false, // デフォルトで false
      contextIsolation: true, // v12以後はデフォルトで true になる予定
      preload: join(__dirname, 'preload.js'),
    },
  })

このように、webPreferences オプションを指定します。nodeIntegration はもう既にデフォルトが false なので指定する必要はないのですが、指定しておくとなんとなくセキュアな気持ちになれます。

contextIsolation も今後でる v12 以後ではデフォルト true になるようですが、まだそうではないので true を指定しておきましょう。セキュアになります。

これらオプションを設定しているときには、プリロードスクリプトが必要です。

const { contextBridge, ipcRenderer } = require("electron");

contextBridge.exposeInMainWorld("rpc", {
  test: (message) => {
    ipcRenderer.invoke('test', message);
  },
});

プリロードスクリプトでは、contextBridge という仕組みを使って、レンダラープロセスに、機能を提供します。

contextBridge.exposeInMainWorldipcRenderer.invoke を覚えておけば大丈夫です。

ここから先は歴史的経緯を交えた解説です。

ElectronでなぜIPCが必要なのか

Electronは、Node.js と Chromium を組み合わせたソフトウェアです。またメインプロセスとレンダラープロセスという二種類のプロセスで成り立っています。

大まかにいうと起動などを司るNode.jsのメインプロセスから、ウィンドウを描画するための Chromium ベースのレンダラープロセスと認識すればいいでしょう。

初期の Electron は、レンダラープロセスは Chromium だけではなくて Node.js も統合されていました。そのため、レンダラープロセスでは、DOM操作だけではなくて Node.js の全機能を使えたのですが、当然のことながらセキュリティホールにしかならないので、Electron 5.0.0 で統合機能はデフォルトでオフになりました

そのため、Node.js を使わないとできないことは、メインプロセス上で実行するしかなくなったのです。このとき、レンダラープロセスとメインプロセスで通信を行うプロセス間通信が IPC です。

const { ipcRenderer } = require('electron')

ipcRenderer.send('hoge', 'fuga')

レンダラープロセスでは electron パッケージの ipcRenderer オブジェクトを取得し send メソッドでメッセージを送信します。この仕組み自体は Electron の最初期から用意されていたため、古い記事などで見かけることもあるでしょう。

ところが、くせ者は const { ipcRenderer } = require('electron') です。このコードは Node.js の require [1]命令を前提にしているのです。

レンダラープロセスから Node.js を排除したはずなのに require しないと IPC できないのは詰んでるようにみえます。

そこで導入されたのがプリロードという特権的なモードです。Node.js統合が無効化されている状態でもプリロードスクリプトは、レンダラープロセスでありながら Node.js 機能が使える特別な初期化用のスクリプトとして動きます。

const { ipcRenderer } = require('electron')

process.once('loaded', () => {
  global.ipcRenderer = ipcRenderer
})

このプリロードスクリプトでは、

  1. ipcRendererrequire する
  2. global 変数経由で ipcRenderer をレンダラープロセスで使えるようにする

ということをやっています。

今でも結構見かけることが多いはずですが、実はこのコードも危ないコードであるために、非推奨となっています。

コンテキストの分離

プリロードスクリプトは前述のとおり、特権的な仕組みです。レンダラープロセスでありながら Node.js の機能が使えるため、セキュリティ上あまりよろしくありません。

そこで、次に Electron のセキュリティ向上のために、V8 コンテキストが分離(contextIsolation)されました。プリロードスクリプトとレンダラープロセスで、それぞれグローバル変数を共有しないというものです。

こうなってくると、プリロード時にどうやって初期化すればレンダラープロセスでIPCが可能になるかが問題となります。そこで用意されたのが contextBridge です。contextBridge はコンテキストが分離されていても問題なく使える、Electron側が用意した安全な仕組みです。

そこで、冒頭に登場したコードです。

const { contextBridge, ipcRenderer } = require("electron");

contextBridge.exposeInMainWorld("rpc", {
  test: (message) => {
    ipcRenderer.invoke('test', message);
  },
});

考え方としては、Electronのみで使える RPC(リモートプロシージャコール)だと考えた方がいいでしょう。rpc.test というメソッドで、特定の機能が実行されるだけのものです。

  • ipcRenderer そのものをレンダラープロセス側に露出してはいけません
  • 可能なら ipcRenderer.send は、より機能が限定されている ipcRendere.invoke に置き換えましょう
脚注
  1. ESModules で import .... を記述した場合も、結局 babel なりで処理されると、Node.js の require に置き換えられます。 ↩︎