Skip to main content

subsecond/
lib.rs

1#![allow(clippy::needless_doctest_main)]
2//! # Subsecond: Hot-patching for Rust
3//!
4//! Subsecond is a library that enables hot-patching for Rust applications. This allows you to change
5//! the code of a running application without restarting it. This is useful for game engines, servers,
6//! and other long-running applications where the typical edit-compile-run cycle is too slow.
7//!
8//! Subsecond also implements a technique we call "ThinLinking" which makes compiling Rust code
9//! significantly faster in development mode, which can be used outside of hot-patching.
10//!
11//! # Usage
12//!
13//! Subsecond is designed to be as simple for both application developers and library authors.
14//!
15//! Simply call your existing functions with [`call`] and Subsecond will automatically detour
16//! that call to the latest version of the function.
17//!
18//! ```rust
19//! for x in 0..5 {
20//!     subsecond::call(|| {
21//!         println!("Hello, world! {}", x);
22//!     });
23//! }
24//! ```
25//!
26//! To actually load patches into your application, a third-party tool that implements the Subsecond
27//! compiler and protocol is required. Subsecond is built and maintained by the Dioxus team, so we
28//! suggest using the dioxus CLI tool to use subsecond.
29//!
30//! To install the Dioxus CLI, we recommend using [`cargo binstall`](https://crates.io/crates/cargo-binstall):
31//!
32//! ```sh
33//! cargo binstall dioxus-cli
34//! ```
35//!
36//! The Dioxus CLI provides several tools for development. To run your application with Subsecond enabled,
37//! use `dx serve` - this takes the same arguments as `cargo run` but will automatically hot-reload your
38//! application when changes are detected.
39//!
40//! As of Dioxus 0.7, "--hotpatch" is required to use hotpatching while Subsecond is still experimental.
41//!
42//! ```sh
43//! dx serve --hotpatch
44//! ```
45//!
46//! ## How it works
47//!
48//! Subsecond works by detouring function calls through a jump table. This jump table contains the latest
49//! version of the program's function pointers, and when a function is called, Subsecond will look up
50//! the function in the jump table and call that instead.
51//!
52//! Unlike libraries like [detour](https://crates.io/crates/detour), Subsecond *does not* modify your
53//! process memory. Patching pointers is wildly unsafe and can lead to crashes and undefined behavior.
54//!
55//! Instead, an external tool compiles only the parts of your project that changed, links them together
56//! using the addresses of the functions in your running program, and then sends the new jump table to
57//! your application. Subsecond then applies the patch and continues running. Since Subsecond doesn't
58//! modify memory, the program must have a runtime integration to handle the patching.
59//!
60//! If the framework you're using doesn't integrate with subsecond, you can rely on the fact that calls
61//! to stale [`call`] instances will emit a safe panic that is automatically caught and retried
62//! by the next [`call`] instance up the callstack.
63//!
64//! Subsecond is only enabled when debug_assertions are enabled so you can safely ship your application
65//! with Subsecond enabled without worrying about the performance overhead.
66//!
67//! ## Workspace support
68//!
69//! Subsecond currently only patches the "tip" crate - ie the crate in which your `main.rs` is located.
70//! Changes to crates outside this crate will be ignored, which can be confusing. We plan to add full
71//! workspace support in the future, but for now be aware of this limitation. Crate setups that have
72//! a `main.rs` importing a `lib.rs` won't patch sensibly since the crate becomes a library for itself.
73//!
74//! This is due to limitations in rustc itself where the build-graph is non-deterministic and changes
75//! to functions that forward generics can cause a cascade of codegen changes.
76//!
77//! ## Globals, statics, and thread-locals
78//!
79//! Subsecond *does* support hot-reloading of globals, statics, and thread locals. However, there are several limitations:
80//!
81//! - You may add new globals at runtime, but their destructors will never be called.
82//! - Globals are tracked across patches, but renames are considered to be *new* globals.
83//! - Changes to static initializers will not be observed.
84//!
85//! Subsecond purposefully handles statics this way since many libraries like Dioxus and Tokio rely
86//! on persistent global runtimes.
87//!
88//! HUGE WARNING: Currently, thread-locals in the "tip" crate (the one being patched) will seemingly
89//! reset to their initial value on new patches. This is because we don't currently bind thread-locals
90//! in the patches to their original addresses in the main program. If you rely on thread-locals heavily
91//! in your tip crate, you should be aware of this. Sufficiently complex setups might crash or even
92//! segfault. We plan to fix this in the future, but for now, you should be aware of this limitation.
93//!
94//! ## Struct layout and alignment
95//!
96//! Subsecond currently does not support hot-reloading of structs. This is because the generated code
97//! assumes a particular layout and alignment of the struct. If layout or alignment change and new
98//! functions are called referencing an old version of the struct, the program will crash.
99//!
100//! To mitigate this, framework authors can integrate with Subsecond to either dispose of the old struct
101//! or to re-allocate the struct in a way that is compatible with the new layout. This is called "re-instancing."
102//!
103//! In practice, frameworks that implement subsecond patching properly will throw out the old state
104//! and thus you should never witness a segfault due to misalignment or size changes. Frameworks are
105//! encouraged to aggressively dispose of old state that might cause size and alignment changes.
106//!
107//! We'd like to lift this limitation in the future by providing utilities to re-instantiate structs,
108//! but for now it's up to the framework authors to handle this. For example, Dioxus apps simply throw
109//! out the old state and rebuild it from scratch.
110//!
111//! ## Pointer versioning
112//!
113//! Currently, Subsecond does not "version" function pointers. We have plans to provide this metadata
114//! so framework authors can safely memoize changes without much runtime overhead. Frameworks like
115//! Dioxus and Bevy circumvent this issue by using the TypeID of structs passed to hot functions as
116//! well as the `ptr_address` method on [`HotFn`] to determine if the function pointer has changed.
117//!
118//! Currently, the `ptr_address` method will always return the most up-to-date version of the function
119//! even if the function contents itself did not change. In essence, this is equivalent to a version
120//! of the function where every function is considered "new." This means that framework authors who
121//! integrate re-instancing in their apps might dispose of old state too aggressively. For now, this
122//! is the safer and more practical approach.
123//!
124//! ## Nesting Calls
125//!
126//! Subsecond calls are designed to be nested. This provides clean integration points to know exactly
127//! where a hooked function is called.
128//!
129//! The highest level call is `fn main()` though by default this is not hooked since initialization code
130//! tends to be side-effectual and modify global state. Instead, we recommend wrapping the hot-patch
131//! points manually with [`call`].
132//!
133//! ```rust
134//! fn main() {
135//!     // Changes to the `for` loop will cause an unwind to this call.
136//!     subsecond::call(|| {
137//!         for x in 0..5 {
138//!             // Changes to the `println!` will be isolated to this call.
139//!             subsecond::call(|| {
140//!                 println!("Hello, world! {}", x);
141//!             });
142//!         }
143//!    });
144//! }
145//! ```
146//!
147//! The goal here is to provide granular control over where patches are applied to limit loss of state
148//! when new code is loaded.
149//!
150//! ## Applying patches
151//!
152//! When running under the Dioxus CLI, the `dx serve` command will automatically apply patches when
153//! changes are detected. Patches are delivered over the [Dioxus Devtools](https://crates.io/crates/dioxus-devtools)
154//! websocket protocol and received by corresponding websocket.
155//!
156//! If you're using Subsecond in your own application that doesn't have a runtime integration, you can
157//! build an integration using the [`apply_patch`] function. This function takes a `JumpTable` which
158//! the dioxus-cli crate can generate.
159//!
160//! To add support for the Dioxus Devtools protocol to your app, you can use the [dioxus-devtools](https://crates.io/crates/dioxus-devtools)
161//! crate which provides a `connect` method that will automatically apply patches to your application.
162//!
163//! Unfortunately, one design quirk of Subsecond is that running apps need to communicate the address
164//! of `main` to the patcher. This is due to a security technique called [ASLR](https://en.wikipedia.org/wiki/Address_space_layout_randomization)
165//! which randomizes the address of functions in memory. See the subsecond-harness and subsecond-cli
166//! for more details on how to implement the protocol.
167//!
168//! ## ThinLink
169//!
170//! ThinLink is a program linker for Rust that is designed to be used with Subsecond. It implements
171//! the powerful patching system that Subsecond uses to hot-reload Rust applications.
172//!
173//! ThinLink is simply a wrapper around your existing linker but with extra features:
174//!
175//! - Automatic dynamic linking to dependencies
176//! - Generation of Subsecond jump tables
177//! - Diffing of object files for function invalidation
178//!
179//! Because ThinLink performs very to little actual linking, it drastically speeds up traditional Rust
180//! development. With a development-optimized profile, ThinLink can shrink an incremental build to less than 500ms.
181//!
182//! ThinLink is automatically integrated into the Dioxus CLI though it's currently not available as
183//! a standalone tool.
184//!
185//! ## Limitations
186//!
187//! Subsecond is a powerful tool but it has several limitations. We talk about them above, but here's
188//! a quick summary:
189//!
190//! - Struct hot reloading requires instancing or unwinding
191//! - Statics are tracked but not destructed
192//!
193//! ## Platform support
194//!
195//! Subsecond works across all major platforms:
196//!
197//! - Android (arm64-v8a, armeabi-v7a)
198//! - iOS (arm64)
199//! - Linux (x86_64, aarch64)
200//! - macOS (x86_64, aarch64)
201//! - Windows (x86_64, arm64)
202//! - WebAssembly (wasm32)
203//!
204//! If you have a new platform you'd like to see supported, please open an issue on the Subsecond repository.
205//! We are keen to add support for new platforms like wasm64, riscv64, and more.
206//!
207//! Note that iOS device is currently not supported due to code-signing requirements. We hope to fix
208//! this in the future, but for now you can use the simulator to test your app.
209//!
210//! ## Adding the Subsecond badge to your project
211//!
212//! If you're a framework author and want your users to know that your library supports Subsecond, you
213//! can add the Subsecond badge to your README! Users will know that your library is hot-reloadable and
214//! can be used with Subsecond.
215//!
216//! [![Subsecond](https://img.shields.io/badge/Subsecond-Enabled-orange)](https://crates.io/crates/subsecond)
217//!
218//! ```markdown
219//! [![Subsecond](https://img.shields.io/badge/Subsecond-Enabled-orange)](https://crates.io/crates/subsecond)
220//! ```
221//!
222//! ## License
223//!
224//! Subsecond and ThinLink are licensed under the MIT license. See the LICENSE file for more information.
225//!
226//! ## Supporting this work
227//!
228//! Subsecond is a project by the Dioxus team. If you'd like to support our work, please consider
229//! [sponsoring us on GitHub](https://github.com/sponsors/DioxusLabs) or eventually deploying your
230//! apps with Dioxus Deploy (currently under construction).
231
232pub use subsecond_types::JumpTable;
233
234use std::{
235    backtrace,
236    mem::transmute,
237    panic::AssertUnwindSafe,
238    sync::{atomic::AtomicPtr, Arc, Mutex},
239};
240
241/// Call a given function with hot-reloading enabled. If the function's code changes, `call` will use
242/// the new version of the function. If code *above* the function changes, this will emit a panic
243/// that forces an unwind to the next [`call`] instance.
244///
245/// WASM/rust does not support unwinding, so [`call`] will not track dependency graph changes.
246/// If you are building a framework for use on WASM, you will need to use `Subsecond::HotFn` directly.
247///
248/// However, if you wrap your calling code in a future, you *can* simply drop the future which will
249/// cause `drop` to execute and get something similar to unwinding. Not great if refcells are open.
250pub fn call<O>(mut f: impl FnMut() -> O) -> O {
251    // Only run in debug mode - the rest of this function will dissolve away
252    if !cfg!(debug_assertions) {
253        return f();
254    }
255
256    let mut hotfn = HotFn::current(f);
257    loop {
258        let res = std::panic::catch_unwind(AssertUnwindSafe(|| hotfn.call(())));
259
260        // If the call succeeds just return the result, otherwise we try to handle the panic if its our own.
261        let err = match res {
262            Ok(res) => return res,
263            Err(err) => err,
264        };
265
266        // If this is our panic then let's handle it, otherwise we just resume unwinding
267        let Some(_hot_payload) = err.downcast_ref::<HotFnPanic>() else {
268            std::panic::resume_unwind(err);
269        };
270    }
271}
272
273// We use an AtomicPtr with a leaked JumpTable and Relaxed ordering to give us a global jump table
274// with very little overhead. Reading this amounts of a Relaxed atomic load which basically
275// is no overhead. We might want to look into using a thread_local with a stop-the-world approach
276// just in case multiple threads try to call the jump table before synchronization with the runtime.
277// For Dioxus purposes, this is not a big deal, but for libraries like bevy which heavily rely on
278// multithreading, it might become an issue.
279static APP_JUMP_TABLE: AtomicPtr<JumpTable> = AtomicPtr::new(std::ptr::null_mut());
280static HOTRELOAD_HANDLERS: Mutex<Vec<Arc<dyn Fn() + Send + Sync>>> = Mutex::new(Vec::new());
281
282/// Register a function that will be called whenever a patch is applied.
283///
284/// This handler will be run immediately after the patch library is loaded into the process and the
285/// JumpTable has been set.
286pub fn register_handler(handler: Arc<dyn Fn() + Send + Sync + 'static>) {
287    HOTRELOAD_HANDLERS.lock().unwrap().push(handler);
288}
289
290/// Get the current jump table, if it exists.
291///
292/// This will return `None` if no jump table has been set yet.
293///
294/// # Safety
295///
296/// The `JumpTable` returned here is a pointer into a leaked box. While technically this reference is
297/// valid, we might change the implementation to invalidate the pointer between hotpatches.
298///
299/// You should only use this lifetime in temporary contexts - not *across* hotpatches!
300pub unsafe fn get_jump_table() -> Option<&'static JumpTable> {
301    let ptr = APP_JUMP_TABLE.load(std::sync::atomic::Ordering::Relaxed);
302    if ptr.is_null() {
303        return None;
304    }
305
306    Some(unsafe { &*ptr })
307}
308unsafe fn commit_patch(table: JumpTable) {
309    APP_JUMP_TABLE.store(
310        Box::into_raw(Box::new(table)),
311        std::sync::atomic::Ordering::Relaxed,
312    );
313    HOTRELOAD_HANDLERS
314        .lock()
315        .unwrap()
316        .clone()
317        .iter()
318        .for_each(|handler| {
319            handler();
320        });
321}
322
323/// A panic issued by the [`call`] function if the caller would be stale if called. This causes
324/// an unwind to the next [`call`] instance that can properly handle the panic and retry the call.
325///
326/// This technique allows Subsecond to provide hot-reloading of codebases that don't have a runtime integration.
327#[derive(Debug)]
328pub struct HotFnPanic {
329    _backtrace: backtrace::Backtrace,
330}
331
332/// A pointer to a hot patched function
333#[non_exhaustive]
334#[derive(PartialEq, Eq, Hash, Clone, Copy, Debug)]
335pub struct HotFnPtr(pub u64);
336
337impl HotFnPtr {
338    /// Create a new [`HotFnPtr`].
339    ///
340    /// The safe way to get one is through [`HotFn::ptr_address`].
341    ///
342    /// # Safety
343    ///
344    /// The underlying `u64` must point to a valid function.
345    pub unsafe fn new(index: u64) -> Self {
346        Self(index)
347    }
348}
349
350/// A hot-reloadable function.
351///
352/// To call this function, use the [`HotFn::call`] method. This will automatically use the latest
353/// version of the function from the JumpTable.
354pub struct HotFn<A, M, F>
355where
356    F: HotFunction<A, M>,
357{
358    inner: F,
359    _marker: std::marker::PhantomData<(A, M)>,
360}
361
362impl<A, M, F: HotFunction<A, M>> HotFn<A, M, F> {
363    /// Create a new [`HotFn`] instance with the current function.
364    ///
365    /// Whenever you call [`HotFn::call`], it will use the current function from the [`JumpTable`].
366    pub const fn current(f: F) -> HotFn<A, M, F> {
367        HotFn {
368            inner: f,
369            _marker: std::marker::PhantomData,
370        }
371    }
372
373    /// Call the function with the given arguments.
374    ///
375    /// This will unwrap the [`HotFnPanic`] panic, propagating up to the next [`HotFn::call`].
376    ///
377    /// If you want to handle the panic yourself, use [`HotFn::try_call`].
378    pub fn call(&mut self, args: A) -> F::Return {
379        self.try_call(args).unwrap()
380    }
381
382    /// Get the address of the function in memory which might be different than the original.
383    ///
384    /// This is useful for implementing a memoization strategy to safely preserve state across
385    /// hot-patches. If the ptr_address of a function did not change between patches, then the
386    /// state that exists "above" the function is still valid.
387    ///
388    /// Note that Subsecond does not track this state over time, so it's up to the runtime integration
389    /// to track this state and diff it.
390    pub fn ptr_address(&self) -> HotFnPtr {
391        if size_of::<F>() == size_of::<fn() -> ()>() {
392            let ptr: usize = unsafe { std::mem::transmute_copy(&self.inner) };
393            return HotFnPtr(ptr as u64);
394        }
395
396        let known_fn_ptr = <F as HotFunction<A, M>>::call_it as *const () as usize;
397        if let Some(jump_table) = unsafe { get_jump_table() } {
398            if let Some(ptr) = jump_table.map.get(&(known_fn_ptr as u64)).cloned() {
399                return HotFnPtr(ptr);
400            }
401        }
402
403        HotFnPtr(known_fn_ptr as u64)
404    }
405
406    /// Attempt to call the function with the given arguments.
407    ///
408    /// If this function is stale and can't be updated in place (ie, changes occurred above this call),
409    /// then this function will emit an [`HotFnPanic`] which can be unwrapped and handled by next [`call`]
410    /// instance.
411    pub fn try_call(&mut self, args: A) -> Result<F::Return, HotFnPanic> {
412        if !cfg!(debug_assertions) {
413            return Ok(self.inner.call_it(args));
414        }
415
416        unsafe {
417            // Try to handle known function pointers. This is *really really* unsafe, but due to how
418            // rust trait objects work, it's impossible to make an arbitrary usize-sized type implement Fn()
419            // since that would require a vtable pointer, pushing out the bounds of the pointer size.
420            if size_of::<F>() == size_of::<fn() -> ()>() {
421                return Ok(self.inner.call_as_ptr(args));
422            }
423
424            // Handle trait objects. This will occur for sizes other than usize. Normal rust functions
425            // become ZST's and thus their <T as SomeFn>::call becomes a function pointer to the function.
426            //
427            // For non-zst (trait object) types, then there might be an issue. The real call function
428            // will likely end up in the vtable and will never be hot-reloaded since signature takes self.
429            if let Some(jump_table) = get_jump_table() {
430                let known_fn_ptr = <F as HotFunction<A, M>>::call_it as *const () as u64;
431                if let Some(ptr) = jump_table.map.get(&known_fn_ptr).cloned() {
432                    // The type sig of the cast should match the call_it function
433                    // Technically function pointers need to be aligned, but that alignment is 1 so we're good
434                    let call_it = transmute::<*const (), fn(&F, A) -> F::Return>(ptr as _);
435                    return Ok(call_it(&self.inner, args));
436                }
437            }
438
439            Ok(self.inner.call_it(args))
440        }
441    }
442
443    /// Attempt to call the function with the given arguments, using the given [`HotFnPtr`].
444    ///
445    /// You can get a [`HotFnPtr`] from [`Self::ptr_address`].
446    ///
447    /// If this function is stale and can't be updated in place (ie, changes occurred above this call),
448    /// then this function will emit an [`HotFnPanic`] which can be unwrapped and handled by next [`call`]
449    /// instance.
450    ///
451    /// # Safety
452    ///
453    /// The [`HotFnPtr`] must be to a function whose arguments layouts haven't changed.
454    pub unsafe fn try_call_with_ptr(
455        &mut self,
456        ptr: HotFnPtr,
457        args: A,
458    ) -> Result<F::Return, HotFnPanic> {
459        if !cfg!(debug_assertions) {
460            return Ok(self.inner.call_it(args));
461        }
462
463        unsafe {
464            // Try to handle known function pointers. This is *really really* unsafe, but due to how
465            // rust trait objects work, it's impossible to make an arbitrary usize-sized type implement Fn()
466            // since that would require a vtable pointer, pushing out the bounds of the pointer size.
467            if size_of::<F>() == size_of::<fn() -> ()>() {
468                return Ok(self.inner.call_as_ptr(args));
469            }
470
471            // Handle trait objects. This will occur for sizes other than usize. Normal rust functions
472            // become ZST's and thus their <T as SomeFn>::call becomes a function pointer to the function.
473            //
474            // For non-zst (trait object) types, then there might be an issue. The real call function
475            // will likely end up in the vtable and will never be hot-reloaded since signature takes self.
476            // The type sig of the cast should match the call_it function
477            // Technically function pointers need to be aligned, but that alignment is 1 so we're good
478            let call_it = transmute::<*const (), fn(&F, A) -> F::Return>(ptr.0 as _);
479            Ok(call_it(&self.inner, args))
480        }
481    }
482}
483
484/// Apply the patch using a given jump table.
485///
486/// # Safety
487///
488/// This function is unsafe because it detours existing functions in memory. This is *wildly* unsafe,
489/// especially if the JumpTable is malformed. Only run this if you know what you're doing.
490///
491/// If the pointers are incorrect, function type signatures will be incorrect and the program will crash,
492/// sometimes in a way that requires a restart of your entire computer. Be careful.
493///
494/// # Warning
495///
496/// This function will load the library and thus allocates. In cannot be used when the program is
497/// stopped (ie in a signal handler).
498///
499/// # Returns
500///
501/// On success, returns the list of host (pre-patch) function pointers that were
502/// rewritten by this patch — already shifted to the live ASLR slide, so they can
503/// be compared against `fn_name as *const ()` values held in Whisker's runtime
504/// `component_owners` map. The Strategy C hot-reload path (Whisker A6+) uses
505/// this list to find live component owners whose body is now stale and re-mount
506/// them.
507pub unsafe fn apply_patch(mut table: JumpTable) -> Result<Vec<*const ()>, PatchError> {
508    // On non-wasm platforms we can just use libloading and the known aslr offsets to load the library
509    #[cfg(any(unix, windows))]
510    {
511        // on android we try to circumvent permissions issues by copying the library to a memmap and then libloading that
512        #[cfg(target_os = "android")]
513        let lib = Box::leak(Box::new(android_memmap_dlopen(&table.lib)?));
514
515        #[cfg(not(target_os = "android"))]
516        let lib = Box::leak(Box::new({
517            match libloading::Library::new(&table.lib) {
518                Ok(lib) => lib,
519                Err(err) => return Err(PatchError::Dlopen(err.to_string())),
520            }
521        }));
522
523        // Whisker fork: anchor on `whisker_aslr_anchor` rather than `main`.
524        //
525        // Upstream uses `main` as the cross-library ASLR anchor. That works
526        // for Dioxus, where `main` is the PIE process entry and unique in
527        // the linker namespace. Whisker ships the user crate as a `dylib`
528        // loaded via Android `System.loadLibrary`, where the namespace can
529        // contain several `main` symbols (`app_process64`'s, stale patch
530        // memfds', etc.) — `dlsym(RTLD_DEFAULT, "main")` then returns the
531        // wrong one and the computed ASLR slide is garbage.
532        //
533        // `whisker_aslr_anchor` is synthesised by `#[whisker::main]` and
534        // is unique to the user's `.so`, so the dlsym lookup is
535        // unambiguous regardless of namespace order.
536        let old_offset = aslr_reference() - table.aslr_reference as usize;
537
538        let new_offset = unsafe {
539            // Leak the library. dlopen is basically a no-op on many platforms and if we even try to drop it,
540            // some code might be called (ie drop) that results in really bad crashes (restart your computer...)
541            //
542            // The CLI (whisker-dev-server) coordinates by ensuring
543            // `whisker_aslr_anchor` is in the export list of every patch.
544            lib.get::<*const ()>(b"whisker_aslr_anchor")
545                .ok()
546                .unwrap()
547                .try_as_raw_ptr()
548                .unwrap()
549                .wrapping_byte_sub(table.new_base_address as usize) as usize
550        };
551
552        // Modify the jump table to be relative to the base address of the loaded library
553        table.map = table
554            .map
555            .iter()
556            .map(|(k, v)| {
557                (
558                    (*k as usize + old_offset) as u64,
559                    (*v as usize + new_offset) as u64,
560                )
561            })
562            .collect();
563
564        // Snapshot the host-side (post-slide) function pointers BEFORE
565        // committing the patch — `commit_patch` consumes `table`. Whisker's
566        // remount logic compares these against `fn as *const ()` values
567        // stashed in its component-owner registry.
568        let patched: Vec<*const ()> = table.map.keys().map(|k| *k as *const ()).collect();
569
570        commit_patch(table);
571        return Ok(patched);
572    };
573
574    // The non-wasm path above returns inside the block; for wasm we drop
575    // through to the wasm-specific code below which never returns Ok with
576    // a list (patches are async-applied). Surface an empty list in that
577    // case rather than restructuring the existing wasm flow.
578    #[allow(unreachable_code)]
579    {
580        let _unused: Vec<*const ()> = Vec::new();
581    }
582
583    // On wasm, we need to download the module, compile it, and then run it.
584    #[cfg(target_arch = "wasm32")]
585    wasm_bindgen_futures::spawn_local(async move {
586        use js_sys::{
587            ArrayBuffer, Object, Reflect,
588            WebAssembly::{self, Instance, Memory, Module, Table},
589        };
590        use wasm_bindgen::prelude::*;
591        use wasm_bindgen::JsValue;
592        use wasm_bindgen::UnwrapThrowExt;
593        use wasm_bindgen_futures::JsFuture;
594
595        let funcs: Table = wasm_bindgen::function_table().unchecked_into();
596        let memory: Memory = wasm_bindgen::memory().unchecked_into();
597        let exports: Object = wasm_bindgen::exports().unchecked_into();
598
599        let path = table.lib.to_str().unwrap();
600        if !path.ends_with(".wasm") {
601            return;
602        }
603
604        // Fetch + decode the patch wasm. Both awaits are pure I/O — they
605        // touch no shared state, so the future is safe to drop here.
606        let response: web_sys::Response =
607            JsFuture::from(web_sys::window().unwrap_throw().fetch_with_str(&path))
608                .await
609                .unwrap()
610                .unchecked_into();
611        if !response.ok() {
612            panic!(
613                "Failed to patch wasm module at {} - response failed with: {}",
614                path,
615                response.status_text()
616            );
617        }
618        let dl_bytes: ArrayBuffer = JsFuture::from(response.array_buffer().unwrap())
619            .await
620            .unwrap()
621            .unchecked_into();
622
623        // Pre-compile. This is the slow part — V8 hands it to a worker —
624        // and yields the longest. The result is a Module with no side
625        // effects on host JS state, so cancelling here is also safe.
626        let module: Module = JsFuture::from(WebAssembly::compile(dl_bytes.unchecked_ref()))
627            .await
628            .unwrap()
629            .unchecked_into();
630
631        // ── HOST-STATE-MUTATING SECTION ───────────────────────────────
632        //
633        // Below we grow shared linear memory and the indirect function
634        // table, then async-instantiate the patch into them and commit
635        // the new jump table. There IS one `.await` for the instantiate
636        // (we can't avoid it: Chrome disallows synchronous
637        // `new WebAssembly.Instance` on the main thread for modules
638        // larger than 8MB, and patches routinely cross that), but it's
639        // safe — the original race we fixed wasn't about yielding here:
640        //
641        //  * `memory.grow` and `funcs.grow` each return their PRIOR
642        //    length atomically. Concurrent `apply_patch` tasks therefore
643        //    each get a unique, non-overlapping `memory_base` /
644        //    `table_base`, so two patches can't land on the same region.
645        //  * Host code can't observe the half-instantiated patch: the
646        //    new memory pages are zero, the new table slots are null,
647        //    and the jump table isn't committed until the very end of
648        //    this block, so nothing redirects through the new slots.
649        //  * The original bug — using `memory.buffer().byteLength()`
650        //    captured before the awaits, which returned 0 if the buffer
651        //    had been detached by a concurrent grow — is gone because
652        //    we derive `memory_base` from `memory.grow()`'s return
653        //    value instead.
654        //  * Cancellation between grow and `commit_patch` leaks memory
655        //    pages and table slots, but doesn't corrupt anything.
656        const PAGE_SIZE: u32 = 64 * 1024;
657        let patch_pages = (dl_bytes.byte_length() as f64 / PAGE_SIZE as f64).ceil() as u32 + 1;
658
659        // Use grow's return value (the prior page count) to derive
660        // memory_base. Atomic w.r.t. concurrent grows, unlike reading
661        // memory.buffer().byteLength().
662        let prev_pages = memory.grow(patch_pages);
663        let memory_base = (prev_pages + 1) * PAGE_SIZE;
664
665        // grow returns the prior table length, which is __table_base.
666        let table_base = funcs.grow(table.ifunc_count as u32).unwrap();
667
668        // Rebase the jump table entries onto the patch's table slot range.
669        for v in table.map.values_mut() {
670            *v += table_base as u64;
671        }
672
673        // Build the env import object: copy every host export through and
674        // add __memory_base / __table_base globals so the patch's PIC
675        // code resolves correctly.
676        let env = Object::new();
677        for key in Object::keys(&exports) {
678            Reflect::set(&env, &key, &Reflect::get(&exports, &key).unwrap()).unwrap();
679        }
680        for (name, value) in [("__table_base", table_base), ("__memory_base", memory_base)] {
681            let descriptor = Object::new();
682            Reflect::set(&descriptor, &"value".into(), &"i32".into()).unwrap();
683            Reflect::set(&descriptor, &"mutable".into(), &false.into()).unwrap();
684            let global = WebAssembly::Global::new(&descriptor, &value.into()).unwrap();
685            Reflect::set(&env, &name.into(), &global.into()).unwrap();
686        }
687        let imports = Object::new();
688        Reflect::set(&imports, &"env".into(), &env).unwrap();
689
690        // Async instantiation of the precompiled module. We use the
691        // (Module, imports) form of `WebAssembly.instantiate`, which
692        // resolves directly to an `Instance` (not `{module, instance}`).
693        // This is the no-size-limit path; the synchronous
694        // `new WebAssembly.Instance` constructor is capped at 8MB on
695        // Chrome's main thread.
696        let instance: Instance = JsFuture::from(WebAssembly::instantiate_module(&module, &imports))
697            .await
698            .unwrap()
699            .unchecked_into();
700        let inst_exports: Object = instance.exports();
701
702        // Run the patch's relocation thunks and constructors. Order
703        // matters: data relocs first (write memory_base- and table_base-
704        // relative pointers into the patch's data segment), then global
705        // relocs (adjust GOT.func.internal globals by __table_base —
706        // wasm-ld synthesizes those as element-segment-relative offsets),
707        // then ctors. `dyn_into` instead of `unchecked_into` so missing
708        // exports just no-op rather than throwing.
709        for func_name in [
710            "__wasm_apply_data_relocs",
711            "__wasm_apply_global_relocs",
712            "__wasm_call_ctors",
713        ] {
714            if let Ok(val) = Reflect::get(&inst_exports, &func_name.into()) {
715                if let Ok(func) = val.dyn_into::<js_sys::Function>() {
716                    _ = func.call0(&JsValue::undefined());
717                }
718            }
719        }
720
721        unsafe { commit_patch(table) };
722    });
723
724    // wasm path applies the patch asynchronously inside the spawned
725    // future. We have no synchronous patched-pointer list to return.
726    Ok(Vec::new())
727}
728
729#[derive(Debug, PartialEq, thiserror::Error)]
730pub enum PatchError {
731    /// The patch failed to apply.
732    ///
733    /// This returns a string instead of the Dlopen error type so we don't need to bring the libloading
734    /// dependency into the public API.
735    #[error("Failed to load library: {0}")]
736    Dlopen(String),
737
738    /// The patch failed to apply on Android, most likely due to a permissions issue.
739    #[error("Failed to load library on Android: {0}")]
740    AndroidMemfd(String),
741}
742
743/// This function returns the address of the main function in the current executable. This is used as
744/// an anchor to reference the current executable's base address.
745///
746/// The point here being that we have a stable address both at runtime and compile time, making it
747/// possible to calculate the ASLR offset from within the process to correct the jump table.
748///
749/// It should only be called from the main executable *first* and not from a shared library since it
750/// self-initializes.
751#[doc(hidden)]
752pub fn aslr_reference() -> usize {
753    #[cfg(target_family = "wasm")]
754    return 0;
755
756    #[cfg(not(target_family = "wasm"))]
757    unsafe {
758        use std::ffi::c_void;
759
760        // The first call to this function should occur in the
761        static mut MAIN_PTR: *mut c_void = std::ptr::null_mut();
762
763        if MAIN_PTR.is_null() {
764            // Whisker fork: anchor on `whisker_aslr_anchor` rather than
765            // `main` so the dlsym/GetProcAddress lookup cannot collide
766            // with system binaries' `main` (e.g. Android's
767            // `app_process64`). See the matching comment in
768            // `apply_patch` above. The symbol is emitted by
769            // `#[whisker::main]` and is unique to the user's `.so`.
770            #[cfg(unix)]
771            {
772                MAIN_PTR = libc::dlsym(libc::RTLD_DEFAULT, c"whisker_aslr_anchor".as_ptr() as _);
773            }
774
775            #[cfg(windows)]
776            {
777                extern "system" {
778                    fn GetModuleHandleA(lpModuleName: *const i8) -> *mut std::ffi::c_void;
779                    fn GetProcAddress(
780                        hModule: *mut std::ffi::c_void,
781                        lpProcName: *const i8,
782                    ) -> *mut std::ffi::c_void;
783                }
784
785                MAIN_PTR = GetProcAddress(
786                    GetModuleHandleA(std::ptr::null()),
787                    c"whisker_aslr_anchor".as_ptr() as _,
788                ) as _;
789            }
790        }
791
792        MAIN_PTR as usize
793    }
794}
795
796/// On Android, we can't dlopen libraries that aren't placed inside /data/data/<package_name>/lib/
797///
798/// If the device isn't rooted, then we can't push the library there.
799/// This is a workaround to copy the library to a memfd and then dlopen it.
800///
801/// I haven't tested it on device yet, so if if it doesn't work, then we can simply revert to using
802/// "adb root" and then pushing the library to the /data/data folder instead of the tmp folder.
803///
804/// Android provides us a flag when calling dlopen to use a file descriptor instead of a path, presumably
805/// because they want to support this.
806/// - https://developer.android.com/ndk/reference/group/libdl
807/// - https://developer.android.com/ndk/reference/structandroid/dlextinfo
808#[cfg(target_os = "android")]
809unsafe fn android_memmap_dlopen(file: &std::path::Path) -> Result<libloading::Library, PatchError> {
810    use std::ffi::{c_void, CStr, CString};
811    use std::os::fd::{AsRawFd, BorrowedFd};
812    use std::ptr;
813
814    #[repr(C)]
815    struct ExtInfo {
816        flags: u64,
817        reserved_addr: *const c_void,
818        reserved_size: libc::size_t,
819        relro_fd: libc::c_int,
820        library_fd: libc::c_int,
821        library_fd_offset: libc::off64_t,
822        library_namespace: *const c_void,
823    }
824
825    extern "C" {
826        fn android_dlopen_ext(
827            filename: *const libc::c_char,
828            flags: libc::c_int,
829            ext_info: *const ExtInfo,
830        ) -> *const c_void;
831    }
832
833    use memmap2::MmapAsRawDesc;
834    use std::os::unix::prelude::{FromRawFd, IntoRawFd};
835
836    let contents = std::fs::read(file)
837        .map_err(|e| PatchError::AndroidMemfd(format!("Failed to read file: {}", e)))?;
838    let mut mfd = memfd::MemfdOptions::default()
839        .create("subsecond-patch")
840        .map_err(|e| PatchError::AndroidMemfd(format!("Failed to create memfd: {}", e)))?;
841    mfd.as_file()
842        .set_len(contents.len() as u64)
843        .map_err(|e| PatchError::AndroidMemfd(format!("Failed to set memfd length: {}", e)))?;
844
845    let raw_fd = mfd.into_raw_fd();
846
847    let mut map = memmap2::MmapMut::map_mut(raw_fd)
848        .map_err(|e| PatchError::AndroidMemfd(format!("Failed to map memfd: {}", e)))?;
849    map.copy_from_slice(&contents);
850    let map = map
851        .make_exec()
852        .map_err(|e| PatchError::AndroidMemfd(format!("Failed to make memfd executable: {}", e)))?;
853
854    let filename = c"/subsecond-patch";
855
856    let info = ExtInfo {
857        flags: 0x10, // ANDROID_DLEXT_USE_LIBRARY_FD
858        reserved_addr: ptr::null(),
859        reserved_size: 0,
860        relro_fd: 0,
861        library_fd: raw_fd,
862        library_fd_offset: 0,
863        library_namespace: ptr::null(),
864    };
865
866    let flags = libloading::os::unix::RTLD_LAZY | libloading::os::unix::RTLD_LOCAL;
867
868    let handle = libloading::os::unix::with_dlerror(
869        || {
870            let ptr = android_dlopen_ext(filename.as_ptr() as _, flags, &info);
871            if ptr.is_null() {
872                return None;
873            } else {
874                return Some(ptr);
875            }
876        },
877        |err| err.to_str().unwrap_or_default().to_string(),
878    )
879    .map_err(|e| {
880        PatchError::AndroidMemfd(format!(
881            "android_dlopen_ext failed: {}",
882            e.unwrap_or_default()
883        ))
884    })?;
885
886    let lib = unsafe { libloading::os::unix::Library::from_raw(handle as *mut c_void) };
887    let lib: libloading::Library = lib.into();
888    Ok(lib)
889}
890
891/// A trait that enables types to be hot-patched.
892///
893/// This trait is only implemented for FnMut types which naturally includes function pointers and
894/// closures that can be re-ran. FnOnce closures are currently not supported since the hot-patching
895/// system we use implies that the function can be called multiple times.
896pub trait HotFunction<Args, Marker> {
897    /// The return type of the function.
898    type Return;
899
900    /// The real function type. This is meant to be a function pointer.
901    /// When we call `call_as_ptr`, we will transmute the function to this type and call it.
902    type Real;
903
904    /// Call the HotFunction with the given arguments.
905    ///
906    /// # Why
907    ///
908    /// "rust-call" isn't stable, so we wrap the underlying call with our own, giving it a stable vtable entry.
909    /// This is more important than it seems since this function becomes "real" and can be hot-patched.
910    fn call_it(&mut self, args: Args) -> Self::Return;
911
912    /// Call the HotFunction as if it were a function pointer.
913    ///
914    /// # Safety
915    ///
916    /// This is only safe if the underlying type is a function (function pointer or virtual/fat pointer).
917    /// Using this will use the JumpTable to find the patched function and call it.
918    unsafe fn call_as_ptr(&mut self, _args: Args) -> Self::Return;
919}
920
921macro_rules! impl_hot_function {
922    (
923        $(
924            ($marker:ident, $($arg:ident),*)
925        ),*
926    ) => {
927        $(
928            /// A marker type for the function.
929            /// This is hidden with the intention to seal this trait.
930            #[doc(hidden)]
931            pub struct $marker;
932
933            impl<T, $($arg,)* R> HotFunction<($($arg,)*), $marker> for T
934            where
935                T: FnMut($($arg),*) -> R,
936            {
937                type Return = R;
938                type Real = fn($($arg),*) -> R;
939
940                fn call_it(&mut self, args: ($($arg,)*)) -> Self::Return {
941                    #[allow(non_snake_case)]
942                    let ( $($arg,)* ) = args;
943                    self($($arg),*)
944                }
945
946                unsafe fn call_as_ptr(&mut self, args: ($($arg,)*)) -> Self::Return {
947                    unsafe {
948                        if let Some(jump_table) = get_jump_table() {
949                            let real = std::mem::transmute_copy::<Self, Self::Real>(&self) as *const ();
950
951                            // Android implements MTE / pointer tagging and we need to preserve the tag.
952                            // If we leave the tag, then indexing our jump table will fail and patching won't work (or crash!)
953                            // This is only implemented on 64-bit platforms since pointer tagging is not available on 32-bit platforms
954                            // In dev, Dioxus disables MTE to work around this issue, but we still handle it anyways.
955                            #[cfg(all(target_pointer_width = "64", target_os = "android"))] let nibble  = real as u64 & 0xFF00_0000_0000_0000;
956                            #[cfg(all(target_pointer_width = "64", target_os = "android"))] let real    = real as u64 & 0x00FFF_FFF_FFFF_FFFF;
957
958                            #[cfg(target_pointer_width = "64")] let real  = real as u64;
959
960                            // No nibble on 32-bit platforms, but we still need to assume u64 since the host always writes 64-bit addresses
961                            #[cfg(target_pointer_width = "32")] let real = real as u64;
962
963                            // Whisker fix for pointer-sized *capturing* closures.
964                            //
965                            // The `size_of::<F>() == size_of::<fn()>()` gate in
966                            // `try_call` routes any closure that is coincidentally
967                            // pointer-sized through this `call_as_ptr` path — which
968                            // assumes `F` is a bare function pointer and recovers its
969                            // address via `transmute_copy`. But a `#[component]` body
970                            // closure that captures exactly one 8-byte value (the
971                            // common case: a single signal / `NodeId`) is *also*
972                            // pointer-sized, and `transmute_copy` then yields the
973                            // captured VALUE (e.g. a slotmap key like `0x1_0000_0001`),
974                            // not a code address. The lookup misses and the stale
975                            // pre-patch body runs — "patch applied" but no visual
976                            // change.
977                            //
978                            // When the transmute-recovered key isn't in the table,
979                            // fall back to the closure's own `call_it`
980                            // monomorphisation address — a real code symbol that the
981                            // JumpTable carries (the same key the non-pointer-sized
982                            // `try_call` path uses). For a genuine fn pointer the
983                            // first lookup already hits, so this only adds a fallback.
984                            // Genuine bare fn pointer: `real` is its code
985                            // address and is in the table. Call it as
986                            // `Self::Real` (a plain `fn(args)`, no `self`).
987                            if let Some(ptr) = jump_table.map.get(&real).cloned() {
988                                // Re-apply the nibble - though this might not be required (we aren't calling malloc for a new pointer)
989                                #[cfg(all(target_pointer_width = "64", target_os = "android"))] let ptr: u64 = ptr | nibble;
990
991                                #[cfg(target_pointer_width = "64")] let ptr: u64 = ptr;
992                                #[cfg(target_pointer_width = "32")] let ptr: u32 = ptr as u32;
993
994                                // Macro-rules requires unpacking the tuple before we call it
995                                #[allow(non_snake_case)]
996                                let ( $($arg,)* ) = args;
997
998
999                                #[cfg(target_pointer_width = "64")]
1000                                type PtrWidth = u64;
1001                                #[cfg(target_pointer_width = "32")]
1002                                type PtrWidth = u32;
1003
1004                                return std::mem::transmute::<PtrWidth, Self::Real>(ptr)($($arg),*);
1005                            }
1006
1007                            // Whisker fix: `real` missed because this is a
1008                            // pointer-sized *capturing* closure, not a bare fn
1009                            // pointer — `transmute_copy` yielded the captured
1010                            // value (e.g. a single signal's `NodeId`), not a code
1011                            // address. Dispatch on the closure's own `call_it`
1012                            // monomorphisation address instead — a real symbol the
1013                            // JumpTable carries — calling it WITH `&mut self` since
1014                            // `call_it` takes `self`.
1015                            let call_it_key =
1016                                <Self as HotFunction<($($arg,)*), $marker>>::call_it
1017                                    as *const () as u64;
1018                            if let Some(ptr) = jump_table.map.get(&call_it_key).cloned() {
1019                                let patched = std::mem::transmute::<
1020                                    *const (),
1021                                    fn(&mut Self, ($($arg,)*)) -> Self::Return,
1022                                >(ptr as *const ());
1023                                return patched(self, args);
1024                            }
1025                        }
1026
1027                        self.call_it(args)
1028                    }
1029                }
1030            }
1031        )*
1032    };
1033}
1034
1035impl_hot_function!(
1036    (Fn0Marker,),
1037    (Fn1Marker, A),
1038    (Fn2Marker, A, B),
1039    (Fn3Marker, A, B, C),
1040    (Fn4Marker, A, B, C, D),
1041    (Fn5Marker, A, B, C, D, E),
1042    (Fn6Marker, A, B, C, D, E, F),
1043    (Fn7Marker, A, B, C, D, E, F, G),
1044    (Fn8Marker, A, B, C, D, E, F, G, H),
1045    (Fn9Marker, A, B, C, D, E, F, G, H, I)
1046);