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
34mod render_capacity;
35use render_capacity::RenderCapacityHint;
36
37#[allow(non_camel_case_types)]
39pub type webui_protocol_t = c_void;
40
41#[allow(non_camel_case_types)]
43pub type webui_render_t = c_void;
44
45thread_local! {
50 static LAST_ERROR: RefCell<Option<CString>> = const { RefCell::new(None) };
52}
53
54fn set_last_error(msg: impl Into<String>) {
56 let mut bytes = msg.into().into_bytes();
57 if let Some(nul_pos) = bytes.iter().position(|byte| *byte == 0) {
58 bytes.truncate(nul_pos);
59 }
60
61 let c_string = unsafe { CString::from_vec_unchecked(bytes) };
63 LAST_ERROR.with(|cell| {
64 cell.replace(Some(c_string));
65 });
66}
67
68fn clear_last_error() {
70 LAST_ERROR.with(|cell| {
71 cell.replace(None);
72 });
73}
74
75struct HandlerContext {
81 handler: Arc<WebUIHandler>,
82 nonce: Option<String>,
84}
85
86struct ProtocolContext {
88 protocol: Arc<Protocol>,
89 render_capacity: RenderCapacityHint,
91}
92
93impl ProtocolContext {
94 fn new(protocol: Protocol) -> Self {
95 Self {
96 protocol: Arc::new(protocol),
97 render_capacity: RenderCapacityHint::default(),
98 }
99 }
100}
101
102struct RenderResultContext {
104 html: String,
105}
106
107webui_handler::define_string_response_writer!(StringResponseWriter, content);
108
109#[no_mangle]
122pub extern "C" fn webui_last_error() -> *const c_char {
123 LAST_ERROR.with(|cell| {
124 let borrow = cell.borrow();
125 match borrow.as_ref() {
126 Some(c_str) => c_str.as_ptr(),
127 None => std::ptr::null(),
128 }
129 })
130}
131
132#[no_mangle]
141pub extern "C" fn webui_handler_create() -> *mut c_void {
142 let handler = WebUIHandler::new();
143 let context = Box::new(HandlerContext {
144 handler: Arc::new(handler),
145 nonce: None,
146 });
147 Box::into_raw(context) as *mut c_void
148}
149
150#[no_mangle]
167pub unsafe extern "C" fn webui_handler_create_with_plugin(plugin_id: *const c_char) -> *mut c_void {
168 clear_last_error();
169
170 let handler = if plugin_id.is_null() {
171 WebUIHandler::new()
172 } else {
173 match CStr::from_ptr(plugin_id).to_str() {
174 Ok("fast" | "fast-v2") => {
175 WebUIHandler::with_plugin(|| Box::new(FastV2HydrationPlugin::new()))
176 }
177 Ok("fast-v3") => WebUIHandler::with_plugin(|| Box::new(FastV3HydrationPlugin::new())),
178 Ok("webui") => WebUIHandler::with_plugin(|| Box::new(WebUIHydrationPlugin::new())),
179 Ok(unknown) => {
180 set_last_error(format!(
181 "unknown plugin: {unknown}. Use \"webui\", \"fast-v3\", \"fast-v2\", or \"fast\"."
182 ));
183 return std::ptr::null_mut();
184 }
185 Err(e) => {
186 set_last_error(format!("invalid UTF-8 in plugin_id: {e}"));
187 return std::ptr::null_mut();
188 }
189 }
190 };
191
192 let context = Box::new(HandlerContext {
193 handler: Arc::new(handler),
194 nonce: None,
195 });
196 Box::into_raw(context) as *mut c_void
197}
198
199#[no_mangle]
206pub unsafe extern "C" fn webui_handler_destroy(handler_ptr: *mut c_void) {
207 if !handler_ptr.is_null() {
208 let _ = Box::from_raw(handler_ptr as *mut HandlerContext);
209 }
210}
211
212#[no_mangle]
221pub unsafe extern "C" fn webui_protocol_create(
222 protocol_data: *const u8,
223 protocol_len: usize,
224) -> *mut webui_protocol_t {
225 clear_last_error();
226 if protocol_data.is_null() {
227 set_last_error("protocol_data is null");
228 return std::ptr::null_mut();
229 }
230
231 match std::panic::catch_unwind(|| {
232 let bytes = unsafe { std::slice::from_raw_parts(protocol_data, protocol_len) };
234 match Protocol::from_protobuf(bytes) {
235 Ok(protocol) => {
236 Box::into_raw(Box::new(ProtocolContext::new(protocol))) as *mut webui_protocol_t
237 }
238 Err(error) => {
239 set_last_error(format!("failed to parse protobuf protocol: {error}"));
240 std::ptr::null_mut()
241 }
242 }
243 }) {
244 Ok(ptr) => ptr,
245 Err(_) => {
246 set_last_error("panic in webui_protocol_create");
247 std::ptr::null_mut()
248 }
249 }
250}
251
252#[no_mangle]
259pub unsafe extern "C" fn webui_protocol_destroy(protocol_ptr: *mut webui_protocol_t) {
260 if !protocol_ptr.is_null() {
261 let _ = unsafe { Box::from_raw(protocol_ptr as *mut ProtocolContext) };
264 }
265}
266
267#[no_mangle]
287pub unsafe extern "C" fn webui_handler_set_nonce(handler_ptr: *mut c_void, nonce: *const c_char) {
288 clear_last_error();
289
290 if handler_ptr.is_null() {
291 set_last_error("handler_ptr is null");
292 return;
293 }
294
295 match std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
296 let context = &mut *(handler_ptr as *mut HandlerContext);
298
299 if nonce.is_null() {
300 context.nonce = None;
301 } else {
302 match CStr::from_ptr(nonce).to_str() {
304 Ok(s) => context.nonce = Some(s.to_string()),
305 Err(e) => {
306 set_last_error(format!("invalid UTF-8 in nonce: {e}"));
307 }
308 }
309 }
310 })) {
311 Ok(()) => {}
312 Err(_) => {
313 set_last_error("panic in webui_handler_set_nonce");
314 }
315 }
316}
317
318#[no_mangle]
338pub unsafe extern "C" fn webui_handler_render(
339 handler_ptr: *mut c_void,
340 protocol_ptr: *const webui_protocol_t,
341 data_json: *const c_char,
342 entry_id: *const c_char,
343 request_path: *const c_char,
344) -> *mut webui_render_t {
345 clear_last_error();
346
347 match std::panic::catch_unwind(|| {
348 let Some(mut html) = (unsafe {
350 render_full_document(handler_ptr, protocol_ptr, data_json, entry_id, request_path)
351 }) else {
352 return std::ptr::null_mut();
353 };
354 if html.capacity() / 2 > html.len() {
357 html.shrink_to_fit();
358 }
359 Box::into_raw(Box::new(RenderResultContext { html })) as *mut webui_render_t
360 }) {
361 Ok(ptr) => ptr,
362 Err(_) => {
363 set_last_error("panic in webui_handler_render");
364 std::ptr::null_mut()
365 }
366 }
367}
368
369#[no_mangle]
380pub unsafe extern "C" fn webui_render_bytes(
381 result_ptr: *const webui_render_t,
382 out_len: *mut usize,
383) -> *const u8 {
384 clear_last_error();
385 match std::panic::catch_unwind(|| {
386 if result_ptr.is_null() || out_len.is_null() {
387 set_last_error("result_ptr and out_len must not be null");
388 return std::ptr::null();
389 }
390 let context = unsafe { &*(result_ptr as *const RenderResultContext) };
392 unsafe { *out_len = context.html.len() };
394 context.html.as_ptr()
395 }) {
396 Ok(bytes) => bytes,
397 Err(_) => {
398 set_last_error("panic in webui_render_bytes");
399 std::ptr::null()
400 }
401 }
402}
403
404#[no_mangle]
413pub unsafe extern "C" fn webui_render_free(result_ptr: *mut webui_render_t) {
414 clear_last_error();
415 if result_ptr.is_null() {
416 return;
417 }
418 let result = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
419 drop(unsafe { Box::from_raw(result_ptr as *mut RenderResultContext) });
421 }));
422 if result.is_err() {
423 set_last_error("panic in webui_render_free");
424 }
425}
426
427unsafe fn render_full_document(
435 handler_ptr: *mut c_void,
436 protocol_ptr: *const webui_protocol_t,
437 data_json: *const c_char,
438 entry_id: *const c_char,
439 request_path: *const c_char,
440) -> Option<String> {
441 if handler_ptr.is_null()
442 || protocol_ptr.is_null()
443 || data_json.is_null()
444 || entry_id.is_null()
445 || request_path.is_null()
446 {
447 set_last_error("one or more required arguments are null");
448 return None;
449 }
450
451 let context = unsafe { &*(handler_ptr as *const HandlerContext) };
453 let protocol_context = unsafe { &*(protocol_ptr as *const ProtocolContext) };
454 unsafe { render_decoded_protocol(context, protocol_context, data_json, entry_id, request_path) }
456}
457
458unsafe fn render_decoded_protocol(
459 context: &HandlerContext,
460 protocol_context: &ProtocolContext,
461 data_json: *const c_char,
462 entry_id: *const c_char,
463 request_path: *const c_char,
464) -> Option<String> {
465 let data_str = match unsafe { CStr::from_ptr(data_json) }.to_str() {
467 Ok(s) => s,
468 Err(e) => {
469 set_last_error(format!("invalid UTF-8 in data_json: {e}"));
470 return None;
471 }
472 };
473 let entry_str = match unsafe { CStr::from_ptr(entry_id) }.to_str() {
475 Ok(s) => s,
476 Err(e) => {
477 set_last_error(format!("invalid UTF-8 in entry_id: {e}"));
478 return None;
479 }
480 };
481 let path_str = match unsafe { CStr::from_ptr(request_path) }.to_str() {
483 Ok(s) => s,
484 Err(e) => {
485 set_last_error(format!("invalid UTF-8 in request_path: {e}"));
486 return None;
487 }
488 };
489
490 let data: Value = match serde_json::from_str(data_str) {
491 Ok(d) => d,
492 Err(e) => {
493 set_last_error(format!("failed to parse data JSON: {e}"));
494 return None;
495 }
496 };
497
498 let mut options = RenderOptions::new(entry_str, path_str);
500 if let Some(ref nonce) = context.nonce {
501 options = options.with_nonce(nonce);
502 }
503
504 let capacity = if protocol_context.protocol.has_fragment(entry_str) {
507 protocol_context.render_capacity.get()
508 } else {
509 0
510 };
511 let mut writer = StringResponseWriter::with_capacity(capacity);
512 match context
513 .handler
514 .render(&protocol_context.protocol, &data, &options, &mut writer)
515 {
516 Ok(_) => {
517 protocol_context
518 .render_capacity
519 .record(writer.content.len());
520 Some(writer.content)
521 }
522 Err(e) => {
523 set_last_error(format!("render failed: {e}"));
524 None
525 }
526 }
527}
528
529#[no_mangle]
540pub unsafe extern "C" fn webui_protocol_render_partial(
541 protocol_ptr: *const webui_protocol_t,
542 state_json: *const c_char,
543 entry_id: *const c_char,
544 request_path: *const c_char,
545 inventory_hex: *const c_char,
546) -> *mut c_char {
547 clear_last_error();
548
549 match std::panic::catch_unwind(|| {
550 if protocol_ptr.is_null()
551 || state_json.is_null()
552 || entry_id.is_null()
553 || request_path.is_null()
554 || inventory_hex.is_null()
555 {
556 set_last_error("one or more required arguments are null");
557 return std::ptr::null_mut();
558 }
559
560 let protocol_context = unsafe { &*(protocol_ptr as *const ProtocolContext) };
562 let state_str = match unsafe { CStr::from_ptr(state_json) }.to_str() {
563 Ok(value) => value,
564 Err(error) => {
565 set_last_error(format!("invalid UTF-8 in state_json: {error}"));
566 return std::ptr::null_mut();
567 }
568 };
569 let entry_str = match unsafe { CStr::from_ptr(entry_id) }.to_str() {
570 Ok(value) => value,
571 Err(error) => {
572 set_last_error(format!("invalid UTF-8 in entry_id: {error}"));
573 return std::ptr::null_mut();
574 }
575 };
576 let request_path_str = match unsafe { CStr::from_ptr(request_path) }.to_str() {
577 Ok(value) => value,
578 Err(error) => {
579 set_last_error(format!("invalid UTF-8 in request_path: {error}"));
580 return std::ptr::null_mut();
581 }
582 };
583 let inventory_str = match unsafe { CStr::from_ptr(inventory_hex) }.to_str() {
584 Ok(value) => value,
585 Err(error) => {
586 set_last_error(format!("invalid UTF-8 in inventory_hex: {error}"));
587 return std::ptr::null_mut();
588 }
589 };
590
591 let output = match protocol_context.protocol.render_partial_json(
592 state_str,
593 entry_str,
594 request_path_str,
595 inventory_str,
596 ) {
597 Ok(value) => value,
598 Err(error) => {
599 set_last_error(format!("render_partial failed: {error}"));
600 return std::ptr::null_mut();
601 }
602 };
603
604 match CString::new(output) {
605 Ok(value) => value.into_raw(),
606 Err(error) => {
607 set_last_error(format!("JSON output contains interior NUL byte: {error}"));
608 std::ptr::null_mut()
609 }
610 }
611 }) {
612 Ok(ptr) => ptr,
613 Err(_) => {
614 set_last_error("panic in webui_protocol_render_partial");
615 std::ptr::null_mut()
616 }
617 }
618}
619
620#[no_mangle]
627pub unsafe extern "C" fn webui_protocol_render_component_templates(
628 protocol_ptr: *const webui_protocol_t,
629 component_tags_json: *const c_char,
630 inventory_hex: *const c_char,
631) -> *mut c_char {
632 clear_last_error();
633
634 match std::panic::catch_unwind(|| {
635 if protocol_ptr.is_null() || component_tags_json.is_null() || inventory_hex.is_null() {
636 set_last_error("one or more required arguments are null");
637 return std::ptr::null_mut();
638 }
639
640 let protocol_context = unsafe { &*(protocol_ptr as *const ProtocolContext) };
642 let tags_str = match unsafe { CStr::from_ptr(component_tags_json) }.to_str() {
643 Ok(value) => value,
644 Err(error) => {
645 set_last_error(format!("invalid UTF-8 in component_tags_json: {error}"));
646 return std::ptr::null_mut();
647 }
648 };
649 let tags: Vec<String> = match serde_json::from_str(tags_str) {
650 Ok(value) => value,
651 Err(error) => {
652 set_last_error(format!("invalid tags JSON: {error}"));
653 return std::ptr::null_mut();
654 }
655 };
656 let tag_refs: Vec<&str> = tags.iter().map(String::as_str).collect();
657 let inventory_str = match unsafe { CStr::from_ptr(inventory_hex) }.to_str() {
658 Ok(value) => value,
659 Err(error) => {
660 set_last_error(format!("invalid UTF-8 in inventory_hex: {error}"));
661 return std::ptr::null_mut();
662 }
663 };
664
665 let result = match protocol_context
666 .protocol
667 .render_component_templates(&tag_refs, inventory_str)
668 {
669 Ok(value) => value,
670 Err(error) => {
671 set_last_error(format!("render_component_templates failed: {error}"));
672 return std::ptr::null_mut();
673 }
674 };
675
676 match CString::new(result.to_string()) {
677 Ok(value) => value.into_raw(),
678 Err(error) => {
679 set_last_error(format!("JSON output contains interior NUL byte: {error}"));
680 std::ptr::null_mut()
681 }
682 }
683 }) {
684 Ok(ptr) => ptr,
685 Err(_) => {
686 set_last_error("panic in webui_protocol_render_component_templates");
687 std::ptr::null_mut()
688 }
689 }
690}
691
692#[no_mangle]
705pub unsafe extern "C" fn webui_free(string_ptr: *mut c_char) {
706 if !string_ptr.is_null() {
707 let _ = CString::from_raw(string_ptr);
708 }
709}
710
711#[no_mangle]
720pub unsafe extern "C" fn webui_protocol_tokens(
721 protocol_ptr: *const webui_protocol_t,
722) -> *mut c_char {
723 clear_last_error();
724
725 match std::panic::catch_unwind(|| {
726 if protocol_ptr.is_null() {
727 set_last_error("protocol_ptr is null");
728 return std::ptr::null_mut();
729 }
730
731 let protocol_context = unsafe { &*(protocol_ptr as *const ProtocolContext) };
733 let joined = protocol_context.protocol.tokens().join("\n");
734
735 match CString::new(joined) {
736 Ok(value) => value.into_raw(),
737 Err(error) => {
738 set_last_error(format!("token string contains null byte: {error}"));
739 std::ptr::null_mut()
740 }
741 }
742 }) {
743 Ok(ptr) => ptr,
744 Err(_) => {
745 set_last_error("panic in webui_protocol_tokens");
746 std::ptr::null_mut()
747 }
748 }
749}
750
751#[allow(non_camel_case_types)]
757pub type webui_streaming_session_t = c_void;
758
759#[allow(non_camel_case_types)]
761pub type webui_streaming_step_t = c_void;
762
763#[allow(non_camel_case_types)]
765pub type webui_boundary_mode_t = u32;
766
767pub const WEBUI_BOUNDARY_MODE_FINAL: webui_boundary_mode_t = 0;
769
770pub const WEBUI_BOUNDARY_MODE_UPDATABLE: webui_boundary_mode_t = 1;
772
773#[allow(non_camel_case_types)]
776pub type webui_boundary_key_type_t = u32;
777
778pub const WEBUI_BOUNDARY_KEY_NONE: webui_boundary_key_type_t = 0;
780
781pub const WEBUI_BOUNDARY_KEY_STRING: webui_boundary_key_type_t = 1;
783
784pub const WEBUI_BOUNDARY_KEY_NUMBER: webui_boundary_key_type_t = 2;
786
787struct StreamingSessionContext {
789 session: StreamingSession,
790}
791
792struct StreamingStepContext {
794 step: StreamStep,
795}
796
797#[no_mangle]
821pub unsafe extern "C" fn webui_streaming_session_create(
822 handler_ptr: *mut c_void,
823 protocol_ptr: *const webui_protocol_t,
824 entry_id: *const c_char,
825 request_path: *const c_char,
826) -> *mut webui_streaming_session_t {
827 clear_last_error();
828
829 match std::panic::catch_unwind(|| {
830 if handler_ptr.is_null()
831 || protocol_ptr.is_null()
832 || entry_id.is_null()
833 || request_path.is_null()
834 {
835 set_last_error("one or more required arguments are null");
836 return std::ptr::null_mut();
837 }
838
839 let handler_context = unsafe { &*(handler_ptr as *const HandlerContext) };
841 let protocol_context = unsafe { &*(protocol_ptr as *const ProtocolContext) };
842
843 let Some(entry) = (unsafe { utf8_arg(entry_id, "entry_id") }) else {
845 return std::ptr::null_mut();
846 };
847 let Some(path) = (unsafe { utf8_arg(request_path, "request_path") }) else {
849 return std::ptr::null_mut();
850 };
851
852 let mut options = SessionOptions::new(entry, path);
853 options.nonce = handler_context.nonce.clone();
854
855 match StreamingSession::new(
856 Arc::clone(&handler_context.handler),
857 Arc::clone(&protocol_context.protocol),
858 options,
859 ) {
860 Ok(session) => Box::into_raw(Box::new(StreamingSessionContext { session }))
861 as *mut webui_streaming_session_t,
862 Err(error) => {
863 set_last_error(error.to_string());
864 std::ptr::null_mut()
865 }
866 }
867 }) {
868 Ok(ptr) => ptr,
869 Err(_) => {
870 set_last_error("panic in webui_streaming_session_create");
871 std::ptr::null_mut()
872 }
873 }
874}
875
876#[no_mangle]
886pub unsafe extern "C" fn webui_streaming_session_destroy(
887 session_ptr: *mut webui_streaming_session_t,
888) {
889 clear_last_error();
890 if session_ptr.is_null() {
891 return;
892 }
893 let result = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
894 drop(unsafe { Box::from_raw(session_ptr as *mut StreamingSessionContext) });
897 }));
898 if result.is_err() {
899 set_last_error("panic in webui_streaming_session_destroy");
900 }
901}
902
903#[no_mangle]
915pub unsafe extern "C" fn webui_streaming_session_start(
916 session_ptr: *mut webui_streaming_session_t,
917 state_json: *const c_char,
918) -> *mut webui_streaming_step_t {
919 clear_last_error();
920 match std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
921 if session_ptr.is_null() || state_json.is_null() {
922 set_last_error("session_ptr and state_json must not be null");
923 return std::ptr::null_mut();
924 }
925 let context = unsafe { &mut *(session_ptr as *mut StreamingSessionContext) };
927 let Some(state) = (unsafe { streaming_json_arg(state_json, "state_json") }) else {
929 return std::ptr::null_mut();
930 };
931 match context.session.start(state) {
932 Ok(step) => owned_streaming_step(step),
933 Err(error) => {
934 set_last_error(error.to_string());
935 std::ptr::null_mut()
936 }
937 }
938 })) {
939 Ok(step) => step,
940 Err(_) => {
941 set_last_error("panic in webui_streaming_session_start");
942 std::ptr::null_mut()
943 }
944 }
945}
946
947#[no_mangle]
960pub unsafe extern "C" fn webui_streaming_session_resume(
961 session_ptr: *mut webui_streaming_session_t,
962 instance_id: u32,
963 state_json: *const c_char,
964 mode: webui_boundary_mode_t,
965) -> *mut webui_streaming_step_t {
966 clear_last_error();
967 match std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
968 if session_ptr.is_null() || state_json.is_null() {
969 set_last_error("session_ptr and state_json must not be null");
970 return std::ptr::null_mut();
971 }
972 let Some(mode) = streaming_boundary_mode(mode) else {
973 return std::ptr::null_mut();
974 };
975 let context = unsafe { &mut *(session_ptr as *mut StreamingSessionContext) };
977 let Some(state) = (unsafe { streaming_json_arg(state_json, "state_json") }) else {
979 return std::ptr::null_mut();
980 };
981 match context
982 .session
983 .resume(BoundaryInstanceId::from_raw(instance_id), state, mode)
984 {
985 Ok(step) => owned_streaming_step(step),
986 Err(error) => {
987 set_last_error(error.to_string());
988 std::ptr::null_mut()
989 }
990 }
991 })) {
992 Ok(step) => step,
993 Err(_) => {
994 set_last_error("panic in webui_streaming_session_resume");
995 std::ptr::null_mut()
996 }
997 }
998}
999
1000#[no_mangle]
1011pub unsafe extern "C" fn webui_streaming_session_advance(
1012 session_ptr: *mut webui_streaming_session_t,
1013) -> *mut webui_streaming_step_t {
1014 clear_last_error();
1015 match std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
1016 if session_ptr.is_null() {
1017 set_last_error("session_ptr must not be null");
1018 return std::ptr::null_mut();
1019 }
1020 let context = unsafe { &mut *(session_ptr as *mut StreamingSessionContext) };
1022 match context.session.advance() {
1023 Ok(step) => owned_streaming_step(step),
1024 Err(error) => {
1025 set_last_error(error.to_string());
1026 std::ptr::null_mut()
1027 }
1028 }
1029 })) {
1030 Ok(step) => step,
1031 Err(_) => {
1032 set_last_error("panic in webui_streaming_session_advance");
1033 std::ptr::null_mut()
1034 }
1035 }
1036}
1037
1038#[no_mangle]
1052pub unsafe extern "C" fn webui_streaming_session_update(
1053 session_ptr: *mut webui_streaming_session_t,
1054 instance_id: u32,
1055 patch_json: *const c_char,
1056 out_len: *mut usize,
1057) -> *mut c_char {
1058 clear_last_error();
1059 match std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
1060 if session_ptr.is_null() || patch_json.is_null() || out_len.is_null() {
1061 set_last_error("session_ptr, patch_json, and out_len must not be null");
1062 return std::ptr::null_mut();
1063 }
1064 let context = unsafe { &mut *(session_ptr as *mut StreamingSessionContext) };
1066 let Some(patch) = (unsafe { streaming_json_arg(patch_json, "patch_json") }) else {
1068 return std::ptr::null_mut();
1069 };
1070 let bytes = match context
1071 .session
1072 .update(BoundaryInstanceId::from_raw(instance_id), &patch)
1073 {
1074 Ok(bytes) => bytes,
1075 Err(error) => {
1076 set_last_error(error.to_string());
1077 return std::ptr::null_mut();
1078 }
1079 };
1080 let length = bytes.len();
1081 match CString::new(bytes) {
1082 Ok(chunk) => {
1083 unsafe { *out_len = length };
1085 chunk.into_raw()
1086 }
1087 Err(error) => {
1088 set_last_error(format!(
1089 "streaming update contains an interior NUL byte: {error}"
1090 ));
1091 std::ptr::null_mut()
1092 }
1093 }
1094 })) {
1095 Ok(bytes) => bytes,
1096 Err(_) => {
1097 set_last_error("panic in webui_streaming_session_update");
1098 std::ptr::null_mut()
1099 }
1100 }
1101}
1102
1103#[no_mangle]
1115pub unsafe extern "C" fn webui_streaming_step_destroy(step_ptr: *mut webui_streaming_step_t) {
1116 clear_last_error();
1117 if step_ptr.is_null() {
1118 return;
1119 }
1120 let result = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
1121 drop(unsafe { Box::from_raw(step_ptr as *mut StreamingStepContext) });
1123 }));
1124 if result.is_err() {
1125 set_last_error("panic in webui_streaming_step_destroy");
1126 }
1127}
1128
1129#[no_mangle]
1140pub unsafe extern "C" fn webui_streaming_step_bytes(
1141 step_ptr: *const webui_streaming_step_t,
1142 out_len: *mut usize,
1143) -> *const u8 {
1144 clear_last_error();
1145 match std::panic::catch_unwind(|| {
1146 if step_ptr.is_null() || out_len.is_null() {
1147 set_last_error("step_ptr and out_len must not be null");
1148 return std::ptr::null();
1149 }
1150 let context = unsafe { &*(step_ptr as *const StreamingStepContext) };
1152 unsafe { *out_len = context.step.bytes.len() };
1154 context.step.bytes.as_ptr()
1155 }) {
1156 Ok(bytes) => bytes,
1157 Err(_) => {
1158 set_last_error("panic in webui_streaming_step_bytes");
1159 std::ptr::null()
1160 }
1161 }
1162}
1163
1164#[no_mangle]
1173pub unsafe extern "C" fn webui_streaming_step_done(
1174 step_ptr: *const webui_streaming_step_t,
1175) -> bool {
1176 clear_last_error();
1177 match std::panic::catch_unwind(|| {
1178 if step_ptr.is_null() {
1179 set_last_error("step_ptr is null");
1180 return false;
1181 }
1182 unsafe { (*(step_ptr as *const StreamingStepContext)).step.done }
1184 }) {
1185 Ok(done) => done,
1186 Err(_) => {
1187 set_last_error("panic in webui_streaming_step_done");
1188 false
1189 }
1190 }
1191}
1192
1193#[no_mangle]
1202pub unsafe extern "C" fn webui_streaming_step_has_boundary(
1203 step_ptr: *const webui_streaming_step_t,
1204) -> bool {
1205 clear_last_error();
1206 match std::panic::catch_unwind(|| {
1207 if step_ptr.is_null() {
1208 set_last_error("step_ptr is null");
1209 return false;
1210 }
1211 unsafe {
1213 (*(step_ptr as *const StreamingStepContext))
1214 .step
1215 .boundary
1216 .is_some()
1217 }
1218 }) {
1219 Ok(has_boundary) => has_boundary,
1220 Err(_) => {
1221 set_last_error("panic in webui_streaming_step_has_boundary");
1222 false
1223 }
1224 }
1225}
1226
1227#[no_mangle]
1237pub unsafe extern "C" fn webui_streaming_step_boundary_instance_id(
1238 step_ptr: *const webui_streaming_step_t,
1239 out_instance_id: *mut u32,
1240) -> bool {
1241 clear_last_error();
1242 match std::panic::catch_unwind(|| {
1243 if step_ptr.is_null() || out_instance_id.is_null() {
1244 set_last_error("step_ptr and out_instance_id must not be null");
1245 return false;
1246 }
1247 let context = unsafe { &*(step_ptr as *const StreamingStepContext) };
1249 let Some(boundary) = streaming_step_boundary(context) else {
1250 return false;
1251 };
1252 unsafe { *out_instance_id = boundary.instance_id.raw() };
1254 true
1255 }) {
1256 Ok(ok) => ok,
1257 Err(_) => {
1258 set_last_error("panic in webui_streaming_step_boundary_instance_id");
1259 false
1260 }
1261 }
1262}
1263
1264#[no_mangle]
1274pub unsafe extern "C" fn webui_streaming_step_boundary_declaration_id(
1275 step_ptr: *const webui_streaming_step_t,
1276 out_declaration_id: *mut u32,
1277) -> bool {
1278 clear_last_error();
1279 match std::panic::catch_unwind(|| {
1280 if step_ptr.is_null() || out_declaration_id.is_null() {
1281 set_last_error("step_ptr and out_declaration_id must not be null");
1282 return false;
1283 }
1284 let context = unsafe { &*(step_ptr as *const StreamingStepContext) };
1286 let Some(boundary) = streaming_step_boundary(context) else {
1287 return false;
1288 };
1289 unsafe { *out_declaration_id = boundary.declaration_id };
1291 true
1292 }) {
1293 Ok(ok) => ok,
1294 Err(_) => {
1295 set_last_error("panic in webui_streaming_step_boundary_declaration_id");
1296 false
1297 }
1298 }
1299}
1300
1301#[no_mangle]
1312pub unsafe extern "C" fn webui_streaming_step_boundary_owner(
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 unsafe { *out_len = boundary.owner.len() };
1329 boundary.owner.as_ptr().cast()
1330 }) {
1331 Ok(owner) => owner,
1332 Err(_) => {
1333 set_last_error("panic in webui_streaming_step_boundary_owner");
1334 std::ptr::null()
1335 }
1336 }
1337}
1338
1339#[no_mangle]
1350pub unsafe extern "C" fn webui_streaming_step_boundary_name(
1351 step_ptr: *const webui_streaming_step_t,
1352 out_len: *mut usize,
1353) -> *const c_char {
1354 clear_last_error();
1355 match std::panic::catch_unwind(|| {
1356 if step_ptr.is_null() || out_len.is_null() {
1357 set_last_error("step_ptr and out_len must not be null");
1358 return std::ptr::null();
1359 }
1360 let context = unsafe { &*(step_ptr as *const StreamingStepContext) };
1362 let Some(boundary) = streaming_step_boundary(context) else {
1363 return std::ptr::null();
1364 };
1365 unsafe { *out_len = boundary.name.len() };
1367 boundary.name.as_ptr().cast()
1368 }) {
1369 Ok(name) => name,
1370 Err(_) => {
1371 set_last_error("panic in webui_streaming_step_boundary_name");
1372 std::ptr::null()
1373 }
1374 }
1375}
1376
1377#[no_mangle]
1388pub unsafe extern "C" fn webui_streaming_step_boundary_key_type(
1389 step_ptr: *const webui_streaming_step_t,
1390 out_key_type: *mut webui_boundary_key_type_t,
1391) -> bool {
1392 clear_last_error();
1393 match std::panic::catch_unwind(|| {
1394 if step_ptr.is_null() || out_key_type.is_null() {
1395 set_last_error("step_ptr and out_key_type must not be null");
1396 return false;
1397 }
1398 let context = unsafe { &*(step_ptr as *const StreamingStepContext) };
1400 let Some(boundary) = streaming_step_boundary(context) else {
1401 return false;
1402 };
1403 let key_type = match boundary.key {
1404 None => WEBUI_BOUNDARY_KEY_NONE,
1405 Some(BoundaryKey::String(_)) => WEBUI_BOUNDARY_KEY_STRING,
1406 Some(BoundaryKey::Number(_)) => WEBUI_BOUNDARY_KEY_NUMBER,
1407 };
1408 unsafe { *out_key_type = key_type };
1410 true
1411 }) {
1412 Ok(ok) => ok,
1413 Err(_) => {
1414 set_last_error("panic in webui_streaming_step_boundary_key_type");
1415 false
1416 }
1417 }
1418}
1419
1420#[no_mangle]
1432pub unsafe extern "C" fn webui_streaming_step_boundary_key_string(
1433 step_ptr: *const webui_streaming_step_t,
1434 out_len: *mut usize,
1435) -> *const c_char {
1436 clear_last_error();
1437 match std::panic::catch_unwind(|| {
1438 if step_ptr.is_null() || out_len.is_null() {
1439 set_last_error("step_ptr and out_len must not be null");
1440 return std::ptr::null();
1441 }
1442 let context = unsafe { &*(step_ptr as *const StreamingStepContext) };
1444 let Some(boundary) = streaming_step_boundary(context) else {
1445 return std::ptr::null();
1446 };
1447 let Some(BoundaryKey::String(value)) = boundary.key.as_ref() else {
1448 set_last_error("pending boundary key is not a string");
1449 return std::ptr::null();
1450 };
1451 unsafe { *out_len = value.len() };
1453 value.as_ptr().cast()
1454 }) {
1455 Ok(value) => value,
1456 Err(_) => {
1457 set_last_error("panic in webui_streaming_step_boundary_key_string");
1458 std::ptr::null()
1459 }
1460 }
1461}
1462
1463#[no_mangle]
1473pub unsafe extern "C" fn webui_streaming_step_boundary_key_number(
1474 step_ptr: *const webui_streaming_step_t,
1475 out_value: *mut f64,
1476) -> bool {
1477 clear_last_error();
1478 match std::panic::catch_unwind(|| {
1479 if step_ptr.is_null() || out_value.is_null() {
1480 set_last_error("step_ptr and out_value must not be null");
1481 return false;
1482 }
1483 let context = unsafe { &*(step_ptr as *const StreamingStepContext) };
1485 let Some(boundary) = streaming_step_boundary(context) else {
1486 return false;
1487 };
1488 let Some(BoundaryKey::Number(value)) = boundary.key.as_ref() else {
1489 set_last_error("pending boundary key is not a number");
1490 return false;
1491 };
1492 let Some(value) = value.as_f64() else {
1493 set_last_error("pending boundary key cannot be represented as a finite number");
1494 return false;
1495 };
1496 unsafe { *out_value = value };
1498 true
1499 }) {
1500 Ok(ok) => ok,
1501 Err(_) => {
1502 set_last_error("panic in webui_streaming_step_boundary_key_number");
1503 false
1504 }
1505 }
1506}
1507
1508fn owned_streaming_step(step: StreamStep) -> *mut webui_streaming_step_t {
1509 Box::into_raw(Box::new(StreamingStepContext { step })) as *mut webui_streaming_step_t
1510}
1511
1512fn streaming_step_boundary(context: &StreamingStepContext) -> Option<&BoundaryDescriptor> {
1513 match context.step.boundary.as_ref() {
1514 Some(boundary) => Some(boundary),
1515 None => {
1516 set_last_error("streaming step has no pending boundary");
1517 None
1518 }
1519 }
1520}
1521
1522fn streaming_boundary_mode(mode: webui_boundary_mode_t) -> Option<BoundaryMode> {
1523 match mode {
1524 WEBUI_BOUNDARY_MODE_FINAL => Some(BoundaryMode::Final),
1525 WEBUI_BOUNDARY_MODE_UPDATABLE => Some(BoundaryMode::Updatable),
1526 _ => {
1527 set_last_error(format!(
1528 "invalid boundary mode {mode}; expected WEBUI_BOUNDARY_MODE_FINAL (0) \
1529 or WEBUI_BOUNDARY_MODE_UPDATABLE (1)"
1530 ));
1531 None
1532 }
1533 }
1534}
1535
1536unsafe fn streaming_json_arg(value: *const c_char, name: &str) -> Option<Value> {
1542 let text = unsafe { utf8_arg(value, name) }?;
1544 match serde_json::from_str(text) {
1545 Ok(value) => Some(value),
1546 Err(error) => {
1547 set_last_error(format!("failed to parse {name}: {error}"));
1548 None
1549 }
1550 }
1551}
1552
1553unsafe fn utf8_arg<'a>(value: *const c_char, name: &str) -> Option<&'a str> {
1559 match unsafe { CStr::from_ptr(value) }.to_str() {
1561 Ok(text) => Some(text),
1562 Err(error) => {
1563 set_last_error(format!("invalid UTF-8 in {name}: {error}"));
1564 None
1565 }
1566 }
1567}