ELDER•DEV
PostsMastodonTwitterGitHub

Rust Hotswap

dev:rust:

January 12th, 2015

⚠ Warning
This post is old! Rust has changed a lot since this post was written, it may not still be accurate.

Mozilla’s Rust language has just reached 1.0 alpha.

I’ve been learning it off and on for a while now, and I’m quite happy to see the breaking changes slow down. As part of learning rust I’ve played around implementing things that would be normally done in c or c++; one of those is the old trick of hot-swapping code by reloading a shared library at runtime.

The rest of this post assumes that you have a pretty fair knowledge of rust. If not, you should probably start with the rust book.

Project Setup

First we’ll set up a folder to develop from: mkdir ./rust-hotswap && cd ./rust-hotswap and a source dir: mkdir ./src.

We’ll use Cargo to build, so next we’ll create a Cargo.toml

Cargo.toml TOML
 1[package]
 2name = "rust-hotswap"
 3version = "0.0.1"
 4
 5[lib]
 6crate-type = ["dylib"]
 7name = "rust-hotswap"
 8path = "src/lib.rs"
 9
10[[bin]]
11name = "rust-hotswap"
12path = "src/main.rs"
13
14[dependencies]
15glob = "*"

This is a pretty generic project file, with one dependency on glob. We’ll discuss glob in a moment.

The key thing is that we specify that the library be built as a dylib so that we get a c style dynamic library.

Code

Next we’ll need our library source, this is the part we will swap out at runtime.

src/lib.rs Rust
 1use std::io;
 2use std::fmt;
 3
 4#[no_mangle]
 5pub extern "C" fn do_tick(num: i32) {
 6    let mut out = io::stdout();
 7
 8    let s = format!("Tick: {}", num);
 9    out.write_str(s.as_slice());
10    out.write_char('\n');
11
12    out.flush();
13}

We use #[no_mangle] to prevent rust from mangling the function name, and extern "C" to export it in the C linkage style. We use io::stdout() to get a handle to stdout instead of using the println! macro because the macro causes an illegal instruction error on exit. I’m not sure exactly why this occurs but i’m sure it has to do with unsafely sharing the stdout handle. It’s probably a good idea to do it this way anyhow, but I might want to look into that later.

Now to actually load the library.

The first important thing to know is that rustc appends a versioning hash to libraries it produces. We could possibly recreate this hash, or we could just look for a dynamic library next to the executable. To do this we use std::os::self_exe_path to get the directory the executable is in, then use it to create a search pattern for glob to find the dynamic library we built.

We have a small helper function get_dylib_path_pattern that appends a platform specific file extension using rust’s cfg conditional compilation and target_os.
After finding the library we can then use the (unstable!) std::dynamic_lib::DynamicLibrary api to load the library, and resolve the symbol for do_tick, which we can then call.

We’ll wrap up the loading and calling bits in a helper function.

Lastly we can throw in a println! and a keypress read between ticks to pause the program, and alert the user that it is now safe to modify the code. This allows you to finally do the following:

  1. Run: cargo run.
  2. Edit src/lib.rs to write something else to stdout.
  3. From another shell cargo build.
  4. Hit enter in the first shell after cargo build completes.
  5. The output this time should now match your modified code.

A real usecase for this would be EG game development, where the game is implemented as a small wrapper executable and the rest as methods in a dynamic library, you can pass in a struct full of globals to the library and between ticks reload the library with modified code. You might monitor the source for changes to the library, and rebuild and reload between ticks during development. In anycase, it’s a cool trick.

Full Source

src/main.rs Rust
 1extern crate glob;
 2
 3use std::io;
 4use glob::glob;
 5use std::dynamic_lib::DynamicLibrary;
 6use std::os::self_exe_path;
 7use std::mem::transmute;
 8
 9#[cfg(any(target_os = "linux",
10          target_os = "freebsd",
11          target_os = "dragonfly"))]
12fn get_dylib_path_pattern(dir: &str) -> String {
13    dir.to_string() + "/*.so"
14}
15
16#[cfg(target_os = "macos")]
17fn get_dylib_path_pattern(dir: &str) -> String {
18    dir.to_string() + "/*.dylib"
19}
20
21#[cfg(target_os = "windows")]
22fn get_dylib_path_pattern(dir: &str) -> String {
23    dir.to_string() + "/*.dll"
24}
25
26fn do_lib_tick(dylib_path_pattern: &str, num: i32) {
27    let mut paths = glob(dylib_path_pattern);
28
29    let path = paths.next().unwrap();
30    println!("path: {}", path.display());
31
32    let lib = match DynamicLibrary::open(Some(&path)) {
33        Ok(lib) => lib,
34        Err(e) => panic!("Failed to load library: {:?}", e)
35    };
36
37    let do_tick: extern "C" fn(i32) = unsafe {
38        match lib.symbol::<u8>("do_tick") {
39            Ok(do_tick) => transmute::<*mut u8, extern "C" fn(i32)>(do_tick),
40            Err(e) => panic!("Failed to load symbol: {:?}", e)
41        }
42    };
43    do_tick(num);
44}
45
46fn main() {
47    let dir = self_exe_path().unwrap();
48    println!("dir: {:?}", dir);
49
50    let dylib_path_pattern = get_dylib_path_pattern(dir.as_str().unwrap());
51    println!("pattern: {}", dylib_path_pattern);
52
53    do_lib_tick(dylib_path_pattern.as_slice(), 1);
54
55    println!("\nNow modify lib.rs, build, and then hit enter.");
56    let mut stdin = io::stdin();
57    stdin.read_char();
58
59    do_lib_tick(dylib_path_pattern.as_slice(), 2);
60}

src/lib.rs Rust
 1use std::io;
 2use std::fmt;
 3
 4#[no_mangle]
 5pub extern "C" fn do_tick(num: i32) {
 6    let mut out = io::stdout();
 7
 8    let s = format!("Tick: {}", num);
 9    out.write_str(s.as_slice());
10    out.write_char('\n');
11
12    out.flush();
13}

Cargo.toml TOML
 1[package]
 2name = "rust-hotswap"
 3version = "0.0.1"
 4
 5[lib]
 6crate-type = ["dylib"]
 7name = "rust-hotswap"
 8path = "src/lib.rs"
 9
10[[bin]]
11name = "rust-hotswap"
12path = "src/main.rs"
13
14[dependencies]
15glob = "*"