An example of "hot-loading" code in rust during development.
Switch branches/tags
Nothing to show
Clone or download
Lokathor Move from CC0 to Unlicense
Not a real change (both Public Domain) but github recognizes Unlicense over CC0.
Latest commit 9ae0f8b Oct 28, 2017
Permalink
Type Name Latest commit message Commit time
Failed to load latest commit information.
src comments cut down, bad copypasta Jun 2, 2017
.gitignore Initial commit May 16, 2017
Cargo.toml done May 20, 2017
LICENSE
README.md readme matches the rest of the code now Jun 20, 2017
rustfmt.toml partial progress May 18, 2017

README.md

License 100% unsafe

This project is released under the Creative Commons 0 license. Everyone should use it as much as they want.

This whole approach used here is directly inspired by Casey Muratori and his Handmade Hero project, specifically Day 21, Day 22, and even Day 23 if you like.

Additional credit goes to WindowsBunny for the great work on winapi of course.

hotload-win32-rs

This project is a minimal example of "hot-loading" code in rust during development (as requested here). It only works on Windows, but there are other repos where you can see how to hotload on Mac and Linux. I haven't looked too closely into the other two projects, but I expect the general design to be similar, and to revolve around loading and unloading a DLL and the related bookwork.

Because this is a tricky subject, the normal "just look at the code #yolo" approach isn't too much help. Actually it would be borderline irresponsible. I don't know how much you already know when you come here, so I'll try to explain as much as I can. Like with most of programming, it's ultimately not hard, just very precise.

Fair Warning: Everything here flies in the face of the absolute memory safety that Rust normally strives for, and it will probably get you funny looks among the Rust Community if you talk about it.

Here be Dragons, This is Dark Magic, etc, etc.

Architecture

So let's look at some facts:

So all we have to do is make Rust produce a C-compatible DLL with unmangled names that doesn't rely on any of its own allocated memory lasting between reloads.

  • The cdylib compilation target lets us make the DLL type we need.
  • The #[no_mangle] directive on a function gives it a normal name within the DLL, at the cost that the name is no longer namespaced at all.
  • All we have to do ourselves is carefully control how data flows across that DLL boundry, and make sure that the DLL isn't allocating any memory that's important. Ideally, the DLL shouldn't allocate any memory at all.

So we'll write our program as a cargo library which compiles into a DLL and also as a cargo binary which is the "platform layer" that manages all the outside world stuff. We will need a very clean separation between the "Platform Layer" portion of the program and the "Library Layer" portion of the program.

[package]
name = "hotload-win32"
version = "0.1.0"
authors = ["Lokathor <zefria@gmail.com>"]
publish = false
license-file = "LICENSE.txt"

[dependencies]
winapi = "0.2"
kernel32-sys = "0.2"

[lib]
name = "hotload_win32"
path = "src/hotload.rs"
crate-type = ["cdylib"]

[[bin]]
name = "hotload_platform"
path = "src/bin/main.rs"

And the files themselves are arranged like this

src/hotload.rs -- the library layer
src/data_types.rs -- the data types the two portions share
src/bin/main.rs -- the platform layer program.

The trouble is that if the platform potion is compiled directly against our cargo library it'll all get static linked together, which is what we don't wnat. So how will we get the driver program to still know what the data types it'll be using are? At first I used a symlink, but then CasualX explained that you can use a simple compiler directive, #[path = "../data_types.rs"], to make the binary file look up a directory from where it's stored to get the right file.

Data Types

This is where we put our defintions for all the types. It's the most important that we put all the types that will be shared between the Platform and Library layer here (structs for abstracted control input, etc). However, we also can't change the memory layout of any other struct while the program is running anyway (at least, not without a whole lot more work for versioning our data and having version upgrade funcs and such). So we'll also put our data types that are "private" to the library in here as well.

Importantly, one of the values passed to the Libarary portion each frame has a pointer to a block of memory that was allocated by the system for the library to use during that frame of the game. If we wanted to be fancier we'd split it up into "permanent storage" and "transient storage" or something like that, and then the library would be required to work based off of just the permanent storage and rebuild whatever was missing from transient storage and so on.

So our convention will be that the GameState value will be pointed to directly by the pointer in the GameMemory struct, and then if the size is big enough we'll have spare stuff left over and we can use that space as scratch space.

Note that since this is rust we can also safely use any of our normal things that allocate (like Vec<T>). As long as we keep them as local variables of the function and don't assign them into the GameMemory value they'll get dropped at the end of the function call into the Library and it'll all be cleared by the Drop trait like normal. That part of the setup is up to you.

Also Note that if your library needs to access the outside world during a frame (such as to read a file) you can put a callback in the GameMemory value for it to activate.

Library

This portion is where we have the function that gets called to update the game state each frame. It probably shouldn't be making drawing calls itself, it should just render the pixels to a provided backbuffer and then the Platform layer can blit that to the screen at the end, or swap SDL/OGL buffers, etc.

In our example, the library just takes the input and mangles it in some way into the output. Just as a demo that the library is having some effect, and that you can recompile and have a different effect.

Platform

This portion is where you gather user inputs and give the game's outputs. This could use gluim, piston, platform drawing calls, whatever you like.

Plus, of course, all the stuff about the DLL loading and unloading.

All Together

Once you stick it all together, you end up with being able to reload the game library on the fly. Your output session might look something like this:

D:\dev\hotload-win32-rs>cargo run
   Compiling hotload-win32 v0.1.0 (file:///D:/dev/hotload-win32-rs)
    Finished dev [unoptimized + debuginfo] target(s) in 0.41 secs
     Running `target\debug\hotload_platform.exe`
Loading The Initial Game DLL.
Setting up our Game Memory.
Entering The Main Loop.
['A', 'B', 'C', 'D', 'E', 'A', 'B', 'C', 'D', 'E']
['A', 'B', 'C', 'D', 'E', 'A', 'B', 'C', 'D', 'E']
['A', 'B', 'C', 'D', 'E', 'A', 'B', 'C', 'D', 'E']
['A', 'B', 'C', 'D', 'E', 'A', 'B', 'C', 'D', 'E']
['A', 'B', 'C', 'D', 'E', 'A', 'B', 'C', 'D', 'E']
['A', 'B', 'C', 'D', 'E', 'A', 'B', 'C', 'D', 'E']
The current DLL has a different file time, Reloading!
['a', 'b', 'c', 'd', 'e', 'a', 'b', 'c', 'd', 'e']
['a', 'b', 'c', 'd', 'e', 'a', 'b', 'c', 'd', 'e']

Of course, the DLL changed because in another terminal window I used cargo build --lib to rebuild things.