From 05e14e0218e2ab5edc09c7288bd23136f788843d Mon Sep 17 00:00:00 2001 From: Delton Ding Date: Mon, 25 May 2026 12:32:59 +0800 Subject: [PATCH 1/2] feat: exprimental android support --- .github/workflows/build.yml | 72 +++- .github/workflows/test.yml | 38 ++ .gitignore | 4 + Cargo.lock | 57 +++ addons/godot_cef/godot_cef.gdextension | 3 + android/build.gradle.kts | 4 + android/godot-cef-webview/build.gradle.kts | 26 ++ .../dsh0416/godotcef/AndroidWebViewBridge.kt | 338 +++++++++++++++ android/settings.gradle.kts | 18 + crates/gdcef/Cargo.toml | 7 +- crates/gdcef/src/desktop.rs | 63 +++ crates/gdcef/src/lib.rs | 56 +-- crates/gdcef/src/mobile_webview/mod.rs | 399 ++++++++++++++++++ .../src/mobile_webview/platform/android.rs | 337 +++++++++++++++ .../gdcef/src/mobile_webview/platform/mod.rs | 17 + .../gdcef/src/mobile_webview/platform/stub.rs | 82 ++++ crates/gdcef/src/utils.rs | 16 +- mise.toml | 43 +- xtask/src/platform.rs | 15 + 19 files changed, 1551 insertions(+), 44 deletions(-) create mode 100644 android/build.gradle.kts create mode 100644 android/godot-cef-webview/build.gradle.kts create mode 100644 android/godot-cef-webview/src/main/java/io/github/dsh0416/godotcef/AndroidWebViewBridge.kt create mode 100644 android/settings.gradle.kts create mode 100644 crates/gdcef/src/desktop.rs create mode 100644 crates/gdcef/src/mobile_webview/mod.rs create mode 100644 crates/gdcef/src/mobile_webview/platform/android.rs create mode 100644 crates/gdcef/src/mobile_webview/platform/mod.rs create mode 100644 crates/gdcef/src/mobile_webview/platform/stub.rs diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index d8b6c11..10027bc 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -184,8 +184,78 @@ jobs: path: artifacts/${{ matrix.target }} retention-days: 30 + build-android: + runs-on: ubuntu-latest + name: Android + + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Setup mise toolchain + uses: jdx/mise-action@v2 + with: + cache: true + + - name: Cache Cargo registry and build + uses: actions/cache@v4 + with: + path: | + ~/.cargo/registry + ~/.cargo/git + target + key: android-build-cargo-${{ hashFiles('**/Cargo.lock', 'mise.toml') }} + restore-keys: | + android-build-cargo- + + - name: Cache Gradle + uses: actions/cache@v4 + with: + path: | + ~/.gradle/caches + ~/.gradle/wrapper + android/.gradle + key: android-build-gradle-${{ hashFiles('android/**/*.gradle.kts', 'mise.toml') }} + restore-keys: | + android-build-gradle- + + - name: Build Android Rust libraries + run: mise exec -- mise run android:build-rust + + - name: Build Android WebView bridge + run: mise exec -- mise run android:kotlin + + - name: Prepare Android artifacts + run: | + CARGO_TARGET_DIR="$(cargo metadata --no-deps --format-version 1 | python -c 'import json,sys; print(json.load(sys.stdin)["target_directory"])')" + mkdir -p artifacts/aarch64-linux-android artifacts/x86_64-linux-android artifacts/android-webview + cp "$CARGO_TARGET_DIR/aarch64-linux-android/release/libgdcef.so" artifacts/aarch64-linux-android/ + cp "$CARGO_TARGET_DIR/x86_64-linux-android/release/libgdcef.so" artifacts/x86_64-linux-android/ + cp android/godot-cef-webview/build/outputs/aar/godot-cef-webview-debug.aar artifacts/android-webview/ + + - name: Upload Android ARM64 artifact + uses: actions/upload-artifact@v4 + with: + name: gdcef-aarch64-linux-android + path: artifacts/aarch64-linux-android + retention-days: 30 + + - name: Upload Android x86_64 artifact + uses: actions/upload-artifact@v4 + with: + name: gdcef-x86_64-linux-android + path: artifacts/x86_64-linux-android + retention-days: 30 + + - name: Upload Android WebView bridge artifact + uses: actions/upload-artifact@v4 + with: + name: gdcef-android-webview + path: artifacts/android-webview + retention-days: 30 + pack: - needs: [build-macos, build-windows, build-linux] + needs: [build-macos, build-windows, build-linux, build-android] runs-on: ubuntu-latest name: Pack Addon diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 37c81c6..95af59f 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -151,6 +151,44 @@ jobs: RUSTC_WRAPPER: sccache run: mise exec -- cargo clippy --workspace --all-features --target ${{ matrix.target }} -- -D warnings + android: + runs-on: ubuntu-latest + name: Android + + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Setup mise toolchain + uses: jdx/mise-action@v2 + with: + cache: true + + - name: Cache Cargo registry and build + uses: actions/cache@v4 + with: + path: | + ~/.cargo/registry + ~/.cargo/git + target + key: android-test-cargo-${{ hashFiles('**/Cargo.lock', 'mise.toml') }} + restore-keys: | + android-test-cargo- + + - name: Cache Gradle + uses: actions/cache@v4 + with: + path: | + ~/.gradle/caches + ~/.gradle/wrapper + android/.gradle + key: android-test-gradle-${{ hashFiles('android/**/*.gradle.kts', 'mise.toml') }} + restore-keys: | + android-test-gradle- + + - name: Check Android builds + run: mise exec -- mise run android:check + fmt: runs-on: ubuntu-latest name: Format diff --git a/.gitignore b/.gitignore index e05bae2..02a799f 100644 --- a/.gitignore +++ b/.gitignore @@ -4,5 +4,9 @@ target/ node_modules/ addons/godot_cef/bin/ +android/.gradle/ +android/**/build/ +android/local.properties + .DS_Store .envrc diff --git a/Cargo.lock b/Cargo.lock index 3ca2c96..4e87389 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -455,6 +455,16 @@ version = "1.0.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1d07550c9036bf2ae0c684c4297d503f838287c83c53686d05370d0e139ae570" +[[package]] +name = "combine" +version = "4.6.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba5a308b75df32fe02788e748662718f03fde005016435c444eea572398219fd" +dependencies = [ + "bytes", + "memchr", +] + [[package]] name = "console" version = "0.16.3" @@ -768,6 +778,7 @@ dependencies = [ "cef_app", "ciborium", "godot", + "jni", "libc", "libloading 0.9.0", "objc2", @@ -1199,6 +1210,36 @@ version = "1.0.18" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682" +[[package]] +name = "jni" +version = "0.22.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5efd9a482cf3a427f00d6b35f14332adc7902ce91efb778580e180ff90fa3498" +dependencies = [ + "cfg-if", + "combine", + "jni-macros", + "jni-sys 0.4.1", + "log", + "simd_cesu8", + "thiserror 2.0.18", + "walkdir", + "windows-link", +] + +[[package]] +name = "jni-macros" +version = "0.22.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a00109accc170f0bdb141fed3e393c565b6f5e072365c3bd58f5b062591560a3" +dependencies = [ + "proc-macro2", + "quote", + "rustc_version", + "simd_cesu8", + "syn", +] + [[package]] name = "jni-sys" version = "0.3.1" @@ -2033,6 +2074,22 @@ version = "0.3.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "703d5c7ef118737c72f1af64ad2f6f8c5e1921f818cdcb97b8fe6fc69bf66214" +[[package]] +name = "simd_cesu8" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94f90157bb87cddf702797c5dadfa0be7d266cdf49e22da2fcaa32eff75b2c33" +dependencies = [ + "rustc_version", + "simdutf8", +] + +[[package]] +name = "simdutf8" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3a9fe34e3e7a50316060351f37187a3f546bce95496156754b601a5fa71b76e" + [[package]] name = "slab" version = "0.4.12" diff --git a/addons/godot_cef/godot_cef.gdextension b/addons/godot_cef/godot_cef.gdextension index 83e0dfd..05af26b 100644 --- a/addons/godot_cef/godot_cef.gdextension +++ b/addons/godot_cef/godot_cef.gdextension @@ -9,10 +9,13 @@ linux.arm64 = "bin/aarch64-unknown-linux-gnu/libgdcef.so" macos = "bin/universal-apple-darwin/Godot CEF.framework" windows.x86_64 = "bin/x86_64-pc-windows-msvc/gdcef.dll" windows.arm64 = "bin/aarch64-pc-windows-msvc/gdcef.dll" +android.arm64 = "bin/aarch64-linux-android/libgdcef.so" +android.x86_64 = "bin/x86_64-linux-android/libgdcef.so" [icons] CefTexture = "icons/webview.png" CefIpcInspector = "icons/webview.png" +AndroidWebViewTexture = "icons/webview.png" [dependencies] diff --git a/android/build.gradle.kts b/android/build.gradle.kts new file mode 100644 index 0000000..7da7b32 --- /dev/null +++ b/android/build.gradle.kts @@ -0,0 +1,4 @@ +plugins { + id("com.android.library") version "8.7.3" apply false + id("org.jetbrains.kotlin.android") version "2.1.21" apply false +} diff --git a/android/godot-cef-webview/build.gradle.kts b/android/godot-cef-webview/build.gradle.kts new file mode 100644 index 0000000..662ac69 --- /dev/null +++ b/android/godot-cef-webview/build.gradle.kts @@ -0,0 +1,26 @@ +import org.jetbrains.kotlin.gradle.dsl.JvmTarget + +plugins { + id("com.android.library") + id("org.jetbrains.kotlin.android") +} + +android { + namespace = "io.github.dsh0416.godotcef" + compileSdk = 35 + + defaultConfig { + minSdk = 29 + } + + compileOptions { + sourceCompatibility = JavaVersion.VERSION_17 + targetCompatibility = JavaVersion.VERSION_17 + } +} + +kotlin { + compilerOptions { + jvmTarget.set(JvmTarget.JVM_17) + } +} diff --git a/android/godot-cef-webview/src/main/java/io/github/dsh0416/godotcef/AndroidWebViewBridge.kt b/android/godot-cef-webview/src/main/java/io/github/dsh0416/godotcef/AndroidWebViewBridge.kt new file mode 100644 index 0000000..6d56f85 --- /dev/null +++ b/android/godot-cef-webview/src/main/java/io/github/dsh0416/godotcef/AndroidWebViewBridge.kt @@ -0,0 +1,338 @@ +package io.github.dsh0416.godotcef + +import android.app.Activity +import android.app.Presentation +import android.content.Context +import android.graphics.PixelFormat +import android.hardware.HardwareBuffer +import android.hardware.display.DisplayManager +import android.hardware.display.VirtualDisplay +import android.media.Image +import android.media.ImageReader +import android.os.Build +import android.os.Bundle +import android.os.Handler +import android.os.HandlerThread +import android.view.Display +import android.view.MotionEvent +import android.view.Surface +import android.view.ViewGroup +import android.view.inputmethod.InputMethodManager +import android.webkit.WebChromeClient +import android.webkit.WebView +import android.webkit.WebViewClient +import java.util.concurrent.ConcurrentHashMap + +/** + * Experimental Android peer for `AndroidWebViewTexture`. + * + * This source is intentionally isolated from the desktop CEF backend. A Godot Android plugin + * should call `initialize(activity)` before the Rust GDExtension asks for a WebView session. + */ +@android.annotation.TargetApi(Build.VERSION_CODES.Q) +object AndroidWebViewBridge { + private val sessions = ConcurrentHashMap() + private val imageThread = HandlerThread("GodotCefWebViewImages").apply { start() } + private val imageHandler = Handler(imageThread.looper) + + @Volatile + private var activity: Activity? = null + + @JvmStatic + fun initialize(activity: Activity) { + this.activity = activity + WebView.setWebContentsDebuggingEnabled(true) + } + + @JvmStatic + fun create(id: Long, width: Int, height: Int, url: String, javaScriptEnabled: Boolean) { + val currentActivity = requireActivity() + val safeWidth = width.coerceAtLeast(1) + val safeHeight = height.coerceAtLeast(1) + + currentActivity.runOnUiThread { + sessions.remove(id)?.release() + val session = Session( + activity = currentActivity, + id = id, + width = safeWidth, + height = safeHeight, + url = url, + javaScriptEnabled = javaScriptEnabled, + ) + sessions[id] = session + session.start() + } + } + + @JvmStatic + fun shutdown(id: Long) { + val session = sessions.remove(id) ?: return + session.activity.runOnUiThread { session.release() } + } + + @JvmStatic + fun acquireLatestFrame(id: Long): HardwareBuffer? { + return sessions[id]?.currentHardwareBuffer() + } + + @JvmStatic + fun getFrameWidth(id: Long): Int { + return sessions[id]?.width ?: 0 + } + + @JvmStatic + fun getFrameHeight(id: Long): Int { + return sessions[id]?.height ?: 0 + } + + @JvmStatic + fun loadUrl(id: Long, url: String) { + sessions[id]?.runOnWebView { loadUrl(url) } + } + + @JvmStatic + fun eval(id: Long, code: String) { + sessions[id]?.runOnWebView { evaluateJavascript(code, null) } + } + + @JvmStatic + fun reload(id: Long) { + sessions[id]?.runOnWebView { reload() } + } + + @JvmStatic + fun goBack(id: Long) { + sessions[id]?.runOnWebView { + if (canGoBack()) { + goBack() + } + } + } + + @JvmStatic + fun goForward(id: Long) { + sessions[id]?.runOnWebView { + if (canGoForward()) { + goForward() + } + } + } + + @JvmStatic + fun focus(id: Long) { + sessions[id]?.focusWebView() + } + + @JvmStatic + fun clearFocus(id: Long) { + sessions[id]?.clearWebViewFocus() + } + + @JvmStatic + fun resize(id: Long, width: Int, height: Int) { + val session = sessions[id] ?: return + val currentUrl = session.currentUrl() + val javaScriptEnabled = session.javaScriptEnabled + shutdown(id) + create(id, width, height, currentUrl, javaScriptEnabled) + } + + @JvmStatic + fun setJavaScriptEnabled(id: Long, enabled: Boolean) { + sessions[id]?.runOnWebView { + settings.javaScriptEnabled = enabled + } + } + + @JvmStatic + fun touch(id: Long, pointerId: Int, x: Float, y: Float, pressed: Boolean) { + val action = if (pressed) MotionEvent.ACTION_DOWN else MotionEvent.ACTION_UP + sessions[id]?.dispatchMotion(pointerId, x, y, action) + } + + @JvmStatic + fun drag(id: Long, pointerId: Int, x: Float, y: Float) { + sessions[id]?.dispatchMotion(pointerId, x, y, MotionEvent.ACTION_MOVE) + } + + @JvmStatic + fun mouseButton(id: Long, x: Float, y: Float, pressed: Boolean) { + touch(id, 0, x, y, pressed) + } + + @JvmStatic + fun mouseMove(id: Long, x: Float, y: Float) { + drag(id, 0, x, y) + } + + private fun requireActivity(): Activity { + return activity ?: error( + "AndroidWebViewBridge.initialize(activity) must be called by the Android plugin first" + ) + } + + private class Session( + val activity: Activity, + private val id: Long, + var width: Int, + var height: Int, + private var url: String, + var javaScriptEnabled: Boolean, + ) { + private val displayManager = + activity.getSystemService(Context.DISPLAY_SERVICE) as DisplayManager + private val lock = Any() + + private var imageReader: ImageReader? = null + private var outputSurface: Surface? = null + private var virtualDisplay: VirtualDisplay? = null + private var presentation: WebPresentation? = null + private var currentImage: Image? = null + + fun start() { + imageReader = ImageReader.newInstance( + width, + height, + PixelFormat.RGBA_8888, + 3, + HardwareBuffer.USAGE_GPU_SAMPLED_IMAGE or HardwareBuffer.USAGE_GPU_COLOR_OUTPUT, + ).also { reader -> + reader.setOnImageAvailableListener({ onImageAvailable(reader) }, imageHandler) + outputSurface = reader.surface + } + + val densityDpi = activity.resources.displayMetrics.densityDpi + virtualDisplay = displayManager.createVirtualDisplay( + "godot-cef-webview-$id", + width, + height, + densityDpi, + outputSurface, + DisplayManager.VIRTUAL_DISPLAY_FLAG_PRESENTATION or + DisplayManager.VIRTUAL_DISPLAY_FLAG_OWN_CONTENT_ONLY, + ) + + val display = virtualDisplay?.display ?: return + presentation = WebPresentation(activity, display, width, height, javaScriptEnabled) + .also { webPresentation -> + webPresentation.show() + webPresentation.webView.loadUrl(url) + } + } + + fun release() { + presentation?.dismiss() + presentation = null + virtualDisplay?.release() + virtualDisplay = null + outputSurface?.release() + outputSurface = null + imageReader?.close() + imageReader = null + synchronized(lock) { + currentImage?.close() + currentImage = null + } + } + + fun currentHardwareBuffer(): HardwareBuffer? { + synchronized(lock) { + return currentImage?.hardwareBuffer + } + } + + fun currentUrl(): String { + return presentation?.webView?.url ?: url + } + + fun runOnWebView(block: WebView.() -> Unit) { + activity.runOnUiThread { + presentation?.webView?.block() + } + } + + fun dispatchMotion(pointerId: Int, x: Float, y: Float, action: Int) { + activity.runOnUiThread { + val eventTime = android.os.SystemClock.uptimeMillis() + val event = MotionEvent.obtain( + eventTime, + eventTime, + action, + x, + y, + 0, + ) + event.source = android.view.InputDevice.SOURCE_TOUCHSCREEN + presentation?.webView?.let { webView -> + if (action == MotionEvent.ACTION_DOWN) { + webView.requestFocus() + showKeyboard(webView) + } + webView.dispatchTouchEvent(event) + } + event.recycle() + } + } + + fun focusWebView() { + activity.runOnUiThread { + presentation?.webView?.let { webView -> + webView.requestFocus() + showKeyboard(webView) + } + } + } + + fun clearWebViewFocus() { + activity.runOnUiThread { + presentation?.webView?.let { webView -> + webView.clearFocus() + hideKeyboard(webView) + } + } + } + + private fun showKeyboard(webView: WebView) { + val imm = activity.getSystemService(Context.INPUT_METHOD_SERVICE) as InputMethodManager + imm.showSoftInput(webView, InputMethodManager.SHOW_IMPLICIT) + } + + private fun hideKeyboard(webView: WebView) { + val imm = activity.getSystemService(Context.INPUT_METHOD_SERVICE) as InputMethodManager + imm.hideSoftInputFromWindow(webView.windowToken, 0) + } + + private fun onImageAvailable(reader: ImageReader) { + val image = reader.acquireLatestImage() ?: return + synchronized(lock) { + currentImage?.close() + currentImage = image + } + } + } + + private class WebPresentation( + context: Context, + display: Display, + private val width: Int, + private val height: Int, + private val javaScriptEnabled: Boolean, + ) : Presentation(context, display) { + lateinit var webView: WebView + private set + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + webView = WebView(context).apply { + layoutParams = ViewGroup.LayoutParams(width, height) + isFocusable = true + isFocusableInTouchMode = true + settings.javaScriptEnabled = javaScriptEnabled + webViewClient = WebViewClient() + webChromeClient = WebChromeClient() + } + setContentView(webView) + } + } +} diff --git a/android/settings.gradle.kts b/android/settings.gradle.kts new file mode 100644 index 0000000..98ab733 --- /dev/null +++ b/android/settings.gradle.kts @@ -0,0 +1,18 @@ +pluginManagement { + repositories { + google() + mavenCentral() + gradlePluginPortal() + } +} + +dependencyResolutionManagement { + repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS) + repositories { + google() + mavenCentral() + } +} + +rootProject.name = "godot-cef-android" +include(":godot-cef-webview") diff --git a/crates/gdcef/Cargo.toml b/crates/gdcef/Cargo.toml index ea39c99..6749384 100644 --- a/crates/gdcef/Cargo.toml +++ b/crates/gdcef/Cargo.toml @@ -15,9 +15,10 @@ workspace = true [dependencies] godot = { workspace = true } + +[target.'cfg(not(target_os = "android"))'.dependencies] cef = { workspace = true } cef-dll-sys = { workspace = true } -process_path = { workspace = true } cef_app = { path = "../cef_app" } software_render = { path = "../software_render" } wide = { workspace = true } @@ -25,6 +26,7 @@ url = { workspace = true } percent-encoding = { workspace = true } adblock = { workspace = true } ciborium = { workspace = true } +process_path = { workspace = true } [target.'cfg(target_os = "macos")'.dependencies] libloading = { workspace = true } @@ -48,3 +50,6 @@ quick-xml = { workspace = true } [target.'cfg(all(target_os = "linux", target_arch = "x86_64"))'.dependencies] retour = { workspace = true } + +[target.'cfg(target_os = "android")'.dependencies] +jni = "0.22.4" diff --git a/crates/gdcef/src/desktop.rs b/crates/gdcef/src/desktop.rs new file mode 100644 index 0000000..c28cfec --- /dev/null +++ b/crates/gdcef/src/desktop.rs @@ -0,0 +1,63 @@ +#[path = "accelerated_osr/mod.rs"] +pub(crate) mod accelerated_osr; +#[path = "browser.rs"] +pub(crate) mod browser; +#[path = "cef_init.rs"] +pub(crate) mod cef_init; +#[path = "cef_ipc_inspector.rs"] +pub(crate) mod cef_ipc_inspector; +#[path = "cef_texture/mod.rs"] +pub(crate) mod cef_texture; +#[path = "cef_texture2d/mod.rs"] +pub(crate) mod cef_texture2d; +#[path = "compat.rs"] +pub(crate) mod compat; +#[path = "cookie.rs"] +pub(crate) mod cookie; +#[path = "cursor.rs"] +pub(crate) mod cursor; +#[path = "drag.rs"] +pub(crate) mod drag; +#[path = "error.rs"] +pub(crate) mod error; +#[path = "godot_protocol/mod.rs"] +pub(crate) mod godot_protocol; +#[path = "input/mod.rs"] +pub(crate) mod input; +#[path = "ipc_data.rs"] +pub(crate) mod ipc_data; +#[path = "render.rs"] +pub(crate) mod render; +#[path = "settings.rs"] +pub(crate) mod settings; +#[path = "utils.rs"] +pub(crate) mod utils; +#[path = "vulkan_hook/mod.rs"] +pub(crate) mod vulkan_hook; +#[path = "webrender.rs"] +pub(crate) mod webrender; +#[path = "webrender_ipc.rs"] +pub(crate) mod webrender_ipc; + +use godot::init::InitStage; + +pub(crate) fn on_stage_init(level: InitStage) { + match level { + InitStage::Core => { + // Install Vulkan hook before RenderingServer is created so Godot's + // Vulkan device can request platform external-memory extensions. + vulkan_hook::install_vulkan_hook(); + + if let Err(error) = utils::ensure_executable_permissions() { + godot::global::godot_warn!( + "[GodotCef] Failed to set executable permissions: {}", + error + ); + } + } + InitStage::Scene => { + settings::register_project_settings(); + } + _ => {} + } +} diff --git a/crates/gdcef/src/lib.rs b/crates/gdcef/src/lib.rs index 524d979..d2ac74d 100644 --- a/crates/gdcef/src/lib.rs +++ b/crates/gdcef/src/lib.rs @@ -1,23 +1,13 @@ -mod accelerated_osr; -mod browser; -mod cef_init; -mod cef_ipc_inspector; -mod cef_texture; -mod cef_texture2d; -mod compat; -mod cookie; -mod cursor; -mod drag; -mod error; -mod godot_protocol; -mod input; -mod ipc_data; -mod render; -mod settings; -mod utils; -mod vulkan_hook; -mod webrender; -mod webrender_ipc; +#[cfg(not(target_os = "android"))] +mod desktop; +mod mobile_webview; + +#[cfg(not(target_os = "android"))] +pub(crate) use desktop::{ + accelerated_osr, browser, cef_init, cef_ipc_inspector, cef_texture, cef_texture2d, compat, + cookie, cursor, drag, error, godot_protocol, input, ipc_data, render, settings, utils, + webrender, webrender_ipc, +}; use godot::init::*; @@ -26,29 +16,19 @@ struct GodotCef; #[gdextension] unsafe impl ExtensionLibrary for GodotCef { fn on_stage_init(level: InitStage) { - match level { - InitStage::Core => { - // Install Vulkan hook at the Core stage, BEFORE RenderingServer is created. - // This allows us to inject platform-specific external memory extensions - // (e.g., VK_KHR_external_memory_win32 and related Win32 external memory extensions) into Godot's Vulkan device. - vulkan_hook::install_vulkan_hook(); + #[cfg(not(target_os = "android"))] + desktop::on_stage_init(level); - if let Err(e) = utils::ensure_executable_permissions() { - godot::global::godot_warn!( - "[GodotCef] Failed to set executable permissions: {}", - e - ); - } - } - InitStage::Scene => { - settings::register_project_settings(); - } - _ => {} - } + #[cfg(target_os = "android")] + let _ = level; } } // Re-export CefTexture for convenience +#[cfg(not(target_os = "android"))] pub use cef_ipc_inspector::CefIpcInspector; +#[cfg(not(target_os = "android"))] pub use cef_texture::CefTexture; +#[cfg(not(target_os = "android"))] pub use cef_texture2d::CefTexture2D; +pub use mobile_webview::AndroidWebViewTexture; diff --git a/crates/gdcef/src/mobile_webview/mod.rs b/crates/gdcef/src/mobile_webview/mod.rs new file mode 100644 index 0000000..004216b --- /dev/null +++ b/crates/gdcef/src/mobile_webview/mod.rs @@ -0,0 +1,399 @@ +mod platform; + +#[cfg(target_os = "android")] +use godot::classes::ExternalTexture; +use godot::classes::image::Format as ImageFormat; +use godot::classes::notify::ObjectNotification; +use godot::classes::{ITexture2D, Image, ImageTexture, InputEvent, RenderingServer, Texture2D}; +use godot::prelude::*; +use std::sync::atomic::{AtomicI64, Ordering}; + +use platform::{AndroidWebViewBridge, FrameUpdate}; + +static NEXT_WEBVIEW_ID: AtomicI64 = AtomicI64::new(1); + +#[derive(GodotClass)] +#[class(base=Texture2D, tool)] +pub struct AndroidWebViewTexture { + base: Base, + + #[export] + #[var(get = get_url_property, set = set_url_property)] + url: GString, + + #[export] + #[var(get = get_texture_size_property, set = set_texture_size_property)] + texture_size: Vector2i, + + #[export] + #[var(get = get_javascript_enabled, set = set_javascript_enabled)] + javascript_enabled: bool, + + fallback_texture: Gd, + #[cfg(target_os = "android")] + external_texture: Gd, + bridge: AndroidWebViewBridge, + instance_id: i64, + started: bool, + last_external_buffer_id: u64, + frame_hook_callable: Option, + frame_hook_connected: bool, +} + +#[godot_api] +impl ITexture2D for AndroidWebViewTexture { + fn init(base: Base) -> Self { + let texture_size = Vector2i::new(1024, 1024); + let fallback_texture = Self::make_placeholder_texture(texture_size); + let frame_hook_callable = base.to_init_gd().callable("_on_frame_pre_draw"); + RenderingServer::singleton().connect("frame_pre_draw", &frame_hook_callable); + + #[cfg(target_os = "android")] + let external_texture = { + let mut texture = ExternalTexture::new_gd(); + texture.set_size(Vector2::new(texture_size.x as f32, texture_size.y as f32)); + texture + }; + + Self { + base, + url: "https://example.com".into(), + texture_size, + javascript_enabled: true, + fallback_texture, + #[cfg(target_os = "android")] + external_texture, + bridge: AndroidWebViewBridge::new(), + instance_id: NEXT_WEBVIEW_ID.fetch_add(1, Ordering::Relaxed), + started: false, + last_external_buffer_id: 0, + frame_hook_callable: Some(frame_hook_callable), + frame_hook_connected: true, + } + } + + fn on_notification(&mut self, what: ObjectNotification) { + if what == ObjectNotification::PREDELETE { + self.cleanup_instance(); + } + } + + fn get_width(&self) -> i32 { + self.texture_size.x + } + + fn get_height(&self) -> i32 { + self.texture_size.y + } + + fn has_alpha(&self) -> bool { + true + } + + fn get_rid(&self) -> Rid { + #[cfg(target_os = "android")] + if self.started { + return self.external_texture.get_rid(); + } + + self.fallback_texture.get_rid() + } +} + +#[godot_api] +impl AndroidWebViewTexture { + #[signal] + fn load_started(url: GString); + + #[signal] + fn load_finished(url: GString); + + #[signal] + fn load_error(url: GString, error_text: GString); + + #[signal] + fn url_changed(url: GString); + + #[signal] + fn title_changed(title: GString); + + #[func] + fn _on_frame_pre_draw(&mut self) { + self.tick(); + } + + #[func] + pub fn start(&mut self) { + if self.started { + return; + } + + match self.bridge.create( + self.instance_id, + self.texture_size, + self.url.to_string(), + self.javascript_enabled, + ) { + Ok(()) => { + self.started = true; + self.base_mut().emit_changed(); + } + Err(error) => { + godot::global::godot_warn!( + "[AndroidWebViewTexture] Failed to start WebView backend: {}", + error + ); + } + } + } + + #[func] + pub fn shutdown(&mut self) { + if !self.started { + return; + } + if let Err(error) = self.bridge.shutdown(self.instance_id) { + godot::global::godot_warn!( + "[AndroidWebViewTexture] Failed to shutdown WebView backend: {}", + error + ); + } + self.started = false; + self.last_external_buffer_id = 0; + self.base_mut().emit_changed(); + } + + #[func] + pub fn update_frame(&mut self) -> bool { + if !self.started { + return false; + } + + let update = match self.bridge.acquire_latest_frame(self.instance_id) { + Ok(Some(update)) => update, + Ok(None) => return false, + Err(error) => { + godot::global::godot_warn!( + "[AndroidWebViewTexture] Failed to acquire WebView frame: {}", + error + ); + return false; + } + }; + + self.apply_frame_update(update) + } + + #[func] + pub fn eval(&mut self, code: GString) { + if let Err(error) = self.bridge.eval(self.instance_id, code.to_string()) { + godot::global::godot_warn!( + "[AndroidWebViewTexture] Failed to evaluate JavaScript: {}", + error + ); + } + } + + #[func] + pub fn reload(&mut self) { + if let Err(error) = self.bridge.reload(self.instance_id) { + godot::global::godot_warn!("[AndroidWebViewTexture] Failed to reload: {}", error); + } + } + + #[func] + pub fn go_back(&mut self) { + if let Err(error) = self.bridge.go_back(self.instance_id) { + godot::global::godot_warn!("[AndroidWebViewTexture] Failed to go back: {}", error); + } + } + + #[func] + pub fn go_forward(&mut self) { + if let Err(error) = self.bridge.go_forward(self.instance_id) { + godot::global::godot_warn!("[AndroidWebViewTexture] Failed to go forward: {}", error); + } + } + + #[func] + pub fn focus_webview(&mut self) { + if let Err(error) = self.bridge.focus(self.instance_id) { + godot::global::godot_warn!( + "[AndroidWebViewTexture] Failed to focus WebView: {}", + error + ); + } + } + + #[func] + pub fn clear_webview_focus(&mut self) { + if let Err(error) = self.bridge.clear_focus(self.instance_id) { + godot::global::godot_warn!( + "[AndroidWebViewTexture] Failed to clear WebView focus: {}", + error + ); + } + } + + #[func] + pub fn forward_input_event(&mut self, event: Gd) { + if let Err(error) = self.bridge.forward_input(self.instance_id, event) { + godot::global::godot_warn!( + "[AndroidWebViewTexture] Failed to forward input event: {}", + error + ); + } + } + + #[func] + pub(crate) fn get_url_property(&self) -> GString { + self.url.clone() + } + + #[func] + pub(crate) fn set_url_property(&mut self, url: GString) { + self.url = url.clone(); + if self.started + && let Err(error) = self.bridge.load_url(self.instance_id, url.to_string()) + { + godot::global::godot_warn!( + "[AndroidWebViewTexture] Failed to load URL '{}': {}", + url, + error + ); + } + } + + #[func] + pub(crate) fn get_texture_size_property(&self) -> Vector2i { + self.texture_size + } + + #[func] + pub(crate) fn set_texture_size_property(&mut self, size: Vector2i) { + let clamped = Vector2i::new(size.x.max(1), size.y.max(1)); + if clamped == self.texture_size { + return; + } + + self.texture_size = clamped; + self.refresh_fallback_texture(); + + #[cfg(target_os = "android")] + self.external_texture + .set_size(Vector2::new(clamped.x as f32, clamped.y as f32)); + + if self.started + && let Err(error) = self.bridge.resize(self.instance_id, clamped) + { + godot::global::godot_warn!( + "[AndroidWebViewTexture] Failed to resize WebView backend: {}", + error + ); + } + + self.base_mut().emit_changed(); + } + + #[func] + pub(crate) fn get_javascript_enabled(&self) -> bool { + self.javascript_enabled + } + + #[func] + pub(crate) fn set_javascript_enabled(&mut self, enabled: bool) { + self.javascript_enabled = enabled; + if self.started + && let Err(error) = self + .bridge + .set_javascript_enabled(self.instance_id, enabled) + { + godot::global::godot_warn!( + "[AndroidWebViewTexture] Failed to update JavaScript setting: {}", + error + ); + } + } + + fn apply_frame_update(&mut self, update: FrameUpdate) -> bool { + if update.width > 0 && update.height > 0 { + let new_size = Vector2i::new(update.width, update.height); + if new_size != self.texture_size { + self.texture_size = new_size; + self.refresh_fallback_texture(); + #[cfg(target_os = "android")] + self.external_texture.set_size(Vector2::new( + self.texture_size.x as f32, + self.texture_size.y as f32, + )); + } + } + + #[cfg(target_os = "android")] + { + if update.external_buffer_id != 0 + && update.external_buffer_id != self.last_external_buffer_id + { + self.external_texture + .set_external_buffer_id(update.external_buffer_id); + self.last_external_buffer_id = update.external_buffer_id; + } + } + + self.base_mut().emit_changed(); + true + } + + fn make_placeholder_texture(size: Vector2i) -> Gd { + let bytes = vec![0u8; (size.x.max(1) * size.y.max(1) * 4) as usize]; + let byte_array = PackedByteArray::from(bytes.as_slice()); + let mut texture = ImageTexture::new_gd(); + if let Some(image) = Image::create_from_data( + size.x.max(1), + size.y.max(1), + false, + ImageFormat::RGBA8, + &byte_array, + ) { + texture.set_image(&image); + } + texture + } + + fn refresh_fallback_texture(&mut self) { + self.fallback_texture = Self::make_placeholder_texture(self.texture_size); + } + + fn tick(&mut self) { + #[cfg(target_os = "android")] + { + if !self.started { + self.start(); + } + let _ = self.update_frame(); + } + } + + fn cleanup_instance(&mut self) { + self.shutdown(); + self.disconnect_frame_hook(); + } + + fn disconnect_frame_hook(&mut self) { + if !self.frame_hook_connected { + return; + } + + if let Some(callable) = self.frame_hook_callable.as_ref() { + RenderingServer::singleton().disconnect("frame_pre_draw", callable); + } + self.frame_hook_callable = None; + self.frame_hook_connected = false; + } +} + +impl Drop for AndroidWebViewTexture { + fn drop(&mut self) { + self.cleanup_instance(); + } +} diff --git a/crates/gdcef/src/mobile_webview/platform/android.rs b/crates/gdcef/src/mobile_webview/platform/android.rs new file mode 100644 index 0000000..79523c5 --- /dev/null +++ b/crates/gdcef/src/mobile_webview/platform/android.rs @@ -0,0 +1,337 @@ +use godot::classes::{ + InputEvent, InputEventMouseButton, InputEventMouseMotion, InputEventScreenDrag, + InputEventScreenTouch, +}; +use godot::prelude::*; +use jni::objects::{JObject, JValue, JValueOwned}; +use jni::signature::{MethodSignature, RuntimeMethodSignature}; +use jni::strings::JNIString; +use jni::sys::{JNI_VERSION_1_6, jint, jobject}; +use jni::{Env, JavaVM}; +use std::ffi::c_void; +use std::sync::OnceLock; + +use super::FrameUpdate; + +const BRIDGE_CLASS: &str = "io/github/dsh0416/godotcef/AndroidWebViewBridge"; + +static JAVA_VM: OnceLock = OnceLock::new(); + +#[repr(C)] +struct AHardwareBuffer { + _private: [u8; 0], +} + +#[link(name = "android")] +unsafe extern "C" { + fn AHardwareBuffer_fromHardwareBuffer( + env: *mut jni::sys::JNIEnv, + hardware_buffer: jobject, + ) -> *mut AHardwareBuffer; + fn AHardwareBuffer_acquire(buffer: *mut AHardwareBuffer); + fn AHardwareBuffer_release(buffer: *mut AHardwareBuffer); +} + +#[unsafe(no_mangle)] +pub extern "system" fn JNI_OnLoad(vm: *mut jni::sys::JavaVM, _reserved: *mut c_void) -> jint { + let vm = unsafe { JavaVM::from_raw(vm) }; + let _ = JAVA_VM.set(vm); + JNI_VERSION_1_6 +} + +#[derive(Debug, Default)] +pub struct AndroidWebViewBridge { + retained_buffer: Option<*mut AHardwareBuffer>, +} + +impl AndroidWebViewBridge { + pub fn new() -> Self { + Self { + retained_buffer: None, + } + } + + pub fn create( + &mut self, + instance_id: i64, + size: Vector2i, + url: String, + javascript_enabled: bool, + ) -> Result<(), String> { + with_env(|env| { + let url = env.new_string(url).map_err(jni_error)?; + let url_obj: JObject = url.into(); + call_bridge_static( + env, + "create", + "(JIILjava/lang/String;Z)V", + &[ + JValue::Long(instance_id), + JValue::Int(size.x), + JValue::Int(size.y), + JValue::Object(&url_obj), + JValue::Bool(javascript_enabled), + ], + )?; + Ok(()) + }) + } + + pub fn shutdown(&mut self, instance_id: i64) -> Result<(), String> { + let result = call_void("shutdown", "(J)V", &[JValue::Long(instance_id)]); + self.release_retained_buffer(); + result + } + + pub fn acquire_latest_frame( + &mut self, + instance_id: i64, + ) -> Result, String> { + with_env(|env| { + let hardware_buffer = call_bridge_static( + env, + "acquireLatestFrame", + "(J)Landroid/hardware/HardwareBuffer;", + &[JValue::Long(instance_id)], + )? + .l() + .map_err(jni_error)?; + if hardware_buffer.is_null() { + return Ok(None); + } + let hardware_buffer_ptr = hardware_buffer_ptr(env, &hardware_buffer)?; + self.retain_buffer(hardware_buffer_ptr); + + let width = + call_bridge_static(env, "getFrameWidth", "(J)I", &[JValue::Long(instance_id)])? + .i() + .map_err(jni_error)?; + let height = + call_bridge_static(env, "getFrameHeight", "(J)I", &[JValue::Long(instance_id)])? + .i() + .map_err(jni_error)?; + + Ok(Some(FrameUpdate { + external_buffer_id: hardware_buffer_ptr as u64, + width, + height, + })) + }) + } + + pub fn load_url(&mut self, instance_id: i64, url: String) -> Result<(), String> { + with_env(|env| { + let url = env.new_string(url).map_err(jni_error)?; + let url_obj: JObject = url.into(); + call_bridge_static( + env, + "loadUrl", + "(JLjava/lang/String;)V", + &[JValue::Long(instance_id), JValue::Object(&url_obj)], + )?; + Ok(()) + }) + } + + pub fn eval(&mut self, instance_id: i64, code: String) -> Result<(), String> { + with_env(|env| { + let code = env.new_string(code).map_err(jni_error)?; + let code_obj: JObject = code.into(); + call_bridge_static( + env, + "eval", + "(JLjava/lang/String;)V", + &[JValue::Long(instance_id), JValue::Object(&code_obj)], + )?; + Ok(()) + }) + } + + pub fn reload(&mut self, instance_id: i64) -> Result<(), String> { + call_void("reload", "(J)V", &[JValue::Long(instance_id)]) + } + + pub fn go_back(&mut self, instance_id: i64) -> Result<(), String> { + call_void("goBack", "(J)V", &[JValue::Long(instance_id)]) + } + + pub fn go_forward(&mut self, instance_id: i64) -> Result<(), String> { + call_void("goForward", "(J)V", &[JValue::Long(instance_id)]) + } + + pub fn focus(&mut self, instance_id: i64) -> Result<(), String> { + call_void("focus", "(J)V", &[JValue::Long(instance_id)]) + } + + pub fn clear_focus(&mut self, instance_id: i64) -> Result<(), String> { + call_void("clearFocus", "(J)V", &[JValue::Long(instance_id)]) + } + + pub fn resize(&mut self, instance_id: i64, size: Vector2i) -> Result<(), String> { + call_void( + "resize", + "(JII)V", + &[ + JValue::Long(instance_id), + JValue::Int(size.x), + JValue::Int(size.y), + ], + ) + } + + pub fn set_javascript_enabled( + &mut self, + instance_id: i64, + enabled: bool, + ) -> Result<(), String> { + call_void( + "setJavaScriptEnabled", + "(JZ)V", + &[JValue::Long(instance_id), JValue::Bool(enabled)], + ) + } + + pub fn forward_input(&mut self, instance_id: i64, event: Gd) -> Result<(), String> { + if let Ok(touch) = event.clone().try_cast::() { + let position = touch.get_position(); + return call_void( + "touch", + "(JIFFZ)V", + &[ + JValue::Long(instance_id), + JValue::Int(touch.get_index()), + JValue::Float(position.x), + JValue::Float(position.y), + JValue::Bool(touch.is_pressed()), + ], + ); + } + + if let Ok(drag) = event.clone().try_cast::() { + let position = drag.get_position(); + return call_void( + "drag", + "(JIFF)V", + &[ + JValue::Long(instance_id), + JValue::Int(drag.get_index()), + JValue::Float(position.x), + JValue::Float(position.y), + ], + ); + } + + if let Ok(button) = event.clone().try_cast::() { + let position = button.get_position(); + return call_void( + "mouseButton", + "(JFFZ)V", + &[ + JValue::Long(instance_id), + JValue::Float(position.x), + JValue::Float(position.y), + JValue::Bool(button.is_pressed()), + ], + ); + } + + if let Ok(motion) = event.try_cast::() { + let position = motion.get_position(); + return call_void( + "mouseMove", + "(JFF)V", + &[ + JValue::Long(instance_id), + JValue::Float(position.x), + JValue::Float(position.y), + ], + ); + } + + Ok(()) + } + + fn retain_buffer(&mut self, buffer: *mut AHardwareBuffer) { + if self.retained_buffer == Some(buffer) { + return; + } + unsafe { + AHardwareBuffer_acquire(buffer); + } + self.release_retained_buffer(); + self.retained_buffer = Some(buffer); + } + + fn release_retained_buffer(&mut self) { + if let Some(buffer) = self.retained_buffer.take() { + unsafe { + AHardwareBuffer_release(buffer); + } + } + } +} + +impl Drop for AndroidWebViewBridge { + fn drop(&mut self) { + self.release_retained_buffer(); + } +} + +fn hardware_buffer_ptr( + env: &mut Env, + hardware_buffer: &JObject, +) -> Result<*mut AHardwareBuffer, String> { + let ptr = + unsafe { AHardwareBuffer_fromHardwareBuffer(env.get_raw(), hardware_buffer.as_raw()) }; + if ptr.is_null() { + return Err("AHardwareBuffer_fromHardwareBuffer returned null".to_string()); + } + Ok(ptr) +} + +fn with_env(f: impl FnOnce(&mut Env) -> Result) -> Result { + let Some(vm) = JAVA_VM.get() else { + return Err("JNI_OnLoad has not provided a JavaVM".to_string()); + }; + vm.attach_current_thread(|env| f(env).map_err(BridgeError::from)) + .map_err(|error| error.0) +} + +fn call_void(name: &str, sig: &str, args: &[JValue]) -> Result<(), String> { + with_env(|env| { + call_bridge_static(env, name, sig, args)?; + Ok(()) + }) +} + +fn call_bridge_static<'local>( + env: &mut Env<'local>, + name: &str, + sig: &str, + args: &[JValue], +) -> Result, String> { + let class = JNIString::new(BRIDGE_CLASS); + let name = JNIString::new(name); + let sig = RuntimeMethodSignature::from_str(sig).map_err(jni_error)?; + let sig_ref = MethodSignature::from(&sig); + env.call_static_method(class, name, sig_ref, args) + .map_err(jni_error) +} + +struct BridgeError(String); + +impl From for BridgeError { + fn from(error: String) -> Self { + Self(error) + } +} + +impl From for BridgeError { + fn from(error: jni::errors::Error) -> Self { + Self(error.to_string()) + } +} + +fn jni_error(error: impl std::fmt::Display) -> String { + error.to_string() +} diff --git a/crates/gdcef/src/mobile_webview/platform/mod.rs b/crates/gdcef/src/mobile_webview/platform/mod.rs new file mode 100644 index 0000000..dfc1c66 --- /dev/null +++ b/crates/gdcef/src/mobile_webview/platform/mod.rs @@ -0,0 +1,17 @@ +#[cfg(target_os = "android")] +mod android; +#[cfg(not(target_os = "android"))] +mod stub; + +#[cfg(target_os = "android")] +pub use android::AndroidWebViewBridge; +#[cfg(not(target_os = "android"))] +pub use stub::AndroidWebViewBridge; + +#[derive(Debug, Clone, Copy, Default)] +#[cfg_attr(not(target_os = "android"), allow(dead_code))] +pub struct FrameUpdate { + pub external_buffer_id: u64, + pub width: i32, + pub height: i32, +} diff --git a/crates/gdcef/src/mobile_webview/platform/stub.rs b/crates/gdcef/src/mobile_webview/platform/stub.rs new file mode 100644 index 0000000..2139084 --- /dev/null +++ b/crates/gdcef/src/mobile_webview/platform/stub.rs @@ -0,0 +1,82 @@ +use godot::classes::InputEvent; +use godot::prelude::*; + +use super::FrameUpdate; + +#[derive(Debug, Default)] +pub struct AndroidWebViewBridge; + +impl AndroidWebViewBridge { + pub fn new() -> Self { + Self + } + + pub fn create( + &mut self, + _instance_id: i64, + _size: Vector2i, + _url: String, + _javascript_enabled: bool, + ) -> Result<(), String> { + Err("Android WebView texture backend is only available on Android".to_string()) + } + + pub fn shutdown(&mut self, _instance_id: i64) -> Result<(), String> { + Ok(()) + } + + pub fn acquire_latest_frame( + &mut self, + _instance_id: i64, + ) -> Result, String> { + Ok(None) + } + + pub fn load_url(&mut self, _instance_id: i64, _url: String) -> Result<(), String> { + Ok(()) + } + + pub fn eval(&mut self, _instance_id: i64, _code: String) -> Result<(), String> { + Ok(()) + } + + pub fn reload(&mut self, _instance_id: i64) -> Result<(), String> { + Ok(()) + } + + pub fn go_back(&mut self, _instance_id: i64) -> Result<(), String> { + Ok(()) + } + + pub fn go_forward(&mut self, _instance_id: i64) -> Result<(), String> { + Ok(()) + } + + pub fn focus(&mut self, _instance_id: i64) -> Result<(), String> { + Ok(()) + } + + pub fn clear_focus(&mut self, _instance_id: i64) -> Result<(), String> { + Ok(()) + } + + pub fn resize(&mut self, _instance_id: i64, _size: Vector2i) -> Result<(), String> { + Ok(()) + } + + pub fn set_javascript_enabled( + &mut self, + _instance_id: i64, + _enabled: bool, + ) -> Result<(), String> { + Ok(()) + } + + pub fn forward_input( + &mut self, + _instance_id: i64, + _event: Gd, + ) -> Result<(), String> { + Ok(()) + } +} diff --git a/crates/gdcef/src/utils.rs b/crates/gdcef/src/utils.rs index a138573..f64ac3f 100644 --- a/crates/gdcef/src/utils.rs +++ b/crates/gdcef/src/utils.rs @@ -2,6 +2,7 @@ use crate::error::{CefError, CefResult}; use godot::classes::Engine; use godot::classes::Os; use godot::{classes::DisplayServer, obj::Singleton}; +#[cfg(not(target_os = "android"))] use process_path::get_dylib_path; use std::path::PathBuf; #[cfg(target_os = "linux")] @@ -192,10 +193,12 @@ fn env_or_empty(name: &str) -> String { std::env::var(name).unwrap_or_default() } +#[cfg(not(target_os = "android"))] fn get_dylib_path_checked() -> CefResult { get_dylib_path().ok_or_else(|| CefError::ResourceNotFound("dylib path".to_string())) } +#[cfg(not(target_os = "android"))] fn get_dylib_dir() -> CefResult { let dylib_path = get_dylib_path_checked()?; dylib_path @@ -276,7 +279,14 @@ pub fn get_subprocess_path() -> CefResult { .map_err(CefError::from) } -#[cfg(unix)] +#[cfg(target_os = "android")] +pub fn get_subprocess_path() -> CefResult { + Err(CefError::ResourceNotFound( + "CEF subprocess is not available on Android".to_string(), + )) +} + +#[cfg(all(unix, not(target_os = "android")))] pub fn ensure_executable_permissions() -> CefResult<()> { use std::os::unix::fs::PermissionsExt; @@ -322,12 +332,12 @@ pub fn ensure_executable_permissions() -> CefResult<()> { Ok(()) } -#[cfg(not(unix))] +#[cfg(any(not(unix), target_os = "android"))] pub fn ensure_executable_permissions() -> CefResult<()> { Ok(()) } -#[cfg(unix)] +#[cfg(all(unix, not(target_os = "android")))] fn get_executable_paths() -> CefResult> { let mut paths = Vec::new(); diff --git a/mise.toml b/mise.toml index 4762919..3c287e0 100644 --- a/mise.toml +++ b/mise.toml @@ -3,12 +3,53 @@ [tools] # Nightly is required by retour-rs for feature(unboxed_closures, tuple_trait). -rust = { version = "nightly", components = "rustfmt,clippy" } +rust = { version = "nightly", components = "rustfmt,clippy", targets = "aarch64-linux-android,x86_64-linux-android" } +java = "temurin-21" +gradle = "8.14.3" +kotlin = "2.2.21" +android-sdk = "20.0" node = "24" pnpm = "10.29.3" "cargo:export-cef-dir" = "148.1.0+147.0.14" +"cargo:cargo-ndk" = "latest" [env] # Keep this in sync with the resolved cef / cef-dll-sys version in Cargo.lock, # using the CEF runtime build version after the '+'. CEF_VERSION = "147.0.14" +ANDROID_RUST_TARGETS = "aarch64-linux-android x86_64-linux-android" + +[tasks."android:sdk"] +description = "Install Android SDK packages required by the Kotlin bridge" +run = [ + "yes | sdkmanager --licenses >/dev/null", + "sdkmanager 'platform-tools' 'platforms;android-35' 'build-tools;35.0.0' 'ndk;28.2.13676358'", +] + +[tasks."android:targets"] +description = "Install Rust targets used by Android builds" +run = "rustup target add $ANDROID_RUST_TARGETS" + +[tasks."android:kotlin"] +description = "Build the experimental Android WebView Kotlin bridge" +depends = ["android:sdk"] +run = "gradle -p android :godot-cef-webview:assembleDebug" + +[tasks."android:rust"] +description = "Check the gdcef crate for Android Rust targets" +depends = ["android:targets"] +run = [ + "cargo check -p gdcef --target aarch64-linux-android", + "cargo check -p gdcef --target x86_64-linux-android", +] + +[tasks."android:build-rust"] +description = "Build Android gdcef shared libraries" +depends = ["android:sdk", "android:targets"] +run = [ + "cargo ndk --target arm64-v8a --target x86_64 --platform 29 -- build -p gdcef --release", +] + +[tasks."android:check"] +description = "Check Android Rust targets and Kotlin bridge" +depends = ["android:rust", "android:kotlin"] diff --git a/xtask/src/platform.rs b/xtask/src/platform.rs index b43a0c8..ffb10f8 100644 --- a/xtask/src/platform.rs +++ b/xtask/src/platform.rs @@ -18,10 +18,13 @@ pub const WINDOWS_X64_TARGET: &str = "x86_64-pc-windows-msvc"; pub const WINDOWS_ARM64_TARGET: &str = "aarch64-pc-windows-msvc"; pub const LINUX_X64_TARGET: &str = "x86_64-unknown-linux-gnu"; pub const LINUX_ARM64_TARGET: &str = "aarch64-unknown-linux-gnu"; +pub const ANDROID_ARM64_TARGET: &str = "aarch64-linux-android"; +pub const ANDROID_X64_TARGET: &str = "x86_64-linux-android"; const MACOS_REQUIRED_FILES: &[&str] = &["Godot CEF.framework", "Godot CEF.app"]; const WINDOWS_REQUIRED_FILES: &[&str] = &["gdcef.dll", "gdcef_helper.exe", "libcef.dll"]; const LINUX_REQUIRED_FILES: &[&str] = &["libgdcef.so", "gdcef_helper", "libcef.so"]; +const ANDROID_REQUIRED_FILES: &[&str] = &["libgdcef.so"]; const LOCALES_DIR: &[&str] = &["locales"]; const NO_REQUIRED_DIRS: &[&str] = &[]; @@ -56,6 +59,18 @@ pub const PLATFORM_SPECS: &[PlatformSpec] = &[ required_files: LINUX_REQUIRED_FILES, required_dirs: LOCALES_DIR, }, + PlatformSpec { + target: ANDROID_ARM64_TARGET, + artifact_name: "gdcef-aarch64-linux-android", + required_files: ANDROID_REQUIRED_FILES, + required_dirs: NO_REQUIRED_DIRS, + }, + PlatformSpec { + target: ANDROID_X64_TARGET, + artifact_name: "gdcef-x86_64-linux-android", + required_files: ANDROID_REQUIRED_FILES, + required_dirs: NO_REQUIRED_DIRS, + }, ]; #[cfg(any(target_os = "windows", test))] From 48fbfa10941b63303ca0b6f954abd7310516dee3 Mon Sep 17 00:00:00 2001 From: Delton Ding Date: Mon, 25 May 2026 12:53:43 +0800 Subject: [PATCH 2/2] fixes --- Cargo.lock | 21 +++++++++++++++------ mise.toml | 21 +++++++++++++++------ 2 files changed, 30 insertions(+), 12 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 4e87389..3ed3c39 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -302,9 +302,9 @@ dependencies = [ [[package]] name = "cef" -version = "148.1.0+147.0.14" +version = "148.2.0+148.0.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f69f182b13a846048af7eb44b231c36ce316b2823ad0455e947f9ba955b2984f" +checksum = "89442ad338365f6a9a32f5595a734860921133a9aad6f9adb0af5624a9121597" dependencies = [ "anyhow", "ash", @@ -330,9 +330,9 @@ dependencies = [ [[package]] name = "cef-dll-sys" -version = "148.1.0+147.0.14" +version = "148.2.0+148.0.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e528ba9dfc0c7a28c820a6bb3f7c80611eaaa11e6b4f8d13913b2c527edf559d" +checksum = "22c85e020ec1e6fc257ef1f089a2fc3ada82ec87de52684e3eaac480bb0bdec5" dependencies = [ "anyhow", "cmake", @@ -785,7 +785,7 @@ dependencies = [ "objc2-metal", "percent-encoding", "process_path", - "quick-xml", + "quick-xml 0.40.1", "retour", "software_render", "url", @@ -1641,7 +1641,7 @@ checksum = "092791278e026273c1b65bbdcfbba3a300f2994c896bd01ab01da613c29c46f1" dependencies = [ "base64", "indexmap", - "quick-xml", + "quick-xml 0.39.4", "serde", "time", ] @@ -1765,6 +1765,15 @@ dependencies = [ "memchr", ] +[[package]] +name = "quick-xml" +version = "0.40.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2474bd2e5029e7ccb6abb2ba48cf2383a333851dedf495901544281590c7da7f" +dependencies = [ + "memchr", +] + [[package]] name = "quote" version = "1.0.45" diff --git a/mise.toml b/mise.toml index 3c287e0..c393f88 100644 --- a/mise.toml +++ b/mise.toml @@ -3,15 +3,10 @@ [tools] # Nightly is required by retour-rs for feature(unboxed_closures, tuple_trait). -rust = { version = "nightly", components = "rustfmt,clippy", targets = "aarch64-linux-android,x86_64-linux-android" } -java = "temurin-21" -gradle = "8.14.3" -kotlin = "2.2.21" -android-sdk = "20.0" +rust = { version = "nightly", components = "rustfmt,clippy" } node = "24" pnpm = "10.29.3" "cargo:export-cef-dir" = "148.1.0+147.0.14" -"cargo:cargo-ndk" = "latest" [env] # Keep this in sync with the resolved cef / cef-dll-sys version in Cargo.lock, @@ -26,6 +21,10 @@ run = [ "sdkmanager 'platform-tools' 'platforms;android-35' 'build-tools;35.0.0' 'ndk;28.2.13676358'", ] +[tasks."android:sdk".tools] +java = "temurin-21" +android-sdk = "20.0" + [tasks."android:targets"] description = "Install Rust targets used by Android builds" run = "rustup target add $ANDROID_RUST_TARGETS" @@ -35,6 +34,12 @@ description = "Build the experimental Android WebView Kotlin bridge" depends = ["android:sdk"] run = "gradle -p android :godot-cef-webview:assembleDebug" +[tasks."android:kotlin".tools] +java = "temurin-21" +gradle = "8.14.3" +kotlin = "2.2.21" +android-sdk = "20.0" + [tasks."android:rust"] description = "Check the gdcef crate for Android Rust targets" depends = ["android:targets"] @@ -50,6 +55,10 @@ run = [ "cargo ndk --target arm64-v8a --target x86_64 --platform 29 -- build -p gdcef --release", ] +[tasks."android:build-rust".tools] +android-sdk = "20.0" +"cargo:cargo-ndk" = "latest" + [tasks."android:check"] description = "Check Android Rust targets and Kotlin bridge" depends = ["android:rust", "android:kotlin"]