hotline_rs/
plugin.rs

1use crate::client::*;
2use crate::gfx;
3use crate::os;
4use crate::reloader;
5
6use std::process::ExitStatus; 
7use std::process::Command;
8use std::io::{self, Write};
9
10/// General dll plugin responder, will check for source code changes and run cargo build to re-build the library
11pub struct PluginReloadResponder {
12    /// Name of the plugin
13    pub name: String,
14    /// Path to the plugins build director, where you would run `cargo build -p <name>`
15    pub path: String,
16    /// Full path to the build binary dylib or dll
17    pub output_filepath: String,
18    /// Array of source code files to track and check for changes
19    pub files: Vec<String>
20}
21
22/// Public trait for defining a plugin in a another library implement this trait and instantiate it with `hotline_plugin!`
23pub trait Plugin<D: gfx::Device, A: os::App> {
24    /// Create a new instance of the plugin
25    fn create() -> Self where Self: Sized;
26    /// Called when the plugin is loaded and after a reload has happened, setup resources and state in here
27    fn setup(&mut self, client: Client<D, A>) -> Client<D, A>;
28    /// Called each and every frame, here put your update and render logic
29    fn update(&mut self, client: Client<D, A>) -> Client<D, A>;
30    // Called where it is safe to make imgui calls
31    fn ui(&mut self, client: Client<D, A>) -> Client<D, A>;
32    // Called when the plugin is to be unloaded, this will clean up
33    fn unload(&mut self, client: Client<D, A>) -> Client<D, A>;
34}
35
36/// Utility function to build all plugins, this can be used to bootstrap them if they don't exist
37pub fn build_all() {
38    let path = super::get_data_path("..");
39    let output = if super::get_config_name() == "release" {
40        Command::new("cargo")
41            .current_dir(path)
42            .arg("build")
43            .arg("--release")
44            .output()
45            .expect("hotline::hot_lib:: hot lib failed to build!")
46    }
47    else {
48        Command::new("cargo")
49            .current_dir(path)
50            .arg("build")
51            .output()
52            .expect("hotline::hot_lib:: hot lib failed to build!")
53    };
54
55    if !output.stdout.is_empty() {
56        println!("{}", String::from_utf8(output.stdout).unwrap());
57    }
58
59    if !output.stderr.is_empty() {
60        println!("{}", String::from_utf8(output.stderr).unwrap());
61    }
62}
63
64/// Reload responder implementation for `PluginLib` uses cargo build, and hot lib reloader
65impl reloader::ReloadResponder for PluginReloadResponder {
66    fn add_file(&mut self, path: &str) {
67        self.files.push(path.to_string());
68    }
69
70    fn get_files(&self) -> Vec<String> {
71        // scan for new files so we can dd them and pickup changes
72        // TODO; this could be more easily be configured in a plugin meta data file
73        let src_path = self.path.to_string() + "/" + &self.name.to_string() + "/src";
74        let src_files = super::get_files_recursive(&src_path, Vec::new());
75        let mut result = self.files.to_vec();
76        result.extend(src_files);
77        result
78    }
79
80    fn get_last_mtime(&self) -> std::time::SystemTime {
81        let meta = std::fs::metadata(&self.output_filepath);
82        if meta.is_ok() {
83            std::fs::metadata(&self.output_filepath).unwrap().modified().unwrap()
84        }
85        else {
86            std::time::SystemTime::now()
87        }
88    }
89
90    fn build(&mut self) -> ExitStatus {
91        let output = if super::get_config_name() == "release" {
92            Command::new("cargo")
93                .current_dir(&self.path)
94                .arg("build")
95                .arg("--release")
96                .arg("-p")
97                .arg(&self.name)
98                .output()
99                .expect("hotline::hot_lib:: hot lib failed to build!")
100        }
101        else {
102            Command::new("cargo")
103                .current_dir(&self.path)
104                .arg("build")
105                .arg("-p")
106                .arg(&self.name)
107                .output()
108                .expect("hotline::hot_lib:: hot lib failed to build!")
109        };
110
111        let mut stdout = io::stdout().lock();
112
113        if !output.stdout.is_empty() {
114            stdout.write_all(&output.stdout).unwrap();
115        }
116
117        if !output.stderr.is_empty() {
118            stdout.write_all(&output.stderr).unwrap();
119        }
120
121        output.status
122    }
123}
124
125/// Macro to instantiate a new hotline plugin, simply defined a concrete plugin type:
126/// struct EmptyPlugin;
127/// 
128/// You can implement the `Plugin` trait for `EmptyPlugin`
129/// impl Plugin<gfx_platform::Device, os_platform::App> for EmptyPlugin {
130/// ..
131/// }
132/// 
133/// Then use this macro to make the plugin loadable from a dll
134/// hotline_plugin![EmptyPlugin];
135#[macro_export]
136macro_rules! hotline_plugin {
137    ($input:ident) => {
138        /// Plugins are created on the heap and the instance is passed from the client to the plugin function calls
139        // c-abi wrapper for `Plugin::create`
140        #[no_mangle]
141        pub fn create() -> *mut core::ffi::c_void {
142            let plugin = $input::create();
143            let ptr = Box::into_raw(Box::new(plugin));
144            ptr.cast()
145        }
146        
147        // c-abi wrapper for `Plugin::update`
148        #[no_mangle]
149        pub fn update(mut client: client::Client<gfx_platform::Device, os_platform::App>, ptr: *mut core::ffi::c_void) -> client::Client<gfx_platform::Device, os_platform::App> {
150            unsafe { 
151                let plugin = ptr.cast::<$input>();
152                let plugin = plugin.as_mut().unwrap();
153                plugin.update(client)
154            }
155        }
156        
157        // c-abi wrapper for `Plugin::setup`
158        #[no_mangle]
159        pub fn setup(mut client: client::Client<gfx_platform::Device, os_platform::App>, ptr: *mut core::ffi::c_void) -> client::Client<gfx_platform::Device, os_platform::App> {
160            unsafe { 
161                let plugin = ptr.cast::<$input>();
162                let plugin = plugin.as_mut().unwrap();
163                plugin.setup(client)
164            }
165        }
166        
167        // c-abi wrapper for `Plugin::reload`
168        #[no_mangle]
169        pub fn unload(mut client: client::Client<gfx_platform::Device, os_platform::App>, ptr: *mut core::ffi::c_void) -> client::Client<gfx_platform::Device, os_platform::App> {
170            unsafe { 
171                let plugin = ptr.cast::<$input>();
172                let plugin = plugin.as_mut().unwrap();
173                plugin.unload(client)
174            }
175        }
176
177        // c-abi wrapper for `Plugin::reload`
178        #[no_mangle]
179        pub fn ui(mut client: client::Client<gfx_platform::Device, os_platform::App>, ptr: *mut core::ffi::c_void, imgui_ctx: *mut core::ffi::c_void) -> client::Client<gfx_platform::Device, os_platform::App> {
180            unsafe { 
181                let plugin = ptr.cast::<$input>();
182                let plugin = plugin.as_mut().unwrap();
183                client.imgui.set_current_context(imgui_ctx);
184                plugin.ui(client)
185            }
186        }
187    }
188}
189
190/// Plugin instances are crated by the `Plugin::create` function, created on the heap
191/// and passed around as a void* through the hotline_plugin macro to become a `Plugin` trait
192pub type PluginInstance = *mut core::ffi::c_void;