CEFの情報整理

RustでCEFを使ってChromiumを扱いたいと思い、それで知ったCEFに関する情報などをここに記載していくよ。
(新参者なので、間違いがあれば教えてくれると嬉しいです。😳)
ちなみに、RustでCEFを使う方法自体は以下に記録していてる。

用語
- cefsimple: 最低限の実装がされた、公式のサンプル
- cefclient: それなりの機能が実装された、公式のサンプル
- magpcss.org: CEFに関する相談ができるフォーラム、困った時の貴重な情報源になる。
- Bootstrap/Runtime: Chromiumの起動設定。AlloyとChromeの二種がある。(詳細後述)
- Runtime Style: ↑と組み合わせて使う、動作設定。AlloyとChromeの二種がある。(詳細後述)
URL集
- リポジトリ
- リポジトリのGitHubミラー
- Chromiumのソースコード
- ChromiumのGitHubミラー
- Wiki
- CEF Documentation
- cef-builds (CEFやサンプルのビルドの配布場所)
- サンプル
- magpcss.org (フォーラム)

CEFのビルド事情(macOS)
macOSでのCEF製アプリの配布は、ビルドした実行ファイルは単なる一つの実行ファイルに収まらず、一つのアプリバンドル(.app
ディレクトリ)の中にまた複数のヘルパーアプリバンドルが入る、少し凝った構造になる。
これはChromiumの設計方針の、パフォーマンスとセキュリティのプロセスによる分離、に関わるものっぽい。
これに関する詳しい情報: https://bitbucket.org/chromiumembedded/cef/wiki/Tutorial#markdown-header-mac-os-x-build-steps
標準出力について
普通にアプリをダブルクリックして起動しても、同然ターミナルが表示されずログが見れない。これではクラッシュ時に何が起きて終了したのかわからない。
この場合、バンドル自体は.app
が名前の最後に付いただけの単なるフォルダなので、その中にある実行ファイルを直接パスで指定して実行すれば良い。lldbで実行することもできる。
例:
$ ./target/debug/cefsimple.app/Contents/MacOS/cefsimple
keychainのパスワード要求
OSのキーチェーンを使うためにパスワードをビルド毎に要求される。
これに関してはuse-mock-keychain
オプションを使えば、モックの偽物のキーチェーンを使わせてデバッグ時の煩わしいパスワード要求を防止できる(っぽい、要出典)。
具体的には、CefApp::OnBeforeCommandLineProcessing
にて、CefCommandLine
のAppendSwitch
を使ってオプションを起動前に設定すれば良い。
例:
command_line->AppendSwitch("use-mock-keychain");
複数のプロセスがクラッシュする
以下のエラーのように、いくつかのプロセスが動作しないエラーが生じることがある。この場合、アプリのバンドルの構造や設定がうまくいっていない可能性が高い。
[60312:347486:0803/110243.477113:ERROR:content/browser/network_service_instance_impl.cc:597] Network service crashed, restarting service.
[60312:347553:0803/110243.477152:ERROR:content/browser/child_process_launcher_helper_mac.cc:148] Sandbox setup failed.
[60312:347486:0803/110243.477354:ERROR:content/browser/gpu/gpu_process_host.cc:951] GPU process launch failed: error_code=1003
[60312:347486:0803/110243.477365:FATAL:content/browser/gpu/gpu_data_manager_impl_private.cc:415] GPU process isn't usable. Goodbye.
自分の場合、Info.plist
のCFBundleExecutable
とCFBundleDisplayName
の名前が別のアプリと被っている場合に発生した。例:Chrome Helper (GPU)
複数のCEFのアプリを作る時に要注意。

CEFの起動設定について
CEFは、どのようなChromiumの機能を使っていくかの設定として、Bootstrap(Runtime)とRuntime Styleの二つの設定がある。これは用語集にあったように、以下の設定がありうる。
- Bootstrap
- Alloy Bootstrap(サ終済み [1])
- Chrome Bootstrap
- Runtime Style
- Alloy Runtime Style
- Chrome Runtime Style
そしてこの二つの設定は組み合わせて使うもので、一般に以下二つの組み合わせがありうる。
- Chrome Bootstrap + Chrome Runtime Style
- Chrome Bootstrap + Alloy Runtime Style
起動設定の違い
Alloy Runtime StyleとChrome Runtime Styleはそれぞれ、Chromiumのソースコードのどこに依存するかが違う。
Alloy Runtime Styleの起動設定だと、CEFはChromiumのソースコードでいうcontent
層(Content API)に依存する。
Chrome Runtime Styleの場合、CEFは主にChromiumのソースコードでいうchrome
層(Chrome UI レイヤー)に依存する。
chrome
層の方が、拡張機能や印刷プレビューといった高レイヤーな機能を扱えるため、Chromiumの機能をフルに使いたい場合はChrome Runtime Styleの方が好ましい。[2]
余談
Bootstrapも同じように依存先が違う。依存先はRuntime Styleと同じで、AlloyとChromeでそれぞれcontent
層とchrome
層となる。Alloyは前述の通りサ終している。
サ終などの詳細はここが参考になる、と思う。

Off-Screen Rendering
Rustのtauriとかicedとかlibui等々のGUIライブラリで作ったウィンドウにChromiumを埋め込んでみたい。それをするにはOff-Screen Rendering機能を使えば良いみたい。
欠点
ちなみに、どうやらOff-Screen Rendering機能にはいくつかの欠点があるよう。それは主に以下の2点。
- Chrome Runtime Styleが使えない
- 少しパフォーマンスが低下する
まず、Chrome Runtime Styleが使えないことに関しては、ランタイムスタイルの設定に関するドキュメントに以下の記載があった。このため、Chromiumにある一部の機能が使えなくなると思われる。
Chrome style provides the full Chrome UI and browser functionality whereas Alloy style provides less default browser functionality but adds additional client callbacks and support for windowless (off-screen) rendering.
次に、パフォーマンスが低下することに関しては、WikiのGeneral Usageにて、以下の記述があった。
Off-screen rendering does not currently support accelerated compositing so performance may suffer as compared to a windowed browser.
手順
具体的な手順はWikiのGeneral Usageの"Off-Screen Rendering"という項目にあった。
基本的な流れは以下。(一部省略しているので、詳細は↑を要確認)
-
CefRenderHandler
を実装、CefClient::GetRenderHandler
からそれのインスタンスを返す -
CefBrowserHost::CreateBrowser()
実行時に、CefWindowInfo
をウィンドウレスモードで設定 -
CefRenderHandler::GetViewRect()
で描画サイズをCEFに教える -
CefRenderHandler::OnPaint()
から描画結果を受け取り、好きな場所(ウィンドウ等)に映す - ウィンドウサイズが変更されるなどでもし描画サイズが変わったら、
CefBrowserHost::WasResized()
を呼び出してCEFに再描画
このうちのCefRenderHandler
は描画の大きさを教え、描画結果を受け取るのに使うわけだ。ただ、General Usageでの説明では、明示されてない限り全部実装しようとは書いてある。
(実際は、GetViewRect
とOnPaint
あたりさえ実装していれば良いっぽい?GitHubで検索すると、それらしか実装していないコードが散見される。)
それと、パフォーマンスが低いことに関しては、OnPaint()
ではなくOnAcceleratedPaint()
を使うとマシになる...?(真偽不明)これの場合、直接GPUライブラリで描画をできるっぽい。これについても少し調査したいところ。
参考資料
GitHubを漁ったり、私が作ったりした実装。参考になるかもしれない。

Off-Screen Rendering つまずきポイント
自分がOSRする時に躓いたポイントをメモしておく。
CefRenderHandler::OnPaint()
が呼ばれない問題
いくつかの可能性が考えられる。チェックリスト↓
-
CefWindowInfo.SetAsWindowless
を実行していない -
CefWindowInfo.external_begin_frame_enabled
をtrue
にしている
この場合、恐らくフレームの描画のタイミングを自分でCefBrowserHost::SendExternalBeginFrame
を使ってCEFに教えなければならない。このため、それをしない限りOnPaint
が呼ばれなくなる。

OSRする場合、これはヘッドレスブラウザの描画を自分で実装する、という状況に等しい。このため、キーボードやマウスの入力も自分で収集してCEFに教える必要があり、これは少し面倒。
自分はRustのGUIにCEFを埋め込みたいと思っているので、OSRは大変であまりしたくない。
どうにかしてOSRを使わずにCEF側が作ったウィンドウを拡張することはできないだろうか。
ちょっと調べてみる。
事例?
OSRを使わずにCEFのウィンドウをいじっている、かもしれないやつ。
cefclient
cefclientを普通に起動すると、Chromiumのタブがないバージョンが起動する。
ChromiumのUIはViews Frameworkというので作られているようで、それをCEFから使えるのかな。
Views Frameworkの説明(以下)には、Viewの階層の根本にはWidgetがあって、それがウィンドウの受け取ったイベントをViewに渡す橋渡しの役割を担っているらしい。であれば、これをCEFで作れば、ウィンドウを好き勝手いじれる?
At the root of a View hierarchy is a Widget, which is a native window. The native window receives messages from Windows, converts them into something the View hierarchy can understand, and then passes them to the RootView. The RootView then begins propagation of the event into the View hierarchy.
そしてコードを見ていたら、どうやらCefWindow
からmacOSのNSWindow
を取り出すことはできるようだ。そしたら、これでネイティブコントロールとか付け加えられたりするんかね?ちょっとやってみようかな。
解決?
文献をあさっていると、CefWindowInfo
のSetAsChild
でウィンドウの中に配置できる、的なのを見つけた。それをRustで試してみたところ動いた!マウスイベントもキーボードイベントも受け付けてる。とても良い。
これでOSRを使わずに自分のウィンドウの中にCEFを埋め込むことができて、マウスイベントとかもCEFが受け付けられる。
ただ、懸念点としてどうやらこれをするとAlloy Runtime Styleになってしまうようだ。このためか、chrome://settingsが開けないのと、Inspect Elementや画像のコピーなどの右クリックの機能がなかったり。
[28484:173465:0805/120221.801136:ERROR:cef/libcef/browser/browser_host_create.cc:249] Chrome style is not supported for this browser
そしてこれに関してはIssueがあるようだ。

macOSでのCEFアプリのデバッグ
CEFを使っている時に自分のコードではなくCEF内部でクラッシュが起きた時、原因究明のためにデバッガを使うことがあると思う。macOSにてlldbを使ってデバッグする時、場合によっては以下のようにシンボル名が不明(___lldb_unnamed_symbol????
)となってしまう。これはデバッグが大変。
(lldb) r Process 46038 launched: '/Users/satishk2/Library/HTML5VideoPlayer.app/Contents/MacOS/HTML5VideoPlayer' (arm64) Process 46038 stopped * thread #1, queue = 'com.apple.main-thread', stop reason = EXC_BREAKPOINT (code=1, subcode=0x10ed74dac) frame #0: 0x000000010ed74dac Chromium Embedded Framework`___lldb_unnamed_symbol7479 + 5367680 Chromium Embedded Framework`fontations_ffi$cxxbridge1$BridgeBitmapGlyph$operator$sizeof: -> 0x10ed74dac <+1023088>: brk #0 0x10ed74db0 <+1023092>: hlt #0 0x10ed74db4 <+1023096>: brk #0x1 0x10ed74db8 <+1023100>: sub sp, sp, #0x70 Target 0: (HTML5VideoPlayer) stopped.
https://magpcss.org/ceforum/viewtopic.php?f=6&t=19860&p=55738&hilit=lldb+unnamed+symbol+dsym より
こういう時はmacOSの場合、デバッグシンボル(dSYM)を読み込ませてあげれば良い。デバッグシンボルはCEFのビルドの配布場所からダウンロードできる。
デバッグシンボルのダウンロード
デバッグシンボルをダウンロードする時は、自分が使っているCEFのビルド時に作られた、同じバージョンの物を使う必要がある。このため、バージョンやコミットIDが一致している必要がある。また、CEFデバッグビルドかCEFリリースビルドのどちらを使っているかも合わせる必要がある。
具体的には、例としてCEFが138.0.34+ga94b31b+chromium-138.0.7204.184
のバージョンのビルドを使っているとして、まずCEFのビルドの配布場所に行く。そして今回はmacOSなので、MacOSのタブを開く。
その後、Version Filterという入力ボックスに自分の求めているバージョン、今回の場合138.0.34+ga94b31b
を入力しApplyでビルドを検索。
そうすることで、下にそのバージョンのビルドが表示されるので、そこからCEFのデバッグビルドを使っているならDebug Symbols、リリースビルドならRelease Symbolsをダウンロードすれば良い。
(ちなみに、普通にRustのcef-rsを使っている場合、Release Symbolsを使えば良い。)
デバッグシンボルの適用
ダウンロードしたdSYMをどうやら、CEFのバイナリの隣に配置すればいいよう。配置場所の例: cefsimple.app/Contents/Frameworks/Chromium Embedded Framework.framework
この状態でlldbを通常通り起動し、以下のコマンドで使いたいデバッグシンボルを追加できる。ただし、dSYMの持つUUIDと自分が使っているCEFのUUIDが一致していなければ、シンボル名は不明のままになる。(詳細は後述)
(lldb) target symbols add "Chromium Embedded Framework.framework.dSYMのパス"
この時、以下のようなエラーが出る場合があるが、CEFのバイナリの横にdSYMを置いている場合無視しても構わない(っぽい)。
error: symbol file 'Chromium Embedded Framework.dSYM/Contents/Resources/DWARF/Chromium Embedded Framework' does not match any existing module
もしtarget symbols add
コマンドを実行する前にdSYMとCEFのUUIDが同じかどうか確認したい場合、以下のようにdwarfdumpコマンドでUUIDを確認することができる。この時、UUIDが違えばうまくいかない。
$ # まず、自分のビルドしたアプリが使ってるCEFのUUIDを確認する。
$ dwarfdump --uuid 'my-cef-app.app/Contents/Frameworks/Chromium Embedded Framework.framework/Chromium Embedded Framework'
UUID: 4C4C444E-5555-3144-A11D-591330C7B1F1 (arm64) my-cef-app.app/Contents/Frameworks/Chromium Embedded Framework.framework/Chromium Embedded Framework
$ # dSYMの方を確認する。
$ dwarfdump --uuid 'Chromium Embedded Framework.dSYM'
UUID: 4C4C444E-5555-3144-A11D-591330C7B1F1 (arm64) Chromium Embedded Framework.dSYM

Too many open filesと出てビルドできない
エラーは以下のような感じ。このチュートリアル通りにビルドをしようとして遭遇。
$ autoninja -C out/Debug_GN_arm64 cef
offline mode
ninja: Entering directory `out/Debug_GN_arm64'
(ある程度、省略)
build step: cxx "./obj/net/dns/dns/dns_config_service.o"
stderr:
In file included from ../../net/dns/dns_config_service.cc:5:
In file included from ../../net/dns/dns_config_service.h:17:
In file included from ../../base/timer/timer.h:72:
In file included from ../../base/functional/bind.h:14:
In file included from ../../base/functional/bind_internal.h:29:
In file included from ../../third_party/abseil-cpp/absl/functional/function_ref.h:54:
In file included from ../../third_party/abseil-cpp/absl/functional/internal/function_ref.h:22:
In file included from ../../third_party/abseil-cpp/absl/functional/any_invocable.h:43:
In file included from ../../third_party/abseil-cpp/absl/functional/internal/any_invocable.h:62:
../../third_party/libc++/src/include/new:99:12: fatal error: cannot open file '../../third_party/libc++/src/include/__new/placement_new_delete.h': Too many open files
99 | # include <__new/placement_new_delete.h>
| ^
1 error generated.
build failed
local:1 remote:0 cache:0 cache-write:0(err:0) fallback:0 retry:0 skip:1071
fs: ops: 25(err:11) / r:14(err:0) 1.02MiB / w:0(err:0) 0B
1.45s Build Failure: 1 done 1 failed 0 remaining - 0.69/s
1 steps failed: exit=1
see ./out/Debug_GN_arm64/siso_output for full command line and output
or ./out/Debug_GN_arm64/siso.INFO
use ./out/Debug_GN_arm64/siso_failed_commands.sh to re-run failed commands
解決策
純粋に表示されてる通りただただファイルを多く開こうとしたが上限に達しただけのよう。以下のコマンドで上限を増やしたらうまく動作した。(デフォルトの上限は256の様子)
$ ulimit -Sn 512
参考: https://github.com/emscripten-core/emscripten/issues/3460

DCHECK failed: !bundle_id.empty().
CFBundleIdentifier
をInfo.plist
に入力しているのに、以下のようにバンドルIDがないというエラーが表示された。
[0821/141554.382924:FATAL:cef/libcef/common/util_mac.mm:49] DCHECK failed: !bundle_id.empty().
この時、アプリのアイコンは使えないマークになっていた。
解決
どうやら実行ファイルとバンドルの名前は一致していないといけないようで、YourApp.app/Contents/MacOS/your-app
のように、YourApp.app
のYourAppとyour-app
が違うとこうなるっぽい。そのため、your-app
もYourApp
のように一致させれば良い。
CEFとは少し関係ないが、macOS特有の問題で沼った。