rscript/
scripting.rs

1//! This modules contains all what is needed to write scripts
2
3use crate::{Hook, VersionReq};
4
5use super::{Message, ScriptInfo, ScriptType};
6use std::io::Write;
7use std::ptr::slice_from_raw_parts;
8
9use serde::{de::DeserializeOwned, Serialize};
10
11/// Trait that should be implemented on a script abstraction struct\
12/// This concerns [ScriptType::OneShot] and [ScriptType::Daemon]\
13/// The implementer should provide [Scripter::script_type], [Scripter::name], [Scripter::hooks] and [Scripter::version_requirement]\
14///  The struct should call [Scripter::execute]\
15///  ```rust, no_run
16///  # use rscript::*;
17///  # use rscript::scripting::Scripter;
18///
19///  // The hook should usually be on a common api crate.
20///  #[derive(serde::Serialize, serde::Deserialize)]
21///  struct MyHook;
22///  impl Hook for MyHook {
23///     const NAME: &'static str = "MyHook";
24///     type Output = ();
25///  }
26///
27///  struct MyScript;
28///  impl MyScript {
29///     fn run(&mut self, hook: &str) {
30///         let _hook: MyHook = Self::read();
31///         eprintln!("hook: {} was triggered", hook);
32///     }
33///  }
34///  impl Scripter for MyScript {
35///     fn name() -> &'static str {
36///         "MyScript"
37///     }
38///     fn script_type() -> ScriptType {
39///         ScriptType::OneShot
40///     }
41///     fn hooks() -> &'static [&'static str] {
42///         &[MyHook::NAME]
43///     }
44///     fn version_requirement() -> VersionReq {
45///         VersionReq::parse(">=0.1.0").expect("version requirement is correct")
46///     }
47///  }
48///
49///  fn main() {
50///     let mut my_script = MyScript;
51///     MyScript::execute(&mut |hook_name|MyScript::run(&mut my_script, hook_name));
52///  }
53pub trait Scripter {
54    // Required methods
55    /// The name of the script
56    fn name() -> &'static str;
57    /// The script type Daemon/OneShot
58    fn script_type() -> ScriptType;
59    /// The hooks that the script is interested in
60    fn hooks() -> &'static [&'static str];
61    /// The version requirement of the program that the script will run against, when running the script with [Scripter::execute] it will use this version to check if there is an incompatibility between the script and the program
62    fn version_requirement() -> VersionReq;
63
64    // Provided methods
65    /// Read a hook from stdin
66    fn read<H: Hook>() -> H {
67        bincode::deserialize_from(std::io::stdin()).unwrap()
68    }
69    /// Write a value to stdout\
70    /// It takes the hook as a type argument in-order to make sure that the output provided correspond to the hook's expected output
71    fn write<H: Hook>(output: &<H as Hook>::Output) {
72        bincode::serialize_into(std::io::stdout(), output).unwrap()
73    }
74    /// This function is the script entry point.\
75    /// 1. It handles the initial greeting and exiting if the script type is [ScriptType::OneShot]
76    /// 2. It handles receiving hooks, the user is expected to provide a function that acts on a hook name, the user function should use the hook name to read the actual hook from stdin using [Scripter::read]
77    ///
78    /// Example of a user function:
79    /// ```rust
80    /// # use rscript::{VersionReq, Hook};
81    /// # use rscript::scripting::Scripter;
82    /// # #[derive(serde::Serialize, serde::Deserialize)]
83    /// # struct MyHook{}
84    /// # impl Hook for MyHook {
85    /// #   const NAME: &'static str = "MyHook";
86    /// #   type Output = usize;
87    /// # }
88    /// # struct MyScript;
89    /// # impl Scripter for MyScript {
90    /// #   fn name() -> &'static str { todo!() }
91    /// #   fn script_type() -> rscript::ScriptType { todo!() }
92    /// #   fn hooks() -> &'static [&'static str] { todo!() }
93    /// #   fn version_requirement() -> VersionReq { todo!() }
94    /// # }
95    ///
96    /// fn run(hook_name: &str) {
97    ///     match hook_name {
98    ///         MyHook::NAME => {
99    ///             let hook: MyHook = MyScript::read();
100    ///             let output = todo!(); // prepare the corresponding hook output
101    ///             MyScript::write::<MyHook>(&output);
102    ///         }
103    ///         _ => unreachable!()
104    ///     }
105    /// }
106    fn execute(func: &mut dyn FnMut(&str)) -> Result<(), super::Error> {
107        // 1 - Handle greeting
108        let mut stdin = std::io::stdin();
109        let mut stdout = std::io::stdout();
110
111        let message: Message = bincode::deserialize_from(&mut stdin)?;
112
113        if message == Message::Greeting {
114            let metadata = ScriptInfo::new(
115                Self::name(),
116                Self::script_type(),
117                Self::hooks(),
118                Self::version_requirement(),
119            );
120            bincode::serialize_into(&mut stdout, &metadata)?;
121            stdout.flush()?;
122
123            // if the script is OneShot it should exit, it will be run again but with message == [Message::Execute]
124            if matches!(Self::script_type(), ScriptType::OneShot) {
125                std::process::exit(0);
126            }
127        } else {
128            // message == Message::Execute
129            // the script will continue its execution
130        }
131
132        // 2 - Handle Executing
133        loop {
134            // OneShot scripts handles greeting each time they are run, so [Message] is already received
135            if matches!(Self::script_type(), ScriptType::Daemon) {
136                let _message: Message = bincode::deserialize_from(&mut stdin)?;
137            }
138
139            let hook_name: String = bincode::deserialize_from(&mut stdin)?;
140
141            func(&hook_name);
142            std::io::stdout().flush()?;
143
144            if matches!(Self::script_type(), ScriptType::OneShot) {
145                // if its OneShot we exit after one execution
146                return Ok(());
147            }
148        }
149    }
150}
151
152/// A [ScriptType::DynamicLib] script needs to export a static instance of this struct named [DynamicScript::NAME]
153/// ```rs
154/// // In a script file
155/// #[no_mangle]
156/// pub static SCRIPT: DynamicScript = DynamicScript { script_info: .., script: .. };
157/// ```
158///
159///
160/// `DynamicScript` contains also methods for writing scripts: [DynamicScript::read], [DynamicScript::write]
161#[repr(C)]
162pub struct DynamicScript {
163    /// A function that returns `ScriptInfo` serialized as `FFiData`\
164    /// *fn() -> ScriptInfo*
165    pub script_info: extern "C" fn() -> FFiData,
166    /// A function that accepts a hook name (casted to `FFiStr`) and the hook itself (serialized as `FFiData`)  and returns the hook output (serialized as `FFiData`)\
167    /// *fn<H: Hook>(hook: &str (H::Name), data: H) -> <H as Hook>::Output>*
168    pub script: extern "C" fn(FFiStr, FFiData) -> FFiData,
169}
170impl DynamicScript {
171    /// ```rust
172    /// pub const NAME: &'static [u8] = b"SCRIPT";
173    /// ```
174    pub const NAME: &'static [u8] = b"SCRIPT";
175
176    /// Read a hook from an FFiData
177    pub fn read<H: Hook>(hook: FFiData) -> H {
178        hook.deserialize().unwrap()
179    }
180    /// Write a value to an FFiData
181    /// It takes the hook as a type argument in-order to make sure that the output provided correspond to the hook's expected output
182    pub fn write<H: Hook>(output: &<H as Hook>::Output) -> FFiData {
183        FFiData::serialize_from(output).unwrap()
184    }
185}
186
187#[repr(C)]
188/// `FFiStr` is used to send the hook name to [ScriptType::DynamicLib] script
189pub struct FFiStr {
190    ptr: *const u8,
191    len: usize,
192}
193impl FFiStr {
194    /// Create a `FFiStr` from a `&str`
195    pub fn new(string: &'static str) -> Self {
196        Self {
197            ptr: string as *const str as _,
198            len: string.len(),
199        }
200    }
201    /// Cast `FFiStr` to `&str`
202    pub fn as_str(&self) -> &str {
203        unsafe { std::str::from_utf8_unchecked(std::slice::from_raw_parts(self.ptr, self.len)) }
204    }
205}
206
207/// `FFiData` is used for communicating arbitrary data between [ScriptType::DynamicLib] scripts and the main program
208#[repr(C)]
209pub struct FFiData {
210    ptr: *mut u8,
211    len: usize,
212    cap: usize,
213}
214impl FFiData {
215    /// Crate a new FFiData from any serialize-able data
216    pub(crate) fn serialize_from<D: Serialize>(data: &D) -> Result<Self, bincode::Error> {
217        let data = bincode::serialize(data)?;
218        let mut vec = std::mem::ManuallyDrop::new(data);
219        let ptr = vec.as_mut_ptr();
220        let len = vec.len();
221        let cap = vec.capacity();
222        Ok(FFiData { ptr, len, cap })
223    }
224    /// De-serialize into a concrete type
225    pub(crate) fn deserialize<D: DeserializeOwned>(&self) -> Result<D, bincode::Error> {
226        let data: &[u8] = unsafe { &*slice_from_raw_parts(self.ptr, self.len) };
227        bincode::deserialize(data)
228    }
229}
230impl Drop for FFiData {
231    fn drop(&mut self) {
232        let _ = unsafe { Vec::from_raw_parts(self.ptr, self.len, self.cap) };
233    }
234}