Skip to main content

nix_bindings_expr/
primop.rs

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