Skip to content
Draft
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
139 changes: 139 additions & 0 deletions ANDROID_CPP.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,139 @@
# C++ Android Support for Slint
Comment thread
ogoffart marked this conversation as resolved.
Outdated

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.
1 change: 1 addition & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

71 changes: 71 additions & 0 deletions api/cpp/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The 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?:)

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Agree. Although these are not default but mandatory features

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The 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".

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We could integrate it into the define_cargo_dependent_feature calls by adding AND NOT ANDROID to the depends_condition for winit/qt/femtovg, and using if(ANDROID) to flip the renderer-skia default to ON. The issue is that these are mandatory for Android, not just defaults — with the current approach, FORCE prevents the user from accidentally enabling winit on Android. With the depends_condition approach, we get the same effect since cmake_dependent_option forces the value OFF when the condition is false.

Want me to try that approach? It would replace the FORCE overrides with conditions on the feature definitions themselves.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The 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)

Comment thread
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")
Expand Down Expand Up @@ -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")
Expand Down Expand Up @@ -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++)
Expand Down
2 changes: 2 additions & 0 deletions api/cpp/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ backend-winit-x11 = ["i-slint-backend-selector/backend-winit-x11", "std"]
backend-winit-wayland = ["i-slint-backend-selector/backend-winit-wayland", "std"]
backend-linuxkms = ["i-slint-backend-selector/backend-linuxkms", "std"]
backend-linuxkms-noseat = ["i-slint-backend-selector/backend-linuxkms-noseat", "std"]
backend-android-activity = ["i-slint-backend-android-activity/native-activity", "i-slint-backend-android-activity/aa-06", "i-slint-backend-selector/backend-android-activity", "renderer-skia", "std"]
Comment thread
ruminorix marked this conversation as resolved.
Outdated
renderer-femtovg = ["i-slint-backend-selector/renderer-femtovg"]
renderer-femtovg-wgpu = ["i-slint-backend-selector/renderer-femtovg-wgpu"]
renderer-skia = ["i-slint-backend-selector/renderer-skia", "i-slint-renderer-skia", "raw-window-handle"]
Expand Down Expand Up @@ -69,6 +70,7 @@ i-slint-renderer-software = { workspace = true, optional = true }
i-slint-core = { workspace = true, features = ["ffi"] }
slint-interpreter = { workspace = true, features = ["ffi", "compat-1-2"], optional = true }
raw-window-handle = { version = "0.6", optional = true }
i-slint-backend-android-activity = { workspace = true, optional = true }

esp-backtrace = { version = "0.17.0", features = ["panic-handler", "println"], optional = true }
esp-println = { version = "0.15.0", default-features = false, features = ["auto", "log-04"], optional = true }
Expand Down
38 changes: 38 additions & 0 deletions api/cpp/android/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
# Slint C++ Android Template
Comment thread
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
38 changes: 38 additions & 0 deletions api/cpp/android/app/build.gradle.kts
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"
}
}
}
22 changes: 22 additions & 0 deletions api/cpp/android/app/src/main/AndroidManifest.xml
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>
18 changes: 18 additions & 0 deletions api/cpp/android/app/src/main/cpp/CMakeLists.txt
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)
13 changes: 13 additions & 0 deletions api/cpp/android/app/src/main/cpp/main.cpp
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();
}
Loading
Loading