Open10

CEFの情報整理

髙木 祐来髙木 祐来

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

ちなみに、RustでCEFを使う方法自体は以下に記録していてる。
https://zenn.dev/tasuren/scraps/01f47381e351d1

髙木 祐来髙木 祐来

用語

  • cefsimple: 最低限の実装がされた、公式のサンプル
  • cefclient: それなりの機能が実装された、公式のサンプル
  • magpcss.org: CEFに関する相談ができるフォーラム、困った時の貴重な情報源になる。
  • Bootstrap/Runtime: Chromiumの起動設定。AlloyとChromeの二種がある。(詳細後述)
  • Runtime Style: ↑と組み合わせて使う、動作設定。AlloyとChromeの二種がある。(詳細後述)

URL集

髙木 祐来髙木 祐来

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にて、CefCommandLineAppendSwitchを使ってオプションを起動前に設定すれば良い。
例:

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.plistCFBundleExecutableCFBundleDisplayNameの名前が別のアプリと被っている場合に発生した。例: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は前述の通りサ終している。
サ終などの詳細はここが参考になる、と思う。

脚注
  1. https://groups.google.com/g/cef-announce/c/s1WaovAopFo/m/LV5eiNX1BgAJStarting ↩︎

  2. https://bitbucket.org/chromiumembedded/cef/wiki/Architecture.md#markdown-header-modern-architecture ↩︎

髙木 祐来髙木 祐来

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"という項目にあった。
https://bitbucket.org/chromiumembedded/cef/wiki/GeneralUsage#markdown-header-off-screen-rendering

基本的な流れは以下。(一部省略しているので、詳細は↑を要確認)

  1. CefRenderHandlerを実装、CefClient::GetRenderHandlerからそれのインスタンスを返す
  2. CefBrowserHost::CreateBrowser()実行時に、CefWindowInfoをウィンドウレスモードで設定
  3. CefRenderHandler::GetViewRect()で描画サイズをCEFに教える
  4. CefRenderHandler::OnPaint()から描画結果を受け取り、好きな場所(ウィンドウ等)に映す
  5. ウィンドウサイズが変更されるなどでもし描画サイズが変わったら、CefBrowserHost::WasResized()を呼び出してCEFに再描画

このうちのCefRenderHandlerは描画の大きさを教え、描画結果を受け取るのに使うわけだ。ただ、General Usageでの説明では、明示されてない限り全部実装しようとは書いてある。
(実際は、GetViewRectOnPaintあたりさえ実装していれば良いっぽい?GitHubで検索すると、それらしか実装していないコードが散見される。)

それと、パフォーマンスが低いことに関しては、OnPaint()ではなくOnAcceleratedPaint()を使うとマシになる...?(真偽不明)これの場合、直接GPUライブラリで描画をできるっぽい。これについても少し調査したいところ。

参考資料

GitHubを漁ったり、私が作ったりした実装。参考になるかもしれない。

髙木 祐来髙木 祐来

Off-Screen Rendering つまずきポイント

自分がOSRする時に躓いたポイントをメモしておく。

CefRenderHandler::OnPaint()が呼ばれない問題

いくつかの可能性が考えられる。チェックリスト↓

  • CefWindowInfo.SetAsWindowlessを実行していない
  • CefWindowInfo.external_begin_frame_enabledtrueにしている
    この場合、恐らくフレームの描画のタイミングを自分で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を取り出すことはできるようだ。そしたら、これでネイティブコントロールとか付け加えられたりするんかね?ちょっとやってみようかな。
https://github.com/chromiumembedded/cef/blob/0638405fcaa83266b66c59377ff32024c7207925/tests/cefclient/browser/views_window_mac.mm#L13-L14

解決?

文献をあさっていると、CefWindowInfoSetAsChildでウィンドウの中に配置できる、的なのを見つけた。それを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があるようだ。
https://github.com/chromiumembedded/cef/issues/3294

髙木 祐来髙木 祐来

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().

CFBundleIdentifierInfo.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-appYourAppのように一致させれば良い。
CEFとは少し関係ないが、macOS特有の問題で沼った。