Skip to main content

compose_rust/
boundary.rs

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