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;
    }
}