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:
1
.gitignore
vendored
1
.gitignore
vendored
@@ -4,3 +4,4 @@ book/
|
|||||||
.DS_Store**/.DS_Store
|
.DS_Store**/.DS_Store
|
||||||
.claude/memo.md
|
.claude/memo.md
|
||||||
docs/plans/
|
docs/plans/
|
||||||
|
.worktrees/
|
||||||
@@ -24,6 +24,11 @@
|
|||||||
### Features
|
### Features
|
||||||
|
|
||||||
- Added `PreloadScripts` component for specifying JavaScript to be executed when the page is initialized.
|
- 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
|
### Bug Fixes
|
||||||
|
|
||||||
|
|||||||
96
CLAUDE.md
96
CLAUDE.md
@@ -14,89 +14,83 @@ This is `bevy_cef`, a Bevy plugin that integrates the Chromium Embedded Framewor
|
|||||||
- Communication through IPC channels and Bevy Remote Protocol (BRP)
|
- Communication through IPC channels and Bevy Remote Protocol (BRP)
|
||||||
|
|
||||||
### Core Components
|
### Core Components
|
||||||
- `CefWebviewUri`: Component specifying webview URL (remote or local via `cef://localhost/`)
|
- `CefWebviewUri`: URL specification (`CefWebviewUri::new("url")` or `CefWebviewUri::local("file.html")`)
|
||||||
- `WebviewSize`: Controls webview rendering dimensions (default 800x800)
|
- `WebviewSize`: Rendering dimensions (default 800x800), controls texture resolution not 3D size
|
||||||
- `WebviewExtendStandardMaterial`: Material for rendering webviews on 3D meshes
|
- `WebviewExtendStandardMaterial`: Primary material for 3D mesh rendering
|
||||||
- `HostWindow`: Optional parent window specification
|
- `WebviewSpriteMaterial`: Material for 2D sprite rendering
|
||||||
- `ZoomLevel`: Webview zoom control
|
- `HostWindow`: Optional parent window (defaults to PrimaryWindow)
|
||||||
- `AudioMuted`: Audio muting control
|
- `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
|
### Plugin Architecture
|
||||||
The main `CefPlugin` orchestrates several sub-plugins:
|
The main `CefPlugin` accepts `CommandLineConfig` for CEF command-line switches and `CefExtensions` for custom JavaScript APIs. Sub-plugins:
|
||||||
- `LocalHostPlugin`: Serves local assets via custom scheme
|
- `LocalHostPlugin`: Serves local assets via `cef://localhost/` scheme
|
||||||
- `MessageLoopPlugin`: CEF message loop integration
|
- `MessageLoopPlugin`: CEF message loop integration (macOS uses `CefDoMessageLoopWork()`)
|
||||||
- `WebviewCoreComponentsPlugin`: Core component registration
|
- `WebviewCoreComponentsPlugin`: Core component registration
|
||||||
- `WebviewPlugin`: Main webview management
|
- `WebviewPlugin`: Webview lifecycle and DevTools
|
||||||
- `IpcPlugin`: Inter-process communication
|
- `IpcPlugin`: IPC containing `IpcRawEventPlugin` and `HostEmitPlugin`
|
||||||
- `KeyboardPlugin`, `NavigationPlugin`, `ZoomPlugin`, `AudioMutePlugin`: Feature-specific functionality
|
- `KeyboardPlugin`, `SystemCursorIconPlugin`, `NavigationPlugin`, `ZoomPlugin`, `AudioMutePlugin`
|
||||||
|
- `RemotePlugin`: Auto-added for BRP support if not present
|
||||||
|
|
||||||
### IPC System
|
### IPC System
|
||||||
Three communication patterns:
|
Three communication patterns:
|
||||||
1. **JS Emit**: Webview → Bevy app via `JsEmitEventPlugin<T>`
|
1. **JS Emit**: Webview → Bevy via `JsEmitEventPlugin<E>` where E: `DeserializeOwned + Send + Sync + 'static`
|
||||||
2. **Host Emit**: Bevy app → Webview via event emission
|
- Events wrapped in `Receive<E>` EntityEvent
|
||||||
3. **BRP (Bevy Remote Protocol)**: Bidirectional RPC calls
|
- 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
|
## Development Commands
|
||||||
|
|
||||||
### Code Quality
|
|
||||||
```bash
|
```bash
|
||||||
# Fix and format code
|
# Fix and format code
|
||||||
make fix
|
make fix
|
||||||
# Which runs:
|
|
||||||
# cargo clippy --fix --allow-dirty --allow-staged --workspace --all --all-features
|
|
||||||
# cargo fmt --all
|
|
||||||
```
|
|
||||||
|
|
||||||
### Development Setup
|
# Run examples (macOS requires debug feature)
|
||||||
The build system automatically handles CEF dependencies on macOS with debug feature:
|
cargo run --example simple --features debug
|
||||||
- Installs `bevy_cef_debug_render_process` tool
|
|
||||||
- Installs `export-cef-dir` tool
|
|
||||||
- Downloads/extracts CEF framework to `$HOME/.local/share/cef`
|
|
||||||
|
|
||||||
### Manual Installation
|
|
||||||
```bash
|
|
||||||
# Install debug render process tool
|
# Install debug render process tool
|
||||||
make install
|
make install
|
||||||
# Or: cargo install --path ./crates/bevy_cef_debug_render_process --force
|
|
||||||
```
|
```
|
||||||
|
|
||||||
## Key Examples
|
### Debug Tools Setup (macOS)
|
||||||
|
Manual installation required before running with `debug` feature:
|
||||||
- `examples/simple.rs`: Basic webview on 3D plane
|
```bash
|
||||||
- `examples/js_emit.rs`: JavaScript to Bevy communication
|
cargo install export-cef-dir
|
||||||
- `examples/host_emit.rs`: Bevy to JavaScript communication
|
export-cef-dir --force $HOME/.local/share
|
||||||
- `examples/brp.rs`: Bidirectional RPC with devtools
|
cargo install bevy_cef_debug_render_process
|
||||||
- `examples/navigation.rs`: Page navigation controls
|
mv $HOME/.cargo/bin/bevy_cef_debug_render_process "$HOME/.local/share/Chromium Embedded Framework.framework/Libraries/bevy_cef_debug_render_process"
|
||||||
- `examples/zoom_level.rs`: Zoom functionality
|
```
|
||||||
- `examples/sprite.rs`: Webview as 2D sprite
|
|
||||||
- `examples/devtool.rs`: Chrome DevTools integration
|
|
||||||
|
|
||||||
## Local Asset Loading
|
## 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
|
- Place assets in `assets/` directory
|
||||||
- Reference as `CefWebviewUri::local("filename.html")`
|
- Reference as `CefWebviewUri::local("filename.html")`
|
||||||
- Or manually: `cef://localhost/filename.html`
|
|
||||||
|
|
||||||
## Testing
|
## Testing
|
||||||
|
|
||||||
No automated tests are present in this codebase. Testing is done through the example applications.
|
No automated tests. Testing done through examples:
|
||||||
|
- `cargo test --workspace --all-features` (for any future tests)
|
||||||
### Manually Testing
|
- Examples: simple, js_emit, host_emit, brp, navigation, zoom_level, sprite, devtool, custom_material, preload_scripts, extensions
|
||||||
|
|
||||||
- Run tests with `cargo test --workspace --all-features`
|
|
||||||
|
|
||||||
## Platform Notes
|
## Platform Notes
|
||||||
|
|
||||||
- Currently focused on macOS development (see Cargo.toml target-specific dependencies)
|
- Primary platform: macOS (uses `objc` crate for window handling)
|
||||||
- CEF framework must be available at `$HOME/.local/share/cef`
|
- CEF framework location: `$HOME/.local/share/Chromium Embedded Framework.framework`
|
||||||
- Uses `objc` crate for macOS-specific window handling
|
- Windows/Linux: Infrastructure ready but needs testing
|
||||||
- DYLD environment variables required for CEF library loading
|
- Key resources (`Browsers`, library loaders) are `NonSend` - CEF is not thread-safe
|
||||||
|
|
||||||
## Workspace Structure
|
## Workspace Structure
|
||||||
|
|
||||||
This is a Cargo workspace with:
|
|
||||||
- Root crate: `bevy_cef` (public API)
|
- Root crate: `bevy_cef` (public API)
|
||||||
- `crates/bevy_cef_core`: Core CEF integration logic
|
- `crates/bevy_cef_core`: Core CEF integration logic
|
||||||
- `crates/bevy_cef_debug_render_process`: Debug render process executable
|
- `crates/bevy_cef_debug_render_process`: Debug render process executable
|
||||||
- `examples/demo`: Standalone demo application
|
|
||||||
39
assets/extensions.html
Normal file
39
assets/extensions.html
Normal 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>
|
||||||
@@ -15,7 +15,7 @@
|
|||||||
<h1 id="count" style="color: aqua">0</h1>
|
<h1 id="count" style="color: aqua">0</h1>
|
||||||
<script>
|
<script>
|
||||||
const count = document.getElementById("count");
|
const count = document.getElementById("count");
|
||||||
window.cef.listen("count", (payload) => {
|
cef.listen("count", (payload) => {
|
||||||
count.innerText = payload;
|
count.innerText = payload;
|
||||||
})
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -17,7 +17,7 @@
|
|||||||
let count = 0;
|
let count = 0;
|
||||||
window.setInterval(() => {
|
window.setInterval(() => {
|
||||||
console.log("Emitting count:", count);
|
console.log("Emitting count:", count);
|
||||||
window.cef.emit({
|
cef.emit({
|
||||||
count,
|
count,
|
||||||
});
|
});
|
||||||
countElement.innerText = count;
|
countElement.innerText = count;
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ mod client_handler;
|
|||||||
mod command_line_config;
|
mod command_line_config;
|
||||||
mod context_menu_handler;
|
mod context_menu_handler;
|
||||||
mod display_handler;
|
mod display_handler;
|
||||||
|
mod extensions;
|
||||||
mod localhost;
|
mod localhost;
|
||||||
mod message_pump;
|
mod message_pump;
|
||||||
mod renderer_handler;
|
mod renderer_handler;
|
||||||
@@ -16,6 +17,7 @@ pub use browsers::*;
|
|||||||
pub use client_handler::*;
|
pub use client_handler::*;
|
||||||
pub use command_line_config::*;
|
pub use command_line_config::*;
|
||||||
pub use context_menu_handler::*;
|
pub use context_menu_handler::*;
|
||||||
|
pub use extensions::*;
|
||||||
pub use localhost::*;
|
pub use localhost::*;
|
||||||
pub use message_pump::*;
|
pub use message_pump::*;
|
||||||
pub use renderer_handler::*;
|
pub use renderer_handler::*;
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
use crate::browser_process::CefExtensions;
|
||||||
use crate::browser_process::CommandLineConfig;
|
use crate::browser_process::CommandLineConfig;
|
||||||
use crate::browser_process::MessageLoopTimer;
|
use crate::browser_process::MessageLoopTimer;
|
||||||
use crate::browser_process::browser_process_handler::BrowserProcessHandlerBuilder;
|
use crate::browser_process::browser_process_handler::BrowserProcessHandlerBuilder;
|
||||||
@@ -17,17 +18,20 @@ pub struct BrowserProcessAppBuilder {
|
|||||||
object: *mut RcImpl<_cef_app_t, Self>,
|
object: *mut RcImpl<_cef_app_t, Self>,
|
||||||
message_loop_working_requester: Sender<MessageLoopTimer>,
|
message_loop_working_requester: Sender<MessageLoopTimer>,
|
||||||
config: CommandLineConfig,
|
config: CommandLineConfig,
|
||||||
|
extensions: CefExtensions,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl BrowserProcessAppBuilder {
|
impl BrowserProcessAppBuilder {
|
||||||
pub fn build(
|
pub fn build(
|
||||||
message_loop_working_requester: Sender<MessageLoopTimer>,
|
message_loop_working_requester: Sender<MessageLoopTimer>,
|
||||||
config: CommandLineConfig,
|
config: CommandLineConfig,
|
||||||
|
extensions: CefExtensions,
|
||||||
) -> cef::App {
|
) -> cef::App {
|
||||||
cef::App::new(Self {
|
cef::App::new(Self {
|
||||||
object: core::ptr::null_mut(),
|
object: core::ptr::null_mut(),
|
||||||
message_loop_working_requester,
|
message_loop_working_requester,
|
||||||
config,
|
config,
|
||||||
|
extensions,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -43,6 +47,7 @@ impl Clone for BrowserProcessAppBuilder {
|
|||||||
object,
|
object,
|
||||||
message_loop_working_requester: self.message_loop_working_requester.clone(),
|
message_loop_working_requester: self.message_loop_working_requester.clone(),
|
||||||
config: self.config.clone(),
|
config: self.config.clone(),
|
||||||
|
extensions: self.extensions.clone(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -65,7 +70,6 @@ impl ImplApp for BrowserProcessAppBuilder {
|
|||||||
let Some(command_line) = command_line else {
|
let Some(command_line) = command_line else {
|
||||||
return;
|
return;
|
||||||
};
|
};
|
||||||
|
|
||||||
for switch in &self.config.switches {
|
for switch in &self.config.switches {
|
||||||
command_line.append_switch(Some(&(*switch).into()));
|
command_line.append_switch(Some(&(*switch).into()));
|
||||||
}
|
}
|
||||||
@@ -84,6 +88,7 @@ impl ImplApp for BrowserProcessAppBuilder {
|
|||||||
fn browser_process_handler(&self) -> Option<BrowserProcessHandler> {
|
fn browser_process_handler(&self) -> Option<BrowserProcessHandler> {
|
||||||
Some(BrowserProcessHandlerBuilder::build(
|
Some(BrowserProcessHandlerBuilder::build(
|
||||||
self.message_loop_working_requester.clone(),
|
self.message_loop_working_requester.clone(),
|
||||||
|
self.extensions.clone(),
|
||||||
))
|
))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
use crate::prelude::MessageLoopTimer;
|
use crate::prelude::{CefExtensions, EXTENSIONS_SWITCH, MessageLoopTimer};
|
||||||
use cef::rc::{Rc, RcImpl};
|
use cef::rc::{Rc, RcImpl};
|
||||||
use cef::*;
|
use cef::*;
|
||||||
use std::sync::mpsc::Sender;
|
use std::sync::mpsc::Sender;
|
||||||
@@ -9,15 +9,18 @@ use std::sync::mpsc::Sender;
|
|||||||
pub struct BrowserProcessHandlerBuilder {
|
pub struct BrowserProcessHandlerBuilder {
|
||||||
object: *mut RcImpl<cef_dll_sys::cef_browser_process_handler_t, Self>,
|
object: *mut RcImpl<cef_dll_sys::cef_browser_process_handler_t, Self>,
|
||||||
message_loop_working_requester: Sender<MessageLoopTimer>,
|
message_loop_working_requester: Sender<MessageLoopTimer>,
|
||||||
|
extensions: CefExtensions,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl BrowserProcessHandlerBuilder {
|
impl BrowserProcessHandlerBuilder {
|
||||||
pub fn build(
|
pub fn build(
|
||||||
message_loop_working_requester: Sender<MessageLoopTimer>,
|
message_loop_working_requester: Sender<MessageLoopTimer>,
|
||||||
|
extensions: CefExtensions,
|
||||||
) -> BrowserProcessHandler {
|
) -> BrowserProcessHandler {
|
||||||
BrowserProcessHandler::new(Self {
|
BrowserProcessHandler::new(Self {
|
||||||
object: core::ptr::null_mut(),
|
object: core::ptr::null_mut(),
|
||||||
message_loop_working_requester,
|
message_loop_working_requester,
|
||||||
|
extensions,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -48,6 +51,7 @@ impl Clone for BrowserProcessHandlerBuilder {
|
|||||||
Self {
|
Self {
|
||||||
object,
|
object,
|
||||||
message_loop_working_requester: self.message_loop_working_requester.clone(),
|
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-certificate-errors".into()));
|
||||||
command_line.append_switch(Some(&"ignore-ssl-errors".into()));
|
command_line.append_switch(Some(&"ignore-ssl-errors".into()));
|
||||||
command_line.append_switch(Some(&"enable-logging=stderr".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) {
|
fn on_schedule_message_pump_work(&self, delay_ms: i64) {
|
||||||
|
|||||||
47
crates/bevy_cef_core/src/browser_process/extensions.rs
Normal file
47
crates/bevy_cef_core/src/browser_process/extensions.rs
Normal 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()
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -10,8 +10,6 @@ pub mod prelude {
|
|||||||
#[cfg(target_os = "macos")]
|
#[cfg(target_os = "macos")]
|
||||||
pub use crate::debug::*;
|
pub use crate::debug::*;
|
||||||
pub use crate::render_process::app::*;
|
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::execute_render_process;
|
||||||
pub use crate::render_process::render_process_handler::*;
|
pub use crate::render_process::render_process_handler::*;
|
||||||
pub use crate::util::*;
|
pub use crate::util::*;
|
||||||
|
|||||||
@@ -3,9 +3,7 @@ use cef::args::Args;
|
|||||||
use cef::{api_hash, execute_process, sys};
|
use cef::{api_hash, execute_process, sys};
|
||||||
|
|
||||||
pub mod app;
|
pub mod app;
|
||||||
pub mod brp;
|
pub mod cef_api_handler;
|
||||||
pub mod emit;
|
|
||||||
pub mod listen;
|
|
||||||
pub mod render_process_handler;
|
pub mod render_process_handler;
|
||||||
|
|
||||||
/// Execute the CEF render process.
|
/// Execute the CEF render process.
|
||||||
|
|||||||
@@ -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
|
|
||||||
}
|
|
||||||
166
crates/bevy_cef_core/src/render_process/cef_api_handler.rs
Normal file
166
crates/bevy_cef_core/src/render_process/cef_api_handler.rs
Normal 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
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,6 +1,5 @@
|
|||||||
use crate::prelude::{EmitBuilder, IntoString};
|
use crate::prelude::{EXTENSIONS_SWITCH, IntoString};
|
||||||
use crate::render_process::brp::BrpBuilder;
|
use crate::render_process::cef_api_handler::CefApiHandler;
|
||||||
use crate::render_process::listen::ListenBuilder;
|
|
||||||
use crate::util::json_to_v8;
|
use crate::util::json_to_v8;
|
||||||
use crate::util::v8_accessor::V8DefaultAccessorBuilder;
|
use crate::util::v8_accessor::V8DefaultAccessorBuilder;
|
||||||
use crate::util::v8_interceptor::V8DefaultInterceptorBuilder;
|
use crate::util::v8_interceptor::V8DefaultInterceptorBuilder;
|
||||||
@@ -8,14 +7,30 @@ use bevy::platform::collections::HashMap;
|
|||||||
use bevy_remote::BrpResult;
|
use bevy_remote::BrpResult;
|
||||||
use cef::rc::{Rc, RcImpl};
|
use cef::rc::{Rc, RcImpl};
|
||||||
use cef::{
|
use cef::{
|
||||||
Browser, CefString, DictionaryValue, Frame, ImplBrowser, ImplDictionaryValue, ImplFrame,
|
Browser, CefString, DictionaryValue, Frame, ImplBrowser, ImplCommandLine, ImplDictionaryValue,
|
||||||
ImplListValue, ImplProcessMessage, ImplRenderProcessHandler, ImplV8Context, ImplV8Value,
|
ImplFrame, ImplListValue, ImplProcessMessage, ImplRenderProcessHandler, ImplV8Context,
|
||||||
ProcessId, ProcessMessage, V8Context, V8Propertyattribute, V8Value, WrapRenderProcessHandler,
|
ImplV8Value, ProcessId, ProcessMessage, V8Context, V8Handler, V8Value,
|
||||||
sys, v8_value_create_function, v8_value_create_object,
|
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::os::raw::c_int;
|
||||||
use std::sync::Mutex;
|
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 BRP_PROMISES: Mutex<HashMap<String, V8Value>> = Mutex::new(HashMap::new());
|
||||||
pub(crate) static LISTEN_EVENTS: 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 {
|
impl ImplRenderProcessHandler for RenderProcessHandlerBuilder {
|
||||||
|
fn on_web_kit_initialized(&self) {
|
||||||
|
register_cef_api_extension();
|
||||||
|
register_extensions_from_command_line();
|
||||||
|
}
|
||||||
|
|
||||||
fn on_browser_created(
|
fn on_browser_created(
|
||||||
&self,
|
&self,
|
||||||
browser: Option<&mut Browser>,
|
browser: Option<&mut Browser>,
|
||||||
@@ -91,7 +111,6 @@ impl ImplRenderProcessHandler for RenderProcessHandlerBuilder {
|
|||||||
&& let Some(browser) = browser
|
&& let Some(browser) = browser
|
||||||
{
|
{
|
||||||
inject_initialize_scripts(browser, context, frame);
|
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) {
|
fn register_cef_api_extension() {
|
||||||
if let Some(g) = context.global()
|
register_extension(
|
||||||
&& let Some(mut cef) = v8_value_create_object(
|
Some(&CEF_API_EXTENSION_NAME.into()),
|
||||||
Some(&mut V8DefaultAccessorBuilder::build()),
|
Some(&CEF_API_EXTENSION_CODE.into()),
|
||||||
Some(&mut V8DefaultInterceptorBuilder::build()),
|
Some(&mut V8Handler::new(CefApiHandler::default())),
|
||||||
)
|
|
||||||
&& 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 handle_brp_message(message: &ProcessMessage, ctx: V8Context) {
|
fn handle_brp_message(message: &ProcessMessage, ctx: V8Context) {
|
||||||
@@ -229,3 +214,32 @@ fn handle_listen_message(message: &ProcessMessage, mut ctx: V8Context) {
|
|||||||
}
|
}
|
||||||
ctx.exit();
|
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,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -18,6 +18,8 @@ use cef_dll_sys::cef_scheme_options_t::{
|
|||||||
use std::env::home_dir;
|
use std::env::home_dir;
|
||||||
use std::path::PathBuf;
|
use std::path::PathBuf;
|
||||||
|
|
||||||
|
pub const EXTENSIONS_SWITCH: &str = "bevy-cef-extensions";
|
||||||
|
|
||||||
pub const SCHEME_CEF: &str = "cef";
|
pub const SCHEME_CEF: &str = "cef";
|
||||||
|
|
||||||
pub const HOST_CEF: &str = "localhost";
|
pub const HOST_CEF: &str = "localhost";
|
||||||
|
|||||||
71
examples/extensions.rs
Normal file
71
examples/extensions.rs
Normal 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())),
|
||||||
|
));
|
||||||
|
}
|
||||||
@@ -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.
|
/// - macOS: Calls [`CefDoMessageLoopWork`](https://cef-builds.spotifycdn.com/docs/106.1/cef__app_8h.html#a830ae43dcdffcf4e719540204cefdb61) every frame.
|
||||||
pub struct MessageLoopPlugin {
|
pub struct MessageLoopPlugin {
|
||||||
pub config: CommandLineConfig,
|
pub config: CommandLineConfig,
|
||||||
|
pub extensions: CefExtensions,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Plugin for MessageLoopPlugin {
|
impl Plugin for MessageLoopPlugin {
|
||||||
@@ -21,7 +22,8 @@ impl Plugin for MessageLoopPlugin {
|
|||||||
let args = Args::new();
|
let args = Args::new();
|
||||||
let (tx, rx) = std::sync::mpsc::channel();
|
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(
|
let ret = execute_process(
|
||||||
Some(args.as_main_args()),
|
Some(args.as_main_args()),
|
||||||
Some(&mut cef_app),
|
Some(&mut cef_app),
|
||||||
|
|||||||
@@ -16,12 +16,12 @@ use crate::mute::AudioMutePlugin;
|
|||||||
use crate::prelude::{IpcPlugin, NavigationPlugin, WebviewPlugin};
|
use crate::prelude::{IpcPlugin, NavigationPlugin, WebviewPlugin};
|
||||||
use crate::zoom::ZoomPlugin;
|
use crate::zoom::ZoomPlugin;
|
||||||
use bevy::prelude::*;
|
use bevy::prelude::*;
|
||||||
use bevy_cef_core::prelude::CommandLineConfig;
|
use bevy_cef_core::prelude::{CefExtensions, CommandLineConfig};
|
||||||
use bevy_remote::RemotePlugin;
|
use bevy_remote::RemotePlugin;
|
||||||
|
|
||||||
pub mod prelude {
|
pub mod prelude {
|
||||||
pub use crate::{CefPlugin, RunOnMainThread, common::*, navigation::*, webview::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;
|
pub struct RunOnMainThread;
|
||||||
@@ -29,6 +29,7 @@ pub struct RunOnMainThread;
|
|||||||
#[derive(Debug, Default)]
|
#[derive(Debug, Default)]
|
||||||
pub struct CefPlugin {
|
pub struct CefPlugin {
|
||||||
pub command_line_config: CommandLineConfig,
|
pub command_line_config: CommandLineConfig,
|
||||||
|
pub extensions: CefExtensions,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Plugin for CefPlugin {
|
impl Plugin for CefPlugin {
|
||||||
@@ -37,6 +38,7 @@ impl Plugin for CefPlugin {
|
|||||||
LocalHostPlugin,
|
LocalHostPlugin,
|
||||||
MessageLoopPlugin {
|
MessageLoopPlugin {
|
||||||
config: self.command_line_config.clone(),
|
config: self.command_line_config.clone(),
|
||||||
|
extensions: self.extensions.clone(),
|
||||||
},
|
},
|
||||||
WebviewCoreComponentsPlugin,
|
WebviewCoreComponentsPlugin,
|
||||||
WebviewPlugin,
|
WebviewPlugin,
|
||||||
|
|||||||
Reference in New Issue
Block a user