feat: add custom CEF V8 extensions support (#17)

* feat: Register extensions in render process

- Add CefExtensions type to hold V8 extension code
- Pass extensions through BrowserProcessAppBuilder
- Register extensions in RenderProcessHandler on WebKit initialization
- Decode JSON extensions from command line switch
- Prefix extension names with "v8/" per CEF convention
- Include actual JSON in error messages for debugging

l

* feat: refactor window.cef API and register as CEF extension

* fix: remove debug print statements in render process handler

* refactor: centralize EXTENSIONS_SWITCH constant in util.rs

* fmt

* refactor: implement Default trait for CefApiHandler

* docs: add documentation for CefApiHandler and its JavaScript API functions

---------

Co-authored-by: not-elm <elmgameinfo@gmail.com>
This commit is contained in:
elm
2026-02-04 16:08:32 +09:00
committed by GitHub
parent b7900a24a0
commit 519aa2b2bf
21 changed files with 473 additions and 454 deletions

3
.gitignore vendored
View File

@@ -3,4 +3,5 @@ target/
book/
.DS_Store**/.DS_Store
.claude/memo.md
docs/plans/
docs/plans/
.worktrees/

View File

@@ -24,6 +24,11 @@
### Features
- Added `PreloadScripts` component for specifying JavaScript to be executed when the page is initialized.
- Added `CefExtensions` type for registering custom JavaScript APIs via CEF's `register_extension`
- Extensions are global and load before any page scripts
- New `extensions` example demonstrating custom JS APIs
- Refactored `window.cef` API (`brp`, `emit`, `listen`) to be registered as a CEF extension during `on_web_kit_initialized`
- The API is now available earlier in the page lifecycle
### Bug Fixes

100
CLAUDE.md
View File

@@ -10,93 +10,87 @@ This is `bevy_cef`, a Bevy plugin that integrates the Chromium Embedded Framewor
### Multi-Process Design
- **Browser Process**: Main application process running Bevy (`bevy_cef_core::browser_process`)
- **Render Process**: Separate CEF render process (`bevy_cef_core::render_process`)
- **Render Process**: Separate CEF render process (`bevy_cef_core::render_process`)
- Communication through IPC channels and Bevy Remote Protocol (BRP)
### Core Components
- `CefWebviewUri`: Component specifying webview URL (remote or local via `cef://localhost/`)
- `WebviewSize`: Controls webview rendering dimensions (default 800x800)
- `WebviewExtendStandardMaterial`: Material for rendering webviews on 3D meshes
- `HostWindow`: Optional parent window specification
- `ZoomLevel`: Webview zoom control
- `AudioMuted`: Audio muting control
- `CefWebviewUri`: URL specification (`CefWebviewUri::new("url")` or `CefWebviewUri::local("file.html")`)
- `WebviewSize`: Rendering dimensions (default 800x800), controls texture resolution not 3D size
- `WebviewExtendStandardMaterial`: Primary material for 3D mesh rendering
- `WebviewSpriteMaterial`: Material for 2D sprite rendering
- `HostWindow`: Optional parent window (defaults to PrimaryWindow)
- `ZoomLevel`: f64 zoom control (0.0 = default)
- `AudioMuted`: bool for audio control
- `PreloadScripts`: Vec<String> of scripts to execute before page scripts
- `CefExtensions`: Custom JS extensions via `register_extension` (global to all webviews)
### Plugin Architecture
The main `CefPlugin` orchestrates several sub-plugins:
- `LocalHostPlugin`: Serves local assets via custom scheme
- `MessageLoopPlugin`: CEF message loop integration
The main `CefPlugin` accepts `CommandLineConfig` for CEF command-line switches and `CefExtensions` for custom JavaScript APIs. Sub-plugins:
- `LocalHostPlugin`: Serves local assets via `cef://localhost/` scheme
- `MessageLoopPlugin`: CEF message loop integration (macOS uses `CefDoMessageLoopWork()`)
- `WebviewCoreComponentsPlugin`: Core component registration
- `WebviewPlugin`: Main webview management
- `IpcPlugin`: Inter-process communication
- `KeyboardPlugin`, `NavigationPlugin`, `ZoomPlugin`, `AudioMutePlugin`: Feature-specific functionality
- `WebviewPlugin`: Webview lifecycle and DevTools
- `IpcPlugin`: IPC containing `IpcRawEventPlugin` and `HostEmitPlugin`
- `KeyboardPlugin`, `SystemCursorIconPlugin`, `NavigationPlugin`, `ZoomPlugin`, `AudioMutePlugin`
- `RemotePlugin`: Auto-added for BRP support if not present
### IPC System
Three communication patterns:
1. **JS Emit**: Webview → Bevy app via `JsEmitEventPlugin<T>`
2. **Host Emit**: Bevy app → Webview via event emission
3. **BRP (Bevy Remote Protocol)**: Bidirectional RPC calls
1. **JS Emit**: Webview → Bevy via `JsEmitEventPlugin<E>` where E: `DeserializeOwned + Send + Sync + 'static`
- Events wrapped in `Receive<E>` EntityEvent
- JavaScript: `window.cef.emit('event_name', data)`
2. **Host Emit**: Bevy → Webview via `HostEmitEvent` (EntityEvent)
- JavaScript: `window.cef.listen('event_name', callback)`
3. **BRP**: Bidirectional RPC via `bevy_remote`
- JavaScript: `await window.cef.brp({ method: 'method_name', params: {...} })`
### EntityEvent Pattern
Navigation and DevTools events are `EntityEvent` types requiring explicit `webview: Entity` field:
- `HostEmitEvent`, `RequestGoBack`, `RequestGoForward`, `RequestShowDevTool`, `RequestCloseDevtool`
## Development Commands
### Code Quality
```bash
# Fix and format code
make fix
# Which runs:
# cargo clippy --fix --allow-dirty --allow-staged --workspace --all --all-features
# cargo fmt --all
```
### Development Setup
The build system automatically handles CEF dependencies on macOS with debug feature:
- Installs `bevy_cef_debug_render_process` tool
- Installs `export-cef-dir` tool
- Downloads/extracts CEF framework to `$HOME/.local/share/cef`
# Run examples (macOS requires debug feature)
cargo run --example simple --features debug
### Manual Installation
```bash
# Install debug render process tool
make install
# Or: cargo install --path ./crates/bevy_cef_debug_render_process --force
```
## Key Examples
- `examples/simple.rs`: Basic webview on 3D plane
- `examples/js_emit.rs`: JavaScript to Bevy communication
- `examples/host_emit.rs`: Bevy to JavaScript communication
- `examples/brp.rs`: Bidirectional RPC with devtools
- `examples/navigation.rs`: Page navigation controls
- `examples/zoom_level.rs`: Zoom functionality
- `examples/sprite.rs`: Webview as 2D sprite
- `examples/devtool.rs`: Chrome DevTools integration
### Debug Tools Setup (macOS)
Manual installation required before running with `debug` feature:
```bash
cargo install export-cef-dir
export-cef-dir --force $HOME/.local/share
cargo install bevy_cef_debug_render_process
mv $HOME/.cargo/bin/bevy_cef_debug_render_process "$HOME/.local/share/Chromium Embedded Framework.framework/Libraries/bevy_cef_debug_render_process"
```
## Local Asset Loading
Local HTML/assets are served via the custom `cef://localhost/` scheme:
Local HTML/assets served via `cef://localhost/` scheme:
- Place assets in `assets/` directory
- Reference as `CefWebviewUri::local("filename.html")`
- Or manually: `cef://localhost/filename.html`
## Testing
No automated tests are present in this codebase. Testing is done through the example applications.
### Manually Testing
- Run tests with `cargo test --workspace --all-features`
No automated tests. Testing done through examples:
- `cargo test --workspace --all-features` (for any future tests)
- Examples: simple, js_emit, host_emit, brp, navigation, zoom_level, sprite, devtool, custom_material, preload_scripts, extensions
## Platform Notes
- Currently focused on macOS development (see Cargo.toml target-specific dependencies)
- CEF framework must be available at `$HOME/.local/share/cef`
- Uses `objc` crate for macOS-specific window handling
- DYLD environment variables required for CEF library loading
- Primary platform: macOS (uses `objc` crate for window handling)
- CEF framework location: `$HOME/.local/share/Chromium Embedded Framework.framework`
- Windows/Linux: Infrastructure ready but needs testing
- Key resources (`Browsers`, library loaders) are `NonSend` - CEF is not thread-safe
## Workspace Structure
This is a Cargo workspace with:
- Root crate: `bevy_cef` (public API)
- `crates/bevy_cef_core`: Core CEF integration logic
- `crates/bevy_cef_debug_render_process`: Debug render process executable
- `examples/demo`: Standalone demo application
- `crates/bevy_cef_debug_render_process`: Debug render process executable

39
assets/extensions.html Normal file
View File

@@ -0,0 +1,39 @@
<!DOCTYPE html>
<html>
<head>
<title>CEF Extensions Example</title>
<style>
body { font-family: sans-serif; padding: 20px; background: #1a1a2e; color: #eee; }
button { padding: 10px 20px; margin: 5px; cursor: pointer; }
#output { margin-top: 20px; padding: 10px; background: #16213e; border-radius: 5px; }
</style>
</head>
<body>
<h1>CEF Extensions Example</h1>
<p>Testing custom JavaScript extensions registered via register_extension.</p>
<button onclick="testExtension()">Test myGame.sendScore()</button>
<button onclick="checkVersion()">Check Version</button>
<div id="output">Output will appear here...</div>
<script>
function testExtension() {
if (typeof myGame !== 'undefined') {
myGame.sendScore(Math.floor(Math.random() * 100));
document.getElementById('output').innerHTML = 'Score sent! Check Bevy console.';
} else {
document.getElementById('output').innerHTML = 'Error: myGame extension not found';
}
}
function checkVersion() {
if (typeof myGame !== 'undefined' && myGame.version) {
document.getElementById('output').innerHTML = 'myGame version: ' + myGame.version;
} else {
document.getElementById('output').innerHTML = 'Error: myGame.version not found';
}
}
</script>
</body>
</html>

View File

@@ -15,7 +15,7 @@
<h1 id="count" style="color: aqua">0</h1>
<script>
const count = document.getElementById("count");
window.cef.listen("count", (payload) => {
cef.listen("count", (payload) => {
count.innerText = payload;
})
</script>

View File

@@ -17,7 +17,7 @@
let count = 0;
window.setInterval(() => {
console.log("Emitting count:", count);
window.cef.emit({
cef.emit({
count,
});
countElement.innerText = count;

View File

@@ -5,6 +5,7 @@ mod client_handler;
mod command_line_config;
mod context_menu_handler;
mod display_handler;
mod extensions;
mod localhost;
mod message_pump;
mod renderer_handler;
@@ -16,6 +17,7 @@ pub use browsers::*;
pub use client_handler::*;
pub use command_line_config::*;
pub use context_menu_handler::*;
pub use extensions::*;
pub use localhost::*;
pub use message_pump::*;
pub use renderer_handler::*;

View File

@@ -1,3 +1,4 @@
use crate::browser_process::CefExtensions;
use crate::browser_process::CommandLineConfig;
use crate::browser_process::MessageLoopTimer;
use crate::browser_process::browser_process_handler::BrowserProcessHandlerBuilder;
@@ -17,17 +18,20 @@ pub struct BrowserProcessAppBuilder {
object: *mut RcImpl<_cef_app_t, Self>,
message_loop_working_requester: Sender<MessageLoopTimer>,
config: CommandLineConfig,
extensions: CefExtensions,
}
impl BrowserProcessAppBuilder {
pub fn build(
message_loop_working_requester: Sender<MessageLoopTimer>,
config: CommandLineConfig,
extensions: CefExtensions,
) -> cef::App {
cef::App::new(Self {
object: core::ptr::null_mut(),
message_loop_working_requester,
config,
extensions,
})
}
}
@@ -43,6 +47,7 @@ impl Clone for BrowserProcessAppBuilder {
object,
message_loop_working_requester: self.message_loop_working_requester.clone(),
config: self.config.clone(),
extensions: self.extensions.clone(),
}
}
}
@@ -65,7 +70,6 @@ impl ImplApp for BrowserProcessAppBuilder {
let Some(command_line) = command_line else {
return;
};
for switch in &self.config.switches {
command_line.append_switch(Some(&(*switch).into()));
}
@@ -84,6 +88,7 @@ impl ImplApp for BrowserProcessAppBuilder {
fn browser_process_handler(&self) -> Option<BrowserProcessHandler> {
Some(BrowserProcessHandlerBuilder::build(
self.message_loop_working_requester.clone(),
self.extensions.clone(),
))
}

View File

@@ -1,4 +1,4 @@
use crate::prelude::MessageLoopTimer;
use crate::prelude::{CefExtensions, EXTENSIONS_SWITCH, MessageLoopTimer};
use cef::rc::{Rc, RcImpl};
use cef::*;
use std::sync::mpsc::Sender;
@@ -9,15 +9,18 @@ use std::sync::mpsc::Sender;
pub struct BrowserProcessHandlerBuilder {
object: *mut RcImpl<cef_dll_sys::cef_browser_process_handler_t, Self>,
message_loop_working_requester: Sender<MessageLoopTimer>,
extensions: CefExtensions,
}
impl BrowserProcessHandlerBuilder {
pub fn build(
message_loop_working_requester: Sender<MessageLoopTimer>,
extensions: CefExtensions,
) -> BrowserProcessHandler {
BrowserProcessHandler::new(Self {
object: core::ptr::null_mut(),
message_loop_working_requester,
extensions,
})
}
}
@@ -48,6 +51,7 @@ impl Clone for BrowserProcessHandlerBuilder {
Self {
object,
message_loop_working_requester: self.message_loop_working_requester.clone(),
extensions: self.extensions.clone(),
}
}
}
@@ -64,6 +68,15 @@ impl ImplBrowserProcessHandler for BrowserProcessHandlerBuilder {
command_line.append_switch(Some(&"ignore-certificate-errors".into()));
command_line.append_switch(Some(&"ignore-ssl-errors".into()));
command_line.append_switch(Some(&"enable-logging=stderr".into()));
// Pass extensions to render process via command line
if !self.extensions.is_empty()
&& let Ok(json) = serde_json::to_string(&self.extensions.0)
{
command_line.append_switch_with_value(
Some(&EXTENSIONS_SWITCH.into()),
Some(&json.as_str().into()),
);
}
}
fn on_schedule_message_pump_work(&self, delay_ms: i64) {

View File

@@ -0,0 +1,47 @@
use bevy::platform::collections::HashMap;
use serde::{Deserialize, Serialize};
/// Custom JavaScript extensions to register via CEF's `register_extension`.
///
/// Extensions are global to all webviews and loaded before any page scripts run.
/// Use existing `window.cef.emit()`, `window.cef.listen()`, and `window.cef.brp()`
/// APIs within your extension code for Bevy communication.
///
/// # Example
///
/// ```no_run
/// use bevy_cef_core::prelude::*;
///
/// let extensions = CefExtensions::new()
/// .add("myGame", r#"
/// var myGame = {
/// sendScore: function(score) {
/// window.cef.emit('score_update', { score: score });
/// }
/// };
/// "#);
/// ```
#[derive(Clone, Debug, Default, Serialize, Deserialize)]
pub struct CefExtensions(pub HashMap<String, String>);
impl CefExtensions {
/// Creates a new empty extensions collection.
pub fn new() -> Self {
Self::default()
}
/// Adds a JavaScript extension.
///
/// # Arguments
/// * `name` - Extension name (will be prefixed with `v8/` internally)
/// * `code` - JavaScript code defining the extension's API
pub fn add(mut self, name: impl Into<String>, code: impl Into<String>) -> Self {
self.0.insert(name.into(), code.into());
self
}
/// Returns true if no extensions are registered.
pub fn is_empty(&self) -> bool {
self.0.is_empty()
}
}

View File

@@ -10,8 +10,6 @@ pub mod prelude {
#[cfg(target_os = "macos")]
pub use crate::debug::*;
pub use crate::render_process::app::*;
pub use crate::render_process::brp::*;
pub use crate::render_process::emit::*;
pub use crate::render_process::execute_render_process;
pub use crate::render_process::render_process_handler::*;
pub use crate::util::*;

View File

@@ -3,9 +3,7 @@ use cef::args::Args;
use cef::{api_hash, execute_process, sys};
pub mod app;
pub mod brp;
pub mod emit;
pub mod listen;
pub mod cef_api_handler;
pub mod render_process_handler;
/// Execute the CEF render process.

View File

@@ -1,181 +0,0 @@
use crate::prelude::{BRP_PROMISES, PROCESS_MESSAGE_BRP, v8_value_to_json};
use cef::rc::{ConvertParam, ConvertReturnValue, Rc, RcImpl};
use cef::{
CefString, Frame, ImplFrame, ImplListValue, ImplProcessMessage, ImplV8Handler, ImplV8Value,
ProcessId, V8Handler, V8Value, WrapV8Handler, process_message_create, sys,
v8_value_create_promise, v8_value_create_string,
};
use cef_dll_sys::{_cef_v8_handler_t, _cef_v8_value_t, cef_process_id_t, cef_string_t};
use std::os::raw::c_int;
use std::ptr::write;
/// Implements the `window.brp` function in JavaScript.
///
/// The function definition is `async <T>(request: BrpRequest) -> Promise<T>`.
///
/// The flow from the execution of the function to the return of the result to the Javascript side is as follows:
/// 1. Send `BrpRequest` to the browser process.
/// 2. The browser process receives `BrpResult` and sends it to the render process.
/// 3. The render process receives the result in `on_process_message_received`.
/// 4. The render process resolves the result to the `Promise` of `window.brp`.
pub struct BrpBuilder {
object: *mut RcImpl<_cef_v8_handler_t, Self>,
frame: Frame,
}
impl BrpBuilder {
pub fn build(frame: Frame) -> V8Handler {
V8Handler::new(Self {
object: core::ptr::null_mut(),
frame,
})
}
}
impl Rc for BrpBuilder {
fn as_base(&self) -> &sys::cef_base_ref_counted_t {
unsafe {
let base = &*self.object;
std::mem::transmute(&base.cef_object)
}
}
}
impl WrapV8Handler for BrpBuilder {
fn wrap_rc(&mut self, object: *mut RcImpl<_cef_v8_handler_t, Self>) {
self.object = object;
}
}
impl Clone for BrpBuilder {
fn clone(&self) -> Self {
let object = unsafe {
let rc_impl = &mut *self.object;
rc_impl.interface.add_ref();
rc_impl
};
Self {
object,
frame: self.frame.clone(),
}
}
}
impl ImplV8Handler for BrpBuilder {
fn execute(
&self,
_: Option<&CefString>,
_: Option<&mut V8Value>,
arguments: Option<&[Option<V8Value>]>,
ret: Option<&mut Option<V8Value>>,
_: Option<&mut CefString>,
) -> c_int {
if let Some(mut process) = process_message_create(Some(&PROCESS_MESSAGE_BRP.into()))
&& let Some(promise) = v8_value_create_promise()
{
if let Some(arguments_list) = process.argument_list()
&& let Some(arguments) = arguments
&& let Some(Some(arg)) = arguments.first()
&& let Some(brp_request) = v8_value_to_json(arg)
&& let Ok(brp_request) = serde_json::to_string(&brp_request)
&& let Some(ret) = ret
{
let id = uuid::Uuid::new_v4().to_string();
arguments_list.set_string(0, Some(&id.as_str().into()));
arguments_list.set_string(1, Some(&brp_request.as_str().into()));
self.frame.send_process_message(
ProcessId::from(cef_process_id_t::PID_BROWSER),
Some(&mut process),
);
ret.replace(promise.clone());
let mut promises = BRP_PROMISES.lock().unwrap();
promises.insert(id, promise);
} else {
let mut exception =
v8_value_create_string(Some(&"Failed to execute BRP request".into()));
promise.resolve_promise(exception.as_mut());
}
}
1
}
fn init_methods(object: &mut _cef_v8_handler_t) {
init_methods::<Self>(object);
}
fn get_raw(&self) -> *mut _cef_v8_handler_t {
self.object.cast()
}
}
fn init_methods<I: ImplV8Handler>(object: &mut _cef_v8_handler_t) {
object.execute = Some(execute::<I>);
}
extern "C" fn execute<I: ImplV8Handler>(
self_: *mut _cef_v8_handler_t,
name: *const cef_string_t,
object: *mut _cef_v8_value_t,
arguments_count: usize,
arguments: *const *mut _cef_v8_value_t,
retval: *mut *mut _cef_v8_value_t,
exception: *mut cef_string_t,
) -> c_int {
let (arg_self_, arg_name, arg_object, arg_arguments_count, arg_arguments, _, arg_exception) = (
self_,
name,
object,
arguments_count,
arguments,
retval,
exception,
);
let arg_self_: &RcImpl<_, I> = RcImpl::get(arg_self_);
let arg_name = if arg_name.is_null() {
None
} else {
Some(arg_name.into())
};
let arg_name = arg_name.as_ref();
let mut arg_object =
unsafe { arg_object.as_mut() }.map(|arg| (arg as *mut _cef_v8_value_t).wrap_result());
let arg_object = arg_object.as_mut();
let vec_arguments = unsafe { arg_arguments.as_ref() }.map(|arg| {
let arg =
unsafe { std::slice::from_raw_parts(std::ptr::from_ref(arg), arg_arguments_count) };
arg.iter()
.map(|arg| {
if arg.is_null() {
None
} else {
Some((*arg).wrap_result())
}
})
.collect::<Vec<_>>()
});
let arg_arguments = vec_arguments.as_deref();
let mut arg_retval: Option<V8Value> = None;
let arg = Some(&mut arg_retval);
let mut arg_exception = if arg_exception.is_null() {
None
} else {
Some(arg_exception.into())
};
let arg_exception = arg_exception.as_mut();
let r = ImplV8Handler::execute(
&arg_self_.interface,
arg_name,
arg_object,
arg_arguments,
arg,
arg_exception,
);
if let Some(ret) = arg_retval {
// When the result is received, the pointer should be updated here
// and the exception should also be updated.
unsafe {
write(retval, ret.into_raw());
}
}
r
}

View File

@@ -0,0 +1,166 @@
use crate::prelude::{BRP_PROMISES, LISTEN_EVENTS, PROCESS_MESSAGE_BRP, PROCESS_MESSAGE_JS_EMIT};
use crate::util::{IntoString, v8_value_to_json};
use cef::rc::{Rc, RcImpl};
use cef::{
CefString, ImplFrame, ImplListValue, ImplProcessMessage, ImplV8Context, ImplV8Handler,
ImplV8Value, ProcessId, V8Value, WrapV8Handler, process_message_create, sys,
v8_context_get_current_context, v8_value_create_promise, v8_value_create_string,
};
use cef_dll_sys::cef_process_id_t;
use std::os::raw::c_int;
/// Handles the `window.cef` JavaScript API functions.
///
/// This handler is registered as a CEF extension during `on_web_kit_initialized`
/// and provides three native functions:
/// - `__cef_brp`: Async Bevy Remote Protocol requests
/// - `__cef_emit`: Send events from JavaScript to Bevy
/// - `__cef_listen`: Register callbacks for events from Bevy
///
/// The Frame is obtained dynamically via `v8_context_get_current_context().frame()`
/// since extensions are global and not bound to a specific context.
pub struct CefApiHandler {
object: *mut RcImpl<sys::_cef_v8_handler_t, Self>,
}
impl Default for CefApiHandler {
fn default() -> Self {
Self {
object: core::ptr::null_mut(),
}
}
}
impl Rc for CefApiHandler {
fn as_base(&self) -> &sys::cef_base_ref_counted_t {
unsafe {
let base = &*self.object;
std::mem::transmute(&base.cef_object)
}
}
}
impl WrapV8Handler for CefApiHandler {
fn wrap_rc(&mut self, object: *mut RcImpl<sys::_cef_v8_handler_t, Self>) {
self.object = object;
}
}
impl Clone for CefApiHandler {
fn clone(&self) -> Self {
let object = unsafe {
let rc_impl = &mut *self.object;
rc_impl.interface.add_ref();
rc_impl
};
Self { object }
}
}
impl ImplV8Handler for CefApiHandler {
fn execute(
&self,
name: Option<&CefString>,
_object: Option<&mut V8Value>,
arguments: Option<&[Option<V8Value>]>,
ret: Option<&mut Option<V8Value>>,
_exception: Option<&mut CefString>,
) -> c_int {
let Some(name) = name else { return 0 };
let name_str = name.to_string();
match name_str.as_str() {
"__cef_brp" => self.execute_brp(arguments, ret),
"__cef_emit" => self.execute_emit(arguments),
"__cef_listen" => self.execute_listen(arguments),
_ => 0,
}
}
#[inline]
fn get_raw(&self) -> *mut sys::_cef_v8_handler_t {
self.object.cast()
}
}
impl CefApiHandler {
fn execute_brp(
&self,
arguments: Option<&[Option<V8Value>]>,
ret: Option<&mut Option<V8Value>>,
) -> c_int {
let Some(context) = v8_context_get_current_context() else {
return 0;
};
let Some(frame) = context.frame() else {
return 0;
};
if let Some(mut process) = process_message_create(Some(&PROCESS_MESSAGE_BRP.into()))
&& let Some(promise) = v8_value_create_promise()
{
if let Some(arguments_list) = process.argument_list()
&& let Some(arguments) = arguments
&& let Some(Some(arg)) = arguments.first()
&& let Some(brp_request) = v8_value_to_json(arg)
&& let Ok(brp_request) = serde_json::to_string(&brp_request)
&& let Some(ret) = ret
{
let id = uuid::Uuid::new_v4().to_string();
arguments_list.set_string(0, Some(&id.as_str().into()));
arguments_list.set_string(1, Some(&brp_request.as_str().into()));
frame.send_process_message(
ProcessId::from(cef_process_id_t::PID_BROWSER),
Some(&mut process),
);
ret.replace(promise.clone());
let mut promises = BRP_PROMISES.lock().unwrap();
promises.insert(id, promise);
} else {
let mut exception =
v8_value_create_string(Some(&"Failed to execute BRP request".into()));
promise.resolve_promise(exception.as_mut());
}
}
1
}
fn execute_emit(&self, arguments: Option<&[Option<V8Value>]>) -> c_int {
let Some(context) = v8_context_get_current_context() else {
return 0;
};
let Some(frame) = context.frame() else {
return 0;
};
if let Some(mut process) = process_message_create(Some(&PROCESS_MESSAGE_JS_EMIT.into()))
&& let Some(arguments_list) = process.argument_list()
&& let Some(arguments) = arguments
&& let Some(Some(arg)) = arguments.first()
&& let Some(arg) = v8_value_to_json(arg)
&& let Ok(arg) = serde_json::to_string(&arg)
{
arguments_list.set_string(0, Some(&arg.as_str().into()));
frame.send_process_message(
ProcessId::from(cef_process_id_t::PID_BROWSER),
Some(&mut process),
);
}
1
}
fn execute_listen(&self, arguments: Option<&[Option<V8Value>]>) -> c_int {
if let Some(arguments) = arguments
&& let Some(Some(id)) = arguments.first()
&& id.is_string().is_positive()
&& let Some(Some(callback)) = arguments.get(1)
&& callback.is_function().is_positive()
{
LISTEN_EVENTS
.lock()
.unwrap()
.insert(id.string_value().into_string(), callback.clone());
}
1
}
}

View File

@@ -1,82 +0,0 @@
use crate::prelude::PROCESS_MESSAGE_JS_EMIT;
use crate::util::v8_value_to_json;
use cef::rc::{Rc, RcImpl};
use cef::{
CefString, Frame, ImplFrame, ImplListValue, ImplProcessMessage, ImplV8Handler, ProcessId,
V8Handler, V8Value, WrapV8Handler, process_message_create, sys,
};
use cef_dll_sys::cef_process_id_t;
use std::os::raw::c_int;
pub struct EmitBuilder {
object: *mut RcImpl<sys::_cef_v8_handler_t, Self>,
frame: Frame,
}
impl EmitBuilder {
pub fn build(frame: Frame) -> V8Handler {
V8Handler::new(Self {
object: core::ptr::null_mut(),
frame,
})
}
}
impl Rc for EmitBuilder {
fn as_base(&self) -> &sys::cef_base_ref_counted_t {
unsafe {
let base = &*self.object;
std::mem::transmute(&base.cef_object)
}
}
}
impl WrapV8Handler for EmitBuilder {
fn wrap_rc(&mut self, object: *mut RcImpl<sys::_cef_v8_handler_t, Self>) {
self.object = object;
}
}
impl Clone for EmitBuilder {
fn clone(&self) -> Self {
let object = unsafe {
let rc_impl = &mut *self.object;
rc_impl.interface.add_ref();
rc_impl
};
Self {
object,
frame: self.frame.clone(),
}
}
}
impl ImplV8Handler for EmitBuilder {
fn execute(
&self,
_: Option<&CefString>,
_: Option<&mut V8Value>,
arguments: Option<&[Option<V8Value>]>,
_: Option<&mut Option<V8Value>>,
_: Option<&mut CefString>,
) -> c_int {
if let Some(mut process) = process_message_create(Some(&PROCESS_MESSAGE_JS_EMIT.into()))
&& let Some(arguments_list) = process.argument_list()
&& let Some(arguments) = arguments
&& let Some(Some(arg)) = arguments.first()
&& let Some(arg) = v8_value_to_json(arg)
&& let Ok(arg) = serde_json::to_string(&arg)
{
arguments_list.set_string(0, Some(&arg.as_str().into()));
self.frame.send_process_message(
ProcessId::from(cef_process_id_t::PID_BROWSER),
Some(&mut process),
);
}
1
}
fn get_raw(&self) -> *mut sys::_cef_v8_handler_t {
self.object.cast()
}
}

View File

@@ -1,77 +0,0 @@
use crate::prelude::LISTEN_EVENTS;
use crate::util::IntoString;
use cef::rc::{Rc, RcImpl};
use cef::{CefString, Frame, ImplV8Handler, ImplV8Value, V8Handler, V8Value, WrapV8Handler, sys};
use std::os::raw::c_int;
pub struct ListenBuilder {
object: *mut RcImpl<sys::_cef_v8_handler_t, Self>,
frame: Frame,
}
impl ListenBuilder {
pub fn build(frame: Frame) -> V8Handler {
V8Handler::new(Self {
object: core::ptr::null_mut(),
frame,
})
}
}
impl Rc for ListenBuilder {
fn as_base(&self) -> &sys::cef_base_ref_counted_t {
unsafe {
let base = &*self.object;
std::mem::transmute(&base.cef_object)
}
}
}
impl WrapV8Handler for ListenBuilder {
fn wrap_rc(&mut self, object: *mut RcImpl<sys::_cef_v8_handler_t, Self>) {
self.object = object;
}
}
impl Clone for ListenBuilder {
fn clone(&self) -> Self {
let object = unsafe {
let rc_impl = &mut *self.object;
rc_impl.interface.add_ref();
rc_impl
};
Self {
object,
frame: self.frame.clone(),
}
}
}
impl ImplV8Handler for ListenBuilder {
fn execute(
&self,
_: Option<&CefString>,
_: Option<&mut V8Value>,
arguments: Option<&[Option<V8Value>]>,
_: Option<&mut Option<V8Value>>,
_: Option<&mut CefString>,
) -> c_int {
if let Some(arguments) = arguments
&& let Some(Some(id)) = arguments.first()
&& 0 < id.is_string()
&& let Some(Some(callback)) = arguments.get(1)
&& 0 < callback.is_function()
{
LISTEN_EVENTS
.lock()
.unwrap()
.insert(id.string_value().into_string(), callback.clone());
}
1
}
#[inline]
fn get_raw(&self) -> *mut sys::_cef_v8_handler_t {
self.object.cast()
}
}

View File

@@ -1,6 +1,5 @@
use crate::prelude::{EmitBuilder, IntoString};
use crate::render_process::brp::BrpBuilder;
use crate::render_process::listen::ListenBuilder;
use crate::prelude::{EXTENSIONS_SWITCH, IntoString};
use crate::render_process::cef_api_handler::CefApiHandler;
use crate::util::json_to_v8;
use crate::util::v8_accessor::V8DefaultAccessorBuilder;
use crate::util::v8_interceptor::V8DefaultInterceptorBuilder;
@@ -8,14 +7,30 @@ use bevy::platform::collections::HashMap;
use bevy_remote::BrpResult;
use cef::rc::{Rc, RcImpl};
use cef::{
Browser, CefString, DictionaryValue, Frame, ImplBrowser, ImplDictionaryValue, ImplFrame,
ImplListValue, ImplProcessMessage, ImplRenderProcessHandler, ImplV8Context, ImplV8Value,
ProcessId, ProcessMessage, V8Context, V8Propertyattribute, V8Value, WrapRenderProcessHandler,
sys, v8_value_create_function, v8_value_create_object,
Browser, CefString, DictionaryValue, Frame, ImplBrowser, ImplCommandLine, ImplDictionaryValue,
ImplFrame, ImplListValue, ImplProcessMessage, ImplRenderProcessHandler, ImplV8Context,
ImplV8Value, ProcessId, ProcessMessage, V8Context, V8Handler, V8Value,
WrapRenderProcessHandler, command_line_get_global, register_extension, sys,
v8_value_create_object,
};
use std::collections::HashMap as StdHashMap;
use std::os::raw::c_int;
use std::sync::Mutex;
const CEF_API_EXTENSION_NAME: &str = "v8/bevy-cef-api";
const CEF_API_EXTENSION_CODE: &str = r#"
var cef;
if (!cef) cef = {};
(function() {
native function __cef_brp();
native function __cef_emit();
native function __cef_listen();
cef.brp = __cef_brp;
cef.emit = __cef_emit;
cef.listen = __cef_listen;
})();
"#;
pub(crate) static BRP_PROMISES: Mutex<HashMap<String, V8Value>> = Mutex::new(HashMap::new());
pub(crate) static LISTEN_EVENTS: Mutex<HashMap<String, V8Value>> = Mutex::new(HashMap::new());
@@ -65,6 +80,11 @@ impl Clone for RenderProcessHandlerBuilder {
}
impl ImplRenderProcessHandler for RenderProcessHandlerBuilder {
fn on_web_kit_initialized(&self) {
register_cef_api_extension();
register_extensions_from_command_line();
}
fn on_browser_created(
&self,
browser: Option<&mut Browser>,
@@ -91,7 +111,6 @@ impl ImplRenderProcessHandler for RenderProcessHandlerBuilder {
&& let Some(browser) = browser
{
inject_initialize_scripts(browser, context, frame);
inject_cef_api(context, frame);
}
}
@@ -137,46 +156,12 @@ fn inject_initialize_scripts(browser: &mut Browser, context: &mut V8Context, fra
}
}
fn inject_cef_api(context: &mut V8Context, frame: &mut Frame) {
if let Some(g) = context.global()
&& let Some(mut cef) = v8_value_create_object(
Some(&mut V8DefaultAccessorBuilder::build()),
Some(&mut V8DefaultInterceptorBuilder::build()),
)
&& let Some(mut brp) = v8_value_create_function(
Some(&"brp".into()),
Some(&mut BrpBuilder::build(frame.clone())),
)
&& let Some(mut emit) = v8_value_create_function(
Some(&"emit".into()),
Some(&mut EmitBuilder::build(frame.clone())),
)
&& let Some(mut listen) = v8_value_create_function(
Some(&"listen".into()),
Some(&mut ListenBuilder::build(frame.clone())),
)
{
cef.set_value_bykey(
Some(&"brp".into()),
Some(&mut brp),
V8Propertyattribute::default(),
);
cef.set_value_bykey(
Some(&"emit".into()),
Some(&mut emit),
V8Propertyattribute::default(),
);
cef.set_value_bykey(
Some(&"listen".into()),
Some(&mut listen),
V8Propertyattribute::default(),
);
g.set_value_bykey(
Some(&"cef".into()),
Some(&mut cef),
V8Propertyattribute::default(),
);
}
fn register_cef_api_extension() {
register_extension(
Some(&CEF_API_EXTENSION_NAME.into()),
Some(&CEF_API_EXTENSION_CODE.into()),
Some(&mut V8Handler::new(CefApiHandler::default())),
);
}
fn handle_brp_message(message: &ProcessMessage, ctx: V8Context) {
@@ -229,3 +214,32 @@ fn handle_listen_message(message: &ProcessMessage, mut ctx: V8Context) {
}
ctx.exit();
}
fn register_extensions_from_command_line() {
let Some(cmd_line) = command_line_get_global() else {
return;
};
if cmd_line.has_switch(Some(&EXTENSIONS_SWITCH.into())) == 0 {
return;
}
let json = cmd_line
.switch_value(Some(&EXTENSIONS_SWITCH.into()))
.into_string();
if json.is_empty() {
return;
}
let Ok(extensions) = serde_json::from_str::<StdHashMap<String, String>>(&json) else {
eprintln!("bevy_cef: failed to parse extensions JSON: {}", json);
return;
};
for (name, code) in extensions {
let full_name = format!("v8/{}", name);
register_extension(
Some(&full_name.as_str().into()),
Some(&code.as_str().into()),
None,
);
}
}

View File

@@ -18,6 +18,8 @@ use cef_dll_sys::cef_scheme_options_t::{
use std::env::home_dir;
use std::path::PathBuf;
pub const EXTENSIONS_SWITCH: &str = "bevy-cef-extensions";
pub const SCHEME_CEF: &str = "cef";
pub const HOST_CEF: &str = "localhost";

71
examples/extensions.rs Normal file
View File

@@ -0,0 +1,71 @@
//! Example demonstrating custom JavaScript extensions via CEF's register_extension.
//!
//! This example shows how to create global JavaScript APIs that are available
//! in all webviews before any page scripts run.
use bevy::prelude::*;
use bevy_cef::prelude::*;
use serde::Deserialize;
fn main() {
App::new()
.add_plugins((
DefaultPlugins,
CefPlugin {
extensions: CefExtensions::new().add(
"myGame",
r#"
var myGame = {
version: "1.0.0",
sendScore: function(score) {
window.cef.emit('score_update', { score: score });
}
};
"#,
),
..Default::default()
},
JsEmitEventPlugin::<ScoreUpdate>::default(),
))
.add_systems(
Startup,
(spawn_camera, spawn_directional_light, spawn_webview),
)
.add_observer(on_score_update)
.run();
}
#[derive(Deserialize, Debug)]
struct ScoreUpdate {
score: u32,
}
fn on_score_update(trigger: On<Receive<ScoreUpdate>>) {
info!("Received score update: {:?}", trigger.score);
}
fn spawn_camera(mut commands: Commands) {
commands.spawn((
Camera3d::default(),
Transform::from_translation(Vec3::new(0., 0., 3.)).looking_at(Vec3::ZERO, Vec3::Y),
));
}
fn spawn_directional_light(mut commands: Commands) {
commands.spawn((
DirectionalLight::default(),
Transform::from_translation(Vec3::new(1., 1., 1.)).looking_at(Vec3::ZERO, Vec3::Y),
));
}
fn spawn_webview(
mut commands: Commands,
mut meshes: ResMut<Assets<Mesh>>,
mut materials: ResMut<Assets<WebviewExtendStandardMaterial>>,
) {
commands.spawn((
CefWebviewUri::local("extensions.html"),
Mesh3d(meshes.add(Plane3d::new(Vec3::Z, Vec2::ONE))),
MeshMaterial3d(materials.add(WebviewExtendStandardMaterial::default())),
));
}

View File

@@ -10,6 +10,7 @@ use cef::{Settings, api_hash, execute_process, initialize, shutdown, sys};
/// - macOS: Calls [`CefDoMessageLoopWork`](https://cef-builds.spotifycdn.com/docs/106.1/cef__app_8h.html#a830ae43dcdffcf4e719540204cefdb61) every frame.
pub struct MessageLoopPlugin {
pub config: CommandLineConfig,
pub extensions: CefExtensions,
}
impl Plugin for MessageLoopPlugin {
@@ -21,7 +22,8 @@ impl Plugin for MessageLoopPlugin {
let args = Args::new();
let (tx, rx) = std::sync::mpsc::channel();
let mut cef_app = BrowserProcessAppBuilder::build(tx, self.config.clone());
let mut cef_app =
BrowserProcessAppBuilder::build(tx, self.config.clone(), self.extensions.clone());
let ret = execute_process(
Some(args.as_main_args()),
Some(&mut cef_app),

View File

@@ -16,12 +16,12 @@ use crate::mute::AudioMutePlugin;
use crate::prelude::{IpcPlugin, NavigationPlugin, WebviewPlugin};
use crate::zoom::ZoomPlugin;
use bevy::prelude::*;
use bevy_cef_core::prelude::CommandLineConfig;
use bevy_cef_core::prelude::{CefExtensions, CommandLineConfig};
use bevy_remote::RemotePlugin;
pub mod prelude {
pub use crate::{CefPlugin, RunOnMainThread, common::*, navigation::*, webview::prelude::*};
pub use bevy_cef_core::prelude::CommandLineConfig;
pub use bevy_cef_core::prelude::{CefExtensions, CommandLineConfig};
}
pub struct RunOnMainThread;
@@ -29,6 +29,7 @@ pub struct RunOnMainThread;
#[derive(Debug, Default)]
pub struct CefPlugin {
pub command_line_config: CommandLineConfig,
pub extensions: CefExtensions,
}
impl Plugin for CefPlugin {
@@ -37,6 +38,7 @@ impl Plugin for CefPlugin {
LocalHostPlugin,
MessageLoopPlugin {
config: self.command_line_config.clone(),
extensions: self.extensions.clone(),
},
WebviewCoreComponentsPlugin,
WebviewPlugin,