From d6b077a59c7ad195ca8aedf28abd154681850e6a Mon Sep 17 00:00:00 2001 From: Santo Shakil Date: Thu, 23 Jul 2026 03:41:34 +0600 Subject: [PATCH] feat(mobile): free native core buffers through a dedicated export --- mobile/integration_test/native_core_test.dart | 2 +- mobile/lib/utils/hooks/blurhash_hook.dart | 2 +- native/crates/immich_core_ffi/include/immich_core.h | 13 +++++++++++-- native/crates/immich_core_ffi/src/capi/mod.rs | 11 +++++++++++ native/crates/immich_core_ffi/src/capi/thumbhash.rs | 4 ++-- native/crates/immich_core_ffi/src/lib.rs | 2 +- native/crates/immich_core_ffi/tests/c_abi.rs | 9 +++++++-- .../immich_native_core/lib/src/ffi/bindings.g.dart | 12 ++++++++++-- .../immich_native_core/test/native_core_test.dart | 2 +- 9 files changed, 45 insertions(+), 12 deletions(-) diff --git a/mobile/integration_test/native_core_test.dart b/mobile/integration_test/native_core_test.dart index 95e642e8be..49c1029df4 100644 --- a/mobile/integration_test/native_core_test.dart +++ b/mobile/integration_test/native_core_test.dart @@ -86,7 +86,7 @@ void main() { expect(pixels[i], 255, reason: 'alpha at $i'); } expect(pixels.toSet().length, greaterThan(2)); - malloc.free(ptr); + immich_core_free(ptr); expect(immich_core_thumbhash_decode(hashPtr, 4, info), equals(nullptr)); } finally { diff --git a/mobile/lib/utils/hooks/blurhash_hook.dart b/mobile/lib/utils/hooks/blurhash_hook.dart index fc5feafc71..87d1f7e02b 100644 --- a/mobile/lib/utils/hooks/blurhash_hook.dart +++ b/mobile/lib/utils/hooks/blurhash_hook.dart @@ -35,7 +35,7 @@ Uint8List? decodeDriftThumbHash(String? thumbHash) { try { return _rgbaToBmp(rgba, info[0], info[1], info[2]); } finally { - malloc.free(rgba); + immich_core_free(rgba); } } finally { malloc.free(hashPtr); diff --git a/native/crates/immich_core_ffi/include/immich_core.h b/native/crates/immich_core_ffi/include/immich_core.h index 7be3e21c64..750524209c 100644 --- a/native/crates/immich_core_ffi/include/immich_core.h +++ b/native/crates/immich_core_ffi/include/immich_core.h @@ -20,6 +20,15 @@ char *immich_core_version(void); */ void immich_core_free_string(char *ptr); +/** + * Release a buffer returned by this library, such as a decoded ThumbHash. + * + * # Safety + * `ptr` must be a buffer previously returned by this library, or null, and + * must not be released twice. + */ +void immich_core_free(uint8_t *ptr); + /** * Returns whether an EXIF orientation swaps width and height. */ @@ -55,8 +64,8 @@ bool immich_core_rgba1010102_to_rgba8888(const uint8_t *src, uintptr_t dst_len); /** - * Decodes a ThumbHash into a libc buffer and writes width, height, and row bytes to `out_info`. - * Free the buffer with `free`; malformed hashes return null. + * Decodes a ThumbHash into a new buffer and writes width, height, and row bytes to `out_info`. + * Free the buffer with `immich_core_free`; malformed hashes return null. * * # Safety * `hash` must be valid for `hash_len` bytes and `out_info` for three u32 writes. diff --git a/native/crates/immich_core_ffi/src/capi/mod.rs b/native/crates/immich_core_ffi/src/capi/mod.rs index 437b113fa0..a907588946 100644 --- a/native/crates/immich_core_ffi/src/capi/mod.rs +++ b/native/crates/immich_core_ffi/src/capi/mod.rs @@ -28,6 +28,17 @@ pub unsafe extern "C" fn immich_core_free_string(ptr: *mut c_char) { }); } +/// Release a buffer returned by this library, such as a decoded ThumbHash. +/// +/// # Safety +/// `ptr` must be a buffer previously returned by this library, or null, and +/// must not be released twice. +#[no_mangle] +pub unsafe extern "C" fn immich_core_free(ptr: *mut u8) { + // SAFETY: the buffer came from this library's libc allocation; free accepts null. + unsafe { libc::free(ptr as *mut core::ffi::c_void) }; +} + fn guard(sentinel: T, f: impl FnOnce() -> T + std::panic::UnwindSafe) -> T { crate::log::ensure_panic_hook(); std::panic::catch_unwind(f).unwrap_or(sentinel) diff --git a/native/crates/immich_core_ffi/src/capi/thumbhash.rs b/native/crates/immich_core_ffi/src/capi/thumbhash.rs index a4231f31b2..faf061ce60 100644 --- a/native/crates/immich_core_ffi/src/capi/thumbhash.rs +++ b/native/crates/immich_core_ffi/src/capi/thumbhash.rs @@ -22,8 +22,8 @@ pub(crate) fn decode_malloc(hash: &[u8]) -> Option<(*mut u8, u32, u32)> { Some((dst, w, h)) } -/// Decodes a ThumbHash into a libc buffer and writes width, height, and row bytes to `out_info`. -/// Free the buffer with `free`; malformed hashes return null. +/// Decodes a ThumbHash into a new buffer and writes width, height, and row bytes to `out_info`. +/// Free the buffer with `immich_core_free`; malformed hashes return null. /// /// # Safety /// `hash` must be valid for `hash_len` bytes and `out_info` for three u32 writes. diff --git a/native/crates/immich_core_ffi/src/lib.rs b/native/crates/immich_core_ffi/src/lib.rs index 429777ae3f..077f9ca672 100644 --- a/native/crates/immich_core_ffi/src/lib.rs +++ b/native/crates/immich_core_ffi/src/lib.rs @@ -18,4 +18,4 @@ pub use capi::image::{ immich_core_rotate_rgba8888, }; pub use capi::thumbhash::immich_core_thumbhash_decode; -pub use capi::{immich_core_free_string, immich_core_version}; +pub use capi::{immich_core_free, immich_core_free_string, immich_core_version}; diff --git a/native/crates/immich_core_ffi/tests/c_abi.rs b/native/crates/immich_core_ffi/tests/c_abi.rs index 4742579f79..3794e15511 100644 --- a/native/crates/immich_core_ffi/tests/c_abi.rs +++ b/native/crates/immich_core_ffi/tests/c_abi.rs @@ -4,7 +4,7 @@ use std::ffi::CStr; use std::ptr; use immich_core_ffi::{ - immich_core_free_string, immich_core_orientation_swaps_dims, + immich_core_free, immich_core_free_string, immich_core_orientation_swaps_dims, immich_core_rgba1010102_to_rgba8888, immich_core_rotate_rgba8888, immich_core_thumbhash_decode, immich_core_version, }; @@ -186,7 +186,12 @@ fn thumbhash_decodes_into_a_malloc_buffer() { let pixels = unsafe { std::slice::from_raw_parts(ptr, len) }; assert!(pixels.chunks_exact(4).all(|px| px[3] == 255)); assert!(pixels.chunks_exact(4).any(|px| px[0] != pixels[0])); - unsafe { libc::free(ptr as *mut libc::c_void) }; + unsafe { immich_core_free(ptr) }; +} + +#[test] +fn free_accepts_null() { + unsafe { immich_core_free(ptr::null_mut()) }; } #[test] diff --git a/native/immich_native_core/lib/src/ffi/bindings.g.dart b/native/immich_native_core/lib/src/ffi/bindings.g.dart index 783d0d9bc1..01b8e0f333 100644 --- a/native/immich_native_core/lib/src/ffi/bindings.g.dart +++ b/native/immich_native_core/lib/src/ffi/bindings.g.dart @@ -18,6 +18,14 @@ external ffi.Pointer immich_core_version(); @ffi.Native)>() external void immich_core_free_string(ffi.Pointer ptr); +/// Release a buffer returned by this library, such as a decoded ThumbHash. +/// +/// # Safety +/// `ptr` must be a buffer previously returned by this library, or null, and +/// must not be released twice. +@ffi.Native)>() +external void immich_core_free(ffi.Pointer ptr); + /// Returns whether an EXIF orientation swaps width and height. @ffi.Native() external bool immich_core_orientation_swaps_dims(int orientation); @@ -74,8 +82,8 @@ external bool immich_core_rgba1010102_to_rgba8888( int dst_len, ); -/// Decodes a ThumbHash into a libc buffer and writes width, height, and row bytes to `out_info`. -/// Free the buffer with `free`; malformed hashes return null. +/// Decodes a ThumbHash into a new buffer and writes width, height, and row bytes to `out_info`. +/// Free the buffer with `immich_core_free`; malformed hashes return null. /// /// # Safety /// `hash` must be valid for `hash_len` bytes and `out_info` for three u32 writes. diff --git a/native/immich_native_core/test/native_core_test.dart b/native/immich_native_core/test/native_core_test.dart index f8eaf6f286..e739fc87bb 100644 --- a/native/immich_native_core/test/native_core_test.dart +++ b/native/immich_native_core/test/native_core_test.dart @@ -125,7 +125,7 @@ void main() { expect(pixels[i], 255, reason: 'alpha at $i'); } expect(pixels.toSet().length, greaterThan(2)); - malloc.free(ptr); + immich_core_free(ptr); expect(immich_core_thumbhash_decode(hashPtr, 4, info), equals(nullptr)); } finally {