Skip to main content

shuyi_sound_out/
sys.rs

1//! Everything of this crate that isn't safe code (ADR-0021 ยง6): the hand-over that
2//! moves a boxed value between threads, which no lock or allocation may touch, and
3//! the web's functions, which `sound.js` provides.
4#![allow(unsafe_code)]
5
6use std::ptr;
7use std::sync::Arc;
8use std::sync::atomic::{AtomicPtr, AtomicU32, Ordering};
9
10/// The most a ring can hold and still tell full from empty by counters of 32 bits.
11const MOST_SLOTS: u32 = 0x8000_0000;
12
13/// A fixed ring of pointers, for one writer and one reader, which owns what it holds.
14struct Ring<T> {
15    slots: Box<[AtomicPtr<T>]>,
16    /// A power of two less one.
17    mask: u32,
18    /// How many were ever put in. Only the writer stores it.
19    head: AtomicU32,
20    /// How many were ever taken out. Only the reader stores it.
21    tail: AtomicU32,
22}
23
24impl<T> Ring<T> {
25    fn slot(&self, at: u32) -> &AtomicPtr<T> {
26        &self.slots[usize::try_from(at & self.mask).unwrap_or(0)]
27    }
28}
29
30impl<T> Drop for Ring<T> {
31    fn drop(&mut self) {
32        for slot in &mut *self.slots {
33            let held = *slot.get_mut();
34            if !held.is_null() {
35                // SAFETY: a non-null slot holds a pointer `HandTo::send` got from
36                // `Box::into_raw` and no reader took; with `&mut self` nobody else
37                // can reach it, and this frees it once, as the slot is dropped next.
38                drop(unsafe { Box::from_raw(held) });
39            }
40        }
41    }
42}
43
44/// Makes the two ends of a hand-over with room for at least `slots` values waiting.
45/// Nothing is allocated or freed by [`HandTo::send`] or [`HandOut::take`]: a value's
46/// box goes from one thread to the other, and whoever ends up with it frees it, so
47/// the end on an audio thread sends each box back rather than dropping it.
48pub fn handover<T: Send>(slots: usize) -> (HandTo<T>, HandOut<T>) {
49    let slots = u32::try_from(slots)
50        .unwrap_or(MOST_SLOTS)
51        .clamp(1, MOST_SLOTS)
52        .next_power_of_two();
53    let ring = Arc::new(Ring {
54        slots: (0..slots)
55            .map(|_| AtomicPtr::new(ptr::null_mut()))
56            .collect(),
57        mask: slots - 1,
58        head: AtomicU32::new(0),
59        tail: AtomicU32::new(0),
60    });
61    (
62        HandTo {
63            ring: Arc::clone(&ring),
64        },
65        HandOut { ring },
66    )
67}
68
69/// The end of a [`handover`] that puts values in.
70pub struct HandTo<T> {
71    ring: Arc<Ring<T>>,
72}
73
74impl<T> HandTo<T> {
75    /// Puts `value` in, or gives it back if there is no room.
76    pub fn send(&mut self, value: Box<T>) -> Result<(), Box<T>> {
77        let ring = &*self.ring;
78        let head = ring.head.load(Ordering::Relaxed);
79        let tail = ring.tail.load(Ordering::Acquire);
80        if head.wrapping_sub(tail) > ring.mask {
81            return Err(value);
82        }
83        ring.slot(head)
84            .store(Box::into_raw(value), Ordering::Release);
85        ring.head.store(head.wrapping_add(1), Ordering::Release);
86        Ok(())
87    }
88}
89
90/// The end of a [`handover`] that takes values out.
91pub struct HandOut<T> {
92    ring: Arc<Ring<T>>,
93}
94
95impl<T> HandOut<T> {
96    /// The next value, if one has come.
97    pub fn take(&mut self) -> Option<Box<T>> {
98        let ring = &*self.ring;
99        let tail = ring.tail.load(Ordering::Relaxed);
100        if ring.head.load(Ordering::Acquire) == tail {
101            return None;
102        }
103        let held = ring.slot(tail).swap(ptr::null_mut(), Ordering::AcqRel);
104        ring.tail.store(tail.wrapping_add(1), Ordering::Release);
105        if held.is_null() {
106            return None;
107        }
108        // SAFETY: `head` said this slot was written, with `Release`, by the one writer,
109        // from `Box::into_raw`; the swap took the pointer out, so this is its only
110        // owner, and the box's value was `Send` to come here.
111        Some(unsafe { Box::from_raw(held) })
112    }
113}
114
115// SAFETY: an end only ever moves whole `T`s, each owned by one side at a time, through
116// atomics; the `Arc`'s contents are atomics and pointers it owns. So with `T: Send`
117// an end can go to another thread. Neither end hands out a shared `&T`.
118unsafe impl<T: Send> Send for HandTo<T> {}
119// SAFETY: as for `HandTo`.
120unsafe impl<T: Send> Send for HandOut<T> {}
121
122/// The functions of `sound.js`, and what it calls here. Plain numbers, and pointers
123/// into this module's own buffers that it reads before returning.
124#[cfg(target_arch = "wasm32")]
125pub(crate) mod web {
126    /// The plugin's version, which the loader compares with the `version` in
127    /// `crates/cli/web/sound.js` (a `shuyi-cli` test keeps them equal). Bump both when
128    /// the functions below change.
129    const PLUGIN_VERSION: u32 = 1;
130
131    unsafe extern "C" {
132        /// Opens the output at about `rate`, asking for `frames` at a time, and says
133        /// the rate it runs at, or 0 if this page can't: `discard` never makes a sound.
134        pub(crate) safe fn shuyi_sound_open(rate: u32, frames: u32, discard: u32) -> u32;
135        /// Holds the output, or lets it go on if `on` is 0.
136        pub(crate) safe fn shuyi_sound_hold(on: u32);
137        /// Starts the browser's own decoder on `bytes` bytes at `pointer`, to learn
138        /// whether it can read them: the number to ask about.
139        safe fn shuyi_sound_vet(pointer: *const u8, bytes: u32) -> u32;
140        /// 0 while the browser decodes, 1 if it read the file, 2 if it refused it.
141        pub(crate) safe fn shuyi_sound_vetted(number: u32) -> u32;
142    }
143
144    /// Asks the browser about `bytes`. It copies them before this returns.
145    pub(crate) fn vet(bytes: &[u8]) -> u32 {
146        shuyi_sound_vet(bytes.as_ptr(), u32::try_from(bytes.len()).unwrap_or(0))
147    }
148
149    /// What the loader calls to check the plugin's version.
150    #[unsafe(no_mangle)]
151    pub extern "C" fn shuyi_sound_crate_version() -> u32 {
152        PLUGIN_VERSION
153    }
154
155    /// What `sound.js` calls for `frames` frames: where they are, until the next call.
156    #[unsafe(no_mangle)]
157    pub extern "C" fn shuyi_sound_fill(frames: u32) -> *const i16 {
158        crate::web::fill(frames)
159    }
160}
161
162#[cfg(test)]
163mod tests {
164    use std::sync::atomic::AtomicUsize;
165
166    use super::*;
167
168    /// Says when it is freed.
169    struct Tracked(Arc<AtomicUsize>);
170
171    impl Drop for Tracked {
172        fn drop(&mut self) {
173            self.0.fetch_add(1, Ordering::AcqRel);
174        }
175    }
176
177    #[test]
178    fn values_come_out_in_order_and_a_full_ring_gives_one_back() {
179        let (mut to, mut out) = handover::<u32>(3);
180        for value in 0..4 {
181            assert!(to.send(Box::new(value)).is_ok());
182        }
183        assert_eq!(to.send(Box::new(9)).map_err(|back| *back), Err(9));
184        for value in 0..4 {
185            assert_eq!(out.take().map(|got| *got), Some(value));
186        }
187        assert!(out.take().is_none());
188        assert!(to.send(Box::new(5)).is_ok());
189        assert_eq!(out.take().map(|got| *got), Some(5));
190    }
191
192    #[test]
193    fn counters_wrap_past_their_width() {
194        let (mut to, mut out) = handover::<u32>(4);
195        to.ring.head.store(u32::MAX - 1, Ordering::Relaxed);
196        to.ring.tail.store(u32::MAX - 1, Ordering::Relaxed);
197        for value in 0..32 {
198            assert!(to.send(Box::new(value)).is_ok());
199            assert!(to.send(Box::new(value + 100)).is_ok());
200            assert_eq!(out.take().map(|got| *got), Some(value));
201            assert_eq!(out.take().map(|got| *got), Some(value + 100));
202            assert!(out.take().is_none());
203        }
204    }
205
206    #[test]
207    fn what_was_never_taken_is_freed_with_the_ring_and_only_once() {
208        let freed = Arc::new(AtomicUsize::new(0));
209        let (mut to, mut out) = handover(4);
210        for _ in 0..3 {
211            assert!(to.send(Box::new(Tracked(Arc::clone(&freed)))).is_ok());
212        }
213        drop(out.take());
214        assert_eq!(freed.load(Ordering::Acquire), 1);
215        drop(to);
216        assert_eq!(
217            freed.load(Ordering::Acquire),
218            1,
219            "the other end still holds"
220        );
221        drop(out);
222        assert_eq!(freed.load(Ordering::Acquire), 3);
223    }
224
225    #[test]
226    fn nothing_is_lost_or_doubled_between_real_threads() {
227        const COUNT: u32 = 200_000;
228        let (mut to, mut out) = handover::<u32>(8);
229        let writer = std::thread::spawn(move || {
230            for value in 0..COUNT {
231                let mut held = Box::new(value);
232                loop {
233                    match to.send(held) {
234                        Ok(()) => break,
235                        Err(back) => {
236                            held = back;
237                            std::thread::yield_now();
238                        }
239                    }
240                }
241            }
242        });
243        let mut next = 0;
244        while next < COUNT {
245            match out.take() {
246                Some(value) => {
247                    assert_eq!(*value, next);
248                    next += 1;
249                }
250                None => std::thread::yield_now(),
251            }
252        }
253        assert!(writer.join().is_ok());
254        assert!(out.take().is_none());
255    }
256}