compose_rust/boundary.rs
1use crate::Selection;
2use crate::measure::MeasureResult;
3use crate::protocol::{HostEvent, ProtocolError, decode_event};
4use crate::runtime::Runtime;
5use crate::schema::{
6 AssetKind, EventPayload, IconRole, LoopMode, PROTOCOL_VERSION, SCHEMA_HASH, Theme,
7};
8use std::cell::RefCell;
9use std::ffi::c_int;
10use std::panic::{AssertUnwindSafe, catch_unwind};
11use std::sync::atomic::{AtomicBool, Ordering};
12use std::sync::{Arc, Mutex, OnceLock};
13use std::task::{Context, Wake, Waker};
14
15pub const STATUS_OK: i32 = 0;
16pub const STATUS_PROTOCOL_ERROR: i32 = -1;
17pub const STATUS_NOT_INITIALIZED: i32 = -2;
18pub const STATUS_ALREADY_INITIALIZED: i32 = -3;
19pub const STATUS_PANIC: i32 = -4;
20/// There is no renderer in this build to run. Returned by the loop entry point, never by
21/// a boundary call: a call could not have got this far without a renderer to make it.
22pub const STATUS_NO_RENDERER: i32 = -5;
23
24/// What the process exits with when the renderer loop does not finish successfully.
25///
26/// 1, not the status itself: exit codes are a single byte on Unix, so `STATUS_NO_RENDERER`
27/// would reach the shell as 251 and read as a signal rather than an ordinary failure.
28const EXIT_FAILURE: i32 = 1;
29
30#[repr(C)]
31#[derive(Clone, Copy, Debug)]
32pub struct MutationBatch {
33 pub ptr: *const u8,
34 pub len: u32,
35 pub result: i64,
36}
37
38impl Default for MutationBatch {
39 fn default() -> Self {
40 Self {
41 ptr: std::ptr::null(),
42 len: 0,
43 result: 0,
44 }
45 }
46}
47
48/// The Renderer's half of the boundary, as the Host calls it.
49#[derive(Clone, Copy)]
50pub struct RendererApi {
51 pub run: extern "C" fn() -> c_int,
52 pub request_frame: extern "C" fn(),
53 /// Reads `count` request records from the buffer and writes `count` results. Only on
54 /// the UI thread, and normally from inside a Host call the Renderer made.
55 pub measure: unsafe extern "C" fn(*const u8, u32, u32, *mut MeasureResult) -> i32,
56}
57
58static RENDERER_API: OnceLock<RendererApi> = OnceLock::new();
59static FRAME_REQUESTED: AtomicBool = AtomicBool::new(false);
60static EVENT_DISPATCH_ACTIVE: AtomicBool = AtomicBool::new(false);
61static DEFERRED_FRAME_REQUEST: AtomicBool = AtomicBool::new(false);
62/// Set between a stop and the start that follows it. While it is set a worker's frame
63/// request is remembered but not delivered, so a process that is not on screen is never
64/// asked to draw.
65static LIFECYCLE_SUPPRESSED: AtomicBool = AtomicBool::new(false);
66
67pub fn install_renderer_api(api: RendererApi) -> Result<(), RendererApi> {
68 RENDERER_API.set(api)
69}
70
71// `renderer_linked` is set by the build script, and only when a renderer was really
72// linked. That is not the same as the feature being on: a documentation build turns the
73// feature on and links nothing, and the result has to behave like the build it is.
74#[cfg(all(renderer_linked, not(any(test, feature = "mock-renderer"))))]
75unsafe extern "C" {
76 fn compose_rust_renderer_run() -> c_int;
77 fn compose_rust_renderer_request_frame();
78 fn compose_rust_renderer_measure(
79 requests: *const u8,
80 length: u32,
81 count: u32,
82 results: *mut MeasureResult,
83 ) -> i32;
84}
85
86#[cfg(all(renderer_linked, not(any(test, feature = "mock-renderer"))))]
87extern "C" fn native_run() -> c_int {
88 // The Linux static renderer refers to the five Host functions weakly, so that its own
89 // test executable links without a Host, and a weak reference does not make a linker
90 // pull in the code it names. This one does: running the renderer keeps them linked,
91 // whichever object file of this crate the compiler happened to put them in.
92 std::hint::black_box(&BOUNDARY_EXPORTS);
93 // SAFETY: The application links the Renderer implementation of this declared C ABI.
94 unsafe { compose_rust_renderer_run() }
95}
96
97#[cfg(all(renderer_linked, not(any(test, feature = "mock-renderer"))))]
98extern "C" fn native_request_frame() {
99 // SAFETY: The Renderer contract makes request_frame thread-safe.
100 unsafe { compose_rust_renderer_request_frame() }
101}
102
103/// # Safety
104/// As `compose_rust_renderer_measure`: readable requests, writable results.
105#[cfg(all(renderer_linked, not(any(test, feature = "mock-renderer"))))]
106unsafe extern "C" fn native_measure(
107 requests: *const u8,
108 length: u32,
109 count: u32,
110 results: *mut MeasureResult,
111) -> i32 {
112 // SAFETY: The caller's promise is passed on unchanged, and the Renderer refuses a call
113 // from the wrong thread rather than measuring on it.
114 unsafe { compose_rust_renderer_measure(requests, length, count, results) }
115}
116
117// The mock renderer and the crate's own tests drive the boundary directly and never want
118// a window, so doing nothing is the correct answer for them and always has been.
119#[cfg(any(test, feature = "mock-renderer"))]
120extern "C" fn native_run() -> c_int {
121 STATUS_OK
122}
123
124#[cfg(any(test, feature = "mock-renderer"))]
125extern "C" fn native_request_frame() {}
126
127// The mock renderer answers by fixed rules, so a test of the Host can predict every size.
128#[cfg(any(test, feature = "mock-renderer"))]
129use crate::measure::fake_measure as native_measure;
130
131// A real build with no renderer. This used to return success, so an application built
132// this way opened no window, drew nothing, printed nothing and exited 0, and there was
133// no way to tell that from a program that had simply finished.
134#[cfg(all(not(renderer_linked), not(any(test, feature = "mock-renderer"))))]
135extern "C" fn native_run() -> c_int {
136 eprintln!("{}", no_renderer_message());
137 STATUS_NO_RENDERER
138}
139
140#[cfg(all(not(renderer_linked), not(any(test, feature = "mock-renderer"))))]
141extern "C" fn native_request_frame() {}
142
143/// There is nothing to measure with, which the caller hears as a refusal and carries on
144/// past with every size at zero.
145#[cfg(all(not(renderer_linked), not(any(test, feature = "mock-renderer"))))]
146unsafe extern "C" fn native_measure(
147 _requests: *const u8,
148 _length: u32,
149 _count: u32,
150 _results: *mut MeasureResult,
151) -> i32 {
152 crate::measure::MEASURE_UNAVAILABLE
153}
154
155/// What a build with no renderer says on its way out.
156///
157/// Compiled into every build, not only the one that prints it, so that a test can read it
158/// whatever the build it is running in was configured with.
159pub fn no_renderer_message() -> String {
160 format!(
161 "compose-rust: this application was built without a renderer, so there is nothing\n\
162 to draw with and nothing to draw on. Exiting {EXIT_FAILURE} rather than looking like\n\
163 a program that ran and finished.\n\
164 \n\
165 A default `cargo build` links the renderer for the platform it is building for and\n\
166 downloads it if it has to. A build reaches this message by turning that off:\n\
167 \n\
168 \x20 default-features = false, without re-enabling `native-renderer`\n\
169 \x20 a documentation build, which has no network to fetch a renderer with\n\
170 \n\
171 Building for a test or with the `mock-renderer` feature is a third way, and that\n\
172 one is silent on purpose: those builds drive the boundary directly and want no\n\
173 window."
174 )
175}
176
177fn renderer_api() -> RendererApi {
178 RENDERER_API.get().copied().unwrap_or(RendererApi {
179 run: native_run,
180 request_frame: native_request_frame,
181 measure: native_measure,
182 })
183}
184
185/// Asks the Renderer to measure, through whichever Renderer is installed.
186///
187/// Crate-internal: the safe way in is `measure::measurer`, which only hands out the right
188/// to call this inside a Host call. The pointers come from slices that outlive the call.
189pub(crate) fn renderer_measure(
190 requests: *const u8,
191 length: u32,
192 count: u32,
193 results: *mut MeasureResult,
194) -> i32 {
195 // SAFETY: the caller passes a readable buffer of `length` bytes and room for `count`
196 // results, both borrowed for the length of this call.
197 unsafe { (renderer_api().measure)(requests, length, count, results) }
198}
199
200/// Coalesced wake used internally by the runtime's scheduler waker.
201///
202/// The flag spans the moment of delivery, not the wait for the frame that answers it. The
203/// Renderer folds requests into its own frame clock, so however many arrive between two
204/// frames it draws once; holding the flag until the frame came back would instead mean
205/// that one request the Renderer was not yet listening for silenced every later one. That
206/// happens on a cold start, where the first composition can be seconds after the first
207/// worker request, and it leaves the application frozen with nothing to unfreeze it.
208pub fn request_frame_from_worker() {
209 if !FRAME_REQUESTED.swap(true, Ordering::AcqRel) {
210 if LIFECYCLE_SUPPRESSED.load(Ordering::Acquire)
211 || EVENT_DISPATCH_ACTIVE.load(Ordering::Acquire)
212 {
213 DEFERRED_FRAME_REQUEST.store(true, Ordering::Release);
214 } else {
215 (renderer_api().request_frame)();
216 FRAME_REQUESTED.store(false, Ordering::Release);
217 }
218 }
219}
220
221thread_local! {
222 /// How deep this thread is inside a Host call. A notification posted from inside one is
223 /// written into the batch that call returns, so it needs no frame of its own.
224 static HOST_CALL_DEPTH: std::cell::Cell<u32> = const { std::cell::Cell::new(0) };
225}
226
227/// Whether this thread is inside `rebuild`, `dispatch` or `render_frame` right now.
228pub(crate) fn in_host_call() -> bool {
229 HOST_CALL_DEPTH.with(std::cell::Cell::get) > 0
230}
231
232/// Marks the span of one Host call on this thread.
233struct HostCallGuard;
234
235impl HostCallGuard {
236 fn enter() -> Self {
237 HOST_CALL_DEPTH.with(|depth| depth.set(depth.get() + 1));
238 Self
239 }
240}
241
242impl Drop for HostCallGuard {
243 fn drop(&mut self) {
244 HOST_CALL_DEPTH.with(|depth| depth.set(depth.get().saturating_sub(1)));
245 }
246}
247
248/// Asks for the frame that carries a queued notification out.
249///
250/// The one request a stopped UI does not hold back. Holding requests while the platform
251/// has the UI off screen is right for timers and animations, which nobody is watching, and
252/// wrong for a notification, whose whole point is the moment nobody is watching. So while
253/// stopped this goes to the Renderer directly, and the Renderer serves it outside its
254/// stopped frame clock. Everything else stays held, and the frame it was waiting for is
255/// still delivered on start.
256pub(crate) fn request_frame_for_notifications() {
257 if LIFECYCLE_SUPPRESSED.load(Ordering::Acquire) {
258 (renderer_api().request_frame)();
259 return;
260 }
261 request_frame_from_worker();
262}
263
264struct EventDispatchGuard;
265
266impl EventDispatchGuard {
267 fn enter() -> Self {
268 EVENT_DISPATCH_ACTIVE.store(true, Ordering::Release);
269 Self
270 }
271}
272
273impl Drop for EventDispatchGuard {
274 fn drop(&mut self) {
275 EVENT_DISPATCH_ACTIVE.store(false, Ordering::Release);
276 if LIFECYCLE_SUPPRESSED.load(Ordering::Acquire) {
277 return;
278 }
279 if DEFERRED_FRAME_REQUEST.swap(false, Ordering::AcqRel) {
280 (renderer_api().request_frame)();
281 FRAME_REQUESTED.store(false, Ordering::Release);
282 }
283 }
284}
285
286struct FrameWake;
287
288impl Wake for FrameWake {
289 fn wake(self: Arc<Self>) {
290 request_frame_from_worker();
291 }
292
293 fn wake_by_ref(self: &Arc<Self>) {
294 request_frame_from_worker();
295 }
296}
297
298/// One streaming Text node's tail, accumulated between frames.
299struct PendingAppend {
300 node_id: u32,
301 text: String,
302 dirty: bool,
303}
304
305/// Makes the runtime a Host drives. Called once per Host, and again on a resync, which
306/// is the one thing a diff against a lost node table cannot answer.
307///
308/// Shared across threads because `launch` runs on the Rust main thread and the Host is
309/// built on the Renderer's UI thread, which is a different one under
310/// `LoopMode::Renderer`. The runtime it makes is not shared: it lives and dies on the UI
311/// thread.
312pub type RuntimeFactory = Arc<dyn Fn() -> Box<dyn Runtime> + Send + Sync>;
313
314pub struct Host {
315 /// Kept so the tree can be built again from nothing when the Renderer asks for a
316 /// resync, which is the one thing a diff against a lost node table cannot answer.
317 factory: RuntimeFactory,
318 theme: Theme,
319 window: crate::schema::Window,
320 runtime: Box<dyn Runtime>,
321 frame_waker: Waker,
322 pending_appends: Vec<PendingAppend>,
323}
324
325impl Host {
326 /// A Host for the runtime `factory` makes, in the theme the application launched with.
327 pub fn new(factory: impl Fn() -> Box<dyn Runtime> + Send + Sync + 'static) -> Self {
328 Self::from_factory(Arc::new(factory), launched_theme())
329 }
330
331 /// The theme the application chose. Choosing nothing follows the host platform, with
332 /// Material 3 where the platform has no look of its own.
333 pub fn with_theme(
334 factory: impl Fn() -> Box<dyn Runtime> + Send + Sync + 'static,
335 theme: Theme,
336 ) -> Self {
337 Self::from_factory(Arc::new(factory), theme)
338 }
339
340 fn from_factory(factory: RuntimeFactory, theme: Theme) -> Self {
341 // A fresh Host has not been measured yet, and the Renderer that is about to drive
342 // it starts from the same assumption. Leaving a previous Host's last measurement
343 // behind would put the two sides out of step, because the Renderer reports only
344 // differences.
345 crate::window::reset_window_size();
346 // Messages queued against a Host that is going away would otherwise be said by
347 // the one replacing it, out of any context that made them make sense.
348 //
349 crate::message::reset_messages();
350 // The Renderer reports its zoom again at start, and a level the last Host asked for
351 // and never sent is that Host's business.
352 crate::zoom::reset_zoom();
353 // The assets do not go with them. The Renderer this Host is about to talk to has
354 // an empty cache, so every registration has to be made again, which is what this
355 // does: the ids stay as they were and the bytes are queued for the first batch.
356 //
357 // Forgetting them instead is what this used to do, and it was wrong in a way that
358 // only showed on the screen. A theme names its fonts by id and a window names its
359 // icon by id, and both are built by the caller before the Host is made out of
360 // them, so the ids travelled and the bytes they named had been thrown away.
361 crate::asset::requeue_all();
362 // The theme a running application changes from, and nothing queued against the
363 // Host this one replaces.
364 crate::theme::install(theme);
365 let runtime = factory();
366 Self {
367 factory,
368 theme,
369 window: launched_window(),
370 runtime,
371 frame_waker: Waker::from(Arc::new(FrameWake)),
372 pending_appends: Vec::new(),
373 }
374 }
375
376 fn batch(&mut self) -> &mut crate::runtime::Batch {
377 self.runtime.batch_mut()
378 }
379
380 pub fn rebuild(&mut self) -> Result<&[u8], ProtocolError> {
381 let _call = HostCallGuard::enter();
382 self.batch().begin();
383 // One record at the root, before any node exists. The Renderer resolves roles to
384 // values, so switching theme or colour scheme costs this one record rather than a
385 // SetProp for every node in the tree.
386 let (theme, window) = (self.theme, self.window);
387 self.batch().set_theme(theme);
388 // Beside the theme, and for the same reason: it is about the window rather than
389 // any node in it, and it is settled once rather than every frame. The Renderer
390 // reads it out of this batch before it stands the window up.
391 self.batch().set_window(window);
392 self.runtime.rebuild();
393 self.flush_messages();
394 self.arm_scheduler_wake();
395 self.runtime.batch_mut().finish()
396 }
397
398 pub fn dispatch_event(&mut self, bytes: &[u8]) -> Result<(&[u8], i64), ProtocolError> {
399 let event = decode_event(bytes)?;
400 self.dispatch(event)
401 }
402
403 /// The batch arena, reported to the Renderer on every boundary call.
404 pub fn arena(&self) -> (*const u8, usize) {
405 self.runtime.batch().arena()
406 }
407
408 /// Answers with the whole tree, for a Renderer that no longer has a node table.
409 ///
410 /// The Host keeps no shadow of what it has already sent, so the only way to produce a
411 /// full-tree batch is to build the application again, and that resets component state.
412 /// A Renderer that keeps its node table across a configuration change never needs
413 /// this call, which is what the Android host does.
414 pub fn resync(&mut self) -> Result<(&[u8], i64), ProtocolError> {
415 *self = Self::from_factory(self.factory.clone(), self.theme);
416 Ok((self.rebuild()?, 0))
417 }
418
419 /// Suppresses timers and animations while the UI is off screen, and releases the
420 /// request that arrived while it was, so nothing is lost by stopping.
421 fn set_lifecycle_running(&mut self, running: bool) -> Result<(&[u8], i64), ProtocolError> {
422 LIFECYCLE_SUPPRESSED.store(!running, Ordering::Release);
423 if running {
424 if DEFERRED_FRAME_REQUEST.swap(false, Ordering::AcqRel) {
425 (renderer_api().request_frame)();
426 }
427 FRAME_REQUESTED.store(false, Ordering::Release);
428 }
429 // An empty batch, not a frame: starting again is the Renderer's cue to draw, and
430 // it asks for that frame itself.
431 self.batch().begin();
432 Ok((self.runtime.batch_mut().finish()?, 0))
433 }
434
435 /// Renders what the runtime has waiting, writes the Host's own records after it, and
436 /// closes the batch. The tail every call that renders shares.
437 fn render_and_finish(&mut self, result: i64) -> Result<(&[u8], i64), ProtocolError> {
438 self.batch().begin();
439 self.runtime.render(None);
440 self.flush_messages();
441 self.arm_scheduler_wake();
442 Ok((self.runtime.batch_mut().finish()?, result))
443 }
444
445 /// An empty batch, for an event that changed nothing anyone is reading.
446 fn empty_batch(&mut self) -> Result<(&[u8], i64), ProtocolError> {
447 self.batch().begin();
448 Ok((self.runtime.batch_mut().finish()?, 0))
449 }
450
451 /// Hands a pressed notification to the components listening for one.
452 ///
453 /// No node and no handler: the notification is not in the tree, and the component that
454 /// posted it may be long gone. What it carries is the key, which the application reads.
455 fn notification_activated(
456 &mut self,
457 action: u32,
458 key: &str,
459 ) -> Result<(&[u8], i64), ProtocolError> {
460 let woke = {
461 let _dispatch_guard = EventDispatchGuard::enter();
462 crate::notification::activate(crate::notification::NotificationActivation {
463 key: key.to_owned(),
464 action,
465 })
466 };
467 self.batch().begin();
468 if woke {
469 self.runtime.render(None);
470 }
471 self.flush_messages();
472 self.arm_scheduler_wake();
473 Ok((self.runtime.batch_mut().finish()?, 0))
474 }
475
476 /// Records whether notifications may be shown, and renders the components that asked.
477 ///
478 /// The same shape as the resolved design system: sent once after start and again only
479 /// when it changes, and a repeat wakes nobody.
480 fn publish_notification_permission(
481 &mut self,
482 state: crate::schema::NotificationPermission,
483 ) -> Result<(&[u8], i64), ProtocolError> {
484 self.batch().begin();
485 if crate::notification::publish_permission(state) {
486 self.runtime.render(None);
487 }
488 self.flush_messages();
489 self.arm_scheduler_wake();
490 Ok((self.runtime.batch_mut().finish()?, 0))
491 }
492
493 /// Records which design system the Renderer resolved the theme to.
494 ///
495 /// The same shape as a size class arriving: it belongs to no node and no handler, and
496 /// a repeat of the same answer wakes nothing, so an application that never asks pays
497 /// a comparison once per change and nothing per frame.
498 fn publish_design_system(
499 &mut self,
500 system: crate::schema::DesignSystem,
501 ) -> Result<(&[u8], i64), ProtocolError> {
502 if crate::design::publish(system) {
503 return self.render_and_finish(0);
504 }
505 self.empty_batch()
506 }
507
508 /// Records how large the Renderer is drawing, and wakes the components that read it.
509 ///
510 /// The level the Renderer reports is also kept as the window's own, so a resync
511 /// rebuilds the window at the level the reader is looking at rather than at one the
512 /// application asked for before the reader changed it.
513 fn publish_zoom(&mut self, k: f32, os: f32, level: i32) -> Result<(&[u8], i64), ProtocolError> {
514 let level = i8::try_from(level).map_err(|_| ProtocolError::InvalidValueKind(0))?;
515 if self.window.zoom_level.is_some() {
516 self.window.zoom_level = Some(level);
517 }
518 if crate::zoom::publish(crate::zoom::Zoom { k, os, level }) {
519 return self.render_and_finish(0);
520 }
521 self.empty_batch()
522 }
523
524 pub fn dispatch(&mut self, event: HostEvent<'_>) -> Result<(&[u8], i64), ProtocolError> {
525 let _call = HostCallGuard::enter();
526 // These address the Host itself: no node, no handler, and an answer before
527 // anything is looked up.
528 match event.payload {
529 EventPayload::Resync => return self.resync(),
530 EventPayload::DesignSystemResolved(system) => {
531 return self.publish_design_system(system);
532 }
533 EventPayload::LifecycleStart => return self.set_lifecycle_running(true),
534 EventPayload::LifecycleStop => return self.set_lifecycle_running(false),
535 EventPayload::NotificationActivated { action, key } => {
536 if event.node_id != 0 || event.handler_id != 0 {
537 return Err(ProtocolError::InvalidValueKind(0));
538 }
539 return self.notification_activated(action, key);
540 }
541 EventPayload::NotificationPermissionChanged(state) => {
542 if event.node_id != 0 || event.handler_id != 0 {
543 return Err(ProtocolError::InvalidValueKind(0));
544 }
545 return self.publish_notification_permission(state);
546 }
547 // About the platform, like the permission above: no node and no handler, and
548 // a repeat of the same answer wakes nobody.
549 EventPayload::ReducedMotionChanged(state) => {
550 if event.node_id != 0 || event.handler_id != 0 {
551 return Err(ProtocolError::InvalidValueKind(0));
552 }
553 self.batch().begin();
554 if crate::animation::publish_reduced_motion(state) {
555 self.runtime.render(None);
556 }
557 self.flush_messages();
558 self.arm_scheduler_wake();
559 return Ok((self.runtime.batch_mut().finish()?, 0));
560 }
561 // A moment in an animation the Renderer is playing. It names the node the
562 // animation plays on but no handler: whoever registered for animation events
563 // hears about it, and what they change is rendered in the same call, so a
564 // handler that starts the next animation does not leave a frame between them.
565 EventPayload::AnimationEvent {
566 animation_id,
567 kind,
568 property,
569 slot,
570 iteration,
571 elapsed_ms,
572 time_nanos,
573 } => {
574 if event.handler_id != 0 {
575 return Err(ProtocolError::InvalidValueKind(0));
576 }
577 let moment = crate::animation::AnimationMoment {
578 node_id: event.node_id,
579 animation_id,
580 kind,
581 property,
582 slot,
583 iteration,
584 elapsed_ms,
585 time_nanos,
586 };
587 let _dispatch_guard = EventDispatchGuard::enter();
588 let mut delivered = false;
589 self.runtime
590 .run_in_context(&mut || delivered = crate::animation::deliver(moment));
591 if !delivered {
592 return self.empty_batch();
593 }
594 return self.render_and_finish(0);
595 }
596 EventPayload::ZoomChanged { k, os, level } => {
597 if event.node_id != 0 || event.handler_id != 0 {
598 return Err(ProtocolError::InvalidValueKind(0));
599 }
600 return self.publish_zoom(k, os, level);
601 }
602 _ => {}
603 }
604 // The window's size belongs to no node and no handler: the Renderer measures the
605 // root content and reports it. It takes the same synchronous path as every other
606 // event, so the batch it produces is applied in the frame that asked for it.
607 if let EventPayload::WindowSizeChanged {
608 width_dp,
609 height_dp,
610 class,
611 height_class,
612 } = event.payload
613 {
614 // Node id zero is the window; anything else is one of its nodes. One event
615 // for both, because they are the same fact measured at two scales and a
616 // second way of saying it would be a second thing to keep in step.
617 let size = crate::window::WindowSize::new(width_dp, height_dp, class, height_class);
618 if event.node_id != 0 {
619 let woke = self
620 .runtime
621 .size_token(event.node_id)
622 .is_some_and(|token| crate::window::publish_node(token, size));
623 if !woke {
624 return self.empty_batch();
625 }
626 return self.render_and_finish(0);
627 }
628 crate::window::publish(size);
629 return self.render_and_finish(0);
630 }
631 // The action on a transient message. It belongs to no node, because the message is
632 // not in the tree, so it is found by its handler id alone and the event carries the
633 // "no node" id the Renderer uses for anything the tree does not own.
634 let message_action = match event.payload {
635 EventPayload::Clicked => crate::message::take_action(event.handler_id),
636 _ => None,
637 };
638 if let Some(mut action) = message_action {
639 if event.node_id != 0 {
640 return Err(ProtocolError::InvalidValueKind(0));
641 }
642 let _dispatch_guard = EventDispatchGuard::enter();
643 self.runtime.run_in_context(&mut *action);
644 return self.render_and_finish(0);
645 }
646 // Everything else names a handler on a node, and handlers belong to the runtime.
647 if let EventPayload::ProtocolError { .. } = event.payload {
648 return Err(ProtocolError::InvalidValueKind(0));
649 }
650 let _dispatch_guard = EventDispatchGuard::enter();
651 let result = self.runtime.handle_event(&event)?;
652 self.render_and_finish(result)
653 }
654
655 pub fn render_frame(&mut self, frame_time_nanos: u64) -> Result<&[u8], ProtocolError> {
656 let _call = HostCallGuard::enter();
657 if LIFECYCLE_SUPPRESSED.load(Ordering::Acquire) {
658 // The platform has the UI off screen and the Renderer called anyway, which it
659 // does for one reason: a notification was queued, and a notification is what
660 // has to get out while nobody is looking. Only that goes. The components are
661 // not rendered and the held frame request is left held, so timers and
662 // animations stay suppressed until start delivers it.
663 self.batch().begin();
664 self.flush_notifications();
665 return self.runtime.batch_mut().finish();
666 }
667 FRAME_REQUESTED.store(false, Ordering::Release);
668 EVENT_DISPATCH_ACTIVE.store(false, Ordering::Release);
669 DEFERRED_FRAME_REQUEST.store(false, Ordering::Release);
670 self.batch().begin();
671 self.runtime.render(Some(frame_time_nanos));
672 self.flush_pending_appends();
673 self.flush_messages();
674 self.arm_scheduler_wake();
675 self.runtime.batch_mut().finish()
676 }
677
678 /// Queues a streamed tail for the next frame. Tokens arriving inside one frame are
679 /// merged into a single `AppendText` record, so a token never costs a batch of its own.
680 pub fn append_text(&mut self, node_id: u32, tail: &str) {
681 match self
682 .pending_appends
683 .iter_mut()
684 .find(|pending| pending.node_id == node_id)
685 {
686 Some(pending) => {
687 pending.text.push_str(tail);
688 pending.dirty = true;
689 }
690 None => self.pending_appends.push(PendingAppend {
691 node_id,
692 text: tail.to_owned(),
693 dirty: true,
694 }),
695 }
696 // Repeated calls collapse into one frame request; see `request_frame_from_worker`.
697 request_frame_from_worker();
698 }
699
700 /// Writes whatever the tree asked to say during this call into the batch it produced.
701 ///
702 /// A message therefore arrives in the same call as the change it is about, which is
703 /// what makes "deleted" and the row disappearing one frame rather than two.
704 fn flush_messages(&mut self) {
705 // A theme the application changed during this call, one record for the whole
706 // tree. Kept as the Host's own as well, so a resync rebuilds with it.
707 if let Some(theme) = crate::theme::take_pending() {
708 self.theme = theme;
709 self.batch().set_theme(theme);
710 }
711 // A zoom level the application asked for, on the window record that carries it.
712 if let Some(level) = crate::zoom::take_pending_level() {
713 self.window.zoom_level = Some(level);
714 let window = self.window;
715 self.batch().set_window(window);
716 }
717 let batch = self.runtime.batch_mut();
718 // Registrations first. Not because the Renderer needs them first, it applies the
719 // whole batch before drawing any of it, but because a batch read by a person
720 // debugging one reads in the order the screen was built.
721 crate::asset::drain(|pending| {
722 batch.register_asset(pending.asset_id, pending.kind, pending.bytes);
723 });
724 crate::asset::drain_releases(|asset_id| batch.release_asset(asset_id));
725 crate::message::drain(|message| {
726 batch.show_message(
727 message.handler_id,
728 &message.text,
729 &message.action,
730 message.duration,
731 );
732 });
733 self.flush_notifications();
734 // Animations an application started from a handler, a component or a worker.
735 let batch = self.runtime.batch_mut();
736 crate::animation::drain(|mutation| batch.write(mutation));
737 }
738
739 /// Writes the notification commands waiting in the Host into this batch.
740 ///
741 /// Workers queue them from their own threads; this is the one place they leave, on the
742 /// UI thread, inside a call. One atomic read when there are none.
743 fn flush_notifications(&mut self) {
744 let batch = self.runtime.batch_mut();
745 let posts = crate::notification::drain(|command| batch.notification(command));
746 crate::notification::note_posted(posts);
747 }
748
749 fn flush_pending_appends(&mut self) {
750 let batch = self.runtime.batch_mut();
751 for pending in &self.pending_appends {
752 if pending.dirty {
753 batch.append_text_node(pending.node_id, &pending.text);
754 }
755 }
756 // Buffers are kept so steady-state streaming reuses their capacity and a streamed
757 // token does not allocate.
758 for pending in &mut self.pending_appends {
759 pending.text.clear();
760 pending.dirty = false;
761 }
762 }
763
764 /// Registers one asset and returns the batch that carries it.
765 ///
766 /// The bytes are copied once here and once more by the Renderer into its cache. After
767 /// that the id is all that travels, so drawing the same image every frame costs a
768 /// fixed-layout property record and nothing else.
769 pub fn register_asset(
770 &mut self,
771 asset_id: u32,
772 kind: AssetKind,
773 bytes: &[u8],
774 ) -> Result<&[u8], ProtocolError> {
775 let batch = self.runtime.batch_mut();
776 batch.begin();
777 batch.register_asset(asset_id, kind, bytes);
778 batch.finish()
779 }
780
781 /// Registers an icon by the meaning it carries. The Renderer holds the artwork for
782 /// every design system, so what crosses is the role and not a picture or a name.
783 pub fn register_icon(&mut self, asset_id: u32, role: IconRole) -> Result<&[u8], ProtocolError> {
784 self.register_asset(
785 asset_id,
786 AssetKind::VectorIcon,
787 &(role as u16).to_le_bytes(),
788 )
789 }
790
791 /// Drops the asset from the Renderer's cache. Using the id afterwards is a reported
792 /// protocol error.
793 pub fn release_asset(&mut self, asset_id: u32) -> Result<&[u8], ProtocolError> {
794 let batch = self.runtime.batch_mut();
795 batch.begin();
796 batch.release_asset(asset_id);
797 batch.finish()
798 }
799
800 pub fn set_text(
801 &mut self,
802 node_id: u32,
803 text: &str,
804 selection: Option<Selection>,
805 ) -> Result<&[u8], ProtocolError> {
806 let batch = self.runtime.batch_mut();
807 batch.begin();
808 batch.set_text_node(node_id, text, selection);
809 batch.finish()
810 }
811
812 fn arm_scheduler_wake(&mut self) {
813 let mut context = Context::from_waker(&self.frame_waker);
814 if self.runtime.poll_work(&mut context).is_ready() {
815 request_frame_from_worker();
816 }
817 }
818}
819
820/// Holds the thread's `Host` and, crucially, keeps it out of thread-local teardown.
821///
822/// Dropping a runtime can reach back into thread-locals of its own: a Dioxus
823/// `VirtualDom` does. Thread-local destruction order is unspecified, so if the UI thread
824/// ends without `compose_rust_host_shutdown`, that drop can run after those locals are
825/// already gone and panic with "cannot access a TLS value during or after destruction".
826/// A panic in a destructor is non-unwinding: it aborts the process, which
827/// is exactly what must not happen: a protocol or teardown fault has to stay recoverable.
828///
829/// So the slot empties itself and leaks the `Host` when the thread is tearing down. An
830/// orderly `shutdown` still drops it properly; only the unorderly path leaks, and that
831/// path is a thread ending anyway.
832struct HostSlot(RefCell<Option<Host>>);
833
834impl Drop for HostSlot {
835 fn drop(&mut self) {
836 std::mem::forget(self.0.borrow_mut().take());
837 }
838}
839
840impl std::ops::Deref for HostSlot {
841 type Target = RefCell<Option<Host>>;
842
843 fn deref(&self) -> &Self::Target {
844 &self.0
845 }
846}
847
848/// The launched application, as the factory for its runtime.
849///
850/// The runtime and every `compose_rust_host_*` call run on the Renderer UI thread, and
851/// that thread is not the one that called `launch`: with `LoopMode::Renderer` the Rust
852/// main thread blocks inside `compose_rust_renderer_run` while Compose composes on the
853/// toolkit's own thread. So the factory, unlike the `Host` it builds, has to be reachable
854/// across threads, which is why it is `Send + Sync` and the runtime it makes is not.
855///
856/// `shutdown` deliberately leaves this set: it belongs to `launch`, not to one UI thread's
857/// `Host`. That is also what `LoopMode::Platform` needs, where the Renderer may tear the
858/// Host down and initialize it again (Android recreates its surface on a configuration
859/// change) without relaunching.
860static APP: Mutex<Option<RuntimeFactory>> = Mutex::new(None);
861
862/// Chosen by `LaunchBuilder::with_theme`, read once when the Host is built.
863static THEME: Mutex<Theme> = Mutex::new(Theme::unified(crate::schema::DesignSystem::Material3));
864
865/// Chosen by `LaunchBuilder::with_window`, read once when the Host is built.
866static WINDOW: Mutex<crate::schema::Window> = Mutex::new(crate::schema::Window::new());
867
868thread_local! {
869 static HOST: HostSlot = const { HostSlot(RefCell::new(None)) };
870}
871
872fn launched_app() -> Option<RuntimeFactory> {
873 APP.lock().map_or(None, |app| (*app).clone())
874}
875
876fn launched_window() -> crate::schema::Window {
877 WINDOW
878 .lock()
879 .map_or_else(|error| *error.into_inner(), |window| *window)
880}
881
882fn launched_theme() -> Theme {
883 THEME
884 .lock()
885 .map_or_else(|error| *error.into_inner(), |theme| *theme)
886}
887
888#[derive(Clone, Copy, Debug)]
889pub struct LaunchBuilder {
890 mode: LoopMode,
891 theme: Theme,
892 window: crate::schema::Window,
893}
894
895impl Default for LaunchBuilder {
896 fn default() -> Self {
897 Self {
898 mode: LoopMode::Renderer,
899 theme: Theme::default(),
900 window: crate::schema::Window::new(),
901 }
902 }
903}
904
905impl LaunchBuilder {
906 pub fn new() -> Self {
907 Self::default()
908 }
909
910 pub fn with_mode(mut self, mode: LoopMode) -> Self {
911 self.mode = mode;
912 self
913 }
914
915 /// `Theme::unified` for one design system everywhere, `Theme::adaptive` to follow the
916 /// host platform. Not calling this follows the host platform, falling back to
917 /// Material 3.
918 pub fn with_theme(mut self, theme: Theme) -> Self {
919 self.theme = theme;
920 self
921 }
922
923 /// What the application asks of its own window: its size, and whether it wears the
924 /// platform's title bar or has content run into it.
925 ///
926 /// Not calling this gets a modern window that the Renderer sizes. The window belongs
927 /// to the Renderer and most of what it looks like is the design system's, so what can
928 /// be said here is short on purpose: nothing names a colour, a corner, or where the
929 /// window buttons go.
930 pub fn with_window(mut self, window: crate::schema::Window) -> Self {
931 self.window = window;
932 self
933 }
934
935 /// Runs the application whose tree `runtime` builds. Does not return while it is
936 /// running, and ends the process with a failing status if the renderer loop could
937 /// not run at all.
938 ///
939 /// `runtime` is called on the Renderer's UI thread each time a Host is made: once at
940 /// start, and again whenever the Renderer asks for the whole tree back.
941 ///
942 /// Exiting rather than returning is the point. Launching is the last statement of
943 /// `main` in every application that uses this crate, so returning from it means `main`
944 /// returns, which means the process exits 0. A window that never opened would then be
945 /// indistinguishable from a program that did its work and stopped.
946 pub fn launch_runtime(self, runtime: impl Fn() -> Box<dyn Runtime> + Send + Sync + 'static) {
947 let status = self.try_launch_runtime(runtime);
948 if status != STATUS_OK {
949 std::process::exit(EXIT_FAILURE);
950 }
951 }
952
953 /// [`LaunchBuilder::launch_runtime`] without the exit: the status the renderer loop
954 /// ended with, handed back for a caller that has its own idea of what to do with it.
955 pub fn try_launch_runtime(
956 self,
957 runtime: impl Fn() -> Box<dyn Runtime> + Send + Sync + 'static,
958 ) -> i32 {
959 let factory: RuntimeFactory = Arc::new(runtime);
960 if let Ok(mut slot) = APP.lock() {
961 *slot = Some(factory);
962 }
963 if let Ok(mut slot) = THEME.lock() {
964 *slot = self.theme;
965 }
966 // A palette that leaves something unreadable is said once, at start, in a debug
967 // build. It does not stop the launch: a screen whose contrast is a little short is
968 // better than no screen, and what to do about it is the application's call.
969 #[cfg(debug_assertions)]
970 for line in palette_report(&self.theme) {
971 eprintln!("{line}");
972 }
973 if let Ok(mut slot) = WINDOW.lock() {
974 *slot = self.window;
975 }
976 // Under `LoopMode::Platform` the platform owns the loop and calls in through the
977 // boundary when it is ready. There is nothing to run and nothing to fail.
978 if self.mode != LoopMode::Renderer {
979 return STATUS_OK;
980 }
981 (renderer_api().run)()
982 }
983}
984
985/// [`LaunchBuilder::launch_runtime`] with every choice left to its default.
986pub fn launch_runtime(runtime: impl Fn() -> Box<dyn Runtime> + Send + Sync + 'static) {
987 LaunchBuilder::new().launch_runtime(runtime);
988}
989
990/// What a theme's palette leaves short of contrast, as lines to print.
991///
992/// Every system an adaptive theme could land on is checked, because which one it lands on
993/// is decided by the Renderer on a machine this code has not seen. A unified theme is
994/// checked against the one system it names. Empty where there is no palette or nothing is
995/// short.
996pub fn palette_report(theme: &Theme) -> Vec<String> {
997 let Some(palette) = theme.palette else {
998 return Vec::new();
999 };
1000 let systems: Vec<crate::schema::DesignSystem> = if theme.adaptive {
1001 crate::schema::DESIGN_SYSTEM_SCHEMA
1002 .iter()
1003 .filter_map(|variant| crate::schema::DesignSystem::try_from(variant.tag).ok())
1004 .collect()
1005 } else {
1006 vec![theme.design_system]
1007 };
1008 systems
1009 .into_iter()
1010 .flat_map(|system| {
1011 palette.check(system).into_iter().map(move |violation| {
1012 format!("compose-rust: palette under {system:?}: {violation}")
1013 })
1014 })
1015 .collect()
1016}
1017
1018fn parse_handshake(bytes: &[u8]) -> Result<LoopMode, ProtocolError> {
1019 if bytes.len() < 12 {
1020 return Err(ProtocolError::Truncated);
1021 }
1022 let hash = u64::from_le_bytes(
1023 bytes[0..8]
1024 .try_into()
1025 .map_err(|_| ProtocolError::Truncated)?,
1026 );
1027 let version = u16::from_le_bytes(
1028 bytes[8..10]
1029 .try_into()
1030 .map_err(|_| ProtocolError::Truncated)?,
1031 );
1032 if hash != SCHEMA_HASH || version != PROTOCOL_VERSION {
1033 // Say what did not match, and say it here rather than leaving the status code to
1034 // carry it. A mismatch means the two halves were generated from different
1035 // versions of the schema, which on a desktop build means a renderer library
1036 // compiled before the last codegen run. The window still opens, because the
1037 // renderer stands it up before it asks, so the only thing on screen is an empty
1038 // page in the default theme: a symptom that looks like a blank application rather
1039 // than like a stale build, and one that cost a morning to read the first time.
1040 eprintln!(
1041 "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."
1042 );
1043 return Err(ProtocolError::InvalidEnvelope);
1044 }
1045 match bytes[10] {
1046 0 => Ok(LoopMode::Renderer),
1047 1 => Ok(LoopMode::Platform),
1048 other => Err(ProtocolError::InvalidValueKind(u16::from(other))),
1049 }
1050}
1051
1052unsafe fn input_slice<'a>(ptr: *const u8, len: u32) -> Result<&'a [u8], ProtocolError> {
1053 if len == 0 {
1054 return Ok(&[]);
1055 }
1056 if ptr.is_null() {
1057 return Err(ProtocolError::Truncated);
1058 }
1059 // SAFETY: The C caller promises a readable buffer of `len` bytes for this call.
1060 Ok(unsafe { std::slice::from_raw_parts(ptr, len as usize) })
1061}
1062
1063unsafe fn write_batch(
1064 out: *mut MutationBatch,
1065 bytes: &[u8],
1066 result: i64,
1067) -> Result<(), ProtocolError> {
1068 if out.is_null() {
1069 return Err(ProtocolError::Truncated);
1070 }
1071 let len = u32::try_from(bytes.len()).map_err(|_| ProtocolError::LengthOverflow)?;
1072 // SAFETY: Null was rejected and the C caller promises writable storage.
1073 unsafe {
1074 out.write(MutationBatch {
1075 ptr: bytes.as_ptr(),
1076 len,
1077 result,
1078 });
1079 }
1080 Ok(())
1081}
1082
1083/// A call-order failure is not a malformed message: the boundary declares distinct statuses
1084/// so the Renderer can tell "you called me too early" from "your bytes were wrong". The
1085/// closures below therefore yield a status directly.
1086fn ffi_status(operation: impl FnOnce() -> Result<(), i32>) -> i32 {
1087 // There is no channel for the Host to raise a ProtocolError event back to the Renderer:
1088 // the boundary is a synchronous call that returns a status, and events only travel
1089 // Renderer to Host. Malformed calls therefore return STATUS_PROTOCOL_ERROR.
1090 match catch_unwind(AssertUnwindSafe(operation)) {
1091 Ok(Ok(())) => STATUS_OK,
1092 Ok(Err(status)) => status,
1093 Err(_) => STATUS_PANIC,
1094 }
1095}
1096
1097/// Every `ProtocolError` leaving an export is reported as one status code.
1098fn protocol_error(_error: ProtocolError) -> i32 {
1099 STATUS_PROTOCOL_ERROR
1100}
1101
1102#[unsafe(no_mangle)]
1103/// Initializes the UI-thread Host from the handshake and returns the initial batch.
1104///
1105/// # Safety
1106/// `handshake` must address `len` readable bytes and `out` must be writable.
1107pub unsafe extern "C" fn compose_rust_host_init(
1108 handshake: *const u8,
1109 len: u32,
1110 out: *mut MutationBatch,
1111) -> i32 {
1112 ffi_status(|| {
1113 // SAFETY: Validated according to the function's C ABI contract.
1114 let bytes = unsafe { input_slice(handshake, len) }.map_err(protocol_error)?;
1115 let _mode = parse_handshake(bytes).map_err(protocol_error)?;
1116 let factory = launched_app().ok_or(STATUS_NOT_INITIALIZED)?;
1117 HOST.with(|slot| {
1118 let mut host_slot = slot.borrow_mut();
1119 if host_slot.is_some() {
1120 return Err(STATUS_ALREADY_INITIALIZED);
1121 }
1122 let mut host = Host::from_factory(factory, launched_theme());
1123 let batch = host.rebuild().map_err(protocol_error)?;
1124 // SAFETY: `out` is checked before writing.
1125 unsafe { write_batch(out, batch, 0) }.map_err(protocol_error)?;
1126 *host_slot = Some(host);
1127 Ok(())
1128 })
1129 })
1130}
1131
1132#[unsafe(no_mangle)]
1133/// Dispatches one encoded event synchronously and returns its diff batch.
1134///
1135/// # Safety
1136/// `event` must address `len` readable bytes and `out` must be writable.
1137pub unsafe extern "C" fn compose_rust_host_dispatch_event(
1138 event: *const u8,
1139 len: u32,
1140 out: *mut MutationBatch,
1141) -> i32 {
1142 ffi_status(|| {
1143 // SAFETY: Validated according to the function's C ABI contract.
1144 let bytes = unsafe { input_slice(event, len) }.map_err(protocol_error)?;
1145 HOST.with(|slot| {
1146 let mut host_slot = slot.borrow_mut();
1147 let host = host_slot.as_mut().ok_or(STATUS_NOT_INITIALIZED)?;
1148 let (batch, result) = host.dispatch_event(bytes).map_err(protocol_error)?;
1149 // SAFETY: `out` is checked before writing.
1150 unsafe { write_batch(out, batch, result) }.map_err(protocol_error)
1151 })
1152 })
1153}
1154
1155#[unsafe(no_mangle)]
1156/// Renders work scheduled before the current platform frame.
1157///
1158/// # Safety
1159/// `out` must point to writable storage for one [`MutationBatch`].
1160pub unsafe extern "C" fn compose_rust_host_render_frame(
1161 frame_time_nanos: u64,
1162 out: *mut MutationBatch,
1163) -> i32 {
1164 ffi_status(|| {
1165 HOST.with(|slot| {
1166 let mut host_slot = slot.borrow_mut();
1167 let host = host_slot.as_mut().ok_or(STATUS_NOT_INITIALIZED)?;
1168 let batch = host
1169 .render_frame(frame_time_nanos)
1170 .map_err(protocol_error)?;
1171 // SAFETY: `out` is checked before writing.
1172 unsafe { write_batch(out, batch, 0) }.map_err(protocol_error)
1173 })
1174 })
1175}
1176
1177#[unsafe(no_mangle)]
1178/// Releases a batch after the Renderer has applied it on the same call stack.
1179///
1180/// # Safety
1181/// `batch` must be null or point to writable storage for one [`MutationBatch`].
1182pub unsafe extern "C" fn compose_rust_host_release_batch(batch: *mut MutationBatch) {
1183 let _ = catch_unwind(AssertUnwindSafe(|| {
1184 if !batch.is_null() {
1185 // SAFETY: The caller supplied its own writable MutationBatch.
1186 unsafe { batch.write(MutationBatch::default()) };
1187 }
1188 }));
1189}
1190
1191/// The UI thread's batch arena.
1192///
1193/// Not a boundary entry point: it is crate-internal, and the generated Android shims use
1194/// it to tell the Renderer where the arena is so it can map it once instead of per call.
1195pub fn current_arena() -> (*const u8, usize) {
1196 HOST.with(|slot| {
1197 slot.borrow()
1198 .as_ref()
1199 .map_or((std::ptr::null(), 0), Host::arena)
1200 })
1201}
1202
1203#[unsafe(no_mangle)]
1204pub extern "C" fn compose_rust_host_shutdown() {
1205 let _ = catch_unwind(AssertUnwindSafe(|| {
1206 HOST.with(|slot| *slot.borrow_mut() = None);
1207 FRAME_REQUESTED.store(false, Ordering::Release);
1208 }));
1209}
1210
1211#[cfg(test)]
1212mod tests {
1213 use super::*;
1214
1215 /// `APP` is process-global, as launching is, so two tests that launch at the same
1216 /// time would each see the other's. Poisoning is ignored: a failing test has already
1217 /// reported itself, and the rest still need the lock.
1218 static LAUNCH: Mutex<()> = Mutex::new(());
1219
1220 fn launch_guard() -> std::sync::MutexGuard<'static, ()> {
1221 LAUNCH
1222 .lock()
1223 .unwrap_or_else(|poisoned| poisoned.into_inner())
1224 }
1225
1226 #[test]
1227 fn malformed_ffi_input_returns_protocol_error() {
1228 let mut output = MutationBatch::default();
1229 // SAFETY: The test passes a valid output pointer and intentionally null input.
1230 let status = unsafe { compose_rust_host_dispatch_event(std::ptr::null(), 4, &mut output) };
1231 assert_eq!(status, STATUS_PROTOCOL_ERROR);
1232 }
1233
1234 #[test]
1235 fn all_exports_isolate_invalid_calls() {
1236 let _launch = launch_guard();
1237 let mut output = MutationBatch::default();
1238 // SAFETY: Each call uses null or valid test-owned pointers as documented.
1239 unsafe {
1240 assert_eq!(
1241 compose_rust_host_init(std::ptr::null(), 1, &mut output),
1242 STATUS_PROTOCOL_ERROR
1243 );
1244 // No handshake has run on this thread, so this is a call-order error.
1245 assert_eq!(
1246 compose_rust_host_render_frame(0, &mut output),
1247 STATUS_NOT_INITIALIZED
1248 );
1249 compose_rust_host_release_batch(std::ptr::null_mut());
1250 }
1251 compose_rust_host_shutdown();
1252 }
1253}
1254
1255/// The design system a demonstration should start in.
1256///
1257/// An application picks its own theme and never needs this. The samples do, because the
1258/// point of a sample is to show what one declaration looks like under each system, and on
1259/// any given machine the adaptive default can only ever show you one of them. Material 3
1260/// and Fluent went unseen for weeks for exactly that reason: everything was checked on a
1261/// Mac, so everything was Cupertino.
1262///
1263/// `DXC_DESIGN` names the system. Anything else, including nothing, adapts to the host.
1264/// `DXC_SCHEME` names `light` or `dark`; anything else leaves the reader's own setting
1265/// alone.
1266pub fn demo_theme() -> Theme {
1267 demo_theme_for(Theme::adaptive(crate::schema::DesignSystem::Material3))
1268}
1269
1270/// The same, for a demonstration that has a theme of its own to start from.
1271///
1272/// A sample whose design is one particular design system in one particular colour scheme
1273/// says so, and that declaration is the sample. But the machine it is being looked at on
1274/// can only draw one of the seven at a time, so without a way to point it somewhere else
1275/// the other six are never seen. This keeps the sample's own theme as the answer and lets
1276/// the two variables override the part they name, so what is on screen is either the
1277/// design the sample chose or exactly the one that was asked for.
1278pub fn demo_theme_for(theme: Theme) -> Theme {
1279 use crate::schema::DesignSystem;
1280 let theme = match std::env::var("DXC_DESIGN").as_deref().map(str::trim) {
1281 Ok("material3") => Theme::unified(DesignSystem::Material3),
1282 Ok("cupertino") => Theme::unified(DesignSystem::Cupertino),
1283 Ok("fluent") => Theme::unified(DesignSystem::Fluent),
1284 Ok("gnome") => Theme::unified(DesignSystem::Gnome),
1285 Ok("breeze") => Theme::unified(DesignSystem::Breeze),
1286 Ok("deepin") => Theme::unified(DesignSystem::Deepin),
1287 // Spelled both ways, because the name is two words everywhere it is written down
1288 // and nobody remembers which one a shell variable wants.
1289 Ok("liquidglass") | Ok("liquid-glass") => Theme::unified(DesignSystem::LiquidGlass),
1290 // Naming a system replaces the sample's design system but keeps its colour
1291 // scheme, so asking to see one design does not also change how light it is.
1292 _ => return with_scheme_override(theme),
1293 }
1294 .with_color_scheme(theme.color_scheme);
1295 with_scheme_override(theme)
1296}
1297
1298/// Applies `DXC_SCHEME` if it names one of the two schemes.
1299fn with_scheme_override(theme: Theme) -> Theme {
1300 match std::env::var("DXC_SCHEME").as_deref().map(str::trim) {
1301 Ok("light") => theme.with_color_scheme(crate::schema::ColorScheme::Light),
1302 Ok("dark") => theme.with_color_scheme(crate::schema::ColorScheme::Dark),
1303 _ => theme,
1304 }
1305}
1306
1307#[cfg(test)]
1308mod demo_theme_tests {
1309 use super::*;
1310 use crate::schema::{ColorScheme, DESIGN_SYSTEM_SCHEMA, DesignSystem};
1311 use std::sync::{Mutex, MutexGuard};
1312
1313 /// Held for the length of any test that sets one of the variables.
1314 ///
1315 /// The variables belong to the process, not to the test, so two of these running at
1316 /// once read each other's settings and fail on a value neither of them asked for.
1317 static ENVIRONMENT: Mutex<()> = Mutex::new(());
1318
1319 /// Clears both variables and keeps everyone else out until the guard is dropped.
1320 fn exclusive_environment() -> MutexGuard<'static, ()> {
1321 let guard = ENVIRONMENT
1322 .lock()
1323 .unwrap_or_else(|error| error.into_inner());
1324 // SAFETY: the lock makes this the only thread touching the environment, and the
1325 // variables are read only by the functions called under the same lock.
1326 unsafe {
1327 std::env::remove_var("DXC_DESIGN");
1328 std::env::remove_var("DXC_SCHEME");
1329 }
1330 guard
1331 }
1332
1333 /// Named for what it defends: a sample that cannot be pointed at a design system
1334 /// leaves five of the six unseen on any one machine.
1335 #[test]
1336 fn fr14_a_named_design_system_is_unified_and_anything_else_adapts() {
1337 let _environment = exclusive_environment();
1338 // SAFETY: `exclusive_environment` holds the lock, so nothing else is reading or
1339 // writing these while this runs.
1340 for (name, system) in [
1341 ("material3", DesignSystem::Material3),
1342 ("cupertino", DesignSystem::Cupertino),
1343 ("fluent", DesignSystem::Fluent),
1344 ("gnome", DesignSystem::Gnome),
1345 ("breeze", DesignSystem::Breeze),
1346 ("deepin", DesignSystem::Deepin),
1347 ("liquidglass", DesignSystem::LiquidGlass),
1348 ("liquid-glass", DesignSystem::LiquidGlass),
1349 ] {
1350 unsafe { std::env::set_var("DXC_DESIGN", name) };
1351 assert_eq!(
1352 demo_theme(),
1353 Theme::unified(system),
1354 "{name} is not selectable"
1355 );
1356 }
1357 assert_eq!(
1358 DESIGN_SYSTEM_SCHEMA.len(),
1359 7,
1360 "a system nobody can select goes unseen"
1361 );
1362
1363 unsafe { std::env::set_var("DXC_DESIGN", "nonsense") };
1364 assert_eq!(demo_theme(), Theme::adaptive(DesignSystem::Material3));
1365
1366 unsafe { std::env::remove_var("DXC_DESIGN") };
1367 assert_eq!(demo_theme(), Theme::adaptive(DesignSystem::Material3));
1368 }
1369
1370 /// Named for what it defends: a machine set to dark draws every sample dark, so a
1371 /// design that is meant to be read light cannot be looked at at all without this.
1372 #[test]
1373 fn fr14_a_named_colour_scheme_is_pinned_and_anything_else_is_left_alone() {
1374 let _environment = exclusive_environment();
1375 // SAFETY: as above.
1376 for (name, scheme) in [("light", ColorScheme::Light), ("dark", ColorScheme::Dark)] {
1377 unsafe { std::env::set_var("DXC_SCHEME", name) };
1378 assert_eq!(
1379 demo_theme().color_scheme,
1380 scheme,
1381 "{name} is not selectable"
1382 );
1383 }
1384
1385 unsafe { std::env::set_var("DXC_SCHEME", "nonsense") };
1386 assert_eq!(demo_theme().color_scheme, ColorScheme::FollowSystem);
1387
1388 unsafe { std::env::remove_var("DXC_SCHEME") };
1389 assert_eq!(demo_theme().color_scheme, ColorScheme::FollowSystem);
1390 }
1391
1392 /// A sample that names its own theme keeps it, and each variable overrides only the
1393 /// half it names. Asking to see Liquid Glass must not also throw away the colour
1394 /// scheme the design was drawn for.
1395 #[test]
1396 fn fr14_a_samples_own_theme_survives_everything_the_variables_do_not_name() {
1397 let _environment = exclusive_environment();
1398 let sample = Theme::unified(DesignSystem::Cupertino).with_color_scheme(ColorScheme::Light);
1399 assert_eq!(demo_theme_for(sample), sample);
1400
1401 unsafe { std::env::set_var("DXC_DESIGN", "liquidglass") };
1402 assert_eq!(
1403 demo_theme_for(sample),
1404 Theme::unified(DesignSystem::LiquidGlass).with_color_scheme(ColorScheme::Light)
1405 );
1406
1407 unsafe { std::env::set_var("DXC_SCHEME", "dark") };
1408 assert_eq!(
1409 demo_theme_for(sample),
1410 Theme::unified(DesignSystem::LiquidGlass).with_color_scheme(ColorScheme::Dark)
1411 );
1412
1413 unsafe {
1414 std::env::remove_var("DXC_DESIGN");
1415 std::env::remove_var("DXC_SCHEME");
1416 };
1417 }
1418 /// A renderer built from a different schema is refused, and says so.
1419 ///
1420 /// The refusal already worked. What did not was finding out why: the renderer stands
1421 /// its window up before it asks the Host anything, so a mismatch leaves an empty page
1422 /// in the default theme and a status code, which reads as an application that draws
1423 /// nothing rather than as a build that is out of date. It cost a morning once.
1424 #[test]
1425 fn pr2_a_handshake_from_another_schema_is_refused() {
1426 let mut wrong = Vec::with_capacity(12);
1427 wrong.extend_from_slice(&SCHEMA_HASH.wrapping_add(1).to_le_bytes());
1428 wrong.extend_from_slice(&PROTOCOL_VERSION.to_le_bytes());
1429 wrong.extend_from_slice(&[LoopMode::Renderer as u8, 0]);
1430 assert!(
1431 matches!(
1432 parse_handshake(&wrong),
1433 Err(crate::protocol::ProtocolError::InvalidEnvelope)
1434 ),
1435 "a handshake carrying another schema's hash was accepted, so the two halves \
1436 would go on to disagree about what every record means"
1437 );
1438
1439 let mut right = Vec::with_capacity(12);
1440 right.extend_from_slice(&SCHEMA_HASH.to_le_bytes());
1441 right.extend_from_slice(&PROTOCOL_VERSION.to_le_bytes());
1442 right.extend_from_slice(&[LoopMode::Renderer as u8, 0]);
1443 assert!(
1444 parse_handshake(&right).is_ok(),
1445 "the handshake this build generates is not one it accepts"
1446 );
1447 }
1448}
1449
1450/// Keeps the five exported boundary functions in the final executable.
1451///
1452/// The Renderer resolves them by name once it is loaded, which means nothing in the
1453/// application ever refers to them and the linker is free to conclude they are dead. It
1454/// does exactly that in any binary whose own code happens not to reach them, a test
1455/// harness for an application being the ordinary case, and the result is a process that
1456/// dies on startup with the dynamic loader unable to bind a symbol that should have been
1457/// right there in the executable.
1458///
1459/// Taking their addresses in a static the compiler is told to keep marks them as roots
1460/// for the dead-code pass, which is the whole job. It costs five pointers.
1461#[used]
1462static BOUNDARY_EXPORTS: BoundaryExports = BoundaryExports([
1463 compose_rust_host_init as *const (),
1464 compose_rust_host_dispatch_event as *const (),
1465 compose_rust_host_render_frame as *const (),
1466 compose_rust_host_release_batch as *const (),
1467 compose_rust_host_shutdown as *const (),
1468]);
1469
1470// The same five names, as a linker directive the object file carries.
1471//
1472// The Renderer finds these with GetProcAddress against the running executable, which
1473// reads the PE export table, and an executable has no export table unless the link is
1474// told to make one. The build script asks for that with `/EXPORT:` arguments, and those
1475// reach the binaries of this package and no further: Cargo does not pass a dependency's
1476// link arguments on to whatever depends on it.
1477//
1478// So every application built on this crate linked without an export table, the Renderer
1479// could not reach back into it, and the window came up empty. It looked like it worked
1480// because the only Windows binaries anyone had run were this package's own examples.
1481//
1482// `.drectve` is how an object file carries linker arguments of its own. The MSVC linker
1483// reads that section out of every object it links, so the directive travels inside the
1484// rlib and applies wherever the rlib ends up.
1485#[cfg(all(target_os = "windows", target_env = "msvc"))]
1486core::arch::global_asm!(
1487 // A section of directives and nothing else, in the COFF flags the MSVC linker reads
1488 // them from: information only ("i") and dropped from the image ("n").
1489 //
1490 // Assembled rather than written as a `static` placed in the section. A static is a
1491 // symbol, and a crate's own DLL asks for every symbol it keeps by name; the LLVM linker
1492 // takes the section's bytes as directives, keeps no symbol for them, and the DLL then
1493 // fails to link on a symbol nobody can define.
1494 ".section .drectve,\"yni\"",
1495 ".ascii \" /EXPORT:compose_rust_host_init\"",
1496 ".ascii \" /EXPORT:compose_rust_host_dispatch_event\"",
1497 ".ascii \" /EXPORT:compose_rust_host_render_frame\"",
1498 ".ascii \" /EXPORT:compose_rust_host_release_batch\"",
1499 ".ascii \" /EXPORT:compose_rust_host_shutdown\"",
1500 ".text",
1501);
1502
1503// Which C runtime an application linking the Kotlin/Native Windows renderer gets, said
1504// where the linker reads it from every object: the build script cannot say it, for the
1505// same reason it cannot ask for the exports above.
1506//
1507// Set by the build script when it links that renderer and the application asked for
1508// nothing (`build/windows_crt.rs` says why each of these is here). The Universal C Runtime
1509// stays the DLL that is part of Windows; vcruntime and the C++ standard library are linked
1510// in, so the executable needs no Visual C++ runtime DLL beside it. An application that asked
1511// for `+crt-static` links all of it statically and needs none of these.
1512#[cfg(all(target_os = "windows", target_env = "msvc", windows_crt_linked_in))]
1513core::arch::global_asm!(
1514 ".section .drectve,\"yni\"",
1515 ".ascii \" /NODEFAULTLIB:vcruntime.lib\"",
1516 ".ascii \" /DEFAULTLIB:libvcruntime.lib\"",
1517 ".ascii \" /NODEFAULTLIB:msvcprt.lib\"",
1518 ".ascii \" /NODEFAULTLIB:libcpmt.lib\"",
1519 ".ascii \" /NODEFAULTLIB:libcmt.lib\"",
1520 ".ascii \" /INCLUDE:longjmp\"",
1521 ".text",
1522);
1523
1524// The same for an application that asked for vcruntime from its DLL
1525// (`DXC_WINDOWS_CRT=dynamic`): the stock static C++ library and C runtime that a prebuilt
1526// object may still name as defaults would define, a second time, what the build script's
1527// blanked copy and the DLLs' import libraries already do.
1528#[cfg(all(target_os = "windows", target_env = "msvc", windows_crt_dynamic))]
1529core::arch::global_asm!(
1530 ".section .drectve,\"yni\"",
1531 ".ascii \" /NODEFAULTLIB:libcpmt.lib\"",
1532 ".ascii \" /NODEFAULTLIB:libcmt.lib\"",
1533 ".text",
1534);
1535
1536// Never read. Being referenced is the entire contract.
1537struct BoundaryExports(#[allow(dead_code)] [*const (); 5]);
1538
1539// SAFETY: The addresses are written once, at compile time, and never read. A static has
1540// to be Sync to exist at all, and raw pointers decline to be only because of what they
1541// might point at; these point at code.
1542unsafe impl Sync for BoundaryExports {}