nix_bindings_expr/
primop.rs

1use crate::eval_state::{EvalState, EvalStateWeak};
2use crate::value::Value;
3use anyhow::Result;
4use nix_bindings_expr_sys as raw;
5use nix_bindings_util::check_call;
6use nix_bindings_util_sys as raw_util;
7use std::ffi::{c_int, c_void, CStr, CString};
8use std::mem::ManuallyDrop;
9use std::ptr::{null, null_mut};
10
11/// A primop error that is not memoized in the thunk that triggered it,
12/// allowing the thunk to be forced again.
13///
14/// Since [Nix 2.34](https://nix.dev/manual/nix/2.34/release-notes/rl-2.34.html#c-api-changes),
15/// primop errors are memoized by default: once a thunk fails, forcing it
16/// again returns the same error. Use `RecoverableError` for errors that
17/// are transient, so the caller can retry.
18///
19/// On Nix < 2.34, all errors are already recoverable, so this type has
20/// no additional effect.
21///
22/// Available since nix-bindings-expr 0.2.1.
23#[derive(Debug)]
24pub struct RecoverableError(String);
25
26impl RecoverableError {
27    pub fn new(msg: impl Into<String>) -> Self {
28        RecoverableError(msg.into())
29    }
30}
31
32impl std::fmt::Display for RecoverableError {
33    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
34        self.0.fmt(f)
35    }
36}
37
38impl std::error::Error for RecoverableError {}
39
40/// Metadata for a primop, used with `PrimOp::new`.
41pub struct PrimOpMeta<'a, const N: usize> {
42    /// Name of the primop. Note that primops do not have to be registered as
43    /// builtins. Nonetheless, a name is required for documentation purposes, e.g.
44    /// :doc in the repl.
45    pub name: &'a CStr,
46
47    /// Documentation for the primop. This is displayed in the repl when using
48    /// :doc. The format is markdown.
49    pub doc: &'a CStr,
50
51    /// The number of arguments the function takes, as well as names for the
52    /// arguments, to be presented in the documentation (if applicable, e.g.
53    /// :doc in the repl).
54    pub args: [&'a CStr; N],
55}
56
57pub struct PrimOp {
58    pub(crate) ptr: *mut raw::PrimOp,
59}
60impl Drop for PrimOp {
61    fn drop(&mut self) {
62        unsafe {
63            raw::gc_decref(null_mut(), self.ptr as *mut c_void);
64        }
65    }
66}
67impl PrimOp {
68    /// Create a new primop with the given metadata and implementation.
69    ///
70    /// When `f` returns an `Err`, the error is propagated to the Nix evaluator.
71    /// To return a [recoverable error](RecoverableError), include it in the
72    /// error chain (e.g. `Err(RecoverableError::new("...").into())`).
73    pub fn new<const N: usize>(
74        eval_state: &mut EvalState,
75        meta: PrimOpMeta<N>,
76        f: Box<dyn Fn(&mut EvalState, &[Value; N]) -> Result<Value>>,
77    ) -> Result<PrimOp> {
78        assert!(N != 0);
79
80        let mut args = Vec::new();
81        for arg in meta.args {
82            args.push(arg.as_ptr());
83        }
84        args.push(null());
85
86        // Primops weren't meant to be dynamically created, as of writing.
87        // This leaks, and so do the primop fields in Nix internally.
88        let user_data = {
89            // We'll be leaking this Box.
90            // TODO: Use the GC with finalizer, if possible.
91            let user_data = ManuallyDrop::new(Box::new(PrimOpContext {
92                arity: N,
93                function: Box::new(move |eval_state, args| f(eval_state, args.try_into().unwrap())),
94                eval_state: eval_state.weak_ref(),
95            }));
96            user_data.as_ref() as *const PrimOpContext as *mut c_void
97        };
98        let op = unsafe {
99            check_call!(raw::alloc_primop(
100                &mut eval_state.context,
101                FUNCTION_ADAPTER,
102                N as c_int,
103                meta.name.as_ptr(),
104                args.as_mut_ptr(), /* TODO add an extra const to bindings to avoid mut here. */
105                meta.doc.as_ptr(),
106                user_data
107            ))?
108        };
109        Ok(PrimOp { ptr: op })
110    }
111}
112
113/// The user_data for our Nix primops
114struct PrimOpContext {
115    arity: usize,
116    function: Box<dyn Fn(&mut EvalState, &[Value]) -> Result<Value>>,
117    eval_state: EvalStateWeak,
118}
119
120unsafe extern "C" fn function_adapter(
121    user_data: *mut ::std::os::raw::c_void,
122    context_out: *mut raw_util::c_context,
123    _state: *mut raw::EvalState,
124    args: *mut *mut raw::Value,
125    ret: *mut raw::Value,
126) {
127    let primop_info = (user_data as *const PrimOpContext).as_ref().unwrap();
128    let mut eval_state = primop_info.eval_state.upgrade().unwrap_or_else(|| {
129        panic!("Nix primop called after EvalState was dropped");
130    });
131    let args_raw_slice = unsafe { std::slice::from_raw_parts(args, primop_info.arity) };
132    let args_vec: Vec<Value> = args_raw_slice
133        .iter()
134        .map(|v| Value::new_borrowed(*v))
135        .collect();
136    let args_slice = args_vec.as_slice();
137
138    let r = primop_info.function.as_ref()(&mut eval_state, args_slice);
139
140    match r {
141        Ok(v) => unsafe {
142            raw::copy_value(context_out, ret, v.raw_ptr());
143        },
144        Err(e) => unsafe {
145            let err_code = error_code(&e);
146            let cstr = CString::new(e.to_string()).unwrap_or_else(|_e| {
147                CString::new("<rust nix-expr application error message contained null byte>")
148                    .unwrap()
149            });
150            raw_util::set_err_msg(context_out, err_code, cstr.as_ptr());
151        },
152    }
153}
154
155fn error_code(e: &anyhow::Error) -> raw_util::err {
156    #[cfg(nix_at_least = "2.34.0pre")]
157    if e.downcast_ref::<RecoverableError>().is_some() {
158        return raw_util::err_NIX_ERR_RECOVERABLE;
159    }
160    raw_util::err_NIX_ERR_UNKNOWN
161}
162
163static FUNCTION_ADAPTER: raw::PrimOpFun = Some(function_adapter);