bash_builtins/variables/dynvars.rs
1//! Module for [`DynamicVariable`].
2
3#![allow(clippy::fn_address_comparisons)]
4
5use super::VariableError;
6use crate::ffi::variables as ffi;
7use std::collections::HashMap;
8use std::ffi::{CStr, CString};
9use std::mem::MaybeUninit;
10use std::os::raw::c_char;
11use std::sync::atomic::AtomicBool;
12use std::sync::{atomic::Ordering::SeqCst, Mutex, MutexGuard};
13use std::{mem, panic};
14
15/// The `DynamicVariable` provides the implementation to create dynamic
16/// variables.
17///
18/// [`get`] is called when the value of the shell variable is required. [`set`]
19/// is called when a value is assigned to a variable.
20///
21/// Use [`variables::bind`] to create a dynamic variable with an instance of a
22/// type implementing `DynamicVariable`.
23///
24/// # Deleting Dynamic Variables
25///
26/// A dynamic variable can be deleted with [`unset`]. However, the instance
27/// bound to it is not dropped, since bash does not notify when a variable is
28/// removed.
29///
30/// If the builtin is removed (`enable -d <name>`), dynamic variables are
31/// removed before unloading the shared object.
32///
33/// # Example
34///
35/// To create a counter using `DynamicVariable`, first we need to implement a
36/// `Counter` type similar to this:
37///
38/// ```
39/// use std::ffi::{CStr, CString};
40/// use bash_builtins::error;
41/// use bash_builtins::variables::DynamicVariable;
42///
43/// struct Counter(isize);
44///
45/// impl DynamicVariable for Counter {
46/// fn get(&mut self) -> Option<CString> {
47/// let value = CString::new(format!("{}", self.0)).ok();
48/// self.0 += 1;
49/// value
50/// }
51///
52/// fn set(&mut self, value: &CStr) {
53/// self.0 = match value.to_str().map(str::parse) {
54/// Ok(Ok(n)) => n,
55/// _ => {
56/// error!("invalid value: {:?}", value);
57/// return;
58/// }
59/// }
60/// }
61/// }
62/// ```
63///
64/// Then, dynamic variables with any name can be created using a function like
65/// this:
66///
67/// ```
68/// # struct Counter(isize);
69/// # impl bash_builtins::variables::DynamicVariable for Counter {
70/// # fn get(&mut self) -> Option<std::ffi::CString> { None }
71/// # fn set(&mut self, _: &std::ffi::CStr) {}
72/// # }
73/// #
74/// use bash_builtins::variables::{bind, VariableError};
75///
76/// fn create_counter(name: &str) -> Result<(), VariableError> {
77/// bind(name, Counter(0))
78/// }
79/// ```
80///
81/// The [`varcounter` example] implements this functionality:
82///
83/// ```notrust
84/// $ cargo build --release --examples
85///
86/// $ enable -f target/release/examples/libvarcounter.so varcounter
87///
88/// $ varcounter FOO BAR
89///
90/// $ echo $FOO
91/// 0
92///
93/// $ echo $FOO
94/// 1
95///
96/// $ echo $FOO
97/// 2
98///
99/// $ echo $FOO $BAR $BAR
100/// 3 0 1
101///
102/// $ FOO=1000
103///
104/// $ echo $FOO
105/// 1000
106///
107/// $ echo $FOO
108/// 1001
109/// ```
110///
111/// [`varcounter` example]: https://github.com/ayosec/bash-builtins.rs/blob/main/examples/varcounter.rs
112/// [`get`]: DynamicVariable::get
113/// [`set`]: DynamicVariable::set
114/// [`unset`]: https://www.gnu.org/software/bash/manual/html_node/Bourne-Shell-Builtins.html#index-unset
115/// [`variables::bind`]: crate::variables::bind
116pub trait DynamicVariable {
117 /// Returns the value for the shell variable.
118 ///
119 /// If it returns `None`, the variable will be empty.
120 fn get(&mut self) -> Option<CString>;
121
122 /// Called when a string is assigned to the shell variable.
123 fn set(&mut self, value: &CStr);
124}
125
126pub(super) fn bind_dynvar(
127 name: &str,
128 dynvar: Box<dyn DynamicVariable>,
129) -> Result<(), VariableError> {
130 let name = CString::new(name).map_err(|_| VariableError::InvalidName)?;
131
132 unsafe {
133 let shell_var = ffi::bind_variable(name.as_ptr(), std::ptr::null(), 0);
134
135 if shell_var.is_null() {
136 return Err(VariableError::InvalidName);
137 }
138
139 (*shell_var).dynamic_value = read_var;
140 (*shell_var).assign_func = assign_var;
141 }
142
143 global_state().insert(name, dynvar);
144
145 Ok(())
146}
147
148/// Track if the global state is initialized.
149static STATE_INIT: AtomicBool = AtomicBool::new(false);
150
151type State = HashMap<CString, Box<dyn DynamicVariable>>;
152
153/// Global state to store the instances of `DynamicVariable` with their
154/// shell variables.
155fn global_state() -> MutexGuard<'static, State> {
156 static mut STATE: MaybeUninit<Mutex<State>> = MaybeUninit::uninit();
157
158 if !STATE_INIT.fetch_or(true, SeqCst) {
159 unsafe {
160 STATE = MaybeUninit::new(Mutex::new(State::default()));
161 libc::atexit(remove_all_dynvars);
162 }
163 }
164
165 match unsafe { (*STATE.as_ptr()).lock() } {
166 Ok(l) => l,
167 Err(e) => e.into_inner(),
168 }
169}
170
171/// Unset variables that contains references to function in this crate.
172///
173/// This function is executed when the shared object is unloaded.
174extern "C" fn remove_all_dynvars() {
175 let state: State = mem::take(&mut *global_state());
176 STATE_INIT.store(false, SeqCst);
177
178 for (varname, _) in state {
179 unsafe {
180 let shell_var = ffi::find_variable(varname.as_ptr());
181 if !shell_var.is_null() && (*shell_var).dynamic_value == read_var {
182 ffi::unbind_variable(varname.as_ptr());
183 }
184 }
185 }
186}
187
188/// Called by bash when a variable is read.
189unsafe extern "C" fn read_var(shell_var: *mut ffi::ShellVar) -> *const ffi::ShellVar {
190 if !STATE_INIT.load(SeqCst) {
191 return shell_var;
192 }
193
194 let result = panic::catch_unwind(|| {
195 global_state()
196 .get_mut(CStr::from_ptr((*shell_var).name))
197 .map(|dynvar| dynvar.get())
198 });
199
200 let new_value = match result {
201 Ok(Some(v)) => v,
202
203 _ => {
204 crate::ffi::internal_error(b"dynamic variable unavailable\0".as_ptr().cast());
205 return shell_var;
206 }
207 };
208
209 libc::free((*shell_var).value.cast());
210
211 // Use the pointer from `CString`. Its memory should be allocated from
212 // `malloc`, so it is safe to use the pointer with `free`.
213 (*shell_var).value = match new_value {
214 Some(v) => v.into_raw(),
215 None => libc::calloc(1, 1).cast(),
216 };
217
218 shell_var
219}
220
221/// Called by bash when a variable is assigned.
222unsafe extern "C" fn assign_var(
223 shell_var: *mut ffi::ShellVar,
224 value: *const c_char,
225 _: libc::intmax_t,
226 _: *const c_char,
227) -> *const ffi::ShellVar {
228 if value.is_null() {
229 return shell_var;
230 }
231
232 let result = panic::catch_unwind(|| {
233 global_state()
234 .get_mut(CStr::from_ptr((*shell_var).name))
235 .map(|dynvar| dynvar.set(CStr::from_ptr(value)))
236 });
237
238 if result.is_err() {
239 crate::ffi::internal_error(b"dynamic variable unavailable\0".as_ptr().cast());
240 }
241
242 shell_var
243}