1#![allow(clippy::missing_safety_doc)]
28
29use std::cell::RefCell;
30use std::ffi::{CStr, CString, c_char, c_int, c_void};
31use std::panic::AssertUnwindSafe;
32use std::path::PathBuf;
33use std::ptr::{self, NonNull};
34use std::sync::Mutex;
35
36use lumen_core::components::Color;
37use lumen_core::property_store::{
38 PropertyKey, PropertyValue, external_property_snapshot, push_external_property,
39};
40use lumen_core::signals::{push_external_array, push_external_clear};
41use lumen_runtime::RunOptions;
42use lumen_script::{NativeExternFn, ScriptValue};
43use std::collections::HashMap;
44use std::sync::Arc;
45
46pub const LUMEN_ABI_MAJOR: u32 = 0;
52pub const LUMEN_ABI_MINOR: u32 = 13;
138pub const LUMEN_ABI_PATCH: u32 = 0;
140
141pub const LUMEN_ABI_VERSION: u32 =
145 (LUMEN_ABI_MAJOR << 16) | (LUMEN_ABI_MINOR << 8) | LUMEN_ABI_PATCH;
146
147#[unsafe(no_mangle)]
150pub extern "C" fn lumen_abi_version() -> u32 {
151 LUMEN_ABI_VERSION
152}
153
154#[repr(u32)]
160#[derive(Debug, Copy, Clone, PartialEq, Eq)]
161pub enum LumenKind {
162 Nil = 0,
164 Bool = 1,
166 Int = 2,
168 Float = 3,
170 String = 4,
172 Array = 5,
174 Map = 6,
176}
177
178#[repr(C)]
181#[derive(Copy, Clone)]
182pub struct LumenArrayView {
183 pub items: *const LumenValue,
185 pub len: usize,
187}
188
189#[repr(C)]
192#[derive(Copy, Clone)]
193pub struct LumenMapView {
194 pub entries: *const LumenMapEntry,
196 pub len: usize,
198}
199
200#[repr(C)]
203#[derive(Copy, Clone)]
204pub union LumenValueData {
205 pub boolean: c_int,
207 pub integer: i64,
209 pub float_: f64,
211 pub string: *const c_char,
213 pub array: LumenArrayView,
215 pub map: LumenMapView,
217}
218
219#[repr(C)]
224#[derive(Copy, Clone)]
225pub struct LumenValue {
226 pub kind: LumenKind,
228 pub as_: LumenValueData,
230}
231
232#[repr(C)]
234#[derive(Copy, Clone)]
235pub struct LumenMapEntry {
236 pub key: *const c_char,
238 pub value: LumenValue,
240}
241
242#[repr(u32)]
249#[derive(Debug, Copy, Clone, PartialEq, Eq)]
250pub enum LumenStatus {
251 Ok = 0,
253 ErrBadPath = 1,
255 ErrBadArg = 2,
257 ErrRuntime = 3,
260 ErrInternal = 4,
262 ErrParse = 5,
264 ErrCss = 6,
266 ErrAsset = 7,
268 ErrWindow = 8,
270 ErrScript = 9,
272 ErrIo = 10,
274 ErrInvalidHandle = 11,
277 ErrInvalidValue = 12,
280 ErrPanic = 13,
284 ErrBufferTooSmall = 14,
289}
290
291pub type LumenFn = unsafe extern "C" fn(
296 argc: c_int,
297 argv: *const LumenValue,
298 user_data: *mut c_void,
299) -> LumenValue;
300
301pub type LumenFnV2 = unsafe extern "C" fn(
317 out: *mut LumenValue,
318 argc: c_int,
319 argv: *const LumenValue,
320 user_data: *mut c_void,
321);
322
323pub type LumenClickFn = unsafe extern "C" fn(id: *const c_char, user_data: *mut c_void);
332
333pub type LumenCloseFn = unsafe extern "C" fn(user_data: *mut c_void) -> c_int;
348
349pub type LumenWatchFn =
368 unsafe extern "C" fn(name: *const c_char, value: *const LumenValue, user_data: *mut c_void);
369
370pub struct LumenApp {
379 dir: PathBuf,
380 artifact_bytes: Option<Vec<u8>>,
387 title: Option<String>,
388 size: Option<(u32, u32)>,
389 exposed: Vec<ExposedFn>,
390 click_handlers: HashMap<String, (LumenClickFn, UserData)>,
394 close_handler: Option<(LumenCloseFn, UserData)>,
397}
398
399#[derive(Copy, Clone)]
409pub struct UserData(Option<NonNull<c_void>>);
410
411impl UserData {
412 pub fn from_raw(p: *mut c_void) -> Self {
414 Self(NonNull::new(p))
415 }
416
417 pub fn as_ptr(self) -> *mut c_void {
420 match self.0 {
421 Some(p) => p.as_ptr(),
422 None => ptr::null_mut(),
423 }
424 }
425}
426
427unsafe impl Send for UserData {}
448unsafe impl Sync for UserData {}
449
450#[derive(Copy, Clone)]
453enum ExposedPtr {
454 V1(LumenFn),
455 V2(LumenFnV2),
456}
457
458struct ExposedFn {
459 name: String,
460 fn_ptr: ExposedPtr,
461 user_data: UserData,
464 arg_count: usize,
465}
466
467thread_local! {
468 static LAST_ERROR: RefCell<Option<CString>> = const { RefCell::new(None) };
469}
470
471static GLOBAL_LAST_ERROR: Mutex<Option<CString>> = Mutex::new(None);
477
478fn set_last_error(s: impl Into<String>) {
479 let s = s.into();
480 if let Ok(c) = CString::new(s.clone()) {
481 LAST_ERROR.with(|cell| {
482 *cell.borrow_mut() = Some(c.clone());
483 });
484 match GLOBAL_LAST_ERROR.lock() {
489 Ok(mut g) => *g = Some(c),
490 Err(poisoned) => {
491 let mut g = poisoned.into_inner();
492 *g = Some(c);
493 }
494 }
495 }
496}
497
498fn catch<F>(f: F) -> LumenStatus
499where
500 F: FnOnce() -> LumenStatus,
501{
502 match std::panic::catch_unwind(AssertUnwindSafe(f)) {
503 Ok(s) => s,
504 Err(_) => {
505 set_last_error("rust panic across FFI");
506 LumenStatus::ErrPanic
507 }
508 }
509}
510
511fn catch_val<T, F>(fallback: T, f: F) -> T
516where
517 F: FnOnce() -> T,
518{
519 match std::panic::catch_unwind(AssertUnwindSafe(f)) {
520 Ok(v) => v,
521 Err(_) => {
522 set_last_error("rust panic across FFI");
523 fallback
524 }
525 }
526}
527
528#[unsafe(no_mangle)]
542pub unsafe extern "C" fn lumen_app_new(dir: *const c_char) -> *mut LumenApp {
543 let r: Result<*mut LumenApp, String> = std::panic::catch_unwind(AssertUnwindSafe(|| {
544 if dir.is_null() {
545 return Err("null dir".to_string());
546 }
547 let s = match unsafe { CStr::from_ptr(dir) }.to_str() {
548 Ok(s) => s,
549 Err(_) => return Err("dir not utf-8".to_string()),
550 };
551 let path = PathBuf::from(s);
552 let meta = std::fs::metadata(&path)
555 .map_err(|e| format!("lumen_app_new: cannot access app directory {s:?}: {e}"))?;
556 if !meta.is_dir() {
557 return Err(format!("lumen_app_new: {s:?} is not a directory"));
558 }
559 if !path.join("main.lmn").is_file() && !path.join("lumen.toml").is_file() {
560 return Err(format!(
561 "lumen_app_new: app directory {s:?} contains neither main.lmn nor lumen.toml"
562 ));
563 }
564 Ok(Box::into_raw(Box::new(LumenApp {
565 dir: path,
566 artifact_bytes: None,
567 title: None,
568 size: None,
569 exposed: Vec::new(),
570 click_handlers: HashMap::new(),
571 close_handler: None,
572 })))
573 }))
574 .unwrap_or_else(|_| Err("panic in lumen_app_new".to_string()));
575 match r {
576 Ok(p) => p,
577 Err(msg) => {
578 set_last_error(msg);
579 ptr::null_mut()
580 }
581 }
582}
583
584#[unsafe(no_mangle)]
602pub unsafe extern "C" fn lumen_app_new_from_lmna(
603 data: *const u8,
604 len: usize,
605 base_dir: *const c_char,
606) -> *mut LumenApp {
607 let r: Result<*mut LumenApp, String> = std::panic::catch_unwind(AssertUnwindSafe(|| {
608 if data.is_null() || len == 0 {
609 return Err("lumen_app_new_from_lmna: null or empty LMNA data".to_string());
610 }
611 let bytes = unsafe { std::slice::from_raw_parts(data, len) }.to_vec();
614 let dir = if base_dir.is_null() {
616 PathBuf::from(".")
617 } else {
618 match unsafe { CStr::from_ptr(base_dir) }.to_str() {
619 Ok(s) => PathBuf::from(s),
620 Err(_) => return Err("lumen_app_new_from_lmna: base_dir not utf-8".to_string()),
621 }
622 };
623 Ok(Box::into_raw(Box::new(LumenApp {
624 dir,
625 artifact_bytes: Some(bytes),
626 title: None,
627 size: None,
628 exposed: Vec::new(),
629 click_handlers: HashMap::new(),
630 close_handler: None,
631 })))
632 }))
633 .unwrap_or_else(|_| Err("panic in lumen_app_new_from_lmna".to_string()));
634 match r {
635 Ok(p) => p,
636 Err(msg) => {
637 set_last_error(msg);
638 ptr::null_mut()
639 }
640 }
641}
642
643#[unsafe(no_mangle)]
653pub unsafe extern "C" fn lumen_app_expose(
654 app: *mut LumenApp,
655 name: *const c_char,
656 arg_count: u32,
657 func: Option<LumenFn>,
658 user_data: *mut c_void,
659) -> LumenStatus {
660 catch(|| {
661 let Some(func) = func else {
662 set_last_error("null fn");
663 return LumenStatus::ErrBadArg;
664 };
665 if app.is_null() {
666 set_last_error("null app");
667 return LumenStatus::ErrInvalidHandle;
668 }
669 if name.is_null() {
670 set_last_error("null name");
671 return LumenStatus::ErrBadArg;
672 }
673 let app = unsafe { &mut *app };
674 let name_str = match unsafe { CStr::from_ptr(name) }.to_str() {
675 Ok(s) => s.to_owned(),
676 Err(_) => {
677 set_last_error("name not utf-8");
678 return LumenStatus::ErrBadArg;
679 }
680 };
681 app.exposed.push(ExposedFn {
682 name: name_str,
683 fn_ptr: ExposedPtr::V1(func),
684 user_data: UserData::from_raw(user_data),
685 arg_count: arg_count as usize,
686 });
687 LumenStatus::Ok
688 })
689}
690
691#[unsafe(no_mangle)]
702pub unsafe extern "C" fn lumen_app_expose_v2(
703 app: *mut LumenApp,
704 name: *const c_char,
705 arg_count: u32,
706 func: Option<LumenFnV2>,
707 user_data: *mut c_void,
708) -> LumenStatus {
709 catch(|| {
710 let Some(func) = func else {
711 set_last_error("null fn");
712 return LumenStatus::ErrBadArg;
713 };
714 if app.is_null() {
715 set_last_error("null app");
716 return LumenStatus::ErrInvalidHandle;
717 }
718 if name.is_null() {
719 set_last_error("null name");
720 return LumenStatus::ErrBadArg;
721 }
722 let app = unsafe { &mut *app };
723 let name_str = match unsafe { CStr::from_ptr(name) }.to_str() {
724 Ok(s) => s.to_owned(),
725 Err(_) => {
726 set_last_error("name not utf-8");
727 return LumenStatus::ErrBadArg;
728 }
729 };
730 app.exposed.push(ExposedFn {
731 name: name_str,
732 fn_ptr: ExposedPtr::V2(func),
733 user_data: UserData::from_raw(user_data),
734 arg_count: arg_count as usize,
735 });
736 LumenStatus::Ok
737 })
738}
739
740#[unsafe(no_mangle)]
753pub unsafe extern "C" fn lumen_app_on_click(
754 app: *mut LumenApp,
755 id: *const c_char,
756 cb: Option<LumenClickFn>,
757 user_data: *mut c_void,
758) -> LumenStatus {
759 catch(|| {
760 let Some(cb) = cb else {
761 set_last_error("lumen_app_on_click: null callback");
762 return LumenStatus::ErrBadArg;
763 };
764 if app.is_null() {
765 set_last_error("lumen_app_on_click: null app");
766 return LumenStatus::ErrInvalidHandle;
767 }
768 if id.is_null() {
769 set_last_error("lumen_app_on_click: null id");
770 return LumenStatus::ErrBadArg;
771 }
772 let app = unsafe { &mut *app };
773 let id_str = match unsafe { CStr::from_ptr(id) }.to_str() {
774 Ok(s) => s.to_owned(),
775 Err(_) => {
776 set_last_error("lumen_app_on_click: id not utf-8");
777 return LumenStatus::ErrBadArg;
778 }
779 };
780 app.click_handlers
781 .insert(id_str, (cb, UserData::from_raw(user_data)));
782 LumenStatus::Ok
783 })
784}
785
786#[unsafe(no_mangle)]
800pub unsafe extern "C" fn lumen_app_on_close(
801 app: *mut LumenApp,
802 cb: Option<LumenCloseFn>,
803 user_data: *mut c_void,
804) -> LumenStatus {
805 catch(|| {
806 let Some(cb) = cb else {
807 set_last_error("lumen_app_on_close: null callback");
808 return LumenStatus::ErrBadArg;
809 };
810 if app.is_null() {
811 set_last_error("lumen_app_on_close: null app");
812 return LumenStatus::ErrInvalidHandle;
813 }
814 let app = unsafe { &mut *app };
815 app.close_handler = Some((cb, UserData::from_raw(user_data)));
816 LumenStatus::Ok
817 })
818}
819
820type WatcherEntry = (LumenWatchFn, UserData);
823
824static SIGNAL_WATCHERS: OnceLock<Mutex<HashMap<String, Vec<WatcherEntry>>>> = OnceLock::new();
829
830fn signal_watchers() -> &'static Mutex<HashMap<String, Vec<WatcherEntry>>> {
831 SIGNAL_WATCHERS.get_or_init(|| Mutex::new(HashMap::new()))
832}
833
834#[unsafe(no_mangle)]
854pub unsafe extern "C" fn lumen_signal_watch(
855 name: *const c_char,
856 cb: Option<LumenWatchFn>,
857 user_data: *mut c_void,
858) -> LumenStatus {
859 catch(|| {
860 let Some(cb) = cb else {
861 set_last_error("lumen_signal_watch: null callback");
862 return LumenStatus::ErrBadArg;
863 };
864 let Some(n) = typed_signal_name(name) else {
865 set_last_error("lumen_signal_watch: null or non-utf8 name");
866 return LumenStatus::ErrBadArg;
867 };
868 signal_watchers()
869 .lock()
870 .unwrap_or_else(|e| e.into_inner())
871 .entry(n)
872 .or_default()
873 .push((cb, UserData::from_raw(user_data)));
874 LumenStatus::Ok
875 })
876}
877
878fn property_to_lumen(v: &PropertyValue, keep: &mut Option<CString>) -> LumenValue {
883 match v {
884 PropertyValue::Bool(b) => LumenValue {
885 kind: LumenKind::Bool,
886 as_: LumenValueData {
887 boolean: *b as c_int,
888 },
889 },
890 PropertyValue::I64(n) => LumenValue {
891 kind: LumenKind::Int,
892 as_: LumenValueData { integer: *n },
893 },
894 PropertyValue::F64(n) => LumenValue {
895 kind: LumenKind::Float,
896 as_: LumenValueData { float_: *n },
897 },
898 PropertyValue::Str(s) => {
899 let cs = CString::new(s.as_ref()).unwrap_or_default();
900 let ptr = cs.as_ptr();
901 *keep = Some(cs);
902 LumenValue {
903 kind: LumenKind::String,
904 as_: LumenValueData { string: ptr },
905 }
906 }
907 PropertyValue::Color(c) => {
908 let [r, g, b, a] = c.to_rgba8();
909 let packed = ((r as i64) << 24) | ((g as i64) << 16) | ((b as i64) << 8) | (a as i64);
910 LumenValue {
911 kind: LumenKind::Int,
912 as_: LumenValueData { integer: packed },
913 }
914 }
915 PropertyValue::Vec2(_) | PropertyValue::Custom(_) => LumenValue {
916 kind: LumenKind::Nil,
917 as_: LumenValueData { integer: 0 },
918 },
919 }
920}
921
922#[unsafe(no_mangle)]
925pub unsafe extern "C" fn lumen_app_set_title(
926 app: *mut LumenApp,
927 title: *const c_char,
928) -> LumenStatus {
929 catch(|| {
930 if app.is_null() {
931 return LumenStatus::ErrInvalidHandle;
932 }
933 if title.is_null() {
934 return LumenStatus::ErrBadArg;
935 }
936 let s = match unsafe { CStr::from_ptr(title) }.to_str() {
937 Ok(s) => s,
938 Err(_) => return LumenStatus::ErrBadArg,
939 };
940 unsafe { &mut *app }.title = Some(s.to_owned());
941 LumenStatus::Ok
942 })
943}
944
945#[unsafe(no_mangle)]
947pub unsafe extern "C" fn lumen_app_set_size(app: *mut LumenApp, w: u32, h: u32) -> LumenStatus {
948 catch(|| {
949 if app.is_null() {
950 return LumenStatus::ErrInvalidHandle;
951 }
952 unsafe { &mut *app }.size = Some((w, h));
953 LumenStatus::Ok
954 })
955}
956
957#[unsafe(no_mangle)]
959pub unsafe extern "C" fn lumen_app_free(app: *mut LumenApp) {
960 if app.is_null() {
961 return;
962 }
963 let _ = catch(|| {
964 unsafe {
965 drop(Box::from_raw(app));
966 }
967 LumenStatus::Ok
968 });
969}
970
971#[unsafe(no_mangle)]
975pub unsafe extern "C" fn lumen_app_run(app: *mut LumenApp) -> LumenStatus {
976 catch(|| {
977 if app.is_null() {
978 return LumenStatus::ErrInvalidHandle;
979 }
980 let app = unsafe { Box::from_raw(app) };
981 run_inner(*app)
982 })
983}
984
985#[unsafe(no_mangle)]
998pub unsafe extern "C" fn lumen_app_run_headless(app: *mut LumenApp, ticks: u32) -> LumenStatus {
999 catch(|| {
1000 if app.is_null() {
1001 return LumenStatus::ErrInvalidHandle;
1002 }
1003 let app = unsafe { Box::from_raw(app) };
1004 run_headless_inner(*app, ticks)
1005 })
1006}
1007
1008#[unsafe(no_mangle)]
1018pub unsafe extern "C" fn lumen_last_error() -> *const c_char {
1019 let tls = LAST_ERROR.with(|c| {
1020 c.borrow()
1021 .as_ref()
1022 .map(|s| s.as_ptr())
1023 .unwrap_or(ptr::null())
1024 });
1025 if !tls.is_null() {
1026 return tls;
1027 }
1028 lumen_last_error_global()
1029}
1030
1031#[unsafe(no_mangle)]
1039pub extern "C" fn lumen_last_error_global() -> *const c_char {
1040 thread_local! {
1044 static GLOBAL_BUFFER: RefCell<Option<CString>> = const { RefCell::new(None) };
1045 }
1046 let snapshot: Option<CString> = match GLOBAL_LAST_ERROR.lock() {
1047 Ok(g) => g.clone(),
1048 Err(poisoned) => poisoned.into_inner().clone(),
1049 };
1050 GLOBAL_BUFFER.with(|cell| {
1051 *cell.borrow_mut() = snapshot;
1052 cell.borrow()
1053 .as_ref()
1054 .map(|s| s.as_ptr())
1055 .unwrap_or(ptr::null())
1056 })
1057}
1058
1059fn build_run_options(app: LumenApp) -> RunOptions {
1068 let LumenApp {
1069 dir,
1070 artifact_bytes,
1071 title,
1072 size,
1073 exposed,
1074 click_handlers,
1075 close_handler,
1076 } = app;
1077
1078 let mut opts = match artifact_bytes {
1084 Some(bytes) => RunOptions::new(&dir).with_artifact_bytes(bytes),
1085 #[cfg(feature = "embed-parser")]
1091 None => RunOptions::new(&dir).with_parser(lumenc::default_parser()),
1092 #[cfg(not(feature = "embed-parser"))]
1093 None => RunOptions::new(&dir),
1094 };
1095 if let Some(t) = title {
1096 opts.title = Some(t);
1097 }
1098 if let Some(sz) = size {
1099 opts.size = sz;
1100 }
1101
1102 for ef in exposed {
1103 let ExposedFn {
1104 name,
1105 fn_ptr,
1106 user_data,
1107 arg_count,
1108 } = ef;
1109 opts = opts.with_native_fn(NativeExternFn::new(
1110 name,
1111 arg_count,
1112 move |args: &[ScriptValue]| {
1113 let mut keep: Vec<CString> = Vec::new();
1116 let lvs: Vec<LumenValue> = args
1117 .iter()
1118 .map(|v| script_value_to_lumen(v, &mut keep))
1119 .collect();
1120 let rv = match fn_ptr {
1121 ExposedPtr::V1(f) => unsafe {
1122 f(lvs.len() as c_int, lvs.as_ptr(), user_data.as_ptr())
1123 },
1124 ExposedPtr::V2(f) => {
1125 let mut out = LumenValue {
1128 kind: LumenKind::Nil,
1129 as_: LumenValueData { integer: 0 },
1130 };
1131 unsafe {
1132 f(
1133 &mut out,
1134 lvs.len() as c_int,
1135 lvs.as_ptr(),
1136 user_data.as_ptr(),
1137 );
1138 }
1139 out
1140 }
1141 };
1142 lumen_to_script_value(&rv)
1143 },
1144 ));
1145 }
1146
1147 if !click_handlers.is_empty() {
1152 opts = opts.with_app_hook(move |app| {
1153 use lumen_core::prelude::{ClickEvent, LumenId, MessageReader, Query, TickStage};
1154 let handlers = click_handlers;
1155 app.add_systems(
1156 TickStage::Systems,
1157 move |mut clicks: MessageReader<ClickEvent>, ids: Query<&LumenId>| {
1158 for click in clicks.read() {
1159 let Ok(id) = ids.get(click.entity) else {
1160 continue;
1161 };
1162 if let Some(&(cb, ud)) = handlers.get(id.0.as_str())
1163 && let Ok(cid) = CString::new(id.0.as_str())
1164 {
1165 unsafe { cb(cid.as_ptr(), ud.as_ptr()) };
1166 }
1167 }
1168 },
1169 );
1170 });
1171 }
1172
1173 if let Some((close_cb, close_ud)) = close_handler {
1183 opts = opts.with_app_hook(move |app| {
1184 use bevy_ecs::message::{MessageCursor, Messages};
1185 use lumen_core::input::CloseRequest;
1186 use lumen_core::prelude::TickStage;
1187 let mut cursor = MessageCursor::<CloseRequest>::default();
1188 app.add_systems(
1189 TickStage::Systems,
1190 move |mut msgs: bevy_ecs::system::ResMut<Messages<CloseRequest>>| {
1191 let requests = cursor.read(&msgs).filter(|ev| !ev.vetoed).count();
1192 let mut veto = false;
1193 for _ in 0..requests {
1194 let allow = unsafe { close_cb(close_ud.as_ptr()) };
1195 if allow == 0 {
1196 veto = true;
1197 }
1198 }
1199 if veto {
1200 msgs.write(CloseRequest { vetoed: true });
1201 }
1202 },
1203 );
1204 });
1205 }
1206
1207 opts = opts.with_app_hook(move |app| {
1216 use lumen_core::prelude::{PropertyKey, PropertyStore, PropertyValue, Res, TickStage};
1217 let mut last: HashMap<String, PropertyValue> = HashMap::new();
1218 app.add_systems(
1219 TickStage::A11ySync,
1220 move |store: Option<Res<PropertyStore>>| {
1221 let Some(store) = store else {
1222 return;
1223 };
1224 let mut fires: Vec<(CString, PropertyValue, LumenWatchFn, UserData)> = Vec::new();
1228 {
1229 let reg = signal_watchers().lock().unwrap_or_else(|e| e.into_inner());
1230 if reg.is_empty() {
1231 return;
1232 }
1233 for (name, entries) in reg.iter() {
1234 let key = PropertyKey::Global(Arc::<str>::from(name.as_str()));
1235 let Some(value) = store.get(&key) else {
1236 continue;
1237 };
1238 let changed = match last.get(name) {
1239 Some(prev) => !prev.eq_value(value),
1240 None => true,
1241 };
1242 if changed {
1243 last.insert(name.clone(), value.clone());
1244 if let Ok(cname) = CString::new(name.as_str()) {
1245 for (cb, ud) in entries {
1246 fires.push((cname.clone(), value.clone(), *cb, *ud));
1247 }
1248 }
1249 }
1250 }
1251 }
1252 for (cname, value, cb, ud) in fires {
1253 let mut keep: Option<CString> = None;
1254 let lv = property_to_lumen(&value, &mut keep);
1255 unsafe { cb(cname.as_ptr(), &lv, ud.as_ptr()) };
1256 }
1257 },
1258 );
1259 });
1260
1261 opts
1262}
1263
1264fn run_inner(app: LumenApp) -> LumenStatus {
1265 let opts = build_run_options(app);
1266 match lumen_runtime::run_app(opts) {
1267 Ok(()) => LumenStatus::Ok,
1268 Err(e) => {
1269 set_last_error(format!("{e}"));
1270 classify_runtime_error(&format!("{e}"))
1271 }
1272 }
1273}
1274
1275fn run_headless_inner(app: LumenApp, ticks: u32) -> LumenStatus {
1276 let opts = build_run_options(app);
1277 match lumen_runtime::run_app_headless(opts, ticks) {
1278 Ok(()) => LumenStatus::Ok,
1279 Err(e) => {
1280 set_last_error(format!("{e}"));
1281 classify_runtime_error(&format!("{e}"))
1282 }
1283 }
1284}
1285
1286fn classify_runtime_error(msg: &str) -> LumenStatus {
1292 let m = msg.to_ascii_lowercase();
1293 if m.contains("css") || m.contains("stylesheet") {
1294 LumenStatus::ErrCss
1295 } else if m.contains("parse") || m.contains("xml") || m.contains("html") {
1296 LumenStatus::ErrParse
1297 } else if m.contains("asset") || m.contains("decode") || m.contains("png") || m.contains("svg")
1298 {
1299 LumenStatus::ErrAsset
1300 } else if m.contains("window") || m.contains("winit") || m.contains("surface") {
1301 LumenStatus::ErrWindow
1302 } else if m.contains("rhai") || m.contains("script") {
1303 LumenStatus::ErrScript
1304 } else if m.contains("io") || m.contains("file") || m.contains("read") || m.contains("write") {
1305 LumenStatus::ErrIo
1306 } else {
1307 LumenStatus::ErrRuntime
1308 }
1309}
1310
1311fn script_value_to_lumen(v: &ScriptValue, keep: &mut Vec<CString>) -> LumenValue {
1320 match v {
1321 ScriptValue::I64(i) => LumenValue {
1322 kind: LumenKind::Int,
1323 as_: LumenValueData { integer: *i },
1324 },
1325 ScriptValue::F64(f) => LumenValue {
1326 kind: LumenKind::Float,
1327 as_: LumenValueData { float_: *f },
1328 },
1329 ScriptValue::Bool(b) => LumenValue {
1330 kind: LumenKind::Bool,
1331 as_: LumenValueData {
1332 boolean: *b as c_int,
1333 },
1334 },
1335 ScriptValue::Str(s) => {
1336 let cs = CString::new(s.as_str()).unwrap_or_default();
1337 let ptr = cs.as_ptr();
1338 keep.push(cs);
1339 LumenValue {
1340 kind: LumenKind::String,
1341 as_: LumenValueData { string: ptr },
1342 }
1343 }
1344 ScriptValue::Unit | ScriptValue::Array(_) | ScriptValue::Map(_) => LumenValue {
1345 kind: LumenKind::Nil,
1346 as_: LumenValueData { integer: 0 },
1347 },
1348 }
1349}
1350
1351fn lumen_to_script_value(v: &LumenValue) -> ScriptValue {
1355 match v.kind {
1356 LumenKind::Nil => ScriptValue::Unit,
1357 LumenKind::Bool => ScriptValue::Bool(unsafe { v.as_.boolean } != 0),
1358 LumenKind::Int => ScriptValue::I64(unsafe { v.as_.integer }),
1359 LumenKind::Float => ScriptValue::F64(unsafe { v.as_.float_ }),
1360 LumenKind::String => {
1361 let p = unsafe { v.as_.string };
1362 if p.is_null() {
1363 ScriptValue::Str(String::new())
1364 } else {
1365 ScriptValue::Str(unsafe { CStr::from_ptr(p) }.to_string_lossy().into_owned())
1366 }
1367 }
1368 LumenKind::Array => {
1369 let view = unsafe { v.as_.array };
1370 let items: &[LumenValue] = if view.items.is_null() || view.len == 0 {
1371 &[]
1372 } else {
1373 unsafe { std::slice::from_raw_parts(view.items, view.len) }
1374 };
1375 ScriptValue::Array(items.iter().map(lumen_to_script_value).collect())
1376 }
1377 LumenKind::Map => {
1378 let view = unsafe { v.as_.map };
1379 let entries: &[LumenMapEntry] = if view.entries.is_null() || view.len == 0 {
1380 &[]
1381 } else {
1382 unsafe { std::slice::from_raw_parts(view.entries, view.len) }
1383 };
1384 let mut m = HashMap::with_capacity(entries.len());
1385 for e in entries {
1386 let key = if e.key.is_null() {
1387 String::new()
1388 } else {
1389 unsafe { CStr::from_ptr(e.key) }
1390 .to_string_lossy()
1391 .into_owned()
1392 };
1393 m.insert(key, lumen_to_script_value(&e.value));
1394 }
1395 ScriptValue::Map(m)
1396 }
1397 }
1398}
1399
1400type ArrayRow = HashMap<String, String>;
1422type ArraySignalMap = HashMap<String, Vec<ArrayRow>>;
1424
1425static ARRAY_SIGNALS: OnceLock<Mutex<ArraySignalMap>> = OnceLock::new();
1426
1427fn array_signals() -> &'static Mutex<ArraySignalMap> {
1428 ARRAY_SIGNALS.get_or_init(|| Mutex::new(HashMap::new()))
1429}
1430
1431fn write_string_out(
1438 value: &str,
1439 buf: *mut c_char,
1440 buf_len: usize,
1441 out_len: *mut usize,
1442) -> LumenStatus {
1443 let bytes = value.as_bytes();
1444 let needed = bytes.len() + 1; if buf.is_null() || buf_len < needed {
1446 if !out_len.is_null() {
1447 unsafe { *out_len = needed };
1448 }
1449 return LumenStatus::ErrBufferTooSmall;
1450 }
1451 unsafe {
1452 ptr::copy_nonoverlapping(bytes.as_ptr(), buf as *mut u8, bytes.len());
1453 *buf.add(bytes.len()) = 0;
1454 }
1455 if !out_len.is_null() {
1456 unsafe { *out_len = bytes.len() };
1457 }
1458 LumenStatus::Ok
1459}
1460
1461#[unsafe(no_mangle)]
1477pub unsafe extern "C" fn lumen_navigate(path: *const c_char) -> LumenStatus {
1478 catch(|| {
1479 if path.is_null() {
1480 set_last_error("lumen_navigate: null path");
1481 return LumenStatus::ErrBadArg;
1482 }
1483 let p = match unsafe { CStr::from_ptr(path) }.to_str() {
1484 Ok(s) => s.to_owned(),
1485 Err(_) => {
1486 set_last_error("lumen_navigate: path not utf-8");
1487 return LumenStatus::ErrBadArg;
1488 }
1489 };
1490 lumen_core::nav::navigate(p);
1491 LumenStatus::Ok
1492 })
1493}
1494
1495#[unsafe(no_mangle)]
1498pub unsafe extern "C" fn lumen_navigate_back() -> LumenStatus {
1499 catch(|| {
1500 lumen_core::nav::back();
1501 LumenStatus::Ok
1502 })
1503}
1504
1505#[unsafe(no_mangle)]
1508pub unsafe extern "C" fn lumen_navigate_forward() -> LumenStatus {
1509 catch(|| {
1510 lumen_core::nav::forward();
1511 LumenStatus::Ok
1512 })
1513}
1514
1515#[unsafe(no_mangle)]
1523pub unsafe extern "C" fn lumen_current_page(
1524 buf: *mut c_char,
1525 buf_len: usize,
1526 out_len: *mut usize,
1527) -> LumenStatus {
1528 catch(|| write_string_out(&lumen_core::nav::current(), buf, buf_len, out_len))
1529}
1530
1531#[unsafe(no_mangle)]
1537pub unsafe extern "C" fn lumen_signal_set_array(
1538 name: *const c_char,
1539 value: *const LumenValue,
1540) -> LumenStatus {
1541 catch(|| {
1542 if name.is_null() || value.is_null() {
1543 return LumenStatus::ErrBadArg;
1544 }
1545 let n = match unsafe { CStr::from_ptr(name) }.to_str() {
1546 Ok(s) => s.to_owned(),
1547 Err(_) => return LumenStatus::ErrBadArg,
1548 };
1549 let v = unsafe { &*value };
1550 if v.kind != LumenKind::Array {
1551 set_last_error("lumen_signal_set_array: value.kind must be LUMEN_ARRAY");
1552 return LumenStatus::ErrInvalidValue;
1553 }
1554 let arr = unsafe { v.as_.array };
1555 let items_slice: &[LumenValue] = if arr.items.is_null() || arr.len == 0 {
1556 &[]
1557 } else {
1558 unsafe { std::slice::from_raw_parts(arr.items, arr.len) }
1559 };
1560 let mut rows: Vec<HashMap<String, String>> = Vec::with_capacity(items_slice.len());
1561 for row in items_slice {
1562 let mut map: HashMap<String, String> = HashMap::new();
1563 if row.kind == LumenKind::Map {
1564 let mv = unsafe { row.as_.map };
1565 let entries: &[LumenMapEntry] = if mv.entries.is_null() || mv.len == 0 {
1566 &[]
1567 } else {
1568 unsafe { std::slice::from_raw_parts(mv.entries, mv.len) }
1569 };
1570 for e in entries {
1571 let k = if e.key.is_null() {
1572 String::new()
1573 } else {
1574 unsafe { CStr::from_ptr(e.key) }
1575 .to_string_lossy()
1576 .into_owned()
1577 };
1578 map.insert(k, stringify_lumen(&e.value));
1579 }
1580 }
1581 rows.push(map);
1582 }
1583 array_signals()
1584 .lock()
1585 .unwrap_or_else(|e| e.into_inner())
1586 .insert(n.clone(), rows.clone());
1587 push_external_array(n, rows);
1588 LumenStatus::Ok
1589 })
1590}
1591
1592#[unsafe(no_mangle)]
1594pub unsafe extern "C" fn lumen_signal_clear(name: *const c_char) -> LumenStatus {
1595 catch(|| {
1596 if name.is_null() {
1597 return LumenStatus::ErrBadArg;
1598 }
1599 let n = match unsafe { CStr::from_ptr(name) }.to_str() {
1600 Ok(s) => s,
1601 Err(_) => return LumenStatus::ErrBadArg,
1602 };
1603 typed_signals()
1604 .lock()
1605 .unwrap_or_else(|e| e.into_inner())
1606 .insert(n.to_owned(), TypedSignalValue::Str(Arc::<str>::from("")));
1607 array_signals()
1608 .lock()
1609 .unwrap_or_else(|e| e.into_inner())
1610 .insert(n.to_owned(), Vec::new());
1611 push_external_clear(n);
1612 LumenStatus::Ok
1613 })
1614}
1615
1616#[unsafe(no_mangle)]
1623pub unsafe extern "C" fn lumen_signal_array_len(
1624 name: *const c_char,
1625 out_len: *mut usize,
1626) -> LumenStatus {
1627 catch(|| {
1628 let Some(n) = typed_signal_name(name) else {
1629 set_last_error("lumen_signal_array_len: null or non-utf8 name");
1630 return LumenStatus::ErrBadArg;
1631 };
1632 if out_len.is_null() {
1633 set_last_error("lumen_signal_array_len: null out_len");
1634 return LumenStatus::ErrBadArg;
1635 }
1636 let len = array_signals()
1637 .lock()
1638 .unwrap_or_else(|e| e.into_inner())
1639 .get(&n)
1640 .map(Vec::len);
1641 match len {
1642 Some(l) => {
1643 unsafe { *out_len = l };
1644 LumenStatus::Ok
1645 }
1646 None => {
1647 set_last_error("lumen_signal_array_len: no array signal by that name");
1648 LumenStatus::ErrBadArg
1649 }
1650 }
1651 })
1652}
1653
1654#[unsafe(no_mangle)]
1667pub unsafe extern "C" fn lumen_signal_array_get_field(
1668 name: *const c_char,
1669 row: usize,
1670 field: *const c_char,
1671 buf: *mut c_char,
1672 buf_len: usize,
1673 out_len: *mut usize,
1674) -> LumenStatus {
1675 catch(|| {
1676 let Some(n) = typed_signal_name(name) else {
1677 set_last_error("lumen_signal_array_get_field: null or non-utf8 name");
1678 return LumenStatus::ErrBadArg;
1679 };
1680 let Some(key) = typed_signal_name(field) else {
1681 set_last_error("lumen_signal_array_get_field: null or non-utf8 field");
1682 return LumenStatus::ErrBadArg;
1683 };
1684 let value = {
1685 let guard = array_signals().lock().unwrap_or_else(|e| e.into_inner());
1686 let Some(rows) = guard.get(&n) else {
1687 set_last_error("lumen_signal_array_get_field: no array signal by that name");
1688 return LumenStatus::ErrBadArg;
1689 };
1690 let Some(row_map) = rows.get(row) else {
1691 set_last_error("lumen_signal_array_get_field: row index out of range");
1692 return LumenStatus::ErrBadArg;
1693 };
1694 row_map.get(&key).cloned()
1695 };
1696 match value {
1697 Some(v) => write_string_out(&v, buf, buf_len, out_len),
1698 None => {
1699 set_last_error("lumen_signal_array_get_field: no such field in row");
1700 LumenStatus::ErrBadArg
1701 }
1702 }
1703 })
1704}
1705
1706fn stringify_lumen(v: &LumenValue) -> String {
1707 match v.kind {
1708 LumenKind::Nil => String::new(),
1709 LumenKind::Bool => (unsafe { v.as_.boolean } != 0).to_string(),
1710 LumenKind::Int => unsafe { v.as_.integer }.to_string(),
1711 LumenKind::Float => format!("{}", unsafe { v.as_.float_ }),
1712 LumenKind::String => {
1713 let p = unsafe { v.as_.string };
1714 if p.is_null() {
1715 String::new()
1716 } else {
1717 unsafe { CStr::from_ptr(p) }.to_string_lossy().into_owned()
1718 }
1719 }
1720 LumenKind::Array | LumenKind::Map => String::new(),
1721 }
1722}
1723
1724use std::sync::OnceLock;
1775
1776#[derive(Clone, Debug)]
1777enum TypedSignalValue {
1778 Str(Arc<str>),
1779 Int64(i64),
1780 Float64(f64),
1781 Bool(bool),
1782 Color([u8; 4]),
1783}
1784
1785static TYPED_SIGNALS: OnceLock<Mutex<HashMap<String, TypedSignalValue>>> = OnceLock::new();
1786
1787fn typed_signals() -> &'static Mutex<HashMap<String, TypedSignalValue>> {
1788 TYPED_SIGNALS.get_or_init(|| Mutex::new(HashMap::new()))
1789}
1790
1791fn typed_signal_name(name: *const c_char) -> Option<String> {
1792 if name.is_null() {
1793 return None;
1794 }
1795 let s = unsafe { CStr::from_ptr(name) }.to_str().ok()?;
1796 Some(s.to_owned())
1797}
1798
1799fn global_key(name: &str) -> PropertyKey {
1802 PropertyKey::Global(Arc::<str>::from(name))
1803}
1804
1805fn typed_read(name: &str) -> Option<TypedSignalValue> {
1819 let key = global_key(name);
1820 if let Some(value) = lumen_core::property_store::typed_property_snapshot().remove(&key) {
1822 return Some(TypedSignalValue::from(value));
1823 }
1824 if let Some(value) = external_property_snapshot().remove(&key) {
1826 return Some(TypedSignalValue::from(value));
1827 }
1828 typed_signals()
1830 .lock()
1831 .unwrap_or_else(|e| e.into_inner())
1832 .get(name)
1833 .cloned()
1834}
1835
1836impl From<PropertyValue> for TypedSignalValue {
1837 fn from(v: PropertyValue) -> Self {
1838 match v {
1839 PropertyValue::Str(s) => Self::Str(s),
1840 PropertyValue::I64(n) => Self::Int64(n),
1841 PropertyValue::F64(n) => Self::Float64(n),
1842 PropertyValue::Bool(b) => Self::Bool(b),
1843 PropertyValue::Color(c) => Self::Color(c.to_rgba8()),
1844 PropertyValue::Vec2(_) | PropertyValue::Custom(_) => Self::Bool(false),
1848 }
1849 }
1850}
1851
1852#[unsafe(no_mangle)]
1860pub unsafe extern "C" fn lumen_signal_set_str(
1861 name: *const c_char,
1862 value: *const c_char,
1863) -> LumenStatus {
1864 catch(|| {
1865 let Some(n) = typed_signal_name(name) else {
1866 set_last_error("lumen_signal_set_str: null or non-utf8 name");
1867 return LumenStatus::ErrBadArg;
1868 };
1869 let v: Arc<str> = if value.is_null() {
1870 Arc::<str>::from("")
1871 } else {
1872 match unsafe { CStr::from_ptr(value) }.to_str() {
1873 Ok(s) => Arc::<str>::from(s),
1874 Err(_) => {
1875 set_last_error("lumen_signal_set_str: value is not utf-8");
1876 return LumenStatus::ErrBadArg;
1877 }
1878 }
1879 };
1880 typed_signals()
1881 .lock()
1882 .unwrap_or_else(|e| e.into_inner())
1883 .insert(n.clone(), TypedSignalValue::Str(Arc::clone(&v)));
1884 push_external_property(global_key(&n), PropertyValue::Str(v));
1885 LumenStatus::Ok
1886 })
1887}
1888
1889#[unsafe(no_mangle)]
1907pub unsafe extern "C" fn lumen_signal_get_str(
1908 name: *const c_char,
1909 buf: *mut c_char,
1910 buf_len: usize,
1911 out_len: *mut usize,
1912) -> LumenStatus {
1913 catch(|| {
1914 let Some(n) = typed_signal_name(name) else {
1915 set_last_error("lumen_signal_get_str: null or non-utf8 name");
1916 return LumenStatus::ErrBadArg;
1917 };
1918 match typed_read(&n) {
1919 Some(TypedSignalValue::Str(s)) => write_string_out(&s, buf, buf_len, out_len),
1920 _ => {
1921 set_last_error("lumen_signal_get_str: no string signal by that name");
1922 LumenStatus::ErrBadArg
1923 }
1924 }
1925 })
1926}
1927
1928#[unsafe(no_mangle)]
1935pub unsafe extern "C" fn lumen_signal_set_int64(name: *const c_char, value: i64) -> LumenStatus {
1936 catch(|| {
1937 let Some(n) = typed_signal_name(name) else {
1938 set_last_error("lumen_signal_set_int64: null or non-utf8 name");
1939 return LumenStatus::ErrBadArg;
1940 };
1941 typed_signals()
1942 .lock()
1943 .unwrap_or_else(|e| e.into_inner())
1944 .insert(n.clone(), TypedSignalValue::Int64(value));
1945 push_external_property(global_key(&n), PropertyValue::I64(value));
1946 LumenStatus::Ok
1947 })
1948}
1949
1950#[unsafe(no_mangle)]
1957pub unsafe extern "C" fn lumen_signal_get_int64(name: *const c_char, out: *mut i64) -> LumenStatus {
1958 catch(|| {
1959 let Some(n) = typed_signal_name(name) else {
1960 set_last_error("lumen_signal_get_int64: null or non-utf8 name");
1961 return LumenStatus::ErrBadArg;
1962 };
1963 if out.is_null() {
1964 set_last_error("lumen_signal_get_int64: null out pointer");
1965 return LumenStatus::ErrBadArg;
1966 }
1967 match typed_read(&n) {
1968 Some(TypedSignalValue::Int64(v)) => {
1969 unsafe { *out = v };
1970 LumenStatus::Ok
1971 }
1972 Some(TypedSignalValue::Float64(v)) => {
1973 unsafe { *out = v as i64 };
1974 LumenStatus::Ok
1975 }
1976 Some(TypedSignalValue::Bool(b)) => {
1977 unsafe { *out = b as i64 };
1978 LumenStatus::Ok
1979 }
1980 _ => LumenStatus::ErrBadArg,
1981 }
1982 })
1983}
1984
1985#[unsafe(no_mangle)]
1990pub unsafe extern "C" fn lumen_signal_set_float64(name: *const c_char, value: f64) -> LumenStatus {
1991 catch(|| {
1992 let Some(n) = typed_signal_name(name) else {
1993 set_last_error("lumen_signal_set_float64: null or non-utf8 name");
1994 return LumenStatus::ErrBadArg;
1995 };
1996 typed_signals()
1997 .lock()
1998 .unwrap_or_else(|e| e.into_inner())
1999 .insert(n.clone(), TypedSignalValue::Float64(value));
2000 push_external_property(global_key(&n), PropertyValue::F64(value));
2001 LumenStatus::Ok
2002 })
2003}
2004
2005#[unsafe(no_mangle)]
2007pub unsafe extern "C" fn lumen_signal_get_float64(
2008 name: *const c_char,
2009 out: *mut f64,
2010) -> LumenStatus {
2011 catch(|| {
2012 let Some(n) = typed_signal_name(name) else {
2013 set_last_error("lumen_signal_get_float64: null or non-utf8 name");
2014 return LumenStatus::ErrBadArg;
2015 };
2016 if out.is_null() {
2017 set_last_error("lumen_signal_get_float64: null out pointer");
2018 return LumenStatus::ErrBadArg;
2019 }
2020 match typed_read(&n) {
2021 Some(TypedSignalValue::Float64(v)) => {
2022 unsafe { *out = v };
2023 LumenStatus::Ok
2024 }
2025 Some(TypedSignalValue::Int64(v)) => {
2026 unsafe { *out = v as f64 };
2027 LumenStatus::Ok
2028 }
2029 _ => LumenStatus::ErrBadArg,
2030 }
2031 })
2032}
2033
2034#[unsafe(no_mangle)]
2039pub unsafe extern "C" fn lumen_signal_set_bool(name: *const c_char, value: bool) -> LumenStatus {
2040 catch(|| {
2041 let Some(n) = typed_signal_name(name) else {
2042 set_last_error("lumen_signal_set_bool: null or non-utf8 name");
2043 return LumenStatus::ErrBadArg;
2044 };
2045 typed_signals()
2046 .lock()
2047 .unwrap_or_else(|e| e.into_inner())
2048 .insert(n.clone(), TypedSignalValue::Bool(value));
2049 push_external_property(global_key(&n), PropertyValue::Bool(value));
2050 LumenStatus::Ok
2051 })
2052}
2053
2054#[unsafe(no_mangle)]
2056pub unsafe extern "C" fn lumen_signal_get_bool(name: *const c_char, out: *mut bool) -> LumenStatus {
2057 catch(|| {
2058 let Some(n) = typed_signal_name(name) else {
2059 set_last_error("lumen_signal_get_bool: null or non-utf8 name");
2060 return LumenStatus::ErrBadArg;
2061 };
2062 if out.is_null() {
2063 set_last_error("lumen_signal_get_bool: null out pointer");
2064 return LumenStatus::ErrBadArg;
2065 }
2066 match typed_read(&n) {
2067 Some(TypedSignalValue::Bool(b)) => {
2068 unsafe { *out = b };
2069 LumenStatus::Ok
2070 }
2071 Some(TypedSignalValue::Int64(v)) => {
2072 unsafe { *out = v != 0 };
2073 LumenStatus::Ok
2074 }
2075 _ => LumenStatus::ErrBadArg,
2076 }
2077 })
2078}
2079
2080#[unsafe(no_mangle)]
2086pub unsafe extern "C" fn lumen_signal_set_color(
2087 name: *const c_char,
2088 rgba: *const u8,
2089) -> LumenStatus {
2090 catch(|| {
2091 let Some(n) = typed_signal_name(name) else {
2092 set_last_error("lumen_signal_set_color: null or non-utf8 name");
2093 return LumenStatus::ErrBadArg;
2094 };
2095 if rgba.is_null() {
2096 set_last_error("lumen_signal_set_color: null rgba pointer");
2097 return LumenStatus::ErrBadArg;
2098 }
2099 let bytes = unsafe { std::slice::from_raw_parts(rgba, 4) };
2100 let arr = [bytes[0], bytes[1], bytes[2], bytes[3]];
2101 typed_signals()
2102 .lock()
2103 .unwrap_or_else(|e| e.into_inner())
2104 .insert(n.clone(), TypedSignalValue::Color(arr));
2105 let color = Color::rgba(
2106 (arr[0] as f32) / 255.0,
2107 (arr[1] as f32) / 255.0,
2108 (arr[2] as f32) / 255.0,
2109 (arr[3] as f32) / 255.0,
2110 );
2111 push_external_property(global_key(&n), PropertyValue::Color(color));
2112 LumenStatus::Ok
2113 })
2114}
2115
2116#[unsafe(no_mangle)]
2119pub unsafe extern "C" fn lumen_signal_get_color(name: *const c_char, out: *mut u8) -> LumenStatus {
2120 catch(|| {
2121 let Some(n) = typed_signal_name(name) else {
2122 set_last_error("lumen_signal_get_color: null or non-utf8 name");
2123 return LumenStatus::ErrBadArg;
2124 };
2125 if out.is_null() {
2126 set_last_error("lumen_signal_get_color: null out pointer");
2127 return LumenStatus::ErrBadArg;
2128 }
2129 match typed_read(&n) {
2130 Some(TypedSignalValue::Color(c)) => {
2131 let dst = unsafe { std::slice::from_raw_parts_mut(out, 4) };
2132 dst.copy_from_slice(&c);
2133 LumenStatus::Ok
2134 }
2135 _ => LumenStatus::ErrBadArg,
2136 }
2137 })
2138}
2139
2140#[unsafe(no_mangle)]
2146pub extern "C" fn lumen_status_message(status: LumenStatus) -> *const c_char {
2147 let s: &'static [u8] = match status {
2148 LumenStatus::Ok => b"ok\0",
2149 LumenStatus::ErrBadPath => b"bad path argument\0",
2150 LumenStatus::ErrBadArg => b"bad argument\0",
2151 LumenStatus::ErrRuntime => b"runtime error\0",
2152 LumenStatus::ErrInternal => b"internal error\0",
2153 LumenStatus::ErrParse => b"parse error\0",
2154 LumenStatus::ErrCss => b"css error\0",
2155 LumenStatus::ErrAsset => b"asset error\0",
2156 LumenStatus::ErrWindow => b"window backend error\0",
2157 LumenStatus::ErrScript => b"script error\0",
2158 LumenStatus::ErrIo => b"io error\0",
2159 LumenStatus::ErrInvalidHandle => b"invalid handle\0",
2160 LumenStatus::ErrInvalidValue => b"invalid value\0",
2161 LumenStatus::ErrPanic => b"rust panic across ffi\0",
2162 LumenStatus::ErrBufferTooSmall => b"output buffer too small\0",
2163 };
2164 s.as_ptr() as *const c_char
2165}
2166
2167pub type LumenNode = u64;
2178
2179#[repr(C)]
2182pub struct LumenNodeList {
2183 pub ptr: *mut LumenNode,
2186 pub len: usize,
2188}
2189
2190fn empty_node_list() -> LumenNodeList {
2191 LumenNodeList {
2192 ptr: ptr::null_mut(),
2193 len: 0,
2194 }
2195}
2196
2197fn node_list_from(nodes: Vec<u64>) -> LumenNodeList {
2198 if nodes.is_empty() {
2199 return empty_node_list();
2200 }
2201 let mut boxed = nodes.into_boxed_slice();
2202 let list = LumenNodeList {
2203 ptr: boxed.as_mut_ptr(),
2204 len: boxed.len(),
2205 };
2206 std::mem::forget(boxed);
2207 list
2208}
2209
2210fn ffi_selector(selector: *const c_char, ctx: &str) -> Result<String, LumenStatus> {
2211 if selector.is_null() {
2212 set_last_error(format!("{ctx}: null selector"));
2213 return Err(LumenStatus::ErrBadArg);
2214 }
2215 match unsafe { CStr::from_ptr(selector) }.to_str() {
2216 Ok(s) => Ok(s.to_owned()),
2217 Err(_) => {
2218 set_last_error(format!("{ctx}: selector not utf-8"));
2219 Err(LumenStatus::ErrBadArg)
2220 }
2221 }
2222}
2223
2224#[unsafe(no_mangle)]
2229pub unsafe extern "C" fn lumen_query(
2230 selector: *const c_char,
2231 out_list: *mut LumenNodeList,
2232) -> LumenStatus {
2233 catch(|| {
2234 if out_list.is_null() {
2235 set_last_error("lumen_query: null out_list");
2236 return LumenStatus::ErrBadArg;
2237 }
2238 let sel = match ffi_selector(selector, "lumen_query") {
2239 Ok(s) => s,
2240 Err(status) => return status,
2241 };
2242 match lumen_script::node_query::run_query(&sel) {
2243 Ok(q) => {
2244 unsafe { *out_list = node_list_from(q.nodes) };
2245 LumenStatus::Ok
2246 }
2247 Err(e) => {
2248 set_last_error(format!("lumen_query: {e}"));
2249 LumenStatus::ErrCss
2250 }
2251 }
2252 })
2253}
2254
2255#[unsafe(no_mangle)]
2257pub unsafe extern "C" fn lumen_query_len(
2258 selector: *const c_char,
2259 out_len: *mut usize,
2260) -> LumenStatus {
2261 catch(|| {
2262 if out_len.is_null() {
2263 set_last_error("lumen_query_len: null out_len");
2264 return LumenStatus::ErrBadArg;
2265 }
2266 let sel = match ffi_selector(selector, "lumen_query_len") {
2267 Ok(s) => s,
2268 Err(status) => return status,
2269 };
2270 match lumen_script::node_query::run_query(&sel) {
2271 Ok(q) => {
2272 unsafe { *out_len = q.len() };
2273 LumenStatus::Ok
2274 }
2275 Err(e) => {
2276 set_last_error(format!("lumen_query_len: {e}"));
2277 LumenStatus::ErrCss
2278 }
2279 }
2280 })
2281}
2282
2283#[unsafe(no_mangle)]
2287pub unsafe extern "C" fn lumen_query_single(
2288 selector: *const c_char,
2289 out: *mut LumenNode,
2290) -> LumenStatus {
2291 catch(|| {
2292 if out.is_null() {
2293 set_last_error("lumen_query_single: null out");
2294 return LumenStatus::ErrBadArg;
2295 }
2296 unsafe { *out = 0 };
2297 let sel = match ffi_selector(selector, "lumen_query_single") {
2298 Ok(s) => s,
2299 Err(status) => return status,
2300 };
2301 match lumen_script::node_query::run_query(&sel) {
2302 Ok(q) => match q.single() {
2303 Ok(node) => {
2304 unsafe { *out = node };
2305 LumenStatus::Ok
2306 }
2307 Err(msg) => {
2308 set_last_error(format!("lumen_query_single: {msg}"));
2309 LumenStatus::ErrBadArg
2310 }
2311 },
2312 Err(e) => {
2313 set_last_error(format!("lumen_query_single: {e}"));
2314 LumenStatus::ErrCss
2315 }
2316 }
2317 })
2318}
2319
2320#[unsafe(no_mangle)]
2323pub unsafe extern "C" fn lumen_get_by_id(id: *const c_char, out: *mut LumenNode) -> LumenStatus {
2324 catch(|| {
2325 if out.is_null() {
2326 set_last_error("lumen_get_by_id: null out");
2327 return LumenStatus::ErrBadArg;
2328 }
2329 let id = match ffi_selector(id, "lumen_get_by_id") {
2330 Ok(s) => s,
2331 Err(status) => return status,
2332 };
2333 unsafe { *out = lumen_script::node_query::run_get_by_id(&id).unwrap_or(0) };
2334 LumenStatus::Ok
2335 })
2336}
2337
2338#[unsafe(no_mangle)]
2341pub unsafe extern "C" fn lumen_document(out: *mut LumenNode) -> LumenStatus {
2342 catch(|| {
2343 if out.is_null() {
2344 set_last_error("lumen_document: null out");
2345 return LumenStatus::ErrBadArg;
2346 }
2347 unsafe { *out = lumen_script::node_query::run_document().unwrap_or(0) };
2348 LumenStatus::Ok
2349 })
2350}
2351
2352fn node_relation(
2355 node: LumenNode,
2356 out: *mut LumenNode,
2357 ctx: &str,
2358 f: impl FnOnce(u64) -> Option<u64>,
2359) -> LumenStatus {
2360 if out.is_null() {
2361 set_last_error(format!("{ctx}: null out"));
2362 return LumenStatus::ErrBadArg;
2363 }
2364 unsafe { *out = f(node).unwrap_or(0) };
2365 LumenStatus::Ok
2366}
2367
2368#[unsafe(no_mangle)]
2370pub unsafe extern "C" fn lumen_node_parent(node: LumenNode, out: *mut LumenNode) -> LumenStatus {
2371 catch(|| {
2372 node_relation(
2373 node,
2374 out,
2375 "lumen_node_parent",
2376 lumen_script::node_query::node_parent,
2377 )
2378 })
2379}
2380
2381#[unsafe(no_mangle)]
2383pub unsafe extern "C" fn lumen_node_first_child(
2384 node: LumenNode,
2385 out: *mut LumenNode,
2386) -> LumenStatus {
2387 catch(|| {
2388 node_relation(
2389 node,
2390 out,
2391 "lumen_node_first_child",
2392 lumen_script::node_query::node_first_child,
2393 )
2394 })
2395}
2396
2397#[unsafe(no_mangle)]
2399pub unsafe extern "C" fn lumen_node_last_child(
2400 node: LumenNode,
2401 out: *mut LumenNode,
2402) -> LumenStatus {
2403 catch(|| {
2404 node_relation(
2405 node,
2406 out,
2407 "lumen_node_last_child",
2408 lumen_script::node_query::node_last_child,
2409 )
2410 })
2411}
2412
2413#[unsafe(no_mangle)]
2415pub unsafe extern "C" fn lumen_node_next(node: LumenNode, out: *mut LumenNode) -> LumenStatus {
2416 catch(|| {
2417 node_relation(
2418 node,
2419 out,
2420 "lumen_node_next",
2421 lumen_script::node_query::node_next,
2422 )
2423 })
2424}
2425
2426#[unsafe(no_mangle)]
2428pub unsafe extern "C" fn lumen_node_prev(node: LumenNode, out: *mut LumenNode) -> LumenStatus {
2429 catch(|| {
2430 node_relation(
2431 node,
2432 out,
2433 "lumen_node_prev",
2434 lumen_script::node_query::node_prev,
2435 )
2436 })
2437}
2438
2439#[unsafe(no_mangle)]
2442pub unsafe extern "C" fn lumen_node_children(
2443 node: LumenNode,
2444 out_list: *mut LumenNodeList,
2445) -> LumenStatus {
2446 catch(|| {
2447 if out_list.is_null() {
2448 set_last_error("lumen_node_children: null out_list");
2449 return LumenStatus::ErrBadArg;
2450 }
2451 unsafe { *out_list = node_list_from(lumen_script::node_query::node_children(node)) };
2452 LumenStatus::Ok
2453 })
2454}
2455
2456#[unsafe(no_mangle)]
2460pub unsafe extern "C" fn lumen_node_closest(
2461 node: LumenNode,
2462 selector: *const c_char,
2463 out: *mut LumenNode,
2464) -> LumenStatus {
2465 catch(|| {
2466 if out.is_null() {
2467 set_last_error("lumen_node_closest: null out");
2468 return LumenStatus::ErrBadArg;
2469 }
2470 unsafe { *out = 0 };
2471 let sel = match ffi_selector(selector, "lumen_node_closest") {
2472 Ok(s) => s,
2473 Err(status) => return status,
2474 };
2475 match lumen_script::node_query::node_closest(node, &sel) {
2476 Ok(hit) => {
2477 unsafe { *out = hit.unwrap_or(0) };
2478 LumenStatus::Ok
2479 }
2480 Err(e) => {
2481 set_last_error(format!("lumen_node_closest: {e}"));
2482 LumenStatus::ErrCss
2483 }
2484 }
2485 })
2486}
2487
2488#[unsafe(no_mangle)]
2492pub unsafe extern "C" fn lumen_node_valid(node: LumenNode, out: *mut c_int) -> LumenStatus {
2493 catch(|| {
2494 if out.is_null() {
2495 set_last_error("lumen_node_valid: null out");
2496 return LumenStatus::ErrBadArg;
2497 }
2498 unsafe { *out = lumen_script::node_query::node_valid(node) as c_int };
2499 LumenStatus::Ok
2500 })
2501}
2502
2503#[unsafe(no_mangle)]
2511pub unsafe extern "C" fn lumen_nodelist_get(
2512 list: LumenNodeList,
2513 index: usize,
2514 out: *mut LumenNode,
2515) -> LumenStatus {
2516 catch(|| {
2517 if out.is_null() {
2518 set_last_error("lumen_nodelist_get: null out");
2519 return LumenStatus::ErrBadArg;
2520 }
2521 if list.ptr.is_null() || index >= list.len {
2522 set_last_error("lumen_nodelist_get: index out of range");
2523 return LumenStatus::ErrBadArg;
2524 }
2525 unsafe { *out = *list.ptr.add(index) };
2526 LumenStatus::Ok
2527 })
2528}
2529
2530#[unsafe(no_mangle)]
2537pub unsafe extern "C" fn lumen_nodelist_free(list: LumenNodeList) {
2538 if list.ptr.is_null() || list.len == 0 {
2539 return;
2540 }
2541 unsafe {
2542 drop(Box::from_raw(std::ptr::slice_from_raw_parts_mut(
2543 list.ptr, list.len,
2544 )));
2545 }
2546}
2547
2548#[repr(C)]
2560pub struct LumenRect {
2561 pub x: f64,
2563 pub y: f64,
2565 pub width: f64,
2567 pub height: f64,
2569 pub client_x: f64,
2571 pub client_y: f64,
2573}
2574
2575#[repr(C)]
2577pub struct LumenScroll {
2578 pub x: f64,
2580 pub y: f64,
2582 pub max_x: f64,
2584 pub max_y: f64,
2586}
2587
2588#[repr(C)]
2590pub struct LumenPointerState {
2591 pub x: f64,
2593 pub y: f64,
2595 pub inside: c_int,
2597 pub buttons: u32,
2599 pub shift: c_int,
2601 pub ctrl: c_int,
2603 pub alt: c_int,
2605 pub super_: c_int,
2607}
2608
2609#[repr(C)]
2611pub struct LumenFrameInfo {
2612 pub frame: u64,
2614 pub dt_ms: f64,
2616 pub dirty_count: u64,
2618}
2619
2620#[repr(C)]
2623pub struct LumenKV {
2624 pub key: *mut c_char,
2626 pub value: *mut c_char,
2628}
2629
2630#[repr(C)]
2633pub struct LumenKVList {
2634 pub ptr: *mut LumenKV,
2636 pub len: usize,
2638}
2639
2640#[repr(C)]
2643pub struct LumenStrList {
2644 pub ptr: *mut *mut c_char,
2646 pub len: usize,
2648}
2649
2650fn owned_cstring(s: &str) -> *mut c_char {
2651 CString::new(s).unwrap_or_default().into_raw()
2652}
2653
2654fn kvlist_from(pairs: Vec<(String, String)>) -> LumenKVList {
2655 if pairs.is_empty() {
2656 return LumenKVList {
2657 ptr: ptr::null_mut(),
2658 len: 0,
2659 };
2660 }
2661 let mut boxed: Box<[LumenKV]> = pairs
2662 .into_iter()
2663 .map(|(k, v)| LumenKV {
2664 key: owned_cstring(&k),
2665 value: owned_cstring(&v),
2666 })
2667 .collect();
2668 let list = LumenKVList {
2669 ptr: boxed.as_mut_ptr(),
2670 len: boxed.len(),
2671 };
2672 std::mem::forget(boxed);
2673 list
2674}
2675
2676fn strlist_from(items: Vec<String>) -> LumenStrList {
2677 if items.is_empty() {
2678 return LumenStrList {
2679 ptr: ptr::null_mut(),
2680 len: 0,
2681 };
2682 }
2683 let mut boxed: Box<[*mut c_char]> = items.iter().map(|s| owned_cstring(s)).collect();
2684 let list = LumenStrList {
2685 ptr: boxed.as_mut_ptr(),
2686 len: boxed.len(),
2687 };
2688 std::mem::forget(boxed);
2689 list
2690}
2691
2692fn rect_to_ffi(r: lumen_script::introspect::NodeRect) -> LumenRect {
2693 LumenRect {
2694 x: r.x as f64,
2695 y: r.y as f64,
2696 width: r.width as f64,
2697 height: r.height as f64,
2698 client_x: r.client_x as f64,
2699 client_y: r.client_y as f64,
2700 }
2701}
2702
2703#[unsafe(no_mangle)]
2705pub unsafe extern "C" fn lumen_node_rect(node: LumenNode, out: *mut LumenRect) -> LumenStatus {
2706 catch(|| {
2707 if out.is_null() {
2708 set_last_error("lumen_node_rect: null out");
2709 return LumenStatus::ErrBadArg;
2710 }
2711 match lumen_script::introspect::node_rect(node) {
2712 Some(r) => {
2713 unsafe { *out = rect_to_ffi(r) };
2714 LumenStatus::Ok
2715 }
2716 None => LumenStatus::ErrInvalidHandle,
2717 }
2718 })
2719}
2720
2721#[unsafe(no_mangle)]
2723pub unsafe extern "C" fn lumen_node_content_rect(
2724 node: LumenNode,
2725 out: *mut LumenRect,
2726) -> LumenStatus {
2727 catch(|| {
2728 if out.is_null() {
2729 set_last_error("lumen_node_content_rect: null out");
2730 return LumenStatus::ErrBadArg;
2731 }
2732 match lumen_script::introspect::node_content_rect(node) {
2733 Some(r) => {
2734 unsafe { *out = rect_to_ffi(r) };
2735 LumenStatus::Ok
2736 }
2737 None => LumenStatus::ErrInvalidHandle,
2738 }
2739 })
2740}
2741
2742#[unsafe(no_mangle)]
2744pub unsafe extern "C" fn lumen_node_scroll(node: LumenNode, out: *mut LumenScroll) -> LumenStatus {
2745 catch(|| {
2746 if out.is_null() {
2747 set_last_error("lumen_node_scroll: null out");
2748 return LumenStatus::ErrBadArg;
2749 }
2750 match lumen_script::introspect::node_scroll(node) {
2751 Some(s) => {
2752 unsafe {
2753 *out = LumenScroll {
2754 x: s.x as f64,
2755 y: s.y as f64,
2756 max_x: s.max_x as f64,
2757 max_y: s.max_y as f64,
2758 }
2759 };
2760 LumenStatus::Ok
2761 }
2762 None => LumenStatus::ErrInvalidHandle,
2763 }
2764 })
2765}
2766
2767#[unsafe(no_mangle)]
2769pub unsafe extern "C" fn lumen_node_is_visible(node: LumenNode, out: *mut c_int) -> LumenStatus {
2770 catch(|| {
2771 if out.is_null() {
2772 set_last_error("lumen_node_is_visible: null out");
2773 return LumenStatus::ErrBadArg;
2774 }
2775 unsafe { *out = c_int::from(lumen_script::introspect::node_is_visible(node)) };
2776 LumenStatus::Ok
2777 })
2778}
2779
2780#[unsafe(no_mangle)]
2782pub unsafe extern "C" fn lumen_node_z_index(node: LumenNode, out: *mut c_int) -> LumenStatus {
2783 catch(|| {
2784 if out.is_null() {
2785 set_last_error("lumen_node_z_index: null out");
2786 return LumenStatus::ErrBadArg;
2787 }
2788 unsafe { *out = lumen_script::introspect::node_z_index(node) as c_int };
2789 LumenStatus::Ok
2790 })
2791}
2792
2793#[unsafe(no_mangle)]
2795pub unsafe extern "C" fn lumen_node_entity_id(
2796 node: LumenNode,
2797 out_index: *mut u32,
2798 out_gen: *mut u32,
2799) -> LumenStatus {
2800 catch(|| {
2801 if out_index.is_null() || out_gen.is_null() {
2802 set_last_error("lumen_node_entity_id: null out");
2803 return LumenStatus::ErrBadArg;
2804 }
2805 match lumen_script::introspect::node_entity_id(node) {
2806 Some((index, generation)) => {
2807 unsafe {
2808 *out_index = index;
2809 *out_gen = generation;
2810 }
2811 LumenStatus::Ok
2812 }
2813 None => LumenStatus::ErrInvalidHandle,
2814 }
2815 })
2816}
2817
2818#[unsafe(no_mangle)]
2821pub unsafe extern "C" fn lumen_node_computed_style(
2822 node: LumenNode,
2823 out: *mut LumenKVList,
2824) -> LumenStatus {
2825 catch(|| {
2826 if out.is_null() {
2827 set_last_error("lumen_node_computed_style: null out");
2828 return LumenStatus::ErrBadArg;
2829 }
2830 unsafe { *out = kvlist_from(lumen_script::introspect::node_computed_style_map(node)) };
2831 LumenStatus::Ok
2832 })
2833}
2834
2835#[unsafe(no_mangle)]
2837pub unsafe extern "C" fn lumen_node_attrs(node: LumenNode, out: *mut LumenKVList) -> LumenStatus {
2838 catch(|| {
2839 if out.is_null() {
2840 set_last_error("lumen_node_attrs: null out");
2841 return LumenStatus::ErrBadArg;
2842 }
2843 unsafe { *out = kvlist_from(lumen_script::introspect::node_attrs(node)) };
2844 LumenStatus::Ok
2845 })
2846}
2847
2848#[unsafe(no_mangle)]
2850pub unsafe extern "C" fn lumen_node_inline_style(
2851 node: LumenNode,
2852 out: *mut LumenKVList,
2853) -> LumenStatus {
2854 catch(|| {
2855 if out.is_null() {
2856 set_last_error("lumen_node_inline_style: null out");
2857 return LumenStatus::ErrBadArg;
2858 }
2859 unsafe { *out = kvlist_from(lumen_script::introspect::node_inline_style(node)) };
2860 LumenStatus::Ok
2861 })
2862}
2863
2864#[unsafe(no_mangle)]
2867pub unsafe extern "C" fn lumen_node_component(
2868 node: LumenNode,
2869 name: *const c_char,
2870 out: *mut LumenKVList,
2871) -> LumenStatus {
2872 catch(|| {
2873 if out.is_null() {
2874 set_last_error("lumen_node_component: null out");
2875 return LumenStatus::ErrBadArg;
2876 }
2877 let name = match ffi_selector(name, "lumen_node_component") {
2878 Ok(s) => s,
2879 Err(status) => return status,
2880 };
2881 match lumen_script::introspect::node_component(node, &name) {
2882 Ok(map) => {
2883 unsafe { *out = kvlist_from(map.unwrap_or_default()) };
2884 LumenStatus::Ok
2885 }
2886 Err(e) => {
2887 set_last_error(format!("lumen_node_component: {e}"));
2888 LumenStatus::ErrBadArg
2889 }
2890 }
2891 })
2892}
2893
2894#[unsafe(no_mangle)]
2896pub unsafe extern "C" fn lumen_node_classes(
2897 node: LumenNode,
2898 out: *mut LumenStrList,
2899) -> LumenStatus {
2900 catch(|| {
2901 if out.is_null() {
2902 set_last_error("lumen_node_classes: null out");
2903 return LumenStatus::ErrBadArg;
2904 }
2905 unsafe { *out = strlist_from(lumen_script::introspect::node_classes(node)) };
2906 LumenStatus::Ok
2907 })
2908}
2909
2910#[unsafe(no_mangle)]
2913pub unsafe extern "C" fn lumen_node_components(
2914 node: LumenNode,
2915 out: *mut LumenStrList,
2916) -> LumenStatus {
2917 catch(|| {
2918 if out.is_null() {
2919 set_last_error("lumen_node_components: null out");
2920 return LumenStatus::ErrBadArg;
2921 }
2922 unsafe { *out = strlist_from(lumen_script::introspect::node_components(node)) };
2923 LumenStatus::Ok
2924 })
2925}
2926
2927#[unsafe(no_mangle)]
2930pub unsafe extern "C" fn lumen_node_outer_markup(
2931 node: LumenNode,
2932 out: *mut *mut c_char,
2933) -> LumenStatus {
2934 catch(|| {
2935 if out.is_null() {
2936 set_last_error("lumen_node_outer_markup: null out");
2937 return LumenStatus::ErrBadArg;
2938 }
2939 unsafe { *out = owned_cstring(&lumen_script::introspect::outer_markup(node)) };
2940 LumenStatus::Ok
2941 })
2942}
2943
2944#[unsafe(no_mangle)]
2948pub unsafe extern "C" fn lumen_node_inner_markup(
2949 node: LumenNode,
2950 out: *mut *mut c_char,
2951) -> LumenStatus {
2952 catch(|| {
2953 if out.is_null() {
2954 set_last_error("lumen_node_inner_markup: null out");
2955 return LumenStatus::ErrBadArg;
2956 }
2957 unsafe { *out = owned_cstring(&lumen_script::introspect::inner_markup(node)) };
2958 LumenStatus::Ok
2959 })
2960}
2961
2962#[unsafe(no_mangle)]
2965pub unsafe extern "C" fn lumen_dump_tree(out: *mut *mut c_char) -> LumenStatus {
2966 catch(|| {
2967 if out.is_null() {
2968 set_last_error("lumen_dump_tree: null out");
2969 return LumenStatus::ErrBadArg;
2970 }
2971 unsafe { *out = owned_cstring(&lumen_script::introspect::dump_tree()) };
2972 LumenStatus::Ok
2973 })
2974}
2975
2976#[unsafe(no_mangle)]
2978pub unsafe extern "C" fn lumen_pointer_state(out: *mut LumenPointerState) -> LumenStatus {
2979 catch(|| {
2980 if out.is_null() {
2981 set_last_error("lumen_pointer_state: null out");
2982 return LumenStatus::ErrBadArg;
2983 }
2984 let p = lumen_script::introspect::pointer_state();
2985 unsafe {
2986 *out = LumenPointerState {
2987 x: p.x as f64,
2988 y: p.y as f64,
2989 inside: c_int::from(p.inside),
2990 buttons: p.buttons,
2991 shift: c_int::from(p.shift),
2992 ctrl: c_int::from(p.ctrl),
2993 alt: c_int::from(p.alt),
2994 super_: c_int::from(p.super_),
2995 }
2996 };
2997 LumenStatus::Ok
2998 })
2999}
3000
3001#[unsafe(no_mangle)]
3003pub unsafe extern "C" fn lumen_frame_info(out: *mut LumenFrameInfo) -> LumenStatus {
3004 catch(|| {
3005 if out.is_null() {
3006 set_last_error("lumen_frame_info: null out");
3007 return LumenStatus::ErrBadArg;
3008 }
3009 let f = lumen_script::introspect::frame_info();
3010 unsafe {
3011 *out = LumenFrameInfo {
3012 frame: f.frame,
3013 dt_ms: f.dt_ms,
3014 dirty_count: f.dirty_count,
3015 }
3016 };
3017 LumenStatus::Ok
3018 })
3019}
3020
3021#[unsafe(no_mangle)]
3024pub unsafe extern "C" fn lumen_signals_all(out: *mut LumenKVList) -> LumenStatus {
3025 catch(|| {
3026 if out.is_null() {
3027 set_last_error("lumen_signals_all: null out");
3028 return LumenStatus::ErrBadArg;
3029 }
3030 unsafe { *out = kvlist_from(lumen_script::introspect::signals_all()) };
3031 LumenStatus::Ok
3032 })
3033}
3034
3035#[unsafe(no_mangle)]
3037pub unsafe extern "C" fn lumen_kvlist_free(list: LumenKVList) {
3038 if list.ptr.is_null() || list.len == 0 {
3039 return;
3040 }
3041 unsafe {
3042 let slice = Box::from_raw(std::ptr::slice_from_raw_parts_mut(list.ptr, list.len));
3043 for kv in slice.iter() {
3044 if !kv.key.is_null() {
3045 drop(CString::from_raw(kv.key));
3046 }
3047 if !kv.value.is_null() {
3048 drop(CString::from_raw(kv.value));
3049 }
3050 }
3051 }
3052}
3053
3054#[unsafe(no_mangle)]
3056pub unsafe extern "C" fn lumen_strlist_free(list: LumenStrList) {
3057 if list.ptr.is_null() || list.len == 0 {
3058 return;
3059 }
3060 unsafe {
3061 let slice = Box::from_raw(std::ptr::slice_from_raw_parts_mut(list.ptr, list.len));
3062 for s in slice.iter() {
3063 if !s.is_null() {
3064 drop(CString::from_raw(*s));
3065 }
3066 }
3067 }
3068}
3069
3070#[unsafe(no_mangle)]
3072pub unsafe extern "C" fn lumen_string_free(s: *mut c_char) {
3073 if s.is_null() {
3074 return;
3075 }
3076 unsafe { drop(CString::from_raw(s)) };
3077}
3078
3079fn push_dom(cmd: lumen_script::ScriptCommand) -> LumenStatus {
3093 lumen_script::node_query::push_external_dom_command(cmd);
3094 LumenStatus::Ok
3095}
3096
3097#[unsafe(no_mangle)]
3101pub unsafe extern "C" fn lumen_node_set_attr(
3102 node: LumenNode,
3103 name: *const c_char,
3104 value: *const c_char,
3105) -> LumenStatus {
3106 catch(|| {
3107 let name = match ffi_selector(name, "lumen_node_set_attr") {
3108 Ok(s) => s,
3109 Err(status) => return status,
3110 };
3111 let value = match ffi_selector(value, "lumen_node_set_attr") {
3112 Ok(s) => s,
3113 Err(status) => return status,
3114 };
3115 push_dom(lumen_script::ScriptCommand::SetAttr { node, name, value })
3116 })
3117}
3118
3119#[unsafe(no_mangle)]
3121pub unsafe extern "C" fn lumen_node_remove_attr(
3122 node: LumenNode,
3123 name: *const c_char,
3124) -> LumenStatus {
3125 catch(|| {
3126 let name = match ffi_selector(name, "lumen_node_remove_attr") {
3127 Ok(s) => s,
3128 Err(status) => return status,
3129 };
3130 push_dom(lumen_script::ScriptCommand::RemoveAttr { node, name })
3131 })
3132}
3133
3134#[unsafe(no_mangle)]
3136pub unsafe extern "C" fn lumen_node_set_text(node: LumenNode, text: *const c_char) -> LumenStatus {
3137 catch(|| {
3138 let text = match ffi_selector(text, "lumen_node_set_text") {
3139 Ok(s) => s,
3140 Err(status) => return status,
3141 };
3142 push_dom(lumen_script::ScriptCommand::SetNodeText { node, text })
3143 })
3144}
3145
3146#[unsafe(no_mangle)]
3152pub unsafe extern "C" fn lumen_node_set_inner_markup(
3153 node: LumenNode,
3154 markup: *const c_char,
3155) -> LumenStatus {
3156 catch(|| {
3157 let markup = match ffi_selector(markup, "lumen_node_set_inner_markup") {
3158 Ok(s) => s,
3159 Err(status) => return status,
3160 };
3161 push_dom(lumen_script::ScriptCommand::SetInnerMarkup { node, markup })
3162 })
3163}
3164
3165#[unsafe(no_mangle)]
3167pub unsafe extern "C" fn lumen_node_class_add(
3168 node: LumenNode,
3169 class: *const c_char,
3170) -> LumenStatus {
3171 catch(|| {
3172 let class = match ffi_selector(class, "lumen_node_class_add") {
3173 Ok(s) => s,
3174 Err(status) => return status,
3175 };
3176 push_dom(lumen_script::ScriptCommand::ClassAdd { node, class })
3177 })
3178}
3179
3180#[unsafe(no_mangle)]
3182pub unsafe extern "C" fn lumen_node_class_remove(
3183 node: LumenNode,
3184 class: *const c_char,
3185) -> LumenStatus {
3186 catch(|| {
3187 let class = match ffi_selector(class, "lumen_node_class_remove") {
3188 Ok(s) => s,
3189 Err(status) => return status,
3190 };
3191 push_dom(lumen_script::ScriptCommand::ClassRemove { node, class })
3192 })
3193}
3194
3195#[unsafe(no_mangle)]
3197pub unsafe extern "C" fn lumen_node_class_toggle(
3198 node: LumenNode,
3199 class: *const c_char,
3200) -> LumenStatus {
3201 catch(|| {
3202 let class = match ffi_selector(class, "lumen_node_class_toggle") {
3203 Ok(s) => s,
3204 Err(status) => return status,
3205 };
3206 push_dom(lumen_script::ScriptCommand::ClassToggle { node, class })
3207 })
3208}
3209
3210#[unsafe(no_mangle)]
3212pub unsafe extern "C" fn lumen_node_set_style(
3213 node: LumenNode,
3214 name: *const c_char,
3215 value: *const c_char,
3216) -> LumenStatus {
3217 catch(|| {
3218 let name = match ffi_selector(name, "lumen_node_set_style") {
3219 Ok(s) => s,
3220 Err(status) => return status,
3221 };
3222 let value = match ffi_selector(value, "lumen_node_set_style") {
3223 Ok(s) => s,
3224 Err(status) => return status,
3225 };
3226 push_dom(lumen_script::ScriptCommand::SetStyleProp { node, name, value })
3227 })
3228}
3229
3230#[unsafe(no_mangle)]
3232pub unsafe extern "C" fn lumen_node_remove_style(
3233 node: LumenNode,
3234 name: *const c_char,
3235) -> LumenStatus {
3236 catch(|| {
3237 let name = match ffi_selector(name, "lumen_node_remove_style") {
3238 Ok(s) => s,
3239 Err(status) => return status,
3240 };
3241 push_dom(lumen_script::ScriptCommand::RemoveStyleProp { node, name })
3242 })
3243}
3244
3245#[unsafe(no_mangle)]
3249pub unsafe extern "C" fn lumen_node_spawn(tag: *const c_char, out: *mut LumenNode) -> LumenStatus {
3250 catch(|| {
3251 if out.is_null() {
3252 set_last_error("lumen_node_spawn: null out");
3253 return LumenStatus::ErrBadArg;
3254 }
3255 let tag = match ffi_selector(tag, "lumen_node_spawn") {
3256 Ok(s) => s,
3257 Err(status) => return status,
3258 };
3259 let (handle, cmd) = lumen_script::node_query::build_spawn(&tag);
3260 unsafe { *out = handle };
3261 push_dom(cmd)
3262 })
3263}
3264
3265#[unsafe(no_mangle)]
3268pub unsafe extern "C" fn lumen_node_clone(source: LumenNode, out: *mut LumenNode) -> LumenStatus {
3269 catch(|| {
3270 if out.is_null() {
3271 set_last_error("lumen_node_clone: null out");
3272 return LumenStatus::ErrBadArg;
3273 }
3274 let (handle, cmd) = lumen_script::node_query::build_clone(source);
3275 unsafe { *out = handle };
3276 push_dom(cmd)
3277 })
3278}
3279
3280#[unsafe(no_mangle)]
3282pub unsafe extern "C" fn lumen_node_append(parent: LumenNode, child: LumenNode) -> LumenStatus {
3283 catch(|| {
3284 push_dom(lumen_script::ScriptCommand::Insert {
3285 parent,
3286 node: child,
3287 before: 0,
3288 })
3289 })
3290}
3291
3292#[unsafe(no_mangle)]
3295pub unsafe extern "C" fn lumen_node_insert_before(
3296 parent: LumenNode,
3297 child: LumenNode,
3298 reference: LumenNode,
3299) -> LumenStatus {
3300 catch(|| {
3301 push_dom(lumen_script::ScriptCommand::Insert {
3302 parent,
3303 node: child,
3304 before: reference,
3305 })
3306 })
3307}
3308
3309#[unsafe(no_mangle)]
3311pub unsafe extern "C" fn lumen_node_set_parent(node: LumenNode, parent: LumenNode) -> LumenStatus {
3312 catch(|| {
3313 push_dom(lumen_script::ScriptCommand::Insert {
3314 parent,
3315 node,
3316 before: 0,
3317 })
3318 })
3319}
3320
3321#[unsafe(no_mangle)]
3324pub unsafe extern "C" fn lumen_node_replace_with(old: LumenNode, new: LumenNode) -> LumenStatus {
3325 catch(|| push_dom(lumen_script::ScriptCommand::ReplaceWith { old, new }))
3326}
3327
3328#[unsafe(no_mangle)]
3330pub unsafe extern "C" fn lumen_node_remove(node: LumenNode) -> LumenStatus {
3331 catch(|| push_dom(lumen_script::ScriptCommand::RemoveNode { node }))
3332}
3333
3334#[unsafe(no_mangle)]
3337pub unsafe extern "C" fn lumen_window_set_href(path: *const c_char) -> LumenStatus {
3338 catch(|| {
3339 let path = match ffi_selector(path, "lumen_window_set_href") {
3340 Ok(s) => s,
3341 Err(status) => return status,
3342 };
3343 lumen_core::nav::navigate(path);
3344 LumenStatus::Ok
3345 })
3346}
3347
3348#[unsafe(no_mangle)]
3350pub unsafe extern "C" fn lumen_window_reload() -> LumenStatus {
3351 catch(|| {
3352 lumen_core::nav::navigate(lumen_core::nav::current());
3353 LumenStatus::Ok
3354 })
3355}
3356
3357#[unsafe(no_mangle)]
3359pub unsafe extern "C" fn lumen_window_set_title(title: *const c_char) -> LumenStatus {
3360 catch(|| {
3361 let title = match ffi_selector(title, "lumen_window_set_title") {
3362 Ok(s) => s,
3363 Err(status) => return status,
3364 };
3365 push_dom(lumen_script::ScriptCommand::WindowSetTitle { title })
3366 })
3367}
3368
3369#[unsafe(no_mangle)]
3371pub unsafe extern "C" fn lumen_window_set_size(width: f32, height: f32) -> LumenStatus {
3372 catch(|| push_dom(lumen_script::ScriptCommand::WindowSetSize { width, height }))
3373}
3374
3375#[unsafe(no_mangle)]
3378pub unsafe extern "C" fn lumen_window_dpr(out: *mut f32) -> LumenStatus {
3379 catch(|| {
3380 if out.is_null() {
3381 set_last_error("lumen_window_dpr: null out");
3382 return LumenStatus::ErrBadArg;
3383 }
3384 unsafe { *out = lumen_core::window_state::dpr() };
3385 LumenStatus::Ok
3386 })
3387}
3388
3389#[unsafe(no_mangle)]
3392pub unsafe extern "C" fn lumen_history_go(delta: c_int) -> LumenStatus {
3393 catch(|| {
3394 for _ in 0..delta.unsigned_abs() {
3395 if delta < 0 {
3396 lumen_core::nav::back();
3397 } else {
3398 lumen_core::nav::forward();
3399 }
3400 }
3401 LumenStatus::Ok
3402 })
3403}
3404
3405#[unsafe(no_mangle)]
3408pub unsafe extern "C" fn lumen_document_spawn(
3409 tag: *const c_char,
3410 out: *mut LumenNode,
3411) -> LumenStatus {
3412 catch(|| {
3413 if out.is_null() {
3414 set_last_error("lumen_document_spawn: null out");
3415 return LumenStatus::ErrBadArg;
3416 }
3417 let tag = match ffi_selector(tag, "lumen_document_spawn") {
3418 Ok(s) => s,
3419 Err(status) => return status,
3420 };
3421 let (handle, cmd) = lumen_script::node_query::build_spawn(&tag);
3422 unsafe { *out = handle };
3423 push_dom(cmd)
3424 })
3425}
3426
3427pub type LumenEventToken = u64;
3442
3443#[repr(C)]
3448pub struct LumenEvent {
3449 pub target: LumenNode,
3451 pub current_target: LumenNode,
3453 pub local_x: f64,
3455 pub local_y: f64,
3457 pub client_x: f64,
3459 pub client_y: f64,
3461 pub delta_x: f64,
3463 pub delta_y: f64,
3465 pub button: i64,
3467 pub shift: u8,
3469 pub ctrl: u8,
3471 pub alt: u8,
3473 pub super_: u8,
3475}
3476
3477pub type LumenEventFn = unsafe extern "C" fn(event: *const LumenEvent, user_data: *mut c_void);
3481
3482struct CEventCallback {
3486 callback: LumenEventFn,
3487 user_data: *mut c_void,
3488}
3489
3490unsafe impl Send for CEventCallback {}
3494unsafe impl Sync for CEventCallback {}
3495
3496fn current_lumen_event() -> LumenEvent {
3498 use lumen_script::event;
3499 let (lx, ly) = event::event_position_local();
3500 let (cx, cy) = event::event_position_client();
3501 let (dx, dy) = event::event_delta();
3502 let (shift, ctrl, alt, super_) = event::event_modifiers();
3503 LumenEvent {
3504 target: event::event_target(),
3505 current_target: event::event_current_target(),
3506 local_x: lx,
3507 local_y: ly,
3508 client_x: cx,
3509 client_y: cy,
3510 delta_x: dx,
3511 delta_y: dy,
3512 button: event::event_button(),
3513 shift: shift as u8,
3514 ctrl: ctrl as u8,
3515 alt: alt as u8,
3516 super_: super_ as u8,
3517 }
3518}
3519
3520#[unsafe(no_mangle)]
3524pub unsafe extern "C" fn lumen_on(
3525 node: LumenNode,
3526 event_type: *const c_char,
3527 capture: c_int,
3528 callback: Option<LumenEventFn>,
3529 user_data: *mut c_void,
3530) -> LumenEventToken {
3531 catch_val(0u64, || {
3532 let Some(callback) = callback else {
3533 set_last_error("lumen_on: null callback");
3534 return 0;
3535 };
3536 let etype = match ffi_selector(event_type, "lumen_on") {
3537 Ok(s) => s,
3538 Err(_) => return 0,
3539 };
3540 let cb = CEventCallback {
3541 callback,
3542 user_data,
3543 };
3544 let invoke: std::sync::Arc<dyn Fn() + Send + Sync> = std::sync::Arc::new(move || {
3545 let cb = &cb;
3549 let ev = current_lumen_event();
3550 unsafe { (cb.callback)(&ev, cb.user_data) };
3552 });
3553 lumen_script::event::register_native_binding(node, etype, capture != 0, invoke)
3554 })
3555}
3556
3557#[unsafe(no_mangle)]
3560pub extern "C" fn lumen_off(token: LumenEventToken) -> LumenStatus {
3561 catch(|| {
3562 lumen_script::event::unregister_binding(token);
3563 LumenStatus::Ok
3564 })
3565}
3566
3567#[unsafe(no_mangle)]
3572pub unsafe extern "C" fn lumen_event_type(
3573 buf: *mut c_char,
3574 buf_len: usize,
3575 out_len: *mut usize,
3576) -> LumenStatus {
3577 catch(|| write_string_out(&lumen_script::event::event_type(), buf, buf_len, out_len))
3578}
3579
3580#[unsafe(no_mangle)]
3583pub unsafe extern "C" fn lumen_event_key(
3584 buf: *mut c_char,
3585 buf_len: usize,
3586 out_len: *mut usize,
3587) -> LumenStatus {
3588 catch(|| write_string_out(&lumen_script::event::event_key(), buf, buf_len, out_len))
3589}
3590
3591#[unsafe(no_mangle)]
3594pub unsafe extern "C" fn lumen_event_value(
3595 buf: *mut c_char,
3596 buf_len: usize,
3597 out_len: *mut usize,
3598) -> LumenStatus {
3599 catch(|| write_string_out(&lumen_script::event::event_value(), buf, buf_len, out_len))
3600}
3601
3602#[unsafe(no_mangle)]
3605pub extern "C" fn lumen_event_target() -> LumenNode {
3606 catch_val(0u64, lumen_script::event::event_target)
3607}
3608
3609#[unsafe(no_mangle)]
3611pub extern "C" fn lumen_event_current_target() -> LumenNode {
3612 catch_val(0u64, lumen_script::event::event_current_target)
3613}
3614
3615#[unsafe(no_mangle)]
3618pub extern "C" fn lumen_event_prevent_default() -> LumenStatus {
3619 catch(|| {
3620 lumen_script::event::event_prevent_default();
3621 LumenStatus::Ok
3622 })
3623}
3624
3625#[unsafe(no_mangle)]
3627pub extern "C" fn lumen_event_stop_propagation() -> LumenStatus {
3628 catch(|| {
3629 lumen_script::event::event_stop_propagation();
3630 LumenStatus::Ok
3631 })
3632}
3633
3634#[unsafe(no_mangle)]
3637pub extern "C" fn lumen_event_stop_immediate_propagation() -> LumenStatus {
3638 catch(|| {
3639 lumen_script::event::event_stop_immediate_propagation();
3640 LumenStatus::Ok
3641 })
3642}
3643
3644#[cfg(test)]
3645mod tests {
3646 use super::*;
3647
3648 static SIGNAL_STATE: Mutex<()> = Mutex::new(());
3654
3655 #[test]
3656 fn abi_version_is_packed_correctly() {
3657 let v = lumen_abi_version();
3658 assert_eq!(v >> 16, LUMEN_ABI_MAJOR);
3659 assert_eq!((v >> 8) & 0xff, LUMEN_ABI_MINOR);
3660 assert_eq!(v & 0xff, LUMEN_ABI_PATCH);
3661 }
3662
3663 #[test]
3664 fn user_data_round_trips() {
3665 let mut x = 7u32;
3666 let raw = &mut x as *mut u32 as *mut c_void;
3667 let ud = UserData::from_raw(raw);
3668 assert_eq!(ud.as_ptr(), raw);
3669 let null = UserData::from_raw(ptr::null_mut());
3670 assert!(null.as_ptr().is_null());
3671 }
3672
3673 #[test]
3674 fn last_error_thread_local_then_global() {
3675 set_last_error("test message");
3676 let p = unsafe { lumen_last_error() };
3677 assert!(!p.is_null());
3678 let s = unsafe { CStr::from_ptr(p) }.to_string_lossy().into_owned();
3679 assert_eq!(s, "test message");
3680
3681 let handle = std::thread::spawn(|| {
3683 let p = unsafe { lumen_last_error() };
3684 assert!(
3685 !p.is_null(),
3686 "global fallback should surface the prior thread's error"
3687 );
3688 let s = unsafe { CStr::from_ptr(p) }.to_string_lossy().into_owned();
3689 assert_eq!(s, "test message");
3690 });
3691 handle.join().unwrap();
3692 }
3693
3694 #[test]
3695 fn classify_runtime_error_picks_specific_variant() {
3696 assert_eq!(
3697 classify_runtime_error("css parse failed"),
3698 LumenStatus::ErrCss
3699 );
3700 assert_eq!(
3701 classify_runtime_error("HTML XML error"),
3702 LumenStatus::ErrParse
3703 );
3704 assert_eq!(
3705 classify_runtime_error("asset PNG decode failed"),
3706 LumenStatus::ErrAsset
3707 );
3708 assert_eq!(
3709 classify_runtime_error("window surface create"),
3710 LumenStatus::ErrWindow
3711 );
3712 assert_eq!(
3713 classify_runtime_error("rhai script error"),
3714 LumenStatus::ErrScript
3715 );
3716 assert_eq!(classify_runtime_error("file io"), LumenStatus::ErrIo);
3717 assert_eq!(
3718 classify_runtime_error("completely opaque thing"),
3719 LumenStatus::ErrRuntime
3720 );
3721 }
3722
3723 #[test]
3724 fn typed_signal_int64_round_trips() {
3725 let _serial = SIGNAL_STATE.lock().unwrap_or_else(|e| e.into_inner());
3726 unsafe {
3727 let name = CString::new("typed_int_test").unwrap();
3728 assert_eq!(lumen_signal_set_int64(name.as_ptr(), 1234), LumenStatus::Ok);
3729 let mut out: i64 = 0;
3730 assert_eq!(
3731 lumen_signal_get_int64(name.as_ptr(), &mut out),
3732 LumenStatus::Ok
3733 );
3734 assert_eq!(out, 1234);
3735 }
3736 }
3737
3738 #[test]
3739 fn typed_signal_float64_round_trips() {
3740 let _serial = SIGNAL_STATE.lock().unwrap_or_else(|e| e.into_inner());
3741 unsafe {
3742 let name = CString::new("typed_float_test").unwrap();
3743 assert_eq!(
3744 lumen_signal_set_float64(name.as_ptr(), 2.5),
3745 LumenStatus::Ok
3746 );
3747 let mut out: f64 = 0.0;
3748 assert_eq!(
3749 lumen_signal_get_float64(name.as_ptr(), &mut out),
3750 LumenStatus::Ok
3751 );
3752 assert_eq!(out, 2.5);
3753 }
3754 }
3755
3756 #[test]
3757 fn typed_signal_bool_round_trips() {
3758 let _serial = SIGNAL_STATE.lock().unwrap_or_else(|e| e.into_inner());
3759 unsafe {
3760 let name = CString::new("typed_bool_test").unwrap();
3761 assert_eq!(lumen_signal_set_bool(name.as_ptr(), true), LumenStatus::Ok);
3762 let mut out = false;
3763 assert_eq!(
3764 lumen_signal_get_bool(name.as_ptr(), &mut out),
3765 LumenStatus::Ok
3766 );
3767 assert!(out);
3768 }
3769 }
3770
3771 #[test]
3772 fn typed_signal_color_round_trips() {
3773 let _serial = SIGNAL_STATE.lock().unwrap_or_else(|e| e.into_inner());
3774 unsafe {
3775 let name = CString::new("typed_color_test").unwrap();
3776 let rgba = [0xffu8, 0x88, 0x00, 0xff];
3777 assert_eq!(
3778 lumen_signal_set_color(name.as_ptr(), rgba.as_ptr()),
3779 LumenStatus::Ok
3780 );
3781 let mut out = [0u8; 4];
3782 assert_eq!(
3783 lumen_signal_get_color(name.as_ptr(), out.as_mut_ptr()),
3784 LumenStatus::Ok
3785 );
3786 assert_eq!(out, rgba);
3787 }
3788 }
3789
3790 #[test]
3791 fn typed_signal_get_missing_returns_err_bad_arg() {
3792 let _serial = SIGNAL_STATE.lock().unwrap_or_else(|e| e.into_inner());
3793 unsafe {
3794 let name = CString::new("never_set_typed").unwrap();
3795 let mut out: i64 = -1;
3796 let s = lumen_signal_get_int64(name.as_ptr(), &mut out);
3797 assert_eq!(s, LumenStatus::ErrBadArg);
3798 }
3799 }
3800
3801 #[test]
3802 fn lumen_status_message_returns_known_strings() {
3803 unsafe {
3804 let p = lumen_status_message(LumenStatus::Ok);
3805 assert!(!p.is_null());
3806 let s = CStr::from_ptr(p).to_str().unwrap();
3807 assert_eq!(s, "ok");
3808 let p2 = lumen_status_message(LumenStatus::ErrBadArg);
3809 let s2 = CStr::from_ptr(p2).to_str().unwrap();
3810 assert!(s2.contains("argument"));
3811 }
3812 }
3813
3814 #[test]
3815 fn typed_setter_routes_through_external_property_bus() {
3816 let _serial = SIGNAL_STATE.lock().unwrap_or_else(|e| e.into_inner());
3817 use lumen_core::prelude::Schedule;
3826 use lumen_core::prelude::World;
3827 use lumen_core::property_store::{
3828 PropertyKey, PropertyStore, PropertyValue, drain_external_properties,
3829 init_external_properties,
3830 };
3831 init_external_properties();
3832 unsafe {
3833 let name = CString::new("ffi_pre_run_int").unwrap();
3834 assert_eq!(lumen_signal_set_int64(name.as_ptr(), 7777), LumenStatus::Ok);
3835 let name = CString::new("ffi_pre_run_str").unwrap();
3836 let value = CString::new("seeded").unwrap();
3837 assert_eq!(
3838 lumen_signal_set_str(name.as_ptr(), value.as_ptr()),
3839 LumenStatus::Ok
3840 );
3841 }
3842 let mut world = World::new();
3846 world.insert_resource(PropertyStore::default());
3847 let mut schedule = Schedule::default();
3848 schedule.add_systems(drain_external_properties);
3849 schedule.run(&mut world);
3850 let store = world.resource::<PropertyStore>();
3851 let cell = store.get(&PropertyKey::Global(Arc::<str>::from("ffi_pre_run_int")));
3852 assert!(
3853 matches!(cell, Some(PropertyValue::I64(7777))),
3854 "typed FFI setter must land a PropertyValue::I64 in PropertyStore; got {cell:?}"
3855 );
3856 let cell = store.get(&PropertyKey::Global(Arc::<str>::from("ffi_pre_run_str")));
3857 assert!(
3858 matches!(cell, Some(PropertyValue::Str(s)) if &**s == "seeded"),
3859 "the string setter must land a PropertyValue::Str in PropertyStore; got {cell:?}"
3860 );
3861 }
3862
3863 #[test]
3864 fn typed_read_falls_back_to_local_cache_when_bus_drained() {
3865 let _serial = SIGNAL_STATE.lock().unwrap_or_else(|e| e.into_inner());
3866 unsafe {
3872 let name = CString::new("ffi_cache_fallback_bool").unwrap();
3873 assert_eq!(lumen_signal_set_bool(name.as_ptr(), true), LumenStatus::Ok);
3874 }
3875 let _ = lumen_core::property_store::external_property_snapshot();
3879 unsafe {
3880 let name = CString::new("ffi_cache_fallback_bool").unwrap();
3881 let mut out = false;
3882 assert_eq!(
3883 lumen_signal_get_bool(name.as_ptr(), &mut out),
3884 LumenStatus::Ok
3885 );
3886 assert!(out, "typed read should fall back to cache after bus drain");
3887 }
3888 }
3889
3890 extern "C" fn noop_watch(_name: *const c_char, _value: *const LumenValue, _ud: *mut c_void) {}
3891
3892 #[test]
3893 fn signal_watch_registers_additively() {
3894 let _serial = SIGNAL_STATE.lock().unwrap_or_else(|e| e.into_inner());
3895 unsafe {
3901 let name = CString::new("watch_reg_test").unwrap();
3902 let cb: LumenWatchFn = noop_watch;
3903 assert_eq!(
3904 lumen_signal_watch(name.as_ptr(), Some(cb), ptr::null_mut()),
3905 LumenStatus::Ok
3906 );
3907 assert_eq!(
3909 lumen_signal_watch(name.as_ptr(), Some(cb), ptr::null_mut()),
3910 LumenStatus::Ok
3911 );
3912 }
3913 let reg = signal_watchers().lock().unwrap();
3915 assert_eq!(reg.get("watch_reg_test").map(Vec::len), Some(2));
3916 }
3917
3918 #[test]
3919 fn property_to_lumen_encodes_each_variant() {
3920 let mut keep: Option<CString> = None;
3921 assert_eq!(
3922 property_to_lumen(&PropertyValue::I64(7), &mut keep).kind,
3923 LumenKind::Int
3924 );
3925 assert_eq!(
3926 property_to_lumen(&PropertyValue::Bool(true), &mut keep).kind,
3927 LumenKind::Bool
3928 );
3929 assert_eq!(
3930 property_to_lumen(&PropertyValue::F64(1.5), &mut keep).kind,
3931 LumenKind::Float
3932 );
3933 let s = property_to_lumen(&PropertyValue::Str(Arc::<str>::from("hi")), &mut keep);
3934 assert_eq!(s.kind, LumenKind::String);
3935 assert!(keep.is_some());
3936 let c = property_to_lumen(
3938 &PropertyValue::Color(Color::rgba(1.0, 0.0, 0.0, 1.0)),
3939 &mut keep,
3940 );
3941 assert_eq!(c.kind, LumenKind::Int);
3942 assert_eq!(unsafe { c.as_.integer } & 0xff, 0xff); assert_eq!((unsafe { c.as_.integer } >> 24) & 0xff, 0xff); }
3945
3946 #[test]
3947 fn status_codes_are_stable() {
3948 assert_eq!(LumenStatus::Ok as u32, 0);
3951 assert_eq!(LumenStatus::ErrBadPath as u32, 1);
3952 assert_eq!(LumenStatus::ErrBadArg as u32, 2);
3953 assert_eq!(LumenStatus::ErrRuntime as u32, 3);
3954 assert_eq!(LumenStatus::ErrInternal as u32, 4);
3955 assert_eq!(LumenStatus::ErrParse as u32, 5);
3956 assert_eq!(LumenStatus::ErrCss as u32, 6);
3957 assert_eq!(LumenStatus::ErrAsset as u32, 7);
3958 assert_eq!(LumenStatus::ErrWindow as u32, 8);
3959 assert_eq!(LumenStatus::ErrScript as u32, 9);
3960 assert_eq!(LumenStatus::ErrIo as u32, 10);
3961 assert_eq!(LumenStatus::ErrInvalidHandle as u32, 11);
3962 assert_eq!(LumenStatus::ErrInvalidValue as u32, 12);
3963 assert_eq!(LumenStatus::ErrPanic as u32, 13);
3964 assert_eq!(LumenStatus::ErrBufferTooSmall as u32, 14);
3965 }
3966
3967 #[test]
3968 fn typed_signal_str_round_trips() {
3969 let _serial = SIGNAL_STATE.lock().unwrap_or_else(|e| e.into_inner());
3970 unsafe {
3971 let name = CString::new("ffi_get_str_test").unwrap();
3972 let value = CString::new("hello world").unwrap();
3973 assert_eq!(
3974 lumen_signal_set_str(name.as_ptr(), value.as_ptr()),
3975 LumenStatus::Ok
3976 );
3977 let mut needed: usize = 0;
3979 assert_eq!(
3980 lumen_signal_get_str(name.as_ptr(), ptr::null_mut(), 0, &mut needed),
3981 LumenStatus::ErrBufferTooSmall
3982 );
3983 assert_eq!(needed, "hello world".len() + 1);
3984 let mut buf = vec![0i8; needed];
3986 let mut out_len: usize = 0;
3987 assert_eq!(
3988 lumen_signal_get_str(name.as_ptr(), buf.as_mut_ptr(), buf.len(), &mut out_len),
3989 LumenStatus::Ok
3990 );
3991 assert_eq!(out_len, "hello world".len());
3992 let got = CStr::from_ptr(buf.as_ptr()).to_str().unwrap();
3993 assert_eq!(got, "hello world");
3994 }
3995 }
3996
3997 #[test]
3998 fn clear_leaves_an_empty_string_signal() {
3999 let _serial = SIGNAL_STATE.lock().unwrap_or_else(|e| e.into_inner());
4000 unsafe {
4001 let name = CString::new("ffi_clear_str_test").unwrap();
4002 let value = CString::new("filled").unwrap();
4003 assert_eq!(
4004 lumen_signal_set_str(name.as_ptr(), value.as_ptr()),
4005 LumenStatus::Ok
4006 );
4007 assert_eq!(lumen_signal_clear(name.as_ptr()), LumenStatus::Ok);
4008 let mut out_len: usize = 0;
4009 let mut buf = [0i8; 8];
4010 assert_eq!(
4011 lumen_signal_get_str(name.as_ptr(), buf.as_mut_ptr(), buf.len(), &mut out_len),
4012 LumenStatus::Ok
4013 );
4014 assert_eq!(out_len, 0);
4015 }
4016 }
4017
4018 #[test]
4019 fn typed_getters_reject_a_type_mismatch() {
4020 let _serial = SIGNAL_STATE.lock().unwrap_or_else(|e| e.into_inner());
4021 unsafe {
4022 let name = CString::new("ffi_mismatch_test").unwrap();
4023 let value = CString::new("not a number").unwrap();
4024 assert_eq!(
4025 lumen_signal_set_str(name.as_ptr(), value.as_ptr()),
4026 LumenStatus::Ok
4027 );
4028 let mut out: i64 = 0;
4029 assert_eq!(
4030 lumen_signal_get_int64(name.as_ptr(), &mut out),
4031 LumenStatus::ErrBadArg
4032 );
4033 }
4034 }
4035
4036 #[test]
4037 fn array_signal_len_and_field_round_trip() {
4038 let _serial = SIGNAL_STATE.lock().unwrap_or_else(|e| e.into_inner());
4039 unsafe {
4040 let key = CString::new("name").unwrap();
4042 let va = CString::new("a").unwrap();
4043 let vb = CString::new("b").unwrap();
4044 let row0 = [LumenMapEntry {
4045 key: key.as_ptr(),
4046 value: LumenValue {
4047 kind: LumenKind::String,
4048 as_: LumenValueData {
4049 string: va.as_ptr(),
4050 },
4051 },
4052 }];
4053 let row1 = [LumenMapEntry {
4054 key: key.as_ptr(),
4055 value: LumenValue {
4056 kind: LumenKind::String,
4057 as_: LumenValueData {
4058 string: vb.as_ptr(),
4059 },
4060 },
4061 }];
4062 let items = [
4063 LumenValue {
4064 kind: LumenKind::Map,
4065 as_: LumenValueData {
4066 map: LumenMapView {
4067 entries: row0.as_ptr(),
4068 len: 1,
4069 },
4070 },
4071 },
4072 LumenValue {
4073 kind: LumenKind::Map,
4074 as_: LumenValueData {
4075 map: LumenMapView {
4076 entries: row1.as_ptr(),
4077 len: 1,
4078 },
4079 },
4080 },
4081 ];
4082 let arr = LumenValue {
4083 kind: LumenKind::Array,
4084 as_: LumenValueData {
4085 array: LumenArrayView {
4086 items: items.as_ptr(),
4087 len: 2,
4088 },
4089 },
4090 };
4091 let name = CString::new("ffi_array_test").unwrap();
4092 assert_eq!(lumen_signal_set_array(name.as_ptr(), &arr), LumenStatus::Ok);
4093
4094 let mut len: usize = 0;
4095 assert_eq!(
4096 lumen_signal_array_len(name.as_ptr(), &mut len),
4097 LumenStatus::Ok
4098 );
4099 assert_eq!(len, 2);
4100
4101 let mut buf = [0i8; 8];
4102 let mut out_len: usize = 0;
4103 assert_eq!(
4104 lumen_signal_array_get_field(
4105 name.as_ptr(),
4106 1,
4107 key.as_ptr(),
4108 buf.as_mut_ptr(),
4109 buf.len(),
4110 &mut out_len
4111 ),
4112 LumenStatus::Ok
4113 );
4114 assert_eq!(CStr::from_ptr(buf.as_ptr()).to_str().unwrap(), "b");
4115 }
4116 }
4117}