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}