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 will 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 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 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).
498pub unsafe fn apply_patch(mut table: JumpTable) -> Result<(), PatchError> {
499    // On non-wasm platforms we can just use libloading and the known aslr offsets to load the library
500    #[cfg(any(unix, windows))]
501    {
502        // on android we try to circumvent permissions issues by copying the library to a memmap and then libloading that
503        #[cfg(target_os = "android")]
504        let lib = Box::leak(Box::new(android_memmap_dlopen(&table.lib)?));
505
506        #[cfg(not(target_os = "android"))]
507        let lib = Box::leak(Box::new({
508            match libloading::Library::new(&table.lib) {
509                Ok(lib) => lib,
510                Err(err) => return Err(PatchError::Dlopen(err.to_string())),
511            }
512        }));
513
514        // Use the `main` symbol as a sentinel for the current executable. This is basically a
515        // cross-platform version of `__mh_execute_header` on macOS that we can use to base the executable.
516        let old_offset = aslr_reference() - table.aslr_reference as usize;
517
518        // Use the `main` symbol as a sentinel for the loaded library. Might want to move away
519        // from this at some point, or make it configurable
520        let new_offset = unsafe {
521            // Leak the library. dlopen is basically a no-op on many platforms and if we even try to drop it,
522            // some code might be called (ie drop) that results in really bad crashes (restart your computer...)
523            //
524            // This code currently assumes "main" always makes it to the export list (which it should)
525            // and requires coordination from the CLI to export it.
526            lib.get::<*const ()>(b"main")
527                .ok()
528                .unwrap()
529                .try_as_raw_ptr()
530                .unwrap()
531                .wrapping_byte_sub(table.new_base_address as usize) as usize
532        };
533
534        // Modify the jump table to be relative to the base address of the loaded library
535        table.map = table
536            .map
537            .iter()
538            .map(|(k, v)| {
539                (
540                    (*k as usize + old_offset) as u64,
541                    (*v as usize + new_offset) as u64,
542                )
543            })
544            .collect();
545
546        commit_patch(table);
547    };
548
549    // On wasm, we need to download the module, compile it, and then run it.
550    #[cfg(target_arch = "wasm32")]
551    wasm_bindgen_futures::spawn_local(async move {
552        use js_sys::{
553            ArrayBuffer, Object, Reflect,
554            WebAssembly::{self, Memory, Table},
555        };
556        use wasm_bindgen::prelude::*;
557        use wasm_bindgen::JsValue;
558        use wasm_bindgen::UnwrapThrowExt;
559        use wasm_bindgen_futures::JsFuture;
560
561        let funcs: Table = wasm_bindgen::function_table().unchecked_into();
562        let memory: Memory = wasm_bindgen::memory().unchecked_into();
563        let exports: Object = wasm_bindgen::exports().unchecked_into();
564        let buffer: ArrayBuffer = memory.buffer().unchecked_into();
565
566        let path = table.lib.to_str().unwrap();
567        if !path.ends_with(".wasm") {
568            return;
569        }
570
571        // Start the fetch of the module
572        let response = web_sys::window().unwrap_throw().fetch_with_str(&path);
573
574        // Wait for the fetch to complete - we need the wasm module size in bytes to reserve in the memory
575        let response: web_sys::Response = JsFuture::from(response).await.unwrap().unchecked_into();
576
577        // If the status is not success, we bail
578        if !response.ok() {
579            panic!(
580                "Failed to patch wasm module at {} - response failed with: {}",
581                path,
582                response.status_text()
583            );
584        }
585
586        let dl_bytes: ArrayBuffer = JsFuture::from(response.array_buffer().unwrap())
587            .await
588            .unwrap()
589            .unchecked_into();
590
591        // Expand the memory and table size to accommodate the new data and functions
592        //
593        // Normally we wouldn't be able to trust that we are allocating *enough* memory
594        // for BSS segments, but ld emits them in the binary when using import-memory.
595        //
596        // Make sure we align the memory base to the page size
597        const PAGE_SIZE: u32 = 64 * 1024;
598        let page_count = (buffer.byte_length() as f64 / PAGE_SIZE as f64).ceil() as u32;
599        let memory_base = (page_count + 1) * PAGE_SIZE;
600
601        // We need to grow the memory to accommodate the new module
602        memory.grow((dl_bytes.byte_length() as f64 / PAGE_SIZE as f64).ceil() as u32 + 1);
603
604        // We grow the ifunc table to accommodate the new functions
605        // In theory we could just put all the ifuncs in the jump map and use that for our count,
606        // but there's no guarantee from the jump table that it references "itself"
607        // We might need a sentinel value for each ifunc in the jump map to indicate that it is
608        let table_base = funcs.grow(table.ifunc_count as u32).unwrap();
609
610        // Adjust the jump table to be relative to the new base address
611        for v in table.map.values_mut() {
612            *v += table_base as u64;
613        }
614
615        // Build up the import object. We copy everything over from the current exports, but then
616        // need to add in the memory and table base offsets for the relocations to work.
617        //
618        // let imports = {
619        //     env: {
620        //         memory: base.memory,
621        //         __tls_base: base.__tls_base,
622        //         __stack_pointer: base.__stack_pointer,
623        //         __indirect_function_table: base.__indirect_function_table,
624        //         __memory_base: memory_base,
625        //         __table_base: table_base,
626        //        ..base_exports
627        //     },
628        // };
629        let env = Object::new();
630
631        // Move memory, __tls_base, __stack_pointer, __indirect_function_table, and all exports over
632        for key in Object::keys(&exports) {
633            Reflect::set(&env, &key, &Reflect::get(&exports, &key).unwrap()).unwrap();
634        }
635
636        // Set the memory and table in the imports
637        // Following this pattern: Global.new({ value: "i32", mutable: false }, value)
638        for (name, value) in [("__table_base", table_base), ("__memory_base", memory_base)] {
639            let descriptor = Object::new();
640            Reflect::set(&descriptor, &"value".into(), &"i32".into()).unwrap();
641            Reflect::set(&descriptor, &"mutable".into(), &false.into()).unwrap();
642            let value = WebAssembly::Global::new(&descriptor, &value.into()).unwrap();
643            Reflect::set(&env, &name.into(), &value.into()).unwrap();
644        }
645
646        // Set the memory and table in the imports
647        let imports = Object::new();
648        Reflect::set(&imports, &"env".into(), &env).unwrap();
649
650        // Download the module, returning { module, instance }
651        // we unwrap here instead of using `?` since this whole thing is async
652        let result_object = JsFuture::from(WebAssembly::instantiate_module(
653            dl_bytes.unchecked_ref(),
654            &imports,
655        ))
656        .await
657        .unwrap();
658
659        // We need to run the data relocations and then fire off the constructors
660        let res: Object = result_object.unchecked_into();
661        let instance: Object = Reflect::get(&res, &"instance".into())
662            .unwrap()
663            .unchecked_into();
664        let exports: Object = Reflect::get(&instance, &"exports".into())
665            .unwrap()
666            .unchecked_into();
667        _ = Reflect::get(&exports, &"__wasm_apply_data_relocs".into())
668            .unwrap()
669            .unchecked_into::<js_sys::Function>()
670            .call0(&JsValue::undefined());
671        _ = Reflect::get(&exports, &"__wasm_apply_global_relocs".into())
672            .unwrap()
673            .unchecked_into::<js_sys::Function>()
674            .call0(&JsValue::undefined());
675        _ = Reflect::get(&exports, &"__wasm_call_ctors".into())
676            .unwrap()
677            .unchecked_into::<js_sys::Function>()
678            .call0(&JsValue::undefined());
679
680        unsafe { commit_patch(table) };
681    });
682
683    Ok(())
684}
685
686#[derive(Debug, PartialEq, thiserror::Error)]
687pub enum PatchError {
688    /// The patch failed to apply.
689    ///
690    /// This returns a string instead of the Dlopen error type so we don't need to bring the libloading
691    /// dependency into the public API.
692    #[error("Failed to load library: {0}")]
693    Dlopen(String),
694
695    /// The patch failed to apply on Android, most likely due to a permissions issue.
696    #[error("Failed to load library on Android: {0}")]
697    AndroidMemfd(String),
698}
699
700/// This function returns the address of the main function in the current executable. This is used as
701/// an anchor to reference the current executable's base address.
702///
703/// The point here being that we have a stable address both at runtime and compile time, making it
704/// possible to calculate the ASLR offset from within the process to correct the jump table.
705///
706/// It should only be called from the main executable *first* and not from a shared library since it
707/// self-initializes.
708#[doc(hidden)]
709pub fn aslr_reference() -> usize {
710    #[cfg(target_family = "wasm")]
711    return 0;
712
713    #[cfg(not(target_family = "wasm"))]
714    unsafe {
715        use std::ffi::c_void;
716
717        // The first call to this function should occur in the
718        static mut MAIN_PTR: *mut c_void = std::ptr::null_mut();
719
720        if MAIN_PTR.is_null() {
721            #[cfg(unix)]
722            {
723                MAIN_PTR = libc::dlsym(libc::RTLD_DEFAULT, c"main".as_ptr() as _);
724            }
725
726            #[cfg(windows)]
727            {
728                extern "system" {
729                    fn GetModuleHandleA(lpModuleName: *const i8) -> *mut std::ffi::c_void;
730                    fn GetProcAddress(
731                        hModule: *mut std::ffi::c_void,
732                        lpProcName: *const i8,
733                    ) -> *mut std::ffi::c_void;
734                }
735
736                MAIN_PTR =
737                    GetProcAddress(GetModuleHandleA(std::ptr::null()), c"main".as_ptr() as _) as _;
738            }
739        }
740
741        MAIN_PTR as usize
742    }
743}
744
745/// On Android, we can't dlopen libraries that aren't placed inside /data/data/<package_name>/lib/
746///
747/// If the device isn't rooted, then we can't push the library there.
748/// This is a workaround to copy the library to a memfd and then dlopen it.
749///
750/// I haven't tested it on device yet, so if if it doesn't work, then we can simply revert to using
751/// "adb root" and then pushing the library to the /data/data folder instead of the tmp folder.
752///
753/// Android provides us a flag when calling dlopen to use a file descriptor instead of a path, presumably
754/// because they want to support this.
755/// - https://developer.android.com/ndk/reference/group/libdl
756/// - https://developer.android.com/ndk/reference/structandroid/dlextinfo
757#[cfg(target_os = "android")]
758unsafe fn android_memmap_dlopen(file: &std::path::Path) -> Result<libloading::Library, PatchError> {
759    use std::ffi::{c_void, CStr, CString};
760    use std::os::fd::{AsRawFd, BorrowedFd};
761    use std::ptr;
762
763    #[repr(C)]
764    struct ExtInfo {
765        flags: u64,
766        reserved_addr: *const c_void,
767        reserved_size: libc::size_t,
768        relro_fd: libc::c_int,
769        library_fd: libc::c_int,
770        library_fd_offset: libc::off64_t,
771        library_namespace: *const c_void,
772    }
773
774    extern "C" {
775        fn android_dlopen_ext(
776            filename: *const libc::c_char,
777            flags: libc::c_int,
778            ext_info: *const ExtInfo,
779        ) -> *const c_void;
780    }
781
782    use memmap2::MmapAsRawDesc;
783    use std::os::unix::prelude::{FromRawFd, IntoRawFd};
784
785    let contents = std::fs::read(file)
786        .map_err(|e| PatchError::AndroidMemfd(format!("Failed to read file: {}", e)))?;
787    let mut mfd = memfd::MemfdOptions::default()
788        .create("subsecond-patch")
789        .map_err(|e| PatchError::AndroidMemfd(format!("Failed to create memfd: {}", e)))?;
790    mfd.as_file()
791        .set_len(contents.len() as u64)
792        .map_err(|e| PatchError::AndroidMemfd(format!("Failed to set memfd length: {}", e)))?;
793
794    let raw_fd = mfd.into_raw_fd();
795
796    let mut map = memmap2::MmapMut::map_mut(raw_fd)
797        .map_err(|e| PatchError::AndroidMemfd(format!("Failed to map memfd: {}", e)))?;
798    map.copy_from_slice(&contents);
799    let map = map
800        .make_exec()
801        .map_err(|e| PatchError::AndroidMemfd(format!("Failed to make memfd executable: {}", e)))?;
802
803    let filename = c"/subsecond-patch";
804
805    let info = ExtInfo {
806        flags: 0x10, // ANDROID_DLEXT_USE_LIBRARY_FD
807        reserved_addr: ptr::null(),
808        reserved_size: 0,
809        relro_fd: 0,
810        library_fd: raw_fd,
811        library_fd_offset: 0,
812        library_namespace: ptr::null(),
813    };
814
815    let flags = libloading::os::unix::RTLD_LAZY | libloading::os::unix::RTLD_LOCAL;
816
817    let handle = libloading::os::unix::with_dlerror(
818        || {
819            let ptr = android_dlopen_ext(filename.as_ptr() as _, flags, &info);
820            if ptr.is_null() {
821                return None;
822            } else {
823                return Some(ptr);
824            }
825        },
826        |err| err.to_str().unwrap_or_default().to_string(),
827    )
828    .map_err(|e| {
829        PatchError::AndroidMemfd(format!(
830            "android_dlopen_ext failed: {}",
831            e.unwrap_or_default()
832        ))
833    })?;
834
835    let lib = unsafe { libloading::os::unix::Library::from_raw(handle as *mut c_void) };
836    let lib: libloading::Library = lib.into();
837    Ok(lib)
838}
839
840/// A trait that enables types to be hot-patched.
841///
842/// This trait is only implemented for FnMut types which naturally includes function pointers and
843/// closures that can be re-ran. FnOnce closures are currently not supported since the hot-patching
844/// system we use implies that the function can be called multiple times.
845pub trait HotFunction<Args, Marker> {
846    /// The return type of the function.
847    type Return;
848
849    /// The real function type. This is meant to be a function pointer.
850    /// When we call `call_as_ptr`, we will transmute the function to this type and call it.
851    type Real;
852
853    /// Call the HotFunction with the given arguments.
854    ///
855    /// # Why
856    ///
857    /// "rust-call" isn't stable, so we wrap the underlying call with our own, giving it a stable vtable entry.
858    /// This is more important than it seems since this function becomes "real" and can be hot-patched.
859    fn call_it(&mut self, args: Args) -> Self::Return;
860
861    /// Call the HotFunction as if it were a function pointer.
862    ///
863    /// # Safety
864    ///
865    /// This is only safe if the underlying type is a function (function pointer or virtual/fat pointer).
866    /// Using this will use the JumpTable to find the patched function and call it.
867    unsafe fn call_as_ptr(&mut self, _args: Args) -> Self::Return;
868}
869
870macro_rules! impl_hot_function {
871    (
872        $(
873            ($marker:ident, $($arg:ident),*)
874        ),*
875    ) => {
876        $(
877            /// A marker type for the function.
878            /// This is hidden with the intention to seal this trait.
879            #[doc(hidden)]
880            pub struct $marker;
881
882            impl<T, $($arg,)* R> HotFunction<($($arg,)*), $marker> for T
883            where
884                T: FnMut($($arg),*) -> R,
885            {
886                type Return = R;
887                type Real = fn($($arg),*) -> R;
888
889                fn call_it(&mut self, args: ($($arg,)*)) -> Self::Return {
890                    #[allow(non_snake_case)]
891                    let ( $($arg,)* ) = args;
892                    self($($arg),*)
893                }
894
895                unsafe fn call_as_ptr(&mut self, args: ($($arg,)*)) -> Self::Return {
896                    unsafe {
897                        if let Some(jump_table) = get_jump_table() {
898                            let real = std::mem::transmute_copy::<Self, Self::Real>(&self) as *const ();
899
900                            // Android implements MTE / pointer tagging and we need to preserve the tag.
901                            // If we leave the tag, then indexing our jump table will fail and patching won't work (or crash!)
902                            // This is only implemented on 64-bit platforms since pointer tagging is not available on 32-bit platforms
903                            // In dev, Dioxus disables MTE to work around this issue, but we still handle it anyways.
904                            #[cfg(all(target_pointer_width = "64", target_os = "android"))] let nibble  = real as u64 & 0xFF00_0000_0000_0000;
905                            #[cfg(all(target_pointer_width = "64", target_os = "android"))] let real    = real as u64 & 0x00FFF_FFF_FFFF_FFFF;
906
907                            #[cfg(target_pointer_width = "64")] let real  = real as u64;
908
909                            // No nibble on 32-bit platforms, but we still need to assume u64 since the host always writes 64-bit addresses
910                            #[cfg(target_pointer_width = "32")] let real = real as u64;
911
912                            if let Some(ptr) = jump_table.map.get(&real).cloned() {
913                                // Re-apply the nibble - though this might not be required (we aren't calling malloc for a new pointer)
914                                #[cfg(all(target_pointer_width = "64", target_os = "android"))] let ptr: u64 = ptr | nibble;
915
916                                #[cfg(target_pointer_width = "64")] let ptr: u64 = ptr;
917                                #[cfg(target_pointer_width = "32")] let ptr: u32 = ptr as u32;
918
919                                // Macro-rules requires unpacking the tuple before we call it
920                                #[allow(non_snake_case)]
921                                let ( $($arg,)* ) = args;
922
923
924                                #[cfg(target_pointer_width = "64")]
925                                type PtrWidth = u64;
926                                #[cfg(target_pointer_width = "32")]
927                                type PtrWidth = u32;
928
929                                return std::mem::transmute::<PtrWidth, Self::Real>(ptr)($($arg),*);
930                            }
931                        }
932
933                        self.call_it(args)
934                    }
935                }
936            }
937        )*
938    };
939}
940
941impl_hot_function!(
942    (Fn0Marker,),
943    (Fn1Marker, A),
944    (Fn2Marker, A, B),
945    (Fn3Marker, A, B, C),
946    (Fn4Marker, A, B, C, D),
947    (Fn5Marker, A, B, C, D, E),
948    (Fn6Marker, A, B, C, D, E, F),
949    (Fn7Marker, A, B, C, D, E, F, G),
950    (Fn8Marker, A, B, C, D, E, F, G, H),
951    (Fn9Marker, A, B, C, D, E, F, G, H, I)
952);