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