Binary patching rust hot-reloading, sub-second rebuilds, independent server/client hot-reload (#3797)
Enable runtime rust hot patching. * roll back wry to 0.45 * cleanup rustc * wip * wip: fix client/server * get the subsecond cli thing working again * back to list of builds but with accessors * migrate some methods to workspace, clean up impl a bit * pass mode through build context * use a build context, similar to cargo/rustc * merge BuildUpdate and HandleUpdate into BuilderUpdate * Move more resolution of args into runner * migrate some methods to runner * hoist out fullstack * fix build request resolver * yay request is cleaned up * fixup resolution a bit * spawn off the build! * re-wire android autodetection * re-wire android autodetection * wire back up a few more things * re-wire tooling verification * I think it's mostly in the right condition for regular app * add depinfo parser * yay okay works with regular apps again * full rebuilds are back * rewire file wtcher * wire patch! * yayyyyyy patching works and is really fast * yay android works * clean up stdout piping * yayyy memap on android so we don't need root * create a static wasm jump table * wip: global offset table for wasm * wip... thinking about making the patches relocatable * wip: customized relocation * YESSSSSS RELOCATABLE WASM * clean up impl a bit * lil bit more cleanup * lil bit more * lil bit more cleanup * lil bit more cleanup, support 32 bit platforms * sick, wasm is completely pic * hmmmm not quite working yet * woooo, patches loading from rust+wasm * integrated into the cli 😎 * condense a bit * cleaned up request a bit * bust fingerprints * Make the file diffing logic a bit more sensible * still working through a few issues, server launches but is acting weird * fix merge conflict * more merge issues * remove stuff we don't want anymore * revert name change * wip: server not working anymore :( * split apart "dioxus fullstack" into "dioxus server" and "dioxus web" * fixup a few more compile errors * grrr opening server * simultaneous frontend and backend patching * use rustc wrapper * wip: passing assets to jumptable * migrate project-like examples * patchy patchy server fns! * rollback some random changes * unwind the js files * rollback hash * no need for patch * more cleanups * lil bit more cleanup, remove some old cruft * allow patching when wasm webpage is closed * tiny bit more robust * lil bit of clean up * clean up ws code * bit more clean ups * condense * undo file moves * move back other project * move other project * migrate out harness and janky CLI * fix compile * anonymize some files, heavily document others * lots more documentation, for posterity! * ton more docs * clean up the cli a bit more * more ws cleanup * more cleanup * migrate build id to cli-config * add command file handling * random small cleanups * wip.... * wip.... * fix: use workspace for krate discovery * fix panic logging on serve by placing it *after* the logs * reorder logging to be sensible on success * bring back swc * ws cruft * small patches to relocs in loader * bump krates * hoist main funcs into the ifunc table, fixing wasm * fix tui output for long lines * add more linker logging * wow, incredible, we build fat binaries, holy heckkkkk * fix args for frontend/backend * small cleanups * properly send along build IDs to clients * fix workspace compiles * clean up jump table work a bit * clean up logging a bit * open existing * fixup some commands * open existing browser integration * wire up the new dx serve client/server syntax * fixup `dx run` command * bring back some old functionality * fix serverfn feature sets * remove toast animation and simplify its code a bit * less intrusive toast * dont change build status when patching on web * add proper cache busting to vcomponent * clean up the patch file a bit * more lints/checks cleaned up * go back to TargetArgs but a BuildTargets resolved struct * clean up more nits * use an atomicptr for jumptable * fix interaction with initial web load + suspense * don't run asset system on wasm/bindgen js in dev * reduce blast radius of PR * use profile to determine if we're in release * cleanup prod html template * cleanup profiles * fix feature resolution of fullstack * if fullstack is explicitly false, don't use it * light cleanups * drop carg config2 * pass along linker args properly * make workspace examples compile with dx again * fewer unwraps and better error loggign * bit more error handlign * small cleanup * drive-by cleanups * use queries instead of initialize for aslr stuff * fix hotpatch with higher opt level * fix aslr bug from multiple clients * fix merge conflict * fix typos * clippy * fix miscompile * fix doctest * properly rollback wry to 0.45 * fix markdown path issues, other test issues * fix test in router * fix release mode test * fix some more tests, clean up a few more items * use fnptr instead of typeid * clean up memozation via subsecond in core * use "main" as sentinel * fix imports and re-exports * get off __rust_alloc for aslr offset detection * wip * wip... fixing unoptimized web * hmmmm mmmm * close,ish, still missing the wasmbindgen dynaimc imports * aha! full wasm works now - disable externref when compilng patch modules - manually link wasm instrinsics - displace the __wbindgen_placeholder__ table with the synthesized wbg imports what an awful pain. There might be a few items missing, so will test against the docsite shortly * remove failing test * hmmmm things aren't perfect * from what I can tell, relocatable wasm just doesn't work with wasm-bindgen sad https://github.com/rustwasm/wasm-bindgen/issues/1420 * AI IS LITERALLY THE WORST * preserve wasm snippets across compiles * IT HOTPATCHES THE DOCSITE, WE ARE LIVING IN THE FUTURE * remove the macos cfg-out * fix: need to allocate 1 more page * fix url issue * properly span props * properly fill in extra symbols * light cleanups and good docs * delete cruft, better docs * More cleanups and simplifications * clippy, typos * remove harnesses * wasm-bindgen error logging * clean up deps * remove cruft * proper priv/pub * use ifuncs for all env imports * cruft * use par_iter, better output * implement a cache for the base module * delete custom sections * cache the main native module too * add par iter in a few more places * clippy, map ident * better logging and status bars * wip: some small windows support * wip more windowss * fix weird regression * implement windows jump table * small fixes for windows * better windows support * windows support pt2 * rust lld is not msvc * whole archive the rlibs * ignore windows and sys crates * logging * ohhhhhh, pop out dlls * dedupe * thANK GOD FAT LINKING WORKS ON WINDOWS * hoist caching * implement patch based on cache * fix location for pdb * pass data symbols, correct object * add all symbol for windows * add log * treat none rva as undefined * whoops * handle windows calling convention * dont use relocated arm64 stub windows * not all are text? * WINDOWS FUNCTION PATCHING WORKS but issue with statics? * use our own entry * handle windows imp symbols * add log * jump to the real symbol not the layout * whoops * hotpatching windows works completely holy heck yay * drop linker args crate * disable ssg playwright for now * light cleanups * some cleanups / preps: - bump to serverfn 0.8.2 - fix playwright test - fix clippy * use fix from #4042 * bring in other fix for liveview * hoist some configuration * use __aslr_reference instead of aslr_reference * add better errors to patcherror * fix android * panic if patching fails * clean up tomls * wip: fixing wasm compile for server * fullstack wasm again * fix cfg * fixup feature detection * fix issue with features * fix playwright! * add barebones template test harness * fix compatibility between the two hotreload engines * fix preldue of server * fix check for wasm-opt * small fixes
This commit is contained in:
1 parent
71d85e6dd5
commit
d976ca184a
142 files changed
+17982
-7156
No files matched your search
@@ -0,0 +1,76 @@
|
||||
# Subsecond
|
||||
|
||||
Subsecond is a hot-reloading library for Rust. It makes it easy to add Rust hot-reloading to your
|
||||
existing Rust project with minimal integration overhead.
|
||||
|
||||
## Usage:
|
||||
|
||||
For library authors you can use "hot" functions with the `subsecond::current` function:
|
||||
|
||||
```rust
|
||||
/// A user-facing tick / launch / start function
|
||||
///
|
||||
/// Typically this will be a request/response handler, a game loop, a main function, callback, etc
|
||||
///
|
||||
/// `current` accepts function pointers and Fn types
|
||||
pub fn tick(handler: Fn(Request) -> Response) {
|
||||
// Create a "hot" function that we can inspect
|
||||
let hot_fn = subsecond::current(handler);
|
||||
|
||||
// Check if this function has been patched
|
||||
if hot_fn.changed() {
|
||||
// do thing
|
||||
}
|
||||
|
||||
// Register a handler to be called when the function is patched
|
||||
hot_fn.on_changed(|| /* do thing */);
|
||||
|
||||
// Call the hot function
|
||||
hot_fn.call((request))
|
||||
}
|
||||
```
|
||||
|
||||
For application authors, you can use `subsecond::call()` to make a function hot-reloadable:
|
||||
|
||||
```rust
|
||||
fn handle_request(request: Request) -> Response {
|
||||
subsecond::call(|| {
|
||||
// do_thing...
|
||||
})
|
||||
}
|
||||
```
|
||||
|
||||
If a hot function is actively being called, then subsecond will rewind the stack to the "cleanest" entrypoint. For example, a hot-reloadable server will have two "hot" points: at the start of the server, and at the start of the request handler. When the server is reloaded, subsecond will rewind the stack to the first hot point, and then call the function again.
|
||||
|
||||
```rust
|
||||
// Changes to `serve` will reload the server
|
||||
fn serve() {
|
||||
let router = Router::new();
|
||||
router.get("/", handle_request);
|
||||
router.serve("0.0.0.0:8080");
|
||||
}
|
||||
|
||||
// Changes below "handle_request" won't be reload the router
|
||||
fn handle_request(request: Request) -> Response {
|
||||
// do thing
|
||||
}
|
||||
```
|
||||
|
||||
Framework authors can interleave their own hot-reload entrypoints alongside user code. This lets you add new anchors into long-running stateful code:
|
||||
|
||||
```rust
|
||||
fn main() {
|
||||
// serve is "hot" and will rebuild the router if it changes
|
||||
webserver::serve("0.0.0.0:8080", || {
|
||||
Router::new()
|
||||
// get is "hot" and changes to handle_request won't rebuild the router
|
||||
.get("/", |req| Response::websocket(handle_socket))
|
||||
})
|
||||
}
|
||||
|
||||
fn handle_socket(ws: &WebSocket) {
|
||||
subsecond::call(|| {
|
||||
// do things with the websocket
|
||||
})
|
||||
}
|
||||
```
|
||||
@@ -0,0 +1,13 @@
|
||||
[package]
|
||||
name = "subsecond-types"
|
||||
version.workspace = true
|
||||
edition = "2021"
|
||||
authors = ["Jonathan Kelley"]
|
||||
description = "Types crate for the Subsecond hotpatch engine."
|
||||
license = "MIT OR Apache-2.0"
|
||||
repository = "https://github.com/DioxusLabs/dioxus/"
|
||||
keywords = ["hotpatch", "engine", "subsecond", "dioxus"]
|
||||
|
||||
|
||||
[dependencies]
|
||||
serde = { workspace = true, features = ["derive"] }
|
||||
@@ -0,0 +1,92 @@
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::{
|
||||
collections::HashMap,
|
||||
hash::{BuildHasherDefault, Hasher},
|
||||
path::PathBuf,
|
||||
};
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)]
|
||||
pub struct JumpTable {
|
||||
/// The dylib containing the patch. This should be a valid path so you can just pass it to LibLoading
|
||||
///
|
||||
/// On wasm you will need to fetch() this file and then pass it to the WebAssembly.instantiate() function
|
||||
pub lib: PathBuf,
|
||||
|
||||
/// old -> new
|
||||
/// does not take into account the base address of the patch when loaded into memory - need dlopen for that
|
||||
///
|
||||
/// These are intended to be `*const ()` pointers but need to be `u64` for the hashmap. On 32-bit platforms
|
||||
/// you will need to cast to `usize` before using them.
|
||||
pub map: AddressMap,
|
||||
|
||||
/// the address of the base address of the old original binary
|
||||
///
|
||||
/// machos: this is the address of the `_mh_execute_header` symbol usually at 0x100000000 and loaded near 0x100000000
|
||||
/// linux: this is the address of the `__executable_start` symbol usually at 0x0 but loaded around 0x555555550000
|
||||
/// windows: this is the address of the `ImageBase` field of the PE header
|
||||
/// wasm: not useful since there's no ASLR
|
||||
///
|
||||
/// While we can generally guess that these values are, it's possible they are different and thus reading
|
||||
/// them dynamically is worthwhile.
|
||||
pub aslr_reference: u64,
|
||||
|
||||
/// the address of the base address of the new binary
|
||||
///
|
||||
/// machos: this is the address of the `_mh_execute_header` symbol usually at 0x100000000 and loaded near 0x100000000
|
||||
/// linux: this is the address of the `__executable_start` symbol usually at 0x0 but loaded around 0x555555550000
|
||||
/// windows: this is the address of the `ImageBase` field of the PE header
|
||||
/// wasm: not useful since there's no ASLR
|
||||
///
|
||||
/// While we can generally guess that these values are, it's possible they are different and thus reading
|
||||
/// them dynamically is worthwhile.
|
||||
pub new_base_address: u64,
|
||||
|
||||
/// The amount of ifuncs this will register. This is used by WASM to know how much space to allocate
|
||||
/// for the ifuncs in the ifunc table
|
||||
pub ifunc_count: u64,
|
||||
}
|
||||
|
||||
/// An address to address hashmap that does not hash addresses since addresses are by definition unique.
|
||||
pub type AddressMap = HashMap<u64, u64, BuildAddressHasher>;
|
||||
pub type BuildAddressHasher = BuildHasherDefault<AddressHasher>;
|
||||
|
||||
#[derive(Default, Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
|
||||
pub struct AddressHasher(u64);
|
||||
impl Hasher for AddressHasher {
|
||||
fn write(&mut self, _: &[u8]) {
|
||||
panic!("Invalid use of NoHashHasher")
|
||||
}
|
||||
fn write_u8(&mut self, n: u8) {
|
||||
self.0 = u64::from(n)
|
||||
}
|
||||
fn write_u16(&mut self, n: u16) {
|
||||
self.0 = u64::from(n)
|
||||
}
|
||||
fn write_u32(&mut self, n: u32) {
|
||||
self.0 = u64::from(n)
|
||||
}
|
||||
fn write_u64(&mut self, n: u64) {
|
||||
self.0 = n
|
||||
}
|
||||
fn write_usize(&mut self, n: usize) {
|
||||
self.0 = n as u64
|
||||
}
|
||||
fn write_i8(&mut self, n: i8) {
|
||||
self.0 = n as u64
|
||||
}
|
||||
fn write_i16(&mut self, n: i16) {
|
||||
self.0 = n as u64
|
||||
}
|
||||
fn write_i32(&mut self, n: i32) {
|
||||
self.0 = n as u64
|
||||
}
|
||||
fn write_i64(&mut self, n: i64) {
|
||||
self.0 = n as u64
|
||||
}
|
||||
fn write_isize(&mut self, n: isize) {
|
||||
self.0 = n as u64
|
||||
}
|
||||
fn finish(&self) -> u64 {
|
||||
self.0
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
[package]
|
||||
name = "subsecond"
|
||||
edition = "2021"
|
||||
version.workspace = true
|
||||
authors = ["Jonathan Kelley"]
|
||||
description = "A runtime hotpatching engine for Rust hot-reloading."
|
||||
license = "MIT OR Apache-2.0"
|
||||
repository = "https://github.com/DioxusLabs/dioxus/"
|
||||
keywords = ["hotpatch", "engine", "subsecond", "dioxus", "hot-reload"]
|
||||
|
||||
[dependencies]
|
||||
serde = { workspace = true, features = ["derive"] }
|
||||
subsecond-types = { workspace = true }
|
||||
thiserror = { workspace = true }
|
||||
|
||||
[target.'cfg(target_arch = "wasm32")'.dependencies]
|
||||
web-sys = { workspace = true, features = ["FetchEvent", "Request", "Window", "Response", "ResponseType", "console"] }
|
||||
wasm-bindgen = { workspace = true }
|
||||
wasm-bindgen-futures = { workspace = true }
|
||||
js-sys = { workspace = true}
|
||||
|
||||
[target.'cfg(not(target_arch = "wasm32"))'.dependencies]
|
||||
libloading = { workspace = true}
|
||||
libc = { workspace = true}
|
||||
memmap2 = { workspace = true}
|
||||
|
||||
[target.'cfg(target_os = "android")'.dependencies]
|
||||
memfd = { workspace = true}
|
||||
@@ -0,0 +1,815 @@
|
||||
#![allow(clippy::needless_doctest_main)]
|
||||
//! # Subsecond: Hot-patching for Rust
|
||||
//!
|
||||
//! Subsecond is a library that enables hot-patching for Rust applications. This allows you to change
|
||||
//! the code of a running application without restarting it. This is useful for game engines, servers,
|
||||
//! and other long-running applications where the typical edit-compile-run cycle is too slow.
|
||||
//!
|
||||
//! Subsecond also implements a technique we call "ThinLinking" which makes compiling Rust code
|
||||
//! significantly faster in development mode, which can be used outside of hot-patching.
|
||||
//!
|
||||
//! # Usage
|
||||
//!
|
||||
//! Subsecond is designed to be as simple for both application developers and library authors.
|
||||
//!
|
||||
//! Simply call your existing functions with [`call`] and Subsecond will automatically detour
|
||||
//! that call to the latest version of the function.
|
||||
//!
|
||||
//! ```rust
|
||||
//! for x in 0..5 {
|
||||
//! subsecond::call(|| {
|
||||
//! println!("Hello, world! {}", x);
|
||||
//! });
|
||||
//! }
|
||||
//! ```
|
||||
//!
|
||||
//! To actually load patches into your application, a third-party tool that implements the Subsecond
|
||||
//! compiler and protocol is required. Subsecond is built and maintained by the Dioxus team, so we
|
||||
//! suggest using the dioxus CLI tool to use subsecond.
|
||||
//!
|
||||
//! To install the Dioxus CLI, we recommend using [`cargo binstall`](https://crates.io/crates/cargo-binstall):
|
||||
//!
|
||||
//! ```sh
|
||||
//! cargo binstall dioxus-cli
|
||||
//! ```
|
||||
//!
|
||||
//! The Dioxus CLI provides several tools for development. To run your application with Subsecond enabled,
|
||||
//! use `dx serve` - this takes the same arguments as `cargo run` but will automatically hot-reload your
|
||||
//! application when changes are detected.
|
||||
//!
|
||||
//! As of Dioxus 0.7, "--hotpatch" is required to use hotpatching while Subsecond is still experimental.
|
||||
//!
|
||||
//! ```sh
|
||||
//! dx serve --hotpatch
|
||||
//! ```
|
||||
//!
|
||||
//! ## How it works
|
||||
//!
|
||||
//! Subsecond works by detouring function calls through a jump table. This jump table contains the latest
|
||||
//! version of the program's function pointers, and when a function is called, Subsecond will look up
|
||||
//! the function in the jump table and call that instead.
|
||||
//!
|
||||
//! Unlike libraries like [detour](https://crates.io/crates/detour), Subsecond *does not* modify your
|
||||
//! process memory. Patching pointers is wildly unsafe and can lead to crashes and undefined behavior.
|
||||
//!
|
||||
//! Instead, an external tool compiles only the parts of your project that changed, links them together
|
||||
//! using the addresses of the functions in your running program, and then sends the new jump table to
|
||||
//! your application. Subsecond then applies the patch and continues running. Since Subsecond doesn't
|
||||
//! modify memory, the program must have a runtime integration to handle the patching.
|
||||
//!
|
||||
//! If the framework you're using doesn't integrate with subsecond, you can rely on the fact that calls
|
||||
//! to stale [`call`] instances will emit a safe panic that is automatically caught and retried
|
||||
//! by the next [`call`] instance up the callstack.
|
||||
//!
|
||||
//! Subsecond is only enabled when debug_assertions are enabled so you can safely ship your application
|
||||
//! with Subsecond enabled without worrying about the performance overhead.
|
||||
//!
|
||||
//! ## Globals and statics
|
||||
//!
|
||||
//! Subsecond *does* support hot-reloading of globals, statics, and thread locals. However, there are several limitations:
|
||||
//!
|
||||
//! - You may add new globals at runtime, but their destructors will never be called.
|
||||
//! - Globals are tracked across patches, but will renames are considered to be *new* globals.
|
||||
//! - Changes to static initializers will not be observed.
|
||||
//!
|
||||
//! Subsecond purposefully handles statics this way since many libraries like Dioxus and Tokio rely
|
||||
//! on persistent global runtimes.
|
||||
//!
|
||||
//! ## Struct layout and alignment
|
||||
//!
|
||||
//! Subsecond currently does not support hot-reloading of structs. This is because the generated code
|
||||
//! assumes a particular layout and alignment of the struct. If layout or alignment change and new
|
||||
//! functions are called referencing an old version of the struct, the program will crash.
|
||||
//!
|
||||
//! To mitigate this, framework authors can integrate with Subsecond to either dispose of the old struct
|
||||
//! or to re-allocate the struct in a way that is compatible with the new layout. This is called "re-instancing."
|
||||
//!
|
||||
//! Because Subsecond performs a safe panic if a stale function is called, you should never witness
|
||||
//! a crash due to a struct layout change. However, changing a struct's layout will likely cause a
|
||||
//! re-instantiation of the struct and potentially a loss of state.
|
||||
//!
|
||||
//! We'd like to lift this limitation in the future by providing utilities to re-instantiate structs,
|
||||
//! but for now it's up to the framework authors to handle this. For example, Dioxus apps simply throw
|
||||
//! out the old state and rebuild it from scratch.
|
||||
//!
|
||||
//! ## Nesting Calls
|
||||
//!
|
||||
//! Subsecond calls are designed to be nested. This provides clean integration points to know exactly
|
||||
//! where a hooked function is called.
|
||||
//!
|
||||
//! The highest level call is `fn main()` though by default this is not hooked since initialization code
|
||||
//! tends to be side-effectual and modify global state. Instead, we recommend wrapping the hot-patch
|
||||
//! points manually with [`call`].
|
||||
//!
|
||||
//! ```rust
|
||||
//! fn main() {
|
||||
//! // Changes to the the `for` loop will cause an unwind to this call.
|
||||
//! subsecond::call(|| {
|
||||
//! for x in 0..5 {
|
||||
//! // Changes to the `println!` will be isolated to this call.
|
||||
//! subsecond::call(|| {
|
||||
//! println!("Hello, world! {}", x);
|
||||
//! });
|
||||
//! }
|
||||
//! });
|
||||
//! }
|
||||
//! ```
|
||||
//!
|
||||
//! The goal here is to provide granular control over where patches are applied to limit loss of state
|
||||
//! when new code is loaded.
|
||||
//!
|
||||
//! ## Applying patches
|
||||
//!
|
||||
//! When running under the Dioxus CLI, the `dx serve` command will automatically apply patches when
|
||||
//! changes are detected. Patches are delivered over the [Dioxus Devtools](https://crates.io/crates/dioxus-devtools)
|
||||
//! websocket protocol and received by corresponding websocket.
|
||||
//!
|
||||
//! If you're using Subsecond in your own application that doesn't have a runtime integration, you can
|
||||
//! build an integration using the [`apply_patch`] function. This function takes a `JumpTable` which
|
||||
//! the dioxus-cli crate can generate.
|
||||
//!
|
||||
//! To add support for the Dioxus Devtools protocol to your app, you can use the [dioxus-devtools](https://crates.io/crates/dioxus-devtools)
|
||||
//! crate which provides a `connect` method that will automatically apply patches to your application.
|
||||
//!
|
||||
//! Unfortunately, one design quirk of Subsecond is that running apps need to communicate the address
|
||||
//! of `main` to the patcher. This is due to a security technique called [ASLR](https://en.wikipedia.org/wiki/Address_space_layout_randomization)
|
||||
//! which randomizes the address of functions in memory. See the subsecond-harness and subsecond-cli
|
||||
//! for more details on how to implement the protocol.
|
||||
//!
|
||||
//! ## ThinLink
|
||||
//!
|
||||
//! ThinLink is a program linker for Rust that is designed to be used with Subsecond. It implements
|
||||
//! the powerful patching system that Subsecond uses to hot-reload Rust applications.
|
||||
//!
|
||||
//! ThinLink is simply a wrapper around your existing linker but with extra features:
|
||||
//!
|
||||
//! - Automatic dynamic linking to dependencies
|
||||
//! - Generation of Subsecond jump tables
|
||||
//! - Diffing of object files for function invalidation
|
||||
//!
|
||||
//! Because ThinLink performs very to little actual linking, it drastically speeds up traditional Rust
|
||||
//! development. With a development-optimized profile, ThinLink can shrink an incremental build to less than 500ms.
|
||||
//!
|
||||
//! ThinLink is automatically integrated into the Dioxus CLI though it's currently not available as
|
||||
//! a standalone tool.
|
||||
//!
|
||||
//! ## Limitations
|
||||
//!
|
||||
//! Subsecond is a powerful tool but it has several limitations. We talk about them above, but here's
|
||||
//! a quick summary:
|
||||
//!
|
||||
//! - Struct hot reloading requires instancing or unwinding
|
||||
//! - Statics are tracked but not destructed
|
||||
//!
|
||||
//! ## Platform support
|
||||
//!
|
||||
//! Subsecond works across all major platforms:
|
||||
//!
|
||||
//! - Android (arm64-v8a, armeabi-v7a)
|
||||
//! - iOS (arm64)
|
||||
//! - Linux (x86_64, aarch64)
|
||||
//! - macOS (x86_64, aarch64)
|
||||
//! - Windows (x86_64, arm64)
|
||||
//! - WebAssembly (wasm32)
|
||||
//!
|
||||
//! If you have a new platform you'd like to see supported, please open an issue on the Subsecond repository.
|
||||
//! We are keen to add support for new platforms like wasm64, riscv64, and more.
|
||||
//!
|
||||
//! Note that iOS device is currently not supported due to code-signing requirements. We hope to fix
|
||||
//! this in the future, but for now you can use the simulator to test your app.
|
||||
//!
|
||||
//! ## Adding the Subsecond badge to your project
|
||||
//!
|
||||
//! If you're a framework author and want your users to know that your library supports Subsecond, you
|
||||
//! can add the Subsecond badge to your README! Users will know that your library is hot-reloadable and
|
||||
//! can be used with Subsecond.
|
||||
//!
|
||||
//! [](https://crates.io/crates/subsecond)
|
||||
//!
|
||||
//! ```markdown
|
||||
//! [](https://crates.io/crates/subsecond)
|
||||
//! ```
|
||||
//!
|
||||
//! ## License
|
||||
//!
|
||||
//! Subsecond and ThinLink are licensed under the MIT license. See the LICENSE file for more information.
|
||||
//!
|
||||
//! ## Supporting this work
|
||||
//!
|
||||
//! Subsecond is a project by the Dioxus team. If you'd like to support our work, please consider
|
||||
//! [sponsoring us on GitHub](https://github.com/sponsors/DioxusLabs) or eventually deploying your
|
||||
//! apps with Dioxus Deploy (currently under construction).
|
||||
|
||||
pub use subsecond_types::JumpTable;
|
||||
|
||||
use std::{
|
||||
backtrace,
|
||||
mem::transmute,
|
||||
panic::AssertUnwindSafe,
|
||||
sync::{atomic::AtomicPtr, Arc, Mutex},
|
||||
};
|
||||
|
||||
/// Call a given function with hot-reloading enabled. If the function's code changes, `call` will use
|
||||
/// the new version of the function. If code *above* the function changes, this will emit a panic
|
||||
/// that forces an unwind to the next [`call`] instance.
|
||||
///
|
||||
/// WASM/rust does not support unwinding, so [`call`] will not track dependency graph changes.
|
||||
/// If you are building a framework for use on WASM, you will need to use `Subsecond::HotFn` directly.
|
||||
///
|
||||
/// However, if you wrap your calling code in a future, you *can* simply drop the future which will
|
||||
/// cause `drop` to execute and get something similar to unwinding. Not great if refcells are open.
|
||||
pub fn call<O>(mut f: impl FnMut() -> O) -> O {
|
||||
// Only run in debug mode - the rest of this function will dissolve away
|
||||
if !cfg!(debug_assertions) {
|
||||
return f();
|
||||
}
|
||||
|
||||
let mut hotfn = HotFn::current(f);
|
||||
loop {
|
||||
let res = std::panic::catch_unwind(AssertUnwindSafe(|| hotfn.call(())));
|
||||
|
||||
// If the call succeeds just return the result, otherwise we try to handle the panic if its our own.
|
||||
let err = match res {
|
||||
Ok(res) => return res,
|
||||
Err(err) => err,
|
||||
};
|
||||
|
||||
// If this is our panic then let's handle it, otherwise we just resume unwinding
|
||||
let Some(_hot_payload) = err.downcast_ref::<HotFnPanic>() else {
|
||||
std::panic::resume_unwind(err);
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
// We use an AtomicPtr with a leaked JumpTable and Relaxed ordering to give us a global jump table
|
||||
// with very very little overhead. Reading this amounts of a Relaxed atomic load which basically
|
||||
// is no overhead. We might want to look into using a thread_local with a stop-the-world approach
|
||||
// just in case multiple threads try to call the jump table before synchronization with the runtime.
|
||||
// For Dioxus purposes, this is not a big deal, but for libraries like bevy which heavily rely on
|
||||
// multithreading, it might become an issue.
|
||||
static APP_JUMP_TABLE: AtomicPtr<JumpTable> = AtomicPtr::new(std::ptr::null_mut());
|
||||
static HOTRELOAD_HANDLERS: Mutex<Vec<Arc<dyn Fn() + Send + Sync>>> = Mutex::new(Vec::new());
|
||||
pub fn register_handler(handler: Arc<dyn Fn() + Send + Sync + 'static>) {
|
||||
HOTRELOAD_HANDLERS.lock().unwrap().push(handler);
|
||||
}
|
||||
fn get_jump_table() -> Option<&'static JumpTable> {
|
||||
let ptr = APP_JUMP_TABLE.load(std::sync::atomic::Ordering::Relaxed);
|
||||
if ptr.is_null() {
|
||||
return None;
|
||||
}
|
||||
|
||||
Some(unsafe { &*ptr })
|
||||
}
|
||||
unsafe fn commit_patch(table: JumpTable) {
|
||||
APP_JUMP_TABLE.store(
|
||||
Box::into_raw(Box::new(table)),
|
||||
std::sync::atomic::Ordering::Relaxed,
|
||||
);
|
||||
HOTRELOAD_HANDLERS
|
||||
.lock()
|
||||
.unwrap()
|
||||
.clone()
|
||||
.iter()
|
||||
.for_each(|handler| {
|
||||
handler();
|
||||
});
|
||||
}
|
||||
|
||||
/// A panic issued by the [`call`] function if the caller would be stale if called. This causes
|
||||
/// an unwind to the next [`call`] instance that can properly handle the panic and retry the call.
|
||||
///
|
||||
/// This technique allows Subsecond to provide hot-reloading of codebases that don't have a runtime integration.
|
||||
#[derive(Debug)]
|
||||
pub struct HotFnPanic {
|
||||
_backtrace: backtrace::Backtrace,
|
||||
}
|
||||
|
||||
/// A hot-reloadable function.
|
||||
///
|
||||
/// To call this function, use the [`HotFn::call`] method. This will automatically use the latest
|
||||
/// version of the function from the JumpTable.
|
||||
pub struct HotFn<A, M, F>
|
||||
where
|
||||
F: HotFunction<A, M>,
|
||||
{
|
||||
inner: F,
|
||||
_marker: std::marker::PhantomData<(A, M)>,
|
||||
}
|
||||
|
||||
impl<A, M, F: HotFunction<A, M>> HotFn<A, M, F> {
|
||||
/// Create a new [`HotFn`] instance with the current function.
|
||||
///
|
||||
/// Whenever you call [`HotFn::call`], it will use the current function from the [`JumpTable`].
|
||||
pub const fn current(f: F) -> HotFn<A, M, F> {
|
||||
HotFn {
|
||||
inner: f,
|
||||
_marker: std::marker::PhantomData,
|
||||
}
|
||||
}
|
||||
|
||||
/// Call the function with the given arguments.
|
||||
///
|
||||
/// This will unwrap the [`HotFnPanic`] panic, propagating up to the next [`HotFn::call`].
|
||||
///
|
||||
/// If you want to handle the panic yourself, use [`HotFn::try_call`].
|
||||
pub fn call(&mut self, args: A) -> F::Return {
|
||||
self.try_call(args).unwrap()
|
||||
}
|
||||
|
||||
/// Get the address of the function in memory which might be different than the original.
|
||||
///
|
||||
/// This is useful for implementing a memoization strategy to safely preserve state across
|
||||
/// hot-patches. If the ptr_address of a function did not change between patches, then the
|
||||
/// state that exists "above" the function is still valid.
|
||||
///
|
||||
/// Note that Subsecond does not track this state over time, so it's up to the runtime integration
|
||||
/// to track this state and diff it.
|
||||
pub fn ptr_address(&self) -> u64 {
|
||||
if size_of::<F>() == size_of::<fn() -> ()>() {
|
||||
let ptr: usize = unsafe { std::mem::transmute_copy(&self.inner) };
|
||||
return ptr as u64;
|
||||
}
|
||||
|
||||
let known_fn_ptr = <F as HotFunction<A, M>>::call_it as *const () as usize;
|
||||
if let Some(jump_table) = get_jump_table() {
|
||||
if let Some(ptr) = jump_table.map.get(&(known_fn_ptr as u64)).cloned() {
|
||||
return ptr;
|
||||
}
|
||||
}
|
||||
|
||||
known_fn_ptr as u64
|
||||
}
|
||||
|
||||
/// Attempt to call the function with the given arguments.
|
||||
///
|
||||
/// If this function is stale and can't be updated in place (ie, changes occurred above this call),
|
||||
/// then this function will emit an [`HotFnPanic`] which can be unwrapped and handled by next [`call`]
|
||||
/// instance.
|
||||
pub fn try_call(&mut self, args: A) -> Result<F::Return, HotFnPanic> {
|
||||
if !cfg!(debug_assertions) {
|
||||
return Ok(self.inner.call_it(args));
|
||||
}
|
||||
|
||||
unsafe {
|
||||
// Try to handle known function pointers. This is *really really* unsafe, but due to how
|
||||
// rust trait objects work, it's impossible to make an arbitrary usize-sized type implement Fn()
|
||||
// since that would require a vtable pointer, pushing out the bounds of the pointer size.
|
||||
if size_of::<F>() == size_of::<fn() -> ()>() {
|
||||
return Ok(self.inner.call_as_ptr(args));
|
||||
}
|
||||
|
||||
// Handle trait objects. This will occur for sizes other than usize. Normal rust functions
|
||||
// become ZST's and thus their <T as SomeFn>::call becomes a function pointer to the function.
|
||||
//
|
||||
// For non-zst (trait object) types, then there might be an issue. The real call function
|
||||
// will likely end up in the vtable and will never be hot-reloaded since signature takes self.
|
||||
if let Some(jump_table) = get_jump_table() {
|
||||
let known_fn_ptr = <F as HotFunction<A, M>>::call_it as *const () as u64;
|
||||
if let Some(ptr) = jump_table.map.get(&known_fn_ptr).cloned() {
|
||||
// The type sig of the cast should match the call_it function
|
||||
// Technically function pointers need to be aligned, but that alignment is 1 so we're good
|
||||
let call_it = transmute::<*const (), fn(&F, A) -> F::Return>(ptr as _);
|
||||
return Ok(call_it(&self.inner, args));
|
||||
}
|
||||
}
|
||||
|
||||
Ok(self.inner.call_it(args))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Apply the patch using a given jump table.
|
||||
///
|
||||
/// # Safety
|
||||
///
|
||||
/// This function is unsafe because it detours existing functions in memory. This is *wildly* unsafe,
|
||||
/// especially if the JumpTable is malformed. Only run this if you know what you're doing.
|
||||
///
|
||||
/// If the pointers are incorrect, function type signatures will be incorrect and the program will crash,
|
||||
/// sometimes in a way that requires a restart of your entire computer. Be careful.
|
||||
///
|
||||
/// # Warning
|
||||
///
|
||||
/// This function will load the library and thus allocates. In cannot be used when the program is
|
||||
/// stopped (ie in a signal handler).
|
||||
pub unsafe fn apply_patch(mut table: JumpTable) -> Result<(), PatchError> {
|
||||
// On non-wasm platforms we can just use libloading and the known aslr offsets to load the library
|
||||
#[cfg(any(unix, windows))]
|
||||
{
|
||||
// on android we try to cirumvent permissions issues by copying the library to a memmap and then libloading that
|
||||
#[cfg(target_os = "android")]
|
||||
let lib = Box::leak(Box::new(android_memmap_dlopen(&table.lib)?));
|
||||
|
||||
#[cfg(not(target_os = "android"))]
|
||||
let lib = Box::leak(Box::new({
|
||||
match libloading::Library::new(&table.lib) {
|
||||
Ok(lib) => lib,
|
||||
Err(err) => return Err(PatchError::Dlopen(err.to_string())),
|
||||
}
|
||||
}));
|
||||
|
||||
// Use the `__aslr_reference` symbol as a sentinel for the current executable. This is basically a
|
||||
// cross-platform version of `__mh_execute_header` on macOS that sets a reference point for the
|
||||
// jump table.
|
||||
let old_offset = __aslr_reference() - table.aslr_reference as usize;
|
||||
|
||||
// Use the `main` symbol as a sentinel for the loaded library. Might want to move away
|
||||
// from this at some point, or make it configurable
|
||||
let new_offset = unsafe {
|
||||
// Leak the library. dlopen is basically a no-op on many platforms and if we even try to drop it,
|
||||
// some code might be called (ie drop) that results in really bad crashes (restart your computer...)
|
||||
//
|
||||
// This code currently assumes "main" always makes it to the export list (which it should)
|
||||
// and requires coordination from the CLI to export it.
|
||||
lib.get::<*const ()>(b"main")
|
||||
.ok()
|
||||
.unwrap()
|
||||
.try_as_raw_ptr()
|
||||
.unwrap()
|
||||
.wrapping_byte_sub(table.new_base_address as usize) as usize
|
||||
};
|
||||
|
||||
// Modify the jump table to be relative to the base address of the loaded library
|
||||
table.map = table
|
||||
.map
|
||||
.iter()
|
||||
.map(|(k, v)| {
|
||||
(
|
||||
(*k as usize + old_offset) as u64,
|
||||
(*v as usize + new_offset) as u64,
|
||||
)
|
||||
})
|
||||
.collect();
|
||||
|
||||
commit_patch(table);
|
||||
};
|
||||
|
||||
// On wasm, we need to download the module, compile it, and then run it.
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
wasm_bindgen_futures::spawn_local(async move {
|
||||
use js_sys::{
|
||||
ArrayBuffer, Object, Reflect,
|
||||
WebAssembly::{self, Memory, Table},
|
||||
};
|
||||
use wasm_bindgen::prelude::*;
|
||||
use wasm_bindgen::JsValue;
|
||||
use wasm_bindgen::UnwrapThrowExt;
|
||||
use wasm_bindgen_futures::JsFuture;
|
||||
|
||||
let funcs: Table = wasm_bindgen::function_table().unchecked_into();
|
||||
let memory: Memory = wasm_bindgen::memory().unchecked_into();
|
||||
let exports: Object = wasm_bindgen::exports().unchecked_into();
|
||||
let buffer: ArrayBuffer = memory.buffer().unchecked_into();
|
||||
|
||||
let path = table.lib.to_str().unwrap();
|
||||
if !path.ends_with(".wasm") {
|
||||
return;
|
||||
}
|
||||
|
||||
// Start the fetch of the module
|
||||
let response = web_sys::window().unwrap_throw().fetch_with_str(&path);
|
||||
|
||||
// Wait for the fetch to complete - we need the wasm module size in bytes to reserve in the memory
|
||||
let response: web_sys::Response = JsFuture::from(response).await.unwrap().unchecked_into();
|
||||
let dl_bytes: ArrayBuffer = JsFuture::from(response.array_buffer().unwrap())
|
||||
.await
|
||||
.unwrap()
|
||||
.unchecked_into();
|
||||
|
||||
// Expand the memory and table size to accommodate the new data and functions
|
||||
//
|
||||
// Normally we wouldn't be able to trust that we are allocating *enough* memory
|
||||
// for BSS segments, but ld emits them in the binary when using import-memory.
|
||||
//
|
||||
// Make sure we align the memory base to the page size
|
||||
const PAGE_SIZE: u32 = 64 * 1024;
|
||||
let page_count = (buffer.byte_length() as f64 / PAGE_SIZE as f64).ceil() as u32;
|
||||
let memory_base = (page_count + 1) * PAGE_SIZE;
|
||||
|
||||
// We need to grow the memory to accommodate the new module
|
||||
memory.grow((dl_bytes.byte_length() as f64 / PAGE_SIZE as f64).ceil() as u32 + 1);
|
||||
|
||||
// We grow the ifunc table to accommodate the new functions
|
||||
// In theory we could just put all the ifuncs in the jump map and use that for our count,
|
||||
// but there's no guarantee from the jump table that it references "itself"
|
||||
// We might need a sentinel value for each ifunc in the jump map to indicate that it is
|
||||
let table_base = funcs.grow(table.ifunc_count as u32).unwrap();
|
||||
|
||||
// Adjust the jump table to be relative to the new base address
|
||||
for v in table.map.values_mut() {
|
||||
*v += table_base as u64;
|
||||
}
|
||||
|
||||
// Build up the import object. We copy everything over from the current exports, but then
|
||||
// need to add in the memory and table base offsets for the relocations to work.
|
||||
//
|
||||
// let imports = {
|
||||
// env: {
|
||||
// memory: base.memory,
|
||||
// __tls_base: base.__tls_base,
|
||||
// __stack_pointer: base.__stack_pointer,
|
||||
// __indirect_function_table: base.__indirect_function_table,
|
||||
// __memory_base: memory_base,
|
||||
// __table_base: table_base,
|
||||
// ..base_exports
|
||||
// },
|
||||
// };
|
||||
let env = Object::new();
|
||||
|
||||
// Move memory, __tls_base, __stack_pointer, __indirect_function_table, and all exports over
|
||||
for key in Object::keys(&exports) {
|
||||
Reflect::set(&env, &key, &Reflect::get(&exports, &key).unwrap()).unwrap();
|
||||
}
|
||||
|
||||
// Set the memory and table in the imports
|
||||
// Following this pattern: Global.new({ value: "i32", mutable: false }, value)
|
||||
for (name, value) in [("__table_base", table_base), ("__memory_base", memory_base)] {
|
||||
let descriptor = Object::new();
|
||||
Reflect::set(&descriptor, &"value".into(), &"i32".into()).unwrap();
|
||||
Reflect::set(&descriptor, &"mutable".into(), &false.into()).unwrap();
|
||||
let value = WebAssembly::Global::new(&descriptor, &value.into()).unwrap();
|
||||
Reflect::set(&env, &name.into(), &value.into()).unwrap();
|
||||
}
|
||||
|
||||
// Set the memory and table in the imports
|
||||
let imports = Object::new();
|
||||
Reflect::set(&imports, &"env".into(), &env).unwrap();
|
||||
|
||||
// Download the module, returning { module, instance }
|
||||
// we unwrap here instead of using `?` since this whole thing is async
|
||||
let result_object = JsFuture::from(WebAssembly::instantiate_module(
|
||||
dl_bytes.unchecked_ref(),
|
||||
&imports,
|
||||
))
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
// We need to run the data relocations and then fire off the constructors
|
||||
let res: Object = result_object.unchecked_into();
|
||||
let instance: Object = Reflect::get(&res, &"instance".into())
|
||||
.unwrap()
|
||||
.unchecked_into();
|
||||
let exports: Object = Reflect::get(&instance, &"exports".into())
|
||||
.unwrap()
|
||||
.unchecked_into();
|
||||
_ = Reflect::get(&exports, &"__wasm_apply_data_relocs".into())
|
||||
.unwrap()
|
||||
.unchecked_into::<js_sys::Function>()
|
||||
.call0(&JsValue::undefined());
|
||||
_ = Reflect::get(&exports, &"__wasm_apply_global_relocs".into())
|
||||
.unwrap()
|
||||
.unchecked_into::<js_sys::Function>()
|
||||
.call0(&JsValue::undefined());
|
||||
_ = Reflect::get(&exports, &"__wasm_call_ctors".into())
|
||||
.unwrap()
|
||||
.unchecked_into::<js_sys::Function>()
|
||||
.call0(&JsValue::undefined());
|
||||
|
||||
unsafe { commit_patch(table) };
|
||||
});
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[derive(Debug, PartialEq, thiserror::Error)]
|
||||
pub enum PatchError {
|
||||
/// The patch failed to apply.
|
||||
///
|
||||
/// This returns a string instead of the Dlopen error type so we don't need to bring the libloading
|
||||
/// dependency into the public API.
|
||||
#[error("Failed to load library: {0}")]
|
||||
Dlopen(String),
|
||||
|
||||
/// The patch failed to apply on Android, most likely due to a permissions issue.
|
||||
#[error("Failed to load library on Android: {0}")]
|
||||
AndroidMemfd(String),
|
||||
}
|
||||
|
||||
/// This function returns its own address, providing a stable reference point for hot-patch engine
|
||||
/// to hook onto. If you were to write an object file for this function, it would amount to:
|
||||
///
|
||||
/// ```asm
|
||||
/// __aslr_reference:
|
||||
/// mov rax, qword ptr [rip + __aslr_reference@GOTPCREL] // notice the @GOTPCREL relocation
|
||||
/// ret
|
||||
/// ```
|
||||
///
|
||||
/// The point here being that we have a stable address both at runtime and compile time, making it
|
||||
/// possible to calculate the ASLR offset from within the process to correct the jump table.
|
||||
#[doc(hidden)]
|
||||
#[inline(never)]
|
||||
#[no_mangle]
|
||||
pub extern "C" fn __aslr_reference() -> usize {
|
||||
__aslr_reference as *const () as usize
|
||||
}
|
||||
|
||||
/// On Android, we can't dlopen libraries that aren't placed inside /data/data/<package_name>/lib/
|
||||
///
|
||||
/// If the device isn't rooted, then we can't push the library there.
|
||||
/// This is a workaround to copy the library to a memfd and then dlopen it.
|
||||
///
|
||||
/// I haven't tested it on device yet, so if if it doesn't work, then we can simply revert to using
|
||||
/// "adb root" and then pushing the library to the /data/data folder instead of the tmp folder.
|
||||
///
|
||||
/// Android provides us a flag when calling dlopen to use a file descriptor instead of a path, presumably
|
||||
/// because they want to support this.
|
||||
/// - https://developer.android.com/ndk/reference/group/libdl
|
||||
/// - https://developer.android.com/ndk/reference/structandroid/dlextinfo
|
||||
#[cfg(target_os = "android")]
|
||||
unsafe fn android_memmap_dlopen(file: &std::path::Path) -> Result<libloading::Library, PatchError> {
|
||||
use std::ffi::{c_void, CStr, CString};
|
||||
use std::os::fd::{AsRawFd, BorrowedFd};
|
||||
use std::ptr;
|
||||
|
||||
#[repr(C)]
|
||||
struct ExtInfo {
|
||||
flags: u64,
|
||||
reserved_addr: *const c_void,
|
||||
reserved_size: libc::size_t,
|
||||
relro_fd: libc::c_int,
|
||||
library_fd: libc::c_int,
|
||||
library_fd_offset: libc::off64_t,
|
||||
library_namespace: *const c_void,
|
||||
}
|
||||
|
||||
extern "C" {
|
||||
fn android_dlopen_ext(
|
||||
filename: *const libc::c_char,
|
||||
flags: libc::c_int,
|
||||
ext_info: *const ExtInfo,
|
||||
) -> *const c_void;
|
||||
}
|
||||
|
||||
use memmap2::MmapAsRawDesc;
|
||||
use std::os::unix::prelude::{FromRawFd, IntoRawFd};
|
||||
|
||||
let contents = std::fs::read(file)
|
||||
.map_err(|e| PatchError::AndroidMemfd(format!("Failed to read file: {}", e)))?;
|
||||
let mut mfd = memfd::MemfdOptions::default()
|
||||
.create("subsecond-patch")
|
||||
.map_err(|e| PatchError::AndroidMemfd(format!("Failed to create memfd: {}", e)))?;
|
||||
mfd.as_file()
|
||||
.set_len(contents.len() as u64)
|
||||
.map_err(|e| PatchError::AndroidMemfd(format!("Failed to set memfd length: {}", e)))?;
|
||||
|
||||
let raw_fd = mfd.into_raw_fd();
|
||||
|
||||
let mut map = memmap2::MmapMut::map_mut(raw_fd)
|
||||
.map_err(|e| PatchError::AndroidMemfd(format!("Failed to map memfd: {}", e)))?;
|
||||
map.copy_from_slice(&contents);
|
||||
let map = map
|
||||
.make_exec()
|
||||
.map_err(|e| PatchError::AndroidMemfd(format!("Failed to make memfd executable: {}", e)))?;
|
||||
|
||||
let filename = c"/subsecond-patch";
|
||||
|
||||
let info = ExtInfo {
|
||||
flags: 0x10, // ANDROID_DLEXT_USE_LIBRARY_FD
|
||||
reserved_addr: ptr::null(),
|
||||
reserved_size: 0,
|
||||
relro_fd: 0,
|
||||
library_fd: raw_fd,
|
||||
library_fd_offset: 0,
|
||||
library_namespace: ptr::null(),
|
||||
};
|
||||
|
||||
let flags = libloading::os::unix::RTLD_LAZY | libloading::os::unix::RTLD_LOCAL;
|
||||
|
||||
let handle = libloading::os::unix::with_dlerror(
|
||||
|| {
|
||||
let ptr = android_dlopen_ext(filename.as_ptr() as _, flags, &info);
|
||||
if ptr.is_null() {
|
||||
return None;
|
||||
} else {
|
||||
return Some(ptr);
|
||||
}
|
||||
},
|
||||
|err| err.to_str().unwrap_or_default().to_string(),
|
||||
)
|
||||
.map_err(|e| {
|
||||
PatchError::AndroidMemfd(format!(
|
||||
"android_dlopen_ext failed: {}",
|
||||
e.unwrap_or_default()
|
||||
))
|
||||
})?;
|
||||
|
||||
let lib = unsafe { libloading::os::unix::Library::from_raw(handle as *mut c_void) };
|
||||
let lib: libloading::Library = lib.into();
|
||||
Ok(lib)
|
||||
}
|
||||
|
||||
/// A trait that enables types to be hot-patched.
|
||||
///
|
||||
/// This trait is only implemented for FnMut types which naturally includes function pointers and
|
||||
/// closures that can be re-ran. FnOnce closures are currently not supported since the hot-patching
|
||||
/// system we use implies that the function can be called multiple times.
|
||||
pub trait HotFunction<Args, Marker> {
|
||||
/// The return type of the function.
|
||||
type Return;
|
||||
|
||||
/// The real function type. This is meant to be a function pointer.
|
||||
/// When we call `call_as_ptr`, we will transmute the function to this type and call it.
|
||||
type Real;
|
||||
|
||||
/// Call the HotFunction with the given arguments.
|
||||
///
|
||||
/// # Why
|
||||
///
|
||||
/// "rust-call" isn't stable, so we wrap the underlying call with our own, giving it a stable vtable entry.
|
||||
/// This is more important than it seems since this function becomes "real" and can be hot-patched.
|
||||
fn call_it(&mut self, args: Args) -> Self::Return;
|
||||
|
||||
/// Call the HotFunction as if it were a function pointer.
|
||||
///
|
||||
/// # Safety
|
||||
///
|
||||
/// This is only safe if the underlying type is a function (function pointer or virtual/fat pointer).
|
||||
/// Using this will use the JumpTable to find the patched function and call it.
|
||||
unsafe fn call_as_ptr(&mut self, _args: Args) -> Self::Return;
|
||||
}
|
||||
|
||||
macro_rules! impl_hot_function {
|
||||
(
|
||||
$(
|
||||
($marker:ident, $($arg:ident),*)
|
||||
),*
|
||||
) => {
|
||||
$(
|
||||
/// A marker type for the function.
|
||||
/// This is hidden with the intention to seal this trait.
|
||||
#[doc(hidden)]
|
||||
pub struct $marker;
|
||||
|
||||
impl<T, $($arg,)* R> HotFunction<($($arg,)*), $marker> for T
|
||||
where
|
||||
T: FnMut($($arg),*) -> R,
|
||||
{
|
||||
type Return = R;
|
||||
type Real = fn($($arg),*) -> R;
|
||||
|
||||
fn call_it(&mut self, args: ($($arg,)*)) -> Self::Return {
|
||||
#[allow(non_snake_case)]
|
||||
let ( $($arg,)* ) = args;
|
||||
self($($arg),*)
|
||||
}
|
||||
|
||||
unsafe fn call_as_ptr(&mut self, args: ($($arg,)*)) -> Self::Return {
|
||||
unsafe {
|
||||
if let Some(jump_table) = get_jump_table() {
|
||||
|
||||
|
||||
let real = std::mem::transmute_copy::<Self, Self::Real>(&self) as *const ();
|
||||
|
||||
// Android implements MTE / pointer tagging and we need to preserve the tag.
|
||||
// If we leave the tag, then indexing our jump table will fail and patching won't work (or crash!)
|
||||
// This is only implemented on 64-bit platforms since pointer tagging is not available on 32-bit platforms
|
||||
// In dev, Dioxus disables MTE to work around this issue, but we still handle it anyways.
|
||||
#[cfg(all(target_pointer_width = "64", target_os = "android"))] let nibble = real as u64 & 0xFF00_0000_0000_0000;
|
||||
#[cfg(target_pointer_width = "64")] let real = real as u64 & 0x00FFF_FFF_FFFF_FFFF;
|
||||
|
||||
#[cfg(target_pointer_width = "64")] let real = real as u64;
|
||||
|
||||
// No nibble on 32-bit platforms, but we still need to assume u64 since the host always writes 64-bit addresses
|
||||
#[cfg(target_pointer_width = "32")] let real = real as u64;
|
||||
|
||||
if let Some(ptr) = jump_table.map.get(&real).cloned() {
|
||||
// Re-apply the nibble - though this might not be required (we aren't calling malloc for a new pointer)
|
||||
#[cfg(all(target_pointer_width = "64", target_os = "android"))] let ptr: u64 = ptr | nibble;
|
||||
|
||||
#[cfg(target_pointer_width = "64")] let ptr: u64 = ptr;
|
||||
#[cfg(target_pointer_width = "32")] let ptr: u32 = ptr as u32;
|
||||
|
||||
// Macro-rules requires unpacking the tuple before we call it
|
||||
#[allow(non_snake_case)]
|
||||
let ( $($arg,)* ) = args;
|
||||
|
||||
|
||||
#[cfg(target_pointer_width = "64")]
|
||||
type PtrWidth = u64;
|
||||
#[cfg(target_pointer_width = "32")]
|
||||
type PtrWidth = u32;
|
||||
|
||||
return std::mem::transmute::<PtrWidth, Self::Real>(ptr)($($arg),*);
|
||||
}
|
||||
}
|
||||
|
||||
self.call_it(args)
|
||||
}
|
||||
}
|
||||
}
|
||||
)*
|
||||
};
|
||||
}
|
||||
|
||||
impl_hot_function!(
|
||||
(Fn0Marker,),
|
||||
(Fn1Marker, A),
|
||||
(Fn2Marker, A, B),
|
||||
(Fn3Marker, A, B, C),
|
||||
(Fn4Marker, A, B, C, D),
|
||||
(Fn5Marker, A, B, C, D, E),
|
||||
(Fn6Marker, A, B, C, D, E, F),
|
||||
(Fn7Marker, A, B, C, D, E, F, G),
|
||||
(Fn8Marker, A, B, C, D, E, F, G, H),
|
||||
(Fn9Marker, A, B, C, D, E, F, G, H, I)
|
||||
);
|
||||
Reference in new issue
Block a user