Skip to main content

compose_rust/
boundary.rs

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;
19/// There is no renderer in this build to run. Returned by the loop entry point, never by
20/// a boundary call: a call could not have got this far without a renderer to make it.
21pub const STATUS_NO_RENDERER: i32 = -5;
22
23/// What the process exits with when the renderer loop does not finish successfully.
24///
25/// 1, not the status itself: exit codes are a single byte on Unix, so `STATUS_NO_RENDERER`
26/// would reach the shell as 251 and read as a signal rather than an ordinary failure.
27const 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);
57/// Set between a stop and the start that follows it. While it is set a worker's frame
58/// request is remembered but not delivered, so a process that is not on screen is never
59/// asked to draw.
60static LIFECYCLE_SUPPRESSED: AtomicBool = AtomicBool::new(false);
61
62pub fn install_renderer_api(api: RendererApi) -> Result<(), RendererApi> {
63    RENDERER_API.set(api)
64}
65
66// `renderer_linked` is set by the build script, and only when a renderer was really
67// linked. That is not the same as the feature being on: a documentation build turns the
68// feature on and links nothing, and the result has to behave like the build it is.
69#[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// Defined by the small library the Linux static renderer ships beside its archive. The
76// renderer finds the Host's functions with dlsym, so the application has to export them,
77// and that library is what makes the linker do it: it needs them, and a linker exports
78// from an executable what a library it links needs. Rust links with --as-needed, which
79// drops a library nothing refers to, so this byte is the reference that keeps it.
80#[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    // A volatile read so that the reference survives optimisation. The value means
92    // nothing; being linked is the point.
93    #[cfg(renderer_host_exports)]
94    // SAFETY: a byte the library defines as a constant.
95    let _ = unsafe { std::ptr::read_volatile(&raw const compose_rust_renderer_host_exports) };
96    // SAFETY: The application links the Renderer implementation of this declared C ABI.
97    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    // SAFETY: The Renderer contract makes request_frame thread-safe.
103    unsafe { compose_rust_renderer_request_frame() }
104}
105
106// The mock renderer and the crate's own tests drive the boundary directly and never want
107// a window, so doing nothing is the correct answer for them and always has been.
108#[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// A real build with no renderer. This used to return success, so an application built
117// this way opened no window, drew nothing, printed nothing and exited 0, and there was
118// no way to tell that from a program that had simply finished.
119#[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
128/// What a build with no renderer says on its way out.
129///
130/// Compiled into every build, not only the one that prints it, so that a test can read it
131/// whatever the build it is running in was configured with.
132pub 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
157/// Coalesced wake used internally by the runtime's scheduler waker.
158///
159/// The flag spans the moment of delivery, not the wait for the frame that answers it. The
160/// Renderer folds requests into its own frame clock, so however many arrive between two
161/// frames it draws once; holding the flag until the frame came back would instead mean
162/// that one request the Renderer was not yet listening for silenced every later one. That
163/// happens on a cold start, where the first composition can be seconds after the first
164/// worker request, and it leaves the application frozen with nothing to unfreeze it.
165pub 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    /// How deep this thread is inside a Host call. A notification posted from inside one is
180    /// written into the batch that call returns, so it needs no frame of its own.
181    static HOST_CALL_DEPTH: std::cell::Cell<u32> = const { std::cell::Cell::new(0) };
182}
183
184/// Whether this thread is inside `rebuild`, `dispatch` or `render_frame` right now.
185pub(crate) fn in_host_call() -> bool {
186    HOST_CALL_DEPTH.with(std::cell::Cell::get) > 0
187}
188
189/// Marks the span of one Host call on this thread.
190struct 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
205/// Asks for the frame that carries a queued notification out.
206///
207/// The one request a stopped UI does not hold back. Holding requests while the platform
208/// has the UI off screen is right for timers and animations, which nobody is watching, and
209/// wrong for a notification, whose whole point is the moment nobody is watching. So while
210/// stopped this goes to the Renderer directly, and the Renderer serves it outside its
211/// stopped frame clock. Everything else stays held, and the frame it was waiting for is
212/// still delivered on start.
213pub(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
255/// One streaming Text node's tail, accumulated between frames.
256struct PendingAppend {
257    node_id: u32,
258    text: String,
259    dirty: bool,
260}
261
262/// Makes the runtime a Host drives. Called once per Host, and again on a resync, which
263/// is the one thing a diff against a lost node table cannot answer.
264///
265/// Shared across threads because `launch` runs on the Rust main thread and the Host is
266/// built on the Renderer's UI thread, which is a different one under
267/// `LoopMode::Renderer`. The runtime it makes is not shared: it lives and dies on the UI
268/// thread.
269pub type RuntimeFactory = Arc<dyn Fn() -> Box<dyn Runtime> + Send + Sync>;
270
271pub struct Host {
272    /// Kept so the tree can be built again from nothing when the Renderer asks for a
273    /// resync, which is the one thing a diff against a lost node table cannot answer.
274    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    /// A Host for the runtime `factory` makes, in the theme the application launched with.
284    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    /// The theme the application chose. Choosing nothing follows the host platform, with
289    /// Material 3 where the platform has no look of its own.
290    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        // A fresh Host has not been measured yet, and the Renderer that is about to drive
299        // it starts from the same assumption. Leaving a previous Host's last measurement
300        // behind would put the two sides out of step, because the Renderer reports only
301        // differences.
302        crate::window::reset_window_size();
303        // Messages queued against a Host that is going away would otherwise be said by
304        // the one replacing it, out of any context that made them make sense.
305        //
306        crate::message::reset_messages();
307        // The assets do not go with them. The Renderer this Host is about to talk to has
308        // an empty cache, so every registration has to be made again, which is what this
309        // does: the ids stay as they were and the bytes are queued for the first batch.
310        //
311        // Forgetting them instead is what this used to do, and it was wrong in a way that
312        // only showed on the screen. A theme names its fonts by id and a window names its
313        // icon by id, and both are built by the caller before the Host is made out of
314        // them, so the ids travelled and the bytes they named had been thrown away.
315        crate::asset::requeue_all();
316        // The theme a running application changes from, and nothing queued against the
317        // Host this one replaces.
318        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        // One record at the root, before any node exists. The Renderer resolves roles to
338        // values, so switching theme or colour scheme costs this one record rather than a
339        // SetProp for every node in the tree.
340        let (theme, window) = (self.theme, self.window);
341        self.batch().set_theme(theme);
342        // Beside the theme, and for the same reason: it is about the window rather than
343        // any node in it, and it is settled once rather than every frame. The Renderer
344        // reads it out of this batch before it stands the window up.
345        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    /// The batch arena, reported to the Renderer on every boundary call.
358    pub fn arena(&self) -> (*const u8, usize) {
359        self.runtime.batch().arena()
360    }
361
362    /// Answers with the whole tree, for a Renderer that no longer has a node table.
363    ///
364    /// The Host keeps no shadow of what it has already sent, so the only way to produce a
365    /// full-tree batch is to build the application again, and that resets component state.
366    /// A Renderer that keeps its node table across a configuration change never needs
367    /// this call, which is what the Android host does.
368    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    /// Suppresses timers and animations while the UI is off screen, and releases the
374    /// request that arrived while it was, so nothing is lost by stopping.
375    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        // An empty batch, not a frame: starting again is the Renderer's cue to draw, and
384        // it asks for that frame itself.
385        self.batch().begin();
386        Ok((self.runtime.batch_mut().finish()?, 0))
387    }
388
389    /// Renders what the runtime has waiting, writes the Host's own records after it, and
390    /// closes the batch. The tail every call that renders shares.
391    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    /// An empty batch, for an event that changed nothing anyone is reading.
400    fn empty_batch(&mut self) -> Result<(&[u8], i64), ProtocolError> {
401        self.batch().begin();
402        Ok((self.runtime.batch_mut().finish()?, 0))
403    }
404
405    /// Hands a pressed notification to the components listening for one.
406    ///
407    /// No node and no handler: the notification is not in the tree, and the component that
408    /// posted it may be long gone. What it carries is the key, which the application reads.
409    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    /// Records whether notifications may be shown, and renders the components that asked.
431    ///
432    /// The same shape as the resolved design system: sent once after start and again only
433    /// when it changes, and a repeat wakes nobody.
434    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    /// Records which design system the Renderer resolved the theme to.
448    ///
449    /// The same shape as a size class arriving: it belongs to no node and no handler, and
450    /// a repeat of the same answer wakes nothing, so an application that never asks pays
451    /// a comparison once per change and nothing per frame.
452    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        // These address the Host itself: no node, no handler, and an answer before
465        // anything is looked up.
466        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        // The window's size belongs to no node and no handler: the Renderer measures the
488        // root content and reports it. It takes the same synchronous path as every other
489        // event, so the batch it produces is applied in the frame that asked for it.
490        if let EventPayload::WindowSizeChanged {
491            width_dp,
492            height_dp,
493            class,
494            height_class,
495        } = event.payload
496        {
497            // Node id zero is the window; anything else is one of its nodes. One event
498            // for both, because they are the same fact measured at two scales and a
499            // second way of saying it would be a second thing to keep in step.
500            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        // The action on a transient message. It belongs to no node, because the message is
515        // not in the tree, so it is found by its handler id alone and the event carries the
516        // "no node" id the Renderer uses for anything the tree does not own.
517        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        // Everything else names a handler on a node, and handlers belong to the runtime.
530        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            // The platform has the UI off screen and the Renderer called anyway, which it
542            // does for one reason: a notification was queued, and a notification is what
543            // has to get out while nobody is looking. Only that goes. The components are
544            // not rendered and the held frame request is left held, so timers and
545            // animations stay suppressed until start delivers it.
546            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    /// Queues a streamed tail for the next frame. Tokens arriving inside one frame are
562    /// merged into a single `AppendText` record, so a token never costs a batch of its own.
563    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        // Repeated calls collapse into one frame request; see `request_frame_from_worker`.
580        request_frame_from_worker();
581    }
582
583    /// Writes whatever the tree asked to say during this call into the batch it produced.
584    ///
585    /// A message therefore arrives in the same call as the change it is about, which is
586    /// what makes "deleted" and the row disappearing one frame rather than two.
587    fn flush_messages(&mut self) {
588        // A theme the application changed during this call, one record for the whole
589        // tree. Kept as the Host's own as well, so a resync rebuilds with it.
590        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        // Registrations first. Not because the Renderer needs them first, it applies the
596        // whole batch before drawing any of it, but because a batch read by a person
597        // debugging one reads in the order the screen was built.
598        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    /// Writes the notification commands waiting in the Host into this batch.
613    ///
614    /// Workers queue them from their own threads; this is the one place they leave, on the
615    /// UI thread, inside a call. One atomic read when there are none.
616    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        // Buffers are kept so steady-state streaming reuses their capacity and a streamed
630        // token does not allocate.
631        for pending in &mut self.pending_appends {
632            pending.text.clear();
633            pending.dirty = false;
634        }
635    }
636
637    /// Registers one asset and returns the batch that carries it.
638    ///
639    /// The bytes are copied once here and once more by the Renderer into its cache. After
640    /// that the id is all that travels, so drawing the same image every frame costs a
641    /// fixed-layout property record and nothing else.
642    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    /// Registers an icon by the meaning it carries. The Renderer holds the artwork for
655    /// every design system, so what crosses is the role and not a picture or a name.
656    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    /// Drops the asset from the Renderer's cache. Using the id afterwards is a reported
665    /// protocol error.
666    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
693/// Holds the thread's `Host` and, crucially, keeps it out of thread-local teardown.
694///
695/// Dropping a runtime can reach back into thread-locals of its own: a Dioxus
696/// `VirtualDom` does. Thread-local destruction order is unspecified, so if the UI thread
697/// ends without `compose_rust_host_shutdown`, that drop can run after those locals are
698/// already gone and panic with "cannot access a TLS value during or after destruction".
699/// A panic in a destructor is non-unwinding: it aborts the process, which
700/// is exactly what must not happen: a protocol or teardown fault has to stay recoverable.
701///
702/// So the slot empties itself and leaks the `Host` when the thread is tearing down. An
703/// orderly `shutdown` still drops it properly; only the unorderly path leaks, and that
704/// path is a thread ending anyway.
705struct 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
721/// The launched application, as the factory for its runtime.
722///
723/// The runtime and every `compose_rust_host_*` call run on the Renderer UI thread, and
724/// that thread is not the one that called `launch`: with `LoopMode::Renderer` the Rust
725/// main thread blocks inside `compose_rust_renderer_run` while Compose composes on the
726/// toolkit's own thread. So the factory, unlike the `Host` it builds, has to be reachable
727/// across threads, which is why it is `Send + Sync` and the runtime it makes is not.
728///
729/// `shutdown` deliberately leaves this set: it belongs to `launch`, not to one UI thread's
730/// `Host`. That is also what `LoopMode::Platform` needs, where the Renderer may tear the
731/// Host down and initialize it again (Android recreates its surface on a configuration
732/// change) without relaunching.
733static APP: Mutex<Option<RuntimeFactory>> = Mutex::new(None);
734
735/// Chosen by `LaunchBuilder::with_theme`, read once when the Host is built.
736static THEME: Mutex<Theme> = Mutex::new(Theme::unified(crate::schema::DesignSystem::Material3));
737
738/// Chosen by `LaunchBuilder::with_window`, read once when the Host is built.
739static 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    /// `Theme::unified` for one design system everywhere, `Theme::adaptive` to follow the
789    /// host platform. Not calling this follows the host platform, falling back to
790    /// Material 3.
791    pub fn with_theme(mut self, theme: Theme) -> Self {
792        self.theme = theme;
793        self
794    }
795
796    /// What the application asks of its own window: its size, and whether it wears the
797    /// platform's title bar or has content run into it.
798    ///
799    /// Not calling this gets a modern window that the Renderer sizes. The window belongs
800    /// to the Renderer and most of what it looks like is the design system's, so what can
801    /// be said here is short on purpose: nothing names a colour, a corner, or where the
802    /// window buttons go.
803    pub fn with_window(mut self, window: crate::schema::Window) -> Self {
804        self.window = window;
805        self
806    }
807
808    /// Runs the application whose tree `runtime` builds. Does not return while it is
809    /// running, and ends the process with a failing status if the renderer loop could
810    /// not run at all.
811    ///
812    /// `runtime` is called on the Renderer's UI thread each time a Host is made: once at
813    /// start, and again whenever the Renderer asks for the whole tree back.
814    ///
815    /// Exiting rather than returning is the point. Launching is the last statement of
816    /// `main` in every application that uses this crate, so returning from it means `main`
817    /// returns, which means the process exits 0. A window that never opened would then be
818    /// indistinguishable from a program that did its work and stopped.
819    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    /// [`LaunchBuilder::launch_runtime`] without the exit: the status the renderer loop
827    /// ended with, handed back for a caller that has its own idea of what to do with it.
828    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        // A palette that leaves something unreadable is said once, at start, in a debug
840        // build. It does not stop the launch: a screen whose contrast is a little short is
841        // better than no screen, and what to do about it is the application's call.
842        #[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        // Under `LoopMode::Platform` the platform owns the loop and calls in through the
850        // boundary when it is ready. There is nothing to run and nothing to fail.
851        if self.mode != LoopMode::Renderer {
852            return STATUS_OK;
853        }
854        (renderer_api().run)()
855    }
856}
857
858/// [`LaunchBuilder::launch_runtime`] with every choice left to its default.
859pub fn launch_runtime(runtime: impl Fn() -> Box<dyn Runtime> + Send + Sync + 'static) {
860    LaunchBuilder::new().launch_runtime(runtime);
861}
862
863/// What a theme's palette leaves short of contrast, as lines to print.
864///
865/// Every system an adaptive theme could land on is checked, because which one it lands on
866/// is decided by the Renderer on a machine this code has not seen. A unified theme is
867/// checked against the one system it names. Empty where there is no palette or nothing is
868/// short.
869pub 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        // Say what did not match, and say it here rather than leaving the status code to
907        // carry it. A mismatch means the two halves were generated from different
908        // versions of the schema, which on a desktop build means a renderer library
909        // compiled before the last codegen run. The window still opens, because the
910        // renderer stands it up before it asks, so the only thing on screen is an empty
911        // page in the default theme: a symptom that looks like a blank application rather
912        // than like a stale build, and one that cost a morning to read the first time.
913        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    // SAFETY: The C caller promises a readable buffer of `len` bytes for this call.
933    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    // SAFETY: Null was rejected and the C caller promises writable storage.
946    unsafe {
947        out.write(MutationBatch {
948            ptr: bytes.as_ptr(),
949            len,
950            result,
951        });
952    }
953    Ok(())
954}
955
956/// A call-order failure is not a malformed message: the boundary declares distinct statuses
957/// so the Renderer can tell "you called me too early" from "your bytes were wrong". The
958/// closures below therefore yield a status directly.
959fn ffi_status(operation: impl FnOnce() -> Result<(), i32>) -> i32 {
960    // There is no channel for the Host to raise a ProtocolError event back to the Renderer:
961    // the boundary is a synchronous call that returns a status, and events only travel
962    // Renderer to Host. Malformed calls therefore return STATUS_PROTOCOL_ERROR.
963    match catch_unwind(AssertUnwindSafe(operation)) {
964        Ok(Ok(())) => STATUS_OK,
965        Ok(Err(status)) => status,
966        Err(_) => STATUS_PANIC,
967    }
968}
969
970/// Every `ProtocolError` leaving an export is reported as one status code.
971fn protocol_error(_error: ProtocolError) -> i32 {
972    STATUS_PROTOCOL_ERROR
973}
974
975#[unsafe(no_mangle)]
976/// Initializes the UI-thread Host from the handshake and returns the initial batch.
977///
978/// # Safety
979/// `handshake` must address `len` readable bytes and `out` must be writable.
980pub 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        // SAFETY: Validated according to the function's C ABI contract.
987        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            // SAFETY: `out` is checked before writing.
998            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)]
1006/// Dispatches one encoded event synchronously and returns its diff batch.
1007///
1008/// # Safety
1009/// `event` must address `len` readable bytes and `out` must be writable.
1010pub 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        // SAFETY: Validated according to the function's C ABI contract.
1017        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            // SAFETY: `out` is checked before writing.
1023            unsafe { write_batch(out, batch, result) }.map_err(protocol_error)
1024        })
1025    })
1026}
1027
1028#[unsafe(no_mangle)]
1029/// Renders work scheduled before the current platform frame.
1030///
1031/// # Safety
1032/// `out` must point to writable storage for one [`MutationBatch`].
1033pub 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            // SAFETY: `out` is checked before writing.
1045            unsafe { write_batch(out, batch, 0) }.map_err(protocol_error)
1046        })
1047    })
1048}
1049
1050#[unsafe(no_mangle)]
1051/// Releases a batch after the Renderer has applied it on the same call stack.
1052///
1053/// # Safety
1054/// `batch` must be null or point to writable storage for one [`MutationBatch`].
1055pub unsafe extern "C" fn compose_rust_host_release_batch(batch: *mut MutationBatch) {
1056    let _ = catch_unwind(AssertUnwindSafe(|| {
1057        if !batch.is_null() {
1058            // SAFETY: The caller supplied its own writable MutationBatch.
1059            unsafe { batch.write(MutationBatch::default()) };
1060        }
1061    }));
1062}
1063
1064/// The UI thread's batch arena.
1065///
1066/// Not a boundary entry point: it is crate-internal, and the generated Android shims use
1067/// it to tell the Renderer where the arena is so it can map it once instead of per call.
1068pub 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    /// `APP` is process-global, as launching is, so two tests that launch at the same
1089    /// time would each see the other's. Poisoning is ignored: a failing test has already
1090    /// reported itself, and the rest still need the lock.
1091    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        // SAFETY: The test passes a valid output pointer and intentionally null input.
1103        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        // SAFETY: Each call uses null or valid test-owned pointers as documented.
1112        unsafe {
1113            assert_eq!(
1114                compose_rust_host_init(std::ptr::null(), 1, &mut output),
1115                STATUS_PROTOCOL_ERROR
1116            );
1117            // No handshake has run on this thread, so this is a call-order error.
1118            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
1128/// The design system a demonstration should start in.
1129///
1130/// An application picks its own theme and never needs this. The samples do, because the
1131/// point of a sample is to show what one declaration looks like under each system, and on
1132/// any given machine the adaptive default can only ever show you one of them. Material 3
1133/// and Fluent went unseen for weeks for exactly that reason: everything was checked on a
1134/// Mac, so everything was Cupertino.
1135///
1136/// `DXC_DESIGN` names the system. Anything else, including nothing, adapts to the host.
1137/// `DXC_SCHEME` names `light` or `dark`; anything else leaves the reader's own setting
1138/// alone.
1139pub fn demo_theme() -> Theme {
1140    demo_theme_for(Theme::adaptive(crate::schema::DesignSystem::Material3))
1141}
1142
1143/// The same, for a demonstration that has a theme of its own to start from.
1144///
1145/// A sample whose design is one particular design system in one particular colour scheme
1146/// says so, and that declaration is the sample. But the machine it is being looked at on
1147/// can only draw one of the seven at a time, so without a way to point it somewhere else
1148/// the other six are never seen. This keeps the sample's own theme as the answer and lets
1149/// the two variables override the part they name, so what is on screen is either the
1150/// design the sample chose or exactly the one that was asked for.
1151pub 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        // Spelled both ways, because the name is two words everywhere it is written down
1161        // and nobody remembers which one a shell variable wants.
1162        Ok("liquidglass") | Ok("liquid-glass") => Theme::unified(DesignSystem::LiquidGlass),
1163        // Naming a system replaces the sample's design system but keeps its colour
1164        // scheme, so asking to see one design does not also change how light it is.
1165        _ => return with_scheme_override(theme),
1166    }
1167    .with_color_scheme(theme.color_scheme);
1168    with_scheme_override(theme)
1169}
1170
1171/// Applies `DXC_SCHEME` if it names one of the two schemes.
1172fn 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    /// Held for the length of any test that sets one of the variables.
1187    ///
1188    /// The variables belong to the process, not to the test, so two of these running at
1189    /// once read each other's settings and fail on a value neither of them asked for.
1190    static ENVIRONMENT: Mutex<()> = Mutex::new(());
1191
1192    /// Clears both variables and keeps everyone else out until the guard is dropped.
1193    fn exclusive_environment() -> MutexGuard<'static, ()> {
1194        let guard = ENVIRONMENT
1195            .lock()
1196            .unwrap_or_else(|error| error.into_inner());
1197        // SAFETY: the lock makes this the only thread touching the environment, and the
1198        // variables are read only by the functions called under the same lock.
1199        unsafe {
1200            std::env::remove_var("DXC_DESIGN");
1201            std::env::remove_var("DXC_SCHEME");
1202        }
1203        guard
1204    }
1205
1206    /// Named for what it defends: a sample that cannot be pointed at a design system
1207    /// leaves five of the six unseen on any one machine.
1208    #[test]
1209    fn fr14_a_named_design_system_is_unified_and_anything_else_adapts() {
1210        let _environment = exclusive_environment();
1211        // SAFETY: `exclusive_environment` holds the lock, so nothing else is reading or
1212        // writing these while this runs.
1213        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    /// Named for what it defends: a machine set to dark draws every sample dark, so a
1244    /// design that is meant to be read light cannot be looked at at all without this.
1245    #[test]
1246    fn fr14_a_named_colour_scheme_is_pinned_and_anything_else_is_left_alone() {
1247        let _environment = exclusive_environment();
1248        // SAFETY: as above.
1249        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    /// A sample that names its own theme keeps it, and each variable overrides only the
1266    /// half it names. Asking to see Liquid Glass must not also throw away the colour
1267    /// scheme the design was drawn for.
1268    #[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    /// A renderer built from a different schema is refused, and says so.
1292    ///
1293    /// The refusal already worked. What did not was finding out why: the renderer stands
1294    /// its window up before it asks the Host anything, so a mismatch leaves an empty page
1295    /// in the default theme and a status code, which reads as an application that draws
1296    /// nothing rather than as a build that is out of date. It cost a morning once.
1297    #[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/// Keeps the five exported boundary functions in the final executable.
1324///
1325/// The Renderer resolves them by name once it is loaded, which means nothing in the
1326/// application ever refers to them and the linker is free to conclude they are dead. It
1327/// does exactly that in any binary whose own code happens not to reach them, a test
1328/// harness for an application being the ordinary case, and the result is a process that
1329/// dies on startup with the dynamic loader unable to bind a symbol that should have been
1330/// right there in the executable.
1331///
1332/// Taking their addresses in a static the compiler is told to keep marks them as roots
1333/// for the dead-code pass, which is the whole job. It costs five pointers.
1334#[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/// The same five names, as a linker directive the object file carries.
1344///
1345/// The Renderer finds these with GetProcAddress against the running executable, which
1346/// reads the PE export table, and an executable has no export table unless the link is
1347/// told to make one. The build script asks for that with `/EXPORT:` arguments, and those
1348/// reach the binaries of this package and no further: Cargo does not pass a dependency's
1349/// link arguments on to whatever depends on it.
1350///
1351/// So every application built on this crate linked without an export table, the Renderer
1352/// could not reach back into it, and the window came up empty. It looked like it worked
1353/// because the only Windows binaries anyone had run were this package's own examples.
1354///
1355/// `.drectve` is how an object file carries linker arguments of its own. The MSVC linker
1356/// reads that section out of every object it links, so the directive travels inside the
1357/// rlib and applies wherever the rlib ends up.
1358#[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
1381/// Never read. Being referenced is the entire contract.
1382struct BoundaryExports(#[allow(dead_code)] [*const (); 5]);
1383
1384// SAFETY: The addresses are written once, at compile time, and never read. A static has
1385// to be Sync to exist at all, and raw pointers decline to be only because of what they
1386// might point at; these point at code.
1387unsafe impl Sync for BoundaryExports {}