14
16

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 5 years have passed since last update.

ElectronAdvent Calendar 2016

Day 16

Electron APIの内部を覗いてみる

Posted at

クロスプラットフォームなデスクトップアプリ開発ができるElectronですが、よりデスクトップアプリらしさを追求していくとクロスプラットフォームなどというものはやはり幻想だったということ思い出します。

  • プラットフォームごとにアプリに一般的に期待される挙動が違う
  • プラットフォームごとにElectronの挙動が微妙に違う
  • 機能の実現にそもそものElectronのAPIが足りない

そんなことがあってAPIを追加したいと思っていたことがあり、ネイティブのAPIを呼び出すべくElectronのソースコードを調べた時のことを書き並べたいと思います。なお、この記事ではElectron 1.4.12を扱います。

知っていると役に立つもの

コードの構成

ドキュメントのSource Code Directory Structureにまとまっています。今回見る必要があるのは下の2つです。

  • atom/ - C++ソースコード
  • lib/*/api/ - JavaScript APIの実装

JavaScriptに近い側から覗いてみることにしましょう。

JavaScript API

lib/*/api/にはElectronのAPIのうち、JavaScriptでの実装・ネイティブへのバインディング・APIのexportが実装されています。
また、EventEmitterとして使うAPIへのプロトタイプ追加もここで行います。(.on()など)

lib/*/api/exports/electron.js

各APIがexportされ、我々が普段利用しているelectronモジュールから使えるようになります。MainプロセスとRendererプロセスのどちらでAPIが使えるのかは、最終的にこの部分で決まるようです。
例えば、両方のプロセスから呼べることになっているscreenは、Renderer側では内部でremoteを使ってMain側のscreenを呼び出す実装になっており、APIの区分とは一体何だったのか...という気分になれます。

process.atomBinding()

ネイティブへのバインディングです。ここで引数になる文字列が、どのネイティブ実装にバインディングされるかを選択するようになっているようで、後のネイティブの実装で使います。

ネイティブ実装

atom/にはElectronのネイティブ部分の実装が含まれます。まず、先のprocess.atomBinding()の引数に使った文字列を使ってREFERENCE_MODULEを列挙しておきます。

関数・クラスのexport

APIの種類によってさらにフォルダが分かれます。

  • atom/browser/api - Mainプロセス
  • atom/common/api - 両方のプロセス
  • atom/renderer/api - Rendererプロセス

各apiフォルダの中にあるatom_api_*.hatom_api_*.ccがAPIのエントリポイントになっていて、Node.jsのnative addonのように初期化と関数/クラスのexportを行い、V8の世界とC++の世界でやり取りをできるようにします。このとき、先ほどREFERENCE_MODULE()で列挙した名前を使います。書き方の雰囲気が似ているので、native addonの作成方法やNANを使ったコードのイメージが頭に入っていると理解しやすいです。

native addonと異なる点としては、exportされる関数の中で引数をいちいちFunctionCallbackInfoから取り出す必要がなく、素直にC++の型を使って関数宣言できるという点が挙げられます。dict.SetMethod()がテンプレート関数になっていて、electron/native-mateがいい感じに自動的に引数と戻り値の変換処理を追加しているためのようです。もし必要な型への変換がなければmate::Converter::FromV8()mate::Converter::ToV8()を実装することで対応できます。

オブジェクト

JavaScriptの世界からV8の世界を経て、ようやくC++の世界にたどり着きました。ここまで来ると素直なC++を書くことができます。しかし現実的には、プラットフォームのAPIを呼び出すとなると何かしらのオブジェクトを扱い、さらにそれをJavaScriptの世界へ送り出すことは避けられないでしょう。

  • Win32 APIのHANDLE
  • WRLでラップしたオブジェクト (WindowsランタイムAPIをWin32アプリから呼ぶために必要)
  • Objective-Cのオブジェクト (Cocoa APIを呼ぶために必要)
  • etc.

ネイティブのオブジェクトを保持するAPIの実装としてはTraynativeImageが参考になったと記憶しています。おおまかには以下のようなポイントがありました。

  • V8へexportするためにmate名前空間のクラスから派生クラスを作る atom/browser/api/atom_api_tray.h
  • ↑から呼び出されてV8に関係なく動作する、実体になるクラスを作る atom/browser/ui/tray_icon.h
    • コンストラクタをprotectedにしてファクトリ関数を使う
    • ファクトリ関数内ではnewでオブジェクトを確保する
    • 非同期イベントを通知するためのObserverを作る atom/browser/ui/tray_icon_observer.h
  • macOS用の実装ではWindows/Linux用クラスの派生クラスを作る atom/browser/ui/tray_icon_cocoa.h
    • ファクトリ関数をObjective-C++で実装する (Objective-C++のヘッダファイルをC++側でインクルードしないようにする)

おわりに

自分に必要だった部分の書き出しのため端折り気味かつ表面的ではありますが、Electronで動作するJavaScriptからどうやって各プラットフォームのネイティブなAPIを呼び出しているかをまとめました。

そもそもネイティブを触りたいだけならnative addonでいいんじゃないのか、ということも考えられて、何でもかんでもElectron本体に乗せるのはあまりよろしくないと思います。しかし、やはりElectronに持ってもらいたい部分もあるかと思いますし、根の深そうな問題に遭遇すれば内部を調べるほかなくなるわけで、そんなことがあったときに助けになればと思います。

14
16
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
14
16

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?