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}