1# Chapter 27: hotpatch, editing a running program 2 3Chapter 15 introduced [Dioxus subsecond](https://github.com/DioxusLabs/dioxus): 4a library that, inside a running Rust process, can redirect calls to a 5function to a new version of it loaded from a shared object. Hot reloading, 6for native code. The catch is building that shared object. Its own tool, 7`dx`, makes itself the compiler wrapper and the linker so it can capture 8every invocation and replay pieces of them later. 9 10This project already has something that knows exactly what changed and how 11to rebuild it: buck2. So `hotpatch` does only the part buck2 genuinely 12cannot. Given the running binary and the objects buck2 just rebuilt, it 13resolves every symbol the new code needs that exists only in the *running* 14process (not in any file on disk, since nothing was relinked), links the 15new objects into a `.so`, and writes a `JumpTable` saying which old 16function addresses now mean what. The running program loads the `.so` and 17from its next call onwards runs the new code. 18 19```mermaid 20sequenceDiagram 21 participant P as patch.sh 22 participant B as buck2 23 participant H as hotpatch 24 participant W as the running window 25 P->>B: rebuild //apps/native:app and packages/panels (objects only) 26 P->>W: patch_info (MCP): pid, aslr_reference 27 P->>H: --base /proc/PID/exe --objects ... --aslr-reference ... 28 H-->>P: patch-N.so and patch-N.json 29 P->>W: dev_mode on, hot_patch patch-N.json, dev_mode off 30 W->>W: applies it at the next frame boundary 31``` 32 33> **Aside: the executable is never rebuilt.** Relinking the window's whole 34> eframe/wgpu binary took about 21 seconds, over 97% of every cycle. So the 35> window's code is a library (`//apps/native:app`) behind a three-line 36> `main.rs`, and the patch is built from that library's fresh objects. The 37> "base" is `/proc/$PID/exe`, the exact bytes the process is running, as the 38> operating system reports them. A panels edit now lands in about 1.1 to 1.6 39> seconds. 40 41> **Try it.** With chapter 15's `hotdemo` running and logging to 42> `run/hotdemo.log`: 43> 44> nix develop 45> tools/hotpatch/patch.sh //apps/hotdemo:hotdemo 46> 47> or, with the window running, `tools/hotpatch/patch.sh //apps/native:native` 48> after editing a label in `packages/panels/src/state.rs`. 49 50## For the people who maintain it 51 52### The tool on its own 53 54```bash 55nix develop -c buck2 run //tools/hotpatch:hotpatch -- \ 56 --base <path to the base exe> \ 57 --objects <one or more .o/.rlib paths> \ 58 --aslr-reference <the running process's subsecond::aslr_reference(), hex or decimal> \ 59 --out-dir <where to write patch-N.so / patch-N.json> 60``` 61 62writes `patch-<n>.so` (the shared object) and `patch-<n>.json` (the 63`JumpTable`, ready for `subsecond::apply_patch`). Linux/x86-64 ELF only. It 64ports the logic (not the code) of `create_undefined_symbol_stub` and 65`create_native_jump_table` from `dioxus-cli`'s `patch.rs` at tag `v0.7.10`; 66`src/main.rs`'s header has the licence notice. 67 68### `patch.sh`, the whole cycle in one command 69 70Run it from inside `nix develop`: it checks `IN_NIX_SHELL` and only 71re-enters the devshell per `buck2`/`curl` call when it is not already in 72one, which used to be most of the cycle time. 73 74For `//apps/hotdemo:hotdemo` (already running and logging to 75`run/hotdemo.log`): it rebuilds the target, finds its fresh `.rcgu.o` 76objects, reads the pid and `aslr_reference` out of the log, runs `hotpatch`, 77and drops the result at `run/hotdemo.patch.json`, which hotdemo picks up on 78its next once-a-second check. 79 80For `//apps/native:native` (the window): the same rebuild, but the pid and 81`aslr_reference` come from its `patch_info` MCP tool, and the patch is 82delivered by turning `dev_mode` on, calling `hot_patch` with the new 83`patch-N.json`, and turning `dev_mode` off, all over the window's own MCP 84endpoint (`http://127.0.0.1:7637/mcp`, session lifecycle: `initialize` gets 85an `Mcp-Session-Id` header back, every further call carries it). The window 86applies the patch at its next frame boundary. 87 88`patch.sh` also folds in every first-party (`//apps/...` or `//packages/...`) 89dependency of the target, transitively, that is itself built with 90`-Csave-temps=true` (`packages/panels` is; `packages/alttp`, 91`packages/console` and `packages/mcp` are not, and are skipped without being 92built). 93 94A tip target is one of three shapes, told apart by its own and its 95dependencies' `-Csave-temps=true`, never by name: 96 971. Has save-temps and no hot-patch-flagged dependency (`apps/hotdemo`): it 98 is the whole patchable crate, rebuilt directly. 992. Has save-temps and delegates to one (`apps/native:native` to 100 `apps/native:app`): it contributes only its stale on-disk "main" sentinel 101 object, never rebuilt. `subsecond::apply_patch` looks up a symbol named 102 `main` in every patch to find where the `.so` landed, and only `main.rs`'s 103 own object has it. 1043. No save-temps of its own: it contributes nothing directly; every object 105 comes from its dependencies. 106 107Where a target's objects land is read from buck2's own record of the rustc 108invocation (the `--out-dir=` in `<crate>-link-diag.args`, found under the 109target's output directory), not guessed from buck2's internal directory 110codes, which differ by target kind and buck2 version (measured 2026-09-20: 111`XIPL` for a `rust_binary`, `LPPL` for a library's `[static_pic]`). 112 113### Cycle time (measured 2026-09-20, in-shell, against the real window) 114 115| Edit | Full relink every time | Now | 116| --- | --- | --- | 117| `packages/panels/src/state.rs` | ~21.3 s | ~1.1 to 1.6 s | 118| `apps/native/src/app.rs` | ~20.8 s | ~4.0 s | 119 120`app[static_pic]` alone still costs ~3.5 s to compile, flat whether the 121toolchain's `-Copt-level=3` is overridden to 1 or `-Ccodegen-units=16` on 122that target: the cost is LLVM codegen against egui/eframe's generic 123surface, so `-Copt-level=3` stays project-wide. Other numbers: `nix develop 124-c true` costs 3.4 to 6.7 s per invocation; a no-change `buck2 build` ~17 ms; 125`hotpatch`'s own link and jump table ~1.8 s for the window, under a second 126for hotdemo. 127 128### What it depends on 129 130- **`//third-party/rust:subsecond` carries its own `-Cdebug-assertions=yes`** 131 (`third-party/rust/fixups/subsecond/fixups.toml`), separately from the 132 patched target's flags: `subsecond::call`'s `cfg!(debug_assertions)` gate 133 resolves against subsecond's own compilation 134 ([research/subsecond-patch-build.md](../../research/subsecond-patch-build.md) §8.1). 135- **The patched function needs `#[inline(never)]`, and must reach 136 `subsecond::call`/`subsecond::HotFn` as a bare `fn` item, not a capturing 137 closure** (§8.2; `apps/hotdemo/CLAUDE.md`, `apps/native/CLAUDE.md`). 138- **`-nodefaultlibs` does not leave `memcpy` dangling.** The stub only 139 resolves names the base binary's own symbol table defines. Ordinary libc 140 functions were never statically linked into the base to begin with, so 141 they stay ordinary undefined dynamic symbols in the patch, resolved by the 142 loader at `dlopen` time like any shared object's. 143 144### In this folder 145 146| Path | What | 147| --- | --- | 148| [src/main.rs](src/main.rs) | `hotpatch`: the undefined-symbol stub (thread-local init bytes included), the link, the jump table. | 149| [patch.sh](patch.sh) | The whole rebuild-and-patch cycle, for hotdemo or the window. | 150| [BUCK](BUCK) | The `rust_binary`, over `object`, `ar`, `libc`, `serde_json` and `subsecond-types`. | 151| [README.md](README.md) | This chapter. | 152| [CLAUDE.md](CLAUDE.md) | The silent-failure traps, for agents. | 153 154← Previous: [Chapter 26, watch/](../watch/) · Up: [tools](../) · Next: [Chapter 28, nix/](../../nix/) →