diff --git a/.gitignore b/.gitignore index 8bab924..114d01c 100644 --- a/.gitignore +++ b/.gitignore @@ -7,4 +7,5 @@ cargo-sources.json Cargo.lock -/.vscode/ \ No newline at end of file +/.vscode/ +**/.DS_Store diff --git a/Cargo.toml b/Cargo.toml index 0772f22..ec5f09f 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -23,18 +23,20 @@ authors = [ repository = "https://github.com/tauri-apps/cef-rs" [workspace.dependencies] -cef = { path = "cef" } +cef = { path = "cef", default-features = false } cef-dll-sys = { version = "143.4.0", path = "sys" } download-cef = { version = "2.2", path = "download-cef" } anyhow = "1" ash = "0.38" bindgen = "0.72" +cargo_metadata = "0.23.1" clap = { version = "4", features = ["derive"] } cmake = "0.1.52" convert_case = "0.10" git-cliff = "2" git-cliff-core = "2" +glib = "0.21" io-surface = "0.16" libc = "0.2" libloading = "0.9" @@ -42,7 +44,8 @@ metal = "0.32" objc = "0.2" objc2 = "0.6.3" objc2-app-kit = { version = "0.3.2", default-features = false } -objc2-io-surface = "0.3" +objc2-foundation = "0.3.2" +objc2-io-surface = "0.3.2" plist = "1" proc-macro2 = "1" quote = "1" @@ -55,6 +58,8 @@ thiserror = "2" toml_edit = "0.24" tracing = "0.1" wgpu = "27" +winres = "0.1" +x11-dl = "2" [workspace.dependencies.windows-sys] version = "0.61" diff --git a/README.md b/README.md index af99317..465cbea 100644 --- a/README.md +++ b/README.md @@ -52,6 +52,11 @@ $env:PATH="$env:PATH;$env:CEF_PATH" ### Run the `cefsimple` Example +This command should work with each platform: +```sh +cargo run --bin bundle-cef-app -- cefsimple -o target/bundle +``` + #### Linux ```sh @@ -61,15 +66,15 @@ cargo run --bin cefsimple #### macOS ```sh -cargo run --bin bundle_cefsimple -open target/debug/cefsimple.app +cargo run --bin bundle-cef-app -- cefsimple -o target/bundle +open target/bundle/cefsimple.app ``` #### Windows (using PowerShell) ```pwsh -cp ./examples/cefsimple/src/win/cefsimple.exe.manifest ./target/debug/ -cargo run --bin cefsimple +cargo run --bin bundle-cef-app -- cefsimple -o ./target/bundle +./target/bundle/cefsimple.exe ``` ## Contributing diff --git a/cef/Cargo.toml b/cef/Cargo.toml index 78f59eb..e74ae70 100644 --- a/cef/Cargo.toml +++ b/cef/Cargo.toml @@ -8,23 +8,44 @@ license.workspace = true authors.workspace = true repository.workspace = true +[lib] + +[[bin]] +name = "bundle-cef-app" + [features] -default = ["sandbox"] +default = ["sandbox", "build-util"] dox = ["cef-dll-sys/dox"] sandbox = ["cef-dll-sys/sandbox"] + # Unified texture import system for CEF hardware acceleration accelerated_osr = [ + "ash", + "libc", + "objc", + "objc2-io-surface", + "metal", + "thiserror", + "tracing", "wgpu", "windows", - "libc", - "ash", - "tracing", - "thiserror", - "objc2-io-surface", - "objc", - "metal" ] +# Build utilities, including the bundle-cef-app tool. +build-util = [ + "anyhow", + "cargo_metadata", + "clap", + "plist", + "semver", + "serde", + "serde_json", + "thiserror", +] + +# Linux X11 support in bundle-cef-app. +linux-x11 = [] + [package.metadata.docs.rs] features = ["dox"] @@ -37,6 +58,13 @@ thiserror = { workspace = true, optional = true } tracing = { workspace = true, optional = true } wgpu = { workspace = true, optional = true } +# bundle +anyhow = { workspace = true, optional = true } +clap = { workspace = true, optional = true } +cargo_metadata = { workspace = true, optional = true } +serde = { workspace = true, optional = true } +serde_json = { workspace = true, optional = true } + [target.'cfg(target_os = "windows")'.dependencies] windows-sys.workspace = true windows = { workspace = true, optional = true } @@ -47,6 +75,8 @@ objc2.workspace = true objc2-io-surface = { workspace = true, optional = true } objc = { workspace = true, optional = true } metal = { workspace = true, optional = true } +plist = { workspace = true, optional = true } +semver = { workspace = true, optional = true } [target.'cfg(target_os = "linux")'.dependencies] libc = { workspace = true, optional = true } diff --git a/cef/src/args.rs b/cef/src/args.rs index 4f8bf61..15cc48b 100644 --- a/cef/src/args.rs +++ b/cef/src/args.rs @@ -56,9 +56,7 @@ impl Args { #[cfg(any(target_os = "macos", target_os = "linux"))] pub fn as_cmd_line(&self) -> Option { - let Some(cmd_line) = command_line_create() else { - return None; - }; + let cmd_line = command_line_create()?; cmd_line.init_from_argv(self.as_main_args().argc, self.as_main_args().argv.cast()); Some(cmd_line) } @@ -81,3 +79,12 @@ impl Args { cmd_line } } + +impl From for Args { + fn from(main_args: MainArgs) -> Self { + Args { + main_args, + ..Default::default() + } + } +} diff --git a/cef/src/bin/bundle-cef-app/linux.rs b/cef/src/bin/bundle-cef-app/linux.rs new file mode 100644 index 0000000..2ad45e4 --- /dev/null +++ b/cef/src/bin/bundle-cef-app/linux.rs @@ -0,0 +1,26 @@ +use cef::build_util::linux::*; +use clap::Parser; +use std::{env, path::PathBuf}; + +#[derive(Parser, Debug)] +#[command(about, long_about = None)] +struct Args { + name: String, + #[arg(long, default_value_t = false)] + release: bool, + #[arg(short, long)] + output: Option, +} + +pub fn main() -> anyhow::Result<()> { + let args = Args::parse(); + let output = match args.output { + Some(output) => PathBuf::from(output), + None => env::current_dir()?, + }; + + let bundle_path = build_bundle(output.as_path(), &args.name, args.release)?; + let bundle_path = bundle_path.display(); + println!("Run the app from {bundle_path}"); + Ok(()) +} diff --git a/cef/src/bin/bundle-cef-app/mac.rs b/cef/src/bin/bundle-cef-app/mac.rs new file mode 100644 index 0000000..02224a3 --- /dev/null +++ b/cef/src/bin/bundle-cef-app/mac.rs @@ -0,0 +1,46 @@ +use cef::build_util::mac::*; +use clap::Parser; +use semver::Version; +use std::{env, path::PathBuf}; + +#[derive(Parser, Debug)] +#[command(about, long_about = None)] +struct Args { + name: String, + #[arg(short, long)] + output: Option, + #[arg(short, long)] + identifier: Option, + #[arg(short, long)] + display_name: Option, + #[arg(short, long, default_value = "English")] + region: String, + #[arg(short, long, default_value = "1.0.0")] + version: String, +} + +pub fn main() -> anyhow::Result<()> { + let args = Args::parse(); + let output = match args.output { + Some(output) => PathBuf::from(output), + None => env::current_dir()?, + }; + let identifier = args + .identifier + .unwrap_or_else(|| format!("apps.tauri.cef-rs.{}", args.name)); + let display_name = args.display_name.unwrap_or_else(|| args.name.clone()); + let version = Version::parse(&args.version)?; + + let bundle_info = BundleInfo { + name: args.name.clone(), + identifier, + display_name, + development_region: args.region, + version, + }; + + let bundle_path = build_bundle(output.as_path(), &args.name, bundle_info)?; + let bundle_path = bundle_path.display(); + println!("Run the app from {bundle_path}"); + Ok(()) +} diff --git a/cef/src/bin/bundle-cef-app/main.rs b/cef/src/bin/bundle-cef-app/main.rs new file mode 100644 index 0000000..8cc9411 --- /dev/null +++ b/cef/src/bin/bundle-cef-app/main.rs @@ -0,0 +1,29 @@ +#[cfg(not(feature = "build-util"))] +fn main() { + let command = std::env::current_exe().expect("Failed to get current executable path"); + eprintln!("Disabled: {command:?} was compiled without the build-util feature."); +} + +#[cfg(all(feature = "build-util", target_os = "macos"))] +mod mac; + +#[cfg(all(feature = "build-util", target_os = "macos"))] +fn main() -> anyhow::Result<()> { + mac::main() +} + +#[cfg(all(feature = "build-util", target_os = "linux"))] +mod linux; + +#[cfg(all(feature = "build-util", target_os = "linux"))] +fn main() -> anyhow::Result<()> { + linux::main() +} + +#[cfg(all(feature = "build-util", target_os = "windows"))] +mod win; + +#[cfg(all(feature = "build-util", target_os = "windows"))] +fn main() -> anyhow::Result<()> { + win::main() +} diff --git a/cef/src/bin/bundle-cef-app/win.rs b/cef/src/bin/bundle-cef-app/win.rs new file mode 100644 index 0000000..623aa9b --- /dev/null +++ b/cef/src/bin/bundle-cef-app/win.rs @@ -0,0 +1,26 @@ +use cef::build_util::win::*; +use clap::Parser; +use std::{env, path::PathBuf}; + +#[derive(Parser, Debug)] +#[command(about, long_about = None)] +struct Args { + name: String, + #[arg(long, default_value_t = false)] + release: bool, + #[arg(short, long)] + output: Option, +} + +pub fn main() -> anyhow::Result<()> { + let args = Args::parse(); + let output = match args.output { + Some(output) => PathBuf::from(output), + None => env::current_dir()?, + }; + + let bundle_path = build_bundle(output.as_path(), &args.name, args.release)?; + let bundle_path = bundle_path.display(); + println!("Run the app from {bundle_path}"); + Ok(()) +} diff --git a/cef/src/build_util/linux.rs b/cef/src/build_util/linux.rs new file mode 100644 index 0000000..c4507cf --- /dev/null +++ b/cef/src/build_util/linux.rs @@ -0,0 +1,77 @@ +use std::{ + fs, io, + path::{Path, PathBuf}, + process::Command, +}; + +#[derive(Debug, thiserror::Error)] +pub enum Error { + #[error("I/O error: {0:?}")] + Io(#[from] io::Error), + #[error("Metadata error: {0:?}")] + Metadata(#[from] super::metadata::Error), +} + +pub type Result = std::result::Result; + +/// See https://bitbucket.org/chromiumembedded/cef/wiki/GeneralUsage.md#markdown-header-linux +pub fn bundle(app_path: &Path, target_path: &Path, executable_name: &str) -> Result { + let cef_path = cef_dll_sys::get_cef_dir().unwrap(); + copy_directory(&cef_path, &app_path)?; + + const LOCALES_DIR: &str = "locales"; + copy_directory(&cef_path.join(LOCALES_DIR), &app_path.join(LOCALES_DIR))?; + + copy_app(app_path, target_path, executable_name) +} + +/// Similar to [`bundle`], but this will invoke `cargo build` to build the executable target. +pub fn build_bundle(app_path: &Path, executable_name: &str, release: bool) -> Result { + let cargo_metadata = super::metadata::get_cargo_metadata()?; + let target_path = + cargo_metadata + .target_directory() + .join(if release { "release" } else { "debug" }); + + cargo_build(executable_name, release)?; + + bundle(app_path, &target_path, executable_name) +} + +fn copy_app(app_path: &Path, target_path: &Path, executable_name: &str) -> Result { + let executable_path = app_path.join(executable_name); + let target_executable = target_path.join(executable_name); + fs::copy(&target_executable, &executable_path)?; + Ok(executable_path) +} + +fn copy_directory(src: &Path, dst: &Path) -> io::Result<()> { + fs::create_dir_all(dst)?; + for entry in fs::read_dir(src)? { + let entry = entry?; + let dst_path = dst.join(entry.file_name()); + if entry.file_type()?.is_file() { + fs::copy(entry.path(), &dst_path)?; + } + } + Ok(()) +} + +fn cargo_build(name: &str, release: bool) -> Result<()> { + println!("Building {name}..."); + + let mut args = vec!["build"]; + if release { + args.push("--release"); + } + #[cfg(feature = "linux-x11")] + args.extend(["-F", "linux-x11"]); + args.extend(["--bin", name]); + + let status = Command::new(super::cargo_path()).args(args).status()?; + if status.success() { + Ok(()) + } else { + Err(io::Error::from(io::ErrorKind::Interrupted).into()) + } +} diff --git a/cef/src/build_util/mac.rs b/cef/src/build_util/mac.rs new file mode 100644 index 0000000..2240779 --- /dev/null +++ b/cef/src/build_util/mac.rs @@ -0,0 +1,342 @@ +use semver::Version; +use serde::Serialize; +use std::{ + collections::HashMap, + fs, io, + path::{Path, PathBuf}, + process::Command, +}; + +#[derive(Debug, thiserror::Error)] +pub enum Error { + #[error("I/O error: {0:?}")] + Io(#[from] io::Error), + #[error("Metadata error: {0:?}")] + Metadata(#[from] super::metadata::Error), + #[error("Plist error: {0:?}")] + Plist(#[from] plist::Error), +} + +pub type Result = std::result::Result; + +/// Common bundle information that is shared in the [Info.plist](https://developer.apple.com/documentation/bundleresources/information-property-list) files. +#[derive(Clone, Serialize)] +pub struct BundleInfo { + /// [CFBundleName](https://developer.apple.com/documentation/bundleresources/information-property-list/cfbundlename) + #[serde(rename = "CFBundleName")] + pub name: String, + /// [CFBundleIdentifier](https://developer.apple.com/documentation/bundleresources/information-property-list/cfbundleidentifier) + #[serde(rename = "CFBundleIdentifier")] + pub identifier: String, + /// [CFBundleDisplayName](https://developer.apple.com/documentation/bundleresources/information-property-list/cfbundledisplayname) + #[serde(rename = "CFBundleDisplayName")] + pub display_name: String, + /// [CFBundleDevelopmentRegion](https://developer.apple.com/documentation/bundleresources/information-property-list/cfbundledevelopmentregion) + #[serde(rename = "CFBundleDevelopmentRegion")] + pub development_region: String, + /// [CFBundleVersion](https://developer.apple.com/documentation/bundleresources/information-property-list/cfbundleversion) + #[serde(rename = "CFBundleVersion", serialize_with = "serialize_version")] + pub version: Version, +} + +impl BundleInfo { + pub fn new( + name: &str, + identifier: &str, + display_name: &str, + development_region: &str, + version: Version, + ) -> Self { + Self { + name: name.to_owned(), + identifier: identifier.to_owned(), + display_name: display_name.to_owned(), + development_region: development_region.to_owned(), + version, + } + } +} + +/// See https://bitbucket.org/chromiumembedded/cef/wiki/GeneralUsage.md#markdown-header-macos +pub fn bundle( + app_path: &Path, + target_path: &Path, + executable_name: &str, + helper_name: &str, + resources_path: Option, + bundle_info: BundleInfo, +) -> Result { + let main_app_path = create_app( + app_path, + executable_name, + false, + resources_path.as_deref(), + bundle_info.clone(), + &target_path.join(executable_name), + )?; + let cef_path = cef_dll_sys::get_cef_dir().unwrap(); + let to = main_app_path.join(FRAMEWORKS_PATH).join(FRAMEWORK); + if to.exists() { + fs::remove_dir_all(&to).unwrap(); + } + copy_directory(&cef_path.join(FRAMEWORK), &to)?; + for helper in HELPERS { + let helper = format!("{executable_name} {helper}"); + create_app( + &main_app_path.join(FRAMEWORKS_PATH), + &helper, + true, + None, + bundle_info.clone(), + &target_path.join(helper_name), + )?; + } + if let Some(resources_path) = resources_path { + let resources_path = resources_path.join("mac"); + let target_path = main_app_path.join(RESOURCES_PATH); + copy_app_resources(&resources_path, &target_path)?; + } + Ok(main_app_path) +} + +/// Similar to [`bundle`], but this will invoke `cargo build` to build both the main executable and +/// helper executable targets. +pub fn build_bundle( + app_path: &Path, + executable_name: &str, + bundle_info: BundleInfo, +) -> Result { + let cargo_metadata = super::metadata::get_cargo_metadata()?; + let target_path = cargo_metadata.target_directory().join("debug"); + let bundle_metadata = cargo_metadata.parse_bundle_metadata(executable_name)?; + + cargo_build(executable_name)?; + cargo_build(&bundle_metadata.helper_name)?; + + bundle( + app_path, + &target_path, + executable_name, + &bundle_metadata.helper_name, + bundle_metadata.resources_path, + bundle_info, + ) +} + +#[derive(Serialize)] +struct InfoPlist { + #[serde(flatten)] + bundle_info: BundleInfo, + + #[serde(rename = "CFBundleExecutable")] + executable_name: String, + #[serde(rename = "CFBundleInfoDictionaryVersion")] + bundle_info_dictionary_version: String, + #[serde(rename = "CFBundlePackageType")] + bundle_package_type: String, + #[serde(rename = "CFBundleIconFile", skip_serializing_if = "String::is_empty")] + icon_file: String, + #[serde(rename = "CFBundleSignature")] + bundle_signature: String, + #[serde( + rename = "CFBundleShortVersionString", + serialize_with = "serialize_version" + )] + short_version: Version, + #[serde(rename = "LSEnvironment")] + environment: HashMap, + #[serde(rename = "LSFileQuarantineEnabled")] + file_quarantine_enabled: bool, + #[serde(rename = "LSMinimumSystemVersion")] + minimum_system_version: String, + #[serde( + rename = "LSUIElement", + skip_serializing_if = "Option::is_none", + serialize_with = "serialize_ui_element" + )] + ui_element: Option<&'static str>, + #[serde(rename = "NSBluetoothAlwaysUsageDescription")] + bluetooth_always_usage_description: String, + #[serde(rename = "NSSupportsAutomaticGraphicsSwitching")] + supports_automatic_graphics_switching: bool, + #[serde(rename = "NSWebBrowserPublicKeyCredentialUsageDescription")] + web_browser_publickey_credential_usage_description: String, + #[serde(rename = "NSCameraUsageDescription")] + camera_usage_description: String, + #[serde(rename = "NSMicrophoneUsageDescription")] + microphone_usage_description: String, +} + +impl InfoPlist { + fn new( + executable_name: &str, + is_helper: bool, + icon_file: Option, + bundle_info: BundleInfo, + ) -> Self { + Self { + executable_name: executable_name.to_owned(), + bundle_info_dictionary_version: "6.0".to_owned(), + bundle_package_type: "APPL".to_owned(), + icon_file: icon_file.unwrap_or_default(), + bundle_signature: "????".to_owned(), + short_version: Version::new( + bundle_info.version.major, + bundle_info.version.minor, + bundle_info.version.patch, + ), + environment: [("MallocNanoZone".to_owned(), "0".to_owned())] + .into_iter() + .collect(), + file_quarantine_enabled: true, + minimum_system_version: "11.0".to_owned(), + ui_element: if is_helper { Some("1") } else { None }, + bluetooth_always_usage_description: executable_name.to_owned(), + supports_automatic_graphics_switching: true, + web_browser_publickey_credential_usage_description: executable_name.to_owned(), + camera_usage_description: executable_name.to_owned(), + microphone_usage_description: executable_name.to_owned(), + bundle_info, + } + } +} + +fn serialize_ui_element( + ui_element: &Option<&'static str>, + serializer: S, +) -> std::result::Result +where + S: serde::Serializer, +{ + match ui_element { + Some(element) => serializer.serialize_str(element), + None => unreachable!("None is skipped"), + } +} + +fn serialize_version(version: &Version, serializer: S) -> std::result::Result +where + S: serde::Serializer, +{ + serializer.serialize_str(&version.to_string()) +} + +const EXEC_PATH: &str = "Contents/MacOS"; +const FRAMEWORKS_PATH: &str = "Contents/Frameworks"; +const RESOURCES_PATH: &str = "Contents/Resources"; +const FRAMEWORK: &str = "Chromium Embedded Framework.framework"; +const HELPERS: &[&str] = &[ + "Helper (GPU)", + "Helper (Renderer)", + "Helper (Plugin)", + "Helper (Alerts)", + "Helper", +]; + +fn create_app_layout(app_path: &Path) -> Result { + for path in [EXEC_PATH, RESOURCES_PATH, FRAMEWORKS_PATH] { + fs::create_dir_all(app_path.join(path))?; + } + Ok(app_path.join("Contents")) +} + +fn create_app( + app_path: &Path, + executable_name: &str, + is_helper: bool, + resources_path: Option<&Path>, + bundle_info: BundleInfo, + bin: &Path, +) -> Result { + let app_path = app_path.join(executable_name).with_extension("app"); + let contents_path = create_app_layout(&app_path)?; + let icon_file = resources_path.and_then(|path| { + let icon_file = format!("{executable_name}.icns"); + if path.join("mac").join(&icon_file).exists() { + Some(icon_file) + } else { + None + } + }); + create_info_plist( + &contents_path, + executable_name, + bundle_info, + is_helper, + icon_file, + )?; + let executable_path = app_path.join(EXEC_PATH).join(executable_name); + fs::copy(bin, executable_path)?; + Ok(app_path) +} + +fn create_info_plist( + contents_path: &Path, + executable_name: &str, + bundle_info: BundleInfo, + is_helper: bool, + icon_file: Option, +) -> Result<()> { + let info_plist = InfoPlist::new(executable_name, is_helper, icon_file, bundle_info); + plist::to_file_xml(contents_path.join("Info.plist"), &info_plist)?; + Ok(()) +} + +fn copy_directory(src: &Path, dst: &Path) -> io::Result<()> { + fs::create_dir_all(dst)?; + for entry in fs::read_dir(src)? { + let entry = entry?; + let dst_path = dst.join(entry.file_name()); + if entry.file_type()?.is_dir() { + copy_directory(&entry.path(), &dst_path)?; + } else { + fs::copy(entry.path(), &dst_path)?; + } + } + Ok(()) +} + +fn copy_app_resources(src: &Path, dst: &Path) -> io::Result<()> { + fs::create_dir_all(dst)?; + for entry in fs::read_dir(src)? { + let entry = entry?; + let mut dst_path = dst.join(entry.file_name()); + if entry.file_type()?.is_dir() { + copy_app_resources(&entry.path(), &dst_path)?; + } else { + let entry = entry.path(); + if entry + .extension() + .map(|ext| ext == "xib") + .unwrap_or_default() + { + dst_path.set_extension("nib"); + let (Some(dst_path), Some(entry)) = (dst_path.to_str(), entry.to_str()) else { + return Err(io::Error::from(io::ErrorKind::NotFound)); + }; + let status = Command::new("xcrun") + .args(["ibtool", "--compile", dst_path, entry]) + .status()?; + if !status.success() { + return Err(io::Error::from(io::ErrorKind::Interrupted)); + } + } else { + fs::copy(entry, &dst_path)?; + } + } + } + Ok(()) +} + +fn cargo_build(name: &str) -> Result<()> { + println!("Building {name}..."); + + let status = Command::new(super::cargo_path()) + .args(["build", "--bin", name]) + .status()?; + if status.success() { + Ok(()) + } else { + Err(io::Error::from(io::ErrorKind::Interrupted).into()) + } +} diff --git a/cef/src/build_util/metadata.rs b/cef/src/build_util/metadata.rs new file mode 100644 index 0000000..d90283e --- /dev/null +++ b/cef/src/build_util/metadata.rs @@ -0,0 +1,86 @@ +use serde::Deserialize; +use std::path::PathBuf; + +#[derive(Debug, thiserror::Error)] +pub enum Error { + #[error("Cargo metadata error: {0:?}")] + Metadata(#[from] cargo_metadata::Error), + #[error("Missing package metadata for {0}")] + MissingPackageMetadata(String), +} + +pub type Result = std::result::Result; + +#[derive(Deserialize)] +struct PackageMetadata { + cef: Cef, +} + +#[derive(Deserialize)] +struct Cef { + bundle: CargoBundleMetadata, +} + +#[derive(Deserialize)] +struct CargoBundleMetadata { + #[cfg(target_os = "macos")] + helper_name: String, + resources_path: Option, +} + +pub struct BundleMetadata { + #[cfg(target_os = "macos")] + pub helper_name: String, + pub resources_path: Option, +} + +impl BundleMetadata { + pub fn parse(executable: &str, metadata: &cargo_metadata::Metadata) -> Option { + let package = metadata + .packages + .iter() + .find(|p| p.targets.iter().any(|t| t.name == executable))?; + let package_metadata = + serde_json::from_value::(package.metadata.clone()).ok()?; + let resources_path = package_metadata + .cef + .bundle + .resources_path + .as_deref() + .and_then(|resources_path| { + package + .manifest_path + .clone() + .into_std_path_buf() + .parent() + .map(|manifest_dir| manifest_dir.join(resources_path)) + }); + Some(Self { + #[cfg(target_os = "macos")] + helper_name: package_metadata.cef.bundle.helper_name, + resources_path, + }) + } +} + +pub struct CargoMetadata(cargo_metadata::Metadata); + +impl CargoMetadata { + pub fn target_directory(&self) -> PathBuf { + PathBuf::from(&self.0.target_directory) + } + + pub fn parse_bundle_metadata(&self, executable: &str) -> Result { + BundleMetadata::parse(executable, &self.0) + .ok_or_else(|| Error::MissingPackageMetadata(executable.to_owned())) + } +} + +/// Run `cargo metadata` to determine the configuration for the current workspace/package. +pub fn get_cargo_metadata() -> Result { + let metadata = cargo_metadata::MetadataCommand::new() + .no_deps() + .other_options(vec!["--frozen".to_string()]) + .exec()?; + Ok(CargoMetadata(metadata)) +} diff --git a/cef/src/build_util/mod.rs b/cef/src/build_util/mod.rs new file mode 100644 index 0000000..086630b --- /dev/null +++ b/cef/src/build_util/mod.rs @@ -0,0 +1,18 @@ +use std::env; + +pub mod metadata; + +#[cfg(target_os = "macos")] +pub mod mac; + +#[cfg(target_os = "linux")] +pub mod linux; + +#[cfg(target_os = "windows")] +pub mod win; + +/// Prefer the path in the `CARGO` environment variable if specified, otherwise just execute +/// `cargo` from wherever it is found in the `PATH`. +fn cargo_path() -> String { + env::var("CARGO").unwrap_or_else(|_| "cargo".to_string()) +} diff --git a/examples/cefsimple/src/win/cefsimple.exe.manifest b/cef/src/build_util/win/cef-app.exe.manifest similarity index 100% rename from examples/cefsimple/src/win/cefsimple.exe.manifest rename to cef/src/build_util/win/cef-app.exe.manifest diff --git a/cef/src/build_util/win/mod.rs b/cef/src/build_util/win/mod.rs new file mode 100644 index 0000000..a98d1c1 --- /dev/null +++ b/cef/src/build_util/win/mod.rs @@ -0,0 +1,117 @@ +use std::{ + fs, + io::{self, Write}, + path::{Path, PathBuf}, + process::Command, +}; + +#[derive(Debug, thiserror::Error)] +pub enum Error { + #[error("I/O error: {0:?}")] + Io(#[from] io::Error), + #[error("Metadata error: {0:?}")] + Metadata(#[from] super::metadata::Error), +} + +pub type Result = std::result::Result; + +/// See https://bitbucket.org/chromiumembedded/cef/wiki/GeneralUsage.md#markdown-header-linux +pub fn bundle(app_path: &Path, target_path: &Path, executable_name: &str) -> Result { + let cef_path = cef_dll_sys::get_cef_dir().unwrap(); + copy_directory(&cef_path, &app_path)?; + + const LOCALES_DIR: &str = "locales"; + copy_directory(&cef_path.join(LOCALES_DIR), &app_path.join(LOCALES_DIR))?; + + copy_app(app_path, target_path, executable_name) +} + +/// Similar to [`bundle`], but this will invoke `cargo build` to build the executable target. +pub fn build_bundle(app_path: &Path, executable_name: &str, release: bool) -> Result { + let cargo_metadata = super::metadata::get_cargo_metadata()?; + let target_path = + cargo_metadata + .target_directory() + .join(if release { "release" } else { "debug" }); + + cargo_build(executable_name, release)?; + + bundle(app_path, &target_path, executable_name) +} + +const MANIFEST_CONTENT: &[u8] = include_bytes!("cef-app.exe.manifest"); + +fn copy_app(app_path: &Path, target_path: &Path, executable_name: &str) -> Result { + let mut manifest_file = + fs::File::create(app_path.join(format!("{executable_name}.exe.manifest")))?; + manifest_file.write_all(MANIFEST_CONTENT)?; + + #[cfg(feature = "sandbox")] + { + let dll_name = format!("{executable_name}.dll"); + let dll_path = app_path.join(&dll_name); + let target_dll = target_path.join(&dll_name); + fs::copy(&target_dll, &dll_path)?; + + let pdb_name = format!("{executable_name}.pdb"); + let pdb_path = app_path.join(&pdb_name); + let target_pdb = target_path.join(&pdb_name); + fs::copy(&target_pdb, &pdb_path)?; + + let executable_name = format!("{executable_name}.exe"); + let executable_path = app_path.join(&executable_name); + let cef_path = cef_dll_sys::get_cef_dir().unwrap(); + let target_executable = cef_path.join("bootstrap.exe"); + fs::copy(&target_executable, &executable_path)?; + + Ok(executable_path) + } + + #[cfg(not(feature = "sandbox"))] + { + let executable_name = format!("{executable_name}.exe"); + let executable_path = app_path.join(&executable_name); + let target_executable = target_path.join(executable_name); + fs::copy(&target_executable, &executable_path)?; + + Ok(executable_path) + } +} + +fn copy_directory(src: &Path, dst: &Path) -> io::Result<()> { + fs::create_dir_all(dst)?; + for entry in fs::read_dir(src)? { + let entry = entry?; + let dst_path = dst.join(entry.file_name()); + if entry.file_type()?.is_file() + && !entry + .path() + .extension() + .map(|ext| ext == "exe") + .unwrap_or_default() + { + fs::copy(entry.path(), &dst_path)?; + } + } + Ok(()) +} + +fn cargo_build(name: &str, release: bool) -> Result<()> { + println!("Building {name}..."); + + let mut args = vec!["build"]; + if release { + args.push("--release"); + } + #[cfg(feature = "sandbox")] + args.extend_from_slice(&["-p", name, "--lib"]); + #[cfg(not(feature = "sandbox"))] + args.extend_from_slice(&["--no-default-features", "--bin", name]); + + let status = Command::new(super::cargo_path()).args(args).status()?; + if status.success() { + Ok(()) + } else { + Err(io::Error::from(io::ErrorKind::Interrupted).into()) + } +} diff --git a/cef/src/lib.rs b/cef/src/lib.rs index e9e5983..6b7477c 100644 --- a/cef/src/lib.rs +++ b/cef/src/lib.rs @@ -3,6 +3,8 @@ pub mod args; pub mod rc; pub mod string; +pub mod window_info; +pub mod wrapper; #[cfg(target_os = "macos")] pub mod application_mac; @@ -16,10 +18,15 @@ pub mod sandbox; #[cfg(feature = "accelerated_osr")] pub mod osr_texture_import; +#[cfg(feature = "build-util")] +pub mod build_util; + #[rustfmt::skip] mod bindings; pub use bindings::*; +pub use rc::Rc as _; + pub use cef_dll_sys as sys; #[cfg(all( @@ -27,3 +34,7 @@ pub use cef_dll_sys as sys; feature = "accelerated_osr" ))] compile_error!("accelerated_osr not supported on this platform"); + +pub const SEEK_SET: i32 = 0; +pub const SEEK_CUR: i32 = 1; +pub const SEEK_END: i32 = 2; diff --git a/cef/src/sandbox.rs b/cef/src/sandbox.rs index f3157b9..dc337b8 100644 --- a/cef/src/sandbox.rs +++ b/cef/src/sandbox.rs @@ -43,6 +43,12 @@ impl Sandbox { } } +impl Default for Sandbox { + fn default() -> Self { + Self::new() + } +} + impl Drop for Sandbox { fn drop(&mut self) { unsafe { diff --git a/cef/src/string.rs b/cef/src/string.rs index 0ca8661..64b9f02 100644 --- a/cef/src/string.rs +++ b/cef/src/string.rs @@ -856,6 +856,7 @@ impl<'a, T> From<&'a mut CefStringCollection> for Option<&'a mut T> { } /// See [_cef_string_list_t] for more documentation. +#[derive(Clone)] pub struct CefStringList(CefStringCollection<_cef_string_list_t>); impl CefStringList { @@ -988,6 +989,7 @@ impl Debug for CefStringList { } /// See [_cef_string_map_t] for more documentation. +#[derive(Clone)] pub struct CefStringMap(CefStringCollection<_cef_string_map_t>); impl CefStringMap { @@ -1134,6 +1136,7 @@ impl Debug for CefStringMap { } /// See [_cef_string_multimap_t] for more documentation. +#[derive(Clone)] pub struct CefStringMultimap(CefStringCollection<_cef_string_multimap_t>); impl CefStringMultimap { diff --git a/cef/src/window_info.rs b/cef/src/window_info.rs new file mode 100644 index 0000000..6bab632 --- /dev/null +++ b/cef/src/window_info.rs @@ -0,0 +1,60 @@ +use crate::{sys::cef_window_handle_t, *}; +#[cfg(target_os = "windows")] +use windows_sys::Win32::UI::WindowsAndMessaging::*; + +impl WindowInfo { + /// Create the browser as a child window. + pub fn set_as_child(self, parent: cef_window_handle_t, bounds: &Rect) -> Self { + Self { + #[cfg(target_os = "windows")] + style: WS_CHILD | WS_CLIPCHILDREN | WS_CLIPSIBLINGS | WS_TABSTOP | WS_VISIBLE, + #[cfg(any(target_os = "linux", target_os = "windows"))] + parent_window: parent, + #[cfg(target_os = "macos")] + parent_view: parent, + bounds: bounds.clone(), + #[cfg(target_os = "macos")] + hidden: 0, + ..self + } + } + + /// Create the browser as a popup window. + #[cfg(target_os = "windows")] + pub fn set_as_popup(self, parent: cef_window_handle_t, title: &str) -> Self { + Self { + window_name: CefString::from(title), + parent_window: parent, + style: WS_OVERLAPPEDWINDOW | WS_CLIPCHILDREN | WS_CLIPSIBLINGS | WS_VISIBLE, + bounds: Rect { + x: CW_USEDEFAULT, + y: CW_USEDEFAULT, + width: CW_USEDEFAULT, + height: CW_USEDEFAULT, + }, + ..self + } + } + + /// Create the browser using windowless (off-screen) rendering. No window + /// will be created for the browser and all rendering will occur via the + /// CefRenderHandler interface. The |parent| value will be used to identify + /// monitor info and to act as the parent window for dialogs, context menus, + /// etc. If |parent| is not provided then the main screen monitor will be used + /// and some functionality that requires a parent window may not function + /// correctly. In order to create windowless browsers the + /// CefSettings.windowless_rendering_enabled value must be set to true. + /// Transparent painting is enabled by default but can be disabled by setting + /// CefBrowserSettings.background_color to an opaque value. + pub fn set_as_windowless(self, parent: cef_window_handle_t) -> Self { + Self { + windowless_rendering_enabled: 1, + #[cfg(any(target_os = "linux", target_os = "windows"))] + parent_window: parent, + #[cfg(target_os = "macos")] + parent_view: parent, + runtime_style: RuntimeStyle::ALLOY, + ..self + } + } +} diff --git a/cef/src/wrapper/browser_info_map.rs b/cef/src/wrapper/browser_info_map.rs new file mode 100644 index 0000000..0731109 --- /dev/null +++ b/cef/src/wrapper/browser_info_map.rs @@ -0,0 +1,164 @@ +use std::{collections::BTreeMap, ops::ControlFlow}; + +#[derive(Clone, Copy, PartialEq, Eq)] +pub enum BrowserInfoMapVisitorResult { + /// Remove the entry from the map. + RemoveEntry, + /// Keep the entry in the map. + KeepEntry, +} + +/// Implement this interface to visit and optionally delete objects in the map. +pub trait BrowserInfoMapVisitor { + fn on_next_info( + &self, + browser_id: i32, + key: K, + value: &V, + ) -> ControlFlow; +} + +#[derive(Default)] +pub struct BrowserInfoMap { + map: BTreeMap>, +} + +impl BrowserInfoMap { + /// Add an object associated with the specified ID values. + pub fn insert(&mut self, browser_id: i32, key: K, value: V) { + self.map.entry(browser_id).or_default().insert(key, value); + } + + /// Find the object with the specified ID values. |visitor| can optionally be + /// used to evaluate or remove the object at the same time. If the object is + /// removed using the Visitor the caller is responsible for destroying it. + pub fn find( + &mut self, + browser_id: i32, + key: K, + visitor: Option<&dyn BrowserInfoMapVisitor>, + ) -> Option { + let info_map = self.map.get_mut(&browser_id)?; + let entry = info_map.get(&key)?; + + if let Some(visitor) = visitor { + let result = match visitor.on_next_info(browser_id, key, entry) { + ControlFlow::Break(result) => result, + ControlFlow::Continue(result) => result, + }; + + if result == BrowserInfoMapVisitorResult::RemoveEntry { + let entry = info_map.remove(&key); + if info_map.is_empty() { + self.map.remove(&browser_id); + } + return entry; + } + } + + Some(entry.clone()) + } + + /// Find all objects. If any objects are removed using the Visitor the caller + /// is responsible for destroying them. + pub fn find_all(&mut self, visitor: &dyn BrowserInfoMapVisitor) { + let browser_ids: Vec<_> = self.map.keys().copied().collect(); + for browser_id in browser_ids { + let info_map = self + .map + .get_mut(&browser_id) + .expect("missing browser info map"); + + let mut keep_going = true; + let mut removed = vec![]; + let keys: Vec<_> = info_map.keys().copied().collect(); + for key in keys { + let value = info_map.get(&key).expect("missing value"); + let result = visitor.on_next_info(browser_id, key, value); + let (stop, result) = match result { + ControlFlow::Break(result) => (true, result), + ControlFlow::Continue(result) => (false, result), + }; + + if result == BrowserInfoMapVisitorResult::RemoveEntry { + removed.push(key); + } + + if stop { + keep_going = false; + break; + } + } + + for key in removed { + info_map.remove(&key); + } + + if info_map.is_empty() { + self.map.remove(&browser_id); + } + + if !keep_going { + break; + } + } + } + + /// Find all objects associated with the specified browser. If any objects are + /// removed using the Visitor the caller is responsible for destroying them. + pub fn find_browser_all(&mut self, browser_id: i32, visitor: &dyn BrowserInfoMapVisitor) { + let info_map = self + .map + .get_mut(&browser_id) + .expect("missing browser info map"); + + let mut removed = vec![]; + let keys: Vec<_> = info_map.keys().copied().collect(); + for key in keys { + let value = info_map.get(&key).expect("missing value"); + let result = visitor.on_next_info(browser_id, key, value); + let (stop, result) = match result { + ControlFlow::Break(result) => (true, result), + ControlFlow::Continue(result) => (false, result), + }; + + if result == BrowserInfoMapVisitorResult::RemoveEntry { + removed.push(key); + } + + if stop { + break; + } + } + + for key in removed { + info_map.remove(&key); + } + + if info_map.is_empty() { + self.map.remove(&browser_id); + } + } + + /// Returns true if the map is empty. + pub fn is_empty(&self) -> bool { + self.map.is_empty() + } + + /// Returns the number of objects in the map. + pub fn len(&self) -> usize { + self.map.values().map(|info_map| info_map.len()).sum() + } + + /// Returns the number of objects in the map that are associated with the + /// specified browser. + pub fn browser_len(&self, browser_id: i32) -> usize { + self.map + .get(&browser_id) + .map_or(0, |info_map| info_map.len()) + } + + pub fn clear(&mut self) { + self.map.clear(); + } +} diff --git a/cef/src/wrapper/byte_read_handler.rs b/cef/src/wrapper/byte_read_handler.rs new file mode 100644 index 0000000..d3499a4 --- /dev/null +++ b/cef/src/wrapper/byte_read_handler.rs @@ -0,0 +1,110 @@ +//! Thread safe implementation of the [ReadHandler] type for reading an in-memory array of bytes. +use crate::*; +use std::sync::{Arc, Mutex}; + +pub struct ByteStream { + bytes: Vec, + offset: usize, +} + +impl ByteStream { + pub fn new(bytes: Vec) -> Self { + ByteStream { bytes, offset: 0 } + } +} + +wrap_read_handler! { + pub struct ByteReadHandler { + stream: Arc>, + } + + impl ReadHandler { + #[allow(clippy::not_unsafe_ptr_arg_deref)] + fn read(&self, ptr: *mut u8, size: usize, n: usize) -> usize { + let Ok(mut stream) = self.stream.lock() else { + return 0; + }; + + let s = (stream.bytes.len() - stream.offset) / size; + let ret = s.min(n); + let buffer = unsafe { std::slice::from_raw_parts_mut(ptr, ret * size) }; + buffer.copy_from_slice(&stream.bytes[stream.offset..stream.offset + ret * size]); + stream.offset += ret * size; + ret + } + + fn seek(&self, offset: i64, whence: ::std::os::raw::c_int) -> ::std::os::raw::c_int { + let Ok(mut stream) = self.stream.lock() else { + return -1; + }; + + const SEEK_SET: i32 = 0; + const SEEK_CUR: i32 = 1; + const SEEK_END: i32 = 2; + + match whence { + SEEK_SET => { + if offset < 0 { + return -1; + } + let offset = offset as usize; + if offset > stream.bytes.len() { + return -1; + } + stream.offset = offset; + 0 + } + SEEK_CUR => { + if offset < 0 { + let offset = -offset as usize; + if offset > stream.offset { + return -1; + } + stream.offset -= offset; + } else { + let offset = offset as usize; + if offset + stream.offset > stream.bytes.len() { + return -1; + } + stream.offset += offset; + } + 0 + } + SEEK_END => { + if offset > 0 { + return -1; + } + let offset = -offset as usize; + if offset > stream.bytes.len() { + return -1; + } + stream.offset = stream.bytes.len() - offset; + 0 + } + _ => -1, + } + } + + fn tell(&self) -> i64 { + let Ok(stream) = self.stream.lock() else { + return 0; + }; + stream.offset as i64 + } + + fn eof(&self) -> i32 { + let Ok(stream) = self.stream.lock() else { + return 1; + }; + if stream.offset >= stream.bytes.len() { + 1 + } else { + 0 + } + } + + fn may_block(&self) -> i32 { + 0 + } + } +} diff --git a/cef/src/wrapper/message_router.rs b/cef/src/wrapper/message_router.rs new file mode 100644 index 0000000..f0487cf --- /dev/null +++ b/cef/src/wrapper/message_router.rs @@ -0,0 +1,1773 @@ +use super::{browser_info_map::*, message_router_utils as mru}; +use crate::*; +use std::{ + collections::{BTreeMap, VecDeque}, + ops::{AddAssign, ControlFlow, Range}, + rc::Rc, + sync::{ + atomic::{AtomicUsize, Ordering}, + Arc, Mutex, OnceLock, Weak, + }, +}; + +/// Used to configure the query router. The same values must be passed to both +/// [MessageRouterBrowserSide] and [MessageRouterRendererSide]. If using +/// multiple router pairs make sure to choose values that do not conflict. +#[derive(Clone, Debug)] +pub struct MessageRouterConfig { + /// Name of the JavaScript function that will be added to the 'window' object + /// for sending a query. The default value is "cefQuery". + pub js_query_function: String, + /// Name of the JavaScript function that will be added to the 'window' object + /// for canceling a pending query. The default value is "cefQueryCancel". + pub js_cancel_function: String, + /// Messages of size (in bytes) larger than this threshold will be sent via + /// shared memory region. + pub message_size_threshold: usize, +} + +impl MessageRouterConfig { + pub const fn validate(&self) -> bool { + !self.js_query_function.is_empty() && !self.js_cancel_function.is_empty() + } +} + +impl Default for MessageRouterConfig { + fn default() -> Self { + Self { + js_query_function: "cefQuery".to_string(), + js_cancel_function: "cefQueryCancel".to_string(), + message_size_threshold: RESPONSE_SIZE_THRESHOLD, + } + } +} + +/// This trait acts as a container for managing binary data. It retains +/// references to the underlying backing store, ensuring it is valid as long as +/// the BinaryBuffer exists. This allows efficient, zero-copy access to data +/// received from another process. +pub trait BinaryBuffer: Send { + /// Returns the read-only pointer to the memory. + fn data(&self) -> &[u8]; + /// Returns the writable pointer to the memory. + fn data_mut(&mut self) -> &mut [u8]; +} + +/// Callback associated with a single pending asynchronous query. Execute the +/// Success or Failure method to send an asynchronous response to the +/// associated JavaScript handler. It is a runtime error to destroy a Callback +/// object associated with an uncanceled query without first executing one of +/// the callback methods. The methods of this class may be called on any +/// browser process thread. +pub trait BrowserSideCallback: Send + Sync { + /// Notify the associated JavaScript onSuccess callback that the query has + /// completed successfully with the specified string |response|. + fn success_str(&self, response: &str); + /// Notify the associated JavaScript onSuccess callback that the query has + /// completed successfully with binary data. + fn success_binary(&self, data: &[u8]); + /// Notify the associated JavaScript onFailure callback that the query has + /// failed with the specified |error_code| and |error_message|. + fn failure(&self, error_code: i32, error_message: &str); +} + +/// Implement this interface to handle queries. All methods will be executed +/// on the browser process UI thread. +pub trait BrowserSideHandler: Send + Sync { + /// Executed when a new query is received. |query_id| uniquely identifies + /// the query for the life span of the router. Return true to handle the + /// query or false to propagate the query to other registered handlers, if + /// any. If no handlers return true from this method then the query will be + /// automatically canceled with an error code of -1 delivered to the + /// JavaScript onFailure callback. If this method returns true then a + /// Callback method must be executed either in this method or asynchronously + /// to complete the query. + fn on_query_str( + &self, + _browser: Option, + _frame: Option, + _query_id: i64, + _request: &str, + _persistent: bool, + _callback: Arc>, + ) -> bool { + false + } + + /// Executed when a new query is received. |query_id| uniquely identifies + /// the query for the life span of the router. Return true to handle the + /// query or false to propagate the query to other registered handlers, if + /// any. If no handlers return true from this method then the query will be + /// automatically canceled with an error code of -1 delivered to the + /// JavaScript onFailure callback. If this method returns true then a + /// Callback method must be executed either in this method or asynchronously + /// to complete the query. + fn on_query_binary( + &self, + _browser: Option, + _frame: Option, + _query_id: i64, + _request: &dyn BinaryBuffer, + _persistent: bool, + _callback: Arc>, + ) -> bool { + false + } + + /// Executed when a query has been canceled either explicitly using the + /// JavaScript cancel function or implicitly due to browser destruction, + /// navigation or renderer process termination. It will only be called for + /// the single handler that returned true from OnQuery for the same + /// |query_id|. No references to the associated Callback object should be + /// kept after this method is called, nor should any Callback methods be + /// executed. + fn on_query_canceled(&self, _browser: Option, _frame: Option, _query_id: i64) {} +} + +/// Implements the browser side of query routing. The methods of this trait may +/// be called on any browser process thread unless otherwise indicated. +pub trait MessageRouterBrowserSide { + type Callback: BrowserSideCallback; + + /// Create a new router with the specified configuration. + fn new(config: MessageRouterConfig) -> Arc; + + /// Add a new query handler. If |first| is true it will be added as the first + /// handler, otherwise it will be added as the last handler. Must be called on + /// the browser process UI thread. + fn add_handler(&self, handler: Arc, first: bool) -> Option; + + /// Remove an existing query handler. Any pending queries associated with the + /// handler will be canceled. Handler::OnQueryCanceled will be called and the + /// associated JavaScript onFailure callback will be executed with an error + /// code of -1. Returns true if the handler is removed successfully or false + /// if the handler is not found. Must be called on the browser process UI + /// thread. + fn remove_handler(&self, handler_id: HandlerId) -> bool; + + /// Cancel all pending queries associated with either |browser| or |handler|. + /// If both |browser| and |handler| are NULL all pending queries will be + /// canceled. Handler::OnQueryCanceled will be called and the associated + /// JavaScript onFailure callback will be executed in all cases with an error + /// code of -1. + fn cancel_pending(&self, browser: Option, handler_id: Option); + + /// Returns the number of queries currently pending for the specified + /// |browser| and/or |handler|. Either or both values may be empty. Must be + /// called on the browser process UI thread. + fn pending_count(&self, browser: Option, handler_id: Option) -> usize; +} + +/// The below methods should be called from other CEF handlers. They must be +/// called exactly as documented for the router to function correctly. +pub trait MessageRouterBrowserSideHandlerCallbacks: MessageRouterBrowserSide { + /// Call from CefLifeSpanHandler::OnBeforeClose. Any pending queries + /// associated with |browser| will be canceled and Handler::OnQueryCanceled + /// will be called. No JavaScript callbacks will be executed since this + /// indicates destruction of the browser. + fn on_before_close(&self, browser: Option); + + /// Call from CefRequestHandler::OnRenderProcessTerminated. Any pending + /// queries associated with |browser| will be canceled and + /// Handler::OnQueryCanceled will be called. No JavaScript callbacks will be + /// executed since this indicates destruction of the context. + fn on_render_process_terminated(&self, browser: Option); + + /// Call from CefRequestHandler::OnBeforeBrowse only if the navigation is + /// allowed to proceed. If |frame| is the main frame then any pending queries + /// associated with |browser| will be canceled and Handler::OnQueryCanceled + /// will be called. No JavaScript callbacks will be executed since this + /// indicates destruction of the context. + fn on_before_browse(&self, browser: Option, frame: Option); + + /// Call from CefClient::OnProcessMessageReceived. Returns true if the message + /// is handled by this router or false otherwise. + fn on_process_message_received( + &self, + browser: Option, + frame: Option, + source_process: ProcessId, + message: Option, + ) -> bool; +} + +/// Implements the renderer side of query routing. The methods of this class +/// must be called on the render process main thread. +pub trait MessageRouterRendererSide { + /// Create a new router with the specified configuration. + fn new(config: MessageRouterConfig) -> Arc; + + /// Returns the number of queries currently pending for the specified + /// |browser| and/or |context|. Either or both values may be empty. + fn pending_count(&self, browser: Option, context: Option) -> usize; +} + +/// The below methods should be called from other CEF handlers. They must be +/// called exactly as documented for the router to function correctly. +pub trait MessageRouterRendererSideHandlerCallbacks: MessageRouterRendererSide { + /// Call from CefRenderProcessHandler::OnContextCreated. Registers the + /// JavaScripts functions with the new context. + fn on_context_created( + &self, + browser: Option, + frame: Option, + context: Option, + ); + + /// Call from CefRenderProcessHandler::OnContextReleased. Any pending queries + /// associated with the released context will be canceled and + /// Handler::OnQueryCanceled will be called in the browser process. + fn on_context_released( + &self, + browser: Option, + frame: Option, + context: Option, + ); + + /// Call from CefRenderProcessHandler::OnProcessMessageReceived. Returns true + /// if the message is handled by this router or false otherwise. + fn on_process_message_received( + &self, + browser: Option, + frame: Option, + source_process: Option, + message: Option, + ) -> bool; +} + +/// ID value reserved for internal use. +const RESERVED_ID: i32 = 0; + +/// Appended to the JS function name for related IPC messages. +const MESSAGE_SUFFIX: &str = "Msg"; + +/// JS object member argument names for cefQuery. +struct ObjectMember; + +impl ObjectMember { + const REQUEST: &str = "request"; + const ON_SUCCESS: &str = "onSuccess"; + const ON_FAILURE: &str = "onFailure"; + const PERSISTENT: &str = "persistent"; +} + +/// Default error information when a query is canceled. +struct CanceledError; + +impl CanceledError { + const CODE: i32 = -1; + const MESSAGE: &str = "The query has been canceled"; +} + +/// Value of 16KB is chosen as a result of performance tests available at +/// http://tests/ipc_performance +const RESPONSE_SIZE_THRESHOLD: usize = 16 * 1024; + +/// A helper template for generating ID values. +struct IdGenerator +where + T: Copy + AddAssign + Ord + From, +{ + next_id: T, + range: Range, +} + +impl IdGenerator +where + T: Copy + AddAssign + Ord + From, +{ + fn new(max: T) -> Self { + let next_id = RESERVED_ID.into(); + Self { + next_id, + range: next_id..max, + } + } + + fn next(&mut self) -> T { + if self.next_id >= self.range.end { + self.next_id = self.range.start; + } + + self.next_id += 1_i32.into(); + self.next_id + } +} + +pub type HandlerId = i32; + +pub struct BrowserSideRouterCallback { + weak_callback: Weak>, + router: Option>, + browser_id: i32, + query_id: i64, + persistent: bool, + message_size_threshold: usize, + query_message_name: String, +} + +impl BrowserSideRouterCallback { + fn new( + router: Arc, + browser_id: i32, + query_id: i64, + persistent: bool, + message_size_threshold: usize, + query_message_name: String, + ) -> Arc> { + Arc::new_cyclic(|weak_self| { + Mutex::new(Self { + weak_callback: weak_self.clone(), + router: Some(router), + browser_id, + query_id, + persistent, + message_size_threshold, + query_message_name, + }) + }) + } + + fn detach(&mut self) { + debug_assert_ne!(currently_on(ThreadId::UI), 0); + self.router = None; + } +} + +impl BrowserSideCallback for BrowserSideRouterCallback { + fn success_str(&self, response: &str) { + let builder = mru::create_browser_response_builder( + self.message_size_threshold, + &self.query_message_name, + mru::MessagePayload::from(response), + ); + + // We need to post task here for two reasons: + // 1) To safely access member variables. + // 2) To let the router to persist the query information before + // the Success callback is executed. + let mut task = BrowserSideRouterCallbackSuccess::new(self.weak_callback.clone(), builder); + post_task(ThreadId::UI, Some(&mut task)); + } + + fn success_binary(&self, data: &[u8]) { + let builder = mru::create_browser_response_builder( + self.message_size_threshold, + &self.query_message_name, + mru::MessagePayload::from(data), + ); + + // We need to post task here for two reasons: + // 1) To safely access member variables. + // 2) To let the router to persist the query information before + // the Success callback is executed. + let mut task = BrowserSideRouterCallbackSuccess::new(self.weak_callback.clone(), builder); + post_task(ThreadId::UI, Some(&mut task)); + } + + fn failure(&self, error_code: i32, error_message: &str) { + // We need to post task here for two reasons: + // 1) To safely access member variables. + // 2) To give previosly submitted tasks by the Success calls to execute + // before we invalidate the callback. + let mut task = BrowserSideRouterCallbackFailure::new( + self.weak_callback.clone(), + error_code, + error_message.to_string(), + ); + post_task(ThreadId::UI, Some(&mut task)); + } +} + +wrap_task! { + struct BrowserSideRouterCallbackSuccess { + weak_callback: Weak>, + builder: Rc, + } + + impl Task { + fn execute(&self) { + let Some(callback) = self.weak_callback.upgrade() else { + return; + }; + let Ok(mut callback) = callback.lock() else { + return; + }; + + let router = if callback.persistent { + callback.router.clone() + } else { + // Non-persistent callbacks are only good for a single use. + callback.router.take() + }; + let Some(router) = router else { + return; + }; + + router.on_callback_success(callback.browser_id, callback.query_id, &*self.builder); + } + } +} + +wrap_task! { + struct BrowserSideRouterCallbackFailure { + weak_callback: Weak>, + error_code: i32, + error_message: String, + } + + impl Task { + fn execute(&self) { + let Some(callback) = self.weak_callback.upgrade() else { + return; + }; + let Ok(mut callback) = callback.lock() else { + return; + }; + + // Failure always invalidates the callback. + let router = callback.router.take(); + let Some(router) = router else { + return; + }; + + router.on_callback_failure(callback.browser_id, callback.query_id, self.error_code, &self.error_message); + } + } +} + +/// Structure representing a pending query. +#[derive(Clone, Default)] +struct BrowserSideQueryInfo { + // Browser and frame originated the query. + browser: Option, + frame: Option, + // IDs that uniquely identify the query in the renderer process. These + // values are opaque to the browser process but must be returned with the + // response. + context_id: i32, + request_id: i32, + // True if the query is persistent. + is_persistent: bool, + // Callback associated with the query that must be detached when the query + // is canceled. + callback: Option>>, + // Handler that should be notified if the query is automatically canceled. + handler: Option<(HandlerId, Arc)>, +} + +#[derive(Default, Clone)] +struct HandlersDeque { + offset: HandlerId, + handlers: VecDeque>>, +} + +pub struct BrowserSideRouter { + weak_self: Weak, + config: MessageRouterConfig, + query_message_name: String, + cancel_message_name: String, + query_id_generator: Mutex>, + handlers: Mutex, + browser_query_info_map: Mutex>, +} + +impl BrowserSideRouter { + /// Retrieve a QueryInfo object from the map based on the browser-side query + /// ID. If |always_remove| is true then the QueryInfo object will always be + /// removed from the map. Othewise, the QueryInfo object will only be removed + /// if the query is non-persistent. If |removed| is true the caller is + /// responsible for deleting the returned QueryInfo object. + fn get_query_info( + &self, + browser_id: i32, + query_id: i64, + always_remove: bool, + ) -> Option { + let Ok(mut browser_query_info_map) = self.browser_query_info_map.lock() else { + return None; + }; + + struct Visitor { + always_remove: bool, + } + + impl BrowserInfoMapVisitor for Visitor { + fn on_next_info( + &self, + _browser_id: i32, + _key: i64, + value: &BrowserSideQueryInfo, + ) -> ControlFlow { + ControlFlow::Continue(if self.always_remove || !value.is_persistent { + BrowserInfoMapVisitorResult::RemoveEntry + } else { + BrowserInfoMapVisitorResult::KeepEntry + }) + } + } + + let visitor = Visitor { always_remove }; + browser_query_info_map.find(browser_id, query_id, Some(&visitor)) + } + + /// Called by CallbackImpl on success. + fn on_callback_success( + &self, + browser_id: i32, + query_id: i64, + builder: &dyn mru::ProcessMessageBuilder, + ) { + debug_assert_ne!(currently_on(ThreadId::UI), 0); + if let Some(info) = self.get_query_info(browser_id, query_id, false) { + self.send_query_success( + info.browser, + info.frame, + info.context_id, + info.request_id, + builder, + ) + } + } + + /// Called by CallbackImpl on failure. + fn on_callback_failure( + &self, + browser_id: i32, + query_id: i64, + error_code: i32, + error_message: &str, + ) { + debug_assert_ne!(currently_on(ThreadId::UI), 0); + if let Some(info) = self.get_query_info(browser_id, query_id, false) { + self.send_query_failure( + info.browser, + info.frame, + info.context_id, + info.request_id, + error_code, + error_message, + ) + } + } + + fn send_query_success( + &self, + _browser: Option, + frame: Option, + context_id: i32, + request_id: i32, + builder: &dyn mru::ProcessMessageBuilder, + ) { + let (Some(frame), Some(mut message)) = ( + frame, + builder.build_browser_response(context_id, request_id), + ) else { + return; + }; + + frame.send_process_message(ProcessId::RENDERER, Some(&mut message)); + } + + fn send_query_failure( + &self, + _browser: Option, + frame: Option, + context_id: i32, + request_id: i32, + error_code: i32, + error_message: &str, + ) { + let (Some(frame), Some(mut message)) = ( + frame, + process_message_create(Some(&CefString::from(self.query_message_name.as_str()))), + ) else { + return; + }; + let Some(args) = message.argument_list() else { + return; + }; + + args.set_int(0, context_id); + args.set_int(1, request_id); + args.set_bool(2, 0); // Indicates a failure result. + args.set_int(3, error_code); + args.set_string(4, Some(&CefString::from(error_message))); + + frame.send_process_message(ProcessId::RENDERER, Some(&mut message)); + } + + /// Cancel a query that has not been sent to a handler. + fn cancel_unhandled_query( + &self, + browser: Option, + frame: Option, + context_id: i32, + request_id: i32, + ) { + self.send_query_failure( + browser, + frame, + context_id, + request_id, + CanceledError::CODE, + CanceledError::MESSAGE, + ); + } + + /// Cancel a query that has already been sent to a handler. + fn cancel_query(&self, query_id: i64, query_info: BrowserSideQueryInfo, notify_renderer: bool) { + if notify_renderer { + self.send_query_failure( + query_info.browser.clone(), + query_info.frame.clone(), + query_info.context_id, + query_info.request_id, + CanceledError::CODE, + CanceledError::MESSAGE, + ); + } + + if let Some((_, handler)) = query_info.handler { + handler.on_query_canceled(query_info.browser, query_info.frame, query_id); + } + + // Invalidate the callback. + if let Some(callback) = query_info.callback { + if let Ok(mut callback) = callback.lock() { + callback.detach(); + } + } + } + + /// Cancel all pending queries associated with either |browser| or |handler|. + /// If both |browser| and |handler| are NULL all pending queries will be + /// canceled. Set |notify_renderer| to true if the renderer should be notified. + fn cancel_pending_for( + &self, + browser: Option, + handler: Option<(HandlerId, Arc)>, + notify_renderer: bool, + ) { + if currently_on(ThreadId::UI) == 0 { + let mut task = BrowserSideRouterCancelPendingFor::new( + self.weak_self.clone(), + browser, + handler, + notify_renderer, + ); + post_task(ThreadId::UI, Some(&mut task)); + return; + } + + let (Some(router), Ok(mut browser_query_info_map)) = + (self.weak_self.upgrade(), self.browser_query_info_map.lock()) + else { + return; + }; + + struct Visitor { + router: Arc, + handler_id: Option, + notify_renderer: bool, + } + + impl BrowserInfoMapVisitor for Visitor { + fn on_next_info( + &self, + _browser_id: i32, + key: i64, + value: &BrowserSideQueryInfo, + ) -> ControlFlow { + ControlFlow::Continue(match (self.handler_id, &value.handler) { + (Some(handler_id), Some(entry)) if handler_id != entry.0 => { + BrowserInfoMapVisitorResult::KeepEntry + } + _ => { + self.router + .cancel_query(key, value.clone(), self.notify_renderer); + BrowserInfoMapVisitorResult::RemoveEntry + } + }) + } + } + + let visitor = Visitor { + router, + handler_id: handler.map(|handler| handler.0), + notify_renderer, + }; + + if let Some(browser) = browser { + // Cancel all queries associated with the specified browser. + browser_query_info_map.find_browser_all(browser.identifier(), &visitor); + } else { + // Cancel all queries for all browsers. + browser_query_info_map.find_all(&visitor); + } + } + + /// Cancel a query based on the renderer-side IDs. If |request_id| is + /// kReservedId all requests associated with |context_id| will be canceled. + fn cancel_pending_request(&self, browser_id: i32, context_id: i32, request_id: i32) { + let (Some(router), Ok(mut browser_query_info_map)) = + (self.weak_self.upgrade(), self.browser_query_info_map.lock()) + else { + return; + }; + + struct Visitor { + router: Arc, + context_id: i32, + request_id: i32, + } + + impl BrowserInfoMapVisitor for Visitor { + fn on_next_info( + &self, + _browser_id: i32, + key: i64, + value: &BrowserSideQueryInfo, + ) -> ControlFlow { + if value.context_id == self.context_id && self.request_id == RESERVED_ID + || value.request_id == self.request_id + { + self.router.cancel_query(key, value.clone(), false); + + if self.request_id == RESERVED_ID { + ControlFlow::Continue(BrowserInfoMapVisitorResult::RemoveEntry) + } else { + // Stop iterating if only canceling a single request. + ControlFlow::Break(BrowserInfoMapVisitorResult::RemoveEntry) + } + } else { + ControlFlow::Continue(BrowserInfoMapVisitorResult::KeepEntry) + } + } + } + + let visitor = Visitor { + router, + context_id, + request_id, + }; + + browser_query_info_map.find_browser_all(browser_id, &visitor); + } +} + +impl MessageRouterBrowserSide for BrowserSideRouter { + type Callback = BrowserSideRouterCallback; + + /// Create a new router with the specified configuration. + fn new(config: MessageRouterConfig) -> Arc { + Arc::new_cyclic(|weak_self| { + let query_message_name = format!("{}{MESSAGE_SUFFIX}", config.js_query_function); + let cancel_message_name = format!("{}{MESSAGE_SUFFIX}", config.js_cancel_function); + Self { + weak_self: weak_self.clone(), + config, + query_message_name, + cancel_message_name, + query_id_generator: Mutex::new(IdGenerator::new(i64::MAX)), + handlers: Default::default(), + browser_query_info_map: Default::default(), + } + }) + } + + /// Add a new query handler. If |first| is true it will be added as the first + /// handler, otherwise it will be added as the last handler. Must be called on + /// the browser process UI thread. + fn add_handler(&self, handler: Arc, first: bool) -> Option { + debug_assert_ne!(currently_on(ThreadId::UI), 0); + let handler_id = if first { + let mut handlers = self.handlers.lock().ok()?; + handlers.offset += 1; + handlers.handlers.push_front(Some(handler)); + -handlers.offset + } else { + let mut handlers = self.handlers.lock().ok()?; + let offset = i32::try_from(handlers.handlers.len()).ok()? - handlers.offset; + handlers.handlers.push_back(Some(handler)); + offset + }; + + Some(handler_id) + } + + /// Remove an existing query handler. Any pending queries associated with the + /// handler will be canceled. Handler::OnQueryCanceled will be called and the + /// associated JavaScript onFailure callback will be executed with an error + /// code of -1. Returns true if the handler is removed successfully or false + /// if the handler is not found. Must be called on the browser process UI + /// thread. + fn remove_handler(&self, handler_id: HandlerId) -> bool { + debug_assert_ne!(currently_on(ThreadId::UI), 0); + let Ok(mut handlers) = self.handlers.lock() else { + return false; + }; + let Ok(index) = usize::try_from(handler_id + handlers.offset) else { + return false; + }; + let Some(entry) = handlers.handlers.get_mut(index) else { + return false; + }; + let handler = entry.take(); + + let trim_start = handlers + .handlers + .iter() + .take_while(|entry| entry.is_none()) + .count(); + for _ in 0..trim_start { + handlers.offset -= 1; + handlers.handlers.pop_front(); + } + + let trim_end = handlers + .handlers + .iter() + .rev() + .take_while(|entry| entry.is_none()) + .count(); + for _ in 0..trim_end { + handlers.handlers.pop_back(); + } + + if let Some(handler) = handler { + self.cancel_pending_for(None, Some((handler_id, handler)), true); + true + } else { + false + } + } + + /// Cancel all pending queries associated with either |browser| or |handler|. + /// If both |browser| and |handler| are NULL all pending queries will be + /// canceled. Handler::OnQueryCanceled will be called and the associated + /// JavaScript onFailure callback will be executed in all cases with an error + /// code of -1. + fn cancel_pending(&self, browser: Option, handler_id: Option) { + let handler = handler_id.and_then(|handler_id| { + let handlers = self.handlers.lock().ok()?; + let index = usize::try_from(handler_id + handlers.offset).ok()?; + let entry = handlers.handlers.get(index)?; + entry.as_ref().map(|handler| (handler_id, handler.clone())) + }); + self.cancel_pending_for(browser, handler, true); + } + + /// Returns the number of queries currently pending for the specified + /// |browser| and/or |handler|. Either or both values may be empty. Must be + /// called on the browser process UI thread. + fn pending_count(&self, browser: Option, handler_id: Option) -> usize { + debug_assert_ne!(currently_on(ThreadId::UI), 0); + let Ok(mut browser_query_info_map) = self.browser_query_info_map.lock() else { + return 0; + }; + if browser_query_info_map.is_empty() { + return 0; + } + + let handler_id = handler_id.and_then(|handler_id| { + let handlers = self.handlers.lock().ok()?; + let index = usize::try_from(handler_id + handlers.offset).ok()?; + let entry = handlers.handlers.get(index)?; + entry.as_ref().map(|_| handler_id) + }); + + if let Some(handler_id) = handler_id { + // Need to iterate over each QueryInfo object to test the handler. + struct Visitor { + handler_id: HandlerId, + count: AtomicUsize, + } + + impl BrowserInfoMapVisitor for Visitor { + fn on_next_info( + &self, + _browser_id: i32, + _key: i64, + value: &BrowserSideQueryInfo, + ) -> ControlFlow + { + if value + .handler + .as_ref() + .map(|handler| handler.0 == self.handler_id) + .unwrap_or(false) + { + self.count.fetch_add(1, Ordering::Relaxed); + } + + ControlFlow::Continue(BrowserInfoMapVisitorResult::KeepEntry) + } + } + + let visitor = Visitor { + handler_id, + count: Default::default(), + }; + + if let Some(browser) = browser { + browser_query_info_map.find_browser_all(browser.identifier(), &visitor); + } else { + browser_query_info_map.find_all(&visitor); + } + + visitor.count.load(Ordering::Relaxed) + } else if let Some(browser) = browser { + // Count queries associated with the specified browser. + browser_query_info_map.browser_len(browser.identifier()) + } else { + // Count all queries for all browsers. + browser_query_info_map.len() + } + } +} + +impl MessageRouterBrowserSideHandlerCallbacks for BrowserSideRouter { + /// Call from CefLifeSpanHandler::OnBeforeClose. Any pending queries + /// associated with |browser| will be canceled and Handler::OnQueryCanceled + /// will be called. No JavaScript callbacks will be executed since this + /// indicates destruction of the browser. + fn on_before_close(&self, browser: Option) { + self.cancel_pending_for(browser, None, false); + } + + /// Call from CefRequestHandler::OnRenderProcessTerminated. Any pending + /// queries associated with |browser| will be canceled and + /// Handler::OnQueryCanceled will be called. No JavaScript callbacks will be + /// executed since this indicates destruction of the context. + fn on_render_process_terminated(&self, browser: Option) { + self.cancel_pending_for(browser, None, false); + } + + /// Call from CefRequestHandler::OnBeforeBrowse only if the navigation is + /// allowed to proceed. If |frame| is the main frame then any pending queries + /// associated with |browser| will be canceled and Handler::OnQueryCanceled + /// will be called. No JavaScript callbacks will be executed since this + /// indicates destruction of the context. + fn on_before_browse(&self, browser: Option, frame: Option) { + if frame.map(|frame| frame.is_main() != 0).unwrap_or(false) { + self.cancel_pending_for(browser, None, false); + } + } + + /// Call from CefClient::OnProcessMessageReceived. Returns true if the message + /// is handled by this router or false otherwise. + fn on_process_message_received( + &self, + browser: Option, + frame: Option, + _source_process: ProcessId, + message: Option, + ) -> bool { + debug_assert_ne!(currently_on(ThreadId::UI), 0); + let Some(message) = message else { + return true; + }; + + let name = CefString::from(&message.name()).to_string(); + if name == self.query_message_name { + let content = mru::RenderMessage::from(Some(message)); + + let (arc_self, browser_id, query_id, handlers, mut browser_query_info_map) = match ( + self.weak_self.upgrade(), + browser.as_ref(), + self.query_id_generator.lock(), + self.handlers.lock(), + self.browser_query_info_map.lock(), + ) { + ( + Some(arc_self), + Some(browser), + Ok(mut query_id_generator), + Ok(handlers), + Ok(browser_query_info_map), + ) if !handlers.handlers.is_empty() => ( + arc_self, + browser.identifier(), + query_id_generator.next(), + handlers.clone(), + browser_query_info_map, + ), + _ => { + self.cancel_unhandled_query( + browser, + frame, + content.context_id, + content.request_id, + ); + return true; + } + }; + + let callback = BrowserSideRouterCallback::new( + arc_self, + browser_id, + query_id, + content.is_persistent, + self.config.message_size_threshold, + name, + ); + + let invoke_handler = { + Box::new( + |handler: &dyn BrowserSideHandler, payload: &mru::MessagePayload| match payload + { + mru::MessagePayload::Empty => handler.on_query_binary( + browser.clone(), + frame.clone(), + query_id, + &mru::EmptyBinaryBuffer, + content.is_persistent, + callback.clone(), + ), + mru::MessagePayload::String(payload) => handler.on_query_str( + browser.clone(), + frame.clone(), + query_id, + &payload.to_string(), + content.is_persistent, + callback.clone(), + ), + mru::MessagePayload::Binary(buffer) => handler.on_query_binary( + browser.clone(), + frame.clone(), + query_id, + buffer.as_ref(), + content.is_persistent, + callback.clone(), + ), + }, + ) + }; + let handler = handlers + .handlers + .iter() + .enumerate() + .find_map(move |(index, handler)| { + let handler_id = i32::try_from(index).ok()? - handlers.offset; + let handler = handler.as_ref()?; + if invoke_handler(handler.as_ref(), &content.payload) { + Some((handler_id, handler.clone())) + } else { + None + } + }); + if let Some(handler) = handler { + // Persist the query information until the callback executes. + // It's safe to do this here because the callback will execute + // asynchronously. + let query_info = BrowserSideQueryInfo { + browser, + frame, + context_id: content.context_id, + request_id: content.request_id, + is_persistent: content.is_persistent, + callback: Some(callback), + handler: Some(handler), + }; + + browser_query_info_map.insert(browser_id, query_id, query_info); + } else { + if let Ok(mut callback) = callback.lock() { + callback.detach(); + } + + // No one chose to handle the query so cancel it. + self.cancel_unhandled_query(browser, frame, content.context_id, content.request_id); + } + + return true; + } else if name == self.cancel_message_name { + let (browser, args) = match (browser, message.argument_list()) { + (Some(browser), Some(args)) => (browser, args), + _ => return true, + }; + debug_assert_eq!(args.size(), 2); + + let browser_id = browser.identifier(); + let context_id = args.int(0); + let request_id = args.int(1); + + self.cancel_pending_request(browser_id, context_id, request_id); + return true; + } + + false + } +} + +wrap_task! { + struct BrowserSideRouterCancelPendingFor { + router: Weak, + browser: Option, + handler: Option<(HandlerId, Arc)>, + notify_renderer: bool, + } + + impl Task { + fn execute(&self) { + let Some(router) = self.router.upgrade() else { + return; + }; + + router.cancel_pending_for(self.browser.clone(), self.handler.clone(), self.notify_renderer); + } + } +} + +/// Structure representing a pending request. +#[derive(Clone, Default)] +pub struct RendererSideRequestInfo { + /// True if the request is persistent. + is_persistent: bool, + /// Success callback function. May be [`None`]. + success_callback: Option, + /// Failure callback function. May be [`None`]. + failure_callback: Option, +} + +pub struct RendererSideRouter { + weak_self: Weak, + config: MessageRouterConfig, + query_message_name: String, + cancel_message_name: String, + context_id_generator: Mutex>, + request_id_generator: Mutex>, + browser_request_info_map: Mutex>, + context_map: Mutex>>, +} + +impl RendererSideRouter { + /// Retrieve a RequestInfo object from the map based on the renderer-side + /// IDs. If |always_remove| is true then the RequestInfo object will always be + /// removed from the map. Othewise, the RequestInfo object will only be removed + /// if the query is non-persistent. + fn get_request_info( + &self, + browser_id: i32, + context_id: i32, + request_id: i32, + always_remove: bool, + ) -> Option { + let Ok(mut browser_request_info_map) = self.browser_request_info_map.lock() else { + return None; + }; + + struct Visitor { + always_remove: bool, + } + + impl BrowserInfoMapVisitor<(i32, i32), RendererSideRequestInfo> for Visitor { + fn on_next_info( + &self, + _browser_id: i32, + _key: (i32, i32), + value: &RendererSideRequestInfo, + ) -> ControlFlow { + ControlFlow::Continue(if self.always_remove || !value.is_persistent { + BrowserInfoMapVisitorResult::RemoveEntry + } else { + BrowserInfoMapVisitorResult::KeepEntry + }) + } + } + + let visitor = Visitor { always_remove }; + browser_request_info_map.find(browser_id, (context_id, request_id), Some(&visitor)) + } + + /// Returns the new request ID. + fn send_query( + &self, + browser: Option, + frame: Option, + context_id: i32, + request: V8Value, + request_info: RendererSideRequestInfo, + ) -> i32 { + debug_assert_ne!(currently_on(ThreadId::RENDERER), 0); + let ( + Some(browser), + Some(frame), + Ok(mut request_id_generator), + Ok(mut browser_request_info_map), + ) = ( + browser, + frame, + self.request_id_generator.lock(), + self.browser_request_info_map.lock(), + ) + else { + return RESERVED_ID; + }; + let request_id = request_id_generator.next(); + let persistent = request_info.is_persistent; + browser_request_info_map.insert( + browser.identifier(), + (context_id, request_id), + request_info, + ); + + let mut message = mru::build_renderer_message( + self.config.message_size_threshold, + &self.query_message_name, + context_id, + request_id, + Some(&request), + persistent, + ); + frame.send_process_message(ProcessId::BROWSER, message.as_mut()); + + request_id + } + + /// If |request_id| is kReservedId all requests associated with |context_id| + /// will be canceled, otherwise only the specified |request_id| will be + /// canceled. Returns true if any request was canceled. + fn send_cancel( + &self, + browser: Option, + frame: Option, + context_id: i32, + request_id: i32, + ) -> bool { + debug_assert_ne!(currently_on(ThreadId::RENDERER), 0); + let (Some(browser_id), Some(frame), Ok(mut browser_request_info_map)) = ( + browser.as_ref().map(Browser::identifier), + frame, + self.browser_request_info_map.lock(), + ) else { + return false; + }; + + let cancel_count = if request_id != RESERVED_ID { + // Cancel a single request. + if self + .get_request_info(browser_id, context_id, request_id, true) + .is_some() + { + 1 + } else { + 0 + } + } else { + // Cancel all requests with the specified context ID. + struct Visitor { + context_id: i32, + cancel_count: AtomicUsize, + } + + impl BrowserInfoMapVisitor<(i32, i32), RendererSideRequestInfo> for Visitor { + fn on_next_info( + &self, + _browser_id: i32, + key: (i32, i32), + _value: &RendererSideRequestInfo, + ) -> ControlFlow + { + ControlFlow::Continue(if self.context_id == key.0 { + self.cancel_count.fetch_add(1, Ordering::Relaxed); + BrowserInfoMapVisitorResult::RemoveEntry + } else { + BrowserInfoMapVisitorResult::KeepEntry + }) + } + } + + let visitor = Visitor { + context_id, + cancel_count: Default::default(), + }; + + browser_request_info_map.find_browser_all(browser_id, &visitor); + visitor.cancel_count.load(Ordering::Relaxed) + }; + + if cancel_count > 0 { + let mut message = + process_message_create(Some(&CefString::from(self.cancel_message_name.as_str()))); + if let Some(args) = message.as_ref().and_then(ProcessMessage::argument_list) { + args.set_int(0, context_id); + args.set_int(1, request_id); + + frame.send_process_message(ProcessId::BROWSER, message.as_mut()); + } + + true + } else { + false + } + } + + fn execute_success_callback( + &self, + browser_id: i32, + context_id: i32, + request_id: i32, + response: mru::MessagePayload, + ) { + debug_assert_ne!(currently_on(ThreadId::RENDERER), 0); + let Some(success_callback) = self + .get_request_info(browser_id, context_id, request_id, false) + .and_then(|info| info.success_callback) + else { + return; + }; + + let Some(mut context) = self.get_context_by_id(context_id) else { + return; + }; + if context.enter() == 0 { + return; + } + + let data = match &response { + mru::MessagePayload::Empty => &[], + mru::MessagePayload::String(s) => s.as_slice().unwrap_or(&[]), + mru::MessagePayload::Binary(b) => b.data(), + }; + + #[cfg(feature = "sandbox")] + let value = v8_value_create_array_buffer_with_copy(data.as_ptr() as *mut u8, data.len()); + #[cfg(not(feature = "sandbox"))] + let value = v8_value_create_array_buffer( + data.as_ptr() as *mut u8, + data.len(), + Some(&mut mru::BinaryValueArrayBufferReleaseCallback::new( + response, + )), + ); + + context.exit(); + + success_callback.execute_function_with_context(Some(&mut context), None, Some(&[value])); + } + + fn execute_failure_callback( + &self, + browser_id: i32, + context_id: i32, + request_id: i32, + error_code: i32, + error_message: &str, + ) { + debug_assert_ne!(currently_on(ThreadId::RENDERER), 0); + let Some(failure_callback) = self + .get_request_info(browser_id, context_id, request_id, true) + .and_then(|info| info.failure_callback) + else { + return; + }; + + let Some(mut context) = self.get_context_by_id(context_id) else { + return; + }; + + failure_callback.execute_function_with_context( + Some(&mut context), + None, + Some(&[ + v8_value_create_int(error_code), + v8_value_create_string(Some(&CefString::from(error_message))), + ]), + ); + } + + fn create_id_for_context(&self, context: V8Context) -> i32 { + debug_assert_ne!(currently_on(ThreadId::RENDERER), 0); + + // The context should not already have an associated ID. + debug_assert_eq!(self.get_id_for_context(context.clone(), false), RESERVED_ID); + + let (Ok(mut context_id_generator), Ok(mut context_map)) = + (self.context_id_generator.lock(), self.context_map.lock()) + else { + return RESERVED_ID; + }; + let context_id = context_id_generator.next(); + context_map.insert(context_id, Some(context)); + context_id + } + + fn get_id_for_context(&self, mut context: V8Context, remove: bool) -> i32 { + debug_assert_ne!(currently_on(ThreadId::RENDERER), 0); + let Ok(mut context_map) = self.context_map.lock() else { + return RESERVED_ID; + }; + let context_id = context_map + .iter() + .find(|(_, entry)| { + entry + .as_ref() + .map_or(0, |entry| entry.is_same(Some(&mut context))) + != 0 + }) + .map(|(context_id, _)| *context_id); + + if let Some(context_id) = context_id { + if remove { + context_map.remove(&context_id); + } + context_id + } else { + RESERVED_ID + } + } + + fn get_context_by_id(&self, context_id: i32) -> Option { + debug_assert_ne!(currently_on(ThreadId::RENDERER), 0); + let context_map = self.context_map.lock().ok()?; + context_map.get(&context_id)?.clone() + } +} + +impl MessageRouterRendererSide for RendererSideRouter { + /// Create a new router with the specified configuration. + fn new(config: MessageRouterConfig) -> Arc { + let query_message_name = format!("{}{MESSAGE_SUFFIX}", config.js_query_function); + let cancel_message_name = format!("{}{MESSAGE_SUFFIX}", config.js_cancel_function); + Arc::new_cyclic(|weak_self| Self { + weak_self: weak_self.clone(), + config, + query_message_name, + cancel_message_name, + context_id_generator: Mutex::new(IdGenerator::new(i32::MAX)), + request_id_generator: Mutex::new(IdGenerator::new(i32::MAX)), + browser_request_info_map: Default::default(), + context_map: Default::default(), + }) + } + + /// Returns the number of queries currently pending for the specified + /// |browser| and/or |context|. Either or both values may be empty. + fn pending_count(&self, browser: Option, context: Option) -> usize { + debug_assert_ne!(currently_on(ThreadId::RENDERER), 0); + let Ok(mut browser_request_info_map) = self.browser_request_info_map.lock() else { + return 0; + }; + if browser_request_info_map.is_empty() { + return 0; + } + + if let Some(context) = context { + let context_id = self.get_id_for_context(context, false); + if context_id == RESERVED_ID { + // Nothing associated with the specified context. + return 0; + } + + // Need to iterate over each RequestInfo object to test the context. + struct Visitor { + context_id: i32, + count: AtomicUsize, + } + + impl BrowserInfoMapVisitor<(i32, i32), RendererSideRequestInfo> for Visitor { + fn on_next_info( + &self, + _browser_id: i32, + key: (i32, i32), + _value: &RendererSideRequestInfo, + ) -> ControlFlow + { + if key.0 == self.context_id { + self.count.fetch_add(1, Ordering::Relaxed); + } + ControlFlow::Continue(BrowserInfoMapVisitorResult::KeepEntry) + } + } + + let visitor = Visitor { + context_id, + count: Default::default(), + }; + + if let Some(browser) = browser { + browser_request_info_map.find_browser_all(browser.identifier(), &visitor); + } else { + browser_request_info_map.find_all(&visitor); + } + + visitor.count.load(Ordering::Relaxed) + } else if let Some(browser) = browser { + browser_request_info_map.browser_len(browser.identifier()) + } else { + browser_request_info_map.len() + } + } +} + +impl MessageRouterRendererSideHandlerCallbacks for RendererSideRouter { + /// Call from CefRenderProcessHandler::OnContextCreated. Registers the + /// JavaScripts functions with the new context. + fn on_context_created( + &self, + _browser: Option, + _frame: Option, + context: Option, + ) { + debug_assert_ne!(currently_on(ThreadId::RENDERER), 0); + + // Register function handlers with the 'window' object. + let Some(window) = context.and_then(|context| context.global()) else { + return; + }; + + let mut handler = RendererSideV8Handler::new( + self.weak_self.clone(), + self.config.clone(), + Default::default(), + ); + let attributes = sys::cef_v8_propertyattribute_t( + [ + sys::cef_v8_propertyattribute_t::V8_PROPERTY_ATTRIBUTE_READONLY, + sys::cef_v8_propertyattribute_t::V8_PROPERTY_ATTRIBUTE_DONTENUM, + sys::cef_v8_propertyattribute_t::V8_PROPERTY_ATTRIBUTE_DONTDELETE, + ] + .into_iter() + .fold(0, |acc, attr| acc | attr.0), + ) + .into(); + + // Add the query function. + let name = CefString::from(self.config.js_query_function.as_str()); + let mut query_func = v8_value_create_function(Some(&name), Some(&mut handler)); + window.set_value_bykey(Some(&name), query_func.as_mut(), attributes); + + // Add the cancel function. + let name = CefString::from(self.config.js_cancel_function.as_str()); + let mut cancel_func = v8_value_create_function(Some(&name), Some(&mut handler)); + window.set_value_bykey(Some(&name), cancel_func.as_mut(), attributes); + } + + /// Call from CefRenderProcessHandler::OnContextReleased. Any pending queries + /// associated with the released context will be canceled and + /// Handler::OnQueryCanceled will be called in the browser process. + fn on_context_released( + &self, + browser: Option, + frame: Option, + context: Option, + ) { + debug_assert_ne!(currently_on(ThreadId::RENDERER), 0); + + // Get the context ID and remove the context from the map. + let context_id = context + .map(|context| self.get_id_for_context(context, true)) + .unwrap_or(RESERVED_ID); + if context_id != RESERVED_ID { + // Cancel all pending requests for the context. + self.send_cancel(browser, frame, context_id, RESERVED_ID); + } + } + + /// Call from CefRenderProcessHandler::OnProcessMessageReceived. Returns true + /// if the message is handled by this router or false otherwise. + fn on_process_message_received( + &self, + browser: Option, + _frame: Option, + _source_process: Option, + message: Option, + ) -> bool { + debug_assert_ne!(currently_on(ThreadId::RENDERER), 0); + let (Some(browser), Some(message)) = (browser, message) else { + return true; + }; + + let name = CefString::from(&message.name()).to_string(); + if name != self.query_message_name { + return false; + } + + let content = mru::BrowserMessage::from(Some(message)); + if content.is_success { + self.execute_success_callback( + browser.identifier(), + content.context_id, + content.request_id, + content.payload, + ); + } else { + let error_message = match content.payload { + mru::MessagePayload::String(s) => s.to_string(), + _ => Default::default(), + }; + self.execute_failure_callback( + browser.identifier(), + content.context_id, + content.request_id, + content.error_code, + error_message.as_str(), + ); + } + + true + } +} + +wrap_v8_handler! { + struct RendererSideV8Handler { + router: Weak, + config: MessageRouterConfig, + context_id: OnceLock, + } + + impl V8Handler { + fn execute( + &self, + name: Option<&CefString>, + _object: Option<&mut V8Value>, + arguments: Option<&[Option]>, + retval: Option<&mut Option>, + exception: Option<&mut CefString>, + ) -> i32 { + macro_rules! return_exception { + ($message:expr) => { + if let Some(exception) = exception { + *exception = CefString::from($message); + } + + return 1; + }; + } + + let Some(name) = name else { + return_exception!("Missing function name"); + }; + let name = name.to_string(); + if name == self.config.js_query_function { + let Some(arg) = arguments + .filter(|arguments| arguments.len() == 1) + .and_then(|arguments| arguments[0].as_ref()) + .filter(|arg| arg.is_object() != 0) + else { + return_exception!("Invalid arguments; expecting a single object"); + }; + + let key = CefString::from(ObjectMember::REQUEST); + let Some(request) = arg.value_bykey(Some(&key)) else { + return_exception!(format!( + "Invalid arguments; object member '{}' is required", + ObjectMember::REQUEST + ) + .as_str()); + }; + if request.is_string() == 0 && request.is_array_buffer() == 0 { + return_exception!(format!("Invalid arguments; object member '{}' must have type string or ArrayBuffer", ObjectMember::REQUEST).as_str()); + } + + let key = CefString::from(ObjectMember::ON_SUCCESS); + let success = if let Some(success) = arg.value_bykey(Some(&key)) { + if success.is_function() == 0 { + return_exception!(format!( + "Invalid arguments; object member '{}' must have type function", + ObjectMember::ON_SUCCESS + ) + .as_str()); + } + Some(success) + } else { + None + }; + + let key = CefString::from(ObjectMember::ON_FAILURE); + let failure = if let Some(failure) = arg.value_bykey(Some(&key)) { + if failure.is_function() == 0 { + return_exception!(format!( + "Invalid arguments; object member '{}' must have type function", + ObjectMember::ON_FAILURE + ) + .as_str()); + } + Some(failure) + } else { + None + }; + + let key = CefString::from(ObjectMember::PERSISTENT); + let persistent = if let Some(persistent) = arg.value_bykey(Some(&key)) { + if persistent.is_bool() == 0 { + return_exception!(format!( + "Invalid arguments; object member '{}' must have type boolean", + ObjectMember::PERSISTENT + ) + .as_str()); + } + Some(persistent) + } else { + None + }; + + if let (Some(router), Some(context)) = + (self.router.upgrade(), v8_context_get_current_context()) + { + let context_id = self.get_id_for_context(context.clone()); + let persistent = persistent.map_or(0, |value| value.bool_value()) != 0; + let request_id = router.send_query( + context.browser(), + context.frame(), + context_id, + request, + RendererSideRequestInfo { + is_persistent: persistent, + success_callback: success, + failure_callback: failure, + }, + ); + + if let Some(retval) = retval { + *retval = v8_value_create_int(request_id); + } + return 1; + } + } else if name == self.config.js_cancel_function { + let Some(arg) = arguments + .filter(|arguments| arguments.len() == 1) + .and_then(|arguments| arguments[0].as_ref()) + .filter(|arg| arg.is_int() != 0) + else { + return_exception!("Invalid arguments; expecting a single integer"); + }; + + let request_id = arg.int_value(); + if request_id != RESERVED_ID { + if let (Some(router), Some(context)) = + (self.router.upgrade(), v8_context_get_current_context()) + { + let context_id = self.get_id_for_context(context.clone()); + let result = router.send_cancel( + context.browser(), + context.frame(), + context_id, + request_id, + ); + + if let Some(retval) = retval { + *retval = v8_value_create_bool(result.into()); + } + return 1; + } + } + } + + 0 + } + } +} + +impl RendererSideV8Handler { + fn get_id_for_context(&self, context: V8Context) -> i32 { + *self.context_id.get_or_init(|| { + let Some(router) = self.router.upgrade() else { + return RESERVED_ID; + }; + router.create_id_for_context(context) + }) + } +} diff --git a/cef/src/wrapper/message_router_utils.rs b/cef/src/wrapper/message_router_utils.rs new file mode 100644 index 0000000..369c846 --- /dev/null +++ b/cef/src/wrapper/message_router_utils.rs @@ -0,0 +1,726 @@ +use super::message_router::*; +use crate::*; +use std::{ + io::{self, Cursor, Read, Write}, + marker::PhantomData, + mem, + rc::Rc, + slice, + sync::Arc, +}; + +const NO_ERROR: i32 = 0; + +const CONTEXT_ID: usize = 0; +const REQUEST_ID: usize = 1; +const RENDERER_PAYLOAD: usize = 2; +const IS_SUCCESS: usize = 2; +const BROWSER_PAYLOAD: usize = 3; +const IS_PERSISTENT: usize = 3; + +pub trait ProcessMessageBuilder { + fn build_browser_response(&self, context_id: i32, request_id: i32) -> Option; + fn build_renderer_message( + &self, + context_id: i32, + request_id: i32, + persistent: bool, + ) -> Option; +} + +#[derive(Clone, Default)] +pub enum MessagePayload { + #[default] + Empty, + String(CefStringUtf8), + Binary(Arc), +} + +impl MessagePayload { + fn size(&self) -> usize { + match self { + Self::Empty => 0, + Self::String(s) => s.as_slice().map(|s| s.len()).unwrap_or(0), + Self::Binary(b) => b.data().len(), + } + } + + fn read_string(f: &mut R) -> io::Result { + let mut buffer = vec![]; + f.read_to_end(&mut buffer)?; + Ok(String::from_utf8(buffer) + .map(|value| value.as_str().into()) + .unwrap_or(Self::Empty)) + } + + // fn read_binary(f: &mut R) -> io::Result { + // let mut buffer = vec![]; + // f.read_to_end(&mut buffer)?; + // Ok(binary_value_create(Some(&buffer)) + // .map(|value| Self::Binary(Arc::new(BinaryValueBuffer::new(None, Some(value))))) + // .unwrap_or(Self::Empty)) + // } + + fn write(&self, f: &mut W) -> io::Result<()> { + let buffer = match self { + Self::String(s) => s.as_slice(), + Self::Binary(b) if !b.data().is_empty() => Some(b.data()), + _ => None, + }; + if let Some(buffer) = buffer { + f.write_all(buffer)?; + } + Ok(()) + } +} + +impl From<&CefString> for MessagePayload { + fn from(s: &CefString) -> Self { + Self::String(CefStringUtf8::from(s)) + } +} + +impl From<&str> for MessagePayload { + fn from(s: &str) -> Self { + Self::String(CefStringUtf8::from(s)) + } +} + +impl From> for MessagePayload { + fn from(v: Option<&V8Value>) -> Self { + let b = v.and_then(|v| { + let ptr = v.array_buffer_data(); + let size = v.array_buffer_byte_length(); + if ptr.is_null() || size == 0 { + return None; + } + + let data = unsafe { slice::from_raw_parts(ptr as *const u8, size) }; + binary_value_create(Some(data)) + }); + b.map(|value| Self::Binary(Arc::new(BinaryValueBuffer::new(None, Some(value))))) + .unwrap_or(Self::Empty) + } +} + +impl From<&[u8]> for MessagePayload { + fn from(data: &[u8]) -> Self { + if data.is_empty() { + Self::Empty + } else { + Self::Binary(Arc::new(BinaryValueBuffer::new( + None, + binary_value_create(Some(data)), + ))) + } + } +} + +#[derive(Clone, Default)] +pub struct BrowserMessage { + pub context_id: i32, + pub request_id: i32, + pub is_success: bool, + pub error_code: i32, + pub payload: MessagePayload, +} + +impl From> for BrowserMessage { + fn from(message: Option) -> Self { + let Some(message) = message else { + return Default::default(); + }; + + if let Some(args) = message.argument_list() { + let context_id = args.int(CONTEXT_ID); + let request_id = args.int(REQUEST_ID); + let is_success = args.bool(IS_SUCCESS); + + if is_success != 0 { + debug_assert_eq!(args.size(), 4); + match args.get_type(BROWSER_PAYLOAD) { + ValueType::STRING => Self { + context_id, + request_id, + is_success: true, + error_code: NO_ERROR, + payload: MessagePayload::String(CefStringUtf8::from(&CefString::from( + &args.string(BROWSER_PAYLOAD), + ))), + }, + ValueType::BINARY => Self { + context_id, + request_id, + is_success: true, + error_code: NO_ERROR, + payload: MessagePayload::Binary(Arc::new(BinaryValueBuffer::new( + Some(message), + args.binary(BROWSER_PAYLOAD), + ))), + }, + payload_type => { + assert_eq!(payload_type, ValueType::NULL); + Self { + context_id, + request_id, + is_success: true, + error_code: NO_ERROR, + payload: MessagePayload::Binary(Arc::new(EmptyBinaryBuffer)), + } + } + } + } else { + debug_assert_eq!(args.size(), 5); + Self { + context_id, + request_id, + is_success: false, + error_code: args.int(3), + payload: MessagePayload::String(CefStringUtf8::from(&CefString::from( + &args.string(4), + ))), + } + } + } else if let Some(region) = message + .shared_memory_region() + .filter(|region| region.is_valid() != 0) + { + debug_assert!(region.size() >= BrowserMessageHeader::SIZE); + debug_assert!(!region.memory().is_null()); + let buffer = + unsafe { slice::from_raw_parts(region.memory() as *const u8, region.size()) }; + let header = { + let mut cursor = Cursor::new(&buffer[..BrowserMessageHeader::SIZE]); + BrowserMessageHeader::read(&mut cursor).unwrap_or_default() + }; + if header.is_binary { + Self { + context_id: header.context_id, + request_id: header.request_id, + is_success: true, + error_code: NO_ERROR, + payload: MessagePayload::Binary(Arc::new(SharedMemoryRegionBuffer::new( + Some(region), + BrowserMessageHeader::SIZE, + ))), + } + } else { + let mut cursor = Cursor::new(&buffer[BrowserMessageHeader::SIZE..]); + Self { + context_id: header.context_id, + request_id: header.request_id, + is_success: true, + error_code: NO_ERROR, + payload: MessagePayload::read_string(&mut cursor).unwrap_or_default(), + } + } + } else { + Default::default() + } + } +} + +#[derive(Clone, Default)] +pub struct RenderMessage { + pub context_id: i32, + pub request_id: i32, + pub is_persistent: bool, + pub payload: MessagePayload, +} + +impl From> for RenderMessage { + fn from(message: Option) -> Self { + let Some(message) = message else { + return Default::default(); + }; + + if let Some(args) = message.argument_list() { + debug_assert_eq!(args.size(), 4); + let context_id = args.int(CONTEXT_ID); + let request_id = args.int(REQUEST_ID); + let is_persistent = args.bool(IS_PERSISTENT) != 0; + + match args.get_type(RENDERER_PAYLOAD) { + ValueType::STRING => Self { + context_id, + request_id, + is_persistent, + payload: MessagePayload::String(CefStringUtf8::from(&CefString::from( + &args.string(RENDERER_PAYLOAD), + ))), + }, + ValueType::BINARY => Self { + context_id, + request_id, + is_persistent, + payload: MessagePayload::Binary(Arc::new(BinaryValueBuffer::new( + Some(message), + args.binary(RENDERER_PAYLOAD), + ))), + }, + payload_type => { + debug_assert_eq!(payload_type, ValueType::NULL); + Self { + context_id, + request_id, + is_persistent, + payload: MessagePayload::Binary(Arc::new(EmptyBinaryBuffer)), + } + } + } + } else if let Some(region) = message + .shared_memory_region() + .filter(|region| region.is_valid() != 0) + { + debug_assert!(region.size() >= RendererMessageHeader::::SIZE); + debug_assert!(!region.memory().is_null()); + let buffer = + unsafe { slice::from_raw_parts(region.memory() as *const u8, region.size()) }; + let header = { + let mut cursor = Cursor::new(&buffer[..RendererMessageHeader::::SIZE]); + RendererMessageHeader::::read(&mut cursor).unwrap_or_default() + }; + if header.is_binary { + Self { + context_id: header.context_id, + request_id: header.request_id, + is_persistent: header.is_persistent, + payload: MessagePayload::Binary(Arc::new(SharedMemoryRegionBuffer::new( + Some(region), + RendererMessageHeader::::SIZE, + ))), + } + } else { + let mut cursor = Cursor::new(&buffer[RendererMessageHeader::::SIZE..]); + Self { + context_id: header.context_id, + request_id: header.request_id, + is_persistent: header.is_persistent, + payload: MessagePayload::read_string(&mut cursor).unwrap_or_default(), + } + } + } else { + Default::default() + } + } +} + +#[cfg(not(feature = "sandbox"))] +wrap_v8_array_buffer_release_callback! { + pub struct BinaryValueArrayBufferReleaseCallback { + value: MessagePayload, + } + + impl V8ArrayBufferReleaseCallback { + fn release_buffer(&self, _buffer: *mut u8) {} + } +} + +trait MessageHeader: Sized { + const SIZE: usize; + + fn new(context_id: i32, request_id: i32, is_binary: bool) -> Self; + fn read(f: &mut R) -> io::Result; + fn write(&self, f: &mut W) -> io::Result<()>; +} + +#[derive(Clone, Default)] +struct BrowserMessageHeader { + context_id: i32, + request_id: i32, + is_binary: bool, +} + +impl MessageHeader for BrowserMessageHeader { + const SIZE: usize = mem::size_of::() * 2 + 1; + + fn new(context_id: i32, request_id: i32, is_binary: bool) -> Self { + Self { + context_id, + request_id, + is_binary, + } + } + + fn read(f: &mut R) -> io::Result { + let mut data = [0_u8; mem::size_of::()]; + f.read_exact(&mut data)?; + let context_id = i32::from_ne_bytes(data); + f.read_exact(&mut data)?; + let request_id = i32::from_ne_bytes(data); + let mut flags = [0_u8]; + f.read_exact(&mut flags)?; + let is_binary = flags[0] != 0; + + Ok(Self { + context_id, + request_id, + is_binary, + }) + } + + fn write(&self, f: &mut W) -> io::Result<()> { + f.write_all(&self.context_id.to_ne_bytes())?; + f.write_all(&self.request_id.to_ne_bytes())?; + f.write_all(&[self.is_binary.into()]) + } +} + +#[derive(Clone, Default)] +struct RendererMessageHeader { + context_id: i32, + request_id: i32, + is_persistent: bool, + is_binary: bool, +} + +impl MessageHeader for RendererMessageHeader { + const SIZE: usize = mem::size_of::() * 2 + 2; + + fn new(context_id: i32, request_id: i32, is_binary: bool) -> Self { + Self { + context_id, + request_id, + is_persistent: DEFAULT_PERSISTENT, + is_binary, + } + } + + fn read(f: &mut R) -> io::Result { + let mut data = [0_u8; mem::size_of::()]; + f.read_exact(&mut data)?; + let context_id = i32::from_ne_bytes(data); + f.read_exact(&mut data)?; + let request_id = i32::from_ne_bytes(data); + let mut flags = [0_u8; 2]; + f.read_exact(&mut flags)?; + let is_persistent = flags[0] != 0; + let is_binary = flags[1] != 0; + + Ok(Self { + context_id, + request_id, + is_persistent, + is_binary, + }) + } + + fn write(&self, f: &mut W) -> io::Result<()> { + f.write_all(&self.context_id.to_ne_bytes())?; + f.write_all(&self.request_id.to_ne_bytes())?; + f.write_all(&[self.is_persistent.into()])?; + f.write_all(&[self.is_binary.into()]) + } +} + +fn build_browser_list_message( + name: &str, + context_id: i32, + request_id: i32, + payload: MessagePayload, +) -> Option { + let message = process_message_create(Some(&CefString::from(name)))?; + let args = message.argument_list()?; + args.set_int(CONTEXT_ID, context_id); + args.set_int(REQUEST_ID, request_id); + args.set_bool(IS_SUCCESS, 1); + + match payload { + MessagePayload::Empty => args.set_null(BROWSER_PAYLOAD), + MessagePayload::String(value) => { + args.set_string(BROWSER_PAYLOAD, Some(&CefString::from(&value))) + } + MessagePayload::Binary(value) => args.set_binary( + BROWSER_PAYLOAD, + binary_value_create(Some(value.data())).as_mut(), + ), + }; + + Some(message) +} + +fn build_render_list_message( + name: &str, + context_id: i32, + request_id: i32, + payload: MessagePayload, + is_persistent: bool, +) -> Option { + let message = process_message_create(Some(&CefString::from(name)))?; + let args = message.argument_list()?; + args.set_int(CONTEXT_ID, context_id); + args.set_int(REQUEST_ID, request_id); + + match payload { + MessagePayload::Empty => args.set_null(RENDERER_PAYLOAD), + MessagePayload::String(value) => { + args.set_string(RENDERER_PAYLOAD, Some(&CefString::from(&value))) + } + MessagePayload::Binary(value) => args.set_binary( + RENDERER_PAYLOAD, + binary_value_create(Some(value.data())).as_mut(), + ), + }; + + args.set_bool(IS_PERSISTENT, is_persistent.into()); + + Some(message) +} + +struct MessagePayloadBuilder { + pub name: String, + pub payload: MessagePayload, +} + +impl ProcessMessageBuilder for MessagePayloadBuilder { + fn build_browser_response(&self, context_id: i32, request_id: i32) -> Option { + build_browser_list_message(&self.name, context_id, request_id, self.payload.clone()) + } + + fn build_renderer_message( + &self, + context_id: i32, + request_id: i32, + persistent: bool, + ) -> Option { + build_render_list_message( + &self.name, + context_id, + request_id, + self.payload.clone(), + persistent, + ) + } +} + +enum SharedProcessMessageRouter
+where + Header: MessageHeader, +{ + SharedMemory { + builder: SharedProcessMessageBuilder, + is_binary: bool, + _phantom: PhantomData
, + }, + Payload(MessagePayloadBuilder), +} + +impl
SharedProcessMessageRouter
+where + Header: MessageHeader, +{ + fn new(name: &str, payload: MessagePayload) -> Self { + let message_size = Header::SIZE + payload.size(); + let builder = + match shared_process_message_builder_create(Some(&CefString::from(name)), message_size) + { + Some(builder) if builder.is_valid() != 0 => builder, + _ => { + return Self::Payload(MessagePayloadBuilder { + name: name.to_owned(), + payload, + }) + } + }; + + let buffer = + unsafe { slice::from_raw_parts_mut(builder.memory() as *mut u8, message_size) }; + let mut cursor = Cursor::new(&mut buffer[Header::SIZE..]); + if payload.write(&mut cursor).is_err() { + return Self::Payload(MessagePayloadBuilder { + name: name.to_owned(), + payload, + }); + } + + Self::SharedMemory { + builder, + is_binary: matches!(payload, MessagePayload::Binary(_)), + _phantom: PhantomData, + } + } +} + +impl
ProcessMessageBuilder for SharedProcessMessageRouter
+where + Header: MessageHeader, +{ + fn build_browser_response(&self, context_id: i32, request_id: i32) -> Option { + match self { + Self::SharedMemory { + builder, is_binary, .. + } => { + let buffer = + unsafe { slice::from_raw_parts_mut(builder.memory() as *mut u8, Header::SIZE) }; + let mut cursor = Cursor::new(buffer); + Header::new(context_id, request_id, *is_binary) + .write(&mut cursor) + .ok()?; + + builder.build() + } + Self::Payload(builder) => builder.build_browser_response(context_id, request_id), + } + } + + fn build_renderer_message( + &self, + context_id: i32, + request_id: i32, + persistent: bool, + ) -> Option { + match self { + Self::SharedMemory { + builder, is_binary, .. + } => { + let buffer = + unsafe { slice::from_raw_parts_mut(builder.memory() as *mut u8, Header::SIZE) }; + let mut cursor = Cursor::new(buffer); + Header::new(context_id, request_id, *is_binary) + .write(&mut cursor) + .ok()?; + + builder.build() + } + Self::Payload(builder) => { + builder.build_renderer_message(context_id, request_id, persistent) + } + } + } +} + +pub struct EmptyBinaryBuffer; + +impl BinaryBuffer for EmptyBinaryBuffer { + fn data(&self) -> &[u8] { + &[] + } + + fn data_mut(&mut self) -> &mut [u8] { + &mut [] + } +} + +pub struct BinaryValueBuffer { + _message: Option, + value: Option, +} + +impl BinaryValueBuffer { + pub fn new(message: Option, value: Option) -> Self { + Self { + _message: message, + value, + } + } +} + +impl BinaryBuffer for BinaryValueBuffer { + fn data(&self) -> &[u8] { + self.value.as_ref().map_or(&[], |v| unsafe { + slice::from_raw_parts(v.raw_data() as *const u8, v.size()) + }) + } + + fn data_mut(&mut self) -> &mut [u8] { + self.value.as_mut().map_or(&mut [], |v| unsafe { + slice::from_raw_parts_mut(v.raw_data() as *mut u8, v.size()) + }) + } +} + +struct SharedMemoryRegionBuffer { + region: Option, + offset: usize, +} + +impl SharedMemoryRegionBuffer { + fn new(region: Option, offset: usize) -> Self { + Self { region, offset } + } + + fn data(&self) -> *mut u8 { + self.region + .as_ref() + .map_or(std::ptr::null_mut(), |r| unsafe { + r.memory().add(self.offset) as *mut u8 + }) + } + + fn size(&self) -> usize { + self.region + .as_ref() + .map_or(0, |r| r.size().saturating_sub(self.offset)) + } +} + +impl BinaryBuffer for SharedMemoryRegionBuffer { + fn data(&self) -> &[u8] { + let data = self.data(); + let size = self.size(); + if data.is_null() || size == 0 { + &[] + } else { + unsafe { slice::from_raw_parts(data as *const u8, size) } + } + } + + fn data_mut(&mut self) -> &mut [u8] { + let data = self.data(); + let size = self.size(); + if data.is_null() || size == 0 { + &mut [] + } else { + unsafe { slice::from_raw_parts_mut(data, size) } + } + } +} + +pub fn create_browser_response_builder( + threshold: usize, + name: &str, + payload: MessagePayload, +) -> Rc { + if payload.size() < threshold { + Rc::new(MessagePayloadBuilder { + name: name.to_string(), + payload, + }) + } else { + Rc::new(SharedProcessMessageRouter::::new( + name, payload, + )) + } +} + +pub fn build_renderer_message( + threshold: usize, + name: &str, + context_id: i32, + request_id: i32, + request: Option<&V8Value>, + persistent: bool, +) -> Option { + let payload = request + .map(|request| { + if request.is_string() != 0 { + MessagePayload::from(&CefString::from(&request.string_value())) + } else { + MessagePayload::from(Some(request)) + } + }) + .unwrap_or(MessagePayload::Empty); + + let builder: Box = if payload.size() < threshold { + Box::new(MessagePayloadBuilder { + name: name.to_string(), + payload, + }) + } else if persistent { + Box::new(SharedProcessMessageRouter::>::new(name, payload)) + } else { + Box::new(SharedProcessMessageRouter::>::new(name, payload)) + }; + + builder.build_renderer_message(context_id, request_id, persistent) +} diff --git a/cef/src/wrapper/mod.rs b/cef/src/wrapper/mod.rs new file mode 100644 index 0000000..893a9aa --- /dev/null +++ b/cef/src/wrapper/mod.rs @@ -0,0 +1,8 @@ +pub mod browser_info_map; +pub mod byte_read_handler; +pub mod message_router; +pub mod resource_manager; +pub mod stream_resource_handler; +pub mod zip_archive; + +mod message_router_utils; diff --git a/cef/src/wrapper/resource_manager.rs b/cef/src/wrapper/resource_manager.rs new file mode 100644 index 0000000..e5ccb99 --- /dev/null +++ b/cef/src/wrapper/resource_manager.rs @@ -0,0 +1,1155 @@ +use super::{stream_resource_handler::StreamResourceHandler, zip_archive::*}; +use crate::*; +use std::{ + collections::{BTreeMap, VecDeque}, + path::PathBuf, + sync::{Arc, Mutex, Weak}, +}; + +pub type UrlFilter = Box String>; +pub type MimeTypeResolver = Box String>; + +struct RequestParams { + url: String, + browser: Browser, + frame: Frame, + request: Request, + url_filter: Arc, + mime_type_resolver: Arc, +} + +#[derive(Default)] +struct ProviderEntry { + provider: Option>, + order: i32, + identifier: String, + pending_requests: VecDeque>>, + deletion_pending: bool, +} + +/// Values associated with the pending request only. Ownership will be passed +/// between requests and the resource manager as request handling proceeds. +struct RequestState { + manager: Weak>, + + /// Callback to execute once request handling is complete. + callback: Option, + + /// Position of the currently associated [ProviderEntry] in the `ResourceManagerProviders` + /// list. + current_entry: usize, + + /// Position of this request object in the currently associated + /// [ProviderEntry]'s `pending_requests` list. + current_request: usize, + + /// Params that will be copied to each request object. + params: Arc, +} + +impl Drop for RequestState { + fn drop(&mut self) { + // Always execute the callback. + if let Some(callback) = self.callback.take() { + callback.cont(); + } + } +} + +pub struct ResourceManagerRequest { + weak_self: Weak>, + state: Option, + params: Arc, +} + +impl ResourceManagerRequest { + fn new(state: Option, params: Arc) -> Arc> { + Arc::new_cyclic(|weak_self| { + Mutex::new(Self { + weak_self: weak_self.clone(), + state, + params, + }) + }) + } + + /// Returns the URL associated with this request. The returned value will be fully qualified + /// but will not contain query or fragment components. It will already have been passed through + /// the URL filter. + pub fn url(&self) -> &str { + &self.params.url + } + + /// Returns the [Browser] associated with this request. + pub fn browser(&self) -> &Browser { + &self.params.browser + } + + /// Returns the [Frame] associated with this request. + pub fn frame(&self) -> &Frame { + &self.params.frame + } + + /// Returns the [Request] associated with this request. + pub fn request(&self) -> &Request { + &self.params.request + } + + /// Returns the current URL filter. + pub fn url_filter(&self) -> &UrlFilter { + &self.params.url_filter + } + + /// Returns the current mime type resolver. + pub fn mime_type_resolver(&self) -> &MimeTypeResolver { + &self.params.mime_type_resolver + } + + pub fn continue_request(&mut self, handler: Option) { + // Disassociate `self.state` immediately so that [ResourceManagerProvider::on_request_canceled] + // is not called unexpectedly if [ResourceManagerProvider::on_request] calls this method + // and then calls [ResourceManager::remove]. + let Some(state) = self.state.take() else { + return; + }; + + let io_thread_id = ThreadId::IO; + let mut task = ContinueRequest::new(Arc::new(Mutex::new(Some(state))), handler); + post_task(io_thread_id, Some(&mut task)); + } + + pub fn stop_request(&mut self) { + // Disassociate `self.state` immediately so that [ResourceManagerProvider::on_request_canceled] + // is not called unexpectedly if [ResourceManagerProvider::on_request] calls this method + // and then calls [ResourceManager::remove]. + let Some(state) = self.state.take() else { + return; + }; + + let io_thread_id = ThreadId::IO; + let mut task = StopRequest::new(Arc::new(Mutex::new(Some(state)))); + post_task(io_thread_id, Some(&mut task)); + } + + /// Detaches and returns `self.state` if the provider indicates that it will not handle the + /// request. Note that `self.state` may already be [None] if [ResourceManagerProvider::on_request] + /// executes a callback before returning, in which case execution will continue asynchronously + /// in any case. + fn send_request(&mut self) -> Option { + debug_assert_ne!( + currently_on(ThreadId::IO), + 0, + "send_request must be called on the IO thread" + ); + + let state = self.state.as_ref()?; + let Some(manager) = state.manager.upgrade() else { + return self.state.take(); + }; + let Ok(mut manager) = manager.lock() else { + return self.state.take(); + }; + let Some(provider_entry) = manager.providers.get_mut(state.current_entry) else { + return self.state.take(); + }; + let Some(provider) = provider_entry.provider.as_mut() else { + return self.state.take(); + }; + + let Some(request) = self.weak_self.upgrade() else { + return self.state.take(); + }; + if !provider.on_request(request) { + return self.state.take(); + } + + None + } +} + +wrap_task! { + struct ContinueRequest { + state: Arc>>, + handler: Option, + } + + impl Task { + fn execute(&self) { + let Ok(mut state) = self.state.lock() else { + return; + }; + let Some(state) = state.take() else { + return; + }; + let Some(manager) = state.manager.upgrade() else { + return; + }; + let Ok(mut manager) = manager.lock() else { + return; + }; + + manager.continue_request(state, self.handler.clone()); + } + } +} + +wrap_task! { + struct StopRequest { + state: Arc>>, + } + + impl Task { + fn execute(&self) { + let Ok(mut state) = self.state.lock() else { + return; + }; + let Some(state) = state.take() else { + return; + }; + let Some(manager) = state.manager.upgrade() else { + return; + }; + let Ok(mut manager) = manager.lock() else { + return; + }; + + manager.stop_request(state); + } + } +} + +/// Interface implemented by resource [ResourceManagerProviders]. A [ResourceManagerProvider] may +/// be created on any thread but the methods will be called on, and the object will be destroyed +/// on, the browser process IO thread. +pub trait ResourceManagerProvider: Send { + /// Called to handle a request. If the ResourceManagerProvider knows immediately that it will + /// not handle the request return false. Otherwise, return true and call [ResourceManagerRequest::continue_request] + /// or [ResourceManagerRequest::stop_request] either in this method or asynchronously to + /// indicate completion. See comments on [ResourceManagerRequest] for additional usage + /// information. + fn on_request(&self, request: Arc>) -> bool; + + /// Called when a request has been canceled. It is still safe to dereference `request` but any + /// calls to [ResourceManagerRequest::continue_request] or [ResourceManagerRequest::stop_request] + /// will be ignored. + fn on_request_canceled(&self, _request: Arc>) {} +} + +/// Provider of fixed contents. +struct ContentProvider { + url: String, + content: String, + mime_type: String, +} + +impl ContentProvider { + fn new_resource_manager_provider( + url: &str, + content: &str, + mime_type: &str, + ) -> Box { + Box::new(Self { + url: url.to_string(), + content: content.to_string(), + mime_type: mime_type.to_string(), + }) + } +} + +impl ResourceManagerProvider for ContentProvider { + fn on_request(&self, request: Arc>) -> bool { + debug_assert_ne!( + currently_on(ThreadId::IO), + 0, + "on_request must be called on the IO thread" + ); + + let Ok(mut request) = request.lock() else { + return false; + }; + + if request.url() != self.url { + // Not handled by this provider. + return false; + } + + let mut data: Vec<_> = self.content.bytes().collect(); + let Some(stream) = stream_reader_create_for_data(data.as_mut_ptr(), data.len()) else { + return false; + }; + + let mime_type = if self.mime_type.is_empty() { + self.mime_type.clone() + } else { + (request.mime_type_resolver())(&self.url) + }; + + request.continue_request(Some(StreamResourceHandler::new_with_stream( + mime_type, stream, + ))); + true + } +} + +/// Provider of contents loaded from a directory on the file system. +struct DirectoryProvider { + url_path: String, + directory_path: PathBuf, +} + +impl DirectoryProvider { + fn new_resource_manager_provider( + url_path: &str, + directory_path: PathBuf, + ) -> Box { + Box::new(Self { + url_path: normalize_url_path(url_path), + directory_path, + }) + } +} + +impl ResourceManagerProvider for DirectoryProvider { + fn on_request(&self, request: Arc>) -> bool { + debug_assert_ne!( + currently_on(ThreadId::IO), + 0, + "on_request must be called on the IO thread" + ); + + let Some(file_path) = ({ + let Ok(request) = request.lock() else { + return false; + }; + let url = request.url(); + if !url.starts_with(self.url_path.as_str()) { + // Not handled by this provider. + return false; + } + + get_file_relative_path(self.directory_path.clone(), self.url_path.as_str(), url) + }) else { + return false; + }; + + let mut task = OpenFileRequest::new(file_path, request); + post_task(ThreadId::FILE_USER_BLOCKING, Some(&mut task)); + + true + } +} + +wrap_task! { + struct OpenFileRequest { + file_path: PathBuf, + request: Arc>, + } + + impl Task { + fn execute(&self) { + debug_assert_ne!( + currently_on(ThreadId::FILE_USER_BLOCKING), + 0, + "execute must be called on the file thread" + ); + + let file_path = self.file_path.display().to_string(); + let Some(stream) = + stream_reader_create_for_file(Some(&CefString::from(file_path.as_str()))) + else { + return; + }; + + // Continue loading on the IO thread. + let mut task = ContinueFileRequest::new(stream, self.request.clone()); + post_task(ThreadId::IO, Some(&mut task)); + } + } +} + +wrap_task! { + struct ContinueFileRequest { + stream: StreamReader, + request: Arc>, + } + + impl Task { + fn execute(&self) { + debug_assert_ne!( + currently_on(ThreadId::IO), + 0, + "execute must be called on the IO thread" + ); + + let Ok(mut request) = self.request.lock() else { + return; + }; + + let handler = StreamResourceHandler::new_with_stream( + (request.mime_type_resolver())(request.url()), + self.stream.clone(), + ); + request.continue_request(Some(handler)); + } + } +} + +struct ArchiveProviderState { + weak_self: Weak>, + url_path: String, + archive_path: PathBuf, + password: String, + load_started: bool, + load_ended: bool, + archive: Option, + pending_requests: Vec>>, +} + +impl ArchiveProviderState { + fn new(url_path: &str, archive_path: PathBuf, password: &str) -> Arc> { + Arc::new_cyclic(|weak_self| { + Mutex::new(Self { + weak_self: weak_self.clone(), + url_path: normalize_url_path(url_path), + archive_path, + password: password.to_string(), + load_started: false, + load_ended: false, + archive: None, + pending_requests: Default::default(), + }) + }) + } + + fn continue_request(&mut self, request: Arc>) -> bool { + debug_assert_ne!( + currently_on(ThreadId::IO), + 0, + "continue_request must be called on the IO thread" + ); + + let Ok(mut request) = request.lock() else { + return false; + }; + + let url = request.url(); + let (Some(archive), Some(relative_path)) = ( + self.archive.as_ref(), + get_file_relative_path(Default::default(), self.url_path.as_str(), url), + ) else { + return false; + }; + + let relative_path = relative_path.display().to_string(); + let Some(file) = archive.file(&relative_path) else { + return false; + }; + let Ok(file) = file.lock() else { + return false; + }; + let Some(stream) = file.stream_reader() else { + return false; + }; + + let handler = + StreamResourceHandler::new_with_stream((request.mime_type_resolver())(url), stream); + request.continue_request(Some(handler)); + true + } +} + +/// Provider of contents loaded from an archive file. +struct ArchiveProvider { + state: Arc>, +} + +impl ArchiveProvider { + fn new_resource_manager_provider( + url_path: &str, + archive_path: PathBuf, + password: &str, + ) -> Box { + Box::new(Self { + state: ArchiveProviderState::new(url_path, archive_path, password), + }) + } +} + +impl ResourceManagerProvider for ArchiveProvider { + fn on_request(&self, request: Arc>) -> bool { + debug_assert_ne!( + currently_on(ThreadId::IO), + 0, + "on_request must be called on the IO thread" + ); + + let (Ok(mut state), Ok(request_lock)) = (self.state.lock(), request.lock()) else { + return false; + }; + let url = request_lock.url(); + if !url.starts_with(state.url_path.as_str()) { + // Not handled by this provider. + return false; + } + + if !state.load_started { + // Initiate archive loading and queue the pending request. + state.load_started = true; + state.pending_requests.push(request.clone()); + + // Load the archive file on the FILE thread. + let mut task = OpenZipRequest::new(state.weak_self.clone()); + post_task(ThreadId::FILE_USER_BLOCKING, Some(&mut task)); + return true; + } + + if state.load_started && !state.load_ended { + // The archive load has already started. Queue the pending request. + state.pending_requests.push(request.clone()); + return true; + } + + // Archive loading is done. + state.continue_request(request.clone()) + } +} + +wrap_task! { + struct OpenZipRequest { + state: Weak>, + } + + impl Task { + fn execute(&self) { + debug_assert_ne!( + currently_on(ThreadId::FILE_USER_BLOCKING), + 0, + "execute must be called on the file thread" + ); + + let Some(state) = self.state.upgrade() else { + return; + }; + let Ok(mut state) = state.lock() else { + return; + }; + + let file_path = state.archive_path.display().to_string(); + let Some(mut stream) = + stream_reader_create_for_file(Some(&CefString::from(file_path.as_str()))) + else { + return; + }; + + let archive = ZipArchive::default(); + if archive.load(&mut stream, state.password.as_str(), true) == 0 { + return; + } + state.archive = Some(archive); + + // Continue loading on the IO thread. + let mut task = ContinueZipRequest::new(state.weak_self.clone()); + post_task(ThreadId::IO, Some(&mut task)); + } + } +} + +wrap_task! { + struct ContinueZipRequest { + state: Weak>, + } + + impl Task { + fn execute(&self) { + debug_assert_ne!( + currently_on(ThreadId::IO), + 0, + "execute must be called on the IO thread" + ); + + let Some(state) = self.state.upgrade() else { + return; + }; + let Ok(mut state) = state.lock() else { + return; + }; + + state.load_ended = true; + + for request in std::mem::take(&mut state.pending_requests) { + state.continue_request(request); + } + } + } +} + +wrap_task! { + struct AddProvider { + manager: Weak>, + provider: Arc>>>, + order: i32, + identifier: String, + } + + impl Task { + fn execute(&self) { + let Some(manager) = self.manager.upgrade() else { + return; + }; + let Ok(mut manager) = manager.lock() else { + return; + }; + let Ok(mut provider) = self.provider.lock() else { + return; + }; + let Some(provider) = provider.take() else { + return; + }; + + manager.add_provider(provider, self.order, &self.identifier); + } + } +} + +wrap_task! { + struct RemoveProviders { + manager: Weak>, + identifier: String, + } + + impl Task { + fn execute(&self) { + let Some(manager) = self.manager.upgrade() else { + return; + }; + let Ok(mut manager) = manager.lock() else { + return; + }; + + manager.remove_providers(&self.identifier); + } + } +} + +wrap_task! { + struct RemoveAllProviders { + manager: Weak>, + } + + impl Task { + fn execute(&self) { + let Some(manager) = self.manager.upgrade() else { + return; + }; + let Ok(mut manager) = manager.lock() else { + return; + }; + + manager.remove_all_providers(); + } + } +} + +wrap_task! { + struct SetUrlFilter { + manager: Weak>, + filter: Arc>>>, + } + + impl Task { + fn execute(&self) { + let Some(manager) = self.manager.upgrade() else { + return; + }; + let Ok(mut manager) = manager.lock() else { + return; + }; + let Ok(mut filter) = self.filter.lock() else { + return; + }; + let Some(filter) = filter.take() else { + return; + }; + + manager.set_url_filter(filter); + } + } +} + +wrap_task! { + struct SetMimeTypeResolver { + manager: Weak>, + resolver: Arc>>>, + } + + impl Task { + fn execute(&self) { + let Some(manager) = self.manager.upgrade() else { + return; + }; + let Ok(mut manager) = manager.lock() else { + return; + }; + let Ok(mut resolver) = self.resolver.lock() else { + return; + }; + let Some(resolver) = resolver.take() else { + return; + }; + + manager.set_mime_type_resolver(resolver); + } + } +} + +/// Type for managing multiple resource providers. For each resource request, providers will be +/// called in order and have the option to: +/// - (a) handle the request by returning a [ResourceHandler], +/// - (b) pass the request to the next provider in order, or +/// - (c) stop handling the request. +/// +/// See comments on the [ResourceManagerRequest] object for additional usage information. The +/// methods of this class may be called on any browser process thread unless otherwise indicated. +pub struct ResourceManager { + providers: VecDeque, + pending_handlers: BTreeMap, + mime_type_resolver: Arc, + url_filter: Arc, + weak_self: Weak>, +} + +impl ResourceManager { + pub fn new() -> Arc> { + Arc::new_cyclic(|weak_self| { + Mutex::new(Self { + providers: Default::default(), + pending_handlers: Default::default(), + mime_type_resolver: Arc::new(Box::new(get_mime_type)), + url_filter: Arc::new(Box::new(get_filtered_url)), + weak_self: weak_self.clone(), + }) + }) + } + + /// Add a provider that maps requests for `url` to `content`. `url` should be fully qualified + /// but not include a query or fragment component. If `mime_type` is empty the [MimeTypeResolver] + /// will be used. See comments on [ResourceManager::add_provider] for usage of the `order` and + /// `identifier` parameters. + pub fn add_content_provider( + &mut self, + url: &str, + content: &str, + mime_type: &str, + order: i32, + identifier: &str, + ) { + self.add_provider( + ContentProvider::new_resource_manager_provider(url, content, mime_type), + order, + identifier, + ) + } + + /// Add a provider that maps requests that start with `url_path` to files under `archive_path`. + /// `url_path` should include an origin and optional path component only. Files will be loaded + /// when a matching URL is requested. See comments on [ResourceManager::add_provider] for usage + /// of the `order` and `identifier` parameters. + pub fn add_directory_provider( + &mut self, + url_path: &str, + directory_path: &str, + order: i32, + identifier: &str, + ) { + self.add_provider( + DirectoryProvider::new_resource_manager_provider(url_path, directory_path.into()), + order, + identifier, + ) + } + + /// Add a provider that maps requests that start with `url_path` to files stored in the archive + /// file at `archive_path`. `url_path` should include an origin and optional path component + /// only. The archive file will be loaded when a matching URL is requested for the first time. + /// See comments on [ResourceManager::add_provider] for usage of the `order` and `identifier` + /// parameters. + pub fn add_archive_provider( + &mut self, + url_path: &str, + archive_path: &str, + password: &str, + order: i32, + identifier: &str, + ) { + self.add_provider( + ArchiveProvider::new_resource_manager_provider(url_path, archive_path.into(), password), + order, + identifier, + ) + } + + /// Add a provider. This object takes ownership of `provider`. Providers will be called in + /// ascending order based on the `order` value. Multiple providers sharing the same `order` + /// value will be called in the order that they were added. The `identifier` value, which does + /// not need to be unique, can be used to remove the provider at a later time. + pub fn add_provider( + &mut self, + provider: Box, + order: i32, + identifier: &str, + ) { + let io_thread_id = ThreadId::IO; + if currently_on(io_thread_id) == 0 { + let mut task = AddProvider::new( + self.weak_self.clone(), + Arc::new(Mutex::new(Some(provider))), + order, + identifier.to_string(), + ); + post_task(io_thread_id, Some(&mut task)); + return; + } + + let provider_entry = ProviderEntry { + provider: Some(provider), + order, + identifier: identifier.to_string(), + ..Default::default() + }; + + if self.providers.is_empty() { + self.providers.push_back(provider_entry); + return; + } + + // Insert before the first entry with a higher `order` value. + let index = self.providers.partition_point(|entry| entry.order < order); + self.providers.insert(index, provider_entry); + } + + /// Remove all providers with the specified `identifier` value. If any removed providers have + /// pending requests the [RequestManagerProvider::on_request_cancel] method will be called. The + /// removed providers may be deleted immediately or at a later time. + pub fn remove_providers(&mut self, identifier: &str) { + let io_thread_id = ThreadId::IO; + if currently_on(io_thread_id) == 0 { + let mut task = RemoveProviders::new(self.weak_self.clone(), identifier.to_string()); + post_task(io_thread_id, Some(&mut task)); + return; + } + + if self.providers.is_empty() { + return; + } + + let mut index = 0; + while index < self.providers.len() { + index = if self.providers[index].identifier == identifier { + self.delete_provider(index, false) + } else { + index + 1 + }; + } + } + + /// Remove all providers. If any removed providers have pending requests the [RequestManagerProvider::on_request_cancel] + /// method will be called. The removed providers may be deleted immediately or at a later time. + pub fn remove_all_providers(&mut self) { + let io_thread_id = ThreadId::IO; + if currently_on(io_thread_id) == 0 { + let mut task = RemoveAllProviders::new(self.weak_self.clone()); + post_task(io_thread_id, Some(&mut task)); + return; + } + + if self.providers.is_empty() { + return; + } + + let mut index = 0; + while index < self.providers.len() { + index = self.delete_provider(index, true); + } + } + + /// Set the url filter. If not set the default no-op filter will be used. Changes to this value + /// will not affect currently pending requests. + pub fn set_url_filter(&mut self, filter: Option) { + let io_thread_id = ThreadId::IO; + if currently_on(io_thread_id) == 0 { + let mut task = + SetUrlFilter::new(self.weak_self.clone(), Arc::new(Mutex::new(Some(filter)))); + post_task(io_thread_id, Some(&mut task)); + return; + } + + self.url_filter = Arc::new(filter.unwrap_or(Box::new(get_filtered_url))); + } + + /// Set the mime type resolver. If not set the default resolver will be used. Changes to this + /// value will not affect currently pending requests. + pub fn set_mime_type_resolver(&mut self, resolver: Option) { + let io_thread_id = ThreadId::IO; + if currently_on(io_thread_id) == 0 { + let mut task = SetMimeTypeResolver::new( + self.weak_self.clone(), + Arc::new(Mutex::new(Some(resolver))), + ); + post_task(io_thread_id, Some(&mut task)); + return; + } + + self.mime_type_resolver = Arc::new(resolver.unwrap_or(Box::new(get_mime_type))); + } + + /// Called from [RequestHandler::on_before_resource_load] on the browser process IO thread. + pub fn on_before_resource_load( + &mut self, + browser: Browser, + frame: Frame, + request: Request, + callback: Callback, + ) -> ReturnValue { + debug_assert_ne!( + currently_on(ThreadId::IO), + 0, + "on_before_resource_load must be called on the IO thread" + ); + + let first_entry = self.get_next_valid_provider(0); + debug_assert!(first_entry <= self.providers.len()); + if first_entry == self.providers.len() { + // No providers so continue the request immediately. + return ReturnValue::CONTINUE; + } + + let url = CefString::from(&request.url()).to_string(); + let url = (*self.url_filter)(&url); + let url = get_url_without_query_or_fragment(&url).to_string(); + let state = RequestState { + manager: self.weak_self.clone(), + callback: Some(callback), + current_entry: first_entry, + current_request: 0, + params: Arc::new(RequestParams { + url, + browser, + frame, + request, + url_filter: self.url_filter.clone(), + mime_type_resolver: self.mime_type_resolver.clone(), + }), + }; + + ReturnValue::from(if self.send_request(state) { + // If the request is potentially handled we need to continue asynchronously. + sys::cef_return_value_t::RV_CONTINUE_ASYNC + } else { + sys::cef_return_value_t::RV_CONTINUE + }) + } + + /// Called from [RequestHandler::resource_handler] on the browser process IO thread. + pub fn resource_handler( + &mut self, + _browser: Browser, + _frame: Frame, + request: Request, + ) -> Option { + debug_assert_ne!( + currently_on(ThreadId::IO), + 0, + "resource_handler must be called on the IO thread" + ); + + self.pending_handlers.remove(&request.identifier()) + } + + /// Send the request to providers in order until one potentially handles it or we run out of + /// providers. Returns true if the request is potentially handled. + fn send_request(&mut self, state: RequestState) -> bool { + let mut potentially_handled = false; + + let current_entry = state.current_entry; + let params = state.params.clone(); + let mut state = Some(state); + + while state.is_some() { + debug_assert!( + current_entry < self.providers.len(), + "Should not be on the last provider entry." + ); + let request = ResourceManagerRequest::new(state.take(), params.clone()); + let Ok(mut request) = request.lock() else { + break; + }; + + // Give the provider an opportunity to handle the request. + state = request.send_request(); + + let Some(mut next_state) = state.take() else { + potentially_handled = true; + break; + }; + + // The provider will not handle the request. Move to the next provider if any. + if self.increment_provider(&mut next_state) { + state = Some(next_state); + } else { + self.stop_request(next_state); + } + } + + potentially_handled + } + + fn continue_request(&mut self, mut state: RequestState, handler: Option) { + debug_assert_ne!( + currently_on(ThreadId::IO), + 0, + "continue_request must be called on the IO thread" + ); + + if let Some(handler) = handler { + // The request has been handled. Associate the request ID with the handler. + self.pending_handlers + .insert(state.params.request.identifier(), handler); + self.stop_request(state); + } else { + // Move to the next provider if any. + if self.increment_provider(&mut state) { + self.send_request(state); + } else { + self.stop_request(state); + } + } + } + + fn stop_request(&mut self, mut state: RequestState) { + debug_assert_ne!( + currently_on(ThreadId::IO), + 0, + "stop_request must be called on the IO thread" + ); + + self.detach_request_from_provider(&mut state); + } + + /// Move state to the next provider if any and return true if there are more providers. + fn increment_provider(&mut self, state: &mut RequestState) -> bool { + let next_entry = self.get_next_valid_provider(state.current_entry); + self.detach_request_from_provider(state); + if next_entry < self.providers.len() { + state.current_entry = next_entry; + true + } else { + false + } + } + + /// The new provider, if any, should be determined before calling this method. + fn detach_request_from_provider(&mut self, state: &mut RequestState) { + // Remove the association from the current provider entry. + let Some(current_entry) = self.providers.get_mut(state.current_entry) else { + return; + }; + current_entry.pending_requests.remove(state.current_request); + if current_entry.deletion_pending && current_entry.pending_requests.is_empty() { + // Delete the current provider entry now. + self.providers.remove(state.current_entry); + } + // Set to the end for error checking purposes. + state.current_entry = self.providers.len(); + } + + /// Move to the next provider that is not pending deletion. + fn get_next_valid_provider(&self, current_provider: usize) -> usize { + self.providers + .iter() + .enumerate() + .skip(current_provider) + .find(|&(_, entry)| !entry.deletion_pending) + .map(|(index, _)| index) + .unwrap_or(self.providers.len()) + } + + fn delete_provider(&mut self, deleted_provider: usize, stop: bool) -> usize { + debug_assert_ne!( + currently_on(ThreadId::IO), + 0, + "delete_provider must be called on the IO thread" + ); + let Some(current_entry) = self.providers.get_mut(deleted_provider) else { + return self.providers.len(); + }; + if current_entry.deletion_pending { + return deleted_provider; + } + if !current_entry.pending_requests.is_empty() { + // Don't delete the provider entry until all pending requests have cleared. + current_entry.deletion_pending = true; + + // Continue pending requests immediately. + for request in current_entry.pending_requests.iter() { + let Ok(mut request_lock) = request.lock() else { + continue; + }; + if request_lock.state.is_some() { + if stop { + request_lock.stop_request(); + } else { + request_lock.continue_request(None); + } + if let Some(provider) = ¤t_entry.provider { + provider.on_request_canceled(request.clone()); + } + } + } + + deleted_provider + 1 + } else { + // Delete the provider entry now. + self.providers.remove(deleted_provider); + deleted_provider + } + } +} + +/// Returns `url` without the query or fragment components, if any. +pub fn get_url_without_query_or_fragment(url: &str) -> &str { + url.split(['?', '#']).next().unwrap_or(url) +} + +/// Determine the mime type based on the `url` file extension. +pub fn get_mime_type(url: &str) -> String { + let url = get_url_without_query_or_fragment(url); + url.rsplit('.') + .next() + .map(|extension| { + let extension = CefString::from(extension); + CefString::from(&crate::get_mime_type(Some(&extension))).to_string() + }) + .unwrap_or_else(|| String::from("text/html")) +} + +/// Default no-op filter. +pub fn get_filtered_url(url: &str) -> String { + url.to_string() +} + +/// Normalize the URL path by adding a trailing slash if it's missing. +pub fn normalize_url_path(url_path: &str) -> String { + format!("{}/", url_path.trim_end_matches('/')) +} + +pub fn get_file_relative_path( + mut base_path: PathBuf, + url_path: &str, + url: &str, +) -> Option { + let segments = url.strip_prefix(url_path)?.split('/'); + for segment in segments { + base_path.push(segment) + } + Some(base_path) +} diff --git a/cef/src/wrapper/stream_resource_handler.rs b/cef/src/wrapper/stream_resource_handler.rs new file mode 100644 index 0000000..f7c30d8 --- /dev/null +++ b/cef/src/wrapper/stream_resource_handler.rs @@ -0,0 +1,121 @@ +//! Implementation of the ResourceHandler class for reading from a Stream. +use crate::*; + +wrap_resource_handler! { + pub struct StreamResourceHandler { + status_code: i32, + status_text: String, + mime_type: String, + header_map: Option, + stream: Option, + } + + impl ResourceHandler { + fn open( + &self, + _request: Option<&mut Request>, + handle_request: Option<&mut i32>, + _callback: Option<&mut Callback>, + ) -> i32 { + debug_assert_eq!( + currently_on(ThreadId::UI), + 0, + "open must not be called on the UI thread" + ); + debug_assert_eq!( + currently_on(ThreadId::IO), + 0, + "open must not be called on the IO thread" + ); + + // Continue the request immediately. + if let Some(handle_request) = handle_request { + *handle_request = 1; + } + + 1 + } + + fn response_headers( + &self, + response: Option<&mut Response>, + response_length: Option<&mut i64>, + _redirect_url: Option<&mut CefString>, + ) { + debug_assert_ne!( + currently_on(ThreadId::IO), + 0, + "response_headers must be called on the IO thread" + ); + + let Some(response) = response else { + return; + }; + + response.set_status(self.status_code); + response.set_status_text(Some(&CefString::from(self.status_text.as_str()))); + response.set_mime_type(Some(&CefString::from(self.mime_type.as_str()))); + + if let Some(mut header_map) = self.header_map.clone() { + response.set_header_map(Some(&mut header_map)); + } + + if let Some(response_length) = response_length { + *response_length = if self.stream.is_some() { -1 } else { 0 }; + } + } + + #[allow(clippy::not_unsafe_ptr_arg_deref)] + fn read( + &self, + data_out: *mut u8, + bytes_to_read: i32, + bytes_read: Option<&mut i32>, + _callback: Option<&mut ResourceReadCallback>, + ) -> i32 { + debug_assert_eq!( + currently_on(ThreadId::UI), + 0, + "read must not be called on the UI thread" + ); + debug_assert_eq!( + currently_on(ThreadId::IO), + 0, + "read must not be called on the IO thread" + ); + if bytes_to_read < 1 { + return 0; + } + let Some(bytes_read) = bytes_read else { + return 0; + }; + let Some(stream) = &self.stream else { + *bytes_read = 0; + return 1; + }; + + // Read until the buffer is full or until read() returns 0 to indicate no more data. + *bytes_read = 0; + loop { + let data_out = unsafe { data_out.add(*bytes_read as usize) }; + let read = stream.read(data_out, 1, (bytes_to_read - *bytes_read) as usize); + *bytes_read += read as i32; + if read == 0 || *bytes_read >= bytes_to_read { + break; + } + } + + if *bytes_read > 0 { + 1 + } else { + 0 + } + } + } +} + +impl StreamResourceHandler { + pub fn new_with_stream(mime_type: String, stream: StreamReader) -> ResourceHandler { + Self::new(200, "OK".to_string(), mime_type, None, Some(stream)) + } +} diff --git a/cef/src/wrapper/zip_archive.rs b/cef/src/wrapper/zip_archive.rs new file mode 100644 index 0000000..62d4e12 --- /dev/null +++ b/cef/src/wrapper/zip_archive.rs @@ -0,0 +1,131 @@ +use super::byte_read_handler::*; +use crate::*; +use std::{ + collections::BTreeMap, + sync::{Arc, Mutex}, +}; + +pub trait File: Send { + fn stream_reader(&self) -> Option; +} + +pub type FileMap = BTreeMap>>; + +struct ZipFile { + data: Vec, +} + +impl File for ZipFile { + fn stream_reader(&self) -> Option { + let mut handler = + ByteReadHandler::new(Arc::new(Mutex::new(ByteStream::new(self.data.clone())))); + stream_reader_create_for_handler(Some(&mut handler)) + } +} + +#[derive(Default)] +pub struct ZipArchive { + contents: Mutex, +} + +impl ZipArchive { + pub fn load( + &self, + stream: &mut StreamReader, + password: &str, + overwrite_existing: bool, + ) -> usize { + let Ok(mut contents) = self.contents.lock() else { + return 0; + }; + let Some(reader) = zip_reader_create(Some(stream)) else { + return 0; + }; + let password = CefString::from(password); + + let mut count = 0; + loop { + let size = reader.file_size(); + if size <= 0 { + // Skip directories and empty files. + continue; + } + let size = size as usize; + + if reader.open_file(Some(&password)) == 0 { + break; + } + + let name = CefString::from(&reader.file_name()) + .to_string() + .to_lowercase(); + + if contents.contains_key(&name) { + if overwrite_existing { + contents.remove(&name); + } else { + // Skip files that already exist. + continue; + } + } + + let mut data = Vec::with_capacity(size); + while data.len() < size && reader.eof() == 0 { + let mut chunk = vec![0; size - data.len()]; + let read = reader.read_file(Some(&mut chunk)); + if read <= 0 { + break; + } + data.extend_from_slice(&chunk[..read as usize]); + } + + debug_assert_eq!(data.len(), size); + reader.close_file(); + count += 1; + + // Add the file to the map. + contents.insert(name, Arc::new(Mutex::new(ZipFile { data }))); + + if reader.move_to_next_file() == 0 { + break; + } + } + + count + } + + pub fn clear(&self) { + let Ok(mut contents) = self.contents.lock() else { + return; + }; + contents.clear(); + } + + pub fn file_count(&self) -> usize { + let Ok(contents) = self.contents.lock() else { + return 0; + }; + contents.len() + } + + pub fn file(&self, name: &str) -> Option>> { + let Ok(contents) = self.contents.lock() else { + return None; + }; + contents.get(name).cloned() + } + + pub fn remove_file(&self, name: &str) -> bool { + let Ok(mut contents) = self.contents.lock() else { + return false; + }; + contents.remove(name).is_some() + } + + pub fn files(&self) -> FileMap { + let Ok(contents) = self.contents.lock() else { + return Default::default(); + }; + contents.clone() + } +} diff --git a/download-cef/src/lib.rs b/download-cef/src/lib.rs index feab4f6..f9e5ac2 100644 --- a/download-cef/src/lib.rs +++ b/download-cef/src/lib.rs @@ -357,7 +357,7 @@ impl CefVersion { where P: AsRef, { - let mut result = self.download_archive_from(&url, &location, show_progress); + let mut result = self.download_archive_from(url, &location, show_progress); let mut retry = 0; while let Err(Error::Io(_)) = &result { @@ -368,7 +368,7 @@ impl CefVersion { retry += 1; thread::sleep(retry_delay * retry); - result = self.download_archive_from(&url, &location, show_progress); + result = self.download_archive_from(url, &location, show_progress); } result @@ -459,7 +459,7 @@ where println!("Downloading CEF archive for {target}..."); } - let index = CefIndex::download_from(&url)?; + let index = CefIndex::download_from(url)?; let platform = index.platform(target)?; let version = platform.version(cef_version)?; diff --git a/examples/cefsimple/Cargo.toml b/examples/cefsimple/Cargo.toml index e4e5ff1..ab347ae 100644 --- a/examples/cefsimple/Cargo.toml +++ b/examples/cefsimple/Cargo.toml @@ -3,27 +3,55 @@ name = "cefsimple" edition = "2024" publish = false +[lib] +crate-type = ["cdylib"] + [[bin]] name = "cefsimple" [[bin]] name = "cefsimple_helper" -path = "src/mac/helper.rs" - -[[bin]] -name = "bundle_cefsimple" -path = "src/mac/bundle_cefsimple.rs" [features] -default = ["sandbox"] -sandbox = ["cef/sandbox"] +default = [ + "sandbox", +] +sandbox = [ + "cef/sandbox", +] +linux-x11 = [ + "x11-dl", +] + +[package.metadata.cef.bundle] +helper_name = "cefsimple_helper" +resources_path = "resources" [dependencies] -cef.workspace = true -cef-dll-sys.workspace = true +cef = { workspace = true, features = [ + "build-util", +] } [target.'cfg(target_os = "macos")'.dependencies] -plist.workspace = true -serde.workspace = true objc2.workspace = true -objc2-app-kit = { workspace = true, features = ["NSApplication", "NSResponder"] } +objc2-foundation.workspace = true +objc2-app-kit = { workspace = true, features = [ + "NSApplication", + "NSEvent", + "NSResponder", + "NSUserInterfaceValidation", + "NSView", + "NSWindow", +] } + +[target.'cfg(target_os = "windows")'.dependencies] +windows-sys = { workspace = true, features = [ + "Win32_Foundation", + "Win32_UI_WindowsAndMessaging", +] } + +[target.'cfg(target_os = "windows")'.build-dependencies] +winres.workspace = true + +[target.'cfg(target_os = "linux")'.dependencies] +x11-dl = { workspace = true, optional = true } diff --git a/examples/cefsimple/build.rs b/examples/cefsimple/build.rs new file mode 100644 index 0000000..56e86eb --- /dev/null +++ b/examples/cefsimple/build.rs @@ -0,0 +1,14 @@ +#[cfg(target_os = "windows")] +include!("src/shared/resources.rs"); + +#[cfg(target_os = "windows")] +fn main() { + winres::WindowsResource::new() + .set_icon_with_id("resources/win/cefsimple.ico", &IDI_CEFSIMPLE.to_string()) + .set_icon_with_id("resources/win/small.ico", &IDI_SMALL.to_string()) + .compile() + .unwrap(); +} + +#[cfg(not(target_os = "windows"))] +fn main() {} diff --git a/examples/cefsimple/resources/mac/English.lproj/InfoPlist.strings b/examples/cefsimple/resources/mac/English.lproj/InfoPlist.strings new file mode 100644 index 0000000..dc5ebb0 --- /dev/null +++ b/examples/cefsimple/resources/mac/English.lproj/InfoPlist.strings @@ -0,0 +1,3 @@ +/* Localized versions of Info.plist keys */ + +NSHumanReadableCopyright = "© Chromium Embedded Framework Authors, 2013"; diff --git a/examples/cefsimple/resources/mac/English.lproj/MainMenu.xib b/examples/cefsimple/resources/mac/English.lproj/MainMenu.xib new file mode 100644 index 0000000..7eff402 --- /dev/null +++ b/examples/cefsimple/resources/mac/English.lproj/MainMenu.xib @@ -0,0 +1,330 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/examples/cefsimple/resources/mac/cefsimple.icns b/examples/cefsimple/resources/mac/cefsimple.icns new file mode 100644 index 0000000..f36742d Binary files /dev/null and b/examples/cefsimple/resources/mac/cefsimple.icns differ diff --git a/examples/cefsimple/resources/win/cefsimple.ico b/examples/cefsimple/resources/win/cefsimple.ico new file mode 100644 index 0000000..d551aa3 Binary files /dev/null and b/examples/cefsimple/resources/win/cefsimple.ico differ diff --git a/examples/cefsimple/resources/win/small.ico b/examples/cefsimple/resources/win/small.ico new file mode 100644 index 0000000..d551aa3 Binary files /dev/null and b/examples/cefsimple/resources/win/small.ico differ diff --git a/examples/cefsimple/src/mac/helper.rs b/examples/cefsimple/src/bin/cefsimple_helper.rs similarity index 100% rename from examples/cefsimple/src/mac/helper.rs rename to examples/cefsimple/src/bin/cefsimple_helper.rs diff --git a/examples/cefsimple/src/lib.rs b/examples/cefsimple/src/lib.rs new file mode 100644 index 0000000..3a6f23e --- /dev/null +++ b/examples/cefsimple/src/lib.rs @@ -0,0 +1,4 @@ +#[cfg(all(target_os = "windows", feature = "sandbox"))] +pub mod shared; +#[cfg(all(target_os = "windows", feature = "sandbox"))] +mod win; diff --git a/examples/cefsimple/src/mac/bundle_cefsimple.rs b/examples/cefsimple/src/mac/bundle_cefsimple.rs deleted file mode 100644 index c4e9db6..0000000 --- a/examples/cefsimple/src/mac/bundle_cefsimple.rs +++ /dev/null @@ -1,182 +0,0 @@ -#[cfg(target_os = "macos")] -mod mac { - use serde::Serialize; - use std::collections::HashMap; - use std::fs; - use std::path::{Path, PathBuf}; - use std::process::{Command, Stdio}; - - #[derive(Serialize)] - struct InfoPlist { - #[serde(rename = "CFBundleDevelopmentRegion")] - cf_bundle_development_region: String, - #[serde(rename = "CFBundleDisplayName")] - cf_bundle_display_name: String, - #[serde(rename = "CFBundleExecutable")] - cf_bundle_executable: String, - #[serde(rename = "CFBundleIdentifier")] - cf_bundle_identifier: String, - #[serde(rename = "CFBundleInfoDictionaryVersion")] - cf_bundle_info_dictionary_version: String, - #[serde(rename = "CFBundleName")] - cf_bundle_name: String, - #[serde(rename = "CFBundlePackageType")] - cf_bundle_package_type: String, - #[serde(rename = "CFBundleSignature")] - cf_bundle_signature: String, - #[serde(rename = "CFBundleVersion")] - cf_bundle_version: String, - #[serde(rename = "CFBundleShortVersionString")] - cf_bundle_short_version_string: String, - #[serde(rename = "LSEnvironment")] - ls_environment: HashMap, - #[serde(rename = "LSFileQuarantineEnabled")] - ls_file_quarantine_enabled: bool, - #[serde(rename = "LSMinimumSystemVersion")] - ls_minimum_system_version: String, - #[serde(rename = "LSUIElement")] - ls_ui_element: Option, - #[serde(rename = "NSBluetoothAlwaysUsageDescription")] - ns_bluetooth_always_usage_description: String, - #[serde(rename = "NSSupportsAutomaticGraphicsSwitching")] - ns_supports_automatic_graphics_switching: bool, - #[serde(rename = "NSWebBrowserPublicKeyCredentialUsageDescription")] - ns_web_browser_publickey_credential_usage_description: String, - #[serde(rename = "NSCameraUsageDescription")] - ns_camera_usage_description: String, - #[serde(rename = "NSMicrophoneUsageDescription")] - ns_microphone_usage_description: String, - } - - const EXEC_PATH: &str = "Contents/MacOS"; - const FRAMEWORKS_PATH: &str = "Contents/Frameworks"; - const RESOURCES_PATH: &str = "Contents/Resources"; - const FRAMEWORK: &str = "Chromium Embedded Framework.framework"; - const HELPERS: &[&str] = &[ - "cefsimple Helper (GPU)", - "cefsimple Helper (Renderer)", - "cefsimple Helper (Plugin)", - "cefsimple Helper (Alerts)", - "cefsimple Helper", - ]; - - fn create_app_layout(app_path: &Path) -> PathBuf { - [EXEC_PATH, RESOURCES_PATH, FRAMEWORKS_PATH] - .iter() - .for_each(|p| fs::create_dir_all(app_path.join(p)).unwrap()); - app_path.join("Contents") - } - - fn create_app(app_path: &Path, exec_name: &str, bin: &Path, is_helper: bool) -> PathBuf { - let app_path = app_path.join(exec_name).with_extension("app"); - let contents_path = create_app_layout(&app_path); - create_info_plist(&contents_path, exec_name, is_helper).unwrap(); - fs::copy(bin, app_path.join(EXEC_PATH).join(exec_name)).unwrap(); - app_path - } - - // See https://bitbucket.org/chromiumembedded/cef/wiki/GeneralUsage.md#markdown-header-macos - fn bundle(app_path: &Path) { - let example_path = PathBuf::from(app_path); - let main_app_path = create_app( - app_path, - "cefsimple", - &example_path.join("cefsimple"), - false, - ); - let cef_path = cef_dll_sys::get_cef_dir().unwrap(); - let to = main_app_path.join(FRAMEWORKS_PATH).join(FRAMEWORK); - if to.exists() { - fs::remove_dir_all(&to).unwrap(); - } - copy_directory(&cef_path.join(FRAMEWORK), &to); - HELPERS.iter().for_each(|helper| { - create_app( - &main_app_path.join(FRAMEWORKS_PATH), - helper, - &example_path.join("cefsimple_helper"), - true, - ); - }); - } - - fn create_info_plist( - contents_path: &Path, - exec_name: &str, - is_helper: bool, - ) -> Result<(), Box> { - let info_plist = InfoPlist { - cf_bundle_development_region: "en".to_string(), - cf_bundle_display_name: exec_name.to_string(), - cf_bundle_executable: exec_name.to_string(), - cf_bundle_identifier: "org.cef-rs.cefsimple.helper".to_string(), - cf_bundle_info_dictionary_version: "6.0".to_string(), - cf_bundle_name: "cef-rs".to_string(), - cf_bundle_package_type: "APPL".to_string(), - cf_bundle_signature: "????".to_string(), - cf_bundle_version: "1.0.0".to_string(), - cf_bundle_short_version_string: "1.0".to_string(), - ls_environment: [("MallocNanoZone".to_string(), "0".to_string())] - .iter() - .cloned() - .collect(), - ls_file_quarantine_enabled: true, - ls_minimum_system_version: "11.0".to_string(), - ls_ui_element: if is_helper { - Some("1".to_string()) - } else { - None - }, - ns_bluetooth_always_usage_description: exec_name.to_string(), - ns_supports_automatic_graphics_switching: true, - ns_web_browser_publickey_credential_usage_description: exec_name.to_string(), - ns_camera_usage_description: exec_name.to_string(), - ns_microphone_usage_description: exec_name.to_string(), - }; - - plist::to_file_xml(contents_path.join("Info.plist"), &info_plist)?; - Ok(()) - } - - fn copy_directory(src: &Path, dst: &Path) { - fs::create_dir_all(dst).unwrap(); - for entry in fs::read_dir(src).unwrap() { - let entry = entry.unwrap(); - let dst_path = dst.join(entry.file_name()); - if entry.file_type().unwrap().is_dir() { - copy_directory(&entry.path(), &dst_path); - } else { - fs::copy(entry.path(), &dst_path).unwrap(); - } - } - } - - fn run_command(args: &[&str]) -> Result<(), Box> { - let status = Command::new("cargo") - .args(args) - .stdout(Stdio::inherit()) - .stderr(Stdio::inherit()) - .status()?; - - if !status.success() { - std::process::exit(1); - } - Ok(()) - } - - pub fn main() -> Result<(), Box> { - let app_path = PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("../../target/debug"); - run_command(&["build", "--bin", "cefsimple"])?; - run_command(&["build", "--bin", "cefsimple_helper"])?; - bundle(&app_path); - Ok(()) - } -} - -#[cfg(target_os = "macos")] -fn main() -> Result<(), Box> { - mac::main() -} - -#[cfg(not(target_os = "macos"))] -fn main() {} diff --git a/examples/cefsimple/src/mac/mod.rs b/examples/cefsimple/src/mac/mod.rs new file mode 100644 index 0000000..865d66c --- /dev/null +++ b/examples/cefsimple/src/mac/mod.rs @@ -0,0 +1,243 @@ +use crate::shared::simple_handler::*; +use cef::application_mac::{CefAppProtocol, CrAppControlProtocol, CrAppProtocol}; +use objc2::{ + ClassType, DefinedClass, MainThreadMarker, MainThreadOnly, define_class, extern_methods, + msg_send, + rc::Retained, + runtime::{AnyObject, Bool, NSObject, NSObjectProtocol, ProtocolObject}, + sel, +}; +use objc2_app_kit::{ + NSApp, NSApplication, NSApplicationDelegate, NSApplicationTerminateReply, NSEvent, + NSUserInterfaceValidations, NSValidatedUserInterfaceItem, +}; +use objc2_foundation::{NSBundle, NSObjectNSThreadPerformAdditions, ns_string}; +use std::{cell::Cell, ptr}; + +define_class! { + #[unsafe(super(NSObject))] + #[thread_kind = MainThreadOnly] + pub struct SimpleAppDelegate; + + impl SimpleAppDelegate { + /// Create the application on the UI thread. + #[unsafe(method(createApplication:))] + unsafe fn create_application(&self, _object: Option<&AnyObject>) { + let app = NSApp(MainThreadMarker::new().expect("Not running on the main thread")); + assert!(app.isKindOfClass(SimpleApplication::class())); + assert!( + app.delegate() + .unwrap() + .isKindOfClass(SimpleAppDelegate::class()) + ); + + let main_bundle = NSBundle::mainBundle(); + let _: Bool = msg_send![&main_bundle, + loadNibNamed: ns_string!("MainMenu"), + owner: &*app, + topLevelObjects: ptr::null_mut::<*const AnyObject>() + ]; + } + } + + unsafe impl NSObjectProtocol for SimpleAppDelegate {} + + unsafe impl NSApplicationDelegate for SimpleAppDelegate { + #[unsafe(method(applicationShouldTerminate:))] + unsafe fn application_should_terminate(&self, _sender: &NSApplication) -> NSApplicationTerminateReply { + NSApplicationTerminateReply::TerminateNow + } + + /// Called when the user clicks the app dock icon while the application is + /// already running. + #[unsafe(method(applicationShouldHandleReopen:hasVisibleWindows:))] + unsafe fn application_should_handle_reopen(&self, _sender: &NSApplication, _has_visible_windows: Bool) -> Bool { + if let Some(handler) = SimpleHandler::instance() { + let mut handler = handler.lock().expect("Failed to lock SimpleHandler"); + if !handler.is_closing() { + handler.show_main_window(); + } + } + Bool::NO + } + + /// Requests that any state restoration archive be created with secure encoding + /// (macOS 12+ only). See https://crrev.com/c737387656 for details. This also + /// fixes an issue with macOS default behavior incorrectly restoring windows + /// after hard reset (holding down the power button). + #[unsafe(method(applicationSupportsSecureRestorableState:))] + unsafe fn application_supports_secure_restorable_state(&self, _sender: &NSApplication) -> Bool { + Bool::YES + } + } + + unsafe impl NSUserInterfaceValidations for SimpleAppDelegate { + #[unsafe(method(validateUserInterfaceItem:))] + unsafe fn validate_user_interface_item(&self, item: &ProtocolObject) -> Bool { + const IDC_FIND: isize = 37000; + + let tag = item.tag(); + if tag == IDC_FIND { + Bool::YES + } else { + Bool::NO + } + } + } +} + +impl SimpleAppDelegate { + fn new(mtm: MainThreadMarker) -> Retained { + let this = SimpleAppDelegate::alloc(mtm).set_ivars(()); + unsafe { msg_send![super(this), init] } + } +} + +/// Instance variables of `SimpleApplication`. +#[derive(Default)] +pub struct SimpleApplicationIvars { + handling_send_event: Cell, +} + +define_class!( + /// A `NSApplication` subclass that implements the required CEF protocols. + /// + /// This class provides the necessary `CefAppProtocol` conformance to + /// ensure that events are handled correctly by the Chromium framework on macOS. + #[unsafe(super(NSApplication))] + #[ivars = SimpleApplicationIvars] + pub struct SimpleApplication; + + impl SimpleApplication { + #[unsafe(method(sendEvent:))] + unsafe fn send_event(&self, event: &NSEvent) { + let was_sending_event = self.is_handling_send_event(); + if !was_sending_event { + self.set_handling_send_event(true); + } + + let _: () = msg_send![super(self), sendEvent:event]; + + if !was_sending_event { + self.set_handling_send_event(false); + } + } + + /// |-terminate:| is the entry point for orderly "quit" operations in Cocoa. This + /// includes the application menu's quit menu item and keyboard equivalent, the + /// application's dock icon menu's quit menu item, "quit" (not "force quit") in + /// the Activity Monitor, and quits triggered by user logout and system restart + /// and shutdown. + /// + /// The default |-terminate:| implementation ends the process by calling exit(), + /// and thus never leaves the main run loop. This is unsuitable for Chromium + /// since Chromium depends on leaving the main run loop to perform an orderly + /// shutdown. We support the normal |-terminate:| interface by overriding the + /// default implementation. Our implementation, which is very specific to the + /// needs of Chromium, works by asking the application delegate to terminate + /// using its |-tryToTerminateApplication:| method. + /// + /// |-tryToTerminateApplication:| differs from the standard + /// |-applicationShouldTerminate:| in that no special event loop is run in the + /// case that immediate termination is not possible (e.g., if dialog boxes + /// allowing the user to cancel have to be shown). Instead, this method tries to + /// close all browsers by calling CloseBrowser(false) via + /// ClientHandler::CloseAllBrowsers. Calling CloseBrowser will result in a call + /// to ClientHandler::DoClose and execution of |-performClose:| on the NSWindow. + /// DoClose sets a flag that is used to differentiate between new close events + /// (e.g., user clicked the window close button) and in-progress close events + /// (e.g., user approved the close window dialog). The NSWindowDelegate + /// |-windowShouldClose:| method checks this flag and either calls + /// CloseBrowser(false) in the case of a new close event or destructs the + /// NSWindow in the case of an in-progress close event. + /// ClientHandler::OnBeforeClose will be called after the CEF NSView hosted in + /// the NSWindow is dealloc'ed. + /// + /// After the final browser window has closed ClientHandler::OnBeforeClose will + /// begin actual tear-down of the application by calling CefQuitMessageLoop. + /// This ends the NSApplication event loop and execution then returns to the + /// main() function for cleanup before application termination. + /// + /// The standard |-applicationShouldTerminate:| is not supported, and code paths + /// leading to it must be redirected. + #[unsafe(method(terminate:))] + unsafe fn terminate(&self, _sender: &AnyObject) { + if let Some(handler) = SimpleHandler::instance() { + let mut handler = handler.lock().expect("Failed to lock SimpleHandler"); + if !handler.is_closing() { + handler.close_all_browsers(false); + } + } + } + } + + unsafe impl CrAppControlProtocol for SimpleApplication { + #[unsafe(method(setHandlingSendEvent:))] + unsafe fn _set_handling_send_event(&self, handling_send_event: Bool) { + self.ivars().handling_send_event.set(handling_send_event); + } + } + + unsafe impl CrAppProtocol for SimpleApplication { + #[unsafe(method(isHandlingSendEvent))] + unsafe fn _is_handling_send_event(&self) -> Bool { + self.ivars().handling_send_event.get() + } + } + + unsafe impl CefAppProtocol for SimpleApplication {} +); + +impl SimpleApplication { + extern_methods! { + #[unsafe(method(sharedApplication))] + fn shared_application() -> Retained; + + #[unsafe(method(setHandlingSendEvent:))] + fn set_handling_send_event(&self, handling_send_event: bool); + + #[unsafe(method(isHandlingSendEvent))] + fn is_handling_send_event(&self) -> bool; + } +} + +pub fn setup_simple_application() { + // Initialize the SimpleApplication instance. + // SAFETY: mtm ensures that here is the main thread. + let _ = SimpleApplication::shared_application(); + + // If there was an invocation to NSApp prior to here, + // then the NSApp will not be a SimpleApplication. + // The following assertion ensures that this doesn't happen. + assert!( + NSApp(MainThreadMarker::new().expect("Not running on the main thread")) + .isKindOfClass(SimpleApplication::class()) + ); +} + +pub fn setup_simple_app_delegate() -> Retained { + let mtm = MainThreadMarker::new().expect("Not running on the main thread"); + + // Create the application delegate. + let simple_delegate = SimpleAppDelegate::new(mtm); + let delegate_proto = + ProtocolObject::::from_retained(simple_delegate.clone()); + let app = NSApp(MainThreadMarker::new().expect("Not running on the main thread")); + assert!(app.isKindOfClass(SimpleApplication::class())); + app.setDelegate(Some(&delegate_proto)); + assert!( + app.delegate() + .unwrap() + .isKindOfClass(SimpleAppDelegate::class()) + ); + + unsafe { + simple_delegate.performSelectorOnMainThread_withObject_waitUntilDone( + sel!(createApplication:), + None, + false, + ); + } + + simple_delegate +} diff --git a/examples/cefsimple/src/main.rs b/examples/cefsimple/src/main.rs index c53c645..fd5ccd8 100644 --- a/examples/cefsimple/src/main.rs +++ b/examples/cefsimple/src/main.rs @@ -1,230 +1,27 @@ -use cef::{args::Args, rc::*, *}; -use std::sync::{Arc, Mutex}; +#![cfg_attr( + all(not(debug_assertions), not(feature = "sandbox"), target_os = "windows"), + windows_subsystem = "windows" +)] -wrap_app! { - struct DemoApp { - window: Arc>>, - } - - impl App { - fn browser_process_handler(&self) -> Option { - Some(DemoBrowserProcessHandler::new( - self.window.clone(), - )) - } - } -} - -wrap_browser_process_handler! { - struct DemoBrowserProcessHandler { - window: Arc>>, - } - - impl BrowserProcessHandler { - // The real lifespan of cef starts from `on_context_initialized`, so all the cef objects should be manipulated after that. - fn on_context_initialized(&self) { - println!("cef context intiialized"); - let mut client = DemoClient::new(); - let url = CefString::from("https://www.google.com"); - - let browser_view = browser_view_create( - Some(&mut client), - Some(&url), - Some(&Default::default()), - Option::<&mut DictionaryValue>::None, - Option::<&mut RequestContext>::None, - Option::<&mut BrowserViewDelegate>::None, - ) - .expect("Failed to create browser view"); - - let mut delegate = DemoWindowDelegate::new(browser_view); - if let Ok(mut window) = self.window.lock() { - *window = Some( - window_create_top_level(Some(&mut delegate)).expect("Failed to create window"), - ); - } - } - } -} - -wrap_client! { - struct DemoClient; - impl Client {} -} - -wrap_window_delegate! { - struct DemoWindowDelegate { - browser_view: BrowserView, - } - - impl ViewDelegate { - fn on_child_view_changed( - &self, - _view: Option<&mut View>, - _added: ::std::os::raw::c_int, - _child: Option<&mut View>, - ) { - // view.as_panel().map(|x| x.as_window().map(|w| w.close())); - } - } - - impl PanelDelegate {} - - impl WindowDelegate { - fn on_window_created(&self, window: Option<&mut Window>) { - if let Some(window) = window { - let view = self.browser_view.clone(); - window.add_child_view(Some(&mut (&view).into())); - window.show(); - } - } - - fn on_window_destroyed(&self, _window: Option<&mut Window>) { - quit_message_loop(); - } - - fn with_standard_window_buttons(&self, _window: Option<&mut Window>) -> ::std::os::raw::c_int { - 1 - } - - fn can_resize(&self, _window: Option<&mut Window>) -> ::std::os::raw::c_int { - 1 - } - - fn can_maximize(&self, _window: Option<&mut Window>) -> ::std::os::raw::c_int { - 1 - } - - fn can_minimize(&self, _window: Option<&mut Window>) -> ::std::os::raw::c_int { - 1 - } - - fn can_close(&self, _window: Option<&mut Window>) -> ::std::os::raw::c_int { - 1 - } - } -} - -// FIXME: Rewrite this demo based on cef/tests/cefsimple -fn main() { - #[cfg(target_os = "macos")] - let _loader = { - let loader = library_loader::LibraryLoader::new(&std::env::current_exe().unwrap(), false); - assert!(loader.load()); - loader - }; - - #[cfg(target_os = "macos")] - { - use objc2::{ - ClassType, MainThreadMarker, msg_send, - rc::Retained, - runtime::{AnyObject, NSObjectProtocol}, - }; - use objc2_app_kit::NSApp; - - use application::SimpleApplication; - - let mtm = MainThreadMarker::new().unwrap(); - - unsafe { - // Initialize the SimpleApplication instance. - // SAFETY: mtm ensures that here is the main thread. - let _: Retained = msg_send![SimpleApplication::class(), sharedApplication]; - } - - // If there was an invocation to NSApp prior to here, - // then the NSApp will not be a SimpleApplication. - // The following assertion ensures that this doesn't happen. - assert!(NSApp(mtm).isKindOfClass(SimpleApplication::class())); - } - - let _ = api_hash(sys::CEF_API_VERSION_LAST, 0); - - let args = Args::new(); - let cmd = args.as_cmd_line().unwrap(); - - let switch = CefString::from("type"); - let is_browser_process = cmd.has_switch(Some(&switch)) != 1; - - let window = Arc::new(Mutex::new(None)); - let mut app = DemoApp::new(window.clone()); - - let ret = execute_process( - Some(args.as_main_args()), - Some(&mut app), - std::ptr::null_mut(), - ); - - if is_browser_process { - println!("launch browser process"); - assert!(ret == -1, "cannot execute browser process"); - } else { - let process_type = CefString::from(&cmd.switch_value(Some(&switch))); - println!("launch process {process_type}"); - assert!(ret >= 0, "cannot execute non-browser process"); - // non-browser process does not initialize cef - return; - } - let settings = Settings { - no_sandbox: !cfg!(feature = "sandbox") as _, - ..Default::default() - }; - assert_eq!( - initialize( - Some(args.as_main_args()), - Some(&settings), - Some(&mut app), - std::ptr::null_mut(), - ), - 1 - ); - - run_message_loop(); - - let window = window.lock().expect("Failed to lock window"); - let window = window.as_ref().expect("Window is None"); - assert!(window.has_one_ref()); - - shutdown(); -} +pub mod shared; #[cfg(target_os = "macos")] -mod application { - use std::cell::Cell; +mod mac; - use cef::application_mac::{CefAppProtocol, CrAppControlProtocol, CrAppProtocol}; - use objc2::{DefinedClass, define_class, runtime::Bool}; - use objc2_app_kit::NSApplication; +#[cfg(not(all(feature = "sandbox", target_os = "windows")))] +fn main() -> Result<(), &'static str> { + let _library = shared::load_cef(); - /// Instance variables of `SimpleApplication`. - pub struct SimpleApplicationIvars { - handling_send_event: Cell, - } + let args = cef::args::Args::new(); + let Some(cmd_line) = args.as_cmd_line() else { + return Err("Failed to parse command line arguments"); + }; - define_class!( - /// A `NSApplication` subclass that implements the required CEF protocols. - /// - /// This class provides the necessary `CefAppProtocol` conformance to - /// ensure that events are handled correctly by the Chromium framework on macOS. - #[unsafe(super(NSApplication))] - #[ivars = SimpleApplicationIvars] - pub struct SimpleApplication; - - unsafe impl CrAppControlProtocol for SimpleApplication { - #[unsafe(method(setHandlingSendEvent:))] - unsafe fn set_handling_send_event(&self, handling_send_event: Bool) { - self.ivars().handling_send_event.set(handling_send_event); - } - } - - unsafe impl CrAppProtocol for SimpleApplication { - #[unsafe(method(isHandlingSendEvent))] - unsafe fn is_handling_send_event(&self) -> Bool { - self.ivars().handling_send_event.get() - } - } - - unsafe impl CefAppProtocol for SimpleApplication {} - ); + shared::run_main(args.as_main_args(), &cmd_line, std::ptr::null_mut()); + Ok(()) +} + +#[cfg(all(feature = "sandbox", target_os = "windows"))] +fn main() -> Result<(), &'static str> { + Err("Running in sandbox mode on Windows requires bootstrap.exe or bootstrapc.exe.") } diff --git a/examples/cefsimple/src/shared/mod.rs b/examples/cefsimple/src/shared/mod.rs new file mode 100644 index 0000000..0bfe251 --- /dev/null +++ b/examples/cefsimple/src/shared/mod.rs @@ -0,0 +1,75 @@ +//! Rust port of the [`cefsimple`](https://github.com/chromiumembedded/cef/tree/master/tests/cefsimple) example. + +use cef::*; + +pub mod resources; +pub mod simple_app; +pub mod simple_handler; + +#[cfg(target_os = "macos")] +pub type Library = library_loader::LibraryLoader; + +#[cfg(not(target_os = "macos"))] +pub type Library = (); + +#[allow(dead_code)] +pub fn load_cef() -> Library { + #[cfg(target_os = "macos")] + let library = { + let loader = library_loader::LibraryLoader::new(&std::env::current_exe().unwrap(), false); + assert!(loader.load()); + loader + }; + #[cfg(not(target_os = "macos"))] + let library = (); + + // Initialize the CEF API version. + let _ = api_hash(sys::CEF_API_VERSION_LAST, 0); + + #[cfg(target_os = "macos")] + crate::mac::setup_simple_application(); + + library +} + +#[allow(dead_code)] +pub fn run_main(main_args: &MainArgs, cmd_line: &CommandLine, sandbox_info: *mut u8) { + let switch = CefString::from("type"); + let is_browser_process = cmd_line.has_switch(Some(&switch)) != 1; + + let ret = execute_process(Some(main_args), None, sandbox_info); + + if is_browser_process { + println!("launch browser process"); + assert_eq!(ret, -1, "cannot execute browser process"); + } else { + let process_type = CefString::from(&cmd_line.switch_value(Some(&switch))); + println!("launch process {process_type}"); + assert!(ret >= 0, "cannot execute non-browser process"); + // non-browser process does not initialize cef + return; + } + + let mut app = simple_app::SimpleApp::new(); + + let settings = Settings { + no_sandbox: !cfg!(feature = "sandbox") as _, + ..Default::default() + }; + assert_eq!( + initialize( + Some(main_args), + Some(&settings), + Some(&mut app), + sandbox_info, + ), + 1 + ); + + #[cfg(target_os = "macos")] + let _delegate = crate::mac::setup_simple_app_delegate(); + + run_message_loop(); + + shutdown(); +} diff --git a/examples/cefsimple/src/shared/resources.rs b/examples/cefsimple/src/shared/resources.rs new file mode 100644 index 0000000..4f96666 --- /dev/null +++ b/examples/cefsimple/src/shared/resources.rs @@ -0,0 +1,2 @@ +pub const IDI_CEFSIMPLE: u16 = 120; +pub const IDI_SMALL: u16 = 121; diff --git a/examples/cefsimple/src/shared/simple_app.rs b/examples/cefsimple/src/shared/simple_app.rs new file mode 100644 index 0000000..b27da29 --- /dev/null +++ b/examples/cefsimple/src/shared/simple_app.rs @@ -0,0 +1,215 @@ +use cef::*; +use std::cell::RefCell; + +use super::simple_handler::*; + +wrap_window_delegate! { + struct SimpleWindowDelegate { + browser_view: RefCell>, + runtime_style: RuntimeStyle, + initial_show_state: ShowState, + } + + impl ViewDelegate { + fn preferred_size(&self, _view: Option<&mut View>) -> Size { + Size { + width: 800, + height: 600, + } + } + } + + impl PanelDelegate {} + + impl WindowDelegate { + fn on_window_created(&self, window: Option<&mut Window>) { + // Add the browser view and show the window. + let browser_view = self.browser_view.borrow(); + let (Some(window), Some(browser_view)) = (window, browser_view.as_ref()) else { + return; + }; + let mut view = View::from(browser_view); + window.add_child_view(Some(&mut view)); + + if self.initial_show_state != ShowState::HIDDEN { + window.show(); + } + } + + fn on_window_destroyed(&self, _window: Option<&mut Window>) { + let mut browser_view = self.browser_view.borrow_mut(); + *browser_view = None; + } + + fn can_close(&self, _window: Option<&mut Window>) -> i32 { + // Allow the window to close if the browser says it's OK. + let browser_view = self.browser_view.borrow(); + let browser_view = browser_view.as_ref().expect("BrowserView is None"); + if let Some(browser) = browser_view.browser() { + let browser_host = browser.host().expect("BrowserHost is None"); + browser_host.try_close_browser() + } else { + 1 + } + } + + fn initial_show_state(&self, _window: Option<&mut Window>) -> ShowState { + self.initial_show_state + } + + fn window_runtime_style(&self) -> RuntimeStyle { + self.runtime_style + } + } +} + +wrap_browser_view_delegate! { + struct SimpleBrowserViewDelegate { + runtime_style: RuntimeStyle, + } + + impl ViewDelegate {} + + impl BrowserViewDelegate { + fn on_popup_browser_view_created( + &self, + _browser_view: Option<&mut BrowserView>, + popup_browser_view: Option<&mut BrowserView>, + _is_devtools: i32, + ) -> i32 { + // Create a new top-level Window for the popup. It will show itself after + // creation. + let mut window_delegate = SimpleWindowDelegate::new( + RefCell::new(popup_browser_view.cloned()), + self.runtime_style, + ShowState::NORMAL, + ); + window_create_top_level(Some(&mut window_delegate)); + + // We created the Window. + 1 + } + + fn browser_runtime_style(&self) -> RuntimeStyle { + self.runtime_style + } + } +} + +wrap_app! { + pub struct SimpleApp; + + impl App { + fn browser_process_handler(&self) -> Option { + Some(SimpleBrowserProcessHandler::new(RefCell::new(None))) + } + } +} + +wrap_browser_process_handler! { + struct SimpleBrowserProcessHandler { + client: RefCell>, + } + + impl BrowserProcessHandler { + fn on_context_initialized(&self) { + debug_assert_ne!(currently_on(ThreadId::UI), 0); + + // Check if Alloy style will be used. + let command_line = command_line_get_global().expect("Failed to get command line"); + let use_alloy_style = + command_line.has_switch(Some(&CefString::from("use-alloy-style"))) != 0; + let runtime_style = if use_alloy_style { + RuntimeStyle::ALLOY + } else { + RuntimeStyle::DEFAULT + }; + + { + // SimpleHandler implements browser-level callbacks. + let mut client = self.client.borrow_mut(); + *client = Some(SimpleHandlerClient::new(SimpleHandler::new( + use_alloy_style, + ))); + } + + // Specify CEF browser settings here. + let settings = BrowserSettings::default(); + + // Check if a "--url=" value was provided via the command-line. If so, use + // that instead of the default URL. + let url = CefString::from(&command_line.switch_value(Some(&CefString::from("url")))) + .to_string(); + let url = if url.is_empty() { + "https://www.google.com/" + } else { + url.as_str() + }; + let url = CefString::from(url); + + // Views is enabled by default (add `--use-native` to disable). + let use_views = command_line.has_switch(Some(&CefString::from("use-native"))) != 0; + + // If using Views create the browser using the Views framework, otherwise + // create the browser using the native platform framework. + if use_views { + // Create the BrowserView. + let mut client = self.default_client(); + let mut delegate = SimpleBrowserViewDelegate::new(runtime_style); + let browser_view = browser_view_create( + client.as_mut(), + Some(&url), + Some(&settings), + None, + None, + Some(&mut delegate), + ); + + // Optionally configure the initial show state. + let initial_show_state = CefString::from( + &command_line.switch_value(Some(&CefString::from("initial-show-state"))), + ) + .to_string(); + let initial_show_state = match initial_show_state.as_str() { + "minimized" => ShowState::MINIMIZED, + "maximized" => ShowState::MAXIMIZED, + // Hidden show state is only supported on MacOS. + #[cfg(target_os = "macos")] + "hidden" => ShowState::HIDDEN, + _ => ShowState::NORMAL, + }; + + // Create the Window. It will show itself after creation. + let mut delegate = SimpleWindowDelegate::new( + RefCell::new(browser_view), + runtime_style, + initial_show_state, + ); + window_create_top_level(Some(&mut delegate)); + } else { + // Information used when creating the native window. + let window_info = WindowInfo { + runtime_style, + ..Default::default() + }; + + #[cfg(target_os = "windows")] + let window_info = window_info.set_as_popup(Default::default(), "cefsimple"); + + let mut client = self.default_client(); + browser_host_create_browser( + Some(&window_info), + client.as_mut(), + Some(&url), + Some(&settings), + None, + None, + ); + } + } + + fn default_client(&self) -> Option { + self.client.borrow().clone() + } + } +} diff --git a/examples/cefsimple/src/shared/simple_handler/linux.rs b/examples/cefsimple/src/shared/simple_handler/linux.rs new file mode 100644 index 0000000..0bd212b --- /dev/null +++ b/examples/cefsimple/src/shared/simple_handler/linux.rs @@ -0,0 +1,76 @@ +use cef::*; + +fn window_from_browser(browser: Option<&mut Browser>) -> Option { + let window = browser?.host()?.window_handle(); + if window == 0 { None } else { Some(window) } +} + +pub fn platform_title_change(browser: Option<&mut Browser>, _title: Option<&CefString>) { + // Retrieve the X11 display shared with Chromium. + let display = get_xdisplay(); + if display.is_null() { + return; + } + + // Retrieve the X11 window handle for the browser. + let Some(_window) = window_from_browser(browser) else { + return; + }; + + #[cfg(feature = "linux-x11")] + unsafe { + use std::ffi::{CString, c_char}; + use x11_dl::xlib::*; + + // Load the Xlib library dynamically. + let Ok(xlib) = Xlib::open() else { + return; + }; + + // Retrieve the atoms required by the below XChangeProperty call. + let Ok(names) = ["_NET_WM_NAME", "UTF8_STRING"] + .into_iter() + .map(CString::new) + .collect::, _>>() + else { + return; + }; + let mut names: Vec<_> = names + .iter() + .map(|name| name.as_ptr() as *mut c_char) + .collect(); + let mut atoms = [0; 2]; + let result = (xlib.XInternAtoms)( + display as *mut _, + names.as_mut_ptr(), + 2, + 0, + atoms.as_mut_ptr(), + ); + if result == 0 { + return; + } + + // Set the window title. + let Ok(title) = CString::new(_title.map(CefString::to_string).unwrap_or_default()) else { + return; + }; + let title = title.as_c_str(); + (xlib.XChangeProperty)( + display as *mut _, + _window, + atoms[0], + atoms[1], + 8, + PropModeReplace, + title.as_ptr() as *const _, + title.count_bytes() as i32, + ); + + // TODO(erg): This is technically wrong. So XStoreName and friends expect + // this in Host Portable Character Encoding instead of UTF-8, which I believe + // is Compound Text. This shouldn't matter 90% of the time since this is the + // fallback to the UTF8 property above. + (xlib.XStoreName)(display as *mut _, _window, title.as_ptr()); + } +} diff --git a/examples/cefsimple/src/shared/simple_handler/mac.rs b/examples/cefsimple/src/shared/simple_handler/mac.rs new file mode 100644 index 0000000..7273980 --- /dev/null +++ b/examples/cefsimple/src/shared/simple_handler/mac.rs @@ -0,0 +1,29 @@ +use cef::*; +use objc2::{Message, rc::Retained}; +use objc2_app_kit::{NSView, NSWindow}; +use objc2_foundation::NSString; + +fn window_from_browser(browser: Option<&mut Browser>) -> Option> { + let view_ptr = browser?.host()?.window_handle().cast::(); + let view = unsafe { view_ptr.as_ref()? }; + let view = view.retain(); + view.window() +} + +pub fn platform_title_change(browser: Option<&mut Browser>, title: Option<&CefString>) { + let Some(window) = window_from_browser(browser) else { + return; + }; + + let title = title.map(CefString::to_string).unwrap_or_default(); + let title = NSString::from_str(&title); + window.setTitle(&title); +} + +pub fn platform_show_window(browser: Option<&mut Browser>) { + let Some(window) = window_from_browser(browser) else { + return; + }; + + window.makeKeyAndOrderFront(Some(&window)); +} diff --git a/examples/cefsimple/src/shared/simple_handler/mod.rs b/examples/cefsimple/src/shared/simple_handler/mod.rs new file mode 100644 index 0000000..d61ddea --- /dev/null +++ b/examples/cefsimple/src/shared/simple_handler/mod.rs @@ -0,0 +1,326 @@ +use cef::*; +use std::sync::{Arc, Mutex, OnceLock, Weak}; + +fn get_data_uri(data: &[u8], mime_type: &str) -> String { + let data = CefString::from(&base64_encode(Some(data))); + let uri = CefString::from(&uriencode(Some(&data), 0)).to_string(); + format!("data:{mime_type};base64,{uri}") +} + +#[cfg(target_os = "macos")] +mod mac; +#[cfg(target_os = "macos")] +use mac::*; + +#[cfg(target_os = "windows")] +mod win; +#[cfg(target_os = "windows")] +use win::*; + +#[cfg(target_os = "linux")] +mod linux; +#[cfg(target_os = "linux")] +use linux::*; + +#[cfg(not(target_os = "macos"))] +fn platform_show_window(_browser: Option<&mut Browser>) { + todo!("Implement platform_show_window for non-macOS platforms"); +} + +static SIMPLE_HANDLER_INSTANCE: OnceLock>> = OnceLock::new(); + +pub struct SimpleHandler { + is_alloy_style: bool, + browser_list: Vec, + is_closing: bool, + weak_self: Weak>, +} + +impl SimpleHandler { + pub fn instance() -> Option>> { + SIMPLE_HANDLER_INSTANCE + .get() + .and_then(|weak| weak.upgrade()) + } + + pub fn new(is_alloy_style: bool) -> Arc> { + Arc::new_cyclic(|weak| { + if let Err(instance) = SIMPLE_HANDLER_INSTANCE.set(weak.clone()) { + assert_eq!(instance.strong_count(), 0, "Replacing a viable instance"); + } + + Mutex::new(Self { + is_alloy_style, + browser_list: Vec::new(), + is_closing: false, + weak_self: weak.clone(), + }) + }) + } + + fn on_title_change(&mut self, browser: Option<&mut Browser>, title: Option<&CefString>) { + debug_assert_ne!(currently_on(ThreadId::UI), 0); + + let mut browser = browser.cloned(); + if let Some(browser_view) = browser_view_get_for_browser(browser.as_mut()) { + if let Some(window) = browser_view.window() { + window.set_title(title); + } + } else if self.is_alloy_style { + platform_title_change(browser.as_mut(), title); + } + } + + fn on_after_created(&mut self, browser: Option<&mut Browser>) { + debug_assert_ne!(currently_on(ThreadId::UI), 0); + + let browser = browser.cloned().expect("Browser is None"); + + // Sanity-check the configured runtime style. + assert_eq!( + browser.host().expect("BrowserHost is None").runtime_style(), + if self.is_alloy_style { + RuntimeStyle::ALLOY + } else { + RuntimeStyle::CHROME + } + ); + + // Add to the list of existing browsers. + self.browser_list.push(browser); + } + + fn do_close(&mut self, _browser: Option<&mut Browser>) -> bool { + debug_assert_ne!(currently_on(ThreadId::UI), 0); + + // Closing the main window requires special handling. See the DoClose() + // documentation in the CEF header for a detailed destription of this + // process. + if self.browser_list.len() == 1 { + // Set a flag to indicate that the window close should be allowed. + self.is_closing = true; + } + + // Allow the close. For windowed browsers this will result in the OS close + // event being sent. + false + } + + fn on_before_close(&mut self, browser: Option<&mut Browser>) { + debug_assert_ne!(currently_on(ThreadId::UI), 0); + + // Remove from the list of existing browsers. + let mut browser = browser.cloned().expect("Browser is None"); + if let Some(index) = self + .browser_list + .iter() + .position(move |elem| elem.is_same(Some(&mut browser)) != 0) + { + self.browser_list.remove(index); + } + + if self.browser_list.is_empty() { + // All browser windows have closed. Quit the application message loop. + quit_message_loop(); + } + } + + fn on_load_error( + &mut self, + _browser: Option<&mut Browser>, + frame: Option<&mut Frame>, + error_code: Errorcode, + error_text: Option<&CefString>, + failed_url: Option<&CefString>, + ) { + debug_assert_ne!(currently_on(ThreadId::UI), 0); + + // Allow Chrome to show the error page. + if !self.is_alloy_style { + return; + } + + // Don't display an error for downloaded files. + let error_code = sys::cef_errorcode_t::from(error_code); + if error_code == sys::cef_errorcode_t::ERR_ABORTED { + return; + } + let error_code = error_code as i32; + + let frame = frame.expect("Frame is None"); + + // Display a load error message using a data: URI. + let error_text = error_text.map(CefString::to_string).unwrap_or_default(); + let failed_url = failed_url.map(CefString::to_string).unwrap_or_default(); + let data = format!( + r#" + + +

Failed to load URL {failed_url} with error {error_text} ({error_code}).

+ + + "# + ); + + let uri = get_data_uri(data.as_bytes(), "text/html"); + let uri = CefString::from(uri.as_str()); + frame.load_url(Some(&uri)); + } + + pub fn show_main_window(&mut self) { + let thread_id = ThreadId::UI; + if currently_on(thread_id) == 0 { + // Execute on the UI thread. + let this = self + .weak_self + .upgrade() + .expect("Weak reference to SimpleHandler is None"); + let mut task = ShowMainWindow::new(this); + post_task(thread_id, Some(&mut task)); + return; + } + + let Some(mut main_browser) = self.browser_list.first().cloned() else { + return; + }; + + if let Some(browser_view) = browser_view_get_for_browser(Some(&mut main_browser)) { + // Show the window using the Views framework. + if let Some(window) = browser_view.window() { + window.show(); + } + } else if self.is_alloy_style { + platform_show_window(Some(&mut main_browser)); + } + } + + pub fn close_all_browsers(&mut self, force_close: bool) { + let thread_id = ThreadId::UI; + if currently_on(thread_id) == 0 { + // Execute on the UI thread. + let this = self + .weak_self + .upgrade() + .expect("Weak reference to SimpleHandler is None"); + let mut task = CloseAllBrowsers::new(this, force_close); + post_task(thread_id, Some(&mut task)); + return; + } + + for browser in self.browser_list.iter() { + let browser_host = browser.host().expect("BrowserHost is None"); + browser_host.close_browser(force_close.into()); + } + } + + pub fn is_closing(&self) -> bool { + self.is_closing + } +} + +wrap_client! { + pub struct SimpleHandlerClient { + inner: Arc>, + } + + impl Client { + fn display_handler(&self) -> Option { + Some(SimpleHandlerDisplayHandler::new(self.inner.clone())) + } + + fn life_span_handler(&self) -> Option { + Some(SimpleHandlerLifeSpanHandler::new(self.inner.clone())) + } + + fn load_handler(&self) -> Option { + Some(SimpleHandlerLoadHandler::new(self.inner.clone())) + } + } +} + +wrap_display_handler! { + struct SimpleHandlerDisplayHandler { + inner: Arc>, + } + + impl DisplayHandler { + fn on_title_change(&self, browser: Option<&mut Browser>, title: Option<&CefString>) { + let mut inner = self.inner.lock().expect("Failed to lock inner"); + inner.on_title_change(browser, title); + } + } +} + +wrap_life_span_handler! { + struct SimpleHandlerLifeSpanHandler { + inner: Arc>, + } + + impl LifeSpanHandler { + fn on_after_created(&self, browser: Option<&mut Browser>) { + let mut inner = self.inner.lock().expect("Failed to lock inner"); + inner.on_after_created(browser); + } + + fn do_close(&self, browser: Option<&mut Browser>) -> i32 { + let mut inner = self.inner.lock().expect("Failed to lock inner"); + inner.do_close(browser).into() + } + + fn on_before_close(&self, browser: Option<&mut Browser>) { + let mut inner = self.inner.lock().expect("Failed to lock inner"); + inner.on_before_close(browser); + } + } +} + +wrap_load_handler! { + struct SimpleHandlerLoadHandler { + inner: Arc>, + } + + impl LoadHandler { + fn on_load_error( + &self, + browser: Option<&mut Browser>, + frame: Option<&mut Frame>, + error_code: Errorcode, + error_text: Option<&CefString>, + failed_url: Option<&CefString>, + ) { + let mut inner = self.inner.lock().expect("Failed to lock inner"); + inner.on_load_error(browser, frame, error_code, error_text, failed_url); + } + } +} + +wrap_task! { + struct ShowMainWindow { + inner: Arc>, + } + + impl Task { + fn execute(&self) { + debug_assert_ne!(currently_on(ThreadId::UI), 0); + + let mut inner = self.inner.lock().expect("Failed to lock inner"); + inner.show_main_window(); + } + } +} + +wrap_task! { + struct CloseAllBrowsers { + inner: Arc>, + force_close: bool, + } + + impl Task { + fn execute(&self) { + debug_assert_ne!(currently_on(ThreadId::UI), 0); + + let mut inner = self.inner.lock().expect("Failed to lock inner"); + inner.close_all_browsers(self.force_close); + } + } +} diff --git a/examples/cefsimple/src/shared/simple_handler/win.rs b/examples/cefsimple/src/shared/simple_handler/win.rs new file mode 100644 index 0000000..3db2ac5 --- /dev/null +++ b/examples/cefsimple/src/shared/simple_handler/win.rs @@ -0,0 +1,18 @@ +use cef::*; +use std::iter; +use windows_sys::Win32::{Foundation::HWND, UI::WindowsAndMessaging::*}; + +fn window_from_browser(browser: Option<&mut Browser>) -> Option { + let window = browser?.host()?.window_handle().0; + Some(window.cast()) +} + +pub fn platform_title_change(browser: Option<&mut Browser>, title: Option<&CefString>) { + let Some(window) = window_from_browser(browser) else { + return; + }; + + let title = title.map(CefString::to_string).unwrap_or_default(); + let title: Vec<_> = title.encode_utf16().chain(iter::once(0)).collect(); + unsafe { SetWindowTextW(window, title.as_ptr()) }; +} diff --git a/examples/cefsimple/src/win.rs b/examples/cefsimple/src/win.rs new file mode 100644 index 0000000..b28debd --- /dev/null +++ b/examples/cefsimple/src/win.rs @@ -0,0 +1,21 @@ +use crate::shared; +use cef::*; + +#[unsafe(no_mangle)] +unsafe extern "C" fn RunWinMain( + instance: sys::HINSTANCE, + _command_line: *const u8, + _command_show: i32, + sandbox_info: *mut u8, +) -> i32 { + let _library = shared::load_cef(); + + let main_args = MainArgs { instance }; + let args = args::Args::from(main_args); + let Some(cmd_line) = args.as_cmd_line() else { + return 1; + }; + + shared::run_main(args.as_main_args(), &cmd_line, sandbox_info); + 0 +} diff --git a/examples/osr/Cargo.toml b/examples/osr/Cargo.toml index 77c4f2e..86c3315 100644 --- a/examples/osr/Cargo.toml +++ b/examples/osr/Cargo.toml @@ -7,6 +7,9 @@ publish = false default = ["accelerated_osr"] accelerated_osr = ["cef/accelerated_osr"] +[package.metadata.cef.bundle] +helper_name = "cefsimple_helper" + [dependencies] cef.workspace = true wgpu.workspace = true diff --git a/examples/osr/src/webrender.rs b/examples/osr/src/webrender.rs index b39f9c3..d3ee695 100644 --- a/examples/osr/src/webrender.rs +++ b/examples/osr/src/webrender.rs @@ -33,8 +33,10 @@ wrap_app! { command_line.append_switch(Some(&"hide-crash-restore-bubble".into())); command_line.append_switch(Some(&"use-mock-keychain".into())); command_line.append_switch(Some(&"enable-logging=stderr".into())); - command_line - .append_switch_with_value(Some(&"remote-debugging-port".into()), Some(&"9229".into())); + command_line.append_switch_with_value( + Some(&"remote-debugging-port".into()), + Some(&"9229".into()), + ); } fn browser_process_handler(&self) -> Option { @@ -223,7 +225,9 @@ wrap_render_handler! { ty: wgpu::BindingType::Texture { multisampled: false, view_dimension: wgpu::TextureViewDimension::D2, - sample_type: wgpu::TextureSampleType::Float { filterable: true }, + sample_type: wgpu::TextureSampleType::Float { + filterable: true, + }, }, count: None, }, @@ -348,7 +352,9 @@ wrap_render_handler! { ty: wgpu::BindingType::Texture { multisampled: false, view_dimension: wgpu::TextureViewDimension::D2, - sample_type: wgpu::TextureSampleType::Float { filterable: true }, + sample_type: wgpu::TextureSampleType::Float { + filterable: true, + }, }, count: None, }, diff --git a/examples/tests_shared/Cargo.toml b/examples/tests_shared/Cargo.toml new file mode 100644 index 0000000..27b8dff --- /dev/null +++ b/examples/tests_shared/Cargo.toml @@ -0,0 +1,34 @@ +[package] +name = "tests_shared" +publish = false + +version.workspace = true +edition.workspace = true +license.workspace = true +authors.workspace = true +repository.workspace = true + +[features] +sandbox = ["cef/sandbox"] + +[dependencies] +cef.workspace = true + +[target.'cfg(target_os = "windows")'.dependencies.windows-sys] +workspace = true +features = [ + "Win32_System_Performance", + "Win32_System_SystemServices", + "Win32_UI_WindowsAndMessaging", + "Win32_Foundation", + "Win32_Graphics_Gdi", + "Win32_UI_Input_KeyboardAndMouse", +] + +[target.'cfg(target_os = "macos")'.dependencies] +objc2.workspace = true +objc2-app-kit = { workspace = true, features = ["NSApplication", "NSResponder"] } +objc2-foundation.workspace = true + +[target.'cfg(target_os = "linux")'.dependencies] +glib.workspace = true diff --git a/examples/tests_shared/resources/osr_test.html b/examples/tests_shared/resources/osr_test.html new file mode 100644 index 0000000..629bce5 --- /dev/null +++ b/examples/tests_shared/resources/osr_test.html @@ -0,0 +1,205 @@ + + OSR Test + + + +

+ OSR Testing h1 - Focus and blur + + this page and will get this red black +

+
    +
  1. OnPaint should be called each time a page loads
  2. +
  3. Move mouse + to require an OnCursorChange call
  4. +
  5. Hover will color this with + red. Will trigger OnPaint once on enter and once on leave
  6. +
  7. Right clicking will show contextual menu and will request + GetScreenPoint
  8. +
  9. IsWindowRenderingDisabled should be true
  10. +
  11. WasResized should trigger full repaint if size changes. +
  12. +
  13. Invalidate should trigger OnPaint once
  14. +
  15. Click and write here with SendKeyEvent to trigger repaints: +
  16. +
  17. Click here with SendMouseClickEvent to navigate: +
  18. +
  19. Mouse over this element will + trigger show a tooltip
  20. +
  21. SELECTED_TEXT_RANGE
  22. +
  23. +
  24. Long touch press should trigger quick menu
  25. +
+ +
+ Drag here +
+
+
+
+
+
+
+
+
+
+
+
+
+
+ + diff --git a/examples/tests_shared/resources/pdf.html b/examples/tests_shared/resources/pdf.html new file mode 100644 index 0000000..619b1ce --- /dev/null +++ b/examples/tests_shared/resources/pdf.html @@ -0,0 +1,9 @@ + + +PDF Test + + + + + + diff --git a/examples/tests_shared/resources/pdf.pdf b/examples/tests_shared/resources/pdf.pdf new file mode 100644 index 0000000..1e0dad5 Binary files /dev/null and b/examples/tests_shared/resources/pdf.pdf differ diff --git a/examples/tests_shared/resources/window_icon.1x.png b/examples/tests_shared/resources/window_icon.1x.png new file mode 100644 index 0000000..9d28862 Binary files /dev/null and b/examples/tests_shared/resources/window_icon.1x.png differ diff --git a/examples/tests_shared/resources/window_icon.2x.png b/examples/tests_shared/resources/window_icon.2x.png new file mode 100644 index 0000000..59b9d49 Binary files /dev/null and b/examples/tests_shared/resources/window_icon.2x.png differ diff --git a/examples/tests_shared/src/browser/client_app_browser.rs b/examples/tests_shared/src/browser/client_app_browser.rs new file mode 100644 index 0000000..e8f6a32 --- /dev/null +++ b/examples/tests_shared/src/browser/client_app_browser.rs @@ -0,0 +1,262 @@ +use crate::{ + browser::main_message_loop_external_pump::*, + common::{client_app::*, client_switches::*}, +}; +use cef::*; +use std::{ + path::{Path, PathBuf}, + sync::Arc, +}; + +pub trait Delegate: Send { + fn on_before_command_line_processing( + &self, + _app: &Arc, + _command_line: &mut CommandLine, + ) { + } + + fn on_register_custom_preferences( + &self, + _app: &Arc, + _type_: PreferencesType, + _registrar: &mut PreferenceRegistrar, + ) { + } + + fn on_context_initialized(&self, _app: &Arc) {} + + fn on_already_running_app_relaunch( + &self, + _app: &Arc, + _command_line: &mut CommandLine, + _current_directory: &Path, + ) -> bool { + false + } + + fn default_client(&self, _app: &Arc) -> Option { + None + } +} + +pub struct ClientAppBrowser { + delegates: Vec>, +} + +impl ClientAppBrowser { + pub fn new(delegates: Vec>) -> Arc { + Arc::new(Self { delegates }) + } + + pub fn populate_settings( + command_line: Option, + cookieable_schemes: Vec, + settings: Settings, + ) -> Settings { + #[cfg(any(target_os = "windows", target_os = "linux"))] + let settings = { + Settings { + multi_threaded_message_loop: command_line.as_ref().map_or(0, |command_line| { + command_line.has_switch(Some(&CefString::from(MULTI_THREADED_MESSAGE_LOOP))) + }), + ..settings + } + }; + + let settings = if settings.multi_threaded_message_loop == 0 { + Settings { + external_message_pump: command_line.as_ref().map_or(0, |command_line| { + command_line.has_switch(Some(&CefString::from(EXTERNAL_MESSAGE_PUMP))) + }), + ..settings + } + } else { + settings + }; + + let cookieable_schemes_list = CefString::from(cookieable_schemes.join(",").as_str()); + + Settings { + cookieable_schemes_list, + ..settings + } + } + + pub fn delegates(&self) -> &[Box] { + &self.delegates + } +} + +wrap_app! { + pub struct ClientAppBrowserApp { + base: ClientApp, + client_app_browser: Arc, + } + + impl App { + fn on_before_command_line_processing( + &self, + process_type: Option<&CefString>, + command_line: Option<&mut CommandLine>, + ) { + let (Some(process_type), Some(command_line)) = (process_type, command_line) else { + return; + }; + + // Pass additional command-line flags to the browser process. + if process_type.to_string().is_empty() { + // Pass additional command-line flags when off-screen rendering is enabled. + if command_line.has_switch(Some(&CefString::from(OFF_SCREEN_RENDERING_ENABLED))) + != 0 + && command_line.has_switch(Some(&CefString::from(SHARED_TEXTURE_ENABLED))) == 0 + { + // Use software rendering and compositing (disable GPU) for increased FPS + // and decreased CPU usage. This will also disable WebGL so remove these + // switches if you need that capability. + // See https://github.com/chromiumembedded/cef/issues/1257 for details. + if command_line.has_switch(Some(&CefString::from(ENABLE_GPU))) == 0 { + command_line.append_switch(Some(&CefString::from("disable-gpu"))); + command_line + .append_switch(Some(&CefString::from("disable-gpu-compositing"))); + } + } + + if command_line.has_switch(Some(&CefString::from(USE_VIEWS))) != 0 + && command_line.has_switch(Some(&CefString::from("top-chrome-md"))) == 0 + { + // Use non-material mode on all platforms by default. Among other things + // this causes menu buttons to show hover state. See usage of + // MaterialDesignController::IsModeMaterial() in Chromium code. + command_line.append_switch_with_value( + Some(&CefString::from("top-chrome-md")), + Some(&CefString::from("non-material")), + ); + } + + // Disable the toolchain prompt on macOS. + #[cfg(target_os = "macos")] + command_line.append_switch(Some(&CefString::from("use-mock-keychain"))); + + // On Linux, in off screen rendering (OSR) shared texture mode, we must + // ensure that ANGLE uses the EGL backend. Without this, DMABUF based + // rendering will fail. The Chromium fallback path uses X11 pixmaps, + // which are only supported by Mesa drivers (e.g., AMD and Intel). + // + // While Mesa supports DMABUFs via both EGL and pixmaps, the EGL based + // DMA BUF import path is more robust and required for compatibility with + // drivers like NVIDIA that do not support pixmaps. + // + // We also append the kOzonePlatform switch with value x11 to ensure + // that X11 semantics are preserved, which is necessary for compatibility + // with some GDK/X11 integrations (e.g. Wayland with AMD). + #[cfg(target_os = "linux")] + if command_line.has_switch(Some(&CefString::from(OFF_SCREEN_RENDERING_ENABLED))) + != 0 + && command_line.has_switch(Some(&CefString::from(SHARED_TEXTURE_ENABLED))) != 0 + { + if command_line.has_switch(Some(&CefString::from(USE_ANGLE))) == 0 { + command_line.append_switch_with_value( + Some(&CefString::from(USE_ANGLE)), + Some(&CefString::from("gl-egl")), + ); + } + if command_line.has_switch(Some(&CefString::from(OZONE_PLATFORM))) == 0 { + command_line.append_switch_with_value( + Some(&CefString::from(OZONE_PLATFORM)), + Some(&CefString::from("X11")), + ); + } + } + } + + for delegate in self.client_app_browser.delegates() { + delegate.on_before_command_line_processing(&self.client_app_browser, command_line); + } + } + + fn on_register_custom_schemes(&self, registrar: Option<&mut SchemeRegistrar>) { + self.base.on_register_custom_schemes(registrar); + } + + fn browser_process_handler(&self) -> Option { + Some(ClientAppBrowserProcessHandler::new( + self.client_app_browser.clone(), + )) + } + } +} + +wrap_browser_process_handler! { + pub struct ClientAppBrowserProcessHandler { + client_app_browser: Arc, + } + + impl BrowserProcessHandler { + fn on_register_custom_preferences( + &self, + type_: PreferencesType, + registrar: Option<&mut PreferenceRegistrar>, + ) { + let Some(registrar) = registrar else { + return; + }; + + for delegate in self.client_app_browser.delegates() { + delegate.on_register_custom_preferences(&self.client_app_browser, type_, registrar); + } + } + + fn on_context_initialized(&self) { + for delegate in self.client_app_browser.delegates() { + delegate.on_context_initialized(&self.client_app_browser); + } + } + + fn on_already_running_app_relaunch( + &self, + command_line: Option<&mut CommandLine>, + current_directory: Option<&CefString>, + ) -> i32 { + let (Some(command_line), Some(current_directory)) = (command_line, current_directory) + else { + return 0; + }; + + let delegates = self.client_app_browser.delegates(); + if !delegates.is_empty() { + let current_directory = PathBuf::from(current_directory.to_string().as_str()); + + for delegate in delegates { + if delegate.on_already_running_app_relaunch( + &self.client_app_browser, + command_line, + current_directory.as_path(), + ) { + return 1; + } + } + } + + 0 + } + + fn on_schedule_message_pump_work(&self, delay_ms: i64) { + if let Some(message_loop) = get_main_message_loop() { + if let Ok(mut message_loop) = message_loop.lock() { + message_loop.on_schedule_message_pump_work(delay_ms); + } + } + } + + fn default_client(&self) -> Option { + for delegate in self.client_app_browser.delegates() { + let client = delegate.default_client(&self.client_app_browser); + if client.is_some() { + return client; + } + } + None + } + } +} diff --git a/examples/tests_shared/src/browser/file_util.rs b/examples/tests_shared/src/browser/file_util.rs new file mode 100644 index 0000000..83506c9 --- /dev/null +++ b/examples/tests_shared/src/browser/file_util.rs @@ -0,0 +1,81 @@ +use cef::*; +use std::{ + fs::File, + io::{Read, Write}, + path::Path, +}; + +fn allow_file_io() -> bool { + currently_on(ThreadId::UI) == 0 && currently_on(ThreadId::IO) == 0 +} + +/// Reads the file at `path` into `contents` and returns true on success and +/// false on error. In case of I/O error, `contents` holds the data that could +/// be read from the file before the error occurred. When the file size exceeds +/// `max_size`, the function returns false with `contents` holding the file +/// truncated to `max_size`. `contents` may be [None], in which case this +/// function is useful for its side effect of priming the disk cache (could be +/// used for unit tests). Calling this function on the browser process UI or IO +/// threads is not allowed. +pub fn read_file_to_buffer>( + path: P, + contents: &mut Option>, + max_size: usize, +) -> bool { + if !allow_file_io() { + return false; + } + + if let Some(contents) = contents.as_mut() { + contents.clear(); + contents.reserve(max_size); + } + let Ok(mut file) = File::open(path) else { + return false; + }; + + // Many files supplied in `path` have incorrect size (proc files etc). + // Hence, the file is read sequentially as opposed to a one-shot read. + const BUFFER_SIZE: usize = 1 << 16; + let mut buffer = vec![0; BUFFER_SIZE]; + let mut size = 0; + + while let Ok(read) = file.read(&mut buffer) { + if read == 0 { + break; + } + + if let Some(contents) = contents.as_mut() { + contents.extend_from_slice(&buffer[..read.min(max_size - size)]); + } + + size += read; + if size > max_size { + return false; + } + } + + true +} + +/// Writes the given buffer into the file, overwriting any data that was +/// previously there. Returns the number of bytes written, or [None] on error. +/// Calling this function on the browser process UI or IO threads is not allowed. +pub fn write_file>(path: P, contents: &[u8]) -> Option { + if !allow_file_io() { + return None; + } + + let mut file = File::create(path).ok()?; + let mut size = 0; + + while size < contents.len() { + let write = file.write(&contents[size..]).unwrap_or(0); + if write == 0 { + break; + } + size += write; + } + + Some(size) +} diff --git a/examples/tests_shared/src/browser/geometry_util.rs b/examples/tests_shared/src/browser/geometry_util.rs new file mode 100644 index 0000000..8ed67e3 --- /dev/null +++ b/examples/tests_shared/src/browser/geometry_util.rs @@ -0,0 +1,56 @@ +use cef::*; + +pub const fn logical_value_to_device(value: i32, scale_factor: f32) -> i32 { + (value as f32 * scale_factor) as i32 +} + +pub const fn logical_rect_to_device(rect: Rect, scale_factor: f32) -> Rect { + Rect { + x: logical_value_to_device(rect.x, scale_factor), + y: logical_value_to_device(rect.y, scale_factor), + width: logical_value_to_device(rect.width, scale_factor), + height: logical_value_to_device(rect.height, scale_factor), + } +} + +pub const fn device_value_to_logical(value: i32, scale_factor: f32) -> i32 { + (value as f32 / scale_factor) as i32 +} + +pub const fn device_rect_to_logical(rect: Rect, scale_factor: f32) -> Rect { + Rect { + x: device_value_to_logical(rect.x, scale_factor), + y: device_value_to_logical(rect.y, scale_factor), + width: device_value_to_logical(rect.width, scale_factor), + height: device_value_to_logical(rect.height, scale_factor), + } +} + +pub const fn device_mouse_event_to_logical(event: MouseEvent, scale_factor: f32) -> MouseEvent { + MouseEvent { + x: device_value_to_logical(event.x, scale_factor), + y: device_value_to_logical(event.y, scale_factor), + ..event + } +} + +pub const fn device_touch_event_to_logical(event: TouchEvent, scale_factor: f32) -> TouchEvent { + TouchEvent { + x: event.x / scale_factor, + y: event.y / scale_factor, + ..event + } +} + +pub fn constrain_window_bounds(display: &Rect, window: &mut Rect) { + window.x = window.x.max(display.x); + window.y = window.y.max(display.y); + window.width = window.width.clamp(100, display.width); + window.height = window.height.clamp(100, display.height); + if window.x + window.width > display.x + display.width { + window.x = display.x + display.width - window.width; + } + if window.y + window.height > display.y + display.height { + window.y = display.y + display.height - window.height; + } +} diff --git a/examples/tests_shared/src/browser/main_message_loop.rs b/examples/tests_shared/src/browser/main_message_loop.rs new file mode 100644 index 0000000..b8d9c21 --- /dev/null +++ b/examples/tests_shared/src/browser/main_message_loop.rs @@ -0,0 +1,135 @@ +use cef::*; +use std::{ + mem, + sync::{Arc, Mutex, OnceLock}, +}; + +#[cfg(target_os = "windows")] +use windows_sys::Win32::Foundation::HWND; + +static INSTANCE: OnceLock>>>> = OnceLock::new(); + +pub fn get_main_message_loop() -> Arc>>> { + INSTANCE.get_or_init(|| Arc::new(Mutex::new(None))).clone() +} + +pub fn set_main_message_loop( + mut main_message_loop: Option>, +) -> Option> { + let instance = get_main_message_loop(); + let Ok(mut instance) = instance.lock() else { + return main_message_loop; + }; + mem::swap(&mut *instance, &mut main_message_loop); + main_message_loop +} + +pub fn currently_on_main_thread() -> bool { + let instance = get_main_message_loop(); + let Ok(instance) = instance.lock() else { + return false; + }; + let Some(instance) = instance.as_ref() else { + return false; + }; + instance.run_tasks_on_current_thread() +} + +pub fn main_post_task(task: Option<&mut Task>) { + let instance = get_main_message_loop(); + let Ok(mut instance) = instance.lock() else { + return; + }; + let Some(instance) = instance.as_mut() else { + return; + }; + instance.post_task(task); +} + +pub fn main_post_once(closure: Box) { + let instance = get_main_message_loop(); + let Ok(mut instance) = instance.lock() else { + return; + }; + let Some(instance) = instance.as_mut() else { + return; + }; + instance.post_once(closure); +} + +pub fn main_post_repeating(closure: Box) { + let instance = get_main_message_loop(); + let Ok(mut instance) = instance.lock() else { + return; + }; + let Some(instance) = instance.as_mut() else { + return; + }; + instance.post_repeating(closure); +} + +wrap_task! { + struct OnceClosure { + closure: Arc>>>, + } + + impl Task { + fn execute(&self) { + let Ok(mut closure) = self.closure.lock() else { + return; + }; + let Some(closure) = closure.take() else { + return; + }; + closure(); + } + } +} + +wrap_task! { + struct RepeatingClosure { + closure: Arc>>>, + } + + impl Task { + fn execute(&self) { + let Ok(mut closure) = self.closure.lock() else { + return; + }; + let Some(closure) = closure.as_mut() else { + return; + }; + closure(); + } + } +} + +pub trait MainMessageLoop: Send { + /// Run the message loop. The thread that this method is called on will be considered the main + /// thread. This blocks until [MainMessageLoop::quit] is called. + fn run(&mut self) -> i32; + + /// Quit the message loop. + fn quit(&mut self); + + /// Post a task for execution on the main message loop. + fn post_task(&mut self, task: Option<&mut Task>); + + /// Returns true if this message loop runs tasks on the current thread. + fn run_tasks_on_current_thread(&self) -> bool; + + #[cfg(target_os = "windows")] + fn set_current_modeless_dialog(&mut self, hwnd: HWND); + + /// Post a closure for execution on the main message loop. + fn post_once(&mut self, closure: Box) { + let mut task = OnceClosure::new(Arc::new(Mutex::new(Some(closure)))); + self.post_task(Some(&mut task)); + } + + /// Post a closure for execution on the main message loop. + fn post_repeating(&mut self, closure: Box) { + let mut task = RepeatingClosure::new(Arc::new(Mutex::new(Some(closure)))); + self.post_task(Some(&mut task)); + } +} diff --git a/examples/tests_shared/src/browser/main_message_loop_external_pump/linux.rs b/examples/tests_shared/src/browser/main_message_loop_external_pump/linux.rs new file mode 100644 index 0000000..2f4a425 --- /dev/null +++ b/examples/tests_shared/src/browser/main_message_loop_external_pump/linux.rs @@ -0,0 +1,243 @@ +use super::*; +use glib::*; +use std::{ + io::{self, Read, Write}, + os::fd::AsRawFd, + sync::{Arc, Mutex, Weak}, + thread, +}; + +/// Return a timeout suitable for the glib loop, -1 to block forever, +/// 0 to return right away, or a timeout in milliseconds from now. +fn get_time_interval_milliseconds(cef_time: &cef::Time) -> i32 { + let mut time = 0.0; + time_to_doublet(Some(cef_time), Some(&mut time)); + if time == 0.0 { + return -1; + } + + let mut cef_now = Default::default(); + time_now(Some(&mut cef_now)); + let mut now = 0.0; + time_to_doublet(Some(&cef_now), Some(&mut now)); + + // Be careful here. CefTime has a precision of microseconds, but we want a + // value in milliseconds. If there are 5.5ms left, should the delay be 5 or + // 6? It should be 6 to avoid executing delayed work too early. + let interval = (time - now).ceil() * 1000.0; + let interval = interval as i32; + + // If this value is negative, then we need to run delayed work soon. + interval.max(0) +} + +fn handle_eintr(mut callback: impl FnMut() -> io::Result) -> io::Result { + loop { + match callback() { + Err(err) if err.kind() == io::ErrorKind::Interrupted => continue, + result => break result, + } + } +} + +pub struct MainMessageLoopExternalPumpInner { + should_quit: bool, + main_context: MainContext, + work_source: Source, + timer_source: Option, + delayed_work_time: Arc>>, + wakeup_pipe_write: io::PipeWriter, +} + +impl Drop for MainMessageLoopExternalPumpInner { + fn drop(&mut self) { + self.work_source.destroy(); + } +} + +impl MainMessageLoopExternalPumpInner { + pub fn new(pump: &Weak>) -> Self { + let (mut wakeup_pipe_read, wakeup_pipe_write) = + io::pipe().expect("Failed to create wakeup pipe"); + let delayed_work_time = Arc::new(Mutex::new(None)); + let main_context = MainContext::default(); + let work_source = { + let pump = pump.clone(); + let delayed_work_time = delayed_work_time.clone(); + unix_fd_source_new( + wakeup_pipe_read.as_raw_fd(), + IOCondition::IN, + None, + Priority::DEFAULT_IDLE, + move |raw_fd, condition| { + let Some(pump) = pump.upgrade() else { + return ControlFlow::Break; + }; + let Ok(mut pump) = pump.lock() else { + return ControlFlow::Break; + }; + + // We usually have a single message on the wakeup pipe, since we are only + // signaled when the queue went from empty to non-empty, but there can be + // two messages if a task posted a task, hence we read at most two bytes. + // The glib poll will tell us whether there was data, so this read shouldn't + // block. + if condition.contains(IOCondition::IN) { + assert_eq!(wakeup_pipe_read.as_raw_fd(), raw_fd); + + let mut buffer = [0; 16]; + let size = handle_eintr(|| wakeup_pipe_read.read(&mut buffer)) + .expect("Error reading from the wakeup pipe."); + + match size { + 16 => { + let mut delay_ms = [0; 8]; + delay_ms.copy_from_slice(&buffer[..8]); + pump.on_schedule_work(i64::from_ne_bytes(delay_ms)); + delay_ms.copy_from_slice(&buffer[8..]); + pump.on_schedule_work(i64::from_ne_bytes(delay_ms)); + } + 8..16 => { + let mut delay_ms = [0; 8]; + delay_ms.copy_from_slice(&buffer[..8]); + pump.on_schedule_work(i64::from_ne_bytes(delay_ms)); + } + _ => {} + } + } + + if let Ok(delayed_work_time) = delayed_work_time.lock() { + let delay = delayed_work_time + .as_ref() + .map(get_time_interval_milliseconds) + .unwrap_or_default(); + + if delay == 0 { + // The timer has expired. That condition will stay true until we process + // that delayed work, so we don't need to record this differently. + pump.on_timer_timeout(); + } + } + + ControlFlow::Continue + }, + ) + }; + work_source.attach(Some(&main_context)); + + Self { + should_quit: false, + main_context, + work_source, + timer_source: None, + delayed_work_time, + wakeup_pipe_write, + } + } + + pub fn on_run(&mut self) -> bool { + // We really only do a single task for each iteration of the loop. If we + // have done something, assume there is likely something more to do. This + // will mean that we don't block on the message pump until there was nothing + // more to do. We also set this to true to make sure not to block on the + // first iteration of the loop. + let mut more_work_is_plausible = true; + + // We run our own loop instead of using g_main_loop_quit in one of the + // callbacks. This is so we only quit our own loops, and we don't quit + // nested loops run by others. + loop { + // Don't block if we think we have more work to do. + let block = !more_work_is_plausible; + + more_work_is_plausible = self.main_context.iteration(block); + if self.should_quit { + break; + } + } + + // We need to run the message pump until it is idle. However we don't have + // that information here so we run the message loop "for a while". + for _ in 0..10 { + // Do some work. + do_message_loop_work(); + + // Sleep to allow the CEF proc to do work. + thread::sleep(Duration::from_micros(50000)); + } + + false + } + + pub fn on_quit(&mut self) { + self.should_quit = true; + } + + pub fn on_schedule_message_pump_work(&mut self, delay: i64) { + let buffer = delay.to_ne_bytes(); + let size = handle_eintr(|| self.wakeup_pipe_write.write(&buffer)).unwrap_or_default(); + assert_eq!( + size, 8, + "Could not write to the UI message loop wakeup pipe!" + ); + } + + pub fn set_timer(&mut self, delay: i64) { + assert!(delay > 0); + + let mut delayed_work_time = self + .delayed_work_time + .lock() + .expect("Failed to lock delayed_work_time member"); + + let mut cef_now = Default::default(); + time_now(Some(&mut cef_now)); + let mut now = 0.0; + time_to_doublet(Some(&cef_now), Some(&mut now)); + + let time = now + delay as f64 / 1000.0; + let mut cef_time = Default::default(); + if time_from_doublet(time, Some(&mut cef_time)) == 0 { + panic!("Failed to convert time to CEF time"); + } + + *delayed_work_time = Some(cef_time); + + if let Some(timer_source) = self.timer_source.take() { + self.work_source.remove_child_source(&timer_source); + } + + let timer_source = timeout_source_new( + Duration::from_millis(delay.max(0).unsigned_abs()), + None, + Priority::DEFAULT_IDLE, + || ControlFlow::Continue, + ); + self.work_source.add_child_source(&timer_source); + } + + pub fn kill_timer(&mut self) { + let mut delayed_work_time = self + .delayed_work_time + .lock() + .expect("Failed to lock delayed_work_time member"); + + *delayed_work_time = None; + + if let Some(timer_source) = self.timer_source.take() { + self.work_source.remove_child_source(&timer_source); + } + } + + pub fn is_timer_pending(&self) -> bool { + let delayed_work_time = self + .delayed_work_time + .lock() + .expect("Failed to lock delayed_work_time member"); + let delay = delayed_work_time + .as_ref() + .map(get_time_interval_milliseconds) + .unwrap_or_default(); + delay > 0 + } +} diff --git a/examples/tests_shared/src/browser/main_message_loop_external_pump/mac.rs b/examples/tests_shared/src/browser/main_message_loop_external_pump/mac.rs new file mode 100644 index 0000000..eaee55d --- /dev/null +++ b/examples/tests_shared/src/browser/main_message_loop_external_pump/mac.rs @@ -0,0 +1,135 @@ +use super::*; +use objc2::{define_class, msg_send, rc::Retained, sel, AnyThread, DefinedClass}; +use objc2_app_kit::{NSApp, NSEventTrackingRunLoopMode}; +use objc2_foundation::{ + MainThreadMarker, NSNumber, NSObject, NSObjectNSThreadPerformAdditions, NSObjectProtocol, + NSRunLoop, NSRunLoopCommonModes, NSThread, NSTimer, +}; +use std::sync::{Mutex, Weak}; + +define_class! { + #[unsafe(super(NSObject))] + #[ivars = Weak>] + struct EventHandler; + + impl EventHandler { + #[unsafe(method(scheduleWork:))] + fn schedule_work(&self, delay_ms: &NSNumber) { + let Ok(delay_ms) = i64::try_from(delay_ms.integerValue()) else { + return; + }; + + let Some(pump) = self.ivars().upgrade() else { + return; + }; + let Ok(mut pump) = pump.lock() else { + return; + }; + + pump.on_schedule_work(delay_ms); + } + + #[unsafe(method(timerTimeout:))] + fn timer_timeout(&self, _: &NSTimer) { + let Some(pump) = self.ivars().upgrade() else { + return; + }; + let Ok(mut pump) = pump.lock() else { + return; + }; + + pump.on_timer_timeout(); + } + } + + unsafe impl NSObjectProtocol for EventHandler {} +} + +impl EventHandler { + fn new(pump: Weak>) -> Retained { + let this = Self::alloc().set_ivars(pump); + unsafe { msg_send![super(this), init] } + } +} + +pub struct MainMessageLoopExternalPumpInner { + owner_thread: Retained, + timer: Option>, + event_handler: Retained, +} + +unsafe impl Send for MainMessageLoopExternalPumpInner {} + +impl MainMessageLoopExternalPumpInner { + pub fn new(pump: &Weak>) -> Self { + let event_handler = EventHandler::new(pump.clone()); + Self { + owner_thread: NSThread::currentThread(), + timer: None, + event_handler, + } + } + + pub fn on_run(&mut self) -> bool { + let Some(mtm) = MainThreadMarker::new() else { + return false; + }; + NSApp(mtm).run(); + true + } + + pub fn on_quit(&mut self) { + let Some(mtm) = MainThreadMarker::new() else { + return; + }; + NSApp(mtm).stop(None); + } + + pub fn on_schedule_message_pump_work(&mut self, delay: i64) { + // This method may be called on any thread. + let delay = isize::try_from(delay).unwrap_or(isize::MAX); + let number = NSNumber::numberWithInteger(delay); + unsafe { + self.event_handler + .performSelector_onThread_withObject_waitUntilDone( + sel!(scheduleWork:), + &self.owner_thread, + Some(&number), + false, + ); + } + } + + pub fn set_timer(&mut self, delay: i64) { + let delay_s = delay as f64 / 1000.0; + let timer = unsafe { + NSTimer::timerWithTimeInterval_target_selector_userInfo_repeats( + delay_s, + &self.event_handler, + sel!(timerTimeout:), + None, + false, + ) + }; + + // Add the timer to default and tracking runloop modes. + let owner_runloop = NSRunLoop::currentRunLoop(); + unsafe { + owner_runloop.addTimer_forMode(&timer, NSRunLoopCommonModes); + owner_runloop.addTimer_forMode(&timer, NSEventTrackingRunLoopMode); + } + + self.timer = Some(timer); + } + + pub fn kill_timer(&mut self) { + let Some(timer) = self.timer.take() else { + return; + }; + timer.invalidate(); + } + + pub fn is_timer_pending(&self) -> bool { + self.timer.is_some() + } +} diff --git a/examples/tests_shared/src/browser/main_message_loop_external_pump/mod.rs b/examples/tests_shared/src/browser/main_message_loop_external_pump/mod.rs new file mode 100644 index 0000000..d1ba543 --- /dev/null +++ b/examples/tests_shared/src/browser/main_message_loop_external_pump/mod.rs @@ -0,0 +1,218 @@ +use super::{main_message_loop::*, main_message_loop_std::*}; +use cef::*; +use std::{ + sync::{Arc, Mutex, OnceLock, Weak}, + time::Duration, +}; +#[cfg(target_os = "windows")] +use windows_sys::Win32::Foundation::HWND; + +#[cfg(target_os = "windows")] +mod win; +#[cfg(target_os = "windows")] +use win::MainMessageLoopExternalPumpInner; + +#[cfg(target_os = "macos")] +mod mac; +#[cfg(target_os = "macos")] +use mac::MainMessageLoopExternalPumpInner; + +#[cfg(target_os = "linux")] +mod linux; +#[cfg(target_os = "linux")] +use linux::MainMessageLoopExternalPumpInner; + +/// Special timer delay placeholder value. Intentionally 32-bit for Windows and +/// OS X platform API compatibility. +const TIMER_DELAY_PLACEHOLDER: i32 = i32::MAX; + +// The maximum number of milliseconds we're willing to wait between calls to +// [MainMessageLoopExternalPump::do_work]. +const MAX_TIMER_DELAY: i64 = 1000 / 30; // 30fps + +static INSTANCE: OnceLock>> = OnceLock::new(); + +pub fn get_main_message_loop() -> Option>> { + INSTANCE.get()?.upgrade() +} + +pub fn set_main_message_loop( + main_message_loop: Option>>, +) -> Option>> { + let main_message_loop = main_message_loop + .as_ref() + .map(Arc::downgrade) + .unwrap_or_default(); + if let Err(instance) = INSTANCE.set(main_message_loop) { + instance.upgrade() + } else { + None + } +} + +pub struct MainMessageLoopExternalPump { + standard_message_loop: Arc>>>, + is_active: bool, + reentrancy_detected: bool, + inner: MainMessageLoopExternalPumpInner, +} + +impl MainMessageLoopExternalPump { + pub fn new() -> Arc> { + let external_pump = Arc::new_cyclic(|weak_ref| { + Mutex::new(Self { + standard_message_loop: MainMessageLoopStd::new(), + is_active: false, + reentrancy_detected: false, + inner: MainMessageLoopExternalPumpInner::new(weak_ref), + }) + }); + set_main_message_loop(Some(external_pump.clone())); + external_pump + } + + /// Called from [BrowserProcessHandler::on_schedule_message_pump_work] on any thread. + /// The platform subclass must implement this method and schedule a call to + /// [MainMessageLoopExternalPump::on_schedule_work] on the main application thread. + pub fn on_schedule_message_pump_work(&mut self, delay: i64) { + self.inner.on_schedule_message_pump_work(delay); + } + + fn on_schedule_work(&mut self, delay: i64) { + assert!(currently_on_main_thread()); + + if delay == i64::from(TIMER_DELAY_PLACEHOLDER) && self.is_timer_pending() { + // Don't set the maximum timer requested from DoWork() if a timer event is + // currently pending. + return; + } + + self.kill_timer(); + + let delay = if delay <= 0 { + // Execute the work immediately. + self.do_work(); + 0 + } else { + // Never wait longer than the maximum allowed time. + delay.min(MAX_TIMER_DELAY) + }; + + // Results in call to on_timer_timeout after the specified delay. + self.set_timer(delay); + } + + fn on_timer_timeout(&mut self) { + assert!(currently_on_main_thread()); + self.kill_timer(); + self.do_work(); + } + + /// Control the pending work timer in the platform subclass. Only called on + /// the main application thread. + fn set_timer(&mut self, delay: i64) { + assert!(!self.is_timer_pending()); + assert!(delay > 0); + self.inner.set_timer(delay); + } + + /// Control the pending work timer in the platform subclass. Only called on + /// the main application thread. + fn kill_timer(&mut self) { + self.inner.kill_timer(); + } + + /// Control the pending work timer in the platform subclass. Only called on + /// the main application thread. + fn is_timer_pending(&self) -> bool { + self.inner.is_timer_pending() + } + + /// Handle work processing. + fn do_work(&mut self) { + let was_reentrant = self.perform_message_loop_work(); + if was_reentrant { + self.on_schedule_message_pump_work(0); + } else if !self.is_timer_pending() { + self.on_schedule_message_pump_work(i64::from(TIMER_DELAY_PLACEHOLDER)); + } + } + + fn perform_message_loop_work(&mut self) -> bool { + if self.is_active { + // When do_message_loop_work is called there may be various callbacks + // (such as paint and IPC messages) that result in additional calls to this + // method. If re-entrancy is detected we must repost a request again to the + // owner thread to ensure that the discarded call is executed in the future. + self.reentrancy_detected = true; + return false; + } + + self.reentrancy_detected = false; + + self.is_active = true; + do_message_loop_work(); + self.is_active = false; + + // `reentrancy_detected` may have changed due to re-entrant calls to this + // method. + self.reentrancy_detected + } +} + +impl MainMessageLoop for MainMessageLoopExternalPump { + fn run(&mut self) -> i32 { + if !self.inner.on_run() { + return 0; + } + + self.kill_timer(); + + // We need to run the message pump until it is idle. However we don't have + // that information here so we run the message loop "for a while". + for _ in 0..10 { + // Do some work. + do_message_loop_work(); + + // Sleep to allow the CEF proc to do work. + std::thread::sleep(Duration::from_millis(50)); + } + + 0 + } + + fn quit(&mut self) { + self.inner.on_quit(); + } + + fn post_task(&mut self, task: Option<&mut Task>) { + let Ok(mut standard_message_loop) = self.standard_message_loop.lock() else { + return; + }; + let Some(standard_message_loop) = standard_message_loop.as_mut() else { + return; + }; + standard_message_loop.post_task(task); + } + + fn run_tasks_on_current_thread(&self) -> bool { + let Ok(standard_message_loop) = self.standard_message_loop.lock() else { + return false; + }; + let Some(standard_message_loop) = standard_message_loop.as_ref() else { + return false; + }; + standard_message_loop.run_tasks_on_current_thread() + } + + #[cfg(target_os = "windows")] + fn set_current_modeless_dialog(&mut self, hwnd: HWND) { + let Ok(mut standard_message_loop) = self.standard_message_loop.lock() else { + return; + }; + let Some(standard_message_loop) = standard_message_loop.as_mut() else { + return; + }; + standard_message_loop.set_current_modeless_dialog(hwnd); + } +} diff --git a/examples/tests_shared/src/browser/main_message_loop_external_pump/win.rs b/examples/tests_shared/src/browser/main_message_loop_external_pump/win.rs new file mode 100644 index 0000000..61be6d7 --- /dev/null +++ b/examples/tests_shared/src/browser/main_message_loop_external_pump/win.rs @@ -0,0 +1,145 @@ +use super::*; +use crate::browser::util_win::*; +use std::{ + mem, ptr, + sync::{Mutex, Weak}, +}; +use windows_sys::{w, Win32::Foundation::*, Win32::UI::WindowsAndMessaging::*}; + +const MSG_HAVE_WORK: u32 = WM_USER + 1; + +pub struct MainMessageLoopExternalPumpInner { + timer_pending: bool, + main_thread_target: Option, +} + +impl MainMessageLoopExternalPumpInner { + pub fn new(pump: &Weak>) -> Self { + let main_thread_target = { + const WINDOW_CLASS_NAME: *const u16 = w!("MainMessageLoopExternalPump"); + + let instance = get_code_module_handle(); + unsafe { + let wcex = WNDCLASSEXW { + cbSize: mem::size_of::() as u32, + lpfnWndProc: Some(Self::window_proc), + hInstance: instance, + lpszClassName: WINDOW_CLASS_NAME, + ..mem::zeroed() + }; + RegisterClassExW(&wcex); + + // Create the message handling window. + let main_thread_target = CreateWindowExW( + 0, + WINDOW_CLASS_NAME, + ptr::null(), + WS_OVERLAPPEDWINDOW, + 0, + 0, + 0, + 0, + HWND_MESSAGE, + ptr::null_mut(), + instance, + ptr::null(), + ); + assert!(!main_thread_target.is_null()); + main_thread_target + } + }; + + set_user_data(main_thread_target, Some(pump.clone())); + + Self { + timer_pending: false, + main_thread_target: Some(main_thread_target as usize), + } + } + + unsafe extern "system" fn window_proc( + hwnd: HWND, + message: u32, + wparam: WPARAM, + lparam: LPARAM, + ) -> LRESULT { + match message { + MSG_HAVE_WORK | WM_TIMER => { + if let Some(message_loop) = + get_user_data::>>(hwnd) + { + if let Some(message_loop) = message_loop.upgrade() { + if let Ok(mut message_loop) = message_loop.lock() { + if message == MSG_HAVE_WORK { + let delay = lparam as i64; + message_loop.on_schedule_work(delay); + } else { + message_loop.on_timer_timeout(); + } + } + } + } + } + WM_DESTROY => { + let _ = set_user_data::>>(hwnd, None); + } + _ => {} + } + DefWindowProcW(hwnd, message, wparam, lparam) + } + + pub fn on_run(&mut self) -> bool { + let mut msg = Default::default(); + unsafe { + while GetMessageW(&mut msg, ptr::null_mut(), 0, 0) != 0 { + TranslateMessage(&msg); + DispatchMessageW(&msg); + } + } + true + } + + pub fn on_quit(&mut self) { + unsafe { + PostMessageW(ptr::null_mut(), WM_QUIT, 0, 0); + } + } + + pub fn on_schedule_message_pump_work(&mut self, delay: i64) { + // This method may be called on any thread. + unsafe { + if let Some(main_thread_target) = self.main_thread_target { + PostMessageW( + main_thread_target as HWND, + MSG_HAVE_WORK, + 0, + delay as LPARAM, + ); + } + } + } + + pub fn set_timer(&mut self, delay: i64) { + self.timer_pending = true; + if let Some(main_thread_target) = self.main_thread_target { + unsafe { + SetTimer(main_thread_target as HWND, 1, delay as u32, None); + } + } + } + + pub fn kill_timer(&mut self) { + if self.timer_pending { + if let Some(main_thread_target) = self.main_thread_target { + unsafe { + KillTimer(main_thread_target as HWND, 1); + } + } + self.timer_pending = false; + } + } + + pub fn is_timer_pending(&self) -> bool { + self.timer_pending + } +} diff --git a/examples/tests_shared/src/browser/main_message_loop_std.rs b/examples/tests_shared/src/browser/main_message_loop_std.rs new file mode 100644 index 0000000..d6ccf75 --- /dev/null +++ b/examples/tests_shared/src/browser/main_message_loop_std.rs @@ -0,0 +1,46 @@ +use super::main_message_loop::*; +use cef::*; +use std::sync::{Arc, Mutex}; + +#[cfg(target_os = "windows")] +use windows_sys::Win32::Foundation::HWND; + +pub struct MainMessageLoopStd; + +impl MainMessageLoopStd { + pub fn new() -> Arc>>> { + set_main_message_loop(Some(Box::new(MainMessageLoopStd))); + get_main_message_loop() + } +} + +impl Drop for MainMessageLoopStd { + fn drop(&mut self) { + set_main_message_loop(None); + } +} + +impl MainMessageLoop for MainMessageLoopStd { + fn run(&mut self) -> i32 { + run_message_loop(); + 0 + } + + fn quit(&mut self) { + quit_message_loop(); + } + + fn post_task(&mut self, task: Option<&mut Task>) { + post_task(ThreadId::UI, task); + } + + fn run_tasks_on_current_thread(&self) -> bool { + currently_on(ThreadId::UI) != 0 + } + + #[cfg(target_os = "windows")] + fn set_current_modeless_dialog(&mut self, _hwnd: HWND) { + // Nothing to do here. The Chromium message loop implementation will internally route + // dialog messages. + } +} diff --git a/examples/tests_shared/src/browser/mod.rs b/examples/tests_shared/src/browser/mod.rs new file mode 100644 index 0000000..7c724a2 --- /dev/null +++ b/examples/tests_shared/src/browser/mod.rs @@ -0,0 +1,10 @@ +pub mod client_app_browser; +pub mod file_util; +pub mod geometry_util; +pub mod main_message_loop; +pub mod main_message_loop_external_pump; +pub mod main_message_loop_std; +pub mod resource_util; + +#[cfg(target_os = "windows")] +pub mod util_win; diff --git a/examples/tests_shared/src/browser/resource_util/mod.rs b/examples/tests_shared/src/browser/resource_util/mod.rs new file mode 100644 index 0000000..84b172a --- /dev/null +++ b/examples/tests_shared/src/browser/resource_util/mod.rs @@ -0,0 +1,9 @@ +#[cfg(target_os = "windows")] +pub mod win; +#[cfg(target_os = "windows")] +pub use win::*; + +#[cfg(not(target_os = "windows"))] +pub mod posix; +#[cfg(not(target_os = "windows"))] +pub use posix::*; diff --git a/examples/tests_shared/src/browser/resource_util/posix.rs b/examples/tests_shared/src/browser/resource_util/posix.rs new file mode 100644 index 0000000..4f31a97 --- /dev/null +++ b/examples/tests_shared/src/browser/resource_util/posix.rs @@ -0,0 +1,42 @@ +use cef::*; +use std::{fs::File, io::Read, path::PathBuf}; + +pub fn get_resource_directory() -> Option { + let mut path = std::env::current_exe().ok()?; + + // Pop the executable file name. + path.pop(); + + #[cfg(target_os = "macos")] + { + // Pop the MacOS directory. + path.pop(); + path.push("Resources"); + } + + #[cfg(target_os = "linux")] + { + path.push("files"); + } + + Some(path) +} + +pub fn load_binary_resource(resource_name: &str) -> Option> { + let path = get_resource_directory()?.join(resource_name); + let mut file = File::open(path).ok()?; + let mut data = Vec::new(); + file.read_to_end(&mut data).ok()?; + Some(data) +} + +pub fn get_binary_resource_reader(resource_name: &str) -> Option { + let path = get_resource_directory()?.join(resource_name); + if !path.exists() { + return None; + } + + let path = path.to_str()?; + let path = CefString::from(path); + stream_reader_create_for_file(Some(&CefString::from(path))) +} diff --git a/examples/tests_shared/src/browser/resource_util/win.rs b/examples/tests_shared/src/browser/resource_util/win.rs new file mode 100644 index 0000000..42493af --- /dev/null +++ b/examples/tests_shared/src/browser/resource_util/win.rs @@ -0,0 +1,124 @@ +use crate::browser::util_win::*; +use cef::{ + wrapper::{byte_read_handler::*, resource_manager::*, stream_resource_handler::*}, + *, +}; +use std::{ + mem, + sync::{Arc, Mutex, OnceLock}, +}; +use windows_sys::Win32::System::LibraryLoader::{ + FindResourceW, LoadResource, LockResource, SizeofResource, +}; + +pub type GetResourceId = Box u16>; + +static INSTANCE: OnceLock>>> = OnceLock::new(); + +pub fn get_fn_get_resource_id() -> Arc>> { + INSTANCE.get_or_init(|| Arc::new(Mutex::new(None))).clone() +} + +pub fn set_fn_get_resource_id(mut get_resource_id: Option) -> Option { + let instance = get_fn_get_resource_id(); + let Ok(mut instance) = instance.lock() else { + return get_resource_id; + }; + mem::swap(&mut *instance, &mut get_resource_id); + get_resource_id +} + +pub fn load_binary_resource(resource_name: &str) -> Option> { + let get_resource_id = get_fn_get_resource_id(); + let get_resource_id = get_resource_id.lock().ok()?; + let get_resource_id = get_resource_id.as_ref()?; + + let resource_id = (*get_resource_id)(resource_name); + let instance = get_code_module_handle(); + + unsafe { + // Defined in https://github.com/chromiumembedded/cef/blob/master/tests/cefclient/browser/resource.h + const RT_BINARY: u16 = 256; + + let resource = FindResourceW( + instance, + resource_id as usize as *const _, + RT_BINARY as usize as *const _, + ); + if resource.is_null() { + return None; + } + + let data = LoadResource(instance, resource); + if data.is_null() { + return None; + } + + let size = SizeofResource(instance, resource); + if size == 0 { + return None; + } + + let ptr = LockResource(data); + if ptr.is_null() { + return None; + } + + Some(std::slice::from_raw_parts(ptr as *const u8, size as usize).to_vec()) + } +} + +pub fn get_binary_resource_reader(resource_name: &str) -> Option { + let data = load_binary_resource(resource_name)?; + let stream = ByteStream::new(data); + let mut handler = ByteReadHandler::new(Arc::new(Mutex::new(stream))); + stream_reader_create_for_handler(Some(&mut handler)) +} + +struct BinaryResourceProvider { + url_path: String, + resource_path_prefix: String, +} + +impl BinaryResourceProvider { + fn new(url_path: &str, resource_path_prefix: &str) -> Self { + Self { + url_path: normalize_url_path(url_path), + resource_path_prefix: normalize_url_path(resource_path_prefix), + } + } +} + +impl ResourceManagerProvider for BinaryResourceProvider { + fn on_request(&self, request: Arc>) -> bool { + assert_ne!( + currently_on(ThreadId::IO), + 0, + "on_request must be called on the IO thread" + ); + + let Ok(mut request) = request.lock() else { + return false; + }; + let url = request.url(); + let Some(relative_path) = url.strip_prefix(self.url_path.as_str()) else { + // Not handled by this provider. + return false; + }; + + let mime_type = request.mime_type_resolver()(url); + let relative_path = format!("{}/{relative_path}", self.resource_path_prefix); + let handler = get_binary_resource_reader(&relative_path) + .map(|stream| StreamResourceHandler::new_with_stream(mime_type, stream)); + + request.continue_request(handler); + true + } +} + +pub fn create_binary_resource_provider( + url_path: &str, + resource_path_prefix: &str, +) -> Box { + Box::new(BinaryResourceProvider::new(url_path, resource_path_prefix)) +} diff --git a/examples/tests_shared/src/browser/util_win.rs b/examples/tests_shared/src/browser/util_win.rs new file mode 100644 index 0000000..5491b87 --- /dev/null +++ b/examples/tests_shared/src/browser/util_win.rs @@ -0,0 +1,256 @@ +use std::{ffi::c_void, mem, ptr, sync::OnceLock}; +use windows_sys::Win32::{ + Foundation::*, + Graphics::Gdi::*, + System::{ + LibraryLoader::{ + GetModuleHandleExW, GET_MODULE_HANDLE_EX_FLAG_FROM_ADDRESS, + GET_MODULE_HANDLE_EX_FLAG_UNCHANGED_REFCOUNT, + }, + Performance::*, + SystemServices::*, + }, + UI::{Input::KeyboardAndMouse::*, WindowsAndMessaging::*}, +}; + +pub fn get_time_now() -> u64 { + static FREQUENCY: OnceLock = OnceLock::new(); + + let frequency = FREQUENCY.get_or_init(|| { + let mut frequency = 0; + unsafe { QueryPerformanceFrequency(&mut frequency) }; + frequency.max(1) as f64 / 1000000.0 + }); + + let mut current_time = 0; + unsafe { QueryPerformanceCounter(&mut current_time) }; + ((current_time as f64 / *frequency) as i64).max(0) as u64 +} + +pub fn set_user_data_ptr(hwnd: HWND, data: *mut c_void) -> *mut c_void { + unsafe { + SetLastError(ERROR_SUCCESS); + let result = SetWindowLongPtrW(hwnd, GWLP_USERDATA, data as isize); + assert!(result != 0 || GetLastError() == ERROR_SUCCESS); + result as *mut _ + } +} + +pub fn set_user_data(hwnd: HWND, data: Option) -> Option> { + let ptr: *mut T = set_user_data_ptr( + hwnd, + data.map(|data| Box::into_raw(Box::new(data)).cast()) + .unwrap_or(ptr::null_mut()), + ) + .cast(); + if ptr.is_null() { + None + } else { + unsafe { Some(Box::from_raw(ptr)) } + } +} + +pub fn get_user_data<'a, T>(hwnd: HWND) -> Option<&'a mut T> { + unsafe { + let ptr = GetWindowLongPtrW(hwnd, GWLP_USERDATA) as *mut T; + ptr.as_mut() + } +} + +pub fn set_window_proc_ptr(hwnd: HWND, proc: WNDPROC) -> WNDPROC { + Some(unsafe { + let old = GetWindowLongPtrW(hwnd, GWLP_WNDPROC); + assert_ne!(old, 0); + if let Some(proc) = proc { + let result = SetWindowLongPtrW(hwnd, GWLP_WNDPROC, mem::transmute(proc)); + assert!(result != 0 || GetLastError() == ERROR_SUCCESS); + } + mem::transmute(old) + }) +} + +#[repr(i32)] +#[derive(Default, Debug)] +pub enum EventFlag { + #[default] + None = 0, + CapsLockOn = 1 << 0, + ShiftDown = 1 << 1, + ControlDown = 1 << 2, + AltDown = 1 << 3, + LeftMouseButton = 1 << 4, + MiddleMouseButton = 1 << 5, + RightMouseButton = 1 << 6, + CommandDown = 1 << 7, + NumLockOn = 1 << 8, + IsKeyPad = 1 << 9, + IsLeft = 1 << 10, + IsRight = 1 << 11, + AltGrDown = 1 << 12, + IsRepeat = 1 << 13, + PrecisionScrollingDelta = 1 << 14, + ScrollByPage = 1 << 15, +} + +pub fn get_resource_string(id: u32) -> String { + let mut buffer = [0u16; 100]; + String::from_utf16_lossy(unsafe { + let len = LoadStringW( + get_code_module_handle(), + id, + buffer.as_mut_ptr(), + buffer.len() as i32, + ) as usize; + &buffer[..len] + }) +} + +pub fn get_cef_mouse_modifiers(wparam: WPARAM) -> i32 { + let mut modifiers = 0; + + if wparam & MK_CONTROL as usize != 0 { + modifiers |= EventFlag::ControlDown as i32; + } + if wparam & MK_SHIFT as usize != 0 { + modifiers |= EventFlag::ShiftDown as i32; + } + if is_key_down(VK_MENU as _) { + modifiers |= EventFlag::AltDown as i32; + } + if wparam & MK_LBUTTON as usize != 0 { + modifiers |= EventFlag::LeftMouseButton as i32; + } + if wparam & MK_MBUTTON as usize != 0 { + modifiers |= EventFlag::MiddleMouseButton as i32; + } + if wparam & MK_RBUTTON as usize != 0 { + modifiers |= EventFlag::RightMouseButton as i32; + } + + // Low bit set from GetKeyState indicates "toggled". + unsafe { + if GetKeyState(VK_NUMLOCK as i32) & 1 != 0 { + modifiers |= EventFlag::NumLockOn as i32; + } + if GetKeyState(VK_CAPITAL as i32) & 1 != 0 { + modifiers |= EventFlag::CapsLockOn as i32; + } + } + + modifiers +} + +pub fn get_cef_keyboard_modifiers(wparam: WPARAM, lparam: LPARAM) -> i32 { + let mut modifiers = 0; + + if is_key_down(VK_SHIFT as _) { + modifiers |= EventFlag::ShiftDown as i32; + } + if is_key_down(VK_CONTROL as _) { + modifiers |= EventFlag::ControlDown as i32; + } + if is_key_down(VK_MENU as _) { + modifiers |= EventFlag::AltDown as i32; + } + + // Low bit set from GetKeyState indicates "toggled". + unsafe { + if GetKeyState(VK_NUMLOCK as i32) & 1 != 0 { + modifiers |= EventFlag::NumLockOn as i32; + } + if GetKeyState(VK_CAPITAL as i32) & 1 != 0 { + modifiers |= EventFlag::CapsLockOn as i32; + } + } + + match wparam as VIRTUAL_KEY { + VK_RETURN => { + if (lparam >> 16) & KF_EXTENDED as isize != 0 { + modifiers |= EventFlag::IsKeyPad as i32; + } + } + VK_INSERT | VK_DELETE | VK_HOME | VK_END | VK_PRIOR | VK_NEXT | VK_UP | VK_DOWN + | VK_LEFT | VK_RIGHT => { + if (lparam >> 16) & KF_EXTENDED as isize == 0 { + modifiers |= EventFlag::IsKeyPad as i32; + } + } + VK_NUMLOCK | VK_NUMPAD0 | VK_NUMPAD1 | VK_NUMPAD2 | VK_NUMPAD3 | VK_NUMPAD4 + | VK_NUMPAD5 | VK_NUMPAD6 | VK_NUMPAD7 | VK_NUMPAD8 | VK_NUMPAD9 | VK_DIVIDE + | VK_MULTIPLY | VK_SUBTRACT | VK_ADD | VK_DECIMAL | VK_CLEAR => { + modifiers |= EventFlag::IsKeyPad as i32; + } + VK_SHIFT => { + if is_key_down(VK_LSHIFT as _) { + modifiers |= EventFlag::IsLeft as i32; + } else if is_key_down(VK_RSHIFT as _) { + modifiers |= EventFlag::IsRight as i32; + } + } + VK_CONTROL => { + if is_key_down(VK_LCONTROL as _) { + modifiers |= EventFlag::IsLeft as i32; + } else if is_key_down(VK_RCONTROL as _) { + modifiers |= EventFlag::IsRight as i32; + } + } + VK_MENU => { + if is_key_down(VK_LMENU as _) { + modifiers |= EventFlag::IsLeft as i32; + } else if is_key_down(VK_RMENU as _) { + modifiers |= EventFlag::IsRight as i32; + } + } + VK_LWIN => { + modifiers |= EventFlag::IsLeft as i32; + } + VK_RWIN => { + modifiers |= EventFlag::IsRight as i32; + } + _ => {} + } + + modifiers +} + +pub fn is_key_down(wparam: WPARAM) -> bool { + unsafe { + let key_state = GetKeyState(wparam as i32) as u16; + key_state & 0x8000 != 0 + } +} + +pub fn get_device_scale_factor() -> f32 { + static SCALE_FACTOR: OnceLock = OnceLock::new(); + + *SCALE_FACTOR.get_or_init(|| unsafe { + let screen_dc = GetDC(ptr::null_mut()); + let dpi_x = GetDeviceCaps(screen_dc, LOGPIXELSX as i32); + ReleaseDC(ptr::null_mut(), screen_dc); + dpi_x as f32 / 96.0 + }) +} + +pub fn get_code_module_handle() -> HINSTANCE { + let mut module: HMODULE = ptr::null_mut(); + unsafe { + let result = GetModuleHandleExW( + GET_MODULE_HANDLE_EX_FLAG_FROM_ADDRESS | GET_MODULE_HANDLE_EX_FLAG_UNCHANGED_REFCOUNT, + get_code_module_handle as *const _, + &mut module, + ); + assert_ne!(result, 0); + } + module +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_get_time_now() { + let time = get_time_now(); + assert!(time > 0); + } +} diff --git a/examples/tests_shared/src/common/binary_value_utils.rs b/examples/tests_shared/src/common/binary_value_utils.rs new file mode 100644 index 0000000..0ca92b4 --- /dev/null +++ b/examples/tests_shared/src/common/binary_value_utils.rs @@ -0,0 +1,147 @@ +use cef::*; +use std::{ + fmt::Debug, + sync::OnceLock, + time::{Duration, Instant}, +}; + +pub const TEST_SEND_PROCESS_MESSAGE: &[u8] = b"testSendProcessMessage"; +pub const TEST_SEND_SMR_PROCESS_MESSAGE: &[u8] = b"testSendSMRProcessMessage"; + +#[derive(Debug)] +pub struct MessageId { + pub id: u32, +} + +impl From for MessageId { + fn from(id: u32) -> Self { + Self { id } + } +} + +impl From<&[u8]> for MessageId { + fn from(bytes: &[u8]) -> Self { + assert_eq!(bytes.len(), 4); + let mut id = [0; 4]; + id.copy_from_slice(bytes); + Self::from(u32::from_ne_bytes(id)) + } +} + +impl From<&MessageId> for [u8; 4] { + fn from(id: &MessageId) -> Self { + id.id.to_ne_bytes() + } +} + +pub struct ElapsedMicros { + duration: u128, +} + +impl ElapsedMicros { + pub fn now() -> Self { + static START_TIME: OnceLock = OnceLock::new(); + let start_time = START_TIME.get_or_init(Instant::now); + + Self { + duration: start_time.elapsed().as_micros(), + } + } +} + +impl From<&[u8]> for ElapsedMicros { + fn from(bytes: &[u8]) -> Self { + assert_eq!(bytes.len(), 16); + let mut duration = [0; 16]; + duration.copy_from_slice(bytes); + Self { + duration: u128::from_ne_bytes(duration), + } + } +} + +impl From<&ElapsedMicros> for [u8; 16] { + fn from(elapsed: &ElapsedMicros) -> Self { + elapsed.duration.to_ne_bytes() + } +} + +impl Debug for ElapsedMicros { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + let duration = u64::try_from(self.duration).unwrap_or(u64::MAX); + let duration = Duration::from_micros(duration); + write!(f, "{duration:?}") + } +} + +#[derive(Debug)] +pub struct BrowserMessage { + pub test_id: MessageId, + pub start_time: ElapsedMicros, +} + +impl From<&BinaryValue> for BrowserMessage { + fn from(message: &BinaryValue) -> Self { + assert_eq!(message.size(), 20); + + let mut data = vec![0; message.size()]; + message.data(Some(&mut data), 0); + + Self { + test_id: MessageId::from(&data[0..4]), + start_time: ElapsedMicros::from(&data[4..20]), + } + } +} + +impl From<&BrowserMessage> for Option { + fn from(message: &BrowserMessage) -> Self { + let mut data = vec![0; 20]; + + let test_id: [u8; 4] = (&message.test_id).into(); + let start_time: [u8; 16] = (&message.start_time).into(); + + data[0..4].copy_from_slice(&test_id); + data[4..20].copy_from_slice(&start_time); + + binary_value_create(Some(&data)) + } +} + +#[derive(Debug)] +pub struct RendererMessage { + pub test_id: MessageId, + pub duration: ElapsedMicros, + pub start_time: ElapsedMicros, +} + +impl From<&BinaryValue> for RendererMessage { + fn from(message: &BinaryValue) -> Self { + assert_eq!(message.size(), 36); + + let mut data = vec![0; message.size()]; + message.data(Some(&mut data), 0); + + Self { + test_id: MessageId::from(&data[0..4]), + duration: ElapsedMicros::from(&data[4..20]), + start_time: ElapsedMicros::from(&data[20..36]), + } + } +} + +impl From<&RendererMessage> for Option { + fn from(message: &RendererMessage) -> Self { + let mut data = vec![0; 36]; + + let test_id: [u8; 4] = (&message.test_id).into(); + let duration: [u8; 16] = (&message.duration).into(); + let start_time: [u8; 16] = (&message.start_time).into(); + + data[0..4].copy_from_slice(&test_id); + data[4..20].copy_from_slice(&duration); + data[20..36].copy_from_slice(&start_time); + + binary_value_create(Some(&data)) + } +} diff --git a/examples/tests_shared/src/common/client_app.rs b/examples/tests_shared/src/common/client_app.rs new file mode 100644 index 0000000..3d8e10e --- /dev/null +++ b/examples/tests_shared/src/common/client_app.rs @@ -0,0 +1,66 @@ +use cef::*; + +pub const PROCESS_TYPE: &str = "type"; +pub const RENDERER_PROCESS: &str = "renderer"; +#[cfg(target_os = "linux")] +pub const ZYGOTE_PROCESS: &str = "zygote"; + +pub enum ProcessType { + Browser, + Renderer, + #[cfg(target_os = "linux")] + Zygote, + Other, +} + +impl From<&CommandLine> for ProcessType { + fn from(value: &CommandLine) -> Self { + let process_type = CefString::from(PROCESS_TYPE); + if value.has_switch(Some(&process_type)) == 0 { + return Self::Browser; + } + + let value = CefString::from(&value.switch_value(Some(&process_type))).to_string(); + match value.as_str() { + RENDERER_PROCESS => Self::Renderer, + #[cfg(target_os = "linux")] + ZYGOTE_PROCESS => Self::Zygote, + _ => Self::Other, + } + } +} + +#[derive(Clone)] +pub struct ClientAppCustomScheme { + name: String, + options: i32, +} + +impl ClientAppCustomScheme { + pub fn new(name: &str, options: &[SchemeOptions]) -> Self { + let options = options.iter().fold(0, |acc, opt| acc | opt.get_raw()) as i32; + Self { + name: name.to_string(), + options, + } + } +} + +wrap_app! { + pub struct ClientApp { + custom_schemes: Vec, + } + + impl App { + fn on_register_custom_schemes(&self, registrar: Option<&mut SchemeRegistrar>) { + let Some(registrar) = registrar else { + return; + }; + + for scheme in &self.custom_schemes { + let name = CefString::from(scheme.name.as_str()); + registrar.add_custom_scheme(Some(&name), scheme.options); + } + } + } +} diff --git a/examples/tests_shared/src/common/client_app_other.rs b/examples/tests_shared/src/common/client_app_other.rs new file mode 100644 index 0000000..4e37ebd --- /dev/null +++ b/examples/tests_shared/src/common/client_app_other.rs @@ -0,0 +1,13 @@ +use cef::*; + +wrap_app! { + pub struct ClientAppOther { + base: App, + } + + impl App { + fn on_register_custom_schemes(&self, registrar: Option<&mut SchemeRegistrar>) { + self.base.on_register_custom_schemes(registrar); + } + } +} diff --git a/examples/tests_shared/src/common/client_switches.rs b/examples/tests_shared/src/common/client_switches.rs new file mode 100644 index 0000000..f39bf4e --- /dev/null +++ b/examples/tests_shared/src/common/client_switches.rs @@ -0,0 +1,52 @@ +//! CEF and Chromium support a wide range of command-line switches. This file +//! only contains command-line switches specific to the cefclient application. +//! View CEF/Chromium documentation or search for *_switches.cc files in the +//! Chromium source code to identify other existing command-line switches. + +pub const MULTI_THREADED_MESSAGE_LOOP: &str = "multi-threaded-message-loop"; +pub const EXTERNAL_MESSAGE_PUMP: &str = "external-message-pump"; +pub const CACHE_PATH: &str = "cache-path"; +pub const URL: &str = "url"; +pub const OFF_SCREEN_RENDERING_ENABLED: &str = "off-screen-rendering-enabled"; +pub const OFF_SCREEN_FRAME_RATE: &str = "off-screen-frame-rate"; +pub const TRANSPARENT_PAINTING_ENABLED: &str = "transparent-painting-enabled"; +pub const SHOW_UPDATE_RECT: &str = "show-update-rect"; +pub const FAKE_SCREEN_BOUNDS: &str = "fake-screen-bounds"; +pub const SHARED_TEXTURE_ENABLED: &str = "shared-texture-enabled"; +pub const EXTERNAL_BEGIN_FRAME_ENABLED: &str = "external-begin-frame-enabled"; +pub const MOUSE_CURSOR_CHANGE_DISABLED: &str = "mouse-cursor-change-disabled"; +pub const OFFLINE: &str = "offline"; +pub const FILTER_CHROME_COMMANDS: &str = "filter-chrome-commands"; +pub const REQUEST_CONTEXT_PER_BROWSER: &str = "request-context-per-browser"; +pub const REQUEST_CONTEXT_SHARED_CACHE: &str = "request-context-shared-cache"; +pub const BACKGROUND_COLOR: &str = "background-color"; +pub const ENABLE_GPU: &str = "enable-gpu"; +pub const FILTER_URL: &str = "filter-url"; +pub const USE_VIEWS: &str = "use-views"; +pub const USE_NATIVE: &str = "use-native"; +pub const HIDE_FRAME: &str = "hide-frame"; +pub const HIDE_CONTROLS: &str = "hide-controls"; +pub const HIDE_OVERLAYS: &str = "hide-overlays"; +pub const ALWAYS_ON_TOP: &str = "always-on-top"; +pub const HIDE_TOP_MENU: &str = "hide-top-menu"; +pub const SSL_CLIENT_CERTIFICATE: &str = "ssl-client-certificate"; +pub const CRL_SETS_PATH: &str = "crl-sets-path"; +pub const NO_ACTIVATE: &str = "no-activate"; +pub const SHOW_CHROME_TOOLBAR: &str = "show-chrome-toolbar"; +pub const INITIAL_SHOW_STATE: &str = "initial-show-state"; +pub const USE_DEFAULT_POPUP: &str = "use-default-popup"; +pub const USE_CLIENT_DIALOGS: &str = "use-client-dialogs"; +pub const USE_TEST_HTTP_SERVER: &str = "use-test-http-server"; +pub const SHOW_WINDOW_BUTTONS: &str = "show-window-buttons"; +pub const USE_WINDOW_MODAL_DIALOG: &str = "use-window-modal-dialog"; +pub const USE_BOTTOM_CONTROLS: &str = "use-bottom-controls"; +pub const HIDE_PIP_FRAME: &str = "hide-pip-frame"; +pub const MOVE_PIP_ENABLED: &str = "move-pip-enabled"; +pub const HIDE_CHROME_BUBBLES: &str = "hide-chrome-bubbles"; +pub const HIDE_WINDOW_ON_CLOSE: &str = "hide-window-on-close"; +pub const ACCEPTS_FIRST_MOUSE: &str = "accepts-first-mouse"; +pub const USE_ALLOY_STYLE: &str = "use-alloy-style"; +pub const USE_CHROME_STYLE_WINDOW: &str = "use-chrome-style-window"; +pub const SHOW_OVERLAY_BROWSER: &str = "show-overlay-browser"; +pub const USE_ANGLE: &str = "use-angle"; +pub const OZONE_PLATFORM: &str = "ozone-platform"; diff --git a/examples/tests_shared/src/common/mod.rs b/examples/tests_shared/src/common/mod.rs new file mode 100644 index 0000000..612571c --- /dev/null +++ b/examples/tests_shared/src/common/mod.rs @@ -0,0 +1,4 @@ +pub mod binary_value_utils; +pub mod client_app; +pub mod client_app_other; +pub mod client_switches; diff --git a/examples/tests_shared/src/lib.rs b/examples/tests_shared/src/lib.rs new file mode 100644 index 0000000..1884222 --- /dev/null +++ b/examples/tests_shared/src/lib.rs @@ -0,0 +1,6 @@ +pub mod browser; +pub mod common; +pub mod renderer; + +#[cfg(target_os = "macos")] +pub mod process_helper_mac; diff --git a/examples/tests_shared/src/process_helper_mac.rs b/examples/tests_shared/src/process_helper_mac.rs new file mode 100644 index 0000000..bfd896f --- /dev/null +++ b/examples/tests_shared/src/process_helper_mac.rs @@ -0,0 +1,44 @@ +use crate::{ + common::{client_app::*, client_app_other::*}, + renderer::client_app_renderer::*, +}; +use cef::{args::Args, *}; + +pub fn run_main( + args: Args, + custom_schemes: Vec, + app_renderer_delegates: Vec>, +) -> Result<(), i32> { + #[cfg(feature = "sandbox")] + let _sandbox = { + let mut sandbox = cef::sandbox::Sandbox::new(); + sandbox.initialize(args.as_main_args()); + sandbox + }; + + let _loader = { + let loader = library_loader::LibraryLoader::new(&std::env::current_exe().unwrap(), true); + assert!(loader.load()); + loader + }; + + let _ = api_hash(sys::CEF_API_VERSION_LAST, 0); + + let app = ClientApp::new(custom_schemes); + let mut app = match args.as_cmd_line().map(|cmd| ProcessType::from(&cmd)) { + Some(ProcessType::Renderer) => { + let app_renderer = ClientAppRenderer::new(app_renderer_delegates); + ClientAppRendererApp::new(app, app_renderer) + } + _ => ClientAppOther::new(app), + }; + + match execute_process( + Some(args.as_main_args()), + Some(&mut app), + std::ptr::null_mut(), + ) { + 0 => Ok(()), + err => Err(err), + } +} diff --git a/examples/tests_shared/src/renderer/client_app_renderer.rs b/examples/tests_shared/src/renderer/client_app_renderer.rs new file mode 100644 index 0000000..3630fb2 --- /dev/null +++ b/examples/tests_shared/src/renderer/client_app_renderer.rs @@ -0,0 +1,255 @@ +use cef::*; +use std::sync::Arc; + +pub trait Delegate: Send { + fn on_web_kit_initialized(&self, _app: &Arc) {} + + fn on_browser_created( + &self, + _app: &Arc, + _browser: Option<&Browser>, + _extra_info: Option<&DictionaryValue>, + ) { + } + + fn on_browser_destroyed(&self, _app: &Arc, _browser: Option<&Browser>) {} + + fn load_handler(&self, _app: &Arc) -> Option { + None + } + + fn on_context_created( + &self, + _app: &Arc, + _browser: Option<&Browser>, + _frame: Option<&Frame>, + _context: Option<&V8Context>, + ) { + } + + fn on_context_released( + &self, + _app: &Arc, + _browser: Option<&Browser>, + _frame: Option<&Frame>, + _context: Option<&V8Context>, + ) { + } + + fn on_uncaught_exception( + &self, + _app: &Arc, + _browser: Option<&Browser>, + _frame: Option<&Frame>, + _context: Option<&V8Context>, + _exception: Option<&V8Exception>, + _stack_trace: Option<&V8StackTrace>, + ) { + } + + fn on_focused_node_changed( + &self, + _app: &Arc, + _browser: Option<&Browser>, + _frame: Option<&Frame>, + _node: Option<&Domnode>, + ) { + } + + fn on_process_message_received( + &self, + _app: &Arc, + _browser: Option<&Browser>, + _frame: Option<&Frame>, + _source_process: ProcessId, + _message: Option<&ProcessMessage>, + ) -> i32 { + 0 + } +} + +pub struct ClientAppRenderer { + delegates: Vec>, +} + +impl ClientAppRenderer { + pub fn new(delegates: Vec>) -> Arc { + Arc::new(Self { delegates }) + } + + pub fn delegates(&self) -> &[Box] { + &self.delegates + } +} + +wrap_render_process_handler! { + struct ClientAppRendererRenderProcessHandler { + client_app_renderer: Arc, + } + + impl RenderProcessHandler { + fn on_web_kit_initialized(&self) { + for delegate in self.client_app_renderer.delegates() { + delegate.on_web_kit_initialized(&self.client_app_renderer); + } + } + + fn on_browser_created( + &self, + browser: Option<&mut Browser>, + extra_info: Option<&mut DictionaryValue>, + ) { + let browser = browser.cloned(); + let extra_info = extra_info.cloned(); + for delegate in self.client_app_renderer.delegates() { + delegate.on_browser_created( + &self.client_app_renderer, + browser.as_ref(), + extra_info.as_ref(), + ); + } + } + + fn on_browser_destroyed(&self, browser: Option<&mut Browser>) { + let browser = browser.cloned(); + for delegate in self.client_app_renderer.delegates() { + delegate.on_browser_destroyed(&self.client_app_renderer, browser.as_ref()); + } + } + + fn load_handler(&self) -> Option { + for delegate in self.client_app_renderer.delegates() { + if let Some(load_handler) = delegate.load_handler(&self.client_app_renderer) { + return Some(load_handler); + } + } + None + } + + fn on_context_created( + &self, + browser: Option<&mut Browser>, + frame: Option<&mut Frame>, + context: Option<&mut V8Context>, + ) { + let browser = browser.cloned(); + let frame = frame.cloned(); + let context = context.cloned(); + for delegate in self.client_app_renderer.delegates() { + delegate.on_context_created( + &self.client_app_renderer, + browser.as_ref(), + frame.as_ref(), + context.as_ref(), + ); + } + } + + fn on_context_released( + &self, + browser: Option<&mut Browser>, + frame: Option<&mut Frame>, + context: Option<&mut V8Context>, + ) { + let browser = browser.cloned(); + let frame = frame.cloned(); + let context = context.cloned(); + for delegate in self.client_app_renderer.delegates() { + delegate.on_context_released( + &self.client_app_renderer, + browser.as_ref(), + frame.as_ref(), + context.as_ref(), + ); + } + } + + fn on_uncaught_exception( + &self, + browser: Option<&mut Browser>, + frame: Option<&mut Frame>, + context: Option<&mut V8Context>, + exception: Option<&mut V8Exception>, + stack_trace: Option<&mut V8StackTrace>, + ) { + let browser = browser.cloned(); + let frame = frame.cloned(); + let context = context.cloned(); + let exception = exception.cloned(); + let stack_trace = stack_trace.cloned(); + for delegate in self.client_app_renderer.delegates() { + delegate.on_uncaught_exception( + &self.client_app_renderer, + browser.as_ref(), + frame.as_ref(), + context.as_ref(), + exception.as_ref(), + stack_trace.as_ref(), + ); + } + } + + fn on_focused_node_changed( + &self, + browser: Option<&mut Browser>, + frame: Option<&mut Frame>, + node: Option<&mut Domnode>, + ) { + let browser = browser.cloned(); + let frame = frame.cloned(); + let node = node.cloned(); + for delegate in self.client_app_renderer.delegates() { + delegate.on_focused_node_changed( + &self.client_app_renderer, + browser.as_ref(), + frame.as_ref(), + node.as_ref(), + ); + } + } + + fn on_process_message_received( + &self, + browser: Option<&mut Browser>, + frame: Option<&mut Frame>, + source_process: ProcessId, + message: Option<&mut ProcessMessage>, + ) -> i32 { + let browser = browser.cloned(); + let frame = frame.cloned(); + let message = message.cloned(); + for delegate in self.client_app_renderer.delegates() { + let handled = delegate.on_process_message_received( + &self.client_app_renderer, + browser.as_ref(), + frame.as_ref(), + source_process, + message.as_ref(), + ); + if handled != 0 { + return handled; + } + } + 0 + } + } +} + +wrap_app! { + pub struct ClientAppRendererApp { + base: App, + client_app_renderer: Arc, + } + + impl App { + fn on_register_custom_schemes(&self, registrar: Option<&mut SchemeRegistrar>) { + self.base.on_register_custom_schemes(registrar); + } + + fn render_process_handler(&self) -> Option { + Some(ClientAppRendererRenderProcessHandler::new( + self.client_app_renderer.clone(), + )) + } + } +} diff --git a/examples/tests_shared/src/renderer/mod.rs b/examples/tests_shared/src/renderer/mod.rs new file mode 100644 index 0000000..933bf10 --- /dev/null +++ b/examples/tests_shared/src/renderer/mod.rs @@ -0,0 +1 @@ +pub mod client_app_renderer; diff --git a/export-cef-dir/src/main.rs b/export-cef-dir/src/main.rs index 943990c..b439049 100644 --- a/export-cef-dir/src/main.rs +++ b/export-cef-dir/src/main.rs @@ -19,7 +19,7 @@ fn default_version() -> &'static str { fn default_download_url() -> &'static str { static DEFAULT_DOWNLOAD_URL: OnceLock = OnceLock::new(); DEFAULT_DOWNLOAD_URL - .get_or_init(|| download_cef::default_download_url()) + .get_or_init(download_cef::default_download_url) .as_str() } diff --git a/get-latest/src/main.rs b/get-latest/src/main.rs index f060b18..482c960 100644 --- a/get-latest/src/main.rs +++ b/get-latest/src/main.rs @@ -44,7 +44,7 @@ type Result = std::result::Result; fn default_download_url() -> &'static str { static DEFAULT_DOWNLOAD_URL: OnceLock = OnceLock::new(); DEFAULT_DOWNLOAD_URL - .get_or_init(|| download_cef::default_download_url()) + .get_or_init(download_cef::default_download_url) .as_str() } diff --git a/update-bindings/src/main.rs b/update-bindings/src/main.rs index 03baad0..a95a29c 100644 --- a/update-bindings/src/main.rs +++ b/update-bindings/src/main.rs @@ -41,7 +41,7 @@ fn default_version() -> &'static str { fn default_download_url() -> &'static str { static DEFAULT_DOWNLOAD_URL: OnceLock = OnceLock::new(); DEFAULT_DOWNLOAD_URL - .get_or_init(|| download_cef::default_download_url()) + .get_or_init(download_cef::default_download_url) .as_str() } diff --git a/update-bindings/src/parse_tree.rs b/update-bindings/src/parse_tree.rs index 1544827..922ee18 100644 --- a/update-bindings/src/parse_tree.rs +++ b/update-bindings/src/parse_tree.rs @@ -2720,6 +2720,7 @@ fn make_my_struct() -> {rust_name} {{ $($generic_type: $first_generic_type_bound $(+ $generic_type_bound)*,)+ )? { + #[allow(clippy::new_ret_no_self)] pub fn new($($field_name: $field_type),*) -> #rust_name { #rust_name::new( Self {