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