immich/native
2026-07-11 04:57:13 +06:00
..
crates feat(mobile): log native failures to logcat and the unified log 2026-07-11 04:57:13 +06:00
immich_native_core feat(mobile): decode thumbhash in the native core 2026-07-11 04:56:46 +06:00
scripts feat: add native rust core 2026-06-29 20:53:47 +06:00
smoke refactor: drop hashing, the core ships the two pixel ports 2026-07-07 00:58:46 +06:00
.gitignore feat: add native rust core 2026-06-29 20:53:47 +06:00
Cargo.lock refactor(mobile): replace the android C layer with the rust core 2026-07-11 04:56:06 +06:00
Cargo.toml refactor(mobile): replace the android C layer with the rust core 2026-07-11 04:56:06 +06:00
mise.toml refactor: rename ffi crate to immich_core_ffi 2026-07-05 01:55:32 +06:00
README.md refactor: drop hashing, the core ships the two pixel ports 2026-07-07 00:58:46 +06:00

immich_native_core (PoC)

Shared Rust core for the pixel work the platforms can't do without native code, built from source into the app via Flutter native assets.

The two capabilities are ports of native code we already ship or have in review, so the same logic exists once, tested, instead of per-platform C:

  • EXIF-orientation rotate — from mobile/android/app/src/main/cpp/native_image.c (#29337, merged; fixes #24796, sideways RAW photos). Byte-for-byte the same affine
    • tiled copy, plus bounds checks the raw C can't have.
  • RGBA_1010102 → RGBA8888 convert — the 10-bit HEIC/AVIF color fix (#29631, fixes #24906). Same round(v*255/1023) LUT + packing as the C, proven on-device against Skia's Bitmap.copy(ARGB_8888).

Both fill a caller-owned output buffer (no allocation at the boundary) because the production callers hold JNI-locked bitmaps. Nothing in the app calls the core yet; wiring native_image.c's JNI shims to it is the follow-up.

Layout

crates/
  immich_core        pure logic, no binding deps. capabilities = cargo features (image).
  immich_core_ffi    the hand-written C ABI + cbindgen header — consumed by dart (ffigen),
                     swift (native C interop) and kotlin (JNI shim)
  immich_core_napi   cdylib (.node) via napi-rs (server, unwired)
immich_native_core/  the Flutter package mobile depends on. build hook + ffigen @Native bindings.
smoke/               host dart + node roundtrip scripts (no device)

Bindings are separate crates (Cargo can't gate crate-type by feature).

How the native lib is built (Flutter native assets — no prebuilt, no CI)

immich_native_core/hook/build.dart (native_toolchain_rust) compiles crates/immich_core_ffi from source on every app build via rustup and bundles it as a Flutter code asset. The Dart side uses ffigen @Native externals bound to that asset — no DynamicLibrary, no prebuilt artifacts, no fetch/publish/separate-repo.

Native assets is on by default on Flutter stable (3.38+), so a stock flutter build runs the hook. Each builder needs rustup (the hook auto-installs the pinned toolchain + targets from crates/immich_core_ffi/rust-toolchain.toml).

Dev commands (mise)

mise run build         cargo build --workspace
mise run test          cargo test --workspace     (host Rust tests, incl. FFI-boundary)
mise run lint          clippy -D warnings         (fmt: mise run fmt)
mise run codegen       regen cbindgen header + ffigen @Native bindings — commit the result
mise run test:flutter  HOST FFI roundtrip through the real build hook (no device)
mise run smoke         Rust tests + host dart:ffi + host napi roundtrips

Add a capability (end to end)

  1. add the logic to crates/immich_core (behind a cargo feature if it pulls a dep).
  2. expose a C entry in crates/immich_core_ffi/src/lib.rs#[no_mangle] pub extern "C", wrap the body in guard(...)/catch_unwind (panic at the boundary → sentinel, never unwind into the host), validate pointers, and either fill a caller-owned buffer or return Rust-owned memory freed via immich_core_free_string.
  3. mise run codegen — regenerates the committed cbindgen header + ffigen @Native bindings.
  4. mise run test:flutter (host) + a case in immich_native_core/test/ and in mobile/integration_test/native_core_test.dart (device). The dart surface is the generated bindings; add a hand-written dart wrapper only when a dart feature consumes it.
  5. platform callers: kotlin via the existing JNI shim pattern (NativeImage.kt), swift reads the same header natively.

Consume from immich/mobile

immich_native_core: { path: ../native/immich_native_core } in mobile/pubspec.yaml, then dart pub get. No app-level Gradle/Podfile edits — the hook builds + bundles the lib. Builders need rustup. See the package README for the iOS App-Extension caveat.

/native/ is codeowned by @santoshakil + @mertalev. License: reuses the immich repo-root AGPL-3.0 (no separate license file).