diff --git a/Cargo.lock b/Cargo.lock index ab3b3c7d06..0e06e3922c 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -5600,6 +5600,7 @@ version = "0.5.1512" dependencies = [ "cc", "libc", + "perry-ffi", ] [[package]] @@ -6343,6 +6344,7 @@ dependencies = [ "libc", "perry-audio-miniaudio", "perry-ext-sharp", + "perry-ffi", "perry-runtime", "perry-ui", "ryu", @@ -6394,6 +6396,7 @@ dependencies = [ "objc2-core-foundation", "objc2-foundation", "objc2-ui-kit", + "perry-ffi", "perry-runtime", "perry-ui", "perry-ui-testkit", @@ -6410,6 +6413,7 @@ dependencies = [ "objc2-app-kit", "objc2-core-foundation", "objc2-foundation", + "perry-ffi", "perry-ui", "perry-ui-testkit", ] @@ -6440,6 +6444,7 @@ dependencies = [ "objc2-core-foundation", "objc2-foundation", "objc2-ui-kit", + "perry-ffi", "perry-runtime", "perry-ui", "perry-ui-testkit", @@ -6456,6 +6461,7 @@ dependencies = [ "objc2-core-foundation", "objc2-foundation", "objc2-ui-kit", + "perry-ffi", "perry-runtime", "perry-ui", "perry-ui-testkit", @@ -6469,6 +6475,7 @@ dependencies = [ "libc", "objc2", "objc2-foundation", + "perry-ffi", "perry-runtime", "perry-ui", "perry-ui-testkit", @@ -6481,6 +6488,7 @@ dependencies = [ "base64 0.22.1", "libc", "perry-audio-miniaudio", + "perry-ffi", "perry-runtime", "perry-ui", "perry-ui-testkit", diff --git a/changelog.d/8453-owned-ui-strings.md b/changelog.d/8453-owned-ui-strings.md new file mode 100644 index 0000000000..d4f5350af2 --- /dev/null +++ b/changelog.d/8453-owned-ui-strings.md @@ -0,0 +1,5 @@ +### Fixed + +- Native UI and audio backends now copy runtime strings into owned Rust + storage before use, preventing dangling borrows when Perry's garbage + collector relocates the original string. diff --git a/crates/perry-audio-miniaudio/Cargo.toml b/crates/perry-audio-miniaudio/Cargo.toml index adf6750310..a9b3071a23 100644 --- a/crates/perry-audio-miniaudio/Cargo.toml +++ b/crates/perry-audio-miniaudio/Cargo.toml @@ -16,3 +16,4 @@ cc = "1" [target.'cfg(any(target_os = "linux", target_os = "windows", target_os = "android"))'.dependencies] libc.workspace = true +perry-ffi.workspace = true diff --git a/crates/perry-audio-miniaudio/src/lib.rs b/crates/perry-audio-miniaudio/src/lib.rs index 56727dfcb2..998d8e09dd 100644 --- a/crates/perry-audio-miniaudio/src/lib.rs +++ b/crates/perry-audio-miniaudio/src/lib.rs @@ -22,33 +22,7 @@ use std::cell::RefCell; use std::ffi::CString; use std::sync::Mutex; -// ============================================================================= -// String header — mirrors perry_runtime::string::StringHeader. Kept inline -// (don't depend on perry-runtime — that would create a dep cycle through -// the UI crates that re-export us). -// ============================================================================= - -#[repr(C)] -struct StringHeader { - pub utf16_len: u32, - pub byte_len: u32, - pub capacity: u32, - pub refcount: u32, - pub flags: u32, -} - -fn str_from_header(ptr: *const u8) -> String { - if ptr.is_null() { - return String::new(); - } - unsafe { - let header = ptr as *const StringHeader; - let len = (*header).byte_len as usize; - let data = ptr.add(std::mem::size_of::()); - let slice = std::slice::from_raw_parts(data, len); - std::str::from_utf8(slice).unwrap_or("").to_owned() - } -} +use perry_ffi::copy_string_from_raw as str_from_header; // ============================================================================= // miniaudio FFI — only the slice we actually need. @@ -419,7 +393,7 @@ pub extern "C" fn perry_audio_load_sound(path_ptr: i64, bus: f64, stream: f64) - return 0; } let is_streaming = stream != 0.0; - let filename = str_from_header(path_ptr as *const u8); + let filename = unsafe { str_from_header(path_ptr as *const u8) }; if filename.is_empty() { return 0; } @@ -944,7 +918,7 @@ pub extern "C" fn perry_audio_create_bus(name_ptr: i64, parent: f64) -> i64 { if !ensure_engine() { return 0; } - let name = str_from_header(name_ptr as *const u8); + let name = unsafe { str_from_header(name_ptr as *const u8) }; let parent_ptr = match resolve_bus_group(parent) { Some(p) => p, None => { diff --git a/crates/perry-ffi/src/lib.rs b/crates/perry-ffi/src/lib.rs index 75f75417e0..06a33e8877 100644 --- a/crates/perry-ffi/src/lib.rs +++ b/crates/perry-ffi/src/lib.rs @@ -237,6 +237,81 @@ pub fn read_bytes(handle: JsString) -> Option<&'static [u8]> { } } +/// Copy a runtime string payload into GC-independent Rust storage. +/// +/// A null pointer is treated as an empty string. Invalid UTF-8 is replaced +/// with the Unicode replacement character, matching +/// [`String::from_utf8_lossy`]. Unlike [`read_string`], the returned value +/// remains valid if a later Perry runtime allocation moves the source object. +/// +/// # Safety +/// +/// `ptr` must be null or point to a valid Perry [`StringHeader`] followed by +/// at least `byte_len` initialized payload bytes. The source must not move or +/// be freed while this function is copying it. +pub unsafe fn copy_string_from_raw(ptr: *const T) -> String { + if ptr.is_null() { + return String::new(); + } + + let ptr = ptr.cast::(); + // SAFETY: upheld by the caller; the payload immediately follows the header. + let header = unsafe { &*(ptr.cast::()) }; + let data = unsafe { ptr.add(std::mem::size_of::()) }; + let bytes = unsafe { std::slice::from_raw_parts(data, header.byte_len as usize) }; + String::from_utf8_lossy(bytes).into_owned() +} + +#[cfg(test)] +mod copy_string_tests { + use super::*; + + fn runtime_string(bytes: &[u8]) -> Vec { + let header_len = std::mem::size_of::(); + let word_count = (header_len + bytes.len()).div_ceil(std::mem::size_of::()); + let mut storage = vec![0_u32; word_count]; + + let header = StringHeader { + utf16_len: String::from_utf8_lossy(bytes).encode_utf16().count() as u32, + byte_len: bytes.len() as u32, + capacity: bytes.len() as u32, + refcount: 1, + flags: 0, + }; + + // SAFETY: `Vec` supplies sufficient alignment and `word_count` + // reserves enough initialized storage for the header and payload. + unsafe { + storage.as_mut_ptr().cast::().write(header); + std::ptr::copy_nonoverlapping( + bytes.as_ptr(), + storage.as_mut_ptr().cast::().add(header_len), + bytes.len(), + ); + } + + storage + } + + #[test] + fn copies_non_ascii_payload_into_owned_storage() { + let mut storage = runtime_string("Grüße 👋".as_bytes()); + // SAFETY: `runtime_string` created a valid header and payload. + let copied = unsafe { copy_string_from_raw(storage.as_ptr()) }; + + storage.fill(0); + assert_eq!(copied, "Grüße 👋"); + } + + #[test] + fn replaces_invalid_utf8_and_accepts_null() { + let storage = runtime_string(&[b'f', 0x80]); + // SAFETY: `runtime_string` created a valid header and payload. + assert_eq!(unsafe { copy_string_from_raw(storage.as_ptr()) }, "f�"); + assert_eq!(unsafe { copy_string_from_raw::(std::ptr::null()) }, ""); + } +} + /// Allocate a runtime string from raw bytes — bypasses the UTF-8 /// validation [`alloc_string`] does implicitly. Use for compressed /// payloads, crypto digests, and other binary-as-string outputs diff --git a/crates/perry-ffi/src/types.rs b/crates/perry-ffi/src/types.rs index 387384519d..d86c602aaa 100644 --- a/crates/perry-ffi/src/types.rs +++ b/crates/perry-ffi/src/types.rs @@ -44,6 +44,8 @@ pub struct StringHeader { pub flags: u32, } +const _: () = assert!(std::mem::size_of::() == 20); + /// Header for a runtime-allocated JS array. #[repr(C)] pub struct ArrayHeader { diff --git a/crates/perry-ui-android/Cargo.toml b/crates/perry-ui-android/Cargo.toml index 64925df494..63e256bc5f 100644 --- a/crates/perry-ui-android/Cargo.toml +++ b/crates/perry-ui-android/Cargo.toml @@ -15,6 +15,7 @@ crate-type = ["rlib", "staticlib"] geisterhand = [] [target.'cfg(target_os = "android")'.dependencies] +perry-ffi.workspace = true perry-ui = { path = "../perry-ui" } perry-audio-miniaudio = { workspace = true } perry-runtime = { path = "../perry-runtime", default-features = false } diff --git a/crates/perry-ui-android/src/app.rs b/crates/perry-ui-android/src/app.rs index caa0618e53..d5c71413b7 100644 --- a/crates/perry-ui-android/src/app.rs +++ b/crates/perry-ui-android/src/app.rs @@ -21,24 +21,14 @@ struct AppConfig { } /// Extract a &str from a *const StringHeader pointer (Perry runtime string format). -pub fn str_from_header(ptr: *const u8) -> &'static str { - if ptr.is_null() { - return ""; - } - unsafe { - let header = ptr as *const perry_runtime::string::StringHeader; - let len = (*header).byte_len as usize; - let data = ptr.add(std::mem::size_of::()); - std::str::from_utf8_unchecked(std::slice::from_raw_parts(data, len)) - } -} +pub(crate) use perry_ffi::copy_string_from_raw as str_from_header; /// Create an app. Stores config for deferred creation. Returns app handle (i64). pub fn app_create(title_ptr: *const u8, width: f64, height: f64) -> i64 { let title = if title_ptr.is_null() { "Perry App".to_string() } else { - str_from_header(title_ptr).to_string() + unsafe { str_from_header(title_ptr) }.to_string() }; let w = if width > 0.0 { width } else { 400.0 }; diff --git a/crates/perry-ui-android/src/audio.rs b/crates/perry-ui-android/src/audio.rs index d77905e606..99ebc0741b 100644 --- a/crates/perry-ui-android/src/audio.rs +++ b/crates/perry-ui-android/src/audio.rs @@ -5,7 +5,6 @@ //! Results are stored in atomics, read lock-free by the main/UI thread. use jni::objects::{JObject, JValue}; -use std::cell::RefCell; use std::fs::File; use std::io::Write; use std::sync::atomic::{AtomicBool, AtomicU64, Ordering}; diff --git a/crates/perry-ui-android/src/background.rs b/crates/perry-ui-android/src/background.rs index 0dcdab5870..b8f1c60540 100644 --- a/crates/perry-ui-android/src/background.rs +++ b/crates/perry-ui-android/src/background.rs @@ -12,17 +12,7 @@ use crate::callback; use crate::jni_bridge; use jni::objects::JValue; -fn str_from_header(ptr: *const u8) -> String { - if ptr.is_null() { - return String::new(); - } - unsafe { - let header = ptr as *const perry_runtime::string::StringHeader; - let len = (*header).byte_len as usize; - let data = ptr.add(std::mem::size_of::()); - String::from_utf8_lossy(std::slice::from_raw_parts(data, len)).into_owned() - } -} +use perry_ffi::copy_string_from_raw as str_from_header; const TAG_TRUE: u64 = 0x7FFC_0000_0000_0004; const TAG_FALSE: u64 = 0x7FFC_0000_0000_0003; @@ -40,7 +30,7 @@ fn boolean_truthy(v: f64) -> bool { } pub fn register_task(identifier_ptr: *const u8, handler: f64) { - let id = str_from_header(identifier_ptr); + let id = unsafe { str_from_header(identifier_ptr) }; if id.is_empty() { return; } @@ -70,11 +60,11 @@ pub fn schedule( requires_network: f64, requires_charging: f64, ) { - let id = str_from_header(identifier_ptr); + let id = unsafe { str_from_header(identifier_ptr) }; if id.is_empty() { return; } - let kind = str_from_header(kind_ptr); + let kind = unsafe { str_from_header(kind_ptr) }; let kind = if kind.is_empty() { "appRefresh".to_string() } else { @@ -108,7 +98,7 @@ pub fn schedule( } pub fn cancel(identifier_ptr: *const u8) { - let id = str_from_header(identifier_ptr); + let id = unsafe { str_from_header(identifier_ptr) }; if id.is_empty() { return; } diff --git a/crates/perry-ui-android/src/clipboard.rs b/crates/perry-ui-android/src/clipboard.rs index d0d00ffa81..40c675a2b1 100644 --- a/crates/perry-ui-android/src/clipboard.rs +++ b/crates/perry-ui-android/src/clipboard.rs @@ -39,7 +39,7 @@ pub fn read() -> f64 { /// Write text to the system clipboard. pub fn write(text_ptr: *const u8) { - let text = crate::app::str_from_header(text_ptr); + let text = unsafe { crate::app::str_from_header(text_ptr) }; let mut env = jni_bridge::get_env(); let jstr = env.new_string(text).expect("Failed to create JNI string"); diff --git a/crates/perry-ui-android/src/dialog.rs b/crates/perry-ui-android/src/dialog.rs index 79190eb534..368d49071f 100644 --- a/crates/perry-ui-android/src/dialog.rs +++ b/crates/perry-ui-android/src/dialog.rs @@ -4,9 +4,7 @@ use crate::callback; use crate::jni_bridge; use jni::objects::JValue; -fn str_from_header(ptr: *const u8) -> &'static str { - crate::app::str_from_header(ptr) -} +use perry_ffi::copy_string_from_raw as str_from_header; extern "C" { fn js_closure_call1(closure: f64, arg: f64) -> f64; @@ -34,8 +32,8 @@ pub fn save_file_dialog( /// Show an alert dialog with title, message, buttons and callback. pub fn alert(title_ptr: *const u8, message_ptr: *const u8, _buttons_ptr: *const u8, callback: f64) { - let title = str_from_header(title_ptr); - let message = str_from_header(message_ptr); + let title = unsafe { str_from_header(title_ptr) }; + let message = unsafe { str_from_header(message_ptr) }; let mut env = jni_bridge::get_env(); let _ = env.push_local_frame(32); diff --git a/crates/perry-ui-android/src/drag_drop.rs b/crates/perry-ui-android/src/drag_drop.rs index efd1b89bd2..27500d57b9 100644 --- a/crates/perry-ui-android/src/drag_drop.rs +++ b/crates/perry-ui-android/src/drag_drop.rs @@ -289,7 +289,7 @@ pub extern "C" fn Java_com_perry_app_PerryBridge_nativeInvokeDropCallback( /// thread, so it pumps microtasks afterwards. #[no_mangle] pub extern "C" fn Java_com_perry_app_PerryBridge_nativeInvokeDragProvider<'local>( - mut env: jni::JNIEnv<'local>, + env: jni::JNIEnv<'local>, _class: jni::objects::JClass<'local>, key: jni::sys::jlong, ) -> jni::objects::JString<'local> { @@ -316,7 +316,7 @@ fn drag_provider_payload(key: i64) -> Option { if sh.is_null() { None } else { - Some(str_from_header(sh).to_string()) + Some(unsafe { str_from_header(sh) }.to_string()) } } } diff --git a/crates/perry-ui-android/src/fetch.rs b/crates/perry-ui-android/src/fetch.rs index a140b57370..dd8e84dd2a 100644 --- a/crates/perry-ui-android/src/fetch.rs +++ b/crates/perry-ui-android/src/fetch.rs @@ -42,9 +42,7 @@ fn with_response(handle: i64, f: impl FnOnce(&FetchResponse) -> T) -> Option< .map(f) } -fn str_from_header(ptr: *const u8) -> &'static str { - crate::app::str_from_header(ptr) -} +use perry_ffi::copy_string_from_raw as str_from_header; /// Perform synchronous HTTP request via Java HttpURLConnection. fn do_fetch( @@ -131,21 +129,21 @@ pub unsafe extern "C" fn js_fetch_with_options( body_ptr: i64, headers_ptr: i64, ) -> i64 { - let url = str_from_header(url_ptr as *const u8); + let url = unsafe { str_from_header(url_ptr as *const u8) }; let method = if method_ptr == 0 { "GET" } else { - str_from_header(method_ptr as *const u8) + unsafe { &str_from_header(method_ptr as *const u8) } }; let body = if body_ptr == 0 { "" } else { - str_from_header(body_ptr as *const u8) + unsafe { &str_from_header(body_ptr as *const u8) } }; let headers = if headers_ptr == 0 { "{}" } else { - str_from_header(headers_ptr as *const u8) + unsafe { &str_from_header(headers_ptr as *const u8) } }; let method_c = format!("{}\0", method); @@ -158,7 +156,7 @@ pub unsafe extern "C" fn js_fetch_with_options( url_c.as_ptr(), ); - match do_fetch(url, method, body, headers) { + match do_fetch(&url, method, body, headers) { Ok(resp) => { let id = store_response(resp); __android_log_print( diff --git a/crates/perry-ui-android/src/ffi/basic_widgets.rs b/crates/perry-ui-android/src/ffi/basic_widgets.rs index 4a63c21b6b..c64e8bcbc5 100644 --- a/crates/perry-ui-android/src/ffi/basic_widgets.rs +++ b/crates/perry-ui-android/src/ffi/basic_widgets.rs @@ -39,7 +39,7 @@ pub extern "C" fn perry_ui_text_create_with_id(text_ptr: i64, id_ptr: i64) -> i6 catch_panic("perry_ui_text_create_with_id", || { let handle = widgets::text::create(text_ptr as *const u8); if id_ptr != 0 { - let id = app::str_from_header(id_ptr as *const u8); + let id = unsafe { app::str_from_header(id_ptr as *const u8) }; widgets::text_registry::register_text_id_handler(handle, id.as_ptr(), id.len()); } handle @@ -53,11 +53,11 @@ pub extern "C" fn perry_ui_set_text(id_ptr: i64, value_ptr: i64) { return; } catch_panic_void("perry_ui_set_text", || { - let id = app::str_from_header(id_ptr as *const u8); + let id = unsafe { app::str_from_header(id_ptr as *const u8) }; let val = if value_ptr == 0 { "" } else { - app::str_from_header(value_ptr as *const u8) + unsafe { &app::str_from_header(value_ptr as *const u8) } }; widgets::text_registry::set_text_handler(id.as_ptr(), id.len(), val.as_ptr(), val.len()); }); diff --git a/crates/perry-ui-android/src/ffi/embed_misc.rs b/crates/perry-ui-android/src/ffi/embed_misc.rs index bb048f83e2..2b2f5f7492 100644 --- a/crates/perry-ui-android/src/ffi/embed_misc.rs +++ b/crates/perry-ui-android/src/ffi/embed_misc.rs @@ -190,11 +190,9 @@ pub extern "C" fn perry_system_get_os_version() -> i64 { } #[no_mangle] pub extern "C" fn perry_system_audio_set_output_filename(filename_ptr: i64) { - fn str_from_header(ptr: *const u8) -> &'static str { - crate::app::str_from_header(ptr) - } - let filename = str_from_header(filename_ptr as *const u8); - audio::set_output_filename(filename); + use perry_ffi::copy_string_from_raw as str_from_header; + let filename = unsafe { str_from_header(filename_ptr as *const u8) }; + audio::set_output_filename(&filename); } #[no_mangle] pub extern "C" fn perry_system_audio_start_recording() { diff --git a/crates/perry-ui-android/src/media_playback.rs b/crates/perry-ui-android/src/media_playback.rs index c6952bf6cf..f518211e62 100644 --- a/crates/perry-ui-android/src/media_playback.rs +++ b/crates/perry-ui-android/src/media_playback.rs @@ -94,24 +94,14 @@ thread_local! { // String helpers // --------------------------------------------------------------------------- -fn str_from_header<'a>(ptr: *const u8) -> &'a str { - if ptr.is_null() { - return ""; - } - unsafe { - let header = ptr as *const perry_runtime::string::StringHeader; - let len = (*header).byte_len as usize; - let data = ptr.add(std::mem::size_of::()); - std::str::from_utf8_unchecked(std::slice::from_raw_parts(data, len)) - } -} +use perry_ffi::copy_string_from_raw as str_from_header; // --------------------------------------------------------------------------- // Public FFI // --------------------------------------------------------------------------- pub fn create_player(url_ptr: *const u8) -> i64 { - let url = str_from_header(url_ptr); + let url = unsafe { str_from_header(url_ptr) }; if url.is_empty() { return 0; } @@ -429,10 +419,10 @@ pub fn set_now_playing( // explicit active player should manage that themselves. let _ = handle; - let title = str_from_header(title_ptr).to_string(); - let artist = str_from_header(artist_ptr).to_string(); - let album = str_from_header(album_ptr).to_string(); - let artwork = str_from_header(artwork_ptr).to_string(); + let title = unsafe { str_from_header(title_ptr) }.to_string(); + let artist = unsafe { str_from_header(artist_ptr) }.to_string(); + let album = unsafe { str_from_header(album_ptr) }.to_string(); + let artwork = unsafe { str_from_header(artwork_ptr) }.to_string(); let session = match ensure_session() { Some(s) => s, diff --git a/crates/perry-ui-android/src/menu.rs b/crates/perry-ui-android/src/menu.rs index a4530b19f5..65d042ace0 100644 --- a/crates/perry-ui-android/src/menu.rs +++ b/crates/perry-ui-android/src/menu.rs @@ -32,7 +32,7 @@ pub fn create() -> i64 { /// Add an item to a context menu. pub fn add_item(menu_handle: i64, title_ptr: *const u8, cb: f64) { - let title = str_from_header(title_ptr).to_string(); + let title = unsafe { str_from_header(title_ptr) }.to_string(); let cb_key = callback::register(cb); MENUS.with(|m| { diff --git a/crates/perry-ui-android/src/state.rs b/crates/perry-ui-android/src/state.rs index 36185741a3..54be6f02e2 100644 --- a/crates/perry-ui-android/src/state.rs +++ b/crates/perry-ui-android/src/state.rs @@ -68,17 +68,7 @@ thread_local! { static TEXTFIELD_BINDINGS: RefCell>> = RefCell::new(HashMap::new()); } -fn str_from_header(ptr: *const u8) -> &'static str { - if ptr.is_null() { - return ""; - } - unsafe { - let header = ptr as *const perry_runtime::string::StringHeader; - let len = (*header).byte_len as usize; - let data = ptr.add(std::mem::size_of::()); - std::str::from_utf8_unchecked(std::slice::from_raw_parts(data, len)) - } -} +use perry_ffi::copy_string_from_raw as str_from_header; /// Check if a f64 value is a NaN-boxed string. Accepts heap /// `STRING_TAG` (0x7FFF) and inline SSO `SHORT_STRING_TAG` (0x7FF9). @@ -94,7 +84,7 @@ fn is_nanboxed_string(value: f64) -> bool { /// `js_nanbox_get_pointer`). fn extract_nanboxed_string(value: f64) -> String { let ptr = unsafe { js_get_string_pointer_unified(value) }; - str_from_header(ptr).to_string() + unsafe { str_from_header(ptr) }.to_string() } fn format_value(value: f64) -> String { @@ -242,9 +232,9 @@ pub fn state_set(handle: i64, value: f64) { if tag == 0x7FFF { // String value let ptr = (bits & 0x0000_FFFF_FFFF_FFFF) as *const u8; - let s = str_from_header(ptr); + let s = unsafe { str_from_header(ptr) }; for binding in bindings { - widgets::textfield::set_string_str(binding.textfield_handle, s); + widgets::textfield::set_string_str(binding.textfield_handle, &s); } } else { let s = format_value(value); @@ -303,8 +293,8 @@ pub fn bind_text_numeric( prefix_ptr: *const u8, suffix_ptr: *const u8, ) { - let prefix = str_from_header(prefix_ptr).to_string(); - let suffix = str_from_header(suffix_ptr).to_string(); + let prefix = unsafe { str_from_header(prefix_ptr) }.to_string(); + let suffix = unsafe { str_from_header(suffix_ptr) }.to_string(); TEXT_BINDINGS.with(|b| { b.borrow_mut() .entry(state_handle) @@ -349,7 +339,7 @@ pub fn bind_text_template( let part_value = unsafe { *values_ptr.add(i) }; if part_type == 0 { - let s = str_from_header(part_value as *const u8).to_string(); + let s = unsafe { str_from_header(part_value as *const u8) }.to_string(); parts.push(TextPart::Literal(s)); } else { state_handles.push(part_value); diff --git a/crates/perry-ui-android/src/system.rs b/crates/perry-ui-android/src/system.rs index 1dcc8a7878..baa29cb6ee 100644 --- a/crates/perry-ui-android/src/system.rs +++ b/crates/perry-ui-android/src/system.rs @@ -3,9 +3,7 @@ use crate::jni_bridge; use jni::objects::JValue; -fn str_from_header(ptr: *const u8) -> &'static str { - crate::app::str_from_header(ptr) -} +use perry_ffi::copy_string_from_raw as str_from_header; extern "C" { fn js_string_from_bytes(ptr: *const u8, len: usize) -> *const u8; @@ -14,7 +12,7 @@ extern "C" { /// Open a URL in the default browser via Intent.ACTION_VIEW. pub fn open_url(url_ptr: *const u8) { - let url = str_from_header(url_ptr); + let url = unsafe { str_from_header(url_ptr) }; let mut env = jni_bridge::get_env(); let _ = env.push_local_frame(32); @@ -104,7 +102,7 @@ pub fn is_dark_mode() -> i64 { /// Set a preference value using SharedPreferences. pub fn preferences_set(key_ptr: *const u8, value: f64) { - let key = str_from_header(key_ptr); + let key = unsafe { str_from_header(key_ptr) }; let mut env = jni_bridge::get_env(); let _ = env.push_local_frame(16); @@ -140,7 +138,7 @@ pub fn preferences_set(key_ptr: *const u8, value: f64) { if tag == 0x7FFF { // String value — extract and store as string let ptr = (bits & 0x0000_FFFF_FFFF_FFFF) as *const u8; - let s = str_from_header(ptr); + let s = unsafe { str_from_header(ptr) }; let jval = env.new_string(s).expect("value string"); let _ = env.call_method( &editor, @@ -174,7 +172,7 @@ pub fn preferences_set(key_ptr: *const u8, value: f64) { /// half the UI vanishes). Always read via `getAll` and branch on type, and /// clear any leftover exception before returning. pub fn preferences_get(key_ptr: *const u8) -> f64 { - let key = str_from_header(key_ptr); + let key = unsafe { str_from_header(key_ptr) }; let mut env = jni_bridge::get_env(); let _ = env.push_local_frame(24); @@ -297,8 +295,8 @@ pub fn preferences_get(key_ptr: *const u8) -> f64 { /// Save a value to the keychain (SharedPreferences with private mode). pub fn keychain_save(key_ptr: *const u8, value_ptr: *const u8) { - let key = str_from_header(key_ptr); - let value = str_from_header(value_ptr); + let key = unsafe { str_from_header(key_ptr) }; + let value = unsafe { str_from_header(value_ptr) }; let mut env = jni_bridge::get_env(); let _ = env.push_local_frame(16); @@ -343,7 +341,7 @@ pub fn keychain_save(key_ptr: *const u8, value_ptr: *const u8) { /// Get a value from the keychain. pub fn keychain_get(key_ptr: *const u8) -> f64 { - let key = str_from_header(key_ptr); + let key = unsafe { str_from_header(key_ptr) }; let mut env = jni_bridge::get_env(); let _ = env.push_local_frame(16); @@ -392,7 +390,7 @@ pub fn keychain_get(key_ptr: *const u8) -> f64 { /// Delete a value from the keychain. pub fn keychain_delete(key_ptr: *const u8) { - let key = str_from_header(key_ptr); + let key = unsafe { str_from_header(key_ptr) }; let mut env = jni_bridge::get_env(); let _ = env.push_local_frame(16); @@ -439,12 +437,12 @@ pub fn keychain_delete(key_ptr: *const u8) { /// (declared in the app template's AndroidManifest.xml — a normal /// permission, no runtime prompt). pub fn haptic_play(type_ptr: *const u8) { - let name = str_from_header(type_ptr); + let name = unsafe { str_from_header(type_ptr) }; // VibrationEffect.createPredefined effect ids (public constants, // API 29+): EFFECT_CLICK=0, EFFECT_DOUBLE_CLICK=1, EFFECT_TICK=2, // EFFECT_HEAVY_CLICK=5. - let effect_id: i32 = match name { + let effect_id: i32 = match name.as_str() { "success" | "medium" | "start" | "stop" => 0, // EFFECT_CLICK "error" | "warning" => 1, // EFFECT_DOUBLE_CLICK (double buzz) "heavy" => 5, // EFFECT_HEAVY_CLICK @@ -453,7 +451,7 @@ pub fn haptic_play(type_ptr: *const u8) { _ => 2, // EFFECT_TICK }; // Duration (ms) for the pre-API-29 `vibrate(long)` fallback. - let fallback_ms: i64 = match name { + let fallback_ms: i64 = match name.as_str() { "error" | "warning" => 80, "heavy" => 60, "success" | "medium" | "start" | "stop" => 40, @@ -764,9 +762,9 @@ pub fn notification_schedule_interval( fn js_is_truthy(value: f64) -> i32; } let repeats_bool = unsafe { js_is_truthy(repeats) != 0 }; - let id = str_from_header(id_ptr); - let title = str_from_header(title_ptr); - let body = str_from_header(body_ptr); + let id = unsafe { str_from_header(id_ptr) }; + let title = unsafe { str_from_header(title_ptr) }; + let body = unsafe { str_from_header(body_ptr) }; let mut env = jni_bridge::get_env(); let _ = env.push_local_frame(16); @@ -802,9 +800,9 @@ pub fn notification_schedule_calendar( body_ptr: *const u8, timestamp_ms: f64, ) { - let id = str_from_header(id_ptr); - let title = str_from_header(title_ptr); - let body = str_from_header(body_ptr); + let id = unsafe { str_from_header(id_ptr) }; + let title = unsafe { str_from_header(title_ptr) }; + let body = unsafe { str_from_header(body_ptr) }; let mut env = jni_bridge::get_env(); let _ = env.push_local_frame(16); @@ -859,7 +857,7 @@ pub fn notification_schedule_location( /// Cancel a scheduled or already-displayed notification by id (#96). pub fn notification_cancel(id_ptr: *const u8) { - let id = str_from_header(id_ptr); + let id = unsafe { str_from_header(id_ptr) }; let mut env = jni_bridge::get_env(); let _ = env.push_local_frame(8); @@ -881,8 +879,8 @@ pub fn notification_cancel(id_ptr: *const u8) { /// Send a notification via PerryBridge. pub fn notification_send(title_ptr: *const u8, body_ptr: *const u8) { - let title = str_from_header(title_ptr); - let body = str_from_header(body_ptr); + let title = unsafe { str_from_header(title_ptr) }; + let body = unsafe { str_from_header(body_ptr) }; let mut env = jni_bridge::get_env(); let _ = env.push_local_frame(16); diff --git a/crates/perry-ui-android/src/toolbar.rs b/crates/perry-ui-android/src/toolbar.rs index 6168562ff6..692da40a4b 100644 --- a/crates/perry-ui-android/src/toolbar.rs +++ b/crates/perry-ui-android/src/toolbar.rs @@ -6,9 +6,7 @@ use jni::objects::JValue; use std::cell::RefCell; use std::collections::HashMap; -fn str_from_header(ptr: *const u8) -> &'static str { - crate::app::str_from_header(ptr) -} +use perry_ffi::copy_string_from_raw as str_from_header; struct ToolbarState { widget_handle: i64, @@ -82,7 +80,7 @@ pub fn create() -> i64 { } pub fn add_item(toolbar_handle: i64, label_ptr: *const u8, _icon_ptr: *const u8, on_press: f64) { - let label = str_from_header(label_ptr); + let label = unsafe { str_from_header(label_ptr) }; let widget_handle = TOOLBARS.with(|t| t.borrow().get(&toolbar_handle).map(|s| s.widget_handle)); diff --git a/crates/perry-ui-android/src/widgets/adbanner.rs b/crates/perry-ui-android/src/widgets/adbanner.rs index 6dddeae604..8965473491 100644 --- a/crates/perry-ui-android/src/widgets/adbanner.rs +++ b/crates/perry-ui-android/src/widgets/adbanner.rs @@ -11,9 +11,7 @@ use crate::jni_bridge; use jni::objects::JValue; -fn str_from_header(ptr: *const u8) -> &'static str { - crate::app::str_from_header(ptr) -} +use perry_ffi::copy_string_from_raw as str_from_header; /// Banner dimensions in dp per size key (matches Google Mobile Ads' /// standard `AdSize` constants). @@ -29,9 +27,9 @@ fn banner_size_dp(size_key: &str) -> (f32, f32) { /// Create the banner placeholder view sized per `size_ptr`. pub fn create(unit_id_ptr: *const u8, size_ptr: *const u8) -> i64 { - let _unit_id = str_from_header(unit_id_ptr); - let size_key = str_from_header(size_ptr); - let (w_dp, h_dp) = banner_size_dp(size_key); + let _unit_id = unsafe { str_from_header(unit_id_ptr) }; + let size_key = unsafe { str_from_header(size_ptr) }; + let (w_dp, h_dp) = banner_size_dp(&size_key); let mut env = jni_bridge::get_env(); let _ = env.push_local_frame(32); diff --git a/crates/perry-ui-android/src/widgets/attributed_text.rs b/crates/perry-ui-android/src/widgets/attributed_text.rs index 59ff18a951..9001927e57 100644 --- a/crates/perry-ui-android/src/widgets/attributed_text.rs +++ b/crates/perry-ui-android/src/widgets/attributed_text.rs @@ -23,17 +23,7 @@ thread_local! { static BUFFERS: RefCell> = RefCell::new(HashMap::new()); } -fn str_from_header(ptr: *const u8) -> &'static str { - if ptr.is_null() { - return ""; - } - unsafe { - let header = ptr as *const perry_runtime::string::StringHeader; - let len = (*header).byte_len as usize; - let data = ptr.add(std::mem::size_of::()); - std::str::from_utf8_unchecked(std::slice::from_raw_parts(data, len)) - } -} +use perry_ffi::copy_string_from_raw as str_from_header; /// Create an empty `TextView` ready to receive `append` runs. pub fn create() -> i64 { @@ -85,7 +75,7 @@ pub fn append( b: f64, a: f64, ) { - let text = str_from_header(text_ptr); + let text = unsafe { str_from_header(text_ptr) }; if text.is_empty() { return; } @@ -109,7 +99,7 @@ pub fn append( // Append the raw text to the SSB; the returned object is the SSB // itself but we don't need the return value. - let java_text = match env.new_string(text) { + let java_text = match env.new_string(&text) { Ok(s) => s, Err(_) => { unsafe { @@ -261,7 +251,7 @@ fn rgba_to_argb(r: f64, g: f64, b: f64, a: f64) -> i32 { /// JNI `GlobalRef` is not `Clone`, but `as_obj` returns the underlying /// `JObject<'static>` we can re-wrap as a new global via JNIEnv. fn env_clone_global(g: &GlobalRef) -> GlobalRef { - let mut env = jni_bridge::get_env(); + let env = jni_bridge::get_env(); env.new_global_ref(g.as_obj()) .expect("clone SSB global ref") } diff --git a/crates/perry-ui-android/src/widgets/bottom_nav.rs b/crates/perry-ui-android/src/widgets/bottom_nav.rs index dae42855c5..1e74ca614e 100644 --- a/crates/perry-ui-android/src/widgets/bottom_nav.rs +++ b/crates/perry-ui-android/src/widgets/bottom_nav.rs @@ -148,8 +148,8 @@ pub fn create(on_select: f64) -> i64 { /// Add a tab item (icon drawable name + label). pub fn add_item(handle: i64, icon_ptr: *const u8, label_ptr: *const u8) { - let icon = crate::app::str_from_header(icon_ptr); - let label = crate::app::str_from_header(label_ptr); + let icon = unsafe { crate::app::str_from_header(icon_ptr) }; + let label = unsafe { crate::app::str_from_header(label_ptr) }; let mut env = jni_bridge::get_env(); let _ = env.push_local_frame(32); let activity = super::get_activity(&mut env); @@ -370,7 +370,7 @@ pub fn add_item(handle: i64, icon_ptr: *const u8, label_ptr: *const u8) { /// Set or clear the badge string on a tab. Empty clears the badge. pub fn set_badge(handle: i64, index: i64, badge_ptr: *const u8) { - let badge = crate::app::str_from_header(badge_ptr); + let badge = unsafe { crate::app::str_from_header(badge_ptr) }; let mut env = jni_bridge::get_env(); let _ = env.push_local_frame(16); diff --git a/crates/perry-ui-android/src/widgets/button.rs b/crates/perry-ui-android/src/widgets/button.rs index f59367b6ee..14a91212e1 100644 --- a/crates/perry-ui-android/src/widgets/button.rs +++ b/crates/perry-ui-android/src/widgets/button.rs @@ -9,7 +9,7 @@ extern "C" { /// Create a Button with a label and closure callback. Returns widget handle. pub fn create(label_ptr: *const u8, on_press: f64) -> i64 { - let label = str_from_header(label_ptr); + let label = unsafe { str_from_header(label_ptr) }; unsafe { __android_log_print( 3, @@ -183,7 +183,7 @@ pub fn set_text_color(handle: i64, r: f64, g: f64, b: f64, a: f64) { /// Set the title text of a button. pub fn set_title(handle: i64, title_ptr: *const u8) { - let title = str_from_header(title_ptr); + let title = unsafe { str_from_header(title_ptr) }; if let Some(view_ref) = super::get_widget(handle) { let mut env = jni_bridge::get_env(); let _ = env.push_local_frame(8); @@ -270,12 +270,12 @@ pub fn sf_symbol_to_emoji(name: &str) -> Option<&'static str> { /// Set an icon on a button (equivalent of SF Symbols on iOS). /// On Android, uses Unicode emoji which are universally supported. pub fn set_image(handle: i64, name_ptr: *const u8) { - let name = str_from_header(name_ptr); + let name = unsafe { str_from_header(name_ptr) }; if let Some(view_ref) = super::get_widget(handle) { let mut env = jni_bridge::get_env(); let _ = env.push_local_frame(16); - let icon_str = if let Some(emoji) = sf_symbol_to_emoji(name) { + let icon_str = if let Some(emoji) = sf_symbol_to_emoji(&name) { emoji.to_string() } else { // Fallback: use the symbol name itself (truncated) diff --git a/crates/perry-ui-android/src/widgets/canvas.rs b/crates/perry-ui-android/src/widgets/canvas.rs index 4b4e03d854..2f59b3520b 100644 --- a/crates/perry-ui-android/src/widgets/canvas.rs +++ b/crates/perry-ui-android/src/widgets/canvas.rs @@ -682,7 +682,7 @@ fn argb(a: f64, r: f64, g: f64, b: f64) -> i32 { } pub fn load_image(path_ptr: *const u8) -> i64 { - let path = crate::app::str_from_header(path_ptr).to_string(); + let path = unsafe { crate::app::str_from_header(path_ptr) }.to_string(); if let Some(handle) = IMAGE_CACHE.lock().unwrap().get(&path).copied() { if let Some((width, height)) = CANVAS_IMAGE_SIZES.lock().unwrap().get(&handle).copied() { return resolved_image_promise(handle, width, height); diff --git a/crates/perry-ui-android/src/widgets/chart.rs b/crates/perry-ui-android/src/widgets/chart.rs index ffc9245321..8ffd9eac2f 100644 --- a/crates/perry-ui-android/src/widgets/chart.rs +++ b/crates/perry-ui-android/src/widgets/chart.rs @@ -53,7 +53,7 @@ pub fn create(kind: i64, width: f64, height: f64) -> i64 { } pub fn add_data_point(handle: i64, label_ptr: *const u8, value: f64) { - let label = str_from_header(label_ptr); + let label = unsafe { str_from_header(label_ptr) }; if let Some(view) = super::get_widget(handle) { let mut env = jni_bridge::get_env(); let _ = env.push_local_frame(8); @@ -97,7 +97,7 @@ pub fn clear_data(handle: i64) { } pub fn set_title(handle: i64, title_ptr: *const u8) { - let title = str_from_header(title_ptr); + let title = unsafe { str_from_header(title_ptr) }; if let Some(view) = super::get_widget(handle) { let mut env = jni_bridge::get_env(); let _ = env.push_local_frame(8); diff --git a/crates/perry-ui-android/src/widgets/combobox.rs b/crates/perry-ui-android/src/widgets/combobox.rs index 0fcb708d16..ad74eeb2f7 100644 --- a/crates/perry-ui-android/src/widgets/combobox.rs +++ b/crates/perry-ui-android/src/widgets/combobox.rs @@ -22,7 +22,7 @@ const TAG_UNDEFINED: u64 = 0x7FFC_0000_0000_0001; /// Create an AutoCompleteTextView with an initial value and on_change callback. pub fn create(initial_ptr: *const u8, on_change: f64) -> i64 { - let initial = str_from_header(initial_ptr); + let initial = unsafe { str_from_header(initial_ptr) }; let mut env = jni_bridge::get_env(); let _ = env.push_local_frame(16); @@ -67,7 +67,7 @@ pub fn create(initial_ptr: *const u8, on_change: f64) -> i64 { /// Append one suggestion item to the combobox. pub fn add_item(handle: i64, value_ptr: *const u8) { - let value = str_from_header(value_ptr); + let value = unsafe { str_from_header(value_ptr) }; if let Some(view) = super::get_widget(handle) { let mut env = jni_bridge::get_env(); let _ = env.push_local_frame(8); @@ -89,7 +89,7 @@ pub fn add_item(handle: i64, value_ptr: *const u8) { /// Programmatically set the currently displayed value. pub fn set_value(handle: i64, value_ptr: *const u8) { - let value = str_from_header(value_ptr); + let value = unsafe { str_from_header(value_ptr) }; if let Some(view) = super::get_widget(handle) { let mut env = jni_bridge::get_env(); let _ = env.push_local_frame(8); diff --git a/crates/perry-ui-android/src/widgets/form.rs b/crates/perry-ui-android/src/widgets/form.rs index c5129107e9..eef62a2141 100644 --- a/crates/perry-ui-android/src/widgets/form.rs +++ b/crates/perry-ui-android/src/widgets/form.rs @@ -3,9 +3,7 @@ use crate::jni_bridge; use jni::objects::JValue; -fn str_from_header(ptr: *const u8) -> &'static str { - crate::app::str_from_header(ptr) -} +use perry_ffi::copy_string_from_raw as str_from_header; /// Create a Form — vertical LinearLayout with padding. pub fn create() -> i64 { @@ -50,7 +48,7 @@ pub fn create() -> i64 { /// Create a Section — vertical LinearLayout with a title label. pub fn section_create(title_ptr: *const u8) -> i64 { - let title = str_from_header(title_ptr); + let title = unsafe { str_from_header(title_ptr) }; let mut env = jni_bridge::get_env(); let _ = env.push_local_frame(32); diff --git a/crates/perry-ui-android/src/widgets/image.rs b/crates/perry-ui-android/src/widgets/image.rs index 8cc3e532d1..f487c017ca 100644 --- a/crates/perry-ui-android/src/widgets/image.rs +++ b/crates/perry-ui-android/src/widgets/image.rs @@ -3,15 +3,13 @@ use crate::jni_bridge; use jni::objects::JValue; -fn str_from_header(ptr: *const u8) -> &'static str { - crate::app::str_from_header(ptr) -} +use perry_ffi::copy_string_from_raw as str_from_header; /// Create an image from a file path. /// For relative paths, tries the Android assets directory first (bundled in APK), /// then falls back to BitmapFactory.decodeFile for absolute paths. pub fn create_file(path_ptr: *const u8) -> i64 { - let path = str_from_header(path_ptr); + let path = unsafe { str_from_header(path_ptr) }; crate::log_debug(&format!("image create_file: path={}", path)); let mut env = jni_bridge::get_env(); let _ = env.push_local_frame(32); @@ -64,7 +62,7 @@ pub fn create_file(path_ptr: *const u8) -> i64 { ) { if let Ok(mgr) = asset_mgr.l() { if !mgr.is_null() { - let jpath = env.new_string(path).expect("asset path string"); + let jpath = env.new_string(&path).expect("asset path string"); // AssetManager.open(path) -> InputStream let stream = env.call_method( &mgr, @@ -147,7 +145,7 @@ pub fn create_file(path_ptr: *const u8) -> i64 { /// Create an image from a named system icon (SF Symbol name → Material Icon). /// Uses the same SF Symbol → Material Icons mapping as button.rs. pub fn create_symbol(name_ptr: *const u8) -> i64 { - let name = str_from_header(name_ptr); + let name = unsafe { str_from_header(name_ptr) }; let mut env = jni_bridge::get_env(); let _ = env.push_local_frame(32); @@ -164,7 +162,7 @@ pub fn create_symbol(name_ptr: *const u8) -> i64 { .expect("Failed to create TextView for symbol"); // Map SF Symbol name to emoji/Unicode character - let icon_str = if let Some(emoji) = super::button::sf_symbol_to_emoji(name) { + let icon_str = if let Some(emoji) = super::button::sf_symbol_to_emoji(&name) { emoji.to_string() } else { name.chars().take(3).collect() diff --git a/crates/perry-ui-android/src/widgets/image_gallery.rs b/crates/perry-ui-android/src/widgets/image_gallery.rs index 39d4eec236..a4c3687935 100644 --- a/crates/perry-ui-android/src/widgets/image_gallery.rs +++ b/crates/perry-ui-android/src/widgets/image_gallery.rs @@ -104,8 +104,8 @@ pub fn create(on_index_change: f64) -> i64 { } pub fn add_image(handle: i64, url_ptr: *const u8, alt_ptr: *const u8) { - let url = crate::app::str_from_header(url_ptr); - let alt = crate::app::str_from_header(alt_ptr); + let url = unsafe { crate::app::str_from_header(url_ptr) }; + let alt = unsafe { crate::app::str_from_header(alt_ptr) }; let mut env = jni_bridge::get_env(); let _ = env.push_local_frame(32); let activity = super::get_activity(&mut env); diff --git a/crates/perry-ui-android/src/widgets/lazyvstack.rs b/crates/perry-ui-android/src/widgets/lazyvstack.rs index f3b854b747..fcd042682f 100644 --- a/crates/perry-ui-android/src/widgets/lazyvstack.rs +++ b/crates/perry-ui-android/src/widgets/lazyvstack.rs @@ -1,7 +1,5 @@ //! LazyVStack — ScrollView + LinearLayout, render-all approach -use crate::jni_bridge; -use jni::objects::JValue; use std::cell::RefCell; use std::collections::HashMap; diff --git a/crates/perry-ui-android/src/widgets/map_view.rs b/crates/perry-ui-android/src/widgets/map_view.rs index f629f6bcd7..f1601ac9d2 100644 --- a/crates/perry-ui-android/src/widgets/map_view.rs +++ b/crates/perry-ui-android/src/widgets/map_view.rs @@ -100,7 +100,7 @@ pub fn set_region(handle: i64, lat: f64, lon: f64, lat_span: f64, lon_span: f64) pub fn add_pin(handle: i64, lat: f64, lon: f64, title_ptr: *const u8) { if let Some(view) = super::get_widget(handle) { - let title = str_from_header(title_ptr); + let title = unsafe { str_from_header(title_ptr) }; let mut env = jni_bridge::get_env(); let _ = env.push_local_frame(4); let jstr = match env.new_string(title) { diff --git a/crates/perry-ui-android/src/widgets/mod.rs b/crates/perry-ui-android/src/widgets/mod.rs index 4d14b41a1d..c966b0d318 100644 --- a/crates/perry-ui-android/src/widgets/mod.rs +++ b/crates/perry-ui-android/src/widgets/mod.rs @@ -352,7 +352,7 @@ pub fn set_enabled(handle: i64, enabled: bool) { /// Set tooltip (API 26+). pub fn set_tooltip(handle: i64, text_ptr: *const u8) { - let text = crate::app::str_from_header(text_ptr); + let text = unsafe { crate::app::str_from_header(text_ptr) }; if let Some(view_ref) = get_widget(handle) { let mut env = jni_bridge::get_env(); let _ = env.push_local_frame(8); diff --git a/crates/perry-ui-android/src/widgets/navstack.rs b/crates/perry-ui-android/src/widgets/navstack.rs index 2f949fde88..ce61b185ab 100644 --- a/crates/perry-ui-android/src/widgets/navstack.rs +++ b/crates/perry-ui-android/src/widgets/navstack.rs @@ -5,9 +5,7 @@ use jni::objects::JValue; use std::cell::RefCell; use std::collections::HashMap; -fn str_from_header(ptr: *const u8) -> &'static str { - crate::app::str_from_header(ptr) -} +use perry_ffi::copy_string_from_raw as str_from_header; struct NavState { pages: Vec, // widget handles for each page @@ -18,7 +16,7 @@ thread_local! { } pub fn create(title_ptr: *const u8, body_handle: i64) -> i64 { - let _title = str_from_header(title_ptr); + let _title = unsafe { str_from_header(title_ptr) }; let mut env = jni_bridge::get_env(); let _ = env.push_local_frame(32); @@ -55,7 +53,7 @@ pub fn create(title_ptr: *const u8, body_handle: i64) -> i64 { } pub fn push(handle: i64, title_ptr: *const u8, body_handle: i64) { - let _title = str_from_header(title_ptr); + let _title = unsafe { str_from_header(title_ptr) }; // Hide current top page NAV_STATES.with(|s| { diff --git a/crates/perry-ui-android/src/widgets/picker.rs b/crates/perry-ui-android/src/widgets/picker.rs index c9e0e4c244..67fb4d41fd 100644 --- a/crates/perry-ui-android/src/widgets/picker.rs +++ b/crates/perry-ui-android/src/widgets/picker.rs @@ -6,9 +6,7 @@ use jni::objects::JValue; use std::cell::RefCell; use std::collections::HashMap; -fn str_from_header(ptr: *const u8) -> &'static str { - crate::app::str_from_header(ptr) -} +use perry_ffi::copy_string_from_raw as str_from_header; struct PickerState { items: Vec, @@ -20,7 +18,7 @@ thread_local! { } pub fn create(label_ptr: *const u8, on_change: f64, _style: i64) -> i64 { - let _label = str_from_header(label_ptr); + let _label = unsafe { str_from_header(label_ptr) }; let mut env = jni_bridge::get_env(); let _ = env.push_local_frame(32); @@ -69,7 +67,7 @@ pub fn create(label_ptr: *const u8, on_change: f64, _style: i64) -> i64 { } pub fn add_item(handle: i64, title_ptr: *const u8) { - let title = str_from_header(title_ptr).to_string(); + let title = unsafe { str_from_header(title_ptr) }.to_string(); PICKER_STATES.with(|s| { let mut states = s.borrow_mut(); if let Some(state) = states.get_mut(&handle) { diff --git a/crates/perry-ui-android/src/widgets/qrcode.rs b/crates/perry-ui-android/src/widgets/qrcode.rs index 7de008f479..46b78cea68 100644 --- a/crates/perry-ui-android/src/widgets/qrcode.rs +++ b/crates/perry-ui-android/src/widgets/qrcode.rs @@ -5,15 +5,13 @@ use crate::jni_bridge; use jni::objects::JValue; -fn str_from_header(ptr: *const u8) -> &'static str { - crate::app::str_from_header(ptr) -} +use perry_ffi::copy_string_from_raw as str_from_header; /// Create a QR code widget displaying the given data string. /// `size` is the display width/height in dp (QR codes are square). /// Returns widget handle. pub fn create(data_ptr: *const u8, size: f64) -> i64 { - let data_str = str_from_header(data_ptr); + let data_str = unsafe { str_from_header(data_ptr) }; let display_size = if size > 0.0 { size } else { 200.0 }; let mut env = jni_bridge::get_env(); @@ -31,7 +29,7 @@ pub fn create(data_ptr: *const u8, size: f64) -> i64 { .expect("Failed to create TextView for QR code"); // Set the data text - let display_text = if data_str.is_empty() { "QR" } else { data_str }; + let display_text = if data_str.is_empty() { "QR" } else { &data_str }; let jstr = env.new_string(display_text).expect("QR text string"); let _ = env.call_method( &text_view, @@ -150,7 +148,7 @@ pub fn create(data_ptr: *const u8, size: f64) -> i64 { /// Update the QR code content of an existing widget. pub fn set_data(handle: i64, data_ptr: *const u8) { - let data_str = str_from_header(data_ptr); + let data_str = unsafe { str_from_header(data_ptr) }; if let Some(view_ref) = super::get_widget(handle) { let mut env = jni_bridge::get_env(); let _ = env.push_local_frame(8); diff --git a/crates/perry-ui-android/src/widgets/rich_text.rs b/crates/perry-ui-android/src/widgets/rich_text.rs index 619faa86d1..745ba43eab 100644 --- a/crates/perry-ui-android/src/widgets/rich_text.rs +++ b/crates/perry-ui-android/src/widgets/rich_text.rs @@ -86,7 +86,7 @@ pub fn create(width: f64, height: f64, on_change: f64) -> i64 { /// Replace the entire content with a plain string. pub fn set_string(handle: i64, text_ptr: *const u8) { - let text = str_from_header(text_ptr); + let text = unsafe { str_from_header(text_ptr) }; if let Some(view) = super::get_widget(handle) { let mut env = jni_bridge::get_env(); let _ = env.push_local_frame(8); @@ -150,7 +150,7 @@ pub fn get_string(handle: i64) -> f64 { /// Parse HTML via `Html.fromHtml(s, FROM_HTML_MODE_COMPACT)` and set it as /// the EditText content. Returns 1 on success, 0 on invalid handle. pub fn set_html(handle: i64, html_ptr: *const u8) -> i64 { - let html = str_from_header(html_ptr); + let html = unsafe { str_from_header(html_ptr) }; let Some(view) = super::get_widget(handle) else { return 0; }; diff --git a/crates/perry-ui-android/src/widgets/securefield.rs b/crates/perry-ui-android/src/widgets/securefield.rs index ca4d8033ea..e594ab42ae 100644 --- a/crates/perry-ui-android/src/widgets/securefield.rs +++ b/crates/perry-ui-android/src/widgets/securefield.rs @@ -4,12 +4,10 @@ use crate::callback; use crate::jni_bridge; use jni::objects::JValue; -fn str_from_header(ptr: *const u8) -> &'static str { - crate::app::str_from_header(ptr) -} +use perry_ffi::copy_string_from_raw as str_from_header; pub fn create(placeholder_ptr: *const u8, on_change: f64) -> i64 { - let placeholder = str_from_header(placeholder_ptr); + let placeholder = unsafe { str_from_header(placeholder_ptr) }; let mut env = jni_bridge::get_env(); let _ = env.push_local_frame(32); diff --git a/crates/perry-ui-android/src/widgets/tabbar.rs b/crates/perry-ui-android/src/widgets/tabbar.rs index 75f32d7b55..4b15788d80 100644 --- a/crates/perry-ui-android/src/widgets/tabbar.rs +++ b/crates/perry-ui-android/src/widgets/tabbar.rs @@ -131,7 +131,7 @@ pub fn create(on_select: f64) -> i64 { /// Add a tab to the tab bar. pub fn add_tab(tabbar_handle: i64, label_ptr: *const u8) { - let label = crate::app::str_from_header(label_ptr); + let label = unsafe { crate::app::str_from_header(label_ptr) }; let mut env = jni_bridge::get_env(); let _ = env.push_local_frame(32); let activity = super::get_activity(&mut env); diff --git a/crates/perry-ui-android/src/widgets/text.rs b/crates/perry-ui-android/src/widgets/text.rs index 39ffd54e91..7400a194c4 100644 --- a/crates/perry-ui-android/src/widgets/text.rs +++ b/crates/perry-ui-android/src/widgets/text.rs @@ -4,7 +4,7 @@ use jni::objects::{JObject, JValue}; /// Create a TextView. Returns widget handle. pub fn create(text_ptr: *const u8) -> i64 { - let text = str_from_header(text_ptr); + let text = unsafe { str_from_header(text_ptr) }; let mut env = jni_bridge::get_env(); let _ = env.push_local_frame(32); @@ -55,8 +55,8 @@ pub fn set_text_str(handle: i64, text: &str) { /// Update the text of an existing TextView from a StringHeader pointer. pub fn set_string(handle: i64, text_ptr: *const u8) { - let text = str_from_header(text_ptr); - set_text_str(handle, text); + let text = unsafe { str_from_header(text_ptr) }; + set_text_str(handle, &text); } /// Set the text color of a TextView (RGBA 0.0-1.0). @@ -135,16 +135,16 @@ pub fn set_font_weight(handle: i64, _size: f64, weight: f64) { /// Set the font family of a TextView. pub fn set_font_family(handle: i64, family_ptr: *const u8) { - let family = str_from_header(family_ptr); + let family = unsafe { str_from_header(family_ptr) }; if let Some(view_ref) = super::get_widget(handle) { let mut env = jni_bridge::get_env(); let _ = env.push_local_frame(16); - let family_name = match family { + let family_name = match family.as_str() { "monospace" | "monospaced" => "monospace", "system" | "default" => "sans-serif", "serif" => "serif", - other => other, + other => &other, }; let jfamily = env.new_string(family_name).expect("family string"); diff --git a/crates/perry-ui-android/src/widgets/textarea.rs b/crates/perry-ui-android/src/widgets/textarea.rs index 156d313a64..0b24244f4e 100644 --- a/crates/perry-ui-android/src/widgets/textarea.rs +++ b/crates/perry-ui-android/src/widgets/textarea.rs @@ -5,7 +5,7 @@ use jni::objects::JValue; /// Create a multi-line EditText (TextArea). Returns widget handle. pub fn create(placeholder_ptr: *const u8, on_change: f64) -> i64 { - let placeholder = str_from_header(placeholder_ptr); + let placeholder = unsafe { str_from_header(placeholder_ptr) }; let mut env = jni_bridge::get_env(); let _ = env.push_local_frame(32); diff --git a/crates/perry-ui-android/src/widgets/textfield.rs b/crates/perry-ui-android/src/widgets/textfield.rs index 70b705b900..6e8021a988 100644 --- a/crates/perry-ui-android/src/widgets/textfield.rs +++ b/crates/perry-ui-android/src/widgets/textfield.rs @@ -5,7 +5,7 @@ use jni::objects::JValue; /// Create an EditText with placeholder and onChange callback. Returns widget handle. pub fn create(placeholder_ptr: *const u8, on_change: f64) -> i64 { - let placeholder = str_from_header(placeholder_ptr); + let placeholder = unsafe { str_from_header(placeholder_ptr) }; let mut env = jni_bridge::get_env(); let _ = env.push_local_frame(32); @@ -83,8 +83,8 @@ pub fn focus(handle: i64) { /// Set the text of an EditText from a StringHeader pointer. pub fn set_string_value(handle: i64, text_ptr: *const u8) { - let text = str_from_header(text_ptr); - set_string_str(handle, text); + let text = unsafe { str_from_header(text_ptr) }; + set_string_str(handle, &text); } pub fn set_string_str(handle: i64, text: &str) { diff --git a/crates/perry-ui-android/src/widgets/toggle.rs b/crates/perry-ui-android/src/widgets/toggle.rs index 0846bc14a2..c809da5bc0 100644 --- a/crates/perry-ui-android/src/widgets/toggle.rs +++ b/crates/perry-ui-android/src/widgets/toggle.rs @@ -35,7 +35,7 @@ pub fn set_state(handle: i64, on: i64) { /// Create a Switch with a label and onChange callback. /// Returns a widget handle for a LinearLayout(HORIZONTAL) containing the label and switch. pub fn create(label_ptr: *const u8, on_change: f64) -> i64 { - let label = str_from_header(label_ptr); + let label = unsafe { str_from_header(label_ptr) }; let mut env = jni_bridge::get_env(); let _ = env.push_local_frame(32); diff --git a/crates/perry-ui-android/src/widgets/tree_view.rs b/crates/perry-ui-android/src/widgets/tree_view.rs index ca9eeef270..805017a446 100644 --- a/crates/perry-ui-android/src/widgets/tree_view.rs +++ b/crates/perry-ui-android/src/widgets/tree_view.rs @@ -50,8 +50,8 @@ thread_local! { /// map and only become a widget when `tree_view_create(root_node, ...)` /// realizes them as a ListView. pub fn node_create(id_ptr: *const u8, label_ptr: *const u8) -> i64 { - let id = str_from_header(id_ptr).to_string(); - let label = str_from_header(label_ptr).to_string(); + let id = unsafe { str_from_header(id_ptr) }.to_string(); + let label = unsafe { str_from_header(label_ptr) }.to_string(); NEXT_NODE_ID.with(|n| { let mut counter = n.borrow_mut(); let handle = *counter; diff --git a/crates/perry-ui-android/src/widgets/webview.rs b/crates/perry-ui-android/src/widgets/webview.rs index 26c7031c4d..57a57ade1d 100644 --- a/crates/perry-ui-android/src/widgets/webview.rs +++ b/crates/perry-ui-android/src/widgets/webview.rs @@ -38,9 +38,7 @@ thread_local! { static WEBVIEW_STATES: RefCell> = RefCell::new(HashMap::new()); } -fn str_from_header(ptr: *const u8) -> &'static str { - crate::app::str_from_header(ptr) -} +use perry_ffi::copy_string_from_raw as str_from_header; fn nanbox_str(s: &str) -> f64 { let bytes = s.as_bytes(); @@ -54,7 +52,7 @@ fn nanbox_str(s: &str) -> f64 { /// so true per-instance isolation needs a separate process — this is /// the best-effort substitute). pub fn create(url_ptr: *const u8, _width: f64, _height: f64, ephemeral_hint: f64) -> i64 { - let url = str_from_header(url_ptr).to_string(); + let url = unsafe { str_from_header(url_ptr) }.to_string(); if ephemeral_hint > 0.5 { // Wipe at init time so the new WebView starts clean. // Mirrors set_ephemeral(1) but happens before any nav. @@ -188,11 +186,11 @@ fn call_void_method(handle: i64, method: &str) { } pub fn load_url(handle: i64, url_ptr: *const u8) { - let url = str_from_header(url_ptr); + let url = unsafe { str_from_header(url_ptr) }; if url.is_empty() { return; } - call_string_method(handle, "loadUrl", "(Ljava/lang/String;)V", url); + call_string_method(handle, "loadUrl", "(Ljava/lang/String;)V", &url); } pub fn reload(handle: i64) { @@ -236,7 +234,7 @@ pub fn can_go_back(handle: i64) -> i64 { /// returns as JSON-encoded strings; we strip outer quotes for plain /// string results matching the Windows / WKWebView ergonomics. pub fn evaluate_js(handle: i64, js_ptr: *const u8, callback: f64) { - let js = str_from_header(js_ptr); + let js = unsafe { str_from_header(js_ptr) }; let view = match super::get_widget(handle) { Some(v) => v, None => return, @@ -503,7 +501,7 @@ pub fn clear_cookies(_handle: i64) { } pub fn set_user_agent(handle: i64, ua_ptr: *const u8) { - let ua = str_from_header(ua_ptr); + let ua = unsafe { str_from_header(ua_ptr) }; let view = match super::get_widget(handle) { Some(v) => v, None => return, @@ -542,7 +540,7 @@ pub fn set_allowed_domains(handle: i64, domains_arr_handle: i64) { let elem = js_array_get_element_f64(domains_arr_handle, i); let str_ptr = js_get_string_pointer_unified(elem); if !str_ptr.is_null() { - domains.push(str_from_header(str_ptr).to_string()); + domains.push(unsafe { str_from_header(str_ptr) }.to_string()); } } } diff --git a/crates/perry-ui-android/src/widgets/wheel_picker.rs b/crates/perry-ui-android/src/widgets/wheel_picker.rs index fdd8309f83..c8351002ad 100644 --- a/crates/perry-ui-android/src/widgets/wheel_picker.rs +++ b/crates/perry-ui-android/src/widgets/wheel_picker.rs @@ -9,9 +9,7 @@ thread_local! { static ITEMS: RefCell>> = RefCell::new(HashMap::new()); } -fn str_from_header(ptr: *const u8) -> &'static str { - crate::app::str_from_header(ptr) -} +use perry_ffi::copy_string_from_raw as str_from_header; pub fn create(on_change: f64) -> i64 { let mut env = jni_bridge::get_env(); @@ -52,7 +50,7 @@ pub fn create(on_change: f64) -> i64 { } pub fn add_item(handle: i64, title_ptr: *const u8) { - let title = str_from_header(title_ptr).to_string(); + let title = unsafe { str_from_header(title_ptr) }.to_string(); let items = ITEMS.with(|m| { let mut all = m.borrow_mut(); let Some(items) = all.get_mut(&handle) else { diff --git a/crates/perry-ui-android/src/window.rs b/crates/perry-ui-android/src/window.rs index 36b8c1abb4..c4b8d1935d 100644 --- a/crates/perry-ui-android/src/window.rs +++ b/crates/perry-ui-android/src/window.rs @@ -5,9 +5,7 @@ use jni::objects::{GlobalRef, JValue}; use std::cell::RefCell; use std::collections::HashMap; -fn str_from_header(ptr: *const u8) -> &'static str { - crate::app::str_from_header(ptr) -} +use perry_ffi::copy_string_from_raw as str_from_header; struct WindowState { title: String, @@ -23,7 +21,7 @@ thread_local! { } pub fn create(title_ptr: *const u8, width: f64, height: f64) -> i64 { - let title = str_from_header(title_ptr).to_string(); + let title = unsafe { str_from_header(title_ptr) }.to_string(); let id = NEXT_WINDOW_ID.with(|n| { let mut n = n.borrow_mut(); let id = *n; diff --git a/crates/perry-ui-android/src/ws.rs b/crates/perry-ui-android/src/ws.rs index 875afb371c..d228027142 100644 --- a/crates/perry-ui-android/src/ws.rs +++ b/crates/perry-ui-android/src/ws.rs @@ -54,20 +54,7 @@ unsafe impl Send for SendPromise {} static PENDING_RESOLVES: Mutex> = Mutex::new(Vec::new()); /// Extract a Rust &str from a Perry StringHeader pointer. -fn str_from_header(ptr: *const StringHeader) -> Option<&'static str> { - if ptr.is_null() { - return None; - } - unsafe { - let header = ptr as *const perry_runtime::string::StringHeader; - let len = (*header).byte_len as usize; - let data = - (ptr as *const u8).add(std::mem::size_of::()); - Some(std::str::from_utf8_unchecked(std::slice::from_raw_parts( - data, len, - ))) - } -} +use perry_ffi::copy_string_from_raw as str_from_header; /// Set the read timeout on the underlying TcpStream (works for both plain and TLS). fn set_read_timeout(ws: &WebSocket>, timeout: Option) { @@ -215,13 +202,11 @@ fn start_connection(url: String, promise: Option) -> usize { pub extern "C" fn js_ws_connect(url_ptr: *const StringHeader) -> *mut Promise { ws_log("js_ws_connect called"); - let url = match str_from_header(url_ptr) { - Some(u) => u.to_string(), - None => { - ws_log("js_ws_connect: null URL"); - return std::ptr::null_mut(); - } - }; + if url_ptr.is_null() { + ws_log("js_ws_connect: null URL"); + return std::ptr::null_mut(); + } + let url = unsafe { str_from_header(url_ptr) }; ws_log(&format!("js_ws_connect: url={}", &url)); @@ -241,13 +226,11 @@ pub unsafe extern "C" fn js_ws_connect_start(url_nanboxed: f64) -> f64 { ws_log("js_ws_connect_start called"); let url_ptr = perry_runtime::js_get_string_pointer_unified(url_nanboxed) as *const StringHeader; - let url = match str_from_header(url_ptr) { - Some(u) => u.to_string(), - None => { - ws_log("js_ws_connect_start: null URL"); - return 0.0; - } - }; + if url_ptr.is_null() { + ws_log("js_ws_connect_start: null URL"); + return 0.0; + } + let url = unsafe { str_from_header(url_ptr) }; ws_log(&format!("js_ws_connect_start: url={}", &url)); @@ -280,10 +263,7 @@ pub extern "C" fn js_ws_send(handle: i64, message_ptr: *const StringHeader) { if idx < 1 || message_ptr.is_null() { return; } - let msg = match str_from_header(message_ptr) { - Some(s) => s.to_string(), - None => return, - }; + let msg = unsafe { str_from_header(message_ptr) }; let conns = CONNECTIONS.lock().unwrap(); if let Some(Some(conn)) = conns.get(idx - 1) { diff --git a/crates/perry-ui-gtk4/Cargo.toml b/crates/perry-ui-gtk4/Cargo.toml index 53c2956a4f..e383d1d2dd 100644 --- a/crates/perry-ui-gtk4/Cargo.toml +++ b/crates/perry-ui-gtk4/Cargo.toml @@ -15,7 +15,7 @@ perry-ui = { path = "../perry-ui" } perry-ui-testkit = { workspace = true } perry-audio-miniaudio = { workspace = true } perry-runtime = { path = "../perry-runtime" } -perry-ffi = { path = "../perry-ffi" } +perry-ffi.workspace = true base64.workspace = true libc.workspace = true gtk4 = { version = "0.9", features = ["v4_6"] } diff --git a/crates/perry-ui-gtk4/src/app.rs b/crates/perry-ui-gtk4/src/app.rs index ad9ecb8cba..8b619b0f6c 100644 --- a/crates/perry-ui-gtk4/src/app.rs +++ b/crates/perry-ui-gtk4/src/app.rs @@ -88,17 +88,7 @@ extern "C" { } /// Extract a &str from a *const StringHeader pointer. -pub(crate) fn str_from_header(ptr: *const u8) -> &'static str { - if ptr.is_null() { - return ""; - } - unsafe { - let header = ptr as *const perry_runtime::string::StringHeader; - let len = (*header).byte_len as usize; - let data = ptr.add(std::mem::size_of::()); - std::str::from_utf8_unchecked(std::slice::from_raw_parts(data, len)) - } -} +pub(crate) use perry_ffi::copy_string_from_raw as str_from_header; /// Create an app with title, width, height. pub fn app_create(title_ptr: *const u8, width: f64, height: f64) -> i64 { @@ -107,7 +97,7 @@ pub fn app_create(title_ptr: *const u8, width: f64, height: f64) -> i64 { let title = if title_ptr.is_null() { "Perry App".to_string() } else { - str_from_header(title_ptr).to_string() + unsafe { str_from_header(title_ptr) }.to_string() }; let w = if width > 0.0 { width } else { 400.0 }; @@ -519,7 +509,7 @@ pub fn app_set_frameless(app_handle: i64, value: f64) { /// Set window level: "floating", "statusBar", "modal", or "normal". pub fn app_set_level(app_handle: i64, value_ptr: *const u8) { - let level_str = str_from_header(value_ptr); + let level_str = unsafe { str_from_header(value_ptr) }; if level_str.is_empty() { return; } @@ -551,7 +541,7 @@ pub fn app_set_transparent(app_handle: i64, value: f64) { /// Set vibrancy material. On GTK4 this is a best-effort CSS opacity effect /// since true vibrancy depends on the compositor. pub fn app_set_vibrancy(app_handle: i64, value_ptr: *const u8) { - let material_str = str_from_header(value_ptr); + let material_str = unsafe { str_from_header(value_ptr) }; if material_str.is_empty() { return; } @@ -567,7 +557,7 @@ pub fn app_set_vibrancy(app_handle: i64, value_ptr: *const u8) { /// Set activation policy: "regular", "accessory", or "background". /// On Linux: "accessory"/"background" skips the taskbar. pub fn app_set_activation_policy(app_handle: i64, value_ptr: *const u8) { - let policy_str = str_from_header(value_ptr); + let policy_str = unsafe { str_from_header(value_ptr) }; if policy_str.is_empty() { return; } @@ -588,7 +578,7 @@ pub fn app_set_activation_policy(app_handle: i64, value_ptr: *const u8) { /// for one of "normal" | "maximized" | "fullscreen". Anything else is /// silently ignored; the state is applied just before `window.present()`. pub fn app_set_window_state(app_handle: i64, value_ptr: *const u8) { - let state_str = str_from_header(value_ptr); + let state_str = unsafe { str_from_header(value_ptr) }; if state_str.is_empty() { return; } @@ -596,7 +586,7 @@ pub fn app_set_window_state(app_handle: i64, value_ptr: *const u8) { let mut apps = a.borrow_mut(); let idx = (app_handle - 1) as usize; if idx < apps.len() { - apps[idx].window_state = match state_str { + apps[idx].window_state = match state_str.as_str() { "maximized" | "fullscreen" => Some(state_str.to_string()), _ => None, }; @@ -615,7 +605,7 @@ fn install_shortcuts_on_window(window: &ApplicationWindow) { let matched = PENDING_SHORTCUTS.with(|ps| { let shortcuts = ps.borrow(); for shortcut in shortcuts.iter() { - let shortcut_key = str_from_header(shortcut.key_ptr); + let shortcut_key = unsafe { str_from_header(shortcut.key_ptr) }; // Convert Perry modifier bits to GDK modifier state let mod_bits = shortcut.modifiers as u64; @@ -636,7 +626,7 @@ fn install_shortcuts_on_window(window: &ApplicationWindow) { } // Check key match (case-insensitive single char) - let key_matches = key_name.eq_ignore_ascii_case(shortcut_key); + let key_matches = key_name.eq_ignore_ascii_case(&shortcut_key); // Check modifier match (mask out irrelevant bits) let relevant = gdk::ModifierType::CONTROL_MASK @@ -739,21 +729,21 @@ pub fn on_terminate(callback: f64) { /// Register a system-wide global hotkey. /// On Linux this is not yet supported (requires X11-specific code or Wayland portals). pub fn register_global_hotkey(key_ptr: *const u8, _modifiers: f64, _callback: f64) { - let key_str = str_from_header(key_ptr); + let key_str = unsafe { str_from_header(key_ptr) }; eprintln!("[perry/ui] registerGlobalHotkey('{}') is not yet supported on Linux (requires X11/Wayland portal)", key_str); } /// Get the icon for an application at the given path. /// Supports .desktop files (Icon= field lookup via GTK icon theme) and direct image paths. pub fn get_app_icon(path_ptr: *const u8) -> i64 { - let path = str_from_header(path_ptr); + let path = unsafe { str_from_header(path_ptr) }; if path.is_empty() { return 0; } // .desktop file: parse for Icon= field if path.ends_with(".desktop") { - if let Ok(content) = std::fs::read_to_string(path) { + if let Ok(content) = std::fs::read_to_string(&path) { for line in content.lines() { if let Some(icon_name) = line.strip_prefix("Icon=") { let icon_name = icon_name.trim(); @@ -779,7 +769,7 @@ pub fn get_app_icon(path_ptr: *const u8) -> i64 { } // Direct file path — try loading as image - if std::path::Path::new(path).exists() { + if std::path::Path::new(&path).exists() { let image = gtk4::Image::from_file(path); image.set_pixel_size(32); return widgets::register_widget(image.upcast()); diff --git a/crates/perry-ui-gtk4/src/clipboard.rs b/crates/perry-ui-gtk4/src/clipboard.rs index 2cde87b2d2..aed0034b6f 100644 --- a/crates/perry-ui-gtk4/src/clipboard.rs +++ b/crates/perry-ui-gtk4/src/clipboard.rs @@ -15,17 +15,7 @@ thread_local! { } /// Extract a &str from a *const StringHeader pointer. -fn str_from_header(ptr: *const u8) -> &'static str { - if ptr.is_null() { - return ""; - } - unsafe { - let header = ptr as *const perry_runtime::string::StringHeader; - let len = (*header).byte_len as usize; - let data = ptr.add(std::mem::size_of::()); - std::str::from_utf8_unchecked(std::slice::from_raw_parts(data, len)) - } -} +use perry_ffi::copy_string_from_raw as str_from_header; /// Read the current text from the system clipboard. /// Returns a NaN-boxed string (f64) or TAG_UNDEFINED if empty. @@ -60,8 +50,8 @@ pub fn read() -> f64 { /// Write text to the system clipboard. pub fn write(text_ptr: *const u8) { - let text = str_from_header(text_ptr); + let text = unsafe { str_from_header(text_ptr) }; let display = gdk::Display::default().expect("No default display"); let clipboard = display.clipboard(); - clipboard.set_text(text); + clipboard.set_text(&text); } diff --git a/crates/perry-ui-gtk4/src/dialog.rs b/crates/perry-ui-gtk4/src/dialog.rs index 4859a09be3..dc6b1d4068 100644 --- a/crates/perry-ui-gtk4/src/dialog.rs +++ b/crates/perry-ui-gtk4/src/dialog.rs @@ -8,22 +8,12 @@ extern "C" { fn js_nanbox_string(ptr: i64) -> f64; } -fn str_from_header(ptr: *const u8) -> &'static str { - if ptr.is_null() { - return ""; - } - unsafe { - let header = ptr as *const perry_runtime::string::StringHeader; - let len = (*header).byte_len as usize; - let data = ptr.add(std::mem::size_of::()); - std::str::from_utf8_unchecked(std::slice::from_raw_parts(data, len)) - } -} +use perry_ffi::copy_string_from_raw as str_from_header; /// Open a save file dialog. callback receives the selected path or undefined. /// default_name_ptr = suggested file name, allowed_types_ptr = unused on GTK4. pub fn save_file_dialog(callback: f64, default_name_ptr: *const u8, _allowed_types_ptr: *const u8) { - let default_name = str_from_header(default_name_ptr); + let default_name = unsafe { str_from_header(default_name_ptr) }; let window: Option = None; let dialog = FileChooserDialog::new( @@ -37,7 +27,7 @@ pub fn save_file_dialog(callback: f64, default_name_ptr: *const u8, _allowed_typ ); dialog.set_modal(true); if !default_name.is_empty() { - dialog.set_current_name(default_name); + dialog.set_current_name(&default_name); } let callback_f64 = callback; @@ -70,8 +60,8 @@ pub fn save_file_dialog(callback: f64, default_name_ptr: *const u8, _allowed_typ /// Show a simple alert dialog with an OK button. Called from `alert(title, message)`. pub fn alert_simple(title_ptr: *const u8, message_ptr: *const u8) { - let title = str_from_header(title_ptr); - let message = str_from_header(message_ptr); + let title = unsafe { str_from_header(title_ptr) }; + let message = unsafe { str_from_header(message_ptr) }; let window: Option = None; let dialog = gtk4::MessageDialog::new( window.as_ref(), @@ -80,7 +70,7 @@ pub fn alert_simple(title_ptr: *const u8, message_ptr: *const u8) { gtk4::ButtonsType::Ok, title, ); - dialog.set_secondary_text(Some(message)); + dialog.set_secondary_text(Some(&message)); dialog.connect_response(|dialog, _| dialog.close()); dialog.show(); } @@ -88,8 +78,8 @@ pub fn alert_simple(title_ptr: *const u8, message_ptr: *const u8) { /// Show an alert dialog with title, message, and buttons. /// buttons_ptr is a NaN-boxed array of strings. callback receives the button index. pub fn alert(title_ptr: *const u8, message_ptr: *const u8, buttons_ptr: *const u8, callback: f64) { - let title = str_from_header(title_ptr); - let message = str_from_header(message_ptr); + let title = unsafe { str_from_header(title_ptr) }; + let message = unsafe { str_from_header(message_ptr) }; // Parse button labels from the Perry array let button_labels = parse_button_labels(buttons_ptr); @@ -102,7 +92,7 @@ pub fn alert(title_ptr: *const u8, message_ptr: *const u8, buttons_ptr: *const u gtk4::ButtonsType::None, title, ); - dialog.set_secondary_text(Some(message)); + dialog.set_secondary_text(Some(&message)); for (i, label) in button_labels.iter().enumerate() { dialog.add_button(label, ResponseType::Other(i as u16)); @@ -147,7 +137,7 @@ fn parse_button_labels(ptr: *const u8) -> Vec { let elem = unsafe { js_array_get_element_f64(arr, i) }; let str_ptr = unsafe { js_get_string_pointer_unified(elem) }; if !str_ptr.is_null() { - let s = str_from_header(str_ptr); + let s = unsafe { str_from_header(str_ptr) }; labels.push(s.to_string()); } } diff --git a/crates/perry-ui-gtk4/src/drag_drop.rs b/crates/perry-ui-gtk4/src/drag_drop.rs index 4572fa8959..a1c5cc7f8a 100644 --- a/crates/perry-ui-gtk4/src/drag_drop.rs +++ b/crates/perry-ui-gtk4/src/drag_drop.rs @@ -74,17 +74,7 @@ thread_local! { /// Extract a `&str` from a runtime `StringHeader` pointer (same layout as /// `clipboard.rs` / `widgets/button.rs`). -fn str_from_header(ptr: *const u8) -> String { - if ptr.is_null() { - return String::new(); - } - unsafe { - let header = ptr as *const perry_runtime::string::StringHeader; - let len = (*header).byte_len as usize; - let data = ptr.add(std::mem::size_of::()); - std::str::from_utf8_unchecked(std::slice::from_raw_parts(data, len)).to_string() - } -} +use perry_ffi::copy_string_from_raw as str_from_header; /// NaN-box a Rust string into a JS string value. unsafe fn nanbox_str(s: &str) -> f64 { @@ -110,7 +100,7 @@ unsafe fn call_provider(cb: f64) -> Option { if sh.is_null() { None } else { - Some(str_from_header(sh)) + Some(unsafe { str_from_header(sh) }) } } diff --git a/crates/perry-ui-gtk4/src/ffi/layout.rs b/crates/perry-ui-gtk4/src/ffi/layout.rs index 5114e22848..6ff574de20 100644 --- a/crates/perry-ui-gtk4/src/ffi/layout.rs +++ b/crates/perry-ui-gtk4/src/ffi/layout.rs @@ -106,13 +106,13 @@ pub extern "C" fn perry_ui_stack_set_detaches_hidden(handle: i64, flag: i64) { /// Set the application icon. #[no_mangle] pub extern "C" fn perry_ui_app_set_icon(path_ptr: i64) { - let path = crate::widgets::image::str_from_header(path_ptr as *const u8); + let path = unsafe { crate::widgets::image::str_from_header(path_ptr as *const u8) }; if path.is_empty() { return; } // Resolve path: try relative to executable, then relative to cwd - let resolved = resolve_asset_path(path); + let resolved = resolve_asset_path(&path); if !resolved.exists() { return; } diff --git a/crates/perry-ui-gtk4/src/ffi/platform_audio_camera_toast.rs b/crates/perry-ui-gtk4/src/ffi/platform_audio_camera_toast.rs index 0e28f42423..73a8de7f85 100644 --- a/crates/perry-ui-gtk4/src/ffi/platform_audio_camera_toast.rs +++ b/crates/perry-ui-gtk4/src/ffi/platform_audio_camera_toast.rs @@ -237,8 +237,8 @@ pub extern "C" fn perry_ui_camera_unregister_frame_callback(handle: i64) { /// msg_ptr is a raw StringHeader pointer (NaN-boxed string, unboxed to i64 by codegen). #[no_mangle] pub extern "C" fn perry_ui_show_toast(msg_ptr: i64) { - let msg = app::str_from_header(msg_ptr as *const u8); - widgets::toast::show_toast(msg); + let msg = unsafe { app::str_from_header(msg_ptr as *const u8) }; + widgets::toast::show_toast(&msg); } /// Create a Text (GtkLabel) widget and register it under a string id so that @@ -247,8 +247,8 @@ pub extern "C" fn perry_ui_show_toast(msg_ptr: i64) { #[no_mangle] pub extern "C" fn perry_ui_text_create_with_id(text_ptr: i64, id_ptr: i64) -> i64 { let handle = widgets::text::create(text_ptr as *const u8); - let id = app::str_from_header(id_ptr as *const u8); - widgets::text_registry::register(id, handle); + let id = unsafe { app::str_from_header(id_ptr as *const u8) }; + widgets::text_registry::register(&id, handle); handle } @@ -256,7 +256,7 @@ pub extern "C" fn perry_ui_text_create_with_id(text_ptr: i64, id_ptr: i64) -> i6 /// perry_ui_text_create_with_id. #[no_mangle] pub extern "C" fn perry_ui_set_text(id_ptr: i64, value_ptr: i64) { - let id = app::str_from_header(id_ptr as *const u8); - let value = app::str_from_header(value_ptr as *const u8); - widgets::text_registry::set_text_for_id(id, value); + let id = unsafe { app::str_from_header(id_ptr as *const u8) }; + let value = unsafe { app::str_from_header(value_ptr as *const u8) }; + widgets::text_registry::set_text_for_id(&id, &value); } diff --git a/crates/perry-ui-gtk4/src/keychain.rs b/crates/perry-ui-gtk4/src/keychain.rs index de4a11b600..450512a1c5 100644 --- a/crates/perry-ui-gtk4/src/keychain.rs +++ b/crates/perry-ui-gtk4/src/keychain.rs @@ -7,17 +7,7 @@ extern "C" { fn js_nanbox_string(ptr: i64) -> f64; } -fn str_from_header(ptr: *const u8) -> &'static str { - if ptr.is_null() { - return ""; - } - unsafe { - let header = ptr as *const perry_runtime::string::StringHeader; - let len = (*header).byte_len as usize; - let data = ptr.add(std::mem::size_of::()); - std::str::from_utf8_unchecked(std::slice::from_raw_parts(data, len)) - } -} +use perry_ffi::copy_string_from_raw as str_from_header; fn keychain_path() -> PathBuf { let config = std::env::var("XDG_DATA_HOME").unwrap_or_else(|_| { @@ -70,8 +60,8 @@ fn save_keychain() { /// Save a value to the keychain. pub fn save(key_ptr: *const u8, value_ptr: *const u8) { ensure_loaded(); - let key = str_from_header(key_ptr); - let value = str_from_header(value_ptr); + let key = unsafe { str_from_header(key_ptr) }; + let value = unsafe { str_from_header(value_ptr) }; KEYCHAIN.with(|k| { k.borrow_mut().insert(key.to_string(), value.to_string()); }); @@ -81,10 +71,10 @@ pub fn save(key_ptr: *const u8, value_ptr: *const u8) { /// Get a value from the keychain. Returns NaN-boxed string or TAG_UNDEFINED. pub fn get(key_ptr: *const u8) -> f64 { ensure_loaded(); - let key = str_from_header(key_ptr); + let key = unsafe { str_from_header(key_ptr) }; KEYCHAIN.with(|k| { let kc = k.borrow(); - if let Some(val) = kc.get(key) { + if let Some(val) = kc.get(&key) { let bytes = val.as_bytes(); let str_ptr = unsafe { js_string_from_bytes(bytes.as_ptr(), bytes.len() as i64) }; unsafe { js_nanbox_string(str_ptr as i64) } @@ -97,9 +87,9 @@ pub fn get(key_ptr: *const u8) -> f64 { /// Delete a value from the keychain. pub fn delete(key_ptr: *const u8) { ensure_loaded(); - let key = str_from_header(key_ptr); + let key = unsafe { str_from_header(key_ptr) }; KEYCHAIN.with(|k| { - k.borrow_mut().remove(key); + k.borrow_mut().remove(&key); }); save_keychain(); } diff --git a/crates/perry-ui-gtk4/src/media_playback.rs b/crates/perry-ui-gtk4/src/media_playback.rs index d3f4bd2adf..0144d4254f 100644 --- a/crates/perry-ui-gtk4/src/media_playback.rs +++ b/crates/perry-ui-gtk4/src/media_playback.rs @@ -86,17 +86,7 @@ thread_local! { // String helpers // --------------------------------------------------------------------------- -fn str_from_header<'a>(ptr: *const u8) -> &'a str { - if ptr.is_null() { - return ""; - } - unsafe { - let header = ptr as *const perry_runtime::string::StringHeader; - let len = (*header).byte_len as usize; - let data = ptr.add(std::mem::size_of::()); - std::str::from_utf8_unchecked(std::slice::from_raw_parts(data, len)) - } -} +use perry_ffi::copy_string_from_raw as str_from_header; fn ensure_gst_init() { GST_INITIALIZED.with(|i| { @@ -114,7 +104,7 @@ fn ensure_gst_init() { // --------------------------------------------------------------------------- pub fn create_player(url_ptr: *const u8) -> i64 { - let url = str_from_header(url_ptr); + let url = unsafe { str_from_header(url_ptr) }; if url.is_empty() { return 0; } @@ -127,7 +117,7 @@ pub fn create_player(url_ptr: *const u8) -> i64 { url.to_string() } else { std::env::current_dir() - .map(|p| p.join(url).to_string_lossy().to_string()) + .map(|p| p.join(&url).to_string_lossy().to_string()) .unwrap_or_else(|_| url.to_string()) }; format!("file://{}", path) @@ -288,10 +278,10 @@ pub fn set_now_playing( album_ptr: *const u8, artwork_ptr: *const u8, ) { - let title = str_from_header(title_ptr).to_string(); - let artist = str_from_header(artist_ptr).to_string(); - let album = str_from_header(album_ptr).to_string(); - let artwork = str_from_header(artwork_ptr).to_string(); + let title = unsafe { str_from_header(title_ptr) }.to_string(); + let artist = unsafe { str_from_header(artist_ptr) }.to_string(); + let album = unsafe { str_from_header(album_ptr) }.to_string(); + let artwork = unsafe { str_from_header(artwork_ptr) }.to_string(); #[cfg(target_os = "linux")] mpris::push_now_playing(title, artist, album, artwork); #[cfg(not(target_os = "linux"))] diff --git a/crates/perry-ui-gtk4/src/menu.rs b/crates/perry-ui-gtk4/src/menu.rs index 3f933b5bdf..65342fe343 100644 --- a/crates/perry-ui-gtk4/src/menu.rs +++ b/crates/perry-ui-gtk4/src/menu.rs @@ -36,17 +36,7 @@ extern "C" { } /// Extract a &str from a *const StringHeader pointer. -fn str_from_header(ptr: *const u8) -> &'static str { - if ptr.is_null() { - return ""; - } - unsafe { - let header = ptr as *const perry_runtime::string::StringHeader; - let len = (*header).byte_len as usize; - let data = ptr.add(std::mem::size_of::()); - std::str::from_utf8_unchecked(std::slice::from_raw_parts(data, len)) - } -} +use perry_ffi::copy_string_from_raw as str_from_header; /// Create a new context menu. Returns menu handle (1-based). pub fn create() -> i64 { @@ -59,7 +49,7 @@ pub fn create() -> i64 { /// Add an item to a menu with a title and callback. pub fn add_item(menu_handle: i64, title_ptr: *const u8, callback: f64) { - let title = str_from_header(title_ptr).to_string(); + let title = unsafe { str_from_header(title_ptr) }.to_string(); MENUS.with(|m| { let mut menus = m.borrow_mut(); let idx = (menu_handle - 1) as usize; @@ -80,8 +70,8 @@ pub fn add_item_with_shortcut( callback: f64, shortcut_ptr: *const u8, ) { - let title = str_from_header(title_ptr).to_string(); - let shortcut = str_from_header(shortcut_ptr).to_string(); + let title = unsafe { str_from_header(title_ptr) }.to_string(); + let shortcut = unsafe { str_from_header(shortcut_ptr) }.to_string(); MENUS.with(|m| { let mut menus = m.borrow_mut(); let idx = (menu_handle - 1) as usize; @@ -119,7 +109,7 @@ pub fn add_separator(menu_handle: i64) { /// Add a submenu to a menu. pub fn add_submenu(menu_handle: i64, title_ptr: *const u8, submenu_handle: i64) { - let title = str_from_header(title_ptr).to_string(); + let title = unsafe { str_from_header(title_ptr) }.to_string(); MENUS.with(|m| { let mut menus = m.borrow_mut(); let idx = (menu_handle - 1) as usize; @@ -143,7 +133,7 @@ pub fn menubar_create() -> i64 { /// Add a menu to the menu bar with a title. pub fn menubar_add_menu(bar_handle: i64, title_ptr: *const u8, menu_handle: i64) { - let title = str_from_header(title_ptr).to_string(); + let title = unsafe { str_from_header(title_ptr) }.to_string(); MENUBARS.with(|m| { let mut bars = m.borrow_mut(); let idx = (bar_handle - 1) as usize; diff --git a/crates/perry-ui-gtk4/src/state.rs b/crates/perry-ui-gtk4/src/state.rs index 7fe48e154b..cd51a6fb40 100644 --- a/crates/perry-ui-gtk4/src/state.rs +++ b/crates/perry-ui-gtk4/src/state.rs @@ -83,17 +83,7 @@ thread_local! { } /// Extract a &str from a *const StringHeader pointer. -fn str_from_header(ptr: *const u8) -> &'static str { - if ptr.is_null() { - return ""; - } - unsafe { - let header = ptr as *const perry_runtime::string::StringHeader; - let len = (*header).byte_len as usize; - let data = ptr.add(std::mem::size_of::()); - std::str::from_utf8_unchecked(std::slice::from_raw_parts(data, len)) - } -} +use perry_ffi::copy_string_from_raw as str_from_header; /// Check if a f64 value is a NaN-boxed string. Accepts heap /// `STRING_TAG` (0x7FFF) and inline SSO `SHORT_STRING_TAG` (0x7FF9). @@ -109,7 +99,7 @@ fn is_nanboxed_string(value: f64) -> bool { /// `js_nanbox_get_pointer`). fn extract_nanboxed_string(value: f64) -> String { let ptr = unsafe { js_get_string_pointer_unified(value) }; - str_from_header(ptr).to_string() + unsafe { str_from_header(ptr) }.to_string() } fn format_value(value: f64) -> String { @@ -267,7 +257,7 @@ pub fn state_set(handle: i64, value: f64) { let text = { let str_ptr = crate::system::js_get_string_pointer_unified_safe(value); if !str_ptr.is_null() { - str_from_header(str_ptr).to_string() + unsafe { str_from_header(str_ptr) }.to_string() } else { format_value(value) } @@ -333,8 +323,8 @@ pub fn bind_text_numeric( prefix_ptr: *const u8, suffix_ptr: *const u8, ) { - let prefix = str_from_header(prefix_ptr).to_string(); - let suffix = str_from_header(suffix_ptr).to_string(); + let prefix = unsafe { str_from_header(prefix_ptr) }.to_string(); + let suffix = unsafe { str_from_header(suffix_ptr) }.to_string(); TEXT_BINDINGS.with(|b| { b.borrow_mut() .entry(state_handle) @@ -382,7 +372,7 @@ pub fn bind_text_template( let part_value = unsafe { *values_ptr.add(i) }; if part_type == 0 { - let s = str_from_header(part_value as *const u8).to_string(); + let s = unsafe { str_from_header(part_value as *const u8) }.to_string(); parts.push(TextPart::Literal(s)); } else { state_handles.push(part_value); @@ -492,7 +482,7 @@ pub fn bind_textfield(state_handle: i64, textfield_handle: i64) { let text = { let str_ptr = unsafe { js_get_string_pointer_unified(value) }; if !str_ptr.is_null() { - str_from_header(str_ptr).to_string() + unsafe { str_from_header(str_ptr) }.to_string() } else { format_value(value) } diff --git a/crates/perry-ui-gtk4/src/system.rs b/crates/perry-ui-gtk4/src/system.rs index d4f5ce4466..11fe80daa0 100644 --- a/crates/perry-ui-gtk4/src/system.rs +++ b/crates/perry-ui-gtk4/src/system.rs @@ -14,17 +14,7 @@ pub fn js_get_string_pointer_unified_safe(value: f64) -> *const u8 { unsafe { js_get_string_pointer_unified(value) } } -fn str_from_header(ptr: *const u8) -> &'static str { - if ptr.is_null() { - return ""; - } - unsafe { - let header = ptr as *const perry_runtime::string::StringHeader; - let len = (*header).byte_len as usize; - let data = ptr.add(std::mem::size_of::()); - std::str::from_utf8_unchecked(std::slice::from_raw_parts(data, len)) - } -} +use perry_ffi::copy_string_from_raw as str_from_header; fn prefs_path() -> PathBuf { let config = std::env::var("XDG_CONFIG_HOME").unwrap_or_else(|_| { @@ -78,9 +68,9 @@ fn save_prefs() { /// Open a URL using the default browser. pub fn open_url(url_ptr: *const u8) { - let url = str_from_header(url_ptr); + let url = unsafe { str_from_header(url_ptr) }; // Try gio first, fall back to xdg-open - if gtk4::gio::AppInfo::launch_default_for_uri(url, None::<>k4::gio::AppLaunchContext>) + if gtk4::gio::AppInfo::launch_default_for_uri(&url, None::<>k4::gio::AppLaunchContext>) .is_err() { let _ = std::process::Command::new("xdg-open").arg(url).spawn(); @@ -107,12 +97,12 @@ pub fn is_dark_mode() -> i64 { /// Set a preference value. value is either a f64 number or a NaN-boxed string. pub fn preferences_set(key_ptr: *const u8, value: f64) { ensure_prefs_loaded(); - let key = str_from_header(key_ptr); + let key = unsafe { str_from_header(key_ptr) }; // Check if value is a NaN-boxed string let str_ptr = unsafe { js_get_string_pointer_unified(value) }; let val_str = if !str_ptr.is_null() { - str_from_header(str_ptr).to_string() + unsafe { str_from_header(str_ptr) }.to_string() } else { format!("{}", value) }; @@ -126,11 +116,11 @@ pub fn preferences_set(key_ptr: *const u8, value: f64) { /// Get a preference value. Returns NaN-boxed string or the numeric value. pub fn preferences_get(key_ptr: *const u8) -> f64 { ensure_prefs_loaded(); - let key = str_from_header(key_ptr); + let key = unsafe { str_from_header(key_ptr) }; PREFS.with(|p| { let prefs = p.borrow(); - if let Some(val) = prefs.get(key) { + if let Some(val) = prefs.get(&key) { // Try to parse as f64 first if let Ok(n) = val.parse::() { n @@ -148,13 +138,13 @@ pub fn preferences_get(key_ptr: *const u8) -> f64 { /// Send a desktop notification. pub fn notification_send(title_ptr: *const u8, body_ptr: *const u8) { - let title = str_from_header(title_ptr); - let body = str_from_header(body_ptr); + let title = unsafe { str_from_header(title_ptr) }; + let body = unsafe { str_from_header(body_ptr) }; crate::app::GTK_APP.with(|ga| { if let Some(app) = ga.borrow().as_ref() { - let notif = gtk4::gio::Notification::new(title); - notif.set_body(Some(body)); + let notif = gtk4::gio::Notification::new(&title); + notif.set_body(Some(&body)); app.send_notification(None, ¬if); } else { // Fallback: try notify-send diff --git a/crates/perry-ui-gtk4/src/toolbar.rs b/crates/perry-ui-gtk4/src/toolbar.rs index 3694eb2b33..582be8ede8 100644 --- a/crates/perry-ui-gtk4/src/toolbar.rs +++ b/crates/perry-ui-gtk4/src/toolbar.rs @@ -14,17 +14,7 @@ extern "C" { fn js_nanbox_get_pointer(value: f64) -> i64; } -fn str_from_header(ptr: *const u8) -> &'static str { - if ptr.is_null() { - return ""; - } - unsafe { - let header = ptr as *const perry_runtime::string::StringHeader; - let len = (*header).byte_len as usize; - let data = ptr.add(std::mem::size_of::()); - std::str::from_utf8_unchecked(std::slice::from_raw_parts(data, len)) - } -} +use perry_ffi::copy_string_from_raw as str_from_header; /// Create a toolbar (HeaderBar). pub fn create() -> i64 { @@ -44,15 +34,15 @@ pub fn create() -> i64 { /// Add a button item to the toolbar. icon_ptr is a named icon (or empty). pub fn add_item(toolbar_handle: i64, label_ptr: *const u8, icon_ptr: *const u8, callback: f64) { - let label = str_from_header(label_ptr); - let icon = str_from_header(icon_ptr); + let label = unsafe { str_from_header(label_ptr) }; + let icon = unsafe { str_from_header(icon_ptr) }; TOOLBARS.with(|t| { if let Some(header) = t.borrow().get(&toolbar_handle) { let button = if !icon.is_empty() { - gtk4::Button::from_icon_name(icon) + gtk4::Button::from_icon_name(&icon) } else { - gtk4::Button::with_label(label) + gtk4::Button::with_label(&label) }; let cb_id = NEXT_TB_CB_ID.with(|id| { diff --git a/crates/perry-ui-gtk4/src/tray.rs b/crates/perry-ui-gtk4/src/tray.rs index 0d5561a471..3bca761e2d 100644 --- a/crates/perry-ui-gtk4/src/tray.rs +++ b/crates/perry-ui-gtk4/src/tray.rs @@ -57,17 +57,7 @@ extern "C" { } /// Extract a &str from a *const StringHeader pointer. Mirrors menu.rs. -fn str_from_header(ptr: *const u8) -> &'static str { - if ptr.is_null() { - return ""; - } - unsafe { - let header = ptr as *const perry_runtime::string::StringHeader; - let len = (*header).byte_len as usize; - let data = ptr.add(std::mem::size_of::()); - std::str::from_utf8_unchecked(std::slice::from_raw_parts(data, len)) - } -} +use perry_ffi::copy_string_from_raw as str_from_header; /// Mutable per-tray state — read by `impl ksni::Tray for PerryTray` on /// each property fetch. Updates require a `Handle::update` call to @@ -374,7 +364,7 @@ fn tray_runtime() -> Option<&'static Runtime> { /// `trayCreate(iconPath)` — start a KSNI service on the background /// runtime, return a 1-based handle index. Returns 0 on failure. pub fn create(icon_path_ptr: *const u8) -> i64 { - let icon_path = str_from_header(icon_path_ptr).to_string(); + let icon_path = unsafe { str_from_header(icon_path_ptr) }.to_string(); let rt = match tray_runtime() { Some(r) => r, @@ -456,7 +446,7 @@ where } pub fn set_icon(handle: i64, icon_path_ptr: *const u8) { - let path = str_from_header(icon_path_ptr).to_string(); + let path = unsafe { str_from_header(icon_path_ptr) }.to_string(); if path.is_empty() { return; } @@ -469,7 +459,7 @@ pub fn set_icon(handle: i64, icon_path_ptr: *const u8) { } pub fn set_tooltip(handle: i64, tooltip_ptr: *const u8) { - let tooltip = str_from_header(tooltip_ptr).to_string(); + let tooltip = unsafe { str_from_header(tooltip_ptr) }.to_string(); with_tray(handle, |tray| { if let Ok(mut s) = tray.state.lock() { s.tooltip = tooltip.clone(); diff --git a/crates/perry-ui-gtk4/src/widgets/attributed_text.rs b/crates/perry-ui-gtk4/src/widgets/attributed_text.rs index 3d4402dd94..954e7dae2f 100644 --- a/crates/perry-ui-gtk4/src/widgets/attributed_text.rs +++ b/crates/perry-ui-gtk4/src/widgets/attributed_text.rs @@ -22,17 +22,7 @@ thread_local! { static BUFFERS: RefCell> = RefCell::new(HashMap::new()); } -fn str_from_header(ptr: *const u8) -> &'static str { - if ptr.is_null() { - return ""; - } - unsafe { - let header = ptr as *const perry_runtime::string::StringHeader; - let len = (*header).byte_len as usize; - let data = ptr.add(std::mem::size_of::()); - std::str::from_utf8_unchecked(std::slice::from_raw_parts(data, len)) - } -} +use perry_ffi::copy_string_from_raw as str_from_header; pub fn create() -> i64 { crate::app::ensure_gtk_init(); @@ -66,7 +56,7 @@ pub fn append( b: f64, a: f64, ) { - let chunk = str_from_header(text_ptr); + let chunk = unsafe { str_from_header(text_ptr) }; if chunk.is_empty() { return; } @@ -89,7 +79,7 @@ pub fn append( // codepoint-based — `text.len()` is exactly the offset of the new // chunk's first byte. let start = buf.text.len() as u32; - buf.text.push_str(chunk); + buf.text.push_str(&chunk); let end = buf.text.len() as u32; let mut push = |mut attr: pango::Attribute| { diff --git a/crates/perry-ui-gtk4/src/widgets/bottom_nav.rs b/crates/perry-ui-gtk4/src/widgets/bottom_nav.rs index a8539c3f8b..2c9b32264e 100644 --- a/crates/perry-ui-gtk4/src/widgets/bottom_nav.rs +++ b/crates/perry-ui-gtk4/src/widgets/bottom_nav.rs @@ -11,17 +11,7 @@ extern "C" { fn js_nanbox_get_pointer(value: f64) -> i64; } -fn str_from_header(ptr: *const u8) -> &'static str { - if ptr.is_null() { - return ""; - } - unsafe { - let header = ptr as *const perry_runtime::string::StringHeader; - let len = (*header).byte_len as usize; - let data = ptr.add(std::mem::size_of::()); - std::str::from_utf8_unchecked(std::slice::from_raw_parts(data, len)) - } -} +use perry_ffi::copy_string_from_raw as str_from_header; struct ItemViews { button: gtk4::Button, @@ -71,8 +61,8 @@ pub fn create(on_select: f64) -> i64 { } pub fn add_item(handle: i64, icon_ptr: *const u8, label_ptr: *const u8) { - let icon_name = str_from_header(icon_ptr); - let label_text = str_from_header(label_ptr); + let icon_name = unsafe { str_from_header(icon_ptr) }; + let label_text = unsafe { str_from_header(label_ptr) }; let bar = STATES.with(|s| s.borrow().get(&handle).map(|st| st.bar.clone())); let Some(bar) = bar else { return }; @@ -83,12 +73,12 @@ pub fn add_item(handle: i64, icon_ptr: *const u8, label_ptr: *const u8) { let icon = if icon_name.is_empty() { gtk4::Image::new() } else { - gtk4::Image::from_icon_name(icon_name) + gtk4::Image::from_icon_name(&icon_name) }; icon.set_pixel_size(24); inner.append(&icon); - let label = gtk4::Label::new(Some(label_text)); + let label = gtk4::Label::new(Some(&label_text)); label.add_css_class("caption"); inner.append(&label); @@ -139,7 +129,7 @@ pub fn add_item(handle: i64, icon_ptr: *const u8, label_ptr: *const u8) { } pub fn set_badge(handle: i64, index: i64, badge_ptr: *const u8) { - let badge_text = str_from_header(badge_ptr); + let badge_text = unsafe { str_from_header(badge_ptr) }; STATES.with(|s| { let mut nav = s.borrow_mut(); let Some(state) = nav.get_mut(&handle) else { @@ -152,7 +142,7 @@ pub fn set_badge(handle: i64, index: i64, badge_ptr: *const u8) { item.container.remove(&old); } if !badge_text.is_empty() { - let badge = gtk4::Label::new(Some(badge_text)); + let badge = gtk4::Label::new(Some(&badge_text)); badge.add_css_class("error"); // Adwaita styles "error" badges red. badge.add_css_class("caption-heading"); item.container.prepend(&badge); diff --git a/crates/perry-ui-gtk4/src/widgets/button.rs b/crates/perry-ui-gtk4/src/widgets/button.rs index 0d8a9d82cf..0b175687c2 100644 --- a/crates/perry-ui-gtk4/src/widgets/button.rs +++ b/crates/perry-ui-gtk4/src/widgets/button.rs @@ -15,23 +15,13 @@ extern "C" { } /// Extract a &str from a *const StringHeader pointer. -fn str_from_header(ptr: *const u8) -> &'static str { - if ptr.is_null() { - return ""; - } - unsafe { - let header = ptr as *const perry_runtime::string::StringHeader; - let len = (*header).byte_len as usize; - let data = ptr.add(std::mem::size_of::()); - std::str::from_utf8_unchecked(std::slice::from_raw_parts(data, len)) - } -} +use perry_ffi::copy_string_from_raw as str_from_header; /// Create a GtkButton with a label and closure callback. pub fn create(label_ptr: *const u8, on_press: f64) -> i64 { crate::app::ensure_gtk_init(); - let label = str_from_header(label_ptr); - let button = Button::with_label(label); + let label = unsafe { str_from_header(label_ptr) }; + let button = Button::with_label(&label); let callback_id = NEXT_BUTTON_ID.with(|id| { let mut id = id.borrow_mut(); @@ -82,10 +72,10 @@ pub fn set_bordered(handle: i64, bordered: bool) { /// Set the title text of a button. pub fn set_title(handle: i64, title_ptr: *const u8) { - let title = str_from_header(title_ptr); + let title = unsafe { str_from_header(title_ptr) }; if let Some(widget) = super::get_widget(handle) { if let Some(button) = widget.downcast_ref::