1#![allow(clippy::missing_safety_doc)]
28
29#[cfg(all(feature = "dynamic-engine", not(windows)))]
34use lumen_engine as _;
35
36use std::cell::RefCell;
37use std::ffi::{CStr, CString, c_char, c_int, c_void};
38use std::panic::AssertUnwindSafe;
39use std::path::PathBuf;
40use std::ptr::{self, NonNull};
41use std::sync::Mutex;
42
43use lumen_core::components::Color;
44use lumen_core::property_store::{
45 PropertyKey, PropertyValue, external_property_snapshot, push_external_property,
46};
47use lumen_core::signals::{push_external_array, push_external_clear};
48use lumen_runtime::{RunError, RunOptions};
49use lumen_script::{ScriptFn, ScriptValue};
50use std::collections::HashMap;
51use std::sync::Arc;
52
53pub mod sdk {
71 pub use bevy_ecs;
72 pub use lumen_core;
73 pub use lumen_runtime;
74 pub use lumen_script;
75 pub use lumen_widget;
76 pub use lumen_widget_macros;
77 #[cfg(feature = "embed-parser")]
78 pub use lumenc;
79}
80
81#[cfg(not(feature = "dynamic-engine"))]
87use lumen_candela as _;
88
89pub const LUMEN_ABI_MAJOR: u32 = 0;
95pub const LUMEN_ABI_MINOR: u32 = 14;
187pub const LUMEN_ABI_PATCH: u32 = 0;
189
190pub const LUMEN_ABI_VERSION: u32 =
194 (LUMEN_ABI_MAJOR << 16) | (LUMEN_ABI_MINOR << 8) | LUMEN_ABI_PATCH;
195
196#[unsafe(no_mangle)]
199pub extern "C" fn lumen_abi_version() -> u32 {
200 LUMEN_ABI_VERSION
201}
202
203#[repr(u32)]
209#[derive(Debug, Copy, Clone, PartialEq, Eq)]
210pub enum LumenKind {
211 Nil = 0,
213 Bool = 1,
215 Int = 2,
217 Float = 3,
219 String = 4,
221 Array = 5,
223 Map = 6,
225}
226
227#[repr(C)]
230#[derive(Copy, Clone)]
231pub struct LumenArrayView {
232 pub items: *const LumenValue,
234 pub len: usize,
236}
237
238#[repr(C)]
241#[derive(Copy, Clone)]
242pub struct LumenMapView {
243 pub entries: *const LumenMapEntry,
245 pub len: usize,
247}
248
249#[repr(C)]
252#[derive(Copy, Clone)]
253pub union LumenValueData {
254 pub boolean: c_int,
256 pub integer: i64,
258 pub float_: f64,
260 pub string: *const c_char,
262 pub array: LumenArrayView,
264 pub map: LumenMapView,
266}
267
268#[repr(C)]
273#[derive(Copy, Clone)]
274pub struct LumenValue {
275 pub kind: LumenKind,
277 pub as_: LumenValueData,
279}
280
281#[repr(C)]
283#[derive(Copy, Clone)]
284pub struct LumenMapEntry {
285 pub key: *const c_char,
287 pub value: LumenValue,
289}
290
291#[repr(u32)]
298#[derive(Debug, Copy, Clone, PartialEq, Eq)]
299pub enum LumenStatus {
300 Ok = 0,
302 ErrBadPath = 1,
304 ErrBadArg = 2,
306 ErrRuntime = 3,
309 ErrInternal = 4,
311 ErrParse = 5,
313 ErrCss = 6,
315 ErrAsset = 7,
317 ErrWindow = 8,
319 ErrScript = 9,
321 ErrIo = 10,
323 ErrInvalidHandle = 11,
326 ErrInvalidValue = 12,
329 ErrPanic = 13,
333 ErrBufferTooSmall = 14,
338}
339
340pub type LumenFn = unsafe extern "C" fn(
360 out: *mut LumenValue,
361 argc: c_int,
362 argv: *const LumenValue,
363 user_data: *mut c_void,
364);
365
366pub type LumenClickFn = unsafe extern "C" fn(id: *const c_char, user_data: *mut c_void);
375
376pub type LumenCloseFn = unsafe extern "C" fn(user_data: *mut c_void) -> c_int;
391
392pub type LumenWatchFn =
411 unsafe extern "C" fn(name: *const c_char, value: *const LumenValue, user_data: *mut c_void);
412
413pub struct LumenApp {
422 dir: PathBuf,
423 artifact_bytes: Option<Vec<u8>>,
430 title: Option<String>,
431 size: Option<(u32, u32)>,
432 exposed: Vec<ExposedFn>,
433 click_handlers: HashMap<String, (LumenClickFn, UserData)>,
437 close_handler: Option<(LumenCloseFn, UserData)>,
440 compiler_plugins: Option<std::sync::Arc<dyn lumen_runtime::CompilerPlugins>>,
445}
446
447#[derive(Copy, Clone)]
457pub struct UserData(Option<NonNull<c_void>>);
458
459impl UserData {
460 pub fn from_raw(p: *mut c_void) -> Self {
462 Self(NonNull::new(p))
463 }
464
465 pub fn as_ptr(self) -> *mut c_void {
468 match self.0 {
469 Some(p) => p.as_ptr(),
470 None => ptr::null_mut(),
471 }
472 }
473}
474
475unsafe impl Send for UserData {}
496unsafe impl Sync for UserData {}
497
498struct ExposedFn {
499 name: String,
500 fn_ptr: LumenFn,
501 user_data: UserData,
504 arg_count: usize,
505}
506
507thread_local! {
508 static LAST_ERROR: RefCell<Option<CString>> = const { RefCell::new(None) };
509}
510
511static GLOBAL_LAST_ERROR: Mutex<Option<CString>> = Mutex::new(None);
517
518fn set_last_error(s: impl Into<String>) {
519 let s = s.into();
520 if let Ok(c) = CString::new(s.clone()) {
521 LAST_ERROR.with(|cell| {
522 *cell.borrow_mut() = Some(c.clone());
523 });
524 match GLOBAL_LAST_ERROR.lock() {
529 Ok(mut g) => *g = Some(c),
530 Err(poisoned) => {
531 let mut g = poisoned.into_inner();
532 *g = Some(c);
533 }
534 }
535 }
536}
537
538fn catch<F>(f: F) -> LumenStatus
539where
540 F: FnOnce() -> LumenStatus,
541{
542 match std::panic::catch_unwind(AssertUnwindSafe(f)) {
543 Ok(s) => s,
544 Err(_) => {
545 set_last_error("rust panic across FFI");
546 LumenStatus::ErrPanic
547 }
548 }
549}
550
551fn catch_val<T, F>(fallback: T, f: F) -> T
556where
557 F: FnOnce() -> T,
558{
559 match std::panic::catch_unwind(AssertUnwindSafe(f)) {
560 Ok(v) => v,
561 Err(_) => {
562 set_last_error("rust panic across FFI");
563 fallback
564 }
565 }
566}
567
568#[unsafe(no_mangle)]
582pub unsafe extern "C" fn lumen_app_new(dir: *const c_char) -> *mut LumenApp {
583 let r: Result<*mut LumenApp, String> = std::panic::catch_unwind(AssertUnwindSafe(|| {
584 if dir.is_null() {
585 return Err("null dir".to_string());
586 }
587 let s = match unsafe { CStr::from_ptr(dir) }.to_str() {
588 Ok(s) => s,
589 Err(_) => return Err("dir not utf-8".to_string()),
590 };
591 let path = PathBuf::from(s);
592 let meta = std::fs::metadata(&path)
595 .map_err(|e| format!("lumen_app_new: cannot access app directory {s:?}: {e}"))?;
596 if !meta.is_dir() {
597 return Err(format!("lumen_app_new: {s:?} is not a directory"));
598 }
599 if !path.join("lumen.toml").is_file() && !path.join("src").join("main.lmn").is_file() {
605 return Err(format!(
606 "lumen_app_new: app directory {s:?} contains neither lumen.toml nor src/main.lmn"
607 ));
608 }
609 #[cfg(feature = "embed-parser")]
614 let compiler_plugins = {
615 let resolved =
616 lumenc::registry_packages(&path, lumen_runtime::modules::Target::Desktop)
617 .map_err(|e| e.to_string())?;
618 Some(
619 lumenc::plugin_host::compiler_plugins_for(&path, false, &resolved.compiler_plugins)
620 .map_err(|e| e.to_string())?,
621 )
622 };
623 #[cfg(not(feature = "embed-parser"))]
624 let compiler_plugins = None;
625 Ok(Box::into_raw(Box::new(LumenApp {
626 dir: path,
627 artifact_bytes: None,
628 title: None,
629 size: None,
630 exposed: Vec::new(),
631 click_handlers: HashMap::new(),
632 close_handler: None,
633 compiler_plugins,
634 })))
635 }))
636 .unwrap_or_else(|_| Err("panic in lumen_app_new".to_string()));
637 match r {
638 Ok(p) => p,
639 Err(msg) => {
640 set_last_error(msg);
641 ptr::null_mut()
642 }
643 }
644}
645
646#[unsafe(no_mangle)]
664pub unsafe extern "C" fn lumen_app_new_from_lmna(
665 data: *const u8,
666 len: usize,
667 base_dir: *const c_char,
668) -> *mut LumenApp {
669 let r: Result<*mut LumenApp, String> = std::panic::catch_unwind(AssertUnwindSafe(|| {
670 if data.is_null() || len == 0 {
671 return Err("lumen_app_new_from_lmna: null or empty LMNA data".to_string());
672 }
673 let bytes = unsafe { std::slice::from_raw_parts(data, len) }.to_vec();
676 let dir = if base_dir.is_null() {
678 PathBuf::from(".")
679 } else {
680 match unsafe { CStr::from_ptr(base_dir) }.to_str() {
681 Ok(s) => PathBuf::from(s),
682 Err(_) => return Err("lumen_app_new_from_lmna: base_dir not utf-8".to_string()),
683 }
684 };
685 Ok(Box::into_raw(Box::new(LumenApp {
686 dir,
687 artifact_bytes: Some(bytes),
688 title: None,
689 size: None,
690 exposed: Vec::new(),
691 click_handlers: HashMap::new(),
692 close_handler: None,
693 compiler_plugins: None,
694 })))
695 }))
696 .unwrap_or_else(|_| Err("panic in lumen_app_new_from_lmna".to_string()));
697 match r {
698 Ok(p) => p,
699 Err(msg) => {
700 set_last_error(msg);
701 ptr::null_mut()
702 }
703 }
704}
705
706#[unsafe(no_mangle)]
719pub unsafe extern "C" fn lumen_app_expose(
720 app: *mut LumenApp,
721 name: *const c_char,
722 arg_count: u32,
723 func: Option<LumenFn>,
724 user_data: *mut c_void,
725) -> LumenStatus {
726 catch(|| {
727 let Some(func) = func else {
728 set_last_error("null fn");
729 return LumenStatus::ErrBadArg;
730 };
731 if app.is_null() {
732 set_last_error("null app");
733 return LumenStatus::ErrInvalidHandle;
734 }
735 if name.is_null() {
736 set_last_error("null name");
737 return LumenStatus::ErrBadArg;
738 }
739 let app = unsafe { &mut *app };
740 let name_str = match unsafe { CStr::from_ptr(name) }.to_str() {
741 Ok(s) => s.to_owned(),
742 Err(_) => {
743 set_last_error("name not utf-8");
744 return LumenStatus::ErrBadArg;
745 }
746 };
747 app.exposed.push(ExposedFn {
748 name: name_str,
749 fn_ptr: func,
750 user_data: UserData::from_raw(user_data),
751 arg_count: arg_count as usize,
752 });
753 LumenStatus::Ok
754 })
755}
756
757#[unsafe(no_mangle)]
770pub unsafe extern "C" fn lumen_app_on_click(
771 app: *mut LumenApp,
772 id: *const c_char,
773 cb: Option<LumenClickFn>,
774 user_data: *mut c_void,
775) -> LumenStatus {
776 catch(|| {
777 let Some(cb) = cb else {
778 set_last_error("lumen_app_on_click: null callback");
779 return LumenStatus::ErrBadArg;
780 };
781 if app.is_null() {
782 set_last_error("lumen_app_on_click: null app");
783 return LumenStatus::ErrInvalidHandle;
784 }
785 if id.is_null() {
786 set_last_error("lumen_app_on_click: null id");
787 return LumenStatus::ErrBadArg;
788 }
789 let app = unsafe { &mut *app };
790 let id_str = match unsafe { CStr::from_ptr(id) }.to_str() {
791 Ok(s) => s.to_owned(),
792 Err(_) => {
793 set_last_error("lumen_app_on_click: id not utf-8");
794 return LumenStatus::ErrBadArg;
795 }
796 };
797 app.click_handlers
798 .insert(id_str, (cb, UserData::from_raw(user_data)));
799 LumenStatus::Ok
800 })
801}
802
803#[unsafe(no_mangle)]
817pub unsafe extern "C" fn lumen_app_on_close(
818 app: *mut LumenApp,
819 cb: Option<LumenCloseFn>,
820 user_data: *mut c_void,
821) -> LumenStatus {
822 catch(|| {
823 let Some(cb) = cb else {
824 set_last_error("lumen_app_on_close: null callback");
825 return LumenStatus::ErrBadArg;
826 };
827 if app.is_null() {
828 set_last_error("lumen_app_on_close: null app");
829 return LumenStatus::ErrInvalidHandle;
830 }
831 let app = unsafe { &mut *app };
832 app.close_handler = Some((cb, UserData::from_raw(user_data)));
833 LumenStatus::Ok
834 })
835}
836
837type WatcherEntry = (LumenWatchFn, UserData);
840
841static SIGNAL_WATCHERS: OnceLock<Mutex<HashMap<String, Vec<WatcherEntry>>>> = OnceLock::new();
846
847fn signal_watchers() -> &'static Mutex<HashMap<String, Vec<WatcherEntry>>> {
848 SIGNAL_WATCHERS.get_or_init(|| Mutex::new(HashMap::new()))
849}
850
851#[unsafe(no_mangle)]
871pub unsafe extern "C" fn lumen_signal_watch(
872 name: *const c_char,
873 cb: Option<LumenWatchFn>,
874 user_data: *mut c_void,
875) -> LumenStatus {
876 catch(|| {
877 let Some(cb) = cb else {
878 set_last_error("lumen_signal_watch: null callback");
879 return LumenStatus::ErrBadArg;
880 };
881 let Some(n) = typed_signal_name(name) else {
882 set_last_error("lumen_signal_watch: null or non-utf8 name");
883 return LumenStatus::ErrBadArg;
884 };
885 signal_watchers()
886 .lock()
887 .unwrap_or_else(|e| e.into_inner())
888 .entry(n)
889 .or_default()
890 .push((cb, UserData::from_raw(user_data)));
891 LumenStatus::Ok
892 })
893}
894
895fn property_to_lumen(v: &PropertyValue, keep: &mut Option<CString>) -> LumenValue {
900 match v {
901 PropertyValue::Bool(b) => LumenValue {
902 kind: LumenKind::Bool,
903 as_: LumenValueData {
904 boolean: *b as c_int,
905 },
906 },
907 PropertyValue::I64(n) => LumenValue {
908 kind: LumenKind::Int,
909 as_: LumenValueData { integer: *n },
910 },
911 PropertyValue::F64(n) => LumenValue {
912 kind: LumenKind::Float,
913 as_: LumenValueData { float_: *n },
914 },
915 PropertyValue::Str(s) => {
916 let cs = CString::new(s.as_ref()).unwrap_or_default();
917 let ptr = cs.as_ptr();
918 *keep = Some(cs);
919 LumenValue {
920 kind: LumenKind::String,
921 as_: LumenValueData { string: ptr },
922 }
923 }
924 PropertyValue::Color(c) => {
925 let [r, g, b, a] = c.to_rgba8();
926 let packed = ((r as i64) << 24) | ((g as i64) << 16) | ((b as i64) << 8) | (a as i64);
927 LumenValue {
928 kind: LumenKind::Int,
929 as_: LumenValueData { integer: packed },
930 }
931 }
932 PropertyValue::Vec2(_) | PropertyValue::Custom(_) => LumenValue {
933 kind: LumenKind::Nil,
934 as_: LumenValueData { integer: 0 },
935 },
936 }
937}
938
939#[unsafe(no_mangle)]
942pub unsafe extern "C" fn lumen_app_set_title(
943 app: *mut LumenApp,
944 title: *const c_char,
945) -> LumenStatus {
946 catch(|| {
947 if app.is_null() {
948 return LumenStatus::ErrInvalidHandle;
949 }
950 if title.is_null() {
951 return LumenStatus::ErrBadArg;
952 }
953 let s = match unsafe { CStr::from_ptr(title) }.to_str() {
954 Ok(s) => s,
955 Err(_) => return LumenStatus::ErrBadArg,
956 };
957 unsafe { &mut *app }.title = Some(s.to_owned());
958 LumenStatus::Ok
959 })
960}
961
962#[unsafe(no_mangle)]
964pub unsafe extern "C" fn lumen_app_set_size(app: *mut LumenApp, w: u32, h: u32) -> LumenStatus {
965 catch(|| {
966 if app.is_null() {
967 return LumenStatus::ErrInvalidHandle;
968 }
969 unsafe { &mut *app }.size = Some((w, h));
970 LumenStatus::Ok
971 })
972}
973
974#[unsafe(no_mangle)]
976pub unsafe extern "C" fn lumen_app_free(app: *mut LumenApp) {
977 if app.is_null() {
978 return;
979 }
980 let _ = catch(|| {
981 unsafe {
982 drop(Box::from_raw(app));
983 }
984 LumenStatus::Ok
985 });
986}
987
988#[unsafe(no_mangle)]
992pub unsafe extern "C" fn lumen_app_run(app: *mut LumenApp) -> LumenStatus {
993 catch(|| {
994 if app.is_null() {
995 return LumenStatus::ErrInvalidHandle;
996 }
997 let app = unsafe { Box::from_raw(app) };
998 run_inner(*app)
999 })
1000}
1001
1002#[unsafe(no_mangle)]
1015pub unsafe extern "C" fn lumen_app_run_headless(app: *mut LumenApp, ticks: u32) -> LumenStatus {
1016 catch(|| {
1017 if app.is_null() {
1018 return LumenStatus::ErrInvalidHandle;
1019 }
1020 let app = unsafe { Box::from_raw(app) };
1021 run_headless_inner(*app, ticks)
1022 })
1023}
1024
1025#[unsafe(no_mangle)]
1035pub unsafe extern "C" fn lumen_last_error() -> *const c_char {
1036 let tls = LAST_ERROR.with(|c| {
1037 c.borrow()
1038 .as_ref()
1039 .map(|s| s.as_ptr())
1040 .unwrap_or(ptr::null())
1041 });
1042 if !tls.is_null() {
1043 return tls;
1044 }
1045 lumen_last_error_global()
1046}
1047
1048#[unsafe(no_mangle)]
1056pub extern "C" fn lumen_last_error_global() -> *const c_char {
1057 thread_local! {
1061 static GLOBAL_BUFFER: RefCell<Option<CString>> = const { RefCell::new(None) };
1062 }
1063 let snapshot: Option<CString> = match GLOBAL_LAST_ERROR.lock() {
1064 Ok(g) => g.clone(),
1065 Err(poisoned) => poisoned.into_inner().clone(),
1066 };
1067 GLOBAL_BUFFER.with(|cell| {
1068 *cell.borrow_mut() = snapshot;
1069 cell.borrow()
1070 .as_ref()
1071 .map(|s| s.as_ptr())
1072 .unwrap_or(ptr::null())
1073 })
1074}
1075
1076fn build_run_options(app: LumenApp) -> RunOptions {
1085 let LumenApp {
1086 dir,
1087 artifact_bytes,
1088 title,
1089 size,
1090 exposed,
1091 click_handlers,
1092 close_handler,
1093 compiler_plugins,
1094 } = app;
1095
1096 let mut opts = match artifact_bytes {
1102 Some(bytes) => RunOptions::new(&dir).with_artifact_bytes(bytes),
1103 #[cfg(feature = "embed-parser")]
1109 None => RunOptions::new(&dir).with_parser(lumenc::default_parser()),
1110 #[cfg(not(feature = "embed-parser"))]
1111 None => RunOptions::new(&dir),
1112 };
1113 opts.compiler_plugins = compiler_plugins;
1115 if let Some(t) = title {
1116 opts.title = Some(t);
1117 }
1118 if let Some(sz) = size {
1119 opts.size = sz;
1120 }
1121
1122 for ef in exposed {
1123 let ExposedFn {
1124 name,
1125 fn_ptr,
1126 user_data,
1127 arg_count,
1128 } = ef;
1129 opts = opts.with_native_fn(ScriptFn::value(
1130 name,
1131 arg_count,
1132 move |args: &[ScriptValue]| {
1133 let mut keep: Vec<CString> = Vec::new();
1136 let lvs: Vec<LumenValue> = args
1137 .iter()
1138 .map(|v| script_value_to_lumen(v, &mut keep))
1139 .collect();
1140 let mut out = LumenValue {
1143 kind: LumenKind::Nil,
1144 as_: LumenValueData { integer: 0 },
1145 };
1146 unsafe {
1147 fn_ptr(
1148 &mut out,
1149 lvs.len() as c_int,
1150 lvs.as_ptr(),
1151 user_data.as_ptr(),
1152 );
1153 }
1154 lumen_to_script_value(&out)
1155 },
1156 ));
1157 }
1158
1159 if !click_handlers.is_empty() {
1164 opts = opts.with_app_hook(move |app| {
1165 use lumen_core::prelude::{ClickEvent, LumenId, MessageReader, Query, TickStage};
1166 let handlers = click_handlers;
1167 app.add_systems(
1168 TickStage::Systems,
1169 move |mut clicks: MessageReader<ClickEvent>, ids: Query<&LumenId>| {
1170 for click in clicks.read() {
1171 let Ok(id) = ids.get(click.entity) else {
1172 continue;
1173 };
1174 if let Some(&(cb, ud)) = handlers.get(id.0.as_str())
1175 && let Ok(cid) = CString::new(id.0.as_str())
1176 {
1177 unsafe { cb(cid.as_ptr(), ud.as_ptr()) };
1178 }
1179 }
1180 },
1181 );
1182 });
1183 }
1184
1185 if let Some((close_cb, close_ud)) = close_handler {
1195 opts = opts.with_app_hook(move |app| {
1196 use bevy_ecs::message::{MessageCursor, Messages};
1197 use lumen_core::input::CloseRequest;
1198 use lumen_core::prelude::TickStage;
1199 let mut cursor = MessageCursor::<CloseRequest>::default();
1200 app.add_systems(
1201 TickStage::Systems,
1202 move |mut msgs: bevy_ecs::system::ResMut<Messages<CloseRequest>>| {
1203 let requests = cursor.read(&msgs).filter(|ev| !ev.vetoed).count();
1204 let mut veto = false;
1205 for _ in 0..requests {
1206 let allow = unsafe { close_cb(close_ud.as_ptr()) };
1207 if allow == 0 {
1208 veto = true;
1209 }
1210 }
1211 if veto {
1212 msgs.write(CloseRequest { vetoed: true });
1213 }
1214 },
1215 );
1216 });
1217 }
1218
1219 opts = opts.with_app_hook(move |app| {
1228 use lumen_core::prelude::{PropertyKey, PropertyStore, PropertyValue, Res, TickStage};
1229 let mut last: HashMap<String, PropertyValue> = HashMap::new();
1230 app.add_systems(
1231 TickStage::A11ySync,
1232 move |store: Option<Res<PropertyStore>>| {
1233 let Some(store) = store else {
1234 return;
1235 };
1236 let mut fires: Vec<(CString, PropertyValue, LumenWatchFn, UserData)> = Vec::new();
1240 {
1241 let reg = signal_watchers().lock().unwrap_or_else(|e| e.into_inner());
1242 if reg.is_empty() {
1243 return;
1244 }
1245 for (name, entries) in reg.iter() {
1246 let key = PropertyKey::Global(Arc::<str>::from(name.as_str()));
1247 let Some(value) = store.get(&key) else {
1248 continue;
1249 };
1250 let changed = match last.get(name) {
1251 Some(prev) => !prev.eq_value(value),
1252 None => true,
1253 };
1254 if changed {
1255 last.insert(name.clone(), value.clone());
1256 if let Ok(cname) = CString::new(name.as_str()) {
1257 for (cb, ud) in entries {
1258 fires.push((cname.clone(), value.clone(), *cb, *ud));
1259 }
1260 }
1261 }
1262 }
1263 }
1264 for (cname, value, cb, ud) in fires {
1265 let mut keep: Option<CString> = None;
1266 let lv = property_to_lumen(&value, &mut keep);
1267 unsafe { cb(cname.as_ptr(), &lv, ud.as_ptr()) };
1268 }
1269 },
1270 );
1271 });
1272
1273 opts
1274}
1275
1276fn run_inner(app: LumenApp) -> LumenStatus {
1277 let opts = build_run_options(app);
1278 match lumen_runtime::run_app(opts) {
1279 Ok(()) => LumenStatus::Ok,
1280 Err(e) => {
1281 set_last_error(format!("{e}"));
1282 classify_run_error(&e)
1283 }
1284 }
1285}
1286
1287fn run_headless_inner(app: LumenApp, ticks: u32) -> LumenStatus {
1288 let opts = build_run_options(app);
1289 match lumen_runtime::run_app_headless(opts, ticks) {
1290 Ok(()) => LumenStatus::Ok,
1291 Err(e) => {
1292 set_last_error(format!("{e}"));
1293 classify_run_error(&e)
1294 }
1295 }
1296}
1297
1298fn classify_run_error(e: &RunError) -> LumenStatus {
1305 match e {
1306 RunError::Layout(_) => LumenStatus::ErrBadPath,
1309 other => classify_runtime_error(&other.to_string()),
1310 }
1311}
1312
1313fn classify_runtime_error(msg: &str) -> LumenStatus {
1319 let m = msg.to_ascii_lowercase();
1320 if m.contains("css") || m.contains("stylesheet") {
1321 LumenStatus::ErrCss
1322 } else if m.contains("parse") || m.contains("xml") || m.contains("html") {
1323 LumenStatus::ErrParse
1324 } else if m.contains("asset") || m.contains("decode") || m.contains("png") || m.contains("svg")
1325 {
1326 LumenStatus::ErrAsset
1327 } else if m.contains("window") || m.contains("winit") || m.contains("surface") {
1328 LumenStatus::ErrWindow
1329 } else if m.contains("script") {
1330 LumenStatus::ErrScript
1331 } else if m.contains("io") || m.contains("file") || m.contains("read") || m.contains("write") {
1332 LumenStatus::ErrIo
1333 } else {
1334 LumenStatus::ErrRuntime
1335 }
1336}
1337
1338fn script_value_to_lumen(v: &ScriptValue, keep: &mut Vec<CString>) -> LumenValue {
1347 match v {
1348 ScriptValue::I64(i) => LumenValue {
1349 kind: LumenKind::Int,
1350 as_: LumenValueData { integer: *i },
1351 },
1352 ScriptValue::F64(f) => LumenValue {
1353 kind: LumenKind::Float,
1354 as_: LumenValueData { float_: *f },
1355 },
1356 ScriptValue::Bool(b) => LumenValue {
1357 kind: LumenKind::Bool,
1358 as_: LumenValueData {
1359 boolean: *b as c_int,
1360 },
1361 },
1362 ScriptValue::Str(s) => {
1363 let cs = CString::new(s.as_str()).unwrap_or_default();
1364 let ptr = cs.as_ptr();
1365 keep.push(cs);
1366 LumenValue {
1367 kind: LumenKind::String,
1368 as_: LumenValueData { string: ptr },
1369 }
1370 }
1371 ScriptValue::Unit | ScriptValue::Array(_) | ScriptValue::Map(_) => LumenValue {
1372 kind: LumenKind::Nil,
1373 as_: LumenValueData { integer: 0 },
1374 },
1375 }
1376}
1377
1378fn lumen_to_script_value(v: &LumenValue) -> ScriptValue {
1382 match v.kind {
1383 LumenKind::Nil => ScriptValue::Unit,
1384 LumenKind::Bool => ScriptValue::Bool(unsafe { v.as_.boolean } != 0),
1385 LumenKind::Int => ScriptValue::I64(unsafe { v.as_.integer }),
1386 LumenKind::Float => ScriptValue::F64(unsafe { v.as_.float_ }),
1387 LumenKind::String => {
1388 let p = unsafe { v.as_.string };
1389 if p.is_null() {
1390 ScriptValue::Str(String::new())
1391 } else {
1392 ScriptValue::Str(unsafe { CStr::from_ptr(p) }.to_string_lossy().into_owned())
1393 }
1394 }
1395 LumenKind::Array => {
1396 let view = unsafe { v.as_.array };
1397 let items: &[LumenValue] = if view.items.is_null() || view.len == 0 {
1398 &[]
1399 } else {
1400 unsafe { std::slice::from_raw_parts(view.items, view.len) }
1401 };
1402 ScriptValue::Array(items.iter().map(lumen_to_script_value).collect())
1403 }
1404 LumenKind::Map => {
1405 let view = unsafe { v.as_.map };
1406 let entries: &[LumenMapEntry] = if view.entries.is_null() || view.len == 0 {
1407 &[]
1408 } else {
1409 unsafe { std::slice::from_raw_parts(view.entries, view.len) }
1410 };
1411 let mut m = HashMap::with_capacity(entries.len());
1412 for e in entries {
1413 let key = if e.key.is_null() {
1414 String::new()
1415 } else {
1416 unsafe { CStr::from_ptr(e.key) }
1417 .to_string_lossy()
1418 .into_owned()
1419 };
1420 m.insert(key, lumen_to_script_value(&e.value));
1421 }
1422 ScriptValue::Map(m)
1423 }
1424 }
1425}
1426
1427type ArrayRow = HashMap<String, String>;
1449type ArraySignalMap = HashMap<String, Vec<ArrayRow>>;
1451
1452static ARRAY_SIGNALS: OnceLock<Mutex<ArraySignalMap>> = OnceLock::new();
1453
1454fn array_signals() -> &'static Mutex<ArraySignalMap> {
1455 ARRAY_SIGNALS.get_or_init(|| Mutex::new(HashMap::new()))
1456}
1457
1458fn write_string_out(
1465 value: &str,
1466 buf: *mut c_char,
1467 buf_len: usize,
1468 out_len: *mut usize,
1469) -> LumenStatus {
1470 let bytes = value.as_bytes();
1471 let needed = bytes.len() + 1; if buf.is_null() || buf_len < needed {
1473 if !out_len.is_null() {
1474 unsafe { *out_len = needed };
1475 }
1476 return LumenStatus::ErrBufferTooSmall;
1477 }
1478 unsafe {
1479 ptr::copy_nonoverlapping(bytes.as_ptr(), buf as *mut u8, bytes.len());
1480 *buf.add(bytes.len()) = 0;
1481 }
1482 if !out_len.is_null() {
1483 unsafe { *out_len = bytes.len() };
1484 }
1485 LumenStatus::Ok
1486}
1487
1488#[unsafe(no_mangle)]
1504pub unsafe extern "C" fn lumen_navigate(path: *const c_char) -> LumenStatus {
1505 catch(|| {
1506 if path.is_null() {
1507 set_last_error("lumen_navigate: null path");
1508 return LumenStatus::ErrBadArg;
1509 }
1510 let p = match unsafe { CStr::from_ptr(path) }.to_str() {
1511 Ok(s) => s.to_owned(),
1512 Err(_) => {
1513 set_last_error("lumen_navigate: path not utf-8");
1514 return LumenStatus::ErrBadArg;
1515 }
1516 };
1517 lumen_core::nav::navigate(p);
1518 LumenStatus::Ok
1519 })
1520}
1521
1522#[unsafe(no_mangle)]
1525pub unsafe extern "C" fn lumen_navigate_back() -> LumenStatus {
1526 catch(|| {
1527 lumen_core::nav::back();
1528 LumenStatus::Ok
1529 })
1530}
1531
1532#[unsafe(no_mangle)]
1535pub unsafe extern "C" fn lumen_navigate_forward() -> LumenStatus {
1536 catch(|| {
1537 lumen_core::nav::forward();
1538 LumenStatus::Ok
1539 })
1540}
1541
1542#[unsafe(no_mangle)]
1550pub unsafe extern "C" fn lumen_current_page(
1551 buf: *mut c_char,
1552 buf_len: usize,
1553 out_len: *mut usize,
1554) -> LumenStatus {
1555 catch(|| write_string_out(&lumen_core::nav::current(), buf, buf_len, out_len))
1556}
1557
1558#[unsafe(no_mangle)]
1564pub unsafe extern "C" fn lumen_signal_set_array(
1565 name: *const c_char,
1566 value: *const LumenValue,
1567) -> LumenStatus {
1568 catch(|| {
1569 if name.is_null() || value.is_null() {
1570 return LumenStatus::ErrBadArg;
1571 }
1572 let n = match unsafe { CStr::from_ptr(name) }.to_str() {
1573 Ok(s) => s.to_owned(),
1574 Err(_) => return LumenStatus::ErrBadArg,
1575 };
1576 let v = unsafe { &*value };
1577 if v.kind != LumenKind::Array {
1578 set_last_error("lumen_signal_set_array: value.kind must be LUMEN_ARRAY");
1579 return LumenStatus::ErrInvalidValue;
1580 }
1581 let arr = unsafe { v.as_.array };
1582 let items_slice: &[LumenValue] = if arr.items.is_null() || arr.len == 0 {
1583 &[]
1584 } else {
1585 unsafe { std::slice::from_raw_parts(arr.items, arr.len) }
1586 };
1587 let mut rows: Vec<HashMap<String, String>> = Vec::with_capacity(items_slice.len());
1588 for row in items_slice {
1589 let mut map: HashMap<String, String> = HashMap::new();
1590 if row.kind == LumenKind::Map {
1591 let mv = unsafe { row.as_.map };
1592 let entries: &[LumenMapEntry] = if mv.entries.is_null() || mv.len == 0 {
1593 &[]
1594 } else {
1595 unsafe { std::slice::from_raw_parts(mv.entries, mv.len) }
1596 };
1597 for e in entries {
1598 let k = if e.key.is_null() {
1599 String::new()
1600 } else {
1601 unsafe { CStr::from_ptr(e.key) }
1602 .to_string_lossy()
1603 .into_owned()
1604 };
1605 map.insert(k, stringify_lumen(&e.value));
1606 }
1607 }
1608 rows.push(map);
1609 }
1610 array_signals()
1611 .lock()
1612 .unwrap_or_else(|e| e.into_inner())
1613 .insert(n.clone(), rows.clone());
1614 push_external_array(n, rows);
1615 LumenStatus::Ok
1616 })
1617}
1618
1619#[unsafe(no_mangle)]
1621pub unsafe extern "C" fn lumen_signal_clear(name: *const c_char) -> LumenStatus {
1622 catch(|| {
1623 if name.is_null() {
1624 return LumenStatus::ErrBadArg;
1625 }
1626 let n = match unsafe { CStr::from_ptr(name) }.to_str() {
1627 Ok(s) => s,
1628 Err(_) => return LumenStatus::ErrBadArg,
1629 };
1630 typed_signals()
1631 .lock()
1632 .unwrap_or_else(|e| e.into_inner())
1633 .insert(n.to_owned(), TypedSignalValue::Str(Arc::<str>::from("")));
1634 array_signals()
1635 .lock()
1636 .unwrap_or_else(|e| e.into_inner())
1637 .insert(n.to_owned(), Vec::new());
1638 push_external_clear(n);
1639 LumenStatus::Ok
1640 })
1641}
1642
1643#[unsafe(no_mangle)]
1650pub unsafe extern "C" fn lumen_signal_array_len(
1651 name: *const c_char,
1652 out_len: *mut usize,
1653) -> LumenStatus {
1654 catch(|| {
1655 let Some(n) = typed_signal_name(name) else {
1656 set_last_error("lumen_signal_array_len: null or non-utf8 name");
1657 return LumenStatus::ErrBadArg;
1658 };
1659 if out_len.is_null() {
1660 set_last_error("lumen_signal_array_len: null out_len");
1661 return LumenStatus::ErrBadArg;
1662 }
1663 let len = array_signals()
1664 .lock()
1665 .unwrap_or_else(|e| e.into_inner())
1666 .get(&n)
1667 .map(Vec::len);
1668 match len {
1669 Some(l) => {
1670 unsafe { *out_len = l };
1671 LumenStatus::Ok
1672 }
1673 None => {
1674 set_last_error("lumen_signal_array_len: no array signal by that name");
1675 LumenStatus::ErrBadArg
1676 }
1677 }
1678 })
1679}
1680
1681#[unsafe(no_mangle)]
1694pub unsafe extern "C" fn lumen_signal_array_get_field(
1695 name: *const c_char,
1696 row: usize,
1697 field: *const c_char,
1698 buf: *mut c_char,
1699 buf_len: usize,
1700 out_len: *mut usize,
1701) -> LumenStatus {
1702 catch(|| {
1703 let Some(n) = typed_signal_name(name) else {
1704 set_last_error("lumen_signal_array_get_field: null or non-utf8 name");
1705 return LumenStatus::ErrBadArg;
1706 };
1707 let Some(key) = typed_signal_name(field) else {
1708 set_last_error("lumen_signal_array_get_field: null or non-utf8 field");
1709 return LumenStatus::ErrBadArg;
1710 };
1711 let value = {
1712 let guard = array_signals().lock().unwrap_or_else(|e| e.into_inner());
1713 let Some(rows) = guard.get(&n) else {
1714 set_last_error("lumen_signal_array_get_field: no array signal by that name");
1715 return LumenStatus::ErrBadArg;
1716 };
1717 let Some(row_map) = rows.get(row) else {
1718 set_last_error("lumen_signal_array_get_field: row index out of range");
1719 return LumenStatus::ErrBadArg;
1720 };
1721 row_map.get(&key).cloned()
1722 };
1723 match value {
1724 Some(v) => write_string_out(&v, buf, buf_len, out_len),
1725 None => {
1726 set_last_error("lumen_signal_array_get_field: no such field in row");
1727 LumenStatus::ErrBadArg
1728 }
1729 }
1730 })
1731}
1732
1733fn stringify_lumen(v: &LumenValue) -> String {
1734 match v.kind {
1735 LumenKind::Nil => String::new(),
1736 LumenKind::Bool => (unsafe { v.as_.boolean } != 0).to_string(),
1737 LumenKind::Int => unsafe { v.as_.integer }.to_string(),
1738 LumenKind::Float => format!("{}", unsafe { v.as_.float_ }),
1739 LumenKind::String => {
1740 let p = unsafe { v.as_.string };
1741 if p.is_null() {
1742 String::new()
1743 } else {
1744 unsafe { CStr::from_ptr(p) }.to_string_lossy().into_owned()
1745 }
1746 }
1747 LumenKind::Array | LumenKind::Map => String::new(),
1748 }
1749}
1750
1751use std::sync::OnceLock;
1802
1803#[derive(Clone, Debug)]
1804enum TypedSignalValue {
1805 Str(Arc<str>),
1806 Int64(i64),
1807 Float64(f64),
1808 Bool(bool),
1809 Color([u8; 4]),
1810}
1811
1812static TYPED_SIGNALS: OnceLock<Mutex<HashMap<String, TypedSignalValue>>> = OnceLock::new();
1813
1814fn typed_signals() -> &'static Mutex<HashMap<String, TypedSignalValue>> {
1815 TYPED_SIGNALS.get_or_init(|| Mutex::new(HashMap::new()))
1816}
1817
1818fn typed_signal_name(name: *const c_char) -> Option<String> {
1819 if name.is_null() {
1820 return None;
1821 }
1822 let s = unsafe { CStr::from_ptr(name) }.to_str().ok()?;
1823 Some(s.to_owned())
1824}
1825
1826fn global_key(name: &str) -> PropertyKey {
1829 PropertyKey::Global(Arc::<str>::from(name))
1830}
1831
1832fn typed_read(name: &str) -> Option<TypedSignalValue> {
1846 let key = global_key(name);
1847 if let Some(value) = lumen_core::property_store::typed_property_snapshot().remove(&key) {
1849 return Some(TypedSignalValue::from(value));
1850 }
1851 if let Some(value) = external_property_snapshot().remove(&key) {
1853 return Some(TypedSignalValue::from(value));
1854 }
1855 typed_signals()
1857 .lock()
1858 .unwrap_or_else(|e| e.into_inner())
1859 .get(name)
1860 .cloned()
1861}
1862
1863impl From<PropertyValue> for TypedSignalValue {
1864 fn from(v: PropertyValue) -> Self {
1865 match v {
1866 PropertyValue::Str(s) => Self::Str(s),
1867 PropertyValue::I64(n) => Self::Int64(n),
1868 PropertyValue::F64(n) => Self::Float64(n),
1869 PropertyValue::Bool(b) => Self::Bool(b),
1870 PropertyValue::Color(c) => Self::Color(c.to_rgba8()),
1871 PropertyValue::Vec2(_) | PropertyValue::Custom(_) => Self::Bool(false),
1875 }
1876 }
1877}
1878
1879#[unsafe(no_mangle)]
1887pub unsafe extern "C" fn lumen_signal_set_str(
1888 name: *const c_char,
1889 value: *const c_char,
1890) -> LumenStatus {
1891 catch(|| {
1892 let Some(n) = typed_signal_name(name) else {
1893 set_last_error("lumen_signal_set_str: null or non-utf8 name");
1894 return LumenStatus::ErrBadArg;
1895 };
1896 let v: Arc<str> = if value.is_null() {
1897 Arc::<str>::from("")
1898 } else {
1899 match unsafe { CStr::from_ptr(value) }.to_str() {
1900 Ok(s) => Arc::<str>::from(s),
1901 Err(_) => {
1902 set_last_error("lumen_signal_set_str: value is not utf-8");
1903 return LumenStatus::ErrBadArg;
1904 }
1905 }
1906 };
1907 typed_signals()
1908 .lock()
1909 .unwrap_or_else(|e| e.into_inner())
1910 .insert(n.clone(), TypedSignalValue::Str(Arc::clone(&v)));
1911 push_external_property(global_key(&n), PropertyValue::Str(v));
1912 LumenStatus::Ok
1913 })
1914}
1915
1916#[unsafe(no_mangle)]
1934pub unsafe extern "C" fn lumen_signal_get_str(
1935 name: *const c_char,
1936 buf: *mut c_char,
1937 buf_len: usize,
1938 out_len: *mut usize,
1939) -> LumenStatus {
1940 catch(|| {
1941 let Some(n) = typed_signal_name(name) else {
1942 set_last_error("lumen_signal_get_str: null or non-utf8 name");
1943 return LumenStatus::ErrBadArg;
1944 };
1945 match typed_read(&n) {
1946 Some(TypedSignalValue::Str(s)) => write_string_out(&s, buf, buf_len, out_len),
1947 _ => {
1948 set_last_error("lumen_signal_get_str: no string signal by that name");
1949 LumenStatus::ErrBadArg
1950 }
1951 }
1952 })
1953}
1954
1955#[unsafe(no_mangle)]
1962pub unsafe extern "C" fn lumen_signal_set_int64(name: *const c_char, value: i64) -> LumenStatus {
1963 catch(|| {
1964 let Some(n) = typed_signal_name(name) else {
1965 set_last_error("lumen_signal_set_int64: null or non-utf8 name");
1966 return LumenStatus::ErrBadArg;
1967 };
1968 typed_signals()
1969 .lock()
1970 .unwrap_or_else(|e| e.into_inner())
1971 .insert(n.clone(), TypedSignalValue::Int64(value));
1972 push_external_property(global_key(&n), PropertyValue::I64(value));
1973 LumenStatus::Ok
1974 })
1975}
1976
1977#[unsafe(no_mangle)]
1984pub unsafe extern "C" fn lumen_signal_get_int64(name: *const c_char, out: *mut i64) -> LumenStatus {
1985 catch(|| {
1986 let Some(n) = typed_signal_name(name) else {
1987 set_last_error("lumen_signal_get_int64: null or non-utf8 name");
1988 return LumenStatus::ErrBadArg;
1989 };
1990 if out.is_null() {
1991 set_last_error("lumen_signal_get_int64: null out pointer");
1992 return LumenStatus::ErrBadArg;
1993 }
1994 match typed_read(&n) {
1995 Some(TypedSignalValue::Int64(v)) => {
1996 unsafe { *out = v };
1997 LumenStatus::Ok
1998 }
1999 Some(TypedSignalValue::Float64(v)) => {
2000 unsafe { *out = v as i64 };
2001 LumenStatus::Ok
2002 }
2003 Some(TypedSignalValue::Bool(b)) => {
2004 unsafe { *out = b as i64 };
2005 LumenStatus::Ok
2006 }
2007 _ => LumenStatus::ErrBadArg,
2008 }
2009 })
2010}
2011
2012#[unsafe(no_mangle)]
2017pub unsafe extern "C" fn lumen_signal_set_float64(name: *const c_char, value: f64) -> LumenStatus {
2018 catch(|| {
2019 let Some(n) = typed_signal_name(name) else {
2020 set_last_error("lumen_signal_set_float64: null or non-utf8 name");
2021 return LumenStatus::ErrBadArg;
2022 };
2023 typed_signals()
2024 .lock()
2025 .unwrap_or_else(|e| e.into_inner())
2026 .insert(n.clone(), TypedSignalValue::Float64(value));
2027 push_external_property(global_key(&n), PropertyValue::F64(value));
2028 LumenStatus::Ok
2029 })
2030}
2031
2032#[unsafe(no_mangle)]
2034pub unsafe extern "C" fn lumen_signal_get_float64(
2035 name: *const c_char,
2036 out: *mut f64,
2037) -> LumenStatus {
2038 catch(|| {
2039 let Some(n) = typed_signal_name(name) else {
2040 set_last_error("lumen_signal_get_float64: null or non-utf8 name");
2041 return LumenStatus::ErrBadArg;
2042 };
2043 if out.is_null() {
2044 set_last_error("lumen_signal_get_float64: null out pointer");
2045 return LumenStatus::ErrBadArg;
2046 }
2047 match typed_read(&n) {
2048 Some(TypedSignalValue::Float64(v)) => {
2049 unsafe { *out = v };
2050 LumenStatus::Ok
2051 }
2052 Some(TypedSignalValue::Int64(v)) => {
2053 unsafe { *out = v as f64 };
2054 LumenStatus::Ok
2055 }
2056 _ => LumenStatus::ErrBadArg,
2057 }
2058 })
2059}
2060
2061#[unsafe(no_mangle)]
2066pub unsafe extern "C" fn lumen_signal_set_bool(name: *const c_char, value: bool) -> LumenStatus {
2067 catch(|| {
2068 let Some(n) = typed_signal_name(name) else {
2069 set_last_error("lumen_signal_set_bool: null or non-utf8 name");
2070 return LumenStatus::ErrBadArg;
2071 };
2072 typed_signals()
2073 .lock()
2074 .unwrap_or_else(|e| e.into_inner())
2075 .insert(n.clone(), TypedSignalValue::Bool(value));
2076 push_external_property(global_key(&n), PropertyValue::Bool(value));
2077 LumenStatus::Ok
2078 })
2079}
2080
2081#[unsafe(no_mangle)]
2083pub unsafe extern "C" fn lumen_signal_get_bool(name: *const c_char, out: *mut bool) -> LumenStatus {
2084 catch(|| {
2085 let Some(n) = typed_signal_name(name) else {
2086 set_last_error("lumen_signal_get_bool: null or non-utf8 name");
2087 return LumenStatus::ErrBadArg;
2088 };
2089 if out.is_null() {
2090 set_last_error("lumen_signal_get_bool: null out pointer");
2091 return LumenStatus::ErrBadArg;
2092 }
2093 match typed_read(&n) {
2094 Some(TypedSignalValue::Bool(b)) => {
2095 unsafe { *out = b };
2096 LumenStatus::Ok
2097 }
2098 Some(TypedSignalValue::Int64(v)) => {
2099 unsafe { *out = v != 0 };
2100 LumenStatus::Ok
2101 }
2102 _ => LumenStatus::ErrBadArg,
2103 }
2104 })
2105}
2106
2107#[unsafe(no_mangle)]
2113pub unsafe extern "C" fn lumen_signal_set_color(
2114 name: *const c_char,
2115 rgba: *const u8,
2116) -> LumenStatus {
2117 catch(|| {
2118 let Some(n) = typed_signal_name(name) else {
2119 set_last_error("lumen_signal_set_color: null or non-utf8 name");
2120 return LumenStatus::ErrBadArg;
2121 };
2122 if rgba.is_null() {
2123 set_last_error("lumen_signal_set_color: null rgba pointer");
2124 return LumenStatus::ErrBadArg;
2125 }
2126 let bytes = unsafe { std::slice::from_raw_parts(rgba, 4) };
2127 let arr = [bytes[0], bytes[1], bytes[2], bytes[3]];
2128 typed_signals()
2129 .lock()
2130 .unwrap_or_else(|e| e.into_inner())
2131 .insert(n.clone(), TypedSignalValue::Color(arr));
2132 let color = Color::rgba(
2133 (arr[0] as f32) / 255.0,
2134 (arr[1] as f32) / 255.0,
2135 (arr[2] as f32) / 255.0,
2136 (arr[3] as f32) / 255.0,
2137 );
2138 push_external_property(global_key(&n), PropertyValue::Color(color));
2139 LumenStatus::Ok
2140 })
2141}
2142
2143#[unsafe(no_mangle)]
2146pub unsafe extern "C" fn lumen_signal_get_color(name: *const c_char, out: *mut u8) -> LumenStatus {
2147 catch(|| {
2148 let Some(n) = typed_signal_name(name) else {
2149 set_last_error("lumen_signal_get_color: null or non-utf8 name");
2150 return LumenStatus::ErrBadArg;
2151 };
2152 if out.is_null() {
2153 set_last_error("lumen_signal_get_color: null out pointer");
2154 return LumenStatus::ErrBadArg;
2155 }
2156 match typed_read(&n) {
2157 Some(TypedSignalValue::Color(c)) => {
2158 let dst = unsafe { std::slice::from_raw_parts_mut(out, 4) };
2159 dst.copy_from_slice(&c);
2160 LumenStatus::Ok
2161 }
2162 _ => LumenStatus::ErrBadArg,
2163 }
2164 })
2165}
2166
2167#[unsafe(no_mangle)]
2173pub extern "C" fn lumen_status_message(status: LumenStatus) -> *const c_char {
2174 let s: &'static [u8] = match status {
2175 LumenStatus::Ok => b"ok\0",
2176 LumenStatus::ErrBadPath => b"bad path argument\0",
2177 LumenStatus::ErrBadArg => b"bad argument\0",
2178 LumenStatus::ErrRuntime => b"runtime error\0",
2179 LumenStatus::ErrInternal => b"internal error\0",
2180 LumenStatus::ErrParse => b"parse error\0",
2181 LumenStatus::ErrCss => b"css error\0",
2182 LumenStatus::ErrAsset => b"asset error\0",
2183 LumenStatus::ErrWindow => b"window backend error\0",
2184 LumenStatus::ErrScript => b"script error\0",
2185 LumenStatus::ErrIo => b"io error\0",
2186 LumenStatus::ErrInvalidHandle => b"invalid handle\0",
2187 LumenStatus::ErrInvalidValue => b"invalid value\0",
2188 LumenStatus::ErrPanic => b"rust panic across ffi\0",
2189 LumenStatus::ErrBufferTooSmall => b"output buffer too small\0",
2190 };
2191 s.as_ptr() as *const c_char
2192}
2193
2194pub type LumenNode = u64;
2205
2206#[repr(C)]
2209pub struct LumenNodeList {
2210 pub ptr: *mut LumenNode,
2213 pub len: usize,
2215}
2216
2217fn empty_node_list() -> LumenNodeList {
2218 LumenNodeList {
2219 ptr: ptr::null_mut(),
2220 len: 0,
2221 }
2222}
2223
2224fn node_list_from(nodes: Vec<u64>) -> LumenNodeList {
2225 if nodes.is_empty() {
2226 return empty_node_list();
2227 }
2228 let mut boxed = nodes.into_boxed_slice();
2229 let list = LumenNodeList {
2230 ptr: boxed.as_mut_ptr(),
2231 len: boxed.len(),
2232 };
2233 std::mem::forget(boxed);
2234 list
2235}
2236
2237fn ffi_selector(selector: *const c_char, ctx: &str) -> Result<String, LumenStatus> {
2238 if selector.is_null() {
2239 set_last_error(format!("{ctx}: null selector"));
2240 return Err(LumenStatus::ErrBadArg);
2241 }
2242 match unsafe { CStr::from_ptr(selector) }.to_str() {
2243 Ok(s) => Ok(s.to_owned()),
2244 Err(_) => {
2245 set_last_error(format!("{ctx}: selector not utf-8"));
2246 Err(LumenStatus::ErrBadArg)
2247 }
2248 }
2249}
2250
2251#[unsafe(no_mangle)]
2256pub unsafe extern "C" fn lumen_query(
2257 selector: *const c_char,
2258 out_list: *mut LumenNodeList,
2259) -> LumenStatus {
2260 catch(|| {
2261 if out_list.is_null() {
2262 set_last_error("lumen_query: null out_list");
2263 return LumenStatus::ErrBadArg;
2264 }
2265 let sel = match ffi_selector(selector, "lumen_query") {
2266 Ok(s) => s,
2267 Err(status) => return status,
2268 };
2269 match lumen_script::node_query::run_query(&sel) {
2270 Ok(q) => {
2271 unsafe { *out_list = node_list_from(q.nodes) };
2272 LumenStatus::Ok
2273 }
2274 Err(e) => {
2275 set_last_error(format!("lumen_query: {e}"));
2276 LumenStatus::ErrCss
2277 }
2278 }
2279 })
2280}
2281
2282#[unsafe(no_mangle)]
2284pub unsafe extern "C" fn lumen_query_len(
2285 selector: *const c_char,
2286 out_len: *mut usize,
2287) -> LumenStatus {
2288 catch(|| {
2289 if out_len.is_null() {
2290 set_last_error("lumen_query_len: null out_len");
2291 return LumenStatus::ErrBadArg;
2292 }
2293 let sel = match ffi_selector(selector, "lumen_query_len") {
2294 Ok(s) => s,
2295 Err(status) => return status,
2296 };
2297 match lumen_script::node_query::run_query(&sel) {
2298 Ok(q) => {
2299 unsafe { *out_len = q.len() };
2300 LumenStatus::Ok
2301 }
2302 Err(e) => {
2303 set_last_error(format!("lumen_query_len: {e}"));
2304 LumenStatus::ErrCss
2305 }
2306 }
2307 })
2308}
2309
2310#[unsafe(no_mangle)]
2314pub unsafe extern "C" fn lumen_query_single(
2315 selector: *const c_char,
2316 out: *mut LumenNode,
2317) -> LumenStatus {
2318 catch(|| {
2319 if out.is_null() {
2320 set_last_error("lumen_query_single: null out");
2321 return LumenStatus::ErrBadArg;
2322 }
2323 unsafe { *out = 0 };
2324 let sel = match ffi_selector(selector, "lumen_query_single") {
2325 Ok(s) => s,
2326 Err(status) => return status,
2327 };
2328 match lumen_script::node_query::run_query(&sel) {
2329 Ok(q) => match q.single() {
2330 Ok(node) => {
2331 unsafe { *out = node };
2332 LumenStatus::Ok
2333 }
2334 Err(msg) => {
2335 set_last_error(format!("lumen_query_single: {msg}"));
2336 LumenStatus::ErrBadArg
2337 }
2338 },
2339 Err(e) => {
2340 set_last_error(format!("lumen_query_single: {e}"));
2341 LumenStatus::ErrCss
2342 }
2343 }
2344 })
2345}
2346
2347#[unsafe(no_mangle)]
2350pub unsafe extern "C" fn lumen_get_by_id(id: *const c_char, out: *mut LumenNode) -> LumenStatus {
2351 catch(|| {
2352 if out.is_null() {
2353 set_last_error("lumen_get_by_id: null out");
2354 return LumenStatus::ErrBadArg;
2355 }
2356 let id = match ffi_selector(id, "lumen_get_by_id") {
2357 Ok(s) => s,
2358 Err(status) => return status,
2359 };
2360 unsafe { *out = lumen_script::node_query::run_get_by_id(&id).unwrap_or(0) };
2361 LumenStatus::Ok
2362 })
2363}
2364
2365#[unsafe(no_mangle)]
2368pub unsafe extern "C" fn lumen_document(out: *mut LumenNode) -> LumenStatus {
2369 catch(|| {
2370 if out.is_null() {
2371 set_last_error("lumen_document: null out");
2372 return LumenStatus::ErrBadArg;
2373 }
2374 unsafe { *out = lumen_script::node_query::run_document().unwrap_or(0) };
2375 LumenStatus::Ok
2376 })
2377}
2378
2379fn node_relation(
2382 node: LumenNode,
2383 out: *mut LumenNode,
2384 ctx: &str,
2385 f: impl FnOnce(u64) -> Option<u64>,
2386) -> LumenStatus {
2387 if out.is_null() {
2388 set_last_error(format!("{ctx}: null out"));
2389 return LumenStatus::ErrBadArg;
2390 }
2391 unsafe { *out = f(node).unwrap_or(0) };
2392 LumenStatus::Ok
2393}
2394
2395#[unsafe(no_mangle)]
2397pub unsafe extern "C" fn lumen_node_parent(node: LumenNode, out: *mut LumenNode) -> LumenStatus {
2398 catch(|| {
2399 node_relation(
2400 node,
2401 out,
2402 "lumen_node_parent",
2403 lumen_script::node_query::node_parent,
2404 )
2405 })
2406}
2407
2408#[unsafe(no_mangle)]
2410pub unsafe extern "C" fn lumen_node_first_child(
2411 node: LumenNode,
2412 out: *mut LumenNode,
2413) -> LumenStatus {
2414 catch(|| {
2415 node_relation(
2416 node,
2417 out,
2418 "lumen_node_first_child",
2419 lumen_script::node_query::node_first_child,
2420 )
2421 })
2422}
2423
2424#[unsafe(no_mangle)]
2426pub unsafe extern "C" fn lumen_node_last_child(
2427 node: LumenNode,
2428 out: *mut LumenNode,
2429) -> LumenStatus {
2430 catch(|| {
2431 node_relation(
2432 node,
2433 out,
2434 "lumen_node_last_child",
2435 lumen_script::node_query::node_last_child,
2436 )
2437 })
2438}
2439
2440#[unsafe(no_mangle)]
2442pub unsafe extern "C" fn lumen_node_next(node: LumenNode, out: *mut LumenNode) -> LumenStatus {
2443 catch(|| {
2444 node_relation(
2445 node,
2446 out,
2447 "lumen_node_next",
2448 lumen_script::node_query::node_next,
2449 )
2450 })
2451}
2452
2453#[unsafe(no_mangle)]
2455pub unsafe extern "C" fn lumen_node_prev(node: LumenNode, out: *mut LumenNode) -> LumenStatus {
2456 catch(|| {
2457 node_relation(
2458 node,
2459 out,
2460 "lumen_node_prev",
2461 lumen_script::node_query::node_prev,
2462 )
2463 })
2464}
2465
2466#[unsafe(no_mangle)]
2469pub unsafe extern "C" fn lumen_node_children(
2470 node: LumenNode,
2471 out_list: *mut LumenNodeList,
2472) -> LumenStatus {
2473 catch(|| {
2474 if out_list.is_null() {
2475 set_last_error("lumen_node_children: null out_list");
2476 return LumenStatus::ErrBadArg;
2477 }
2478 unsafe { *out_list = node_list_from(lumen_script::node_query::node_children(node)) };
2479 LumenStatus::Ok
2480 })
2481}
2482
2483#[unsafe(no_mangle)]
2487pub unsafe extern "C" fn lumen_node_closest(
2488 node: LumenNode,
2489 selector: *const c_char,
2490 out: *mut LumenNode,
2491) -> LumenStatus {
2492 catch(|| {
2493 if out.is_null() {
2494 set_last_error("lumen_node_closest: null out");
2495 return LumenStatus::ErrBadArg;
2496 }
2497 unsafe { *out = 0 };
2498 let sel = match ffi_selector(selector, "lumen_node_closest") {
2499 Ok(s) => s,
2500 Err(status) => return status,
2501 };
2502 match lumen_script::node_query::node_closest(node, &sel) {
2503 Ok(hit) => {
2504 unsafe { *out = hit.unwrap_or(0) };
2505 LumenStatus::Ok
2506 }
2507 Err(e) => {
2508 set_last_error(format!("lumen_node_closest: {e}"));
2509 LumenStatus::ErrCss
2510 }
2511 }
2512 })
2513}
2514
2515#[unsafe(no_mangle)]
2519pub unsafe extern "C" fn lumen_node_valid(node: LumenNode, out: *mut c_int) -> LumenStatus {
2520 catch(|| {
2521 if out.is_null() {
2522 set_last_error("lumen_node_valid: null out");
2523 return LumenStatus::ErrBadArg;
2524 }
2525 unsafe { *out = lumen_script::node_query::node_valid(node) as c_int };
2526 LumenStatus::Ok
2527 })
2528}
2529
2530#[unsafe(no_mangle)]
2538pub unsafe extern "C" fn lumen_nodelist_get(
2539 list: LumenNodeList,
2540 index: usize,
2541 out: *mut LumenNode,
2542) -> LumenStatus {
2543 catch(|| {
2544 if out.is_null() {
2545 set_last_error("lumen_nodelist_get: null out");
2546 return LumenStatus::ErrBadArg;
2547 }
2548 if list.ptr.is_null() || index >= list.len {
2549 set_last_error("lumen_nodelist_get: index out of range");
2550 return LumenStatus::ErrBadArg;
2551 }
2552 unsafe { *out = *list.ptr.add(index) };
2553 LumenStatus::Ok
2554 })
2555}
2556
2557#[unsafe(no_mangle)]
2564pub unsafe extern "C" fn lumen_nodelist_free(list: LumenNodeList) {
2565 if list.ptr.is_null() || list.len == 0 {
2566 return;
2567 }
2568 unsafe {
2569 drop(Box::from_raw(std::ptr::slice_from_raw_parts_mut(
2570 list.ptr, list.len,
2571 )));
2572 }
2573}
2574
2575#[repr(C)]
2587pub struct LumenRect {
2588 pub x: f64,
2590 pub y: f64,
2592 pub width: f64,
2594 pub height: f64,
2596 pub client_x: f64,
2598 pub client_y: f64,
2600}
2601
2602#[repr(C)]
2604pub struct LumenScroll {
2605 pub x: f64,
2607 pub y: f64,
2609 pub max_x: f64,
2611 pub max_y: f64,
2613}
2614
2615#[repr(C)]
2617pub struct LumenPointerState {
2618 pub x: f64,
2620 pub y: f64,
2622 pub inside: c_int,
2624 pub buttons: u32,
2626 pub shift: c_int,
2628 pub ctrl: c_int,
2630 pub alt: c_int,
2632 pub super_: c_int,
2634}
2635
2636#[repr(C)]
2638pub struct LumenFrameInfo {
2639 pub frame: u64,
2641 pub dt_ms: f64,
2643 pub dirty_count: u64,
2645}
2646
2647#[repr(C)]
2650pub struct LumenKV {
2651 pub key: *mut c_char,
2653 pub value: *mut c_char,
2655}
2656
2657#[repr(C)]
2660pub struct LumenKVList {
2661 pub ptr: *mut LumenKV,
2663 pub len: usize,
2665}
2666
2667#[repr(C)]
2670pub struct LumenStrList {
2671 pub ptr: *mut *mut c_char,
2673 pub len: usize,
2675}
2676
2677fn owned_cstring(s: &str) -> *mut c_char {
2678 CString::new(s).unwrap_or_default().into_raw()
2679}
2680
2681fn kvlist_from(pairs: Vec<(String, String)>) -> LumenKVList {
2682 if pairs.is_empty() {
2683 return LumenKVList {
2684 ptr: ptr::null_mut(),
2685 len: 0,
2686 };
2687 }
2688 let mut boxed: Box<[LumenKV]> = pairs
2689 .into_iter()
2690 .map(|(k, v)| LumenKV {
2691 key: owned_cstring(&k),
2692 value: owned_cstring(&v),
2693 })
2694 .collect();
2695 let list = LumenKVList {
2696 ptr: boxed.as_mut_ptr(),
2697 len: boxed.len(),
2698 };
2699 std::mem::forget(boxed);
2700 list
2701}
2702
2703fn strlist_from(items: Vec<String>) -> LumenStrList {
2704 if items.is_empty() {
2705 return LumenStrList {
2706 ptr: ptr::null_mut(),
2707 len: 0,
2708 };
2709 }
2710 let mut boxed: Box<[*mut c_char]> = items.iter().map(|s| owned_cstring(s)).collect();
2711 let list = LumenStrList {
2712 ptr: boxed.as_mut_ptr(),
2713 len: boxed.len(),
2714 };
2715 std::mem::forget(boxed);
2716 list
2717}
2718
2719fn rect_to_ffi(r: lumen_script::introspect::NodeRect) -> LumenRect {
2720 LumenRect {
2721 x: r.x as f64,
2722 y: r.y as f64,
2723 width: r.width as f64,
2724 height: r.height as f64,
2725 client_x: r.client_x as f64,
2726 client_y: r.client_y as f64,
2727 }
2728}
2729
2730#[unsafe(no_mangle)]
2732pub unsafe extern "C" fn lumen_node_rect(node: LumenNode, out: *mut LumenRect) -> LumenStatus {
2733 catch(|| {
2734 if out.is_null() {
2735 set_last_error("lumen_node_rect: null out");
2736 return LumenStatus::ErrBadArg;
2737 }
2738 match lumen_script::introspect::node_rect(node) {
2739 Some(r) => {
2740 unsafe { *out = rect_to_ffi(r) };
2741 LumenStatus::Ok
2742 }
2743 None => LumenStatus::ErrInvalidHandle,
2744 }
2745 })
2746}
2747
2748#[unsafe(no_mangle)]
2750pub unsafe extern "C" fn lumen_node_content_rect(
2751 node: LumenNode,
2752 out: *mut LumenRect,
2753) -> LumenStatus {
2754 catch(|| {
2755 if out.is_null() {
2756 set_last_error("lumen_node_content_rect: null out");
2757 return LumenStatus::ErrBadArg;
2758 }
2759 match lumen_script::introspect::node_content_rect(node) {
2760 Some(r) => {
2761 unsafe { *out = rect_to_ffi(r) };
2762 LumenStatus::Ok
2763 }
2764 None => LumenStatus::ErrInvalidHandle,
2765 }
2766 })
2767}
2768
2769#[unsafe(no_mangle)]
2771pub unsafe extern "C" fn lumen_node_scroll(node: LumenNode, out: *mut LumenScroll) -> LumenStatus {
2772 catch(|| {
2773 if out.is_null() {
2774 set_last_error("lumen_node_scroll: null out");
2775 return LumenStatus::ErrBadArg;
2776 }
2777 match lumen_script::introspect::node_scroll(node) {
2778 Some(s) => {
2779 unsafe {
2780 *out = LumenScroll {
2781 x: s.x as f64,
2782 y: s.y as f64,
2783 max_x: s.max_x as f64,
2784 max_y: s.max_y as f64,
2785 }
2786 };
2787 LumenStatus::Ok
2788 }
2789 None => LumenStatus::ErrInvalidHandle,
2790 }
2791 })
2792}
2793
2794#[unsafe(no_mangle)]
2796pub unsafe extern "C" fn lumen_node_is_visible(node: LumenNode, out: *mut c_int) -> LumenStatus {
2797 catch(|| {
2798 if out.is_null() {
2799 set_last_error("lumen_node_is_visible: null out");
2800 return LumenStatus::ErrBadArg;
2801 }
2802 unsafe { *out = c_int::from(lumen_script::introspect::node_is_visible(node)) };
2803 LumenStatus::Ok
2804 })
2805}
2806
2807#[unsafe(no_mangle)]
2809pub unsafe extern "C" fn lumen_node_z_index(node: LumenNode, out: *mut c_int) -> LumenStatus {
2810 catch(|| {
2811 if out.is_null() {
2812 set_last_error("lumen_node_z_index: null out");
2813 return LumenStatus::ErrBadArg;
2814 }
2815 unsafe { *out = lumen_script::introspect::node_z_index(node) as c_int };
2816 LumenStatus::Ok
2817 })
2818}
2819
2820#[unsafe(no_mangle)]
2822pub unsafe extern "C" fn lumen_node_entity_id(
2823 node: LumenNode,
2824 out_index: *mut u32,
2825 out_gen: *mut u32,
2826) -> LumenStatus {
2827 catch(|| {
2828 if out_index.is_null() || out_gen.is_null() {
2829 set_last_error("lumen_node_entity_id: null out");
2830 return LumenStatus::ErrBadArg;
2831 }
2832 match lumen_script::introspect::node_entity_id(node) {
2833 Some((index, generation)) => {
2834 unsafe {
2835 *out_index = index;
2836 *out_gen = generation;
2837 }
2838 LumenStatus::Ok
2839 }
2840 None => LumenStatus::ErrInvalidHandle,
2841 }
2842 })
2843}
2844
2845#[unsafe(no_mangle)]
2848pub unsafe extern "C" fn lumen_node_computed_style(
2849 node: LumenNode,
2850 out: *mut LumenKVList,
2851) -> LumenStatus {
2852 catch(|| {
2853 if out.is_null() {
2854 set_last_error("lumen_node_computed_style: null out");
2855 return LumenStatus::ErrBadArg;
2856 }
2857 unsafe { *out = kvlist_from(lumen_script::introspect::node_computed_style_map(node)) };
2858 LumenStatus::Ok
2859 })
2860}
2861
2862#[unsafe(no_mangle)]
2864pub unsafe extern "C" fn lumen_node_attrs(node: LumenNode, out: *mut LumenKVList) -> LumenStatus {
2865 catch(|| {
2866 if out.is_null() {
2867 set_last_error("lumen_node_attrs: null out");
2868 return LumenStatus::ErrBadArg;
2869 }
2870 unsafe { *out = kvlist_from(lumen_script::introspect::node_attrs(node)) };
2871 LumenStatus::Ok
2872 })
2873}
2874
2875#[unsafe(no_mangle)]
2877pub unsafe extern "C" fn lumen_node_inline_style(
2878 node: LumenNode,
2879 out: *mut LumenKVList,
2880) -> LumenStatus {
2881 catch(|| {
2882 if out.is_null() {
2883 set_last_error("lumen_node_inline_style: null out");
2884 return LumenStatus::ErrBadArg;
2885 }
2886 unsafe { *out = kvlist_from(lumen_script::introspect::node_inline_style(node)) };
2887 LumenStatus::Ok
2888 })
2889}
2890
2891#[unsafe(no_mangle)]
2894pub unsafe extern "C" fn lumen_node_component(
2895 node: LumenNode,
2896 name: *const c_char,
2897 out: *mut LumenKVList,
2898) -> LumenStatus {
2899 catch(|| {
2900 if out.is_null() {
2901 set_last_error("lumen_node_component: null out");
2902 return LumenStatus::ErrBadArg;
2903 }
2904 let name = match ffi_selector(name, "lumen_node_component") {
2905 Ok(s) => s,
2906 Err(status) => return status,
2907 };
2908 match lumen_script::introspect::node_component(node, &name) {
2909 Ok(map) => {
2910 unsafe { *out = kvlist_from(map.unwrap_or_default()) };
2911 LumenStatus::Ok
2912 }
2913 Err(e) => {
2914 set_last_error(format!("lumen_node_component: {e}"));
2915 LumenStatus::ErrBadArg
2916 }
2917 }
2918 })
2919}
2920
2921#[unsafe(no_mangle)]
2923pub unsafe extern "C" fn lumen_node_classes(
2924 node: LumenNode,
2925 out: *mut LumenStrList,
2926) -> LumenStatus {
2927 catch(|| {
2928 if out.is_null() {
2929 set_last_error("lumen_node_classes: null out");
2930 return LumenStatus::ErrBadArg;
2931 }
2932 unsafe { *out = strlist_from(lumen_script::introspect::node_classes(node)) };
2933 LumenStatus::Ok
2934 })
2935}
2936
2937#[unsafe(no_mangle)]
2940pub unsafe extern "C" fn lumen_node_components(
2941 node: LumenNode,
2942 out: *mut LumenStrList,
2943) -> LumenStatus {
2944 catch(|| {
2945 if out.is_null() {
2946 set_last_error("lumen_node_components: null out");
2947 return LumenStatus::ErrBadArg;
2948 }
2949 unsafe { *out = strlist_from(lumen_script::introspect::node_components(node)) };
2950 LumenStatus::Ok
2951 })
2952}
2953
2954#[unsafe(no_mangle)]
2957pub unsafe extern "C" fn lumen_node_outer_markup(
2958 node: LumenNode,
2959 out: *mut *mut c_char,
2960) -> LumenStatus {
2961 catch(|| {
2962 if out.is_null() {
2963 set_last_error("lumen_node_outer_markup: null out");
2964 return LumenStatus::ErrBadArg;
2965 }
2966 unsafe { *out = owned_cstring(&lumen_script::introspect::outer_markup(node)) };
2967 LumenStatus::Ok
2968 })
2969}
2970
2971#[unsafe(no_mangle)]
2975pub unsafe extern "C" fn lumen_node_inner_markup(
2976 node: LumenNode,
2977 out: *mut *mut c_char,
2978) -> LumenStatus {
2979 catch(|| {
2980 if out.is_null() {
2981 set_last_error("lumen_node_inner_markup: null out");
2982 return LumenStatus::ErrBadArg;
2983 }
2984 unsafe { *out = owned_cstring(&lumen_script::introspect::inner_markup(node)) };
2985 LumenStatus::Ok
2986 })
2987}
2988
2989#[unsafe(no_mangle)]
2992pub unsafe extern "C" fn lumen_dump_tree(out: *mut *mut c_char) -> LumenStatus {
2993 catch(|| {
2994 if out.is_null() {
2995 set_last_error("lumen_dump_tree: null out");
2996 return LumenStatus::ErrBadArg;
2997 }
2998 unsafe { *out = owned_cstring(&lumen_script::introspect::dump_tree()) };
2999 LumenStatus::Ok
3000 })
3001}
3002
3003#[unsafe(no_mangle)]
3005pub unsafe extern "C" fn lumen_pointer_state(out: *mut LumenPointerState) -> LumenStatus {
3006 catch(|| {
3007 if out.is_null() {
3008 set_last_error("lumen_pointer_state: null out");
3009 return LumenStatus::ErrBadArg;
3010 }
3011 let p = lumen_script::introspect::pointer_state();
3012 unsafe {
3013 *out = LumenPointerState {
3014 x: p.x as f64,
3015 y: p.y as f64,
3016 inside: c_int::from(p.inside),
3017 buttons: p.buttons,
3018 shift: c_int::from(p.shift),
3019 ctrl: c_int::from(p.ctrl),
3020 alt: c_int::from(p.alt),
3021 super_: c_int::from(p.super_),
3022 }
3023 };
3024 LumenStatus::Ok
3025 })
3026}
3027
3028#[unsafe(no_mangle)]
3030pub unsafe extern "C" fn lumen_frame_info(out: *mut LumenFrameInfo) -> LumenStatus {
3031 catch(|| {
3032 if out.is_null() {
3033 set_last_error("lumen_frame_info: null out");
3034 return LumenStatus::ErrBadArg;
3035 }
3036 let f = lumen_script::introspect::frame_info();
3037 unsafe {
3038 *out = LumenFrameInfo {
3039 frame: f.frame,
3040 dt_ms: f.dt_ms,
3041 dirty_count: f.dirty_count,
3042 }
3043 };
3044 LumenStatus::Ok
3045 })
3046}
3047
3048#[unsafe(no_mangle)]
3051pub unsafe extern "C" fn lumen_signals_all(out: *mut LumenKVList) -> LumenStatus {
3052 catch(|| {
3053 if out.is_null() {
3054 set_last_error("lumen_signals_all: null out");
3055 return LumenStatus::ErrBadArg;
3056 }
3057 unsafe { *out = kvlist_from(lumen_script::introspect::signals_all()) };
3058 LumenStatus::Ok
3059 })
3060}
3061
3062#[unsafe(no_mangle)]
3064pub unsafe extern "C" fn lumen_kvlist_free(list: LumenKVList) {
3065 if list.ptr.is_null() || list.len == 0 {
3066 return;
3067 }
3068 unsafe {
3069 let slice = Box::from_raw(std::ptr::slice_from_raw_parts_mut(list.ptr, list.len));
3070 for kv in slice.iter() {
3071 if !kv.key.is_null() {
3072 drop(CString::from_raw(kv.key));
3073 }
3074 if !kv.value.is_null() {
3075 drop(CString::from_raw(kv.value));
3076 }
3077 }
3078 }
3079}
3080
3081#[unsafe(no_mangle)]
3083pub unsafe extern "C" fn lumen_strlist_free(list: LumenStrList) {
3084 if list.ptr.is_null() || list.len == 0 {
3085 return;
3086 }
3087 unsafe {
3088 let slice = Box::from_raw(std::ptr::slice_from_raw_parts_mut(list.ptr, list.len));
3089 for s in slice.iter() {
3090 if !s.is_null() {
3091 drop(CString::from_raw(*s));
3092 }
3093 }
3094 }
3095}
3096
3097#[unsafe(no_mangle)]
3099pub unsafe extern "C" fn lumen_string_free(s: *mut c_char) {
3100 if s.is_null() {
3101 return;
3102 }
3103 unsafe { drop(CString::from_raw(s)) };
3104}
3105
3106fn push_dom(cmd: lumen_script::ScriptCommand) -> LumenStatus {
3120 lumen_script::node_query::push_external_dom_command(cmd);
3121 LumenStatus::Ok
3122}
3123
3124#[unsafe(no_mangle)]
3128pub unsafe extern "C" fn lumen_node_set_attr(
3129 node: LumenNode,
3130 name: *const c_char,
3131 value: *const c_char,
3132) -> LumenStatus {
3133 catch(|| {
3134 let name = match ffi_selector(name, "lumen_node_set_attr") {
3135 Ok(s) => s,
3136 Err(status) => return status,
3137 };
3138 let value = match ffi_selector(value, "lumen_node_set_attr") {
3139 Ok(s) => s,
3140 Err(status) => return status,
3141 };
3142 push_dom(lumen_script::ScriptCommand::SetAttr { node, name, value })
3143 })
3144}
3145
3146#[unsafe(no_mangle)]
3148pub unsafe extern "C" fn lumen_node_remove_attr(
3149 node: LumenNode,
3150 name: *const c_char,
3151) -> LumenStatus {
3152 catch(|| {
3153 let name = match ffi_selector(name, "lumen_node_remove_attr") {
3154 Ok(s) => s,
3155 Err(status) => return status,
3156 };
3157 push_dom(lumen_script::ScriptCommand::RemoveAttr { node, name })
3158 })
3159}
3160
3161#[unsafe(no_mangle)]
3163pub unsafe extern "C" fn lumen_node_set_text(node: LumenNode, text: *const c_char) -> LumenStatus {
3164 catch(|| {
3165 let text = match ffi_selector(text, "lumen_node_set_text") {
3166 Ok(s) => s,
3167 Err(status) => return status,
3168 };
3169 push_dom(lumen_script::ScriptCommand::SetNodeText { node, text })
3170 })
3171}
3172
3173#[unsafe(no_mangle)]
3179pub unsafe extern "C" fn lumen_node_set_inner_markup(
3180 node: LumenNode,
3181 markup: *const c_char,
3182) -> LumenStatus {
3183 catch(|| {
3184 let markup = match ffi_selector(markup, "lumen_node_set_inner_markup") {
3185 Ok(s) => s,
3186 Err(status) => return status,
3187 };
3188 push_dom(lumen_script::ScriptCommand::SetInnerMarkup { node, markup })
3189 })
3190}
3191
3192#[unsafe(no_mangle)]
3194pub unsafe extern "C" fn lumen_node_class_add(
3195 node: LumenNode,
3196 class: *const c_char,
3197) -> LumenStatus {
3198 catch(|| {
3199 let class = match ffi_selector(class, "lumen_node_class_add") {
3200 Ok(s) => s,
3201 Err(status) => return status,
3202 };
3203 push_dom(lumen_script::ScriptCommand::ClassAdd { node, class })
3204 })
3205}
3206
3207#[unsafe(no_mangle)]
3209pub unsafe extern "C" fn lumen_node_class_remove(
3210 node: LumenNode,
3211 class: *const c_char,
3212) -> LumenStatus {
3213 catch(|| {
3214 let class = match ffi_selector(class, "lumen_node_class_remove") {
3215 Ok(s) => s,
3216 Err(status) => return status,
3217 };
3218 push_dom(lumen_script::ScriptCommand::ClassRemove { node, class })
3219 })
3220}
3221
3222#[unsafe(no_mangle)]
3224pub unsafe extern "C" fn lumen_node_class_toggle(
3225 node: LumenNode,
3226 class: *const c_char,
3227) -> LumenStatus {
3228 catch(|| {
3229 let class = match ffi_selector(class, "lumen_node_class_toggle") {
3230 Ok(s) => s,
3231 Err(status) => return status,
3232 };
3233 push_dom(lumen_script::ScriptCommand::ClassToggle { node, class })
3234 })
3235}
3236
3237#[unsafe(no_mangle)]
3239pub unsafe extern "C" fn lumen_node_set_style(
3240 node: LumenNode,
3241 name: *const c_char,
3242 value: *const c_char,
3243) -> LumenStatus {
3244 catch(|| {
3245 let name = match ffi_selector(name, "lumen_node_set_style") {
3246 Ok(s) => s,
3247 Err(status) => return status,
3248 };
3249 let value = match ffi_selector(value, "lumen_node_set_style") {
3250 Ok(s) => s,
3251 Err(status) => return status,
3252 };
3253 push_dom(lumen_script::ScriptCommand::SetStyleProp { node, name, value })
3254 })
3255}
3256
3257#[unsafe(no_mangle)]
3259pub unsafe extern "C" fn lumen_node_remove_style(
3260 node: LumenNode,
3261 name: *const c_char,
3262) -> LumenStatus {
3263 catch(|| {
3264 let name = match ffi_selector(name, "lumen_node_remove_style") {
3265 Ok(s) => s,
3266 Err(status) => return status,
3267 };
3268 push_dom(lumen_script::ScriptCommand::RemoveStyleProp { node, name })
3269 })
3270}
3271
3272#[unsafe(no_mangle)]
3276pub unsafe extern "C" fn lumen_node_spawn(tag: *const c_char, out: *mut LumenNode) -> LumenStatus {
3277 catch(|| {
3278 if out.is_null() {
3279 set_last_error("lumen_node_spawn: null out");
3280 return LumenStatus::ErrBadArg;
3281 }
3282 let tag = match ffi_selector(tag, "lumen_node_spawn") {
3283 Ok(s) => s,
3284 Err(status) => return status,
3285 };
3286 let (handle, cmd) = lumen_script::node_query::build_spawn(&tag);
3287 unsafe { *out = handle };
3288 push_dom(cmd)
3289 })
3290}
3291
3292#[unsafe(no_mangle)]
3295pub unsafe extern "C" fn lumen_node_clone(source: LumenNode, out: *mut LumenNode) -> LumenStatus {
3296 catch(|| {
3297 if out.is_null() {
3298 set_last_error("lumen_node_clone: null out");
3299 return LumenStatus::ErrBadArg;
3300 }
3301 let (handle, cmd) = lumen_script::node_query::build_clone(source);
3302 unsafe { *out = handle };
3303 push_dom(cmd)
3304 })
3305}
3306
3307#[unsafe(no_mangle)]
3309pub unsafe extern "C" fn lumen_node_append(parent: LumenNode, child: LumenNode) -> LumenStatus {
3310 catch(|| {
3311 push_dom(lumen_script::ScriptCommand::Insert {
3312 parent,
3313 node: child,
3314 before: 0,
3315 })
3316 })
3317}
3318
3319#[unsafe(no_mangle)]
3322pub unsafe extern "C" fn lumen_node_insert_before(
3323 parent: LumenNode,
3324 child: LumenNode,
3325 reference: LumenNode,
3326) -> LumenStatus {
3327 catch(|| {
3328 push_dom(lumen_script::ScriptCommand::Insert {
3329 parent,
3330 node: child,
3331 before: reference,
3332 })
3333 })
3334}
3335
3336#[unsafe(no_mangle)]
3338pub unsafe extern "C" fn lumen_node_set_parent(node: LumenNode, parent: LumenNode) -> LumenStatus {
3339 catch(|| {
3340 push_dom(lumen_script::ScriptCommand::Insert {
3341 parent,
3342 node,
3343 before: 0,
3344 })
3345 })
3346}
3347
3348#[unsafe(no_mangle)]
3351pub unsafe extern "C" fn lumen_node_replace_with(old: LumenNode, new: LumenNode) -> LumenStatus {
3352 catch(|| push_dom(lumen_script::ScriptCommand::ReplaceWith { old, new }))
3353}
3354
3355#[unsafe(no_mangle)]
3357pub unsafe extern "C" fn lumen_node_remove(node: LumenNode) -> LumenStatus {
3358 catch(|| push_dom(lumen_script::ScriptCommand::RemoveNode { node }))
3359}
3360
3361#[unsafe(no_mangle)]
3364pub unsafe extern "C" fn lumen_window_set_href(path: *const c_char) -> LumenStatus {
3365 catch(|| {
3366 let path = match ffi_selector(path, "lumen_window_set_href") {
3367 Ok(s) => s,
3368 Err(status) => return status,
3369 };
3370 lumen_core::nav::navigate(path);
3371 LumenStatus::Ok
3372 })
3373}
3374
3375#[unsafe(no_mangle)]
3377pub unsafe extern "C" fn lumen_window_reload() -> LumenStatus {
3378 catch(|| {
3379 lumen_core::nav::navigate(lumen_core::nav::current());
3380 LumenStatus::Ok
3381 })
3382}
3383
3384#[unsafe(no_mangle)]
3386pub unsafe extern "C" fn lumen_window_set_title(title: *const c_char) -> LumenStatus {
3387 catch(|| {
3388 let title = match ffi_selector(title, "lumen_window_set_title") {
3389 Ok(s) => s,
3390 Err(status) => return status,
3391 };
3392 push_dom(lumen_script::ScriptCommand::WindowSetTitle { title })
3393 })
3394}
3395
3396#[unsafe(no_mangle)]
3398pub unsafe extern "C" fn lumen_window_set_size(width: f32, height: f32) -> LumenStatus {
3399 catch(|| push_dom(lumen_script::ScriptCommand::WindowSetSize { width, height }))
3400}
3401
3402#[unsafe(no_mangle)]
3405pub unsafe extern "C" fn lumen_window_dpr(out: *mut f32) -> LumenStatus {
3406 catch(|| {
3407 if out.is_null() {
3408 set_last_error("lumen_window_dpr: null out");
3409 return LumenStatus::ErrBadArg;
3410 }
3411 unsafe { *out = lumen_core::window_state::dpr() };
3412 LumenStatus::Ok
3413 })
3414}
3415
3416#[unsafe(no_mangle)]
3419pub unsafe extern "C" fn lumen_history_go(delta: c_int) -> LumenStatus {
3420 catch(|| {
3421 for _ in 0..delta.unsigned_abs() {
3422 if delta < 0 {
3423 lumen_core::nav::back();
3424 } else {
3425 lumen_core::nav::forward();
3426 }
3427 }
3428 LumenStatus::Ok
3429 })
3430}
3431
3432#[unsafe(no_mangle)]
3435pub unsafe extern "C" fn lumen_document_spawn(
3436 tag: *const c_char,
3437 out: *mut LumenNode,
3438) -> LumenStatus {
3439 catch(|| {
3440 if out.is_null() {
3441 set_last_error("lumen_document_spawn: null out");
3442 return LumenStatus::ErrBadArg;
3443 }
3444 let tag = match ffi_selector(tag, "lumen_document_spawn") {
3445 Ok(s) => s,
3446 Err(status) => return status,
3447 };
3448 let (handle, cmd) = lumen_script::node_query::build_spawn(&tag);
3449 unsafe { *out = handle };
3450 push_dom(cmd)
3451 })
3452}
3453
3454pub type LumenEventToken = u64;
3469
3470#[repr(C)]
3475pub struct LumenEvent {
3476 pub target: LumenNode,
3478 pub current_target: LumenNode,
3480 pub local_x: f64,
3482 pub local_y: f64,
3484 pub client_x: f64,
3486 pub client_y: f64,
3488 pub delta_x: f64,
3490 pub delta_y: f64,
3492 pub button: i64,
3494 pub shift: u8,
3496 pub ctrl: u8,
3498 pub alt: u8,
3500 pub super_: u8,
3502}
3503
3504pub type LumenEventFn = unsafe extern "C" fn(event: *const LumenEvent, user_data: *mut c_void);
3508
3509struct CEventCallback {
3513 callback: LumenEventFn,
3514 user_data: *mut c_void,
3515}
3516
3517unsafe impl Send for CEventCallback {}
3521unsafe impl Sync for CEventCallback {}
3522
3523fn current_lumen_event() -> LumenEvent {
3525 use lumen_script::event;
3526 let (lx, ly) = event::event_position_local();
3527 let (cx, cy) = event::event_position_client();
3528 let (dx, dy) = event::event_delta();
3529 let (shift, ctrl, alt, super_) = event::event_modifiers();
3530 LumenEvent {
3531 target: event::event_target(),
3532 current_target: event::event_current_target(),
3533 local_x: lx,
3534 local_y: ly,
3535 client_x: cx,
3536 client_y: cy,
3537 delta_x: dx,
3538 delta_y: dy,
3539 button: event::event_button(),
3540 shift: shift as u8,
3541 ctrl: ctrl as u8,
3542 alt: alt as u8,
3543 super_: super_ as u8,
3544 }
3545}
3546
3547#[unsafe(no_mangle)]
3551pub unsafe extern "C" fn lumen_on(
3552 node: LumenNode,
3553 event_type: *const c_char,
3554 capture: c_int,
3555 callback: Option<LumenEventFn>,
3556 user_data: *mut c_void,
3557) -> LumenEventToken {
3558 catch_val(0u64, || {
3559 let Some(callback) = callback else {
3560 set_last_error("lumen_on: null callback");
3561 return 0;
3562 };
3563 let etype = match ffi_selector(event_type, "lumen_on") {
3564 Ok(s) => s,
3565 Err(_) => return 0,
3566 };
3567 let cb = CEventCallback {
3568 callback,
3569 user_data,
3570 };
3571 let invoke: std::sync::Arc<dyn Fn() + Send + Sync> = std::sync::Arc::new(move || {
3572 let cb = &cb;
3576 let ev = current_lumen_event();
3577 unsafe { (cb.callback)(&ev, cb.user_data) };
3579 });
3580 lumen_script::event::register_native_binding(node, etype, capture != 0, invoke)
3581 })
3582}
3583
3584#[unsafe(no_mangle)]
3587pub extern "C" fn lumen_off(token: LumenEventToken) -> LumenStatus {
3588 catch(|| {
3589 lumen_script::event::unregister_binding(token);
3590 LumenStatus::Ok
3591 })
3592}
3593
3594#[unsafe(no_mangle)]
3599pub unsafe extern "C" fn lumen_event_type(
3600 buf: *mut c_char,
3601 buf_len: usize,
3602 out_len: *mut usize,
3603) -> LumenStatus {
3604 catch(|| write_string_out(&lumen_script::event::event_type(), buf, buf_len, out_len))
3605}
3606
3607#[unsafe(no_mangle)]
3610pub unsafe extern "C" fn lumen_event_key(
3611 buf: *mut c_char,
3612 buf_len: usize,
3613 out_len: *mut usize,
3614) -> LumenStatus {
3615 catch(|| write_string_out(&lumen_script::event::event_key(), buf, buf_len, out_len))
3616}
3617
3618#[unsafe(no_mangle)]
3621pub unsafe extern "C" fn lumen_event_value(
3622 buf: *mut c_char,
3623 buf_len: usize,
3624 out_len: *mut usize,
3625) -> LumenStatus {
3626 catch(|| write_string_out(&lumen_script::event::event_value(), buf, buf_len, out_len))
3627}
3628
3629#[unsafe(no_mangle)]
3632pub extern "C" fn lumen_event_target() -> LumenNode {
3633 catch_val(0u64, lumen_script::event::event_target)
3634}
3635
3636#[unsafe(no_mangle)]
3638pub extern "C" fn lumen_event_current_target() -> LumenNode {
3639 catch_val(0u64, lumen_script::event::event_current_target)
3640}
3641
3642#[unsafe(no_mangle)]
3645pub extern "C" fn lumen_event_prevent_default() -> LumenStatus {
3646 catch(|| {
3647 lumen_script::event::event_prevent_default();
3648 LumenStatus::Ok
3649 })
3650}
3651
3652#[unsafe(no_mangle)]
3654pub extern "C" fn lumen_event_stop_propagation() -> LumenStatus {
3655 catch(|| {
3656 lumen_script::event::event_stop_propagation();
3657 LumenStatus::Ok
3658 })
3659}
3660
3661#[unsafe(no_mangle)]
3664pub extern "C" fn lumen_event_stop_immediate_propagation() -> LumenStatus {
3665 catch(|| {
3666 lumen_script::event::event_stop_immediate_propagation();
3667 LumenStatus::Ok
3668 })
3669}
3670
3671#[cfg(test)]
3672mod tests {
3673 use super::*;
3674
3675 static SIGNAL_STATE: Mutex<()> = Mutex::new(());
3681
3682 #[test]
3683 fn abi_version_is_packed_correctly() {
3684 let v = lumen_abi_version();
3685 assert_eq!(v >> 16, LUMEN_ABI_MAJOR);
3686 assert_eq!((v >> 8) & 0xff, LUMEN_ABI_MINOR);
3687 assert_eq!(v & 0xff, LUMEN_ABI_PATCH);
3688 }
3689
3690 #[test]
3691 fn user_data_round_trips() {
3692 let mut x = 7u32;
3693 let raw = &mut x as *mut u32 as *mut c_void;
3694 let ud = UserData::from_raw(raw);
3695 assert_eq!(ud.as_ptr(), raw);
3696 let null = UserData::from_raw(ptr::null_mut());
3697 assert!(null.as_ptr().is_null());
3698 }
3699
3700 #[test]
3701 fn last_error_thread_local_then_global() {
3702 set_last_error("test message");
3703 let p = unsafe { lumen_last_error() };
3704 assert!(!p.is_null());
3705 let s = unsafe { CStr::from_ptr(p) }.to_string_lossy().into_owned();
3706 assert_eq!(s, "test message");
3707
3708 let handle = std::thread::spawn(|| {
3710 let p = unsafe { lumen_last_error() };
3711 assert!(
3712 !p.is_null(),
3713 "global fallback should surface the prior thread's error"
3714 );
3715 let s = unsafe { CStr::from_ptr(p) }.to_string_lossy().into_owned();
3716 assert_eq!(s, "test message");
3717 });
3718 handle.join().unwrap();
3719 }
3720
3721 #[test]
3722 fn classify_runtime_error_picks_specific_variant() {
3723 assert_eq!(
3724 classify_runtime_error("css parse failed"),
3725 LumenStatus::ErrCss
3726 );
3727 assert_eq!(
3728 classify_runtime_error("HTML XML error"),
3729 LumenStatus::ErrParse
3730 );
3731 assert_eq!(
3732 classify_runtime_error("asset PNG decode failed"),
3733 LumenStatus::ErrAsset
3734 );
3735 assert_eq!(
3736 classify_runtime_error("window surface create"),
3737 LumenStatus::ErrWindow
3738 );
3739 assert_eq!(
3740 classify_runtime_error("script error in on_click"),
3741 LumenStatus::ErrScript
3742 );
3743 assert_eq!(classify_runtime_error("file io"), LumenStatus::ErrIo);
3744 assert_eq!(
3745 classify_runtime_error("completely opaque thing"),
3746 LumenStatus::ErrRuntime
3747 );
3748 }
3749
3750 #[test]
3751 fn a_flat_app_directory_is_a_path_error_not_a_css_error() {
3752 let err = RunError::Layout(lumen_runtime::app_layout::FlatLayout {
3755 dir: std::path::PathBuf::from("/apps/demo"),
3756 files: vec!["main.css".to_owned(), "main.lmn".to_owned()],
3757 });
3758 assert_eq!(classify_run_error(&err), LumenStatus::ErrBadPath);
3759 }
3760
3761 #[test]
3762 fn typed_signal_int64_round_trips() {
3763 let _serial = SIGNAL_STATE.lock().unwrap_or_else(|e| e.into_inner());
3764 unsafe {
3765 let name = CString::new("typed_int_test").unwrap();
3766 assert_eq!(lumen_signal_set_int64(name.as_ptr(), 1234), LumenStatus::Ok);
3767 let mut out: i64 = 0;
3768 assert_eq!(
3769 lumen_signal_get_int64(name.as_ptr(), &mut out),
3770 LumenStatus::Ok
3771 );
3772 assert_eq!(out, 1234);
3773 }
3774 }
3775
3776 #[test]
3777 fn typed_signal_float64_round_trips() {
3778 let _serial = SIGNAL_STATE.lock().unwrap_or_else(|e| e.into_inner());
3779 unsafe {
3780 let name = CString::new("typed_float_test").unwrap();
3781 assert_eq!(
3782 lumen_signal_set_float64(name.as_ptr(), 2.5),
3783 LumenStatus::Ok
3784 );
3785 let mut out: f64 = 0.0;
3786 assert_eq!(
3787 lumen_signal_get_float64(name.as_ptr(), &mut out),
3788 LumenStatus::Ok
3789 );
3790 assert_eq!(out, 2.5);
3791 }
3792 }
3793
3794 #[test]
3795 fn typed_signal_bool_round_trips() {
3796 let _serial = SIGNAL_STATE.lock().unwrap_or_else(|e| e.into_inner());
3797 unsafe {
3798 let name = CString::new("typed_bool_test").unwrap();
3799 assert_eq!(lumen_signal_set_bool(name.as_ptr(), true), LumenStatus::Ok);
3800 let mut out = false;
3801 assert_eq!(
3802 lumen_signal_get_bool(name.as_ptr(), &mut out),
3803 LumenStatus::Ok
3804 );
3805 assert!(out);
3806 }
3807 }
3808
3809 #[test]
3810 fn typed_signal_color_round_trips() {
3811 let _serial = SIGNAL_STATE.lock().unwrap_or_else(|e| e.into_inner());
3812 unsafe {
3813 let name = CString::new("typed_color_test").unwrap();
3814 let rgba = [0xffu8, 0x88, 0x00, 0xff];
3815 assert_eq!(
3816 lumen_signal_set_color(name.as_ptr(), rgba.as_ptr()),
3817 LumenStatus::Ok
3818 );
3819 let mut out = [0u8; 4];
3820 assert_eq!(
3821 lumen_signal_get_color(name.as_ptr(), out.as_mut_ptr()),
3822 LumenStatus::Ok
3823 );
3824 assert_eq!(out, rgba);
3825 }
3826 }
3827
3828 #[test]
3829 fn typed_signal_get_missing_returns_err_bad_arg() {
3830 let _serial = SIGNAL_STATE.lock().unwrap_or_else(|e| e.into_inner());
3831 unsafe {
3832 let name = CString::new("never_set_typed").unwrap();
3833 let mut out: i64 = -1;
3834 let s = lumen_signal_get_int64(name.as_ptr(), &mut out);
3835 assert_eq!(s, LumenStatus::ErrBadArg);
3836 }
3837 }
3838
3839 #[test]
3840 fn lumen_status_message_returns_known_strings() {
3841 unsafe {
3842 let p = lumen_status_message(LumenStatus::Ok);
3843 assert!(!p.is_null());
3844 let s = CStr::from_ptr(p).to_str().unwrap();
3845 assert_eq!(s, "ok");
3846 let p2 = lumen_status_message(LumenStatus::ErrBadArg);
3847 let s2 = CStr::from_ptr(p2).to_str().unwrap();
3848 assert!(s2.contains("argument"));
3849 }
3850 }
3851
3852 #[test]
3853 fn typed_setter_routes_through_external_property_bus() {
3854 let _serial = SIGNAL_STATE.lock().unwrap_or_else(|e| e.into_inner());
3855 use lumen_core::prelude::Schedule;
3864 use lumen_core::prelude::World;
3865 use lumen_core::property_store::{
3866 PropertyKey, PropertyStore, PropertyValue, drain_external_properties,
3867 init_external_properties,
3868 };
3869 init_external_properties();
3870 unsafe {
3871 let name = CString::new("ffi_pre_run_int").unwrap();
3872 assert_eq!(lumen_signal_set_int64(name.as_ptr(), 7777), LumenStatus::Ok);
3873 let name = CString::new("ffi_pre_run_str").unwrap();
3874 let value = CString::new("seeded").unwrap();
3875 assert_eq!(
3876 lumen_signal_set_str(name.as_ptr(), value.as_ptr()),
3877 LumenStatus::Ok
3878 );
3879 }
3880 let mut world = World::new();
3884 world.insert_resource(PropertyStore::default());
3885 let mut schedule = Schedule::default();
3886 schedule.add_systems(drain_external_properties);
3887 schedule.run(&mut world);
3888 let store = world.resource::<PropertyStore>();
3889 let cell = store.get(&PropertyKey::Global(Arc::<str>::from("ffi_pre_run_int")));
3890 assert!(
3891 matches!(cell, Some(PropertyValue::I64(7777))),
3892 "typed FFI setter must land a PropertyValue::I64 in PropertyStore; got {cell:?}"
3893 );
3894 let cell = store.get(&PropertyKey::Global(Arc::<str>::from("ffi_pre_run_str")));
3895 assert!(
3896 matches!(cell, Some(PropertyValue::Str(s)) if &**s == "seeded"),
3897 "the string setter must land a PropertyValue::Str in PropertyStore; got {cell:?}"
3898 );
3899 }
3900
3901 #[test]
3902 fn typed_read_falls_back_to_local_cache_when_bus_drained() {
3903 let _serial = SIGNAL_STATE.lock().unwrap_or_else(|e| e.into_inner());
3904 unsafe {
3910 let name = CString::new("ffi_cache_fallback_bool").unwrap();
3911 assert_eq!(lumen_signal_set_bool(name.as_ptr(), true), LumenStatus::Ok);
3912 }
3913 let _ = lumen_core::property_store::external_property_snapshot();
3917 unsafe {
3918 let name = CString::new("ffi_cache_fallback_bool").unwrap();
3919 let mut out = false;
3920 assert_eq!(
3921 lumen_signal_get_bool(name.as_ptr(), &mut out),
3922 LumenStatus::Ok
3923 );
3924 assert!(out, "typed read should fall back to cache after bus drain");
3925 }
3926 }
3927
3928 extern "C" fn noop_watch(_name: *const c_char, _value: *const LumenValue, _ud: *mut c_void) {}
3929
3930 #[test]
3931 fn signal_watch_registers_additively() {
3932 let _serial = SIGNAL_STATE.lock().unwrap_or_else(|e| e.into_inner());
3933 unsafe {
3939 let name = CString::new("watch_reg_test").unwrap();
3940 let cb: LumenWatchFn = noop_watch;
3941 assert_eq!(
3942 lumen_signal_watch(name.as_ptr(), Some(cb), ptr::null_mut()),
3943 LumenStatus::Ok
3944 );
3945 assert_eq!(
3947 lumen_signal_watch(name.as_ptr(), Some(cb), ptr::null_mut()),
3948 LumenStatus::Ok
3949 );
3950 }
3951 let reg = signal_watchers().lock().unwrap();
3953 assert_eq!(reg.get("watch_reg_test").map(Vec::len), Some(2));
3954 }
3955
3956 #[test]
3957 fn property_to_lumen_encodes_each_variant() {
3958 let mut keep: Option<CString> = None;
3959 assert_eq!(
3960 property_to_lumen(&PropertyValue::I64(7), &mut keep).kind,
3961 LumenKind::Int
3962 );
3963 assert_eq!(
3964 property_to_lumen(&PropertyValue::Bool(true), &mut keep).kind,
3965 LumenKind::Bool
3966 );
3967 assert_eq!(
3968 property_to_lumen(&PropertyValue::F64(1.5), &mut keep).kind,
3969 LumenKind::Float
3970 );
3971 let s = property_to_lumen(&PropertyValue::Str(Arc::<str>::from("hi")), &mut keep);
3972 assert_eq!(s.kind, LumenKind::String);
3973 assert!(keep.is_some());
3974 let c = property_to_lumen(
3976 &PropertyValue::Color(Color::rgba(1.0, 0.0, 0.0, 1.0)),
3977 &mut keep,
3978 );
3979 assert_eq!(c.kind, LumenKind::Int);
3980 assert_eq!(unsafe { c.as_.integer } & 0xff, 0xff); assert_eq!((unsafe { c.as_.integer } >> 24) & 0xff, 0xff); }
3983
3984 #[test]
3985 fn status_codes_are_stable() {
3986 assert_eq!(LumenStatus::Ok as u32, 0);
3989 assert_eq!(LumenStatus::ErrBadPath as u32, 1);
3990 assert_eq!(LumenStatus::ErrBadArg as u32, 2);
3991 assert_eq!(LumenStatus::ErrRuntime as u32, 3);
3992 assert_eq!(LumenStatus::ErrInternal as u32, 4);
3993 assert_eq!(LumenStatus::ErrParse as u32, 5);
3994 assert_eq!(LumenStatus::ErrCss as u32, 6);
3995 assert_eq!(LumenStatus::ErrAsset as u32, 7);
3996 assert_eq!(LumenStatus::ErrWindow as u32, 8);
3997 assert_eq!(LumenStatus::ErrScript as u32, 9);
3998 assert_eq!(LumenStatus::ErrIo as u32, 10);
3999 assert_eq!(LumenStatus::ErrInvalidHandle as u32, 11);
4000 assert_eq!(LumenStatus::ErrInvalidValue as u32, 12);
4001 assert_eq!(LumenStatus::ErrPanic as u32, 13);
4002 assert_eq!(LumenStatus::ErrBufferTooSmall as u32, 14);
4003 }
4004
4005 #[test]
4006 fn typed_signal_str_round_trips() {
4007 let _serial = SIGNAL_STATE.lock().unwrap_or_else(|e| e.into_inner());
4008 unsafe {
4009 let name = CString::new("ffi_get_str_test").unwrap();
4010 let value = CString::new("hello world").unwrap();
4011 assert_eq!(
4012 lumen_signal_set_str(name.as_ptr(), value.as_ptr()),
4013 LumenStatus::Ok
4014 );
4015 let mut needed: usize = 0;
4017 assert_eq!(
4018 lumen_signal_get_str(name.as_ptr(), ptr::null_mut(), 0, &mut needed),
4019 LumenStatus::ErrBufferTooSmall
4020 );
4021 assert_eq!(needed, "hello world".len() + 1);
4022 let mut buf = vec![0i8; needed];
4024 let mut out_len: usize = 0;
4025 assert_eq!(
4026 lumen_signal_get_str(name.as_ptr(), buf.as_mut_ptr(), buf.len(), &mut out_len),
4027 LumenStatus::Ok
4028 );
4029 assert_eq!(out_len, "hello world".len());
4030 let got = CStr::from_ptr(buf.as_ptr()).to_str().unwrap();
4031 assert_eq!(got, "hello world");
4032 }
4033 }
4034
4035 #[test]
4036 fn clear_leaves_an_empty_string_signal() {
4037 let _serial = SIGNAL_STATE.lock().unwrap_or_else(|e| e.into_inner());
4038 unsafe {
4039 let name = CString::new("ffi_clear_str_test").unwrap();
4040 let value = CString::new("filled").unwrap();
4041 assert_eq!(
4042 lumen_signal_set_str(name.as_ptr(), value.as_ptr()),
4043 LumenStatus::Ok
4044 );
4045 assert_eq!(lumen_signal_clear(name.as_ptr()), LumenStatus::Ok);
4046 let mut out_len: usize = 0;
4047 let mut buf = [0i8; 8];
4048 assert_eq!(
4049 lumen_signal_get_str(name.as_ptr(), buf.as_mut_ptr(), buf.len(), &mut out_len),
4050 LumenStatus::Ok
4051 );
4052 assert_eq!(out_len, 0);
4053 }
4054 }
4055
4056 #[test]
4057 fn typed_getters_reject_a_type_mismatch() {
4058 let _serial = SIGNAL_STATE.lock().unwrap_or_else(|e| e.into_inner());
4059 unsafe {
4060 let name = CString::new("ffi_mismatch_test").unwrap();
4061 let value = CString::new("not a number").unwrap();
4062 assert_eq!(
4063 lumen_signal_set_str(name.as_ptr(), value.as_ptr()),
4064 LumenStatus::Ok
4065 );
4066 let mut out: i64 = 0;
4067 assert_eq!(
4068 lumen_signal_get_int64(name.as_ptr(), &mut out),
4069 LumenStatus::ErrBadArg
4070 );
4071 }
4072 }
4073
4074 #[test]
4075 fn array_signal_len_and_field_round_trip() {
4076 let _serial = SIGNAL_STATE.lock().unwrap_or_else(|e| e.into_inner());
4077 unsafe {
4078 let key = CString::new("name").unwrap();
4080 let va = CString::new("a").unwrap();
4081 let vb = CString::new("b").unwrap();
4082 let row0 = [LumenMapEntry {
4083 key: key.as_ptr(),
4084 value: LumenValue {
4085 kind: LumenKind::String,
4086 as_: LumenValueData {
4087 string: va.as_ptr(),
4088 },
4089 },
4090 }];
4091 let row1 = [LumenMapEntry {
4092 key: key.as_ptr(),
4093 value: LumenValue {
4094 kind: LumenKind::String,
4095 as_: LumenValueData {
4096 string: vb.as_ptr(),
4097 },
4098 },
4099 }];
4100 let items = [
4101 LumenValue {
4102 kind: LumenKind::Map,
4103 as_: LumenValueData {
4104 map: LumenMapView {
4105 entries: row0.as_ptr(),
4106 len: 1,
4107 },
4108 },
4109 },
4110 LumenValue {
4111 kind: LumenKind::Map,
4112 as_: LumenValueData {
4113 map: LumenMapView {
4114 entries: row1.as_ptr(),
4115 len: 1,
4116 },
4117 },
4118 },
4119 ];
4120 let arr = LumenValue {
4121 kind: LumenKind::Array,
4122 as_: LumenValueData {
4123 array: LumenArrayView {
4124 items: items.as_ptr(),
4125 len: 2,
4126 },
4127 },
4128 };
4129 let name = CString::new("ffi_array_test").unwrap();
4130 assert_eq!(lumen_signal_set_array(name.as_ptr(), &arr), LumenStatus::Ok);
4131
4132 let mut len: usize = 0;
4133 assert_eq!(
4134 lumen_signal_array_len(name.as_ptr(), &mut len),
4135 LumenStatus::Ok
4136 );
4137 assert_eq!(len, 2);
4138
4139 let mut buf = [0i8; 8];
4140 let mut out_len: usize = 0;
4141 assert_eq!(
4142 lumen_signal_array_get_field(
4143 name.as_ptr(),
4144 1,
4145 key.as_ptr(),
4146 buf.as_mut_ptr(),
4147 buf.len(),
4148 &mut out_len
4149 ),
4150 LumenStatus::Ok
4151 );
4152 assert_eq!(CStr::from_ptr(buf.as_ptr()).to_str().unwrap(), "b");
4153 }
4154 }
4155}