1use crate::Selection;
2use crate::protocol::{HostEvent, ProtocolError, decode_event};
3use crate::runtime::Runtime;
4use crate::schema::{
5 AssetKind, EventPayload, IconRole, LoopMode, PROTOCOL_VERSION, SCHEMA_HASH, Theme,
6};
7use std::cell::RefCell;
8use std::ffi::c_int;
9use std::panic::{AssertUnwindSafe, catch_unwind};
10use std::sync::atomic::{AtomicBool, Ordering};
11use std::sync::{Arc, Mutex, OnceLock};
12use std::task::{Context, Wake, Waker};
13
14pub const STATUS_OK: i32 = 0;
15pub const STATUS_PROTOCOL_ERROR: i32 = -1;
16pub const STATUS_NOT_INITIALIZED: i32 = -2;
17pub const STATUS_ALREADY_INITIALIZED: i32 = -3;
18pub const STATUS_PANIC: i32 = -4;
19pub const STATUS_NO_RENDERER: i32 = -5;
22
23const EXIT_FAILURE: i32 = 1;
28
29#[repr(C)]
30#[derive(Clone, Copy, Debug)]
31pub struct MutationBatch {
32 pub ptr: *const u8,
33 pub len: u32,
34 pub result: i64,
35}
36
37impl Default for MutationBatch {
38 fn default() -> Self {
39 Self {
40 ptr: std::ptr::null(),
41 len: 0,
42 result: 0,
43 }
44 }
45}
46
47#[derive(Clone, Copy)]
48pub struct RendererApi {
49 pub run: extern "C" fn() -> c_int,
50 pub request_frame: extern "C" fn(),
51}
52
53static RENDERER_API: OnceLock<RendererApi> = OnceLock::new();
54static FRAME_REQUESTED: AtomicBool = AtomicBool::new(false);
55static EVENT_DISPATCH_ACTIVE: AtomicBool = AtomicBool::new(false);
56static DEFERRED_FRAME_REQUEST: AtomicBool = AtomicBool::new(false);
57static LIFECYCLE_SUPPRESSED: AtomicBool = AtomicBool::new(false);
61
62pub fn install_renderer_api(api: RendererApi) -> Result<(), RendererApi> {
63 RENDERER_API.set(api)
64}
65
66#[cfg(all(renderer_linked, not(any(test, feature = "mock-renderer"))))]
70unsafe extern "C" {
71 fn compose_rust_renderer_run() -> c_int;
72 fn compose_rust_renderer_request_frame();
73}
74
75#[cfg(all(
81 renderer_host_exports,
82 renderer_linked,
83 not(any(test, feature = "mock-renderer"))
84))]
85unsafe extern "C" {
86 static compose_rust_renderer_host_exports: u8;
87}
88
89#[cfg(all(renderer_linked, not(any(test, feature = "mock-renderer"))))]
90extern "C" fn native_run() -> c_int {
91 #[cfg(renderer_host_exports)]
94 let _ = unsafe { std::ptr::read_volatile(&raw const compose_rust_renderer_host_exports) };
96 unsafe { compose_rust_renderer_run() }
98}
99
100#[cfg(all(renderer_linked, not(any(test, feature = "mock-renderer"))))]
101extern "C" fn native_request_frame() {
102 unsafe { compose_rust_renderer_request_frame() }
104}
105
106#[cfg(any(test, feature = "mock-renderer"))]
109extern "C" fn native_run() -> c_int {
110 STATUS_OK
111}
112
113#[cfg(any(test, feature = "mock-renderer"))]
114extern "C" fn native_request_frame() {}
115
116#[cfg(all(not(renderer_linked), not(any(test, feature = "mock-renderer"))))]
120extern "C" fn native_run() -> c_int {
121 eprintln!("{}", no_renderer_message());
122 STATUS_NO_RENDERER
123}
124
125#[cfg(all(not(renderer_linked), not(any(test, feature = "mock-renderer"))))]
126extern "C" fn native_request_frame() {}
127
128pub fn no_renderer_message() -> String {
133 format!(
134 "compose-rust: this application was built without a renderer, so there is nothing\n\
135 to draw with and nothing to draw on. Exiting {EXIT_FAILURE} rather than looking like\n\
136 a program that ran and finished.\n\
137 \n\
138 A default `cargo build` links the renderer for the platform it is building for and\n\
139 downloads it if it has to. A build reaches this message by turning that off:\n\
140 \n\
141 \x20 default-features = false, without re-enabling `native-renderer`\n\
142 \x20 a documentation build, which has no network to fetch a renderer with\n\
143 \n\
144 Building for a test or with the `mock-renderer` feature is a third way, and that\n\
145 one is silent on purpose: those builds drive the boundary directly and want no\n\
146 window."
147 )
148}
149
150fn renderer_api() -> RendererApi {
151 RENDERER_API.get().copied().unwrap_or(RendererApi {
152 run: native_run,
153 request_frame: native_request_frame,
154 })
155}
156
157pub fn request_frame_from_worker() {
166 if !FRAME_REQUESTED.swap(true, Ordering::AcqRel) {
167 if LIFECYCLE_SUPPRESSED.load(Ordering::Acquire)
168 || EVENT_DISPATCH_ACTIVE.load(Ordering::Acquire)
169 {
170 DEFERRED_FRAME_REQUEST.store(true, Ordering::Release);
171 } else {
172 (renderer_api().request_frame)();
173 FRAME_REQUESTED.store(false, Ordering::Release);
174 }
175 }
176}
177
178thread_local! {
179 static HOST_CALL_DEPTH: std::cell::Cell<u32> = const { std::cell::Cell::new(0) };
182}
183
184pub(crate) fn in_host_call() -> bool {
186 HOST_CALL_DEPTH.with(std::cell::Cell::get) > 0
187}
188
189struct HostCallGuard;
191
192impl HostCallGuard {
193 fn enter() -> Self {
194 HOST_CALL_DEPTH.with(|depth| depth.set(depth.get() + 1));
195 Self
196 }
197}
198
199impl Drop for HostCallGuard {
200 fn drop(&mut self) {
201 HOST_CALL_DEPTH.with(|depth| depth.set(depth.get().saturating_sub(1)));
202 }
203}
204
205pub(crate) fn request_frame_for_notifications() {
214 if LIFECYCLE_SUPPRESSED.load(Ordering::Acquire) {
215 (renderer_api().request_frame)();
216 return;
217 }
218 request_frame_from_worker();
219}
220
221struct EventDispatchGuard;
222
223impl EventDispatchGuard {
224 fn enter() -> Self {
225 EVENT_DISPATCH_ACTIVE.store(true, Ordering::Release);
226 Self
227 }
228}
229
230impl Drop for EventDispatchGuard {
231 fn drop(&mut self) {
232 EVENT_DISPATCH_ACTIVE.store(false, Ordering::Release);
233 if LIFECYCLE_SUPPRESSED.load(Ordering::Acquire) {
234 return;
235 }
236 if DEFERRED_FRAME_REQUEST.swap(false, Ordering::AcqRel) {
237 (renderer_api().request_frame)();
238 FRAME_REQUESTED.store(false, Ordering::Release);
239 }
240 }
241}
242
243struct FrameWake;
244
245impl Wake for FrameWake {
246 fn wake(self: Arc<Self>) {
247 request_frame_from_worker();
248 }
249
250 fn wake_by_ref(self: &Arc<Self>) {
251 request_frame_from_worker();
252 }
253}
254
255struct PendingAppend {
257 node_id: u32,
258 text: String,
259 dirty: bool,
260}
261
262pub type RuntimeFactory = Arc<dyn Fn() -> Box<dyn Runtime> + Send + Sync>;
270
271pub struct Host {
272 factory: RuntimeFactory,
275 theme: Theme,
276 window: crate::schema::Window,
277 runtime: Box<dyn Runtime>,
278 frame_waker: Waker,
279 pending_appends: Vec<PendingAppend>,
280}
281
282impl Host {
283 pub fn new(factory: impl Fn() -> Box<dyn Runtime> + Send + Sync + 'static) -> Self {
285 Self::from_factory(Arc::new(factory), launched_theme())
286 }
287
288 pub fn with_theme(
291 factory: impl Fn() -> Box<dyn Runtime> + Send + Sync + 'static,
292 theme: Theme,
293 ) -> Self {
294 Self::from_factory(Arc::new(factory), theme)
295 }
296
297 fn from_factory(factory: RuntimeFactory, theme: Theme) -> Self {
298 crate::window::reset_window_size();
303 crate::message::reset_messages();
307 crate::asset::requeue_all();
316 crate::theme::install(theme);
319 let runtime = factory();
320 Self {
321 factory,
322 theme,
323 window: launched_window(),
324 runtime,
325 frame_waker: Waker::from(Arc::new(FrameWake)),
326 pending_appends: Vec::new(),
327 }
328 }
329
330 fn batch(&mut self) -> &mut crate::runtime::Batch {
331 self.runtime.batch_mut()
332 }
333
334 pub fn rebuild(&mut self) -> Result<&[u8], ProtocolError> {
335 let _call = HostCallGuard::enter();
336 self.batch().begin();
337 let (theme, window) = (self.theme, self.window);
341 self.batch().set_theme(theme);
342 self.batch().set_window(window);
346 self.runtime.rebuild();
347 self.flush_messages();
348 self.arm_scheduler_wake();
349 self.runtime.batch_mut().finish()
350 }
351
352 pub fn dispatch_event(&mut self, bytes: &[u8]) -> Result<(&[u8], i64), ProtocolError> {
353 let event = decode_event(bytes)?;
354 self.dispatch(event)
355 }
356
357 pub fn arena(&self) -> (*const u8, usize) {
359 self.runtime.batch().arena()
360 }
361
362 pub fn resync(&mut self) -> Result<(&[u8], i64), ProtocolError> {
369 *self = Self::from_factory(self.factory.clone(), self.theme);
370 Ok((self.rebuild()?, 0))
371 }
372
373 fn set_lifecycle_running(&mut self, running: bool) -> Result<(&[u8], i64), ProtocolError> {
376 LIFECYCLE_SUPPRESSED.store(!running, Ordering::Release);
377 if running {
378 if DEFERRED_FRAME_REQUEST.swap(false, Ordering::AcqRel) {
379 (renderer_api().request_frame)();
380 }
381 FRAME_REQUESTED.store(false, Ordering::Release);
382 }
383 self.batch().begin();
386 Ok((self.runtime.batch_mut().finish()?, 0))
387 }
388
389 fn render_and_finish(&mut self, result: i64) -> Result<(&[u8], i64), ProtocolError> {
392 self.batch().begin();
393 self.runtime.render();
394 self.flush_messages();
395 self.arm_scheduler_wake();
396 Ok((self.runtime.batch_mut().finish()?, result))
397 }
398
399 fn empty_batch(&mut self) -> Result<(&[u8], i64), ProtocolError> {
401 self.batch().begin();
402 Ok((self.runtime.batch_mut().finish()?, 0))
403 }
404
405 fn notification_activated(
410 &mut self,
411 action: u32,
412 key: &str,
413 ) -> Result<(&[u8], i64), ProtocolError> {
414 let woke = {
415 let _dispatch_guard = EventDispatchGuard::enter();
416 crate::notification::activate(crate::notification::NotificationActivation {
417 key: key.to_owned(),
418 action,
419 })
420 };
421 self.batch().begin();
422 if woke {
423 self.runtime.render();
424 }
425 self.flush_messages();
426 self.arm_scheduler_wake();
427 Ok((self.runtime.batch_mut().finish()?, 0))
428 }
429
430 fn publish_notification_permission(
435 &mut self,
436 state: crate::schema::NotificationPermission,
437 ) -> Result<(&[u8], i64), ProtocolError> {
438 self.batch().begin();
439 if crate::notification::publish_permission(state) {
440 self.runtime.render();
441 }
442 self.flush_messages();
443 self.arm_scheduler_wake();
444 Ok((self.runtime.batch_mut().finish()?, 0))
445 }
446
447 fn publish_design_system(
453 &mut self,
454 system: crate::schema::DesignSystem,
455 ) -> Result<(&[u8], i64), ProtocolError> {
456 if crate::design::publish(system) {
457 return self.render_and_finish(0);
458 }
459 self.empty_batch()
460 }
461
462 pub fn dispatch(&mut self, event: HostEvent<'_>) -> Result<(&[u8], i64), ProtocolError> {
463 let _call = HostCallGuard::enter();
464 match event.payload {
467 EventPayload::Resync => return self.resync(),
468 EventPayload::DesignSystemResolved(system) => {
469 return self.publish_design_system(system);
470 }
471 EventPayload::LifecycleStart => return self.set_lifecycle_running(true),
472 EventPayload::LifecycleStop => return self.set_lifecycle_running(false),
473 EventPayload::NotificationActivated { action, key } => {
474 if event.node_id != 0 || event.handler_id != 0 {
475 return Err(ProtocolError::InvalidValueKind(0));
476 }
477 return self.notification_activated(action, key);
478 }
479 EventPayload::NotificationPermissionChanged(state) => {
480 if event.node_id != 0 || event.handler_id != 0 {
481 return Err(ProtocolError::InvalidValueKind(0));
482 }
483 return self.publish_notification_permission(state);
484 }
485 _ => {}
486 }
487 if let EventPayload::WindowSizeChanged {
491 width_dp,
492 height_dp,
493 class,
494 height_class,
495 } = event.payload
496 {
497 let size = crate::window::WindowSize::new(width_dp, height_dp, class, height_class);
501 if event.node_id != 0 {
502 let woke = self
503 .runtime
504 .size_token(event.node_id)
505 .is_some_and(|token| crate::window::publish_node(token, size));
506 if !woke {
507 return self.empty_batch();
508 }
509 return self.render_and_finish(0);
510 }
511 crate::window::publish(size);
512 return self.render_and_finish(0);
513 }
514 let message_action = match event.payload {
518 EventPayload::Clicked => crate::message::take_action(event.handler_id),
519 _ => None,
520 };
521 if let Some(mut action) = message_action {
522 if event.node_id != 0 {
523 return Err(ProtocolError::InvalidValueKind(0));
524 }
525 let _dispatch_guard = EventDispatchGuard::enter();
526 self.runtime.run_in_context(&mut *action);
527 return self.render_and_finish(0);
528 }
529 if let EventPayload::ProtocolError { .. } = event.payload {
531 return Err(ProtocolError::InvalidValueKind(0));
532 }
533 let _dispatch_guard = EventDispatchGuard::enter();
534 let result = self.runtime.handle_event(&event)?;
535 self.render_and_finish(result)
536 }
537
538 pub fn render_frame(&mut self, _frame_time_nanos: u64) -> Result<&[u8], ProtocolError> {
539 let _call = HostCallGuard::enter();
540 if LIFECYCLE_SUPPRESSED.load(Ordering::Acquire) {
541 self.batch().begin();
547 self.flush_notifications();
548 return self.runtime.batch_mut().finish();
549 }
550 FRAME_REQUESTED.store(false, Ordering::Release);
551 EVENT_DISPATCH_ACTIVE.store(false, Ordering::Release);
552 DEFERRED_FRAME_REQUEST.store(false, Ordering::Release);
553 self.batch().begin();
554 self.runtime.render();
555 self.flush_pending_appends();
556 self.flush_messages();
557 self.arm_scheduler_wake();
558 self.runtime.batch_mut().finish()
559 }
560
561 pub fn append_text(&mut self, node_id: u32, tail: &str) {
564 match self
565 .pending_appends
566 .iter_mut()
567 .find(|pending| pending.node_id == node_id)
568 {
569 Some(pending) => {
570 pending.text.push_str(tail);
571 pending.dirty = true;
572 }
573 None => self.pending_appends.push(PendingAppend {
574 node_id,
575 text: tail.to_owned(),
576 dirty: true,
577 }),
578 }
579 request_frame_from_worker();
581 }
582
583 fn flush_messages(&mut self) {
588 if let Some(theme) = crate::theme::take_pending() {
591 self.theme = theme;
592 self.batch().set_theme(theme);
593 }
594 let batch = self.runtime.batch_mut();
595 crate::asset::drain(|pending| {
599 batch.register_asset(pending.asset_id, pending.kind, pending.bytes);
600 });
601 crate::message::drain(|message| {
602 batch.show_message(
603 message.handler_id,
604 &message.text,
605 &message.action,
606 message.duration,
607 );
608 });
609 self.flush_notifications();
610 }
611
612 fn flush_notifications(&mut self) {
617 let batch = self.runtime.batch_mut();
618 let posts = crate::notification::drain(|command| batch.notification(command));
619 crate::notification::note_posted(posts);
620 }
621
622 fn flush_pending_appends(&mut self) {
623 let batch = self.runtime.batch_mut();
624 for pending in &self.pending_appends {
625 if pending.dirty {
626 batch.append_text_node(pending.node_id, &pending.text);
627 }
628 }
629 for pending in &mut self.pending_appends {
632 pending.text.clear();
633 pending.dirty = false;
634 }
635 }
636
637 pub fn register_asset(
643 &mut self,
644 asset_id: u32,
645 kind: AssetKind,
646 bytes: &[u8],
647 ) -> Result<&[u8], ProtocolError> {
648 let batch = self.runtime.batch_mut();
649 batch.begin();
650 batch.register_asset(asset_id, kind, bytes);
651 batch.finish()
652 }
653
654 pub fn register_icon(&mut self, asset_id: u32, role: IconRole) -> Result<&[u8], ProtocolError> {
657 self.register_asset(
658 asset_id,
659 AssetKind::VectorIcon,
660 &(role as u16).to_le_bytes(),
661 )
662 }
663
664 pub fn release_asset(&mut self, asset_id: u32) -> Result<&[u8], ProtocolError> {
667 let batch = self.runtime.batch_mut();
668 batch.begin();
669 batch.release_asset(asset_id);
670 batch.finish()
671 }
672
673 pub fn set_text(
674 &mut self,
675 node_id: u32,
676 text: &str,
677 selection: Option<Selection>,
678 ) -> Result<&[u8], ProtocolError> {
679 let batch = self.runtime.batch_mut();
680 batch.begin();
681 batch.set_text_node(node_id, text, selection);
682 batch.finish()
683 }
684
685 fn arm_scheduler_wake(&mut self) {
686 let mut context = Context::from_waker(&self.frame_waker);
687 if self.runtime.poll_work(&mut context).is_ready() {
688 request_frame_from_worker();
689 }
690 }
691}
692
693struct HostSlot(RefCell<Option<Host>>);
706
707impl Drop for HostSlot {
708 fn drop(&mut self) {
709 std::mem::forget(self.0.borrow_mut().take());
710 }
711}
712
713impl std::ops::Deref for HostSlot {
714 type Target = RefCell<Option<Host>>;
715
716 fn deref(&self) -> &Self::Target {
717 &self.0
718 }
719}
720
721static APP: Mutex<Option<RuntimeFactory>> = Mutex::new(None);
734
735static THEME: Mutex<Theme> = Mutex::new(Theme::unified(crate::schema::DesignSystem::Material3));
737
738static WINDOW: Mutex<crate::schema::Window> = Mutex::new(crate::schema::Window::new());
740
741thread_local! {
742 static HOST: HostSlot = const { HostSlot(RefCell::new(None)) };
743}
744
745fn launched_app() -> Option<RuntimeFactory> {
746 APP.lock().map_or(None, |app| (*app).clone())
747}
748
749fn launched_window() -> crate::schema::Window {
750 WINDOW
751 .lock()
752 .map_or_else(|error| *error.into_inner(), |window| *window)
753}
754
755fn launched_theme() -> Theme {
756 THEME
757 .lock()
758 .map_or_else(|error| *error.into_inner(), |theme| *theme)
759}
760
761#[derive(Clone, Copy, Debug)]
762pub struct LaunchBuilder {
763 mode: LoopMode,
764 theme: Theme,
765 window: crate::schema::Window,
766}
767
768impl Default for LaunchBuilder {
769 fn default() -> Self {
770 Self {
771 mode: LoopMode::Renderer,
772 theme: Theme::default(),
773 window: crate::schema::Window::new(),
774 }
775 }
776}
777
778impl LaunchBuilder {
779 pub fn new() -> Self {
780 Self::default()
781 }
782
783 pub fn with_mode(mut self, mode: LoopMode) -> Self {
784 self.mode = mode;
785 self
786 }
787
788 pub fn with_theme(mut self, theme: Theme) -> Self {
792 self.theme = theme;
793 self
794 }
795
796 pub fn with_window(mut self, window: crate::schema::Window) -> Self {
804 self.window = window;
805 self
806 }
807
808 pub fn launch_runtime(self, runtime: impl Fn() -> Box<dyn Runtime> + Send + Sync + 'static) {
820 let status = self.try_launch_runtime(runtime);
821 if status != STATUS_OK {
822 std::process::exit(EXIT_FAILURE);
823 }
824 }
825
826 pub fn try_launch_runtime(
829 self,
830 runtime: impl Fn() -> Box<dyn Runtime> + Send + Sync + 'static,
831 ) -> i32 {
832 let factory: RuntimeFactory = Arc::new(runtime);
833 if let Ok(mut slot) = APP.lock() {
834 *slot = Some(factory);
835 }
836 if let Ok(mut slot) = THEME.lock() {
837 *slot = self.theme;
838 }
839 #[cfg(debug_assertions)]
843 for line in palette_report(&self.theme) {
844 eprintln!("{line}");
845 }
846 if let Ok(mut slot) = WINDOW.lock() {
847 *slot = self.window;
848 }
849 if self.mode != LoopMode::Renderer {
852 return STATUS_OK;
853 }
854 (renderer_api().run)()
855 }
856}
857
858pub fn launch_runtime(runtime: impl Fn() -> Box<dyn Runtime> + Send + Sync + 'static) {
860 LaunchBuilder::new().launch_runtime(runtime);
861}
862
863pub fn palette_report(theme: &Theme) -> Vec<String> {
870 let Some(palette) = theme.palette else {
871 return Vec::new();
872 };
873 let systems: Vec<crate::schema::DesignSystem> = if theme.adaptive {
874 crate::schema::DESIGN_SYSTEM_SCHEMA
875 .iter()
876 .filter_map(|variant| crate::schema::DesignSystem::try_from(variant.tag).ok())
877 .collect()
878 } else {
879 vec![theme.design_system]
880 };
881 systems
882 .into_iter()
883 .flat_map(|system| {
884 palette.check(system).into_iter().map(move |violation| {
885 format!("compose-rust: palette under {system:?}: {violation}")
886 })
887 })
888 .collect()
889}
890
891fn parse_handshake(bytes: &[u8]) -> Result<LoopMode, ProtocolError> {
892 if bytes.len() < 12 {
893 return Err(ProtocolError::Truncated);
894 }
895 let hash = u64::from_le_bytes(
896 bytes[0..8]
897 .try_into()
898 .map_err(|_| ProtocolError::Truncated)?,
899 );
900 let version = u16::from_le_bytes(
901 bytes[8..10]
902 .try_into()
903 .map_err(|_| ProtocolError::Truncated)?,
904 );
905 if hash != SCHEMA_HASH || version != PROTOCOL_VERSION {
906 eprintln!(
914 "compose-rust: the renderer was built from a different schema than this program. It sent hash {hash:#x} version {version}, and this build expects hash {SCHEMA_HASH:#x} version {PROTOCOL_VERSION}. Rebuild the renderer after running codegen; if it was already rebuilt, its build directory is holding a cached copy of the generated protocol and has to be cleared."
915 );
916 return Err(ProtocolError::InvalidEnvelope);
917 }
918 match bytes[10] {
919 0 => Ok(LoopMode::Renderer),
920 1 => Ok(LoopMode::Platform),
921 other => Err(ProtocolError::InvalidValueKind(u16::from(other))),
922 }
923}
924
925unsafe fn input_slice<'a>(ptr: *const u8, len: u32) -> Result<&'a [u8], ProtocolError> {
926 if len == 0 {
927 return Ok(&[]);
928 }
929 if ptr.is_null() {
930 return Err(ProtocolError::Truncated);
931 }
932 Ok(unsafe { std::slice::from_raw_parts(ptr, len as usize) })
934}
935
936unsafe fn write_batch(
937 out: *mut MutationBatch,
938 bytes: &[u8],
939 result: i64,
940) -> Result<(), ProtocolError> {
941 if out.is_null() {
942 return Err(ProtocolError::Truncated);
943 }
944 let len = u32::try_from(bytes.len()).map_err(|_| ProtocolError::LengthOverflow)?;
945 unsafe {
947 out.write(MutationBatch {
948 ptr: bytes.as_ptr(),
949 len,
950 result,
951 });
952 }
953 Ok(())
954}
955
956fn ffi_status(operation: impl FnOnce() -> Result<(), i32>) -> i32 {
960 match catch_unwind(AssertUnwindSafe(operation)) {
964 Ok(Ok(())) => STATUS_OK,
965 Ok(Err(status)) => status,
966 Err(_) => STATUS_PANIC,
967 }
968}
969
970fn protocol_error(_error: ProtocolError) -> i32 {
972 STATUS_PROTOCOL_ERROR
973}
974
975#[unsafe(no_mangle)]
976pub unsafe extern "C" fn compose_rust_host_init(
981 handshake: *const u8,
982 len: u32,
983 out: *mut MutationBatch,
984) -> i32 {
985 ffi_status(|| {
986 let bytes = unsafe { input_slice(handshake, len) }.map_err(protocol_error)?;
988 let _mode = parse_handshake(bytes).map_err(protocol_error)?;
989 let factory = launched_app().ok_or(STATUS_NOT_INITIALIZED)?;
990 HOST.with(|slot| {
991 let mut host_slot = slot.borrow_mut();
992 if host_slot.is_some() {
993 return Err(STATUS_ALREADY_INITIALIZED);
994 }
995 let mut host = Host::from_factory(factory, launched_theme());
996 let batch = host.rebuild().map_err(protocol_error)?;
997 unsafe { write_batch(out, batch, 0) }.map_err(protocol_error)?;
999 *host_slot = Some(host);
1000 Ok(())
1001 })
1002 })
1003}
1004
1005#[unsafe(no_mangle)]
1006pub unsafe extern "C" fn compose_rust_host_dispatch_event(
1011 event: *const u8,
1012 len: u32,
1013 out: *mut MutationBatch,
1014) -> i32 {
1015 ffi_status(|| {
1016 let bytes = unsafe { input_slice(event, len) }.map_err(protocol_error)?;
1018 HOST.with(|slot| {
1019 let mut host_slot = slot.borrow_mut();
1020 let host = host_slot.as_mut().ok_or(STATUS_NOT_INITIALIZED)?;
1021 let (batch, result) = host.dispatch_event(bytes).map_err(protocol_error)?;
1022 unsafe { write_batch(out, batch, result) }.map_err(protocol_error)
1024 })
1025 })
1026}
1027
1028#[unsafe(no_mangle)]
1029pub unsafe extern "C" fn compose_rust_host_render_frame(
1034 frame_time_nanos: u64,
1035 out: *mut MutationBatch,
1036) -> i32 {
1037 ffi_status(|| {
1038 HOST.with(|slot| {
1039 let mut host_slot = slot.borrow_mut();
1040 let host = host_slot.as_mut().ok_or(STATUS_NOT_INITIALIZED)?;
1041 let batch = host
1042 .render_frame(frame_time_nanos)
1043 .map_err(protocol_error)?;
1044 unsafe { write_batch(out, batch, 0) }.map_err(protocol_error)
1046 })
1047 })
1048}
1049
1050#[unsafe(no_mangle)]
1051pub unsafe extern "C" fn compose_rust_host_release_batch(batch: *mut MutationBatch) {
1056 let _ = catch_unwind(AssertUnwindSafe(|| {
1057 if !batch.is_null() {
1058 unsafe { batch.write(MutationBatch::default()) };
1060 }
1061 }));
1062}
1063
1064pub fn current_arena() -> (*const u8, usize) {
1069 HOST.with(|slot| {
1070 slot.borrow()
1071 .as_ref()
1072 .map_or((std::ptr::null(), 0), Host::arena)
1073 })
1074}
1075
1076#[unsafe(no_mangle)]
1077pub extern "C" fn compose_rust_host_shutdown() {
1078 let _ = catch_unwind(AssertUnwindSafe(|| {
1079 HOST.with(|slot| *slot.borrow_mut() = None);
1080 FRAME_REQUESTED.store(false, Ordering::Release);
1081 }));
1082}
1083
1084#[cfg(test)]
1085mod tests {
1086 use super::*;
1087
1088 static LAUNCH: Mutex<()> = Mutex::new(());
1092
1093 fn launch_guard() -> std::sync::MutexGuard<'static, ()> {
1094 LAUNCH
1095 .lock()
1096 .unwrap_or_else(|poisoned| poisoned.into_inner())
1097 }
1098
1099 #[test]
1100 fn malformed_ffi_input_returns_protocol_error() {
1101 let mut output = MutationBatch::default();
1102 let status = unsafe { compose_rust_host_dispatch_event(std::ptr::null(), 4, &mut output) };
1104 assert_eq!(status, STATUS_PROTOCOL_ERROR);
1105 }
1106
1107 #[test]
1108 fn all_exports_isolate_invalid_calls() {
1109 let _launch = launch_guard();
1110 let mut output = MutationBatch::default();
1111 unsafe {
1113 assert_eq!(
1114 compose_rust_host_init(std::ptr::null(), 1, &mut output),
1115 STATUS_PROTOCOL_ERROR
1116 );
1117 assert_eq!(
1119 compose_rust_host_render_frame(0, &mut output),
1120 STATUS_NOT_INITIALIZED
1121 );
1122 compose_rust_host_release_batch(std::ptr::null_mut());
1123 }
1124 compose_rust_host_shutdown();
1125 }
1126}
1127
1128pub fn demo_theme() -> Theme {
1140 demo_theme_for(Theme::adaptive(crate::schema::DesignSystem::Material3))
1141}
1142
1143pub fn demo_theme_for(theme: Theme) -> Theme {
1152 use crate::schema::DesignSystem;
1153 let theme = match std::env::var("DXC_DESIGN").as_deref().map(str::trim) {
1154 Ok("material3") => Theme::unified(DesignSystem::Material3),
1155 Ok("cupertino") => Theme::unified(DesignSystem::Cupertino),
1156 Ok("fluent") => Theme::unified(DesignSystem::Fluent),
1157 Ok("gnome") => Theme::unified(DesignSystem::Gnome),
1158 Ok("breeze") => Theme::unified(DesignSystem::Breeze),
1159 Ok("deepin") => Theme::unified(DesignSystem::Deepin),
1160 Ok("liquidglass") | Ok("liquid-glass") => Theme::unified(DesignSystem::LiquidGlass),
1163 _ => return with_scheme_override(theme),
1166 }
1167 .with_color_scheme(theme.color_scheme);
1168 with_scheme_override(theme)
1169}
1170
1171fn with_scheme_override(theme: Theme) -> Theme {
1173 match std::env::var("DXC_SCHEME").as_deref().map(str::trim) {
1174 Ok("light") => theme.with_color_scheme(crate::schema::ColorScheme::Light),
1175 Ok("dark") => theme.with_color_scheme(crate::schema::ColorScheme::Dark),
1176 _ => theme,
1177 }
1178}
1179
1180#[cfg(test)]
1181mod demo_theme_tests {
1182 use super::*;
1183 use crate::schema::{ColorScheme, DESIGN_SYSTEM_SCHEMA, DesignSystem};
1184 use std::sync::{Mutex, MutexGuard};
1185
1186 static ENVIRONMENT: Mutex<()> = Mutex::new(());
1191
1192 fn exclusive_environment() -> MutexGuard<'static, ()> {
1194 let guard = ENVIRONMENT
1195 .lock()
1196 .unwrap_or_else(|error| error.into_inner());
1197 unsafe {
1200 std::env::remove_var("DXC_DESIGN");
1201 std::env::remove_var("DXC_SCHEME");
1202 }
1203 guard
1204 }
1205
1206 #[test]
1209 fn fr14_a_named_design_system_is_unified_and_anything_else_adapts() {
1210 let _environment = exclusive_environment();
1211 for (name, system) in [
1214 ("material3", DesignSystem::Material3),
1215 ("cupertino", DesignSystem::Cupertino),
1216 ("fluent", DesignSystem::Fluent),
1217 ("gnome", DesignSystem::Gnome),
1218 ("breeze", DesignSystem::Breeze),
1219 ("deepin", DesignSystem::Deepin),
1220 ("liquidglass", DesignSystem::LiquidGlass),
1221 ("liquid-glass", DesignSystem::LiquidGlass),
1222 ] {
1223 unsafe { std::env::set_var("DXC_DESIGN", name) };
1224 assert_eq!(
1225 demo_theme(),
1226 Theme::unified(system),
1227 "{name} is not selectable"
1228 );
1229 }
1230 assert_eq!(
1231 DESIGN_SYSTEM_SCHEMA.len(),
1232 7,
1233 "a system nobody can select goes unseen"
1234 );
1235
1236 unsafe { std::env::set_var("DXC_DESIGN", "nonsense") };
1237 assert_eq!(demo_theme(), Theme::adaptive(DesignSystem::Material3));
1238
1239 unsafe { std::env::remove_var("DXC_DESIGN") };
1240 assert_eq!(demo_theme(), Theme::adaptive(DesignSystem::Material3));
1241 }
1242
1243 #[test]
1246 fn fr14_a_named_colour_scheme_is_pinned_and_anything_else_is_left_alone() {
1247 let _environment = exclusive_environment();
1248 for (name, scheme) in [("light", ColorScheme::Light), ("dark", ColorScheme::Dark)] {
1250 unsafe { std::env::set_var("DXC_SCHEME", name) };
1251 assert_eq!(
1252 demo_theme().color_scheme,
1253 scheme,
1254 "{name} is not selectable"
1255 );
1256 }
1257
1258 unsafe { std::env::set_var("DXC_SCHEME", "nonsense") };
1259 assert_eq!(demo_theme().color_scheme, ColorScheme::FollowSystem);
1260
1261 unsafe { std::env::remove_var("DXC_SCHEME") };
1262 assert_eq!(demo_theme().color_scheme, ColorScheme::FollowSystem);
1263 }
1264
1265 #[test]
1269 fn fr14_a_samples_own_theme_survives_everything_the_variables_do_not_name() {
1270 let _environment = exclusive_environment();
1271 let sample = Theme::unified(DesignSystem::Cupertino).with_color_scheme(ColorScheme::Light);
1272 assert_eq!(demo_theme_for(sample), sample);
1273
1274 unsafe { std::env::set_var("DXC_DESIGN", "liquidglass") };
1275 assert_eq!(
1276 demo_theme_for(sample),
1277 Theme::unified(DesignSystem::LiquidGlass).with_color_scheme(ColorScheme::Light)
1278 );
1279
1280 unsafe { std::env::set_var("DXC_SCHEME", "dark") };
1281 assert_eq!(
1282 demo_theme_for(sample),
1283 Theme::unified(DesignSystem::LiquidGlass).with_color_scheme(ColorScheme::Dark)
1284 );
1285
1286 unsafe {
1287 std::env::remove_var("DXC_DESIGN");
1288 std::env::remove_var("DXC_SCHEME");
1289 };
1290 }
1291 #[test]
1298 fn pr2_a_handshake_from_another_schema_is_refused() {
1299 let mut wrong = Vec::with_capacity(12);
1300 wrong.extend_from_slice(&SCHEMA_HASH.wrapping_add(1).to_le_bytes());
1301 wrong.extend_from_slice(&PROTOCOL_VERSION.to_le_bytes());
1302 wrong.extend_from_slice(&[LoopMode::Renderer as u8, 0]);
1303 assert!(
1304 matches!(
1305 parse_handshake(&wrong),
1306 Err(crate::protocol::ProtocolError::InvalidEnvelope)
1307 ),
1308 "a handshake carrying another schema's hash was accepted, so the two halves \
1309 would go on to disagree about what every record means"
1310 );
1311
1312 let mut right = Vec::with_capacity(12);
1313 right.extend_from_slice(&SCHEMA_HASH.to_le_bytes());
1314 right.extend_from_slice(&PROTOCOL_VERSION.to_le_bytes());
1315 right.extend_from_slice(&[LoopMode::Renderer as u8, 0]);
1316 assert!(
1317 parse_handshake(&right).is_ok(),
1318 "the handshake this build generates is not one it accepts"
1319 );
1320 }
1321}
1322
1323#[used]
1335static BOUNDARY_EXPORTS: BoundaryExports = BoundaryExports([
1336 compose_rust_host_init as *const (),
1337 compose_rust_host_dispatch_event as *const (),
1338 compose_rust_host_render_frame as *const (),
1339 compose_rust_host_release_batch as *const (),
1340 compose_rust_host_shutdown as *const (),
1341]);
1342
1343#[cfg(all(target_os = "windows", target_env = "msvc"))]
1359const EXPORT_DIRECTIVES: &str = concat!(
1360 " /EXPORT:compose_rust_host_init",
1361 " /EXPORT:compose_rust_host_dispatch_event",
1362 " /EXPORT:compose_rust_host_render_frame",
1363 " /EXPORT:compose_rust_host_release_batch",
1364 " /EXPORT:compose_rust_host_shutdown",
1365);
1366
1367#[cfg(all(target_os = "windows", target_env = "msvc"))]
1368#[used]
1369#[unsafe(link_section = ".drectve")]
1370static EXPORT_DIRECTIVE_BYTES: [u8; EXPORT_DIRECTIVES.len()] = {
1371 let source = EXPORT_DIRECTIVES.as_bytes();
1372 let mut bytes = [0u8; EXPORT_DIRECTIVES.len()];
1373 let mut index = 0;
1374 while index < source.len() {
1375 bytes[index] = source[index];
1376 index += 1;
1377 }
1378 bytes
1379};
1380
1381struct BoundaryExports(#[allow(dead_code)] [*const (); 5]);
1383
1384unsafe impl Sync for BoundaryExports {}