1#![allow(unsafe_code)]
6
7use serde_json::Value;
22use std::cell::RefCell;
23use std::ffi::{CStr, CString};
24use std::os::raw::{c_char, c_void};
25use std::sync::Arc;
26use webui_handler::plugin::fast_v2::FastV2HydrationPlugin;
27use webui_handler::plugin::fast_v3::FastV3HydrationPlugin;
28use webui_handler::plugin::webui::WebUIHydrationPlugin;
29use webui_handler::{
30 BoundaryDescriptor, BoundaryInstanceId, BoundaryKey, BoundaryMode, Protocol, RenderOptions,
31 SessionOptions, StreamStep, StreamingSession, WebUIHandler,
32};
33
34#[allow(non_camel_case_types)]
36pub type webui_protocol_t = c_void;
37
38thread_local! {
43 static LAST_ERROR: RefCell<Option<CString>> = const { RefCell::new(None) };
45}
46
47fn set_last_error(msg: impl Into<String>) {
49 let mut bytes = msg.into().into_bytes();
50 if let Some(nul_pos) = bytes.iter().position(|byte| *byte == 0) {
51 bytes.truncate(nul_pos);
52 }
53
54 let c_string = unsafe { CString::from_vec_unchecked(bytes) };
56 LAST_ERROR.with(|cell| {
57 cell.replace(Some(c_string));
58 });
59}
60
61fn clear_last_error() {
63 LAST_ERROR.with(|cell| {
64 cell.replace(None);
65 });
66}
67
68struct HandlerContext {
74 handler: Arc<WebUIHandler>,
75 nonce: Option<String>,
77}
78
79struct ProtocolContext {
81 protocol: Arc<Protocol>,
82}
83
84webui_handler::define_string_response_writer!(StringResponseWriter, content);
85
86#[no_mangle]
99pub extern "C" fn webui_last_error() -> *const c_char {
100 LAST_ERROR.with(|cell| {
101 let borrow = cell.borrow();
102 match borrow.as_ref() {
103 Some(c_str) => c_str.as_ptr(),
104 None => std::ptr::null(),
105 }
106 })
107}
108
109#[no_mangle]
118pub extern "C" fn webui_handler_create() -> *mut c_void {
119 let handler = WebUIHandler::new();
120 let context = Box::new(HandlerContext {
121 handler: Arc::new(handler),
122 nonce: None,
123 });
124 Box::into_raw(context) as *mut c_void
125}
126
127#[no_mangle]
144pub unsafe extern "C" fn webui_handler_create_with_plugin(plugin_id: *const c_char) -> *mut c_void {
145 clear_last_error();
146
147 let handler = if plugin_id.is_null() {
148 WebUIHandler::new()
149 } else {
150 match CStr::from_ptr(plugin_id).to_str() {
151 Ok("fast" | "fast-v2") => {
152 WebUIHandler::with_plugin(|| Box::new(FastV2HydrationPlugin::new()))
153 }
154 Ok("fast-v3") => WebUIHandler::with_plugin(|| Box::new(FastV3HydrationPlugin::new())),
155 Ok("webui") => WebUIHandler::with_plugin(|| Box::new(WebUIHydrationPlugin::new())),
156 Ok(unknown) => {
157 set_last_error(format!(
158 "unknown plugin: {unknown}. Use \"webui\", \"fast-v3\", \"fast-v2\", or \"fast\"."
159 ));
160 return std::ptr::null_mut();
161 }
162 Err(e) => {
163 set_last_error(format!("invalid UTF-8 in plugin_id: {e}"));
164 return std::ptr::null_mut();
165 }
166 }
167 };
168
169 let context = Box::new(HandlerContext {
170 handler: Arc::new(handler),
171 nonce: None,
172 });
173 Box::into_raw(context) as *mut c_void
174}
175
176#[no_mangle]
183pub unsafe extern "C" fn webui_handler_destroy(handler_ptr: *mut c_void) {
184 if !handler_ptr.is_null() {
185 let _ = Box::from_raw(handler_ptr as *mut HandlerContext);
186 }
187}
188
189#[no_mangle]
198pub unsafe extern "C" fn webui_protocol_create(
199 protocol_data: *const u8,
200 protocol_len: usize,
201) -> *mut webui_protocol_t {
202 clear_last_error();
203 if protocol_data.is_null() {
204 set_last_error("protocol_data is null");
205 return std::ptr::null_mut();
206 }
207
208 match std::panic::catch_unwind(|| {
209 let bytes = unsafe { std::slice::from_raw_parts(protocol_data, protocol_len) };
211 match Protocol::from_protobuf(bytes) {
212 Ok(protocol) => Box::into_raw(Box::new(ProtocolContext {
213 protocol: Arc::new(protocol),
214 })) as *mut webui_protocol_t,
215 Err(error) => {
216 set_last_error(format!("failed to parse protobuf protocol: {error}"));
217 std::ptr::null_mut()
218 }
219 }
220 }) {
221 Ok(ptr) => ptr,
222 Err(_) => {
223 set_last_error("panic in webui_protocol_create");
224 std::ptr::null_mut()
225 }
226 }
227}
228
229#[no_mangle]
236pub unsafe extern "C" fn webui_protocol_destroy(protocol_ptr: *mut webui_protocol_t) {
237 if !protocol_ptr.is_null() {
238 let _ = unsafe { Box::from_raw(protocol_ptr as *mut ProtocolContext) };
241 }
242}
243
244#[no_mangle]
264pub unsafe extern "C" fn webui_handler_set_nonce(handler_ptr: *mut c_void, nonce: *const c_char) {
265 clear_last_error();
266
267 if handler_ptr.is_null() {
268 set_last_error("handler_ptr is null");
269 return;
270 }
271
272 match std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
273 let context = &mut *(handler_ptr as *mut HandlerContext);
275
276 if nonce.is_null() {
277 context.nonce = None;
278 } else {
279 match CStr::from_ptr(nonce).to_str() {
281 Ok(s) => context.nonce = Some(s.to_string()),
282 Err(e) => {
283 set_last_error(format!("invalid UTF-8 in nonce: {e}"));
284 }
285 }
286 }
287 })) {
288 Ok(()) => {}
289 Err(_) => {
290 set_last_error("panic in webui_handler_set_nonce");
291 }
292 }
293}
294
295#[no_mangle]
307pub unsafe extern "C" fn webui_handler_render(
308 handler_ptr: *mut c_void,
309 protocol_ptr: *const webui_protocol_t,
310 data_json: *const c_char,
311 entry_id: *const c_char,
312 request_path: *const c_char,
313) -> *mut c_char {
314 clear_last_error();
315
316 match std::panic::catch_unwind(|| {
317 if handler_ptr.is_null()
318 || protocol_ptr.is_null()
319 || data_json.is_null()
320 || entry_id.is_null()
321 || request_path.is_null()
322 {
323 set_last_error("one or more required arguments are null");
324 return std::ptr::null_mut();
325 }
326
327 let context = unsafe { &*(handler_ptr as *const HandlerContext) };
329 let protocol_context = unsafe { &*(protocol_ptr as *const ProtocolContext) };
330 unsafe {
332 render_decoded_protocol(
333 context,
334 &protocol_context.protocol,
335 data_json,
336 entry_id,
337 request_path,
338 )
339 }
340 }) {
341 Ok(ptr) => ptr,
342 Err(_) => {
343 set_last_error("panic in webui_handler_render");
344 std::ptr::null_mut()
345 }
346 }
347}
348
349unsafe fn render_decoded_protocol(
350 context: &HandlerContext,
351 protocol: &Protocol,
352 data_json: *const c_char,
353 entry_id: *const c_char,
354 request_path: *const c_char,
355) -> *mut c_char {
356 let data_str = match unsafe { CStr::from_ptr(data_json) }.to_str() {
358 Ok(s) => s,
359 Err(e) => {
360 set_last_error(format!("invalid UTF-8 in data_json: {e}"));
361 return std::ptr::null_mut();
362 }
363 };
364 let entry_str = match unsafe { CStr::from_ptr(entry_id) }.to_str() {
366 Ok(s) => s,
367 Err(e) => {
368 set_last_error(format!("invalid UTF-8 in entry_id: {e}"));
369 return std::ptr::null_mut();
370 }
371 };
372 let path_str = match unsafe { CStr::from_ptr(request_path) }.to_str() {
374 Ok(s) => s,
375 Err(e) => {
376 set_last_error(format!("invalid UTF-8 in request_path: {e}"));
377 return std::ptr::null_mut();
378 }
379 };
380
381 let data: Value = match serde_json::from_str(data_str) {
382 Ok(d) => d,
383 Err(e) => {
384 set_last_error(format!("failed to parse data JSON: {e}"));
385 return std::ptr::null_mut();
386 }
387 };
388
389 let mut options = RenderOptions::new(entry_str, path_str);
391 if let Some(ref nonce) = context.nonce {
392 options = options.with_nonce(nonce);
393 }
394
395 let mut writer = StringResponseWriter::with_capacity(0);
396 match context
397 .handler
398 .render(protocol, &data, &options, &mut writer)
399 {
400 Ok(_) => match CString::new(writer.content) {
401 Ok(s) => s.into_raw(),
402 Err(e) => {
403 set_last_error(format!("rendered output contains interior NUL byte: {e}"));
404 std::ptr::null_mut()
405 }
406 },
407 Err(e) => {
408 set_last_error(format!("render failed: {e}"));
409 std::ptr::null_mut()
410 }
411 }
412}
413
414#[no_mangle]
425pub unsafe extern "C" fn webui_protocol_render_partial(
426 protocol_ptr: *const webui_protocol_t,
427 state_json: *const c_char,
428 entry_id: *const c_char,
429 request_path: *const c_char,
430 inventory_hex: *const c_char,
431) -> *mut c_char {
432 clear_last_error();
433
434 match std::panic::catch_unwind(|| {
435 if protocol_ptr.is_null()
436 || state_json.is_null()
437 || entry_id.is_null()
438 || request_path.is_null()
439 || inventory_hex.is_null()
440 {
441 set_last_error("one or more required arguments are null");
442 return std::ptr::null_mut();
443 }
444
445 let protocol_context = unsafe { &*(protocol_ptr as *const ProtocolContext) };
447 let state_str = match unsafe { CStr::from_ptr(state_json) }.to_str() {
448 Ok(value) => value,
449 Err(error) => {
450 set_last_error(format!("invalid UTF-8 in state_json: {error}"));
451 return std::ptr::null_mut();
452 }
453 };
454 let entry_str = match unsafe { CStr::from_ptr(entry_id) }.to_str() {
455 Ok(value) => value,
456 Err(error) => {
457 set_last_error(format!("invalid UTF-8 in entry_id: {error}"));
458 return std::ptr::null_mut();
459 }
460 };
461 let request_path_str = match unsafe { CStr::from_ptr(request_path) }.to_str() {
462 Ok(value) => value,
463 Err(error) => {
464 set_last_error(format!("invalid UTF-8 in request_path: {error}"));
465 return std::ptr::null_mut();
466 }
467 };
468 let inventory_str = match unsafe { CStr::from_ptr(inventory_hex) }.to_str() {
469 Ok(value) => value,
470 Err(error) => {
471 set_last_error(format!("invalid UTF-8 in inventory_hex: {error}"));
472 return std::ptr::null_mut();
473 }
474 };
475
476 let output = match protocol_context.protocol.render_partial(
477 state_str,
478 entry_str,
479 request_path_str,
480 inventory_str,
481 ) {
482 Ok(value) => value,
483 Err(error) => {
484 set_last_error(format!("render_partial failed: {error}"));
485 return std::ptr::null_mut();
486 }
487 };
488
489 match CString::new(output) {
490 Ok(value) => value.into_raw(),
491 Err(error) => {
492 set_last_error(format!("JSON output contains interior NUL byte: {error}"));
493 std::ptr::null_mut()
494 }
495 }
496 }) {
497 Ok(ptr) => ptr,
498 Err(_) => {
499 set_last_error("panic in webui_protocol_render_partial");
500 std::ptr::null_mut()
501 }
502 }
503}
504
505#[no_mangle]
512pub unsafe extern "C" fn webui_protocol_render_component_templates(
513 protocol_ptr: *const webui_protocol_t,
514 component_tags_json: *const c_char,
515 inventory_hex: *const c_char,
516) -> *mut c_char {
517 clear_last_error();
518
519 match std::panic::catch_unwind(|| {
520 if protocol_ptr.is_null() || component_tags_json.is_null() || inventory_hex.is_null() {
521 set_last_error("one or more required arguments are null");
522 return std::ptr::null_mut();
523 }
524
525 let protocol_context = unsafe { &*(protocol_ptr as *const ProtocolContext) };
527 let tags_str = match unsafe { CStr::from_ptr(component_tags_json) }.to_str() {
528 Ok(value) => value,
529 Err(error) => {
530 set_last_error(format!("invalid UTF-8 in component_tags_json: {error}"));
531 return std::ptr::null_mut();
532 }
533 };
534 let tags: Vec<String> = match serde_json::from_str(tags_str) {
535 Ok(value) => value,
536 Err(error) => {
537 set_last_error(format!("invalid tags JSON: {error}"));
538 return std::ptr::null_mut();
539 }
540 };
541 let tag_refs: Vec<&str> = tags.iter().map(String::as_str).collect();
542 let inventory_str = match unsafe { CStr::from_ptr(inventory_hex) }.to_str() {
543 Ok(value) => value,
544 Err(error) => {
545 set_last_error(format!("invalid UTF-8 in inventory_hex: {error}"));
546 return std::ptr::null_mut();
547 }
548 };
549
550 let result = match protocol_context
551 .protocol
552 .render_component_templates(&tag_refs, inventory_str)
553 {
554 Ok(value) => value,
555 Err(error) => {
556 set_last_error(format!("render_component_templates failed: {error}"));
557 return std::ptr::null_mut();
558 }
559 };
560
561 match CString::new(result.to_string()) {
562 Ok(value) => value.into_raw(),
563 Err(error) => {
564 set_last_error(format!("JSON output contains interior NUL byte: {error}"));
565 std::ptr::null_mut()
566 }
567 }
568 }) {
569 Ok(ptr) => ptr,
570 Err(_) => {
571 set_last_error("panic in webui_protocol_render_component_templates");
572 std::ptr::null_mut()
573 }
574 }
575}
576
577#[no_mangle]
585pub unsafe extern "C" fn webui_free(string_ptr: *mut c_char) {
586 if !string_ptr.is_null() {
587 let _ = CString::from_raw(string_ptr);
588 }
589}
590
591#[no_mangle]
600pub unsafe extern "C" fn webui_protocol_tokens(
601 protocol_ptr: *const webui_protocol_t,
602) -> *mut c_char {
603 clear_last_error();
604
605 match std::panic::catch_unwind(|| {
606 if protocol_ptr.is_null() {
607 set_last_error("protocol_ptr is null");
608 return std::ptr::null_mut();
609 }
610
611 let protocol_context = unsafe { &*(protocol_ptr as *const ProtocolContext) };
613 let joined = protocol_context.protocol.tokens().join("\n");
614
615 match CString::new(joined) {
616 Ok(value) => value.into_raw(),
617 Err(error) => {
618 set_last_error(format!("token string contains null byte: {error}"));
619 std::ptr::null_mut()
620 }
621 }
622 }) {
623 Ok(ptr) => ptr,
624 Err(_) => {
625 set_last_error("panic in webui_protocol_tokens");
626 std::ptr::null_mut()
627 }
628 }
629}
630
631#[allow(non_camel_case_types)]
637pub type webui_streaming_session_t = c_void;
638
639#[allow(non_camel_case_types)]
641pub type webui_streaming_step_t = c_void;
642
643#[allow(non_camel_case_types)]
645pub type webui_boundary_mode_t = u32;
646
647pub const WEBUI_BOUNDARY_MODE_FINAL: webui_boundary_mode_t = 0;
649
650pub const WEBUI_BOUNDARY_MODE_UPDATABLE: webui_boundary_mode_t = 1;
652
653#[allow(non_camel_case_types)]
656pub type webui_boundary_key_type_t = u32;
657
658pub const WEBUI_BOUNDARY_KEY_NONE: webui_boundary_key_type_t = 0;
660
661pub const WEBUI_BOUNDARY_KEY_STRING: webui_boundary_key_type_t = 1;
663
664pub const WEBUI_BOUNDARY_KEY_NUMBER: webui_boundary_key_type_t = 2;
666
667struct StreamingSessionContext {
669 session: StreamingSession,
670}
671
672struct StreamingStepContext {
674 step: StreamStep,
675}
676
677#[no_mangle]
701pub unsafe extern "C" fn webui_streaming_session_create(
702 handler_ptr: *mut c_void,
703 protocol_ptr: *const webui_protocol_t,
704 entry_id: *const c_char,
705 request_path: *const c_char,
706) -> *mut webui_streaming_session_t {
707 clear_last_error();
708
709 match std::panic::catch_unwind(|| {
710 if handler_ptr.is_null()
711 || protocol_ptr.is_null()
712 || entry_id.is_null()
713 || request_path.is_null()
714 {
715 set_last_error("one or more required arguments are null");
716 return std::ptr::null_mut();
717 }
718
719 let handler_context = unsafe { &*(handler_ptr as *const HandlerContext) };
721 let protocol_context = unsafe { &*(protocol_ptr as *const ProtocolContext) };
722
723 let Some(entry) = (unsafe { utf8_arg(entry_id, "entry_id") }) else {
725 return std::ptr::null_mut();
726 };
727 let Some(path) = (unsafe { utf8_arg(request_path, "request_path") }) else {
729 return std::ptr::null_mut();
730 };
731
732 let mut options = SessionOptions::new(entry, path);
733 options.nonce = handler_context.nonce.clone();
734
735 match StreamingSession::new(
736 Arc::clone(&handler_context.handler),
737 Arc::clone(&protocol_context.protocol),
738 options,
739 ) {
740 Ok(session) => Box::into_raw(Box::new(StreamingSessionContext { session }))
741 as *mut webui_streaming_session_t,
742 Err(error) => {
743 set_last_error(error.to_string());
744 std::ptr::null_mut()
745 }
746 }
747 }) {
748 Ok(ptr) => ptr,
749 Err(_) => {
750 set_last_error("panic in webui_streaming_session_create");
751 std::ptr::null_mut()
752 }
753 }
754}
755
756#[no_mangle]
766pub unsafe extern "C" fn webui_streaming_session_destroy(
767 session_ptr: *mut webui_streaming_session_t,
768) {
769 clear_last_error();
770 if session_ptr.is_null() {
771 return;
772 }
773 let result = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
774 drop(unsafe { Box::from_raw(session_ptr as *mut StreamingSessionContext) });
777 }));
778 if result.is_err() {
779 set_last_error("panic in webui_streaming_session_destroy");
780 }
781}
782
783#[no_mangle]
795pub unsafe extern "C" fn webui_streaming_session_start(
796 session_ptr: *mut webui_streaming_session_t,
797 state_json: *const c_char,
798) -> *mut webui_streaming_step_t {
799 clear_last_error();
800 match std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
801 if session_ptr.is_null() || state_json.is_null() {
802 set_last_error("session_ptr and state_json must not be null");
803 return std::ptr::null_mut();
804 }
805 let context = unsafe { &mut *(session_ptr as *mut StreamingSessionContext) };
807 let Some(state) = (unsafe { streaming_json_arg(state_json, "state_json") }) else {
809 return std::ptr::null_mut();
810 };
811 match context.session.start(state) {
812 Ok(step) => owned_streaming_step(step),
813 Err(error) => {
814 set_last_error(error.to_string());
815 std::ptr::null_mut()
816 }
817 }
818 })) {
819 Ok(step) => step,
820 Err(_) => {
821 set_last_error("panic in webui_streaming_session_start");
822 std::ptr::null_mut()
823 }
824 }
825}
826
827#[no_mangle]
840pub unsafe extern "C" fn webui_streaming_session_resume(
841 session_ptr: *mut webui_streaming_session_t,
842 instance_id: u32,
843 state_json: *const c_char,
844 mode: webui_boundary_mode_t,
845) -> *mut webui_streaming_step_t {
846 clear_last_error();
847 match std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
848 if session_ptr.is_null() || state_json.is_null() {
849 set_last_error("session_ptr and state_json must not be null");
850 return std::ptr::null_mut();
851 }
852 let Some(mode) = streaming_boundary_mode(mode) else {
853 return std::ptr::null_mut();
854 };
855 let context = unsafe { &mut *(session_ptr as *mut StreamingSessionContext) };
857 let Some(state) = (unsafe { streaming_json_arg(state_json, "state_json") }) else {
859 return std::ptr::null_mut();
860 };
861 match context
862 .session
863 .resume(BoundaryInstanceId::from_raw(instance_id), state, mode)
864 {
865 Ok(step) => owned_streaming_step(step),
866 Err(error) => {
867 set_last_error(error.to_string());
868 std::ptr::null_mut()
869 }
870 }
871 })) {
872 Ok(step) => step,
873 Err(_) => {
874 set_last_error("panic in webui_streaming_session_resume");
875 std::ptr::null_mut()
876 }
877 }
878}
879
880#[no_mangle]
891pub unsafe extern "C" fn webui_streaming_session_advance(
892 session_ptr: *mut webui_streaming_session_t,
893) -> *mut webui_streaming_step_t {
894 clear_last_error();
895 match std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
896 if session_ptr.is_null() {
897 set_last_error("session_ptr must not be null");
898 return std::ptr::null_mut();
899 }
900 let context = unsafe { &mut *(session_ptr as *mut StreamingSessionContext) };
902 match context.session.advance() {
903 Ok(step) => owned_streaming_step(step),
904 Err(error) => {
905 set_last_error(error.to_string());
906 std::ptr::null_mut()
907 }
908 }
909 })) {
910 Ok(step) => step,
911 Err(_) => {
912 set_last_error("panic in webui_streaming_session_advance");
913 std::ptr::null_mut()
914 }
915 }
916}
917
918#[no_mangle]
932pub unsafe extern "C" fn webui_streaming_session_update(
933 session_ptr: *mut webui_streaming_session_t,
934 instance_id: u32,
935 patch_json: *const c_char,
936 out_len: *mut usize,
937) -> *mut c_char {
938 clear_last_error();
939 match std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
940 if session_ptr.is_null() || patch_json.is_null() || out_len.is_null() {
941 set_last_error("session_ptr, patch_json, and out_len must not be null");
942 return std::ptr::null_mut();
943 }
944 let context = unsafe { &mut *(session_ptr as *mut StreamingSessionContext) };
946 let Some(patch) = (unsafe { streaming_json_arg(patch_json, "patch_json") }) else {
948 return std::ptr::null_mut();
949 };
950 let bytes = match context
951 .session
952 .update(BoundaryInstanceId::from_raw(instance_id), &patch)
953 {
954 Ok(bytes) => bytes,
955 Err(error) => {
956 set_last_error(error.to_string());
957 return std::ptr::null_mut();
958 }
959 };
960 let length = bytes.len();
961 match CString::new(bytes) {
962 Ok(chunk) => {
963 unsafe { *out_len = length };
965 chunk.into_raw()
966 }
967 Err(error) => {
968 set_last_error(format!(
969 "streaming update contains an interior NUL byte: {error}"
970 ));
971 std::ptr::null_mut()
972 }
973 }
974 })) {
975 Ok(bytes) => bytes,
976 Err(_) => {
977 set_last_error("panic in webui_streaming_session_update");
978 std::ptr::null_mut()
979 }
980 }
981}
982
983#[no_mangle]
995pub unsafe extern "C" fn webui_streaming_step_destroy(step_ptr: *mut webui_streaming_step_t) {
996 clear_last_error();
997 if step_ptr.is_null() {
998 return;
999 }
1000 let result = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
1001 drop(unsafe { Box::from_raw(step_ptr as *mut StreamingStepContext) });
1003 }));
1004 if result.is_err() {
1005 set_last_error("panic in webui_streaming_step_destroy");
1006 }
1007}
1008
1009#[no_mangle]
1020pub unsafe extern "C" fn webui_streaming_step_bytes(
1021 step_ptr: *const webui_streaming_step_t,
1022 out_len: *mut usize,
1023) -> *const u8 {
1024 clear_last_error();
1025 match std::panic::catch_unwind(|| {
1026 if step_ptr.is_null() || out_len.is_null() {
1027 set_last_error("step_ptr and out_len must not be null");
1028 return std::ptr::null();
1029 }
1030 let context = unsafe { &*(step_ptr as *const StreamingStepContext) };
1032 unsafe { *out_len = context.step.bytes.len() };
1034 context.step.bytes.as_ptr()
1035 }) {
1036 Ok(bytes) => bytes,
1037 Err(_) => {
1038 set_last_error("panic in webui_streaming_step_bytes");
1039 std::ptr::null()
1040 }
1041 }
1042}
1043
1044#[no_mangle]
1053pub unsafe extern "C" fn webui_streaming_step_done(
1054 step_ptr: *const webui_streaming_step_t,
1055) -> bool {
1056 clear_last_error();
1057 match std::panic::catch_unwind(|| {
1058 if step_ptr.is_null() {
1059 set_last_error("step_ptr is null");
1060 return false;
1061 }
1062 unsafe { (*(step_ptr as *const StreamingStepContext)).step.done }
1064 }) {
1065 Ok(done) => done,
1066 Err(_) => {
1067 set_last_error("panic in webui_streaming_step_done");
1068 false
1069 }
1070 }
1071}
1072
1073#[no_mangle]
1082pub unsafe extern "C" fn webui_streaming_step_has_boundary(
1083 step_ptr: *const webui_streaming_step_t,
1084) -> bool {
1085 clear_last_error();
1086 match std::panic::catch_unwind(|| {
1087 if step_ptr.is_null() {
1088 set_last_error("step_ptr is null");
1089 return false;
1090 }
1091 unsafe {
1093 (*(step_ptr as *const StreamingStepContext))
1094 .step
1095 .boundary
1096 .is_some()
1097 }
1098 }) {
1099 Ok(has_boundary) => has_boundary,
1100 Err(_) => {
1101 set_last_error("panic in webui_streaming_step_has_boundary");
1102 false
1103 }
1104 }
1105}
1106
1107#[no_mangle]
1117pub unsafe extern "C" fn webui_streaming_step_boundary_instance_id(
1118 step_ptr: *const webui_streaming_step_t,
1119 out_instance_id: *mut u32,
1120) -> bool {
1121 clear_last_error();
1122 match std::panic::catch_unwind(|| {
1123 if step_ptr.is_null() || out_instance_id.is_null() {
1124 set_last_error("step_ptr and out_instance_id must not be null");
1125 return false;
1126 }
1127 let context = unsafe { &*(step_ptr as *const StreamingStepContext) };
1129 let Some(boundary) = streaming_step_boundary(context) else {
1130 return false;
1131 };
1132 unsafe { *out_instance_id = boundary.instance_id.raw() };
1134 true
1135 }) {
1136 Ok(ok) => ok,
1137 Err(_) => {
1138 set_last_error("panic in webui_streaming_step_boundary_instance_id");
1139 false
1140 }
1141 }
1142}
1143
1144#[no_mangle]
1154pub unsafe extern "C" fn webui_streaming_step_boundary_declaration_id(
1155 step_ptr: *const webui_streaming_step_t,
1156 out_declaration_id: *mut u32,
1157) -> bool {
1158 clear_last_error();
1159 match std::panic::catch_unwind(|| {
1160 if step_ptr.is_null() || out_declaration_id.is_null() {
1161 set_last_error("step_ptr and out_declaration_id must not be null");
1162 return false;
1163 }
1164 let context = unsafe { &*(step_ptr as *const StreamingStepContext) };
1166 let Some(boundary) = streaming_step_boundary(context) else {
1167 return false;
1168 };
1169 unsafe { *out_declaration_id = boundary.declaration_id };
1171 true
1172 }) {
1173 Ok(ok) => ok,
1174 Err(_) => {
1175 set_last_error("panic in webui_streaming_step_boundary_declaration_id");
1176 false
1177 }
1178 }
1179}
1180
1181#[no_mangle]
1192pub unsafe extern "C" fn webui_streaming_step_boundary_owner(
1193 step_ptr: *const webui_streaming_step_t,
1194 out_len: *mut usize,
1195) -> *const c_char {
1196 clear_last_error();
1197 match std::panic::catch_unwind(|| {
1198 if step_ptr.is_null() || out_len.is_null() {
1199 set_last_error("step_ptr and out_len must not be null");
1200 return std::ptr::null();
1201 }
1202 let context = unsafe { &*(step_ptr as *const StreamingStepContext) };
1204 let Some(boundary) = streaming_step_boundary(context) else {
1205 return std::ptr::null();
1206 };
1207 unsafe { *out_len = boundary.owner.len() };
1209 boundary.owner.as_ptr().cast()
1210 }) {
1211 Ok(owner) => owner,
1212 Err(_) => {
1213 set_last_error("panic in webui_streaming_step_boundary_owner");
1214 std::ptr::null()
1215 }
1216 }
1217}
1218
1219#[no_mangle]
1230pub unsafe extern "C" fn webui_streaming_step_boundary_name(
1231 step_ptr: *const webui_streaming_step_t,
1232 out_len: *mut usize,
1233) -> *const c_char {
1234 clear_last_error();
1235 match std::panic::catch_unwind(|| {
1236 if step_ptr.is_null() || out_len.is_null() {
1237 set_last_error("step_ptr and out_len must not be null");
1238 return std::ptr::null();
1239 }
1240 let context = unsafe { &*(step_ptr as *const StreamingStepContext) };
1242 let Some(boundary) = streaming_step_boundary(context) else {
1243 return std::ptr::null();
1244 };
1245 unsafe { *out_len = boundary.name.len() };
1247 boundary.name.as_ptr().cast()
1248 }) {
1249 Ok(name) => name,
1250 Err(_) => {
1251 set_last_error("panic in webui_streaming_step_boundary_name");
1252 std::ptr::null()
1253 }
1254 }
1255}
1256
1257#[no_mangle]
1268pub unsafe extern "C" fn webui_streaming_step_boundary_key_type(
1269 step_ptr: *const webui_streaming_step_t,
1270 out_key_type: *mut webui_boundary_key_type_t,
1271) -> bool {
1272 clear_last_error();
1273 match std::panic::catch_unwind(|| {
1274 if step_ptr.is_null() || out_key_type.is_null() {
1275 set_last_error("step_ptr and out_key_type must not be null");
1276 return false;
1277 }
1278 let context = unsafe { &*(step_ptr as *const StreamingStepContext) };
1280 let Some(boundary) = streaming_step_boundary(context) else {
1281 return false;
1282 };
1283 let key_type = match boundary.key {
1284 None => WEBUI_BOUNDARY_KEY_NONE,
1285 Some(BoundaryKey::String(_)) => WEBUI_BOUNDARY_KEY_STRING,
1286 Some(BoundaryKey::Number(_)) => WEBUI_BOUNDARY_KEY_NUMBER,
1287 };
1288 unsafe { *out_key_type = key_type };
1290 true
1291 }) {
1292 Ok(ok) => ok,
1293 Err(_) => {
1294 set_last_error("panic in webui_streaming_step_boundary_key_type");
1295 false
1296 }
1297 }
1298}
1299
1300#[no_mangle]
1312pub unsafe extern "C" fn webui_streaming_step_boundary_key_string(
1313 step_ptr: *const webui_streaming_step_t,
1314 out_len: *mut usize,
1315) -> *const c_char {
1316 clear_last_error();
1317 match std::panic::catch_unwind(|| {
1318 if step_ptr.is_null() || out_len.is_null() {
1319 set_last_error("step_ptr and out_len must not be null");
1320 return std::ptr::null();
1321 }
1322 let context = unsafe { &*(step_ptr as *const StreamingStepContext) };
1324 let Some(boundary) = streaming_step_boundary(context) else {
1325 return std::ptr::null();
1326 };
1327 let Some(BoundaryKey::String(value)) = boundary.key.as_ref() else {
1328 set_last_error("pending boundary key is not a string");
1329 return std::ptr::null();
1330 };
1331 unsafe { *out_len = value.len() };
1333 value.as_ptr().cast()
1334 }) {
1335 Ok(value) => value,
1336 Err(_) => {
1337 set_last_error("panic in webui_streaming_step_boundary_key_string");
1338 std::ptr::null()
1339 }
1340 }
1341}
1342
1343#[no_mangle]
1353pub unsafe extern "C" fn webui_streaming_step_boundary_key_number(
1354 step_ptr: *const webui_streaming_step_t,
1355 out_value: *mut f64,
1356) -> bool {
1357 clear_last_error();
1358 match std::panic::catch_unwind(|| {
1359 if step_ptr.is_null() || out_value.is_null() {
1360 set_last_error("step_ptr and out_value must not be null");
1361 return false;
1362 }
1363 let context = unsafe { &*(step_ptr as *const StreamingStepContext) };
1365 let Some(boundary) = streaming_step_boundary(context) else {
1366 return false;
1367 };
1368 let Some(BoundaryKey::Number(value)) = boundary.key.as_ref() else {
1369 set_last_error("pending boundary key is not a number");
1370 return false;
1371 };
1372 let Some(value) = value.as_f64() else {
1373 set_last_error("pending boundary key cannot be represented as a finite number");
1374 return false;
1375 };
1376 unsafe { *out_value = value };
1378 true
1379 }) {
1380 Ok(ok) => ok,
1381 Err(_) => {
1382 set_last_error("panic in webui_streaming_step_boundary_key_number");
1383 false
1384 }
1385 }
1386}
1387
1388fn owned_streaming_step(step: StreamStep) -> *mut webui_streaming_step_t {
1389 Box::into_raw(Box::new(StreamingStepContext { step })) as *mut webui_streaming_step_t
1390}
1391
1392fn streaming_step_boundary(context: &StreamingStepContext) -> Option<&BoundaryDescriptor> {
1393 match context.step.boundary.as_ref() {
1394 Some(boundary) => Some(boundary),
1395 None => {
1396 set_last_error("streaming step has no pending boundary");
1397 None
1398 }
1399 }
1400}
1401
1402fn streaming_boundary_mode(mode: webui_boundary_mode_t) -> Option<BoundaryMode> {
1403 match mode {
1404 WEBUI_BOUNDARY_MODE_FINAL => Some(BoundaryMode::Final),
1405 WEBUI_BOUNDARY_MODE_UPDATABLE => Some(BoundaryMode::Updatable),
1406 _ => {
1407 set_last_error(format!(
1408 "invalid boundary mode {mode}; expected WEBUI_BOUNDARY_MODE_FINAL (0) \
1409 or WEBUI_BOUNDARY_MODE_UPDATABLE (1)"
1410 ));
1411 None
1412 }
1413 }
1414}
1415
1416unsafe fn streaming_json_arg(value: *const c_char, name: &str) -> Option<Value> {
1422 let text = unsafe { utf8_arg(value, name) }?;
1424 match serde_json::from_str(text) {
1425 Ok(value) => Some(value),
1426 Err(error) => {
1427 set_last_error(format!("failed to parse {name}: {error}"));
1428 None
1429 }
1430 }
1431}
1432
1433unsafe fn utf8_arg<'a>(value: *const c_char, name: &str) -> Option<&'a str> {
1439 match unsafe { CStr::from_ptr(value) }.to_str() {
1441 Ok(text) => Some(text),
1442 Err(error) => {
1443 set_last_error(format!("invalid UTF-8 in {name}: {error}"));
1444 None
1445 }
1446 }
1447}