1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257
//! # dymod //! //! This crate provides a macro, `dymod!`, which allows you to specify a Rust module which will by dynamically loaded and hotswapped in debug mode, but statically linked in release mode. //! //! Note that this is _very_ much experimental. The current version of this crate is very opinionated about how you structure your dynamic code. Hopefully this will be relaxed a little in future. //! //! //! ## Usage //! //! Your dynamically loaded code should be placed in its own sub-crate under your main crate: //! //! ``` //! mycrate/ //! Cargo.toml //! src/ //! main.rs //! subcrate/ //! Cargo.toml //! src/ //! lib.rs //! ``` //! //! Your subcrate must also be compiled as a dylib, so in your `subcrate/Cargo.toml` add: //! //! ```toml //! [lib] //! crate-type = ["dylib"] //! ``` //! //! Now you need to add the code that you want to hotswap. Any functions should be `pub` and `#[no_mangle]`. See the [Limitations]("#limitations") section below for what kind of code you can put here. //! //! ```rust //! // subcrate/src/lib.rs //! //! #[no_mangle] //! pub fn count_sheep(sheep: u32) -> &'static str //! { //! match sheep //! { //! 0 => "None", //! 1 => "One", //! 2 => "Two", //! 3 => "Many", //! _ => "Lots" //! } //! } //! ``` //! //! Finally, use the `dymod!` macro to specify your module, along with the functions that are dynamically available from it. //! //! ```rust //! // mycrate/src/main.rs //! //! #[macro_use] //! extern crate dymod; //! //! dymod! //! { //! #[path = "../subcrate/src/lib.rs"] //! pub mod subcrate //! { //! fn count_sheep(sheep: u32) -> &'static str; //! } //! } //! //! fn main() //! { //! assert_eq!(subcrate::count_sheep(3), "Many"); //! loop //! { //! // You can now edit the count_sheep function and see //! // the results change while this code is running. //! println!("{}", subcrate::count_sheep(3)); //! } //! } //! ``` //! //! ## Safety //! //! This is really, really unsafe! But only in debug mode. In release mode, the module you specify is linked statically as if it was a regular module, and there should be no safety concerns. //! //! Here is a partial list of what can go wrong in debug mode: //! //! - If you are holding on to data owned by the dylib when the dylib is hotswapped, you will get undefined behaviour. //! - If you take ownership of any data allocated by the dylib, dropping that data will probably cause a segfault. //! - If you change the definition of a struct on either side of the boundary, you could get undefined behaviour //! //! //! ## Limitations //! //! So as described above, you cannot rely on hotswapping to work if you change struct definitions while your code is running. //! //! You also cannot reliably take ownership of heap allocated data from one side of the boundary to the other. //! //! Generic functions will not work either. //! //! This is again, just a partial list. There really are quite a lot of constraints on what you can do. //! //! //! ## So what is this actually good for then? //! //! I suppose we'll see! //! //! Here are some examples of code that should work and would be useful to hotswap: //! //! ```rust //! #[no_mangle] //! pub fn game_update(state: &mut GameState) //! { //! // Modify game state. //! // No need to return anything problematic. //! unimplemented!() //! } //! //! #[no_mangle] //! pub fn animate_from_to(point_a: [f32; 2], point_b: [f32; 2], time: f32) -> [f32; 2] //! { //! // Returns only stack-allocated values and so is safe. //! // Specific kind of animation can be changed on the fly. //! unimplemented!() //! } //! //! #[no_mangle] //! pub fn get_configuration() -> Config //! { //! // Again, returns only stack-allocated values. //! // Allows changing some configuration while running. //! Config //! { //! ... //! } //! } //! ``` #[cfg(debug_assertions)] extern crate sharedlib; #[cfg(debug_assertions)] pub use sharedlib::{Lib, Func, Symbol}; /// Takes a module definition and allows it to be hotswapped in debug mode. /// /// # Examples /// /// ```rust,ignored /// dymod! /// { /// #[path = "../subcrate/src/lib.rs"] /// pub mod subcrate /// { /// fn count_sheep(sheep: u32) -> &'static str; /// } /// } /// ``` /// /// This creates a module with a single function, `count_sheep`. In debug mode, this function /// will call into the dynamically loaded `subcrate` dylib. If that crate is recompiled, this /// function will use the updated code. /// /// In release mode, this module becomes just a regular Rust module with the contents of /// `../subcrate/src/lib.rs`. No dynamic linking is performed at all, and the functions are as /// safe as if they were included normally in this crate. /// /// # Panics /// /// Panics can occur _only_ in debug mode as a result of the various pitfalls of dynamic /// linking. These can be the result of: /// /// 1. Dropping data which was allocated in the other library. /// 2. Holding onto references to data that is dropped when the dylib is hotswapped. /// 3. Changing the definition of a struct that is passed to or from the other library. /// 4. Very many other things. /// /// These problems should all disappear in release mode, where this code is just statically /// linked as normal. /// /// # Safety /// /// As above, dynamic linking is inherently unsafe. In debug mode, these things can cause a /// variety of undefined behaviour. For example, see [`sharedlib`](https://docs.rs/sharedlib/7.0.0/sharedlib/#pitfalls), which this crate uses internally. #[macro_export] macro_rules! dymod { ( #[path = $libpath: tt] pub mod $modname: ident { $(fn $fnname: ident ( $($argname: ident : $argtype: ty),* ) -> $returntype: ty;)* } ) => { #[cfg(debug_assertions)] pub mod $modname { use ::std::time::SystemTime; use ::std::path::PathBuf; use $crate::{Lib, Func, Symbol}; static mut DYLIB: Option<Lib> = None; static mut MODIFIED_TIME: Option<SystemTime> = None; fn load_lib() -> &'static Lib { unsafe { let dylibpath = { let mut path = PathBuf::from(env!("CARGO_MANIFEST_DIR")); path.push(stringify!($modname)); path.push("target/debug"); path.push(&format!("lib{}.dylib", stringify!($modname))); path }; let file_changed = { let metadata = ::std::fs::metadata(&dylibpath).unwrap(); let modified_time = metadata.modified().unwrap(); let changed = MODIFIED_TIME != Some(modified_time); MODIFIED_TIME = Some(modified_time); changed }; if DYLIB.is_none() || file_changed { // We need to drop the dylib before we reload it. { DYLIB = None; } let lib = Lib::new(&dylibpath).unwrap(); DYLIB = Some(lib); } DYLIB.as_ref().unwrap() } } $( pub fn $fnname($($argname: $argtype),*) -> $returntype { let lib = load_lib(); unsafe { let sym: Func<fn($($argtype),*) -> $returntype> = lib.find_func(stringify!($fnname)).unwrap(); let symfn = sym.get(); symfn($($argname),*) } } )* } #[cfg(not(debug_assertions))] #[path = $libpath] pub mod $modname; } }