1use std::{
8 panic::{AssertUnwindSafe, catch_unwind},
9 sync::{Mutex, OnceLock},
10};
11
12use konjure_sdk::{EntityId, Event, Geometry, Mesh, Runtime, SceneDocument, compile, validate};
13use serde::Serialize;
14
15pub const KJ_ABI_VERSION: u32 = 1;
16const MAX_JSON_BYTES: usize = 1024 * 1024;
17const DISPATCH_RECEIPT_CAPACITY: usize = MAX_JSON_BYTES;
21
22#[repr(C)]
23#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
24pub struct KjHandle {
25 pub slot: u32,
26 pub generation: u32,
27}
28
29#[repr(C)]
30#[derive(Clone, Copy, Debug, PartialEq, Eq)]
31pub enum KjStatus {
32 Ok = 0,
33 InvalidArgument = 1,
34 InvalidHandle = 2,
35 BufferTooSmall = 3,
36 CompileError = 4,
37 RuntimeError = 5,
38 InternalError = 6,
39}
40
41#[derive(Serialize)]
42struct MeshBatch<'a> {
43 entries: Vec<MeshEntry<'a>>,
44}
45
46#[derive(Serialize)]
47struct MeshEntry<'a> {
48 entity: &'a EntityId,
49 topology: MeshTopology,
50 mesh: &'a Mesh,
51}
52
53#[derive(Serialize)]
54#[serde(rename_all = "snake_case")]
55enum MeshTopology {
56 Triangles,
57 Lines,
58}
59
60struct Entry {
61 generation: u32,
62 runtime: Option<Runtime>,
63}
64
65#[derive(Default)]
66struct Registry {
67 entries: Vec<Entry>,
68}
69
70impl Registry {
71 fn insert(&mut self, runtime: Runtime) -> KjHandle {
72 for (slot, entry) in self.entries.iter_mut().enumerate() {
73 if entry.runtime.is_none() {
74 entry.runtime = Some(runtime);
75 return KjHandle {
76 slot: u32::try_from(slot).expect("registry slot fits u32"),
77 generation: entry.generation,
78 };
79 }
80 }
81 let slot = u32::try_from(self.entries.len()).expect("registry slot fits u32");
82 self.entries.push(Entry {
83 generation: 1,
84 runtime: Some(runtime),
85 });
86 KjHandle {
87 slot,
88 generation: 1,
89 }
90 }
91
92 fn get(&self, handle: KjHandle) -> Option<&Runtime> {
93 self.entries
94 .get(usize::try_from(handle.slot).ok()?)
95 .filter(|entry| entry.generation == handle.generation)
96 .and_then(|entry| entry.runtime.as_ref())
97 }
98
99 fn get_mut(&mut self, handle: KjHandle) -> Option<&mut Runtime> {
100 self.entries
101 .get_mut(usize::try_from(handle.slot).ok()?)
102 .filter(|entry| entry.generation == handle.generation)
103 .and_then(|entry| entry.runtime.as_mut())
104 }
105
106 fn remove(&mut self, handle: KjHandle) -> bool {
107 let Some(entry) = self
108 .entries
109 .get_mut(usize::try_from(handle.slot).ok().unwrap_or(usize::MAX))
110 else {
111 return false;
112 };
113 if entry.generation != handle.generation || entry.runtime.take().is_none() {
114 return false;
115 }
116 entry.generation = entry.generation.wrapping_add(1).max(1);
117 true
118 }
119}
120
121fn registry() -> &'static Mutex<Registry> {
122 static REGISTRY: OnceLock<Mutex<Registry>> = OnceLock::new();
123 REGISTRY.get_or_init(|| Mutex::new(Registry::default()))
124}
125
126fn write_bytes(bytes: &[u8], output: *mut u8, capacity: usize, required: *mut usize) -> KjStatus {
127 if required.is_null() {
128 return KjStatus::InvalidArgument;
129 }
130 unsafe {
132 *required = bytes.len();
133 }
134 if bytes.is_empty() {
135 return KjStatus::Ok;
136 }
137 if output.is_null() || capacity < bytes.len() {
138 return KjStatus::BufferTooSmall;
139 }
140 unsafe {
142 std::ptr::copy_nonoverlapping(bytes.as_ptr(), output, bytes.len());
143 }
144 KjStatus::Ok
145}
146
147fn write_error(message: &str, output: *mut u8, capacity: usize, required: *mut usize) {
148 let _ = write_bytes(message.as_bytes(), output, capacity, required);
149}
150
151fn read_utf8<'a>(input: *const u8, length: usize, max: usize) -> Result<&'a str, KjStatus> {
152 if length > max || (length > 0 && input.is_null()) {
153 return Err(KjStatus::InvalidArgument);
154 }
155 if length == 0 {
158 return Ok("");
159 }
160 let bytes = unsafe { std::slice::from_raw_parts(input, length) };
162 std::str::from_utf8(bytes).map_err(|_| KjStatus::InvalidArgument)
163}
164
165fn contained(
166 error: *mut u8,
167 error_capacity: usize,
168 error_required: *mut usize,
169 operation: impl FnOnce() -> Result<KjStatus, (KjStatus, String)>,
170) -> KjStatus {
171 match catch_unwind(AssertUnwindSafe(operation)) {
172 Ok(Ok(status)) => {
173 write_error("", error, error_capacity, error_required);
174 status
175 }
176 Ok(Err((status, message))) => {
177 write_error(&message, error, error_capacity, error_required);
178 status
179 }
180 Err(_) => {
181 write_error(
182 "internal Konjure panic contained at FFI boundary",
183 error,
184 error_capacity,
185 error_required,
186 );
187 KjStatus::InternalError
188 }
189 }
190}
191
192fn serialize<T: serde::Serialize>(
193 value: &T,
194 output: *mut u8,
195 capacity: usize,
196 required: *mut usize,
197) -> Result<KjStatus, (KjStatus, String)> {
198 serde_json::to_vec(value)
199 .map(|bytes| write_bytes(&bytes, output, capacity, required))
200 .map_err(|error| (KjStatus::InternalError, error.to_string()))
201}
202
203#[derive(Clone, Copy)]
204struct OutputBuffer {
205 output: *mut u8,
206 capacity: usize,
207 required: *mut usize,
208}
209
210#[derive(Clone, Copy)]
211struct ErrorBuffer {
212 output: *mut u8,
213 capacity: usize,
214 required: *mut usize,
215}
216
217#[unsafe(no_mangle)]
219pub extern "C" fn kj_abi_version() -> u32 {
220 KJ_ABI_VERSION
221}
222
223#[unsafe(no_mangle)]
229pub unsafe extern "C" fn kj_runtime_compile(
230 source: *const u8,
231 source_len: usize,
232 out_handle: *mut KjHandle,
233 error: *mut u8,
234 error_capacity: usize,
235 error_required: *mut usize,
236) -> KjStatus {
237 contained(error, error_capacity, error_required, || {
238 if out_handle.is_null() {
239 return Err((KjStatus::InvalidArgument, "out_handle is required".into()));
240 }
241 let source = read_utf8(source, source_len, konjure_sdk::MAX_SOURCE_BYTES)
242 .map_err(|status| (status, "source must be valid bounded UTF-8".into()))?;
243 let scene = compile(source).map_err(|error| (KjStatus::CompileError, error.to_string()))?;
244 let runtime =
245 Runtime::new(scene).map_err(|error| (KjStatus::RuntimeError, error.to_string()))?;
246 let handle = registry()
247 .lock()
248 .map_err(|_| (KjStatus::InternalError, "runtime registry poisoned".into()))?
249 .insert(runtime);
250 unsafe {
252 *out_handle = handle;
253 }
254 Ok(KjStatus::Ok)
255 })
256}
257
258#[unsafe(no_mangle)]
263pub unsafe extern "C" fn kj_runtime_from_document_json(
264 document: *const u8,
265 document_len: usize,
266 out_handle: *mut KjHandle,
267 error: *mut u8,
268 error_capacity: usize,
269 error_required: *mut usize,
270) -> KjStatus {
271 contained(error, error_capacity, error_required, || {
272 if out_handle.is_null() {
273 return Err((KjStatus::InvalidArgument, "out_handle is required".into()));
274 }
275 let document = read_utf8(document, document_len, MAX_JSON_BYTES)
276 .map_err(|status| (status, "document must be valid bounded UTF-8".into()))?;
277 let scene: SceneDocument = serde_json::from_str(document)
278 .map_err(|error| (KjStatus::InvalidArgument, error.to_string()))?;
279 validate(&scene).map_err(|error| (KjStatus::RuntimeError, error.to_string()))?;
280 let handle = registry()
281 .lock()
282 .map_err(|_| (KjStatus::InternalError, "runtime registry poisoned".into()))?
283 .insert(Runtime::new(scene).expect("validated scene constructs runtime"));
284 unsafe {
286 *out_handle = handle;
287 }
288 Ok(KjStatus::Ok)
289 })
290}
291
292#[unsafe(no_mangle)]
294pub extern "C" fn kj_runtime_dispose(
295 handle: KjHandle,
296 error: *mut u8,
297 error_capacity: usize,
298 error_required: *mut usize,
299) -> KjStatus {
300 contained(error, error_capacity, error_required, || {
301 if registry()
302 .lock()
303 .map_err(|_| (KjStatus::InternalError, "runtime registry poisoned".into()))?
304 .remove(handle)
305 {
306 Ok(KjStatus::Ok)
307 } else {
308 Err((
309 KjStatus::InvalidHandle,
310 "invalid or disposed runtime handle".into(),
311 ))
312 }
313 })
314}
315
316fn query(
317 handle: KjHandle,
318 output: OutputBuffer,
319 error: ErrorBuffer,
320 operation: impl FnOnce(&Runtime) -> Result<Vec<u8>, String>,
321) -> KjStatus {
322 contained(error.output, error.capacity, error.required, || {
323 let registry = registry()
324 .lock()
325 .map_err(|_| (KjStatus::InternalError, "runtime registry poisoned".into()))?;
326 let runtime = registry.get(handle).ok_or((
327 KjStatus::InvalidHandle,
328 "invalid or disposed runtime handle".into(),
329 ))?;
330 let bytes = operation(runtime).map_err(|message| (KjStatus::RuntimeError, message))?;
331 Ok(write_bytes(
332 &bytes,
333 output.output,
334 output.capacity,
335 output.required,
336 ))
337 })
338}
339
340#[unsafe(no_mangle)]
342pub extern "C" fn kj_runtime_scene_json(
343 handle: KjHandle,
344 output: *mut u8,
345 capacity: usize,
346 required: *mut usize,
347 error: *mut u8,
348 error_capacity: usize,
349 error_required: *mut usize,
350) -> KjStatus {
351 query(
352 handle,
353 OutputBuffer {
354 output,
355 capacity,
356 required,
357 },
358 ErrorBuffer {
359 output: error,
360 capacity: error_capacity,
361 required: error_required,
362 },
363 |runtime| serde_json::to_vec(runtime.scene()).map_err(|error| error.to_string()),
364 )
365}
366
367#[unsafe(no_mangle)]
369pub extern "C" fn kj_runtime_meshes_json(
370 handle: KjHandle,
371 output: *mut u8,
372 capacity: usize,
373 required: *mut usize,
374 error: *mut u8,
375 error_capacity: usize,
376 error_required: *mut usize,
377) -> KjStatus {
378 query(
379 handle,
380 OutputBuffer {
381 output,
382 capacity,
383 required,
384 },
385 ErrorBuffer {
386 output: error,
387 capacity: error_capacity,
388 required: error_required,
389 },
390 |runtime| {
391 let entries = runtime
392 .meshes()
393 .iter()
394 .map(|(entity, mesh)| {
395 let is_line = runtime
396 .scene()
397 .entities
398 .iter()
399 .find(|candidate| candidate.id == *entity)
400 .is_some_and(|candidate| {
401 matches!(&candidate.geometry, Geometry::Line { .. })
402 });
403 let topology = if is_line {
404 MeshTopology::Lines
405 } else {
406 MeshTopology::Triangles
407 };
408 MeshEntry {
409 entity,
410 topology,
411 mesh,
412 }
413 })
414 .collect();
415 serde_json::to_vec(&MeshBatch { entries }).map_err(|error| error.to_string())
416 },
417 )
418}
419
420#[unsafe(no_mangle)]
422pub extern "C" fn kj_runtime_sample_json(
423 handle: KjHandle,
424 time_seconds: f64,
425 output: *mut u8,
426 capacity: usize,
427 required: *mut usize,
428 error: *mut u8,
429 error_capacity: usize,
430 error_required: *mut usize,
431) -> KjStatus {
432 query(
433 handle,
434 OutputBuffer {
435 output,
436 capacity,
437 required,
438 },
439 ErrorBuffer {
440 output: error,
441 capacity: error_capacity,
442 required: error_required,
443 },
444 |runtime| {
445 runtime
446 .sample(time_seconds)
447 .and_then(|frame| {
448 serde_json::to_vec(&frame).map_err(|error| konjure_sdk::Diagnostic {
449 message: error.to_string(),
450 span: None,
451 })
452 })
453 .map_err(|error| error.to_string())
454 },
455 )
456}
457
458#[unsafe(no_mangle)]
463pub unsafe extern "C" fn kj_runtime_dispatch_json(
464 handle: KjHandle,
465 event: *const u8,
466 event_len: usize,
467 output: *mut u8,
468 capacity: usize,
469 required: *mut usize,
470 error: *mut u8,
471 error_capacity: usize,
472 error_required: *mut usize,
473) -> KjStatus {
474 contained(error, error_capacity, error_required, || {
475 let event = read_utf8(event, event_len, MAX_JSON_BYTES)
476 .map_err(|status| (status, "event must be valid bounded UTF-8".into()))?;
477 let event: Event = serde_json::from_str(event)
478 .map_err(|error| (KjStatus::InvalidArgument, error.to_string()))?;
479 if required.is_null() {
480 return Err((KjStatus::InvalidArgument, "required is required".into()));
481 }
482 unsafe {
484 *required = DISPATCH_RECEIPT_CAPACITY;
485 }
486 if output.is_null() || capacity < DISPATCH_RECEIPT_CAPACITY {
487 return Ok(KjStatus::BufferTooSmall);
488 }
489 let mut registry = registry()
490 .lock()
491 .map_err(|_| (KjStatus::InternalError, "runtime registry poisoned".into()))?;
492 let runtime = registry.get_mut(handle).ok_or((
493 KjStatus::InvalidHandle,
494 "invalid or disposed runtime handle".into(),
495 ))?;
496 serialize(
497 &runtime
498 .dispatch(event)
499 .map_err(|error| (KjStatus::RuntimeError, error.to_string()))?,
500 output,
501 capacity,
502 required,
503 )
504 })
505}
506
507#[cfg(test)]
508mod tests {
509 use super::*;
510
511 const SOURCE: &str =
512 "scene orbit { node star { sphere 0.2m; color #ffffff; action pause toggle_motion; } }";
513
514 fn compile_runtime() -> KjHandle {
515 let mut handle = KjHandle::default();
516 let mut error_len = 0;
517 assert_eq!(
518 unsafe {
519 kj_runtime_compile(
520 SOURCE.as_ptr(),
521 SOURCE.len(),
522 &mut handle,
523 std::ptr::null_mut(),
524 0,
525 &mut error_len,
526 )
527 },
528 KjStatus::Ok
529 );
530 handle
531 }
532
533 #[test]
534 fn abi_and_two_pass_buffers_are_stable() {
535 assert_eq!(kj_abi_version(), KJ_ABI_VERSION);
536 let handle = compile_runtime();
537 let mut required = 0;
538 let mut error_len = 0;
539 assert_eq!(
540 kj_runtime_sample_json(
541 handle,
542 0.0,
543 std::ptr::null_mut(),
544 0,
545 &mut required,
546 std::ptr::null_mut(),
547 0,
548 &mut error_len
549 ),
550 KjStatus::BufferTooSmall
551 );
552 let mut buffer = vec![0; required];
553 assert_eq!(
554 kj_runtime_sample_json(
555 handle,
556 0.0,
557 buffer.as_mut_ptr(),
558 buffer.len(),
559 &mut required,
560 std::ptr::null_mut(),
561 0,
562 &mut error_len
563 ),
564 KjStatus::Ok
565 );
566 assert!(
567 std::str::from_utf8(&buffer)
568 .unwrap()
569 .contains("time_seconds")
570 );
571 assert_eq!(
572 kj_runtime_dispose(handle, std::ptr::null_mut(), 0, &mut error_len),
573 KjStatus::Ok
574 );
575 }
576
577 #[test]
578 fn rejects_bad_handles_and_double_dispose() {
579 let handle = KjHandle {
580 slot: 99,
581 generation: 1,
582 };
583 let mut error_len = 0;
584 assert_eq!(
585 kj_runtime_dispose(handle, std::ptr::null_mut(), 0, &mut error_len),
586 KjStatus::InvalidHandle
587 );
588 let handle = compile_runtime();
589 assert_eq!(
590 kj_runtime_dispose(handle, std::ptr::null_mut(), 0, &mut error_len),
591 KjStatus::Ok
592 );
593 assert_eq!(
594 kj_runtime_dispose(handle, std::ptr::null_mut(), 0, &mut error_len),
595 KjStatus::InvalidHandle
596 );
597 }
598
599 #[test]
600 fn reports_invalid_utf8_per_call() {
601 let invalid = [0xff];
602 let mut handle = KjHandle::default();
603 let mut error_len = 0;
604 assert_eq!(
605 unsafe {
606 kj_runtime_compile(
607 invalid.as_ptr(),
608 invalid.len(),
609 &mut handle,
610 std::ptr::null_mut(),
611 0,
612 &mut error_len,
613 )
614 },
615 KjStatus::InvalidArgument
616 );
617 assert!(error_len > 0);
618 }
619
620 #[test]
621 fn accepts_null_empty_input_without_undefined_behavior() {
622 let mut handle = KjHandle::default();
623 let mut error_len = 0;
624 assert_eq!(
625 unsafe {
626 kj_runtime_compile(
627 std::ptr::null(),
628 0,
629 &mut handle,
630 std::ptr::null_mut(),
631 0,
632 &mut error_len,
633 )
634 },
635 KjStatus::CompileError
636 );
637 assert!(error_len > 0);
638 }
639
640 #[test]
641 fn undersized_dispatch_output_does_not_apply_the_event() {
642 let handle = compile_runtime();
643 let mut required = 0;
644 let mut error_len = 0;
645 let event = br#"{"kind":"invoke_action","entity":"star","action":"pause"}"#;
646 assert_eq!(
647 unsafe {
648 kj_runtime_dispatch_json(
649 handle,
650 event.as_ptr(),
651 event.len(),
652 std::ptr::null_mut(),
653 0,
654 &mut required,
655 std::ptr::null_mut(),
656 0,
657 &mut error_len,
658 )
659 },
660 KjStatus::BufferTooSmall
661 );
662 assert_eq!(required, DISPATCH_RECEIPT_CAPACITY);
663 let mut frame_required = 0;
664 assert_eq!(
665 kj_runtime_sample_json(
666 handle,
667 0.0,
668 std::ptr::null_mut(),
669 0,
670 &mut frame_required,
671 std::ptr::null_mut(),
672 0,
673 &mut error_len
674 ),
675 KjStatus::BufferTooSmall
676 );
677 let mut frame = vec![0; frame_required];
678 assert_eq!(
679 kj_runtime_sample_json(
680 handle,
681 0.0,
682 frame.as_mut_ptr(),
683 frame.len(),
684 &mut frame_required,
685 std::ptr::null_mut(),
686 0,
687 &mut error_len
688 ),
689 KjStatus::Ok
690 );
691 assert!(
692 !std::str::from_utf8(&frame)
693 .unwrap()
694 .contains("action_applied")
695 );
696 let mut receipt = vec![0; required];
697 assert_eq!(
698 unsafe {
699 kj_runtime_dispatch_json(
700 handle,
701 event.as_ptr(),
702 event.len(),
703 receipt.as_mut_ptr(),
704 receipt.len(),
705 &mut required,
706 std::ptr::null_mut(),
707 0,
708 &mut error_len,
709 )
710 },
711 KjStatus::Ok
712 );
713 assert!(
714 std::str::from_utf8(&receipt[..required])
715 .unwrap()
716 .contains("action_applied")
717 );
718 }
719}