-
Notifications
You must be signed in to change notification settings - Fork 882
cpp: add Android backend support #11628
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: master
Are you sure you want to change the base?
Changes from 1 commit
5f73e4f
eb2fdad
0aa0fe8
47920be
079f353
b7fd64c
85ce0f0
bb30ab0
841152a
b633213
a6ac20d
3107315
6493c4e
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,139 @@ | ||
| # C++ Android Support for Slint | ||
|
|
||
| Tracking issue: https://github.com/slint-ui/slint/issues/6281 | ||
|
|
||
| ## Status | ||
|
|
||
| Initial implementation complete. The todo example has been successfully built | ||
| and tested as an APK on an Android device. | ||
|
|
||
| ## What was implemented | ||
|
|
||
| ### Phase 1: Rust-side plumbing (api/cpp/) | ||
|
|
||
| **Cargo.toml** — new `backend-android-activity` feature: | ||
| - Enables `i-slint-backend-android-activity` (native-activity, aa-06) | ||
| - Enables `i-slint-backend-selector/backend-android-activity` | ||
| - Pulls in `renderer-skia` and `std` | ||
| - No version suffix needed (C++ doesn't expose AndroidApp types) | ||
|
|
||
| **lib.rs** — `android_main` bridge: | ||
| - Gated on `#[cfg(all(target_os = "android", feature = "backend-android-activity"))]` | ||
| - Implements `android_main(AndroidApp)` that initializes the Android platform | ||
| - Calls `extern "C" slint_main()` — the user's C++ entry point | ||
|
|
||
| **cbindgen.rs**: | ||
| - Added `backend_android_activity` to feature declarations | ||
| (generates `SLINT_FEATURE_BACKEND_ANDROID_ACTIVITY` define) | ||
| - Added `using ::slint::Timer` and `Option<T>` template in cbindgen_private | ||
| namespace (needed for Android-only `ContextMenu::long_press_timer` field) | ||
|
|
||
| ### Phase 2: CMake integration (api/cpp/CMakeLists.txt) | ||
|
|
||
| - `define_cargo_dependent_feature(backend-android-activity ...)` option | ||
| - Android auto-configuration block (before feature definitions, so FORCE | ||
| cache values are picked up): | ||
| - Maps `ANDROID_ABI` -> Rust target triple (arm64-v8a, armeabi-v7a, x86_64, x86) | ||
| - Forces `BUILD_SHARED_LIBS=ON` | ||
| - Enables android backend + skia, disables winit/qt/femtovg | ||
| - Sets Material style as default | ||
| - NDK/SDK environment variable propagation to the Rust build | ||
| (`ANDROID_NDK_ROOT`, `ANDROID_HOME` via `CMAKE_ANDROID_NDK`/`CMAKE_ANDROID_SDK`) | ||
|
|
||
| ### Phase 3: Gradle template project (api/cpp/android/) | ||
|
|
||
| Complete, copyable template project with: | ||
| - `build.gradle.kts` / `settings.gradle.kts` / `gradle.properties` | ||
| - `gradle/wrapper/gradle-wrapper.properties` (Gradle 8.11) | ||
| - `app/build.gradle.kts` (compileSdk 35, minSdk 26, arm64-v8a) | ||
| - `app/src/main/AndroidManifest.xml` (NativeActivity) | ||
| - `app/src/main/cpp/CMakeLists.txt` (FetchContent for Slint) | ||
| - `app/src/main/cpp/main.cpp` (example `slint_main()`) | ||
| - `app/src/main/cpp/main.slint` (hello world UI) | ||
|
|
||
| ### Phase 4: Documentation | ||
|
|
||
| - New page: `docs/astro/.../android-cpp.mdx` covering prerequisites, project | ||
| setup, writing apps, building, deploying, JNI interop, troubleshooting | ||
| - Updated `android.mdx` — removed "only Rust" note, added link to C++ guide | ||
| - Updated `general.mdx` — mentions C++ Android support | ||
|
|
||
| ### Phase 5: Todo example (examples/todo/cpp/android/) | ||
|
|
||
| Android build of the existing C++ todo example, tested and produces a working | ||
| APK for arm64-v8a. | ||
|
|
||
| ### Bug fixes discovered during implementation | ||
|
|
||
| 1. **slint_callbacks.h — `std::apply` incompatibility with NDK r27**: | ||
| NDK r27's libc++ is stricter about `std::apply` with const tuple references. | ||
| Fixed by replacing `std::apply` with a custom `detail::apply` helper that | ||
| properly forwards tuple elements. | ||
|
|
||
| 2. **cbindgen.rs — missing `Option<Timer>` definition for Android**: | ||
| The `ContextMenu` struct has an Android-only `long_press_timer` field of | ||
| type `Option<Timer>`. cbindgen forward-declared `Option` but never defined | ||
| it, and `Timer` was not in the `cbindgen_private` namespace. Fixed by adding | ||
| a layout-compatible `Option<T>` template and a `using` declaration for Timer. | ||
|
|
||
| ## Architecture | ||
|
|
||
| ### Runtime flow | ||
|
|
||
| ``` | ||
| Android OS loads libapp.so | ||
| -> android-activity crate glue calls android_main(AndroidApp) [Rust, in slint-cpp] | ||
| -> initializes AndroidPlatform (Skia renderer, input, clipboard, etc.) | ||
| -> calls extern "C" slint_main() [user's C++ code] | ||
| -> normal Slint C++ API: create windows, run event loop | ||
| ``` | ||
|
|
||
| ### Build flow (Gradle) | ||
|
|
||
| ``` | ||
| Gradle (orchestrates) | ||
| -> CMake (NDK toolchain) | ||
| -> Corrosion (cross-compiles slint-cpp Rust crate to Android target) | ||
| -> NDK clang (compiles user's C++ code) | ||
| -> links into libapp.so | ||
| -> packages into APK | ||
| ``` | ||
|
|
||
| ### Build flow (manual CMake, used for the todo example) | ||
|
|
||
| ``` | ||
| cmake -DCMAKE_TOOLCHAIN_FILE=$NDK/build/cmake/android.toolchain.cmake \ | ||
| -DANDROID_ABI=arm64-v8a -DANDROID_PLATFORM=android-26 ... | ||
| cmake --build . | ||
| # Then package with aapt2 + zipalign + apksigner | ||
| ``` | ||
|
|
||
| ## Design Decisions | ||
|
|
||
| | Decision | Rationale | | ||
| |---|---| | ||
| | `slint_main()` entry point | Platform-neutral; hides Android/Rust complexity; reusable for iOS | | ||
| | Gradle + CMake (not xbuild) | Standard C++ Android workflow; no new tools | | ||
| | Skia renderer only | Proven on Android; FemtoVG has fontconfig issues | | ||
| | `backend-android-activity` (no version suffix) | C++ doesn't expose AndroidApp types | | ||
| | Template project (not generator) | Simple, inspectable; same pattern as ESP-IDF | | ||
| | Minimum API 26 | Required for InMemoryDexClassLoader; 95%+ of devices | | ||
| | Android block before feature definitions | Cache FORCE values must be set before option() reads them | | ||
|
|
||
| ## Remaining Work | ||
|
|
||
| 1. **JNI exposure**: Expose `JNIEnv*` and activity `jobject` to C++ users | ||
| for calling Java APIs (sensors, notifications, etc.) | ||
|
|
||
| 2. **Android lifecycle callbacks**: Expose pause/resume/save-state events | ||
| to C++ (the Rust backend handles them internally today). | ||
|
|
||
| 3. **GameActivity support**: Currently uses NativeActivity only. | ||
|
|
||
| 4. **Multi-ABI builds**: Template defaults to arm64-v8a. Supporting multiple | ||
| ABIs multiplies Rust compile time. | ||
|
|
||
| 5. **CI integration**: Add Android C++ build to CI pipeline. | ||
|
|
||
| 6. **Strip/compress native libraries**: The APK is ~33MB uncompressed. | ||
| Stripping debug symbols and compressing the .so files would help. | ||
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -24,6 +24,40 @@ endif () | |
| list(PREPEND CMAKE_MODULE_PATH ${Corrosion_SOURCE_DIR}/cmake) | ||
| find_package(Rust 1.92 REQUIRED MODULE) | ||
|
|
||
| # When building for Android (NDK toolchain sets CMAKE_SYSTEM_NAME to "Android"), | ||
| # auto-configure the Rust target, enable the android backend, and set sensible defaults. | ||
| # This block must run before the feature options are defined so that the forced cache values | ||
| # are picked up by define_cargo_feature / define_cargo_dependent_feature. | ||
| if(ANDROID) | ||
| # Map Android ABI to Rust target triple | ||
| if(ANDROID_ABI STREQUAL "arm64-v8a") | ||
| set(Rust_CARGO_TARGET "aarch64-linux-android" CACHE STRING "" FORCE) | ||
| elseif(ANDROID_ABI STREQUAL "armeabi-v7a") | ||
| set(Rust_CARGO_TARGET "armv7-linux-androideabi" CACHE STRING "" FORCE) | ||
| elseif(ANDROID_ABI STREQUAL "x86_64") | ||
| set(Rust_CARGO_TARGET "x86_64-linux-android" CACHE STRING "" FORCE) | ||
| elseif(ANDROID_ABI STREQUAL "x86") | ||
| set(Rust_CARGO_TARGET "i686-linux-android" CACHE STRING "" FORCE) | ||
| else() | ||
| message(FATAL_ERROR "Unsupported ANDROID_ABI: ${ANDROID_ABI}") | ||
| endif() | ||
|
|
||
| # Android requires a shared library | ||
| set(BUILD_SHARED_LIBS ON CACHE BOOL "" FORCE) | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I wonder if these defaults could be set at the source instead of here with these kind of ugly overrides. We're in the right file, aren't we?:)
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Agree. Although these are not default but mandatory features
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Updated the comment to say "Android requires the skia renderer and cannot use desktop backends".
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. We could integrate it into the Want me to try that approach? It would replace the FORCE overrides with conditions on the feature definitions themselves.
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. If mandatory perhaps that should become an option in the macro/function. But IMO it's sufficient if defaults are on/off correctly and it's all in one place. My concern is that if we remove/rename the feature we need to look at multiple places. (Not a super big issue but worth research if it can be done better) |
||
|
|
||
| # Enable the android backend and skia renderer, disable desktop backends | ||
| set(SLINT_FEATURE_BACKEND_ANDROID_ACTIVITY ON CACHE BOOL "" FORCE) | ||
| set(SLINT_FEATURE_RENDERER_SKIA ON CACHE BOOL "" FORCE) | ||
| set(SLINT_FEATURE_BACKEND_WINIT OFF CACHE BOOL "" FORCE) | ||
| set(SLINT_FEATURE_BACKEND_QT OFF CACHE BOOL "" FORCE) | ||
| set(SLINT_FEATURE_RENDERER_FEMTOVG OFF CACHE BOOL "" FORCE) | ||
|
|
||
|
ruminorix marked this conversation as resolved.
Outdated
|
||
| # Default to Material style on Android | ||
| if(NOT SLINT_STYLE) | ||
| set(SLINT_STYLE "material" CACHE STRING "" FORCE) | ||
| endif() | ||
| endif() | ||
|
|
||
| option(BUILD_SHARED_LIBS "Build Slint as shared library" ON) | ||
| option(SLINT_FEATURE_COMPILER "Enable support for compiling .slint files to C++ ahead of time" ON) | ||
| add_feature_info(SLINT_FEATURE_COMPILER SLINT_FEATURE_COMPILER "Enable support for compiling .slint files to C++ ahead of time") | ||
|
|
@@ -111,6 +145,8 @@ define_cargo_dependent_feature(backend-qt "Enable Qt based rendering backend" OF | |
| define_cargo_dependent_feature(backend-linuxkms "Enable support for the backend that renders a single window fullscreen on Linux. Requires libseat. If you don't have libseat, select `backend-linuxkms-noseat` instead." OFF "NOT SLINT_FEATURE_FREESTANDING") | ||
| define_cargo_dependent_feature(backend-linuxkms-noseat "Enable support for the backend that renders a single window fullscreen on Linux" OFF "NOT SLINT_FEATURE_FREESTANDING") | ||
|
|
||
| define_cargo_dependent_feature(backend-android-activity "Enable the Android backend for building Slint apps on Android" OFF "NOT SLINT_FEATURE_FREESTANDING") | ||
|
|
||
| define_cargo_dependent_feature(gettext "Enable support of translations using gettext" OFF "NOT SLINT_FEATURE_FREESTANDING") | ||
| define_cargo_dependent_feature(accessibility "Enable integration with operating system provided accessibility APIs" ON "NOT SLINT_FEATURE_FREESTANDING") | ||
| define_cargo_dependent_feature(testing "Enable support for testing API (experimental)" ON "NOT SLINT_FEATURE_FREESTANDING") | ||
|
|
@@ -306,6 +342,41 @@ if (SLINT_BUILD_RUNTIME) | |
| ) | ||
| endif() | ||
|
|
||
| # Propagate Android NDK/SDK paths to the Rust build so that the android-activity | ||
| # backend can compile Java sources and produce DEX bytecode. | ||
| if(ANDROID) | ||
| if(DEFINED ENV{ANDROID_NDK_ROOT}) | ||
| set_property( | ||
| TARGET slint_cpp | ||
| APPEND | ||
| PROPERTY CORROSION_ENVIRONMENT_VARIABLES | ||
| "ANDROID_NDK_ROOT=$ENV{ANDROID_NDK_ROOT}" | ||
| ) | ||
| elseif(CMAKE_ANDROID_NDK) | ||
| set_property( | ||
| TARGET slint_cpp | ||
| APPEND | ||
| PROPERTY CORROSION_ENVIRONMENT_VARIABLES | ||
| "ANDROID_NDK_ROOT=${CMAKE_ANDROID_NDK}" | ||
| ) | ||
| endif() | ||
| if(DEFINED ENV{ANDROID_HOME}) | ||
| set_property( | ||
| TARGET slint_cpp | ||
| APPEND | ||
| PROPERTY CORROSION_ENVIRONMENT_VARIABLES | ||
| "ANDROID_HOME=$ENV{ANDROID_HOME}" | ||
| ) | ||
| elseif(CMAKE_ANDROID_SDK) | ||
| set_property( | ||
| TARGET slint_cpp | ||
| APPEND | ||
| PROPERTY CORROSION_ENVIRONMENT_VARIABLES | ||
| "ANDROID_HOME=${CMAKE_ANDROID_SDK}" | ||
| ) | ||
| endif() | ||
| endif() | ||
|
|
||
| if(SLINT_FEATURE_RENDERER_SKIA OR SLINT_FEATURE_RENDERER_SKIA_OPENGL OR SLINT_FEATURE_RENDERER_SKIA_VULKAN) | ||
| find_program(CLANGCC clang) | ||
| find_program(CLANGCXX clang++) | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,38 @@ | ||
| # Slint C++ Android Template | ||
|
ruminorix marked this conversation as resolved.
Outdated
|
||
|
|
||
| This directory contains a template project for building Slint C++ applications | ||
| on Android using Gradle and CMake. | ||
|
|
||
| See the [Slint C++ Android documentation](https://slint.dev/docs/guide/platforms/mobile/android-cpp) | ||
| for detailed instructions. | ||
|
|
||
| ## Quick Start | ||
|
|
||
| 1. Copy this directory to create your project. | ||
|
|
||
| 2. Set up the required environment variables: | ||
| ```sh | ||
| export ANDROID_HOME=$HOME/Android/Sdk | ||
| export ANDROID_NDK_ROOT=$ANDROID_HOME/ndk/<version> | ||
| ``` | ||
|
|
||
| 3. Install the Rust Android target: | ||
| ```sh | ||
| rustup target add aarch64-linux-android | ||
| ``` | ||
|
|
||
| 4. Edit `app/src/main/cpp/main.cpp` and `app/src/main/cpp/main.slint` with | ||
| your application code. | ||
|
|
||
| 5. Build and deploy: | ||
| ```sh | ||
| ./gradlew installDebug | ||
| ``` | ||
|
|
||
| ## Project Structure | ||
|
|
||
| - `app/build.gradle.kts` - Android app configuration (SDK versions, CMake setup) | ||
| - `app/src/main/AndroidManifest.xml` - Android manifest with NativeActivity | ||
| - `app/src/main/cpp/CMakeLists.txt` - CMake build for C++ code and Slint | ||
| - `app/src/main/cpp/main.cpp` - Application entry point (`slint_main`) | ||
| - `app/src/main/cpp/main.slint` - Slint UI definition | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,38 @@ | ||
| // Copyright © SixtyFPS GmbH <info@slint.dev> | ||
| // SPDX-License-Identifier: GPL-3.0-only OR LicenseRef-Slint-Royalty-free-2.0 OR LicenseRef-Slint-Software-3.0 | ||
|
|
||
| plugins { | ||
| id("com.android.application") | ||
| } | ||
|
|
||
| android { | ||
| namespace = "dev.slint.app" | ||
| compileSdk = 35 | ||
|
|
||
| defaultConfig { | ||
| applicationId = "dev.slint.app" | ||
| // API 26 required for InMemoryDexClassLoader used by the Slint Android backend | ||
| minSdk = 26 | ||
| targetSdk = 35 | ||
| versionCode = 1 | ||
| versionName = "1.0" | ||
|
|
||
| ndk { | ||
| // Add other ABIs as needed: "armeabi-v7a", "x86_64", "x86" | ||
| abiFilters += "arm64-v8a" | ||
| } | ||
| } | ||
|
|
||
| buildTypes { | ||
| release { | ||
| isMinifyEnabled = false | ||
| } | ||
| } | ||
|
|
||
| externalNativeBuild { | ||
| cmake { | ||
| path = file("src/main/cpp/CMakeLists.txt") | ||
| version = "3.22.1" | ||
| } | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,22 @@ | ||
| <!-- Copyright © SixtyFPS GmbH <info@slint.dev> --> | ||
| <!-- SPDX-License-Identifier: GPL-3.0-only OR LicenseRef-Slint-Royalty-free-2.0 OR LicenseRef-Slint-Software-3.0 --> | ||
|
|
||
| <?xml version="1.0" encoding="utf-8"?> | ||
| <manifest xmlns:android="http://schemas.android.com/apk/res/android"> | ||
| <application | ||
| android:label="Slint App" | ||
| android:hasCode="false"> | ||
| <activity | ||
| android:name="android.app.NativeActivity" | ||
| android:exported="true" | ||
| android:configChanges="orientation|screenSize|keyboardHidden"> | ||
| <meta-data | ||
| android:name="android.app.lib_name" | ||
| android:value="app" /> | ||
| <intent-filter> | ||
| <action android:name="android.intent.action.MAIN" /> | ||
| <category android:name="android.intent.category.LAUNCHER" /> | ||
| </intent-filter> | ||
| </activity> | ||
| </application> | ||
| </manifest> |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,18 @@ | ||
| # Copyright © SixtyFPS GmbH <info@slint.dev> | ||
| # SPDX-License-Identifier: GPL-3.0-only OR LicenseRef-Slint-Royalty-free-2.0 OR LicenseRef-Slint-Software-3.0 | ||
|
|
||
| cmake_minimum_required(VERSION 3.21) | ||
| project(SlintApp LANGUAGES CXX) | ||
|
|
||
| include(FetchContent) | ||
| FetchContent_Declare( | ||
| Slint | ||
| GIT_REPOSITORY https://github.com/slint-ui/slint.git | ||
| GIT_TAG v1.17.0 | ||
| SOURCE_SUBDIR api/cpp | ||
| ) | ||
| FetchContent_MakeAvailable(Slint) | ||
|
|
||
| add_library(app SHARED main.cpp) | ||
| target_link_libraries(app PRIVATE Slint::Slint) | ||
| slint_target_sources(app main.slint) |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,13 @@ | ||
| // Copyright © SixtyFPS GmbH <info@slint.dev> | ||
| // SPDX-License-Identifier: GPL-3.0-only OR LicenseRef-Slint-Royalty-free-2.0 OR LicenseRef-Slint-Software-3.0 | ||
|
|
||
| #include "main.h" // Generated from main.slint | ||
|
|
||
| // Entry point for Slint applications on Android. | ||
| // The Slint runtime takes care of Android platform initialization; | ||
| // this function is called once the platform is ready. | ||
| extern "C" void slint_main() | ||
| { | ||
| auto window = MainWindow::create(); | ||
| window->run(); | ||
| } |
Uh oh!
There was an error while loading. Please reload this page.