wasmtime/runtime/component/concurrent/
tls.rs

1//! A small self-contained module to manage passing a `&mut dyn VMStore` across
2//! function boundaries without it actually being a function parameter.
3//!
4//! Much of concurrent.rs and futures_and_streams.rs work with `Future` which
5//! does not allow customizing state being passed to each poll of a future. In
6//! Wasmtime, however, the mutable store is available during a calls to
7//! `Future::poll`, but not across calls of `Future::poll`. That means that
8//! effectively what we would ideally want is to thread `&mut dyn VMStore` as a
9//! parameter to futures, but that's not possible with Rust's future trait.
10//!
11//! This module is the workaround to otherwise enable this which is to use
12//! thread-local-storage instead to pass around this pointer. The goal of this
13//! module is to enable the `set` API to pretend like it's passing a pointer as
14//! a parameter to a closure and then `get` can be called to acquire this
15//! parameter. This module is intentionally small and isolated to keep the
16//! internal implementation details private and reduce the surface area that
17//! must be audited for the `unsafe` blocks contained within.
18
19use crate::runtime::vm::VMStore;
20use core::cell::Cell;
21use core::mem;
22use core::ptr::NonNull;
23
24std::thread_local! {
25    // Note that care is currently taken to minimize the size of this TLS
26    // variable as it's expected we'll refactor this in the future and have to
27    // plumb it to the platform abstraction layer of Wasmtime eventually where
28    // we want as minimal an impact as possible. Thus this TLS variable is
29    // a single pointer.
30    static STORAGE: Cell<Option<NonNull<SetStorage>>> = const { Cell::new(None) };
31}
32
33enum SetStorage {
34    Present(NonNull<dyn VMStore>),
35    Taken,
36}
37
38/// Configures `store` to be available for the duration of `f` through calls to
39/// the [`get`] function below.
40///
41/// This function will replace any prior state that was configured and overwrite
42/// it. Upon `f` returning the previous state will be restored. This function
43/// intentionally borrows `store` for the entire duration of `f` meaning that
44/// `f` is not allowed to access `store` via Rust's borrow checker.
45pub fn set<R>(store: &mut dyn VMStore, f: impl FnOnce() -> R) -> R {
46    let mut storage = SetStorage::Present(NonNull::from(store));
47    let _reset = ResetTls(STORAGE.with(|s| s.replace(Some(NonNull::from(&mut storage)))));
48    return f();
49
50    struct ResetTls(Option<NonNull<SetStorage>>);
51
52    impl Drop for ResetTls {
53        fn drop(&mut self) {
54            STORAGE.with(|s| s.set(self.0));
55        }
56    }
57}
58
59/// Acquires a reference to the previous store configured via [`set`] above,
60/// yielding this reference to the closure `f provided here.
61///
62/// This function will "take" the store from thread-local-storage for the
63/// duration of the `get` function here. This "take" operation means that
64/// recursive calls to `get` here will fail as the second one won't be able to
65/// re-acquire the same pointer the first one has (due to it having `&mut`
66/// exclusive access.
67///
68/// # Panics
69///
70/// This function will panic if [`set`] has not been previously called or if the
71/// current pointer is taken by a previous call to [`get`] on the stack.
72pub fn get<R>(f: impl FnOnce(&mut dyn VMStore) -> R) -> R {
73    try_get(|val| match val {
74        TryGet::Some(store) => f(store),
75        TryGet::None => get_failed(false),
76        TryGet::Taken => get_failed(true),
77    })
78}
79
80#[cold]
81fn get_failed(taken: bool) -> ! {
82    if taken {
83        panic!(
84            "attempted to recursively call `Accessor::with` when the pointer \
85            was already taken by a previous call to `Accessor::with`; try \
86            using `RUST_BACKTRACE=1` to find two stack frames to \
87            `Accessor::with` on the stack"
88        );
89    } else {
90        panic!(
91            "`Accessor::with` was called when the TLS pointer was not \
92             previously set; this is likely a bug in Wasmtime and we would \
93             appreciate an issue being filed to help fix this."
94        );
95    }
96}
97
98/// Values yielded to the [`try_get`] closure as an argument.
99pub enum TryGet<'a> {
100    /// The [`set`] API was not previously called, so there is no store
101    /// available at all.
102    None,
103    /// The [`set`] API was previously called but it was then subsequently taken
104    /// via a call to [`get`] meaning it's not available.
105    Taken,
106    /// The [`set`] API was previously called and this is the store that it was
107    /// called with.
108    Some(&'a mut dyn VMStore),
109}
110
111/// Same as [`get`] except that this does not panic if `set` has not been
112/// called.
113pub fn try_get<R>(f: impl FnOnce(TryGet<'_>) -> R) -> R {
114    // SAFETY: This is The Unsafe Block of this module on which everything
115    // hinges. The overall idea is that the pointer previously provided to
116    // `set` is passed to the closure here but only at most once because it's
117    // passed mutably. Thus there's a number of things that this takes care of:
118    //
119    // * The lifetime in `TryGet` that's handed out is anonymous via the
120    //   type signature of `f`, meaning that it cannot be safely persisted
121    //   outside that closure. That means that once `f` is returned this
122    //   function has exclusive access to the store again.
123    //
124    // * If `STORAGE` is not set then that means `set` has not been configured,
125    //   thus `TryGet::None` is yielded.
126    //
127    // * If `STORAGE` is set then we're guaranteed it's set for the entire
128    //   lifetime of this function call, and we're also guaranteed that the
129    //   pointer stored in there is the same pointer we'll be modifying for
130    //   this whole function call.
131    //
132    // * The `STORAGE` pointer is read/written only in a scoped manner here and
133    //   borrows of this value are not persisted for very long.
134    //
135    // With all of that put together it should make it such that this is a safe
136    // reborrow of the store provided to `set` to pass to the closure `f` here.
137    unsafe {
138        let storage = STORAGE.with(|s| s.get());
139        let _reset;
140        let val = match storage {
141            Some(mut storage) => match mem::replace(storage.as_mut(), SetStorage::Taken) {
142                SetStorage::Taken => TryGet::Taken,
143                SetStorage::Present(mut ptr) => {
144                    _reset = ResetStorage(storage, ptr);
145                    TryGet::Some(ptr.as_mut())
146                }
147            },
148            None => TryGet::None,
149        };
150        return f(val);
151    }
152
153    struct ResetStorage(NonNull<SetStorage>, NonNull<dyn VMStore>);
154
155    impl Drop for ResetStorage {
156        fn drop(&mut self) {
157            unsafe {
158                *self.0.as_mut() = SetStorage::Present(self.1);
159            }
160        }
161    }
162}
163
164#[cfg(test)]
165mod tests {
166    use super::{TryGet, get, set, try_get};
167    use crate::{AsContextMut, Engine, Store};
168
169    #[test]
170    fn test_simple() {
171        let engine = Engine::default();
172        let mut store = Store::new(&engine, ());
173
174        set(store.as_context_mut().0, || {
175            get(|_| {});
176            try_get(|t| {
177                assert!(matches!(t, TryGet::Some(_)));
178            });
179        });
180    }
181
182    #[test]
183    fn test_try_get() {
184        let engine = Engine::default();
185        let mut store = Store::new(&engine, ());
186
187        try_get(|t| {
188            assert!(matches!(t, TryGet::None));
189            try_get(|t| {
190                assert!(matches!(t, TryGet::None));
191            });
192        });
193        set(store.as_context_mut().0, || {
194            get(|_| {
195                try_get(|t| {
196                    assert!(matches!(t, TryGet::Taken));
197                    try_get(|t| {
198                        assert!(matches!(t, TryGet::Taken));
199                    });
200                });
201            });
202            try_get(|t| {
203                assert!(matches!(t, TryGet::Some(_)));
204                try_get(|t| {
205                    assert!(matches!(t, TryGet::Taken));
206                    try_get(|t| {
207                        assert!(matches!(t, TryGet::Taken));
208                    });
209                });
210            });
211            try_get(|t| {
212                assert!(matches!(t, TryGet::Some(_)));
213                try_get(|t| {
214                    assert!(matches!(t, TryGet::Taken));
215                });
216            });
217        });
218        try_get(|t| {
219            assert!(matches!(t, TryGet::None));
220        });
221    }
222
223    #[test]
224    #[should_panic(expected = "attempted to recursively call")]
225    fn test_get_panic() {
226        let engine = Engine::default();
227        let mut store = Store::new(&engine, ());
228
229        set(store.as_context_mut().0, || {
230            get(|_| {
231                get(|_| {
232                    panic!("should not get here");
233                });
234            });
235        });
236    }
237}