Skip to main content

flutter_plugin_sdk/
lib.rs

1//! Public, semantically versioned Rust API for Flutter Rust-shell plugins.
2//!
3//! The SDK deliberately exposes no Flutter C++ or Impeller types. Those remain
4//! behind the private, lockstep engine bridge.
5
6#![forbid(unsafe_code)]
7
8use std::{
9    sync::{
10        Arc,
11        atomic::{AtomicU8, Ordering},
12    },
13    thread::ThreadId,
14};
15
16/// The source compatibility version of this SDK.
17pub const PLUGIN_SDK_API_VERSION: u32 = 1;
18
19/// GPU API types pinned to the version used by the Rust shell.
20///
21/// Plugins must use this re-export for values passed through SDK callbacks.
22pub mod gpu {
23    pub use wgpu;
24}
25
26/// Errors returned while registering a Rust-shell plugin.
27#[derive(Debug, Clone, Copy, PartialEq, Eq)]
28pub enum PluginError {
29    /// The plugin requires an SDK capability not provided by this shell build.
30    Unsupported,
31    /// A texture descriptor has zero or unsupported dimensions or format.
32    InvalidDescriptor,
33    /// Every ring slot is currently reserved, ready, or used by Flutter.
34    Busy,
35    /// The reserved frame has not recorded any commands yet.
36    NoFrame,
37    /// The texture or shell is shutting down.
38    Shutdown,
39}
40
41/// A convenient result type for plugin registration.
42pub type Result<T> = core::result::Result<T, PluginError>;
43
44/// Pixel formats accepted by engine-owned GPU textures.
45#[derive(Debug, Clone, Copy, PartialEq, Eq)]
46pub enum TextureFormat {
47    /// Eight-bit linear red, green, blue, and alpha channels.
48    Rgba8Unorm,
49}
50
51/// Size and format of an engine-owned GPU texture.
52#[derive(Debug, Clone, Copy, PartialEq, Eq)]
53pub struct TextureDescriptor {
54    /// Width in physical pixels.
55    pub width: u32,
56    /// Height in physical pixels.
57    pub height: u32,
58    /// Storage and sampling format.
59    pub format: TextureFormat,
60}
61
62impl TextureDescriptor {
63    fn is_valid(self) -> bool {
64        self.width > 0 && self.height > 0
65    }
66}
67
68/// One producer callback that records wgpu commands for an engine-owned slot.
69#[doc(hidden)]
70pub type WgpuRenderTask =
71    Box<dyn FnOnce(&wgpu::Device, &mut wgpu::CommandEncoder, &wgpu::TextureView) + Send + 'static>;
72
73/// Private runtime implementation behind a public [`WgpuTexture`].
74#[doc(hidden)]
75#[async_trait::async_trait]
76pub trait WgpuTextureBackendHandle: Send + Sync {
77    /// Flutter texture-registry identifier.
78    fn texture_id(&self) -> i64;
79    /// Attempts to reserve one ring slot without blocking.
80    fn try_next_frame(&self) -> Result<Arc<dyn WgpuTextureFrameBackend>>;
81    /// Asynchronously waits until one ring slot can be reserved.
82    async fn next_frame(&self) -> Result<Arc<dyn WgpuTextureFrameBackend>>;
83}
84
85/// Private runtime implementation behind one reserved [`WgpuTextureFrame`].
86#[doc(hidden)]
87pub trait WgpuTextureFrameBackend: Send + Sync {
88    /// Records commands without submitting to the shared Vulkan queue.
89    fn render(&self, task: WgpuRenderTask) -> Result<()>;
90    /// Publishes the recorded slot and marks the Flutter texture dirty.
91    fn present(&self) -> Result<()>;
92}
93
94/// One producer callback that writes directly into shell-owned pixel memory.
95#[doc(hidden)]
96pub type PixelWriteTask = Box<dyn FnOnce(&mut [u8], usize) + Send + 'static>;
97
98/// Private runtime implementation behind a public [`PixelBufferTexture`].
99#[doc(hidden)]
100#[async_trait::async_trait]
101pub trait PixelBufferTextureBackendHandle: Send + Sync {
102    /// Flutter texture-registry identifier.
103    fn texture_id(&self) -> i64;
104    /// Attempts to reserve one shell-owned pixel buffer without blocking.
105    fn try_next_frame(&self) -> Result<Arc<dyn PixelBufferTextureFrameBackend>>;
106    /// Asynchronously waits until one shell-owned pixel buffer is available.
107    async fn next_frame(&self) -> Result<Arc<dyn PixelBufferTextureFrameBackend>>;
108}
109
110/// Private runtime implementation behind one reserved pixel-buffer frame.
111#[doc(hidden)]
112pub trait PixelBufferTextureFrameBackend: Send + Sync {
113    /// Lets the producer write directly into shell-owned memory.
114    fn write_pixels(&self, task: PixelWriteTask) -> Result<()>;
115    /// Publishes the written slot and marks the Flutter texture dirty.
116    fn present(&self) -> Result<()>;
117}
118
119/// Private runtime factory installed into [`PluginRegistrar`].
120#[doc(hidden)]
121pub trait WgpuTextureBackend: Send + Sync {
122    /// Creates one engine-owned texture.
123    fn create_texture(
124        &self,
125        descriptor: TextureDescriptor,
126    ) -> Result<Arc<dyn WgpuTextureBackendHandle>>;
127    /// Creates one CPU-produced texture backed by shell-owned upload buffers.
128    fn create_pixel_buffer_texture(
129        &self,
130        descriptor: TextureDescriptor,
131    ) -> Result<Arc<dyn PixelBufferTextureBackendHandle>>;
132}
133
134/// GPU texture creation capability exposed by the plugin registrar.
135#[derive(Clone)]
136pub struct GpuTextures {
137    backend: Arc<dyn WgpuTextureBackend>,
138}
139
140impl GpuTextures {
141    /// Creates a texture whose storage and synchronization are owned by the
142    /// shell. The returned ID can be passed directly to Dart's `Texture` widget.
143    pub fn create_texture(&self, descriptor: TextureDescriptor) -> Result<WgpuTexture> {
144        if !descriptor.is_valid() {
145            return Err(PluginError::InvalidDescriptor);
146        }
147        Ok(WgpuTexture {
148            backend: self.backend.create_texture(descriptor)?,
149        })
150    }
151
152    /// Creates a CPU-produced texture. Producers write directly into reusable
153    /// shell-owned buffers, avoiding a plugin-to-shell pixel copy.
154    pub fn create_pixel_buffer_texture(
155        &self,
156        descriptor: TextureDescriptor,
157    ) -> Result<PixelBufferTexture> {
158        if !descriptor.is_valid() {
159            return Err(PluginError::InvalidDescriptor);
160        }
161        Ok(PixelBufferTexture {
162            backend: self.backend.create_pixel_buffer_texture(descriptor)?,
163        })
164    }
165
166    /// Constructs the capability from the private shell runtime.
167    #[doc(hidden)]
168    pub fn for_shell(backend: Arc<dyn WgpuTextureBackend>) -> Self {
169        Self { backend }
170    }
171}
172
173/// Safe CPU producer handle backed by shell-owned pixel buffers.
174pub struct PixelBufferTexture {
175    backend: Arc<dyn PixelBufferTextureBackendHandle>,
176}
177
178impl PixelBufferTexture {
179    /// Identifier consumed by Flutter's Dart `Texture` widget.
180    pub fn texture_id(&self) -> i64 {
181        self.backend.texture_id()
182    }
183
184    /// Attempts to reserve a writable pixel buffer without blocking.
185    pub fn try_next_frame(&self) -> Result<PixelBufferTextureFrame> {
186        Ok(PixelBufferTextureFrame::new(self.backend.try_next_frame()?))
187    }
188
189    /// Waits asynchronously for a writable pixel buffer.
190    pub async fn next_frame(&self) -> Result<PixelBufferTextureFrame> {
191        self.backend
192            .next_frame()
193            .await
194            .map(PixelBufferTextureFrame::new)
195    }
196}
197
198/// Exclusive reservation of one shell-owned CPU pixel buffer.
199pub struct PixelBufferTextureFrame {
200    backend: Arc<dyn PixelBufferTextureFrameBackend>,
201    written: bool,
202}
203
204impl PixelBufferTextureFrame {
205    fn new(backend: Arc<dyn PixelBufferTextureFrameBackend>) -> Self {
206        Self {
207            backend,
208            written: false,
209        }
210    }
211
212    /// Invokes `writer` with tightly packed RGBA8 storage and its row stride.
213    /// The slice belongs to the shell and is reused after Flutter releases the
214    /// frame; plugin code must not retain references into it.
215    pub fn write_pixels(
216        &mut self,
217        writer: impl FnOnce(&mut [u8], usize) + Send + 'static,
218    ) -> Result<()> {
219        if self.written {
220            return Err(PluginError::Busy);
221        }
222        self.backend.write_pixels(Box::new(writer))?;
223        self.written = true;
224        Ok(())
225    }
226
227    /// Publishes this buffer and schedules Flutter to repaint its texture.
228    pub fn present(self) -> Result<()> {
229        if !self.written {
230            return Err(PluginError::NoFrame);
231        }
232        self.backend.present()
233    }
234}
235
236/// Safe producer handle for an engine-owned wgpu texture ring.
237pub struct WgpuTexture {
238    backend: Arc<dyn WgpuTextureBackendHandle>,
239}
240
241impl WgpuTexture {
242    /// Identifier consumed by Flutter's Dart `Texture` widget.
243    pub fn texture_id(&self) -> i64 {
244        self.backend.texture_id()
245    }
246
247    /// Attempts to reserve an available ring slot without blocking.
248    pub fn try_next_frame(&self) -> Result<WgpuTextureFrame> {
249        Ok(WgpuTextureFrame::new(self.backend.try_next_frame()?))
250    }
251
252    /// Asynchronously waits until a ring slot can be reserved.
253    /// Dropping the future cancels the wait without reserving a slot.
254    pub async fn next_frame(&self) -> Result<WgpuTextureFrame> {
255        self.backend.next_frame().await.map(WgpuTextureFrame::new)
256    }
257}
258
259/// Exclusive reservation of one engine-owned texture-ring slot.
260pub struct WgpuTextureFrame {
261    backend: Arc<dyn WgpuTextureFrameBackend>,
262    rendered: bool,
263}
264
265impl WgpuTextureFrame {
266    fn new(backend: Arc<dyn WgpuTextureFrameBackend>) -> Self {
267        Self {
268            backend,
269            rendered: false,
270        }
271    }
272
273    /// Records commands into this frame. A frame may be recorded once.
274    pub fn render(
275        &mut self,
276        task: impl FnOnce(&wgpu::Device, &mut wgpu::CommandEncoder, &wgpu::TextureView) + Send + 'static,
277    ) -> Result<()> {
278        if self.rendered {
279            return Err(PluginError::Busy);
280        }
281        self.backend.render(Box::new(task))?;
282        self.rendered = true;
283        Ok(())
284    }
285
286    /// Publishes this slot and schedules Flutter to repaint its existing
287    /// texture layer. Consuming `self` prevents repeated publication.
288    pub fn present(self) -> Result<()> {
289        if !self.rendered {
290            return Err(PluginError::NoFrame);
291        }
292        self.backend.present()
293    }
294}
295
296/// A unit of work that is safe to transfer to the shell's main thread.
297#[doc(hidden)]
298pub type MainThreadTask = Box<dyn FnOnce() + Send + 'static>;
299
300/// Error returned when work can no longer be posted to the main thread.
301#[derive(Debug, Clone, Copy, PartialEq, Eq)]
302pub enum DispatchError {
303    /// The shell has not completed initialization yet.
304    NotReady,
305    /// The application is shutting down and no longer accepts callbacks.
306    Shutdown,
307}
308
309const DISPATCHER_STARTING: u8 = 0;
310const DISPATCHER_RUNNING: u8 = 1;
311const DISPATCHER_SHUTDOWN: u8 = 2;
312
313/// Worker-safe handle for posting short operations to Flutter's main thread.
314///
315/// Dispatch is always asynchronous, including when called from the main
316/// thread. This prevents a callback from unexpectedly re-entering Dart or
317/// mutable platform state in the middle of an FFI call.
318#[derive(Clone)]
319pub struct MainThreadDispatcher {
320    post: Arc<dyn Fn(MainThreadTask) -> bool + Send + Sync>,
321    main_thread: ThreadId,
322    state: Arc<AtomicU8>,
323}
324
325impl MainThreadDispatcher {
326    /// Posts `task` for a later turn of the main event loop.
327    pub fn dispatch(
328        &self,
329        task: impl FnOnce() + Send + 'static,
330    ) -> core::result::Result<(), DispatchError> {
331        match self.state.load(Ordering::Acquire) {
332            DISPATCHER_STARTING => return Err(DispatchError::NotReady),
333            DISPATCHER_SHUTDOWN => return Err(DispatchError::Shutdown),
334            DISPATCHER_RUNNING => {}
335            _ => unreachable!("invalid main-thread dispatcher state"),
336        }
337        let state = Arc::clone(&self.state);
338        let guarded_task = Box::new(move || {
339            if state.load(Ordering::Acquire) == DISPATCHER_RUNNING {
340                task();
341            }
342        });
343        if !(self.post)(guarded_task) {
344            return Err(DispatchError::Shutdown);
345        }
346        Ok(())
347    }
348
349    /// Whether the caller is currently running on the owning main thread.
350    pub fn is_main_thread(&self) -> bool {
351        std::thread::current().id() == self.main_thread
352    }
353
354    /// Creates a dispatcher backed by the private shell runtime.
355    #[doc(hidden)]
356    pub fn for_shell(
357        post: impl Fn(MainThreadTask) -> bool + Send + Sync + 'static,
358        main_thread: ThreadId,
359    ) -> Self {
360        Self::for_shell_with_state(post, main_thread, DISPATCHER_RUNNING)
361    }
362
363    /// Creates a dispatcher that rejects work until shell startup completes.
364    #[doc(hidden)]
365    pub fn for_shell_inactive(
366        post: impl Fn(MainThreadTask) -> bool + Send + Sync + 'static,
367        main_thread: ThreadId,
368    ) -> Self {
369        Self::for_shell_with_state(post, main_thread, DISPATCHER_STARTING)
370    }
371
372    fn for_shell_with_state(
373        post: impl Fn(MainThreadTask) -> bool + Send + Sync + 'static,
374        main_thread: ThreadId,
375        state: u8,
376    ) -> Self {
377        Self {
378            post: Arc::new(post),
379            main_thread,
380            state: Arc::new(AtomicU8::new(state)),
381        }
382    }
383
384    /// Enables dispatch after the shell and its implicit view are initialized.
385    #[doc(hidden)]
386    pub fn start_for_shell(&self) -> bool {
387        self.state
388            .compare_exchange(
389                DISPATCHER_STARTING,
390                DISPATCHER_RUNNING,
391                Ordering::AcqRel,
392                Ordering::Acquire,
393            )
394            .is_ok()
395    }
396
397    /// Stops this dispatcher and every clone from accepting new work.
398    #[doc(hidden)]
399    pub fn shutdown_for_shell(&self) {
400        self.state.store(DISPATCHER_SHUTDOWN, Ordering::Release);
401    }
402}
403
404/// The shell-owned registration context passed to every plugin.
405///
406/// Capabilities are added here as the shell implements them. Keeping this type
407/// opaque prevents plugins from depending on private engine handles.
408pub struct PluginRegistrar {
409    main_thread_dispatcher: MainThreadDispatcher,
410    gpu_textures: Option<GpuTextures>,
411}
412
413impl PluginRegistrar {
414    /// Creates the registrar used by the shell during application startup.
415    ///
416    /// This is public only so the private shell runtime can construct the
417    /// registrar across crate boundaries; plugin code should only receive it
418    /// from [`FlutterRustPlugin::register`].
419    #[doc(hidden)]
420    pub fn for_shell(main_thread_dispatcher: MainThreadDispatcher) -> Self {
421        Self {
422            main_thread_dispatcher,
423            gpu_textures: None,
424        }
425    }
426
427    /// Returns the dispatcher for main-thread-only platform operations.
428    pub fn main_thread_dispatcher(&self) -> &MainThreadDispatcher {
429        &self.main_thread_dispatcher
430    }
431
432    /// Returns the engine-owned wgpu texture capability.
433    pub fn gpu(&self) -> Result<&GpuTextures> {
434        self.gpu_textures.as_ref().ok_or(PluginError::Unsupported)
435    }
436
437    /// Installs the shell's GPU backend after its engine and implicit view are
438    /// ready. Plugin code must not call this method.
439    #[doc(hidden)]
440    pub fn install_gpu_for_shell(&mut self, gpu_textures: GpuTextures) -> bool {
441        if self.gpu_textures.is_some() {
442            return false;
443        }
444        self.gpu_textures = Some(gpu_textures);
445        true
446    }
447}
448
449/// A source-linked plugin compiled into the application's Rust aggregate.
450pub trait FlutterRustPlugin: Send + Sync + 'static {
451    /// Registers the plugin's platform services, FRB APIs, and textures.
452    fn register(&self, registrar: &mut PluginRegistrar) -> Result<()>;
453}
454
455#[cfg(test)]
456mod tests {
457    use super::*;
458    use parking_lot::Mutex;
459    use std::collections::VecDeque;
460
461    struct FakeGpuBackend {
462        operations: Arc<Mutex<Vec<&'static str>>>,
463    }
464
465    struct FakeTextureBackend {
466        operations: Arc<Mutex<Vec<&'static str>>>,
467    }
468
469    struct FakeFrameBackend {
470        operations: Arc<Mutex<Vec<&'static str>>>,
471    }
472
473    struct FakePixelTextureBackend {
474        operations: Arc<Mutex<Vec<&'static str>>>,
475    }
476
477    struct FakePixelFrameBackend {
478        operations: Arc<Mutex<Vec<&'static str>>>,
479        pixels: Mutex<Vec<u8>>,
480    }
481
482    impl WgpuTextureBackend for FakeGpuBackend {
483        fn create_texture(
484            &self,
485            _descriptor: TextureDescriptor,
486        ) -> Result<Arc<dyn WgpuTextureBackendHandle>> {
487            self.operations.lock().push("create");
488            Ok(Arc::new(FakeTextureBackend {
489                operations: Arc::clone(&self.operations),
490            }))
491        }
492
493        fn create_pixel_buffer_texture(
494            &self,
495            _descriptor: TextureDescriptor,
496        ) -> Result<Arc<dyn PixelBufferTextureBackendHandle>> {
497            self.operations.lock().push("create_pixels");
498            Ok(Arc::new(FakePixelTextureBackend {
499                operations: Arc::clone(&self.operations),
500            }))
501        }
502    }
503
504    #[async_trait::async_trait]
505    impl PixelBufferTextureBackendHandle for FakePixelTextureBackend {
506        fn texture_id(&self) -> i64 {
507            23
508        }
509
510        fn try_next_frame(&self) -> Result<Arc<dyn PixelBufferTextureFrameBackend>> {
511            self.operations.lock().push("reserve_pixels");
512            Ok(Arc::new(FakePixelFrameBackend {
513                operations: Arc::clone(&self.operations),
514                pixels: Mutex::new(vec![0; 32]),
515            }))
516        }
517
518        async fn next_frame(&self) -> Result<Arc<dyn PixelBufferTextureFrameBackend>> {
519            self.try_next_frame()
520        }
521    }
522
523    impl PixelBufferTextureFrameBackend for FakePixelFrameBackend {
524        fn write_pixels(&self, task: PixelWriteTask) -> Result<()> {
525            self.operations.lock().push("write_pixels");
526            task(&mut self.pixels.lock(), 16);
527            Ok(())
528        }
529
530        fn present(&self) -> Result<()> {
531            self.operations.lock().push("present_pixels");
532            Ok(())
533        }
534    }
535
536    #[async_trait::async_trait]
537    impl WgpuTextureBackendHandle for FakeTextureBackend {
538        fn texture_id(&self) -> i64 {
539            17
540        }
541
542        fn try_next_frame(&self) -> Result<Arc<dyn WgpuTextureFrameBackend>> {
543            self.operations.lock().push("reserve");
544            Ok(Arc::new(FakeFrameBackend {
545                operations: Arc::clone(&self.operations),
546            }))
547        }
548
549        async fn next_frame(&self) -> Result<Arc<dyn WgpuTextureFrameBackend>> {
550            self.try_next_frame()
551        }
552    }
553
554    impl WgpuTextureFrameBackend for FakeFrameBackend {
555        fn render(&self, _task: WgpuRenderTask) -> Result<()> {
556            self.operations.lock().push("render");
557            Ok(())
558        }
559
560        fn present(&self) -> Result<()> {
561            self.operations.lock().push("present");
562            Ok(())
563        }
564    }
565
566    struct TestPlugin;
567
568    impl FlutterRustPlugin for TestPlugin {
569        fn register(&self, _registrar: &mut PluginRegistrar) -> Result<()> {
570            Ok(())
571        }
572    }
573
574    #[test]
575    fn plugins_register_with_the_shell_registrar() {
576        let queue = Arc::new(Mutex::new(VecDeque::<MainThreadTask>::new()));
577        let queue_for_post = Arc::clone(&queue);
578        let dispatcher = MainThreadDispatcher::for_shell(
579            move |task| {
580                queue_for_post.lock().push_back(task);
581                true
582            },
583            std::thread::current().id(),
584        );
585        let mut registrar = PluginRegistrar::for_shell(dispatcher);
586        TestPlugin.register(&mut registrar).unwrap();
587    }
588
589    #[test]
590    fn gpu_capability_validates_and_hides_the_runtime_backend() {
591        let dispatcher = MainThreadDispatcher::for_shell(|_| true, std::thread::current().id());
592        let mut registrar = PluginRegistrar::for_shell(dispatcher);
593        assert!(matches!(registrar.gpu(), Err(PluginError::Unsupported)));
594
595        let operations = Arc::new(Mutex::new(Vec::new()));
596        let capability = GpuTextures::for_shell(Arc::new(FakeGpuBackend {
597            operations: Arc::clone(&operations),
598        }));
599        assert!(registrar.install_gpu_for_shell(capability.clone()));
600        assert!(!registrar.install_gpu_for_shell(capability));
601        assert!(matches!(
602            registrar.gpu().unwrap().create_texture(TextureDescriptor {
603                width: 0,
604                height: 32,
605                format: TextureFormat::Rgba8Unorm,
606            }),
607            Err(PluginError::InvalidDescriptor)
608        ));
609
610        let texture = registrar
611            .gpu()
612            .unwrap()
613            .create_texture(TextureDescriptor {
614                width: 64,
615                height: 32,
616                format: TextureFormat::Rgba8Unorm,
617            })
618            .unwrap();
619        assert_eq!(texture.texture_id(), 17);
620        assert!(matches!(
621            texture.try_next_frame().unwrap().present(),
622            Err(PluginError::NoFrame)
623        ));
624        let mut frame = texture.try_next_frame().unwrap();
625        frame.render(|_, _, _| {}).unwrap();
626        assert_eq!(frame.render(|_, _, _| {}), Err(PluginError::Busy));
627        frame.present().unwrap();
628
629        assert_eq!(
630            *operations.lock(),
631            vec!["create", "reserve", "reserve", "render", "present"]
632        );
633    }
634
635    #[test]
636    fn pixel_buffer_frames_write_directly_into_shell_storage() {
637        let operations = Arc::new(Mutex::new(Vec::new()));
638        let gpu = GpuTextures::for_shell(Arc::new(FakeGpuBackend {
639            operations: Arc::clone(&operations),
640        }));
641        let texture = gpu
642            .create_pixel_buffer_texture(TextureDescriptor {
643                width: 4,
644                height: 2,
645                format: TextureFormat::Rgba8Unorm,
646            })
647            .unwrap();
648        assert_eq!(texture.texture_id(), 23);
649        assert_eq!(
650            texture.try_next_frame().unwrap().present(),
651            Err(PluginError::NoFrame)
652        );
653        let mut frame = texture.try_next_frame().unwrap();
654        frame
655            .write_pixels(|pixels, row_bytes| {
656                assert_eq!(row_bytes, 16);
657                assert_eq!(pixels.len(), 32);
658                pixels.fill(0x7f);
659            })
660            .unwrap();
661        assert_eq!(frame.write_pixels(|_, _| {}), Err(PluginError::Busy));
662        frame.present().unwrap();
663        assert_eq!(
664            *operations.lock(),
665            vec![
666                "create_pixels",
667                "reserve_pixels",
668                "reserve_pixels",
669                "write_pixels",
670                "present_pixels"
671            ]
672        );
673    }
674
675    #[test]
676    fn worker_dispatch_is_deferred_non_reentrant_and_rejects_shutdown() {
677        let queue = Arc::new(Mutex::new(VecDeque::<MainThreadTask>::new()));
678        let queue_for_post = Arc::clone(&queue);
679        let dispatcher = MainThreadDispatcher::for_shell(
680            move |task| {
681                queue_for_post.lock().push_back(task);
682                true
683            },
684            std::thread::current().id(),
685        );
686        let order = Arc::new(Mutex::new(Vec::new()));
687        let worker_dispatcher = dispatcher.clone();
688        let nested_dispatcher = dispatcher.clone();
689        let order_in_task = Arc::clone(&order);
690        std::thread::spawn(move || {
691            assert!(!worker_dispatcher.is_main_thread());
692            worker_dispatcher
693                .dispatch(move || {
694                    assert!(nested_dispatcher.is_main_thread());
695                    order_in_task.lock().push(1);
696                    let nested_order = Arc::clone(&order_in_task);
697                    nested_dispatcher
698                        .dispatch(move || nested_order.lock().push(2))
699                        .unwrap();
700                })
701                .unwrap();
702        })
703        .join()
704        .unwrap();
705
706        assert!(order.lock().is_empty());
707        let first = queue.lock().pop_front().unwrap();
708        first();
709        assert_eq!(*order.lock(), vec![1]);
710        let second = queue.lock().pop_front().unwrap();
711        second();
712        assert_eq!(*order.lock(), vec![1, 2]);
713        assert!(dispatcher.is_main_thread());
714
715        dispatcher.shutdown_for_shell();
716        assert_eq!(dispatcher.dispatch(|| {}), Err(DispatchError::Shutdown));
717    }
718
719    #[test]
720    fn startup_and_shutdown_gate_queued_callbacks_deterministically() {
721        for _ in 0..100 {
722            let queue = Arc::new(Mutex::new(VecDeque::<MainThreadTask>::new()));
723            let queue_for_post = Arc::clone(&queue);
724            let dispatcher = MainThreadDispatcher::for_shell_inactive(
725                move |task| {
726                    queue_for_post.lock().push_back(task);
727                    true
728                },
729                std::thread::current().id(),
730            );
731            assert_eq!(dispatcher.dispatch(|| {}), Err(DispatchError::NotReady));
732            assert!(queue.lock().is_empty());
733            assert!(dispatcher.start_for_shell());
734            assert!(!dispatcher.start_for_shell());
735
736            let ran = Arc::new(AtomicU8::new(0));
737            let ran_in_task = Arc::clone(&ran);
738            dispatcher
739                .dispatch(move || ran_in_task.store(1, Ordering::Release))
740                .unwrap();
741            dispatcher.shutdown_for_shell();
742            queue.lock().pop_front().unwrap()();
743            assert_eq!(ran.load(Ordering::Acquire), 0);
744            assert_eq!(dispatcher.dispatch(|| {}), Err(DispatchError::Shutdown));
745        }
746    }
747}