Skip to main content

nix_bindings/
primop.rs

1//! Nix primitive operations (primops).
2//!
3//! This module provides a safe, closure-based API for registering custom Nix
4//! primitive operations (primops).
5//!
6//! # Overview
7//!
8//! Primops are Rust functions that appear as Nix builtins. There are two ways
9//! to expose a primop:
10//!
11//! * **Global builtin**: call [`PrimOp::register`] *before* creating any
12//!   [`EvalState`](crate::EvalState). All subsequently created states will
13//!   include the primop in `builtins`.
14//! * **Value-embedded**: call [`PrimOp::into_value`] on an existing
15//!   [`EvalState`](crate::EvalState) to obtain a callable
16//!   [`Value`](crate::Value).
17//!
18//! # Example
19//!
20//! ```no_run
21//! use std::sync::Arc;
22//!
23//! use nix_bindings::{
24//!   Context,
25//!   EvalStateBuilder,
26//!   Store,
27//!   primop::{NixValueOps, PrimOp},
28//! };
29//!
30//! # fn main() -> Result<(), Box<dyn std::error::Error>> {
31//! let ctx = Arc::new(Context::new()?);
32//!
33//! // Register a global builtin that doubles an integer
34//! PrimOp::new(&ctx, "double", 1, Some("Double an integer"), |args, ret| {
35//!   let n = args[0].as_int()?;
36//!   ret.set_int(n * 2)
37//! })?
38//! .register(&ctx)?;
39//!
40//! let store = Arc::new(Store::open(&ctx, None)?);
41//! let state = EvalStateBuilder::new(&store)?.build()?;
42//! let result = state.eval_from_string("builtins.double 21", "<eval>")?;
43//! assert_eq!(result.as_int()?, 42);
44//! # Ok(())
45//! # }
46//! ```
47
48use std::{
49  collections::HashSet,
50  ffi::CString,
51  marker::PhantomData,
52  os::raw::c_void,
53  panic::{self, AssertUnwindSafe},
54  sync::{Arc, Mutex, OnceLock},
55};
56
57use crate::{Context, Error, Result, ValueType, check_err, sys};
58
59type PrimOpFn =
60  dyn Fn(&[PrimOpArg<'_>], &mut PrimOpRet<'_>) -> Result<()> + Send + Sync;
61
62/// The boxed Rust closure called by the trampoline.
63///
64/// Arity is stored alongside the closure so the trampoline can slice the args
65/// array correctly.
66struct ClosureData {
67  arity: usize,
68  f:     Box<PrimOpFn>,
69}
70
71/// C-compatible trampoline that dispatches to the boxed Rust closure.
72///
73/// # Safety
74///
75/// `user_data` must be a valid `*mut ClosureData` allocated via
76/// `Box::into_raw`.  `args` must be a valid array of at least `arity`
77/// non-null `*mut nix_value` pointers. `ret` must be a valid, writable
78/// `*mut nix_value`.
79unsafe extern "C" fn trampoline(
80  user_data: *mut c_void,
81  context: *mut sys::nix_c_context,
82  state: *mut sys::EvalState,
83  args: *mut *mut sys::nix_value,
84  ret: *mut sys::nix_value,
85) {
86  let data = unsafe { &*(user_data as *const ClosureData) };
87
88  // Build arg wrappers.
89  //
90  // The pointers are borrowed from Nix and must NOT be
91  // decreffed by our code. When arity is 0, `args` may be null. Passing a null
92  // pointer to slice::from_raw_parts (even with len=0) produces a dangling
93  // reference which violates Rust's validity invariant for references.
94  let arg_wrappers: Vec<PrimOpArg<'_>> = if data.arity == 0 {
95    Vec::new()
96  } else {
97    let arg_slice = unsafe { std::slice::from_raw_parts(args, data.arity) };
98    arg_slice
99      .iter()
100      .map(|&p| {
101        PrimOpArg {
102          inner: p,
103          ctx: context,
104          state,
105          _phantom: PhantomData,
106        }
107      })
108      .collect()
109  };
110
111  let mut ret_wrapper = PrimOpRet {
112    inner: ret,
113    ctx: context,
114    state,
115    written: false,
116    _phantom: PhantomData,
117  };
118
119  let result = panic::catch_unwind(AssertUnwindSafe(|| {
120    (data.f)(&arg_wrappers, &mut ret_wrapper)
121  }));
122
123  let err_msg = match result {
124    Ok(Ok(())) => {
125      if ret_wrapper.written {
126        return;
127      }
128      "primop returned Ok(()) without calling a set_* method on the return slot"
129        .to_string()
130    },
131    Ok(Err(e)) => format!("primop error: {e}"),
132    Err(payload) => {
133      // Try to pull a useful message out of the panic payload before
134      // giving up. catch_unwind boxes the payload; the standard panics
135      // produce &str or String.
136      let detail = if let Some(s) = payload.downcast_ref::<&'static str>() {
137        (*s).to_owned()
138      } else if let Some(s) = payload.downcast_ref::<String>() {
139        s.clone()
140      } else {
141        "unknown panic payload".to_owned()
142      };
143      format!("primop panicked: {detail}")
144    },
145  };
146
147  let msg_c = CString::new(err_msg)
148    .unwrap_or_else(|_| CString::new("primop error").unwrap());
149  unsafe {
150    sys::nix_set_err_msg(
151      context,
152      sys::nix_err_NIX_ERR_NIX_ERROR,
153      msg_c.as_ptr(),
154    );
155  }
156}
157
158/// GC finalizer that frees the `ClosureData` box when the GC collects the
159/// associated `PrimOp` object.
160///
161/// `cd` is the `*mut ClosureData` stored at primop-allocation time.
162unsafe extern "C" fn drop_closure_finalizer(
163  _obj: *mut c_void,
164  cd: *mut c_void,
165) {
166  // Reconstruct and immediately drop the Box, running the destructor.
167  let _ = unsafe { Box::from_raw(cd as *mut ClosureData) };
168}
169
170// The shared value-access trait lives at the crate root as
171// `NixValueOps` (see `crate::value_ops`). Re-exported here for backwards
172// compatibility with `use nix_bindings::primop::NixValueOps`.
173pub use crate::value_ops::NixValueOps;
174use crate::value_ops::NixValueRaw;
175
176/// A borrowed Nix value passed as an argument to a primop callback.
177///
178/// The value is owned by the Nix evaluator and must **not** be decreffed by
179/// the caller. It is valid only for the duration of the primop invocation
180/// (expressed by the `'a` lifetime).
181pub struct PrimOpArg<'a> {
182  inner:    *mut sys::nix_value,
183  ctx:      *mut sys::nix_c_context,
184  state:    *mut sys::EvalState,
185  _phantom: PhantomData<&'a ()>,
186}
187
188// `PrimOpArg`'s raw `nix_c_context` and `EvalState` pointers are valid
189// only while the trampoline is on the stack. The `'a` lifetime + raw
190// pointer fields leave the type `!Send + !Sync` by auto-trait, which is
191// exactly what we want: a `Send` impl would let a user smuggle the
192// argument into a thread that outlives the call and dereference a
193// dangling context.
194
195impl NixValueRaw for PrimOpArg<'_> {
196  fn raw_ctx(&self) -> *mut sys::nix_c_context {
197    self.ctx
198  }
199
200  fn raw_state(&self) -> *mut sys::EvalState {
201    self.state
202  }
203
204  fn raw_inner(&self) -> *mut sys::nix_value {
205    self.inner
206  }
207}
208
209impl PrimOpArg<'_> {
210  // Scalar accessors (value_type, force, as_int, as_float, as_bool,
211  // as_string, as_path) live on the NixValueOps trait; bring it into
212  // scope to use them.
213
214  /// Interpret this argument as an attribute set.
215  ///
216  /// Automatically forces the value if it is a thunk.
217  ///
218  /// Returns an [`ArgAttrs`] wrapper that provides read access to the
219  /// attribute set's keys and values. The returned wrapper borrows this
220  /// argument; it does not own any GC references.
221  ///
222  /// # Errors
223  ///
224  /// Returns an error if forcing fails or the resolved value is not an
225  /// attribute set.
226  pub fn as_attrs(&self) -> Result<ArgAttrs<'_>> {
227    self.force()?;
228    if self.value_type() != ValueType::Attrs {
229      return Err(Error::InvalidType {
230        expected: "attrs",
231        actual:   self.value_type().to_string(),
232      });
233    }
234    Ok(ArgAttrs {
235      inner:    self.inner,
236      ctx:      self.ctx,
237      state:    self.state,
238      _phantom: PhantomData,
239    })
240  }
241
242  /// Interpret this argument as a list.
243  ///
244  /// Automatically forces the value if it is a thunk.
245  ///
246  /// Returns an [`ArgList`] wrapper that provides read access to the list
247  /// elements. The returned wrapper borrows this argument; it does not own
248  /// any GC references.
249  ///
250  /// # Errors
251  ///
252  /// Returns an error if forcing fails or the resolved value is not a
253  /// list.
254  pub fn as_list(&self) -> Result<ArgList<'_>> {
255    self.force()?;
256    if self.value_type() != ValueType::List {
257      return Err(Error::InvalidType {
258        expected: "list",
259        actual:   self.value_type().to_string(),
260      });
261    }
262    Ok(ArgList {
263      inner:    self.inner,
264      ctx:      self.ctx,
265      state:    self.state,
266      _phantom: PhantomData,
267    })
268  }
269}
270
271/// The writable return-value slot provided to a primop closure.
272///
273/// Exactly one `set_*` method must be called before returning `Ok(())`.
274/// The trampoline verifies this at runtime and synthesises a Nix
275/// `EvalError` when the closure returned `Ok(())` without writing the
276/// slot.
277pub struct PrimOpRet<'a> {
278  inner:    *mut sys::nix_value,
279  ctx:      *mut sys::nix_c_context,
280  state:    *mut sys::EvalState,
281  /// Becomes `true` after the closure invokes any `set_*` method.
282  /// Inspected by the trampoline after the closure returns.
283  written:  bool,
284  _phantom: PhantomData<&'a mut ()>,
285}
286
287impl<'a> PrimOpRet<'a> {
288  /// Mark the slot as written. Called from every `set_*`/`copy_*` path on
289  /// success so the trampoline can verify the closure actually produced a
290  /// value before returning Ok.
291  fn mark_written(&mut self) {
292    self.written = true;
293  }
294
295  /// Write an integer result.
296  ///
297  /// # Errors
298  ///
299  /// Returns an error if the write fails.
300  pub fn set_int(&mut self, i: i64) -> Result<()> {
301    unsafe { check_err(self.ctx, sys::nix_init_int(self.ctx, self.inner, i)) }?;
302    self.mark_written();
303    Ok(())
304  }
305
306  /// Write a float result.
307  ///
308  /// # Errors
309  ///
310  /// Returns an error if the write fails.
311  pub fn set_float(&mut self, f: f64) -> Result<()> {
312    unsafe {
313      check_err(self.ctx, sys::nix_init_float(self.ctx, self.inner, f))
314    }?;
315    self.mark_written();
316    Ok(())
317  }
318
319  /// Write a boolean result.
320  ///
321  /// # Errors
322  ///
323  /// Returns an error if the write fails.
324  pub fn set_bool(&mut self, b: bool) -> Result<()> {
325    unsafe {
326      check_err(self.ctx, sys::nix_init_bool(self.ctx, self.inner, b))
327    }?;
328    self.mark_written();
329    Ok(())
330  }
331
332  /// Write a null result.
333  ///
334  /// # Errors
335  ///
336  /// Returns an error if the write fails.
337  pub fn set_null(&mut self) -> Result<()> {
338    unsafe { check_err(self.ctx, sys::nix_init_null(self.ctx, self.inner)) }?;
339    self.mark_written();
340    Ok(())
341  }
342
343  /// Write a string result.
344  ///
345  /// # Errors
346  ///
347  /// Returns an error if `s` contains an interior NUL byte or the write
348  /// fails.
349  pub fn set_string(&mut self, s: &str) -> Result<()> {
350    let s_c = CString::new(s)?;
351    unsafe {
352      check_err(
353        self.ctx,
354        sys::nix_init_string(self.ctx, self.inner, s_c.as_ptr()),
355      )
356    }?;
357    self.mark_written();
358    Ok(())
359  }
360
361  /// Write a path result.
362  ///
363  /// The path string is interpreted by Nix the same way a `path` literal
364  /// would be (e.g. it can be an absolute filesystem path or a store path).
365  ///
366  /// # Pure Evaluation
367  ///
368  /// This calls `nix_init_path_string`, which the Nix evaluator rejects for
369  /// absolute paths when running with `--pure-eval`. It returns
370  /// [`Error::EvalError`] in that case. If the path is a store path your primop
371  /// already added to the store, use [`set_store_path`](Self::set_store_path)
372  /// (requires the `shim` feature) instead; it registers the path with the
373  /// evaluator's allowlist so the value is usable in pure mode.
374  ///
375  /// # Errors
376  ///
377  /// Returns an error if `p` contains an interior NUL byte, the write fails,
378  /// or Nix is running in pure evaluation mode and `p` is an absolute path.
379  pub fn set_path(&mut self, p: &str) -> Result<()> {
380    let p_c = CString::new(p)?;
381    unsafe {
382      check_err(
383        self.ctx,
384        sys::nix_init_path_string(
385          self.ctx,
386          self.state,
387          self.inner,
388          p_c.as_ptr(),
389        ),
390      )
391    }?;
392    self.mark_written();
393    Ok(())
394  }
395
396  /// Type-safe variant of [`set_store_path`](Self::set_store_path).
397  ///
398  /// Takes a parsed [`StorePath`](crate::store::StorePath) instead of a raw
399  /// string. Eliminates the chance of passing a non-store-path string that
400  /// would be rejected by Nix's parser at the C++ level. Uses
401  /// [`Store::print_path`](crate::store::Store::print_path) to render the
402  /// path back to its canonical form.
403  ///
404  /// Requires the `shim` and `store` features.
405  ///
406  /// # Errors
407  ///
408  /// Returns an error if rendering or the write fails.
409  #[cfg(all(feature = "shim", feature = "store"))]
410  pub fn set_store_path_typed(
411    &mut self,
412    store: &crate::store::Store,
413    path: &crate::store::StorePath,
414  ) -> Result<()> {
415    let rendered = store.print_path(path)?;
416    self.set_store_path(&rendered)
417  }
418
419  /// Write a store path as a Nix path value, registering it as accessible
420  /// in the evaluator's allowlist first.
421  ///
422  /// In pure evaluation mode (`--pure-eval`) Nix wraps the filesystem in an
423  /// `AllowListSourceAccessor` that rejects any path not explicitly permitted.
424  /// This method calls `EvalState::allowPath` before writing the value,
425  /// mirroring what Nix's own fetch builtins do after adding a path to the
426  /// store.  The resulting path value is usable (e.g. as `src` in a
427  /// derivation) without triggering the access restriction.
428  ///
429  /// The caller must ensure `p` is a canonical store path that already exists
430  /// in the store.
431  ///
432  /// # Errors
433  ///
434  /// Returns an error if `p` contains an interior NUL byte, is not a valid
435  /// store path, or the write fails.
436  #[cfg(feature = "shim")]
437  pub fn set_store_path(&mut self, p: &str) -> Result<()> {
438    let p_c = CString::new(p)?;
439    unsafe {
440      check_err(
441        self.ctx,
442        sys::nix_eval_state_allow_path(self.ctx, self.state, p_c.as_ptr()),
443      )?;
444      check_err(
445        self.ctx,
446        sys::nix_init_path_string(
447          self.ctx,
448          self.state,
449          self.inner,
450          p_c.as_ptr(),
451        ),
452      )
453    }?;
454    self.mark_written();
455    Ok(())
456  }
457
458  /// Copy the value pointed to by `src` into the return slot.
459  ///
460  /// This is useful when the result is an existing [`Value`](crate::Value)
461  /// that should be forwarded as-is.
462  ///
463  /// # Safety
464  ///
465  /// `src` must be a valid, non-null `*mut nix_value` that remains live for
466  /// the duration of the call.
467  ///
468  /// # Errors
469  ///
470  /// Returns an error if the copy fails.
471  pub unsafe fn copy_from_raw(
472    &mut self,
473    src: *mut sys::nix_value,
474  ) -> Result<()> {
475    unsafe {
476      check_err(self.ctx, sys::nix_copy_value(self.ctx, self.inner, src))
477    }?;
478    self.mark_written();
479    Ok(())
480  }
481
482  /// Write an attribute set result.
483  ///
484  /// Builds an attribute set from the given key-value pairs and writes it
485  /// into the return slot. Each value is a [`PrimOpValue`] obtained from a
486  /// primop argument or created via the `make_*` methods on this struct.
487  ///
488  /// # Errors
489  ///
490  /// Returns an error if construction fails.
491  pub fn set_attrs(
492    &mut self,
493    pairs: &[(&str, &PrimOpValue<'_>)],
494  ) -> Result<()> {
495    let builder = unsafe {
496      sys::nix_make_bindings_builder(self.ctx, self.state, pairs.len().max(1))
497    };
498    if builder.is_null() {
499      return Err(Error::NullPointer);
500    }
501
502    struct BuilderGuard(*mut sys::BindingsBuilder);
503    impl Drop for BuilderGuard {
504      fn drop(&mut self) {
505        unsafe { sys::nix_bindings_builder_free(self.0) };
506      }
507    }
508    let _guard = BuilderGuard(builder);
509
510    for (key, value) in pairs {
511      let key_c = CString::new(*key)?;
512      // SAFETY: builder, key, and value are valid
513      unsafe {
514        check_err(
515          self.ctx,
516          sys::nix_bindings_builder_insert(
517            self.ctx,
518            builder,
519            key_c.as_ptr(),
520            value.inner,
521          ),
522        )?;
523      }
524    }
525
526    // SAFETY: builder is valid, result slot is valid
527    unsafe {
528      check_err(self.ctx, sys::nix_make_attrs(self.ctx, self.inner, builder))
529    }?;
530    self.mark_written();
531    Ok(())
532  }
533
534  /// Write a list result.
535  ///
536  /// Builds a list from the given values and writes it into the return
537  /// slot. Each value is a [`PrimOpValue`] obtained from a primop
538  /// argument or created via the `make_*` methods on this struct.
539  ///
540  /// # Errors
541  ///
542  /// Returns an error if construction fails.
543  pub fn set_list(&mut self, items: &[&PrimOpValue<'_>]) -> Result<()> {
544    let builder = unsafe {
545      sys::nix_make_list_builder(self.ctx, self.state, items.len().max(1))
546    };
547    if builder.is_null() {
548      return Err(Error::NullPointer);
549    }
550
551    struct ListGuard(*mut sys::ListBuilder);
552    impl Drop for ListGuard {
553      fn drop(&mut self) {
554        unsafe { sys::nix_list_builder_free(self.0) };
555      }
556    }
557    let _guard = ListGuard(builder);
558
559    for (i, value) in items.iter().enumerate() {
560      unsafe {
561        check_err(
562          self.ctx,
563          sys::nix_list_builder_insert(
564            self.ctx,
565            builder,
566            i as std::os::raw::c_uint,
567            value.inner,
568          ),
569        )?;
570      }
571    }
572
573    unsafe {
574      check_err(self.ctx, sys::nix_make_list(self.ctx, builder, self.inner))
575    }?;
576    self.mark_written();
577    Ok(())
578  }
579
580  /// Allocate and initialise an integer [`PrimOpValue`].
581  ///
582  /// # Errors
583  ///
584  /// Returns an error if allocation or initialisation fails.
585  pub fn make_int(&self, i: i64) -> Result<PrimOpValue<'a>> {
586    let v = PrimOpValue::alloc(self.ctx, self.state)?;
587    unsafe {
588      check_err(self.ctx, sys::nix_init_int(self.ctx, v.inner, i))?;
589    }
590    Ok(v)
591  }
592
593  /// Allocate and initialise a float [`PrimOpValue`].
594  ///
595  /// # Errors
596  ///
597  /// Returns an error if allocation or initialisation fails.
598  pub fn make_float(&self, f: f64) -> Result<PrimOpValue<'a>> {
599    let v = PrimOpValue::alloc(self.ctx, self.state)?;
600    unsafe {
601      check_err(self.ctx, sys::nix_init_float(self.ctx, v.inner, f))?;
602    }
603    Ok(v)
604  }
605
606  /// Allocate and initialise a boolean [`PrimOpValue`].
607  ///
608  /// # Errors
609  ///
610  /// Returns an error if allocation or initialisation fails.
611  pub fn make_bool(&self, b: bool) -> Result<PrimOpValue<'a>> {
612    let v = PrimOpValue::alloc(self.ctx, self.state)?;
613    unsafe {
614      check_err(self.ctx, sys::nix_init_bool(self.ctx, v.inner, b))?;
615    }
616    Ok(v)
617  }
618
619  /// Allocate and initialise a null [`PrimOpValue`].
620  ///
621  /// # Errors
622  ///
623  /// Returns an error if allocation or initialisation fails.
624  pub fn make_null(&self) -> Result<PrimOpValue<'a>> {
625    let v = PrimOpValue::alloc(self.ctx, self.state)?;
626    unsafe {
627      check_err(self.ctx, sys::nix_init_null(self.ctx, v.inner))?;
628    }
629    Ok(v)
630  }
631
632  /// Allocate and initialise a string [`PrimOpValue`].
633  ///
634  /// # Errors
635  ///
636  /// Returns an error if allocation, string conversion, or initialisation
637  /// fails.
638  pub fn make_string(&self, s: &str) -> Result<PrimOpValue<'a>> {
639    let v = PrimOpValue::alloc(self.ctx, self.state)?;
640    let s_c = CString::new(s)?;
641    unsafe {
642      check_err(
643        self.ctx,
644        sys::nix_init_string(self.ctx, v.inner, s_c.as_ptr()),
645      )?;
646    }
647    Ok(v)
648  }
649
650  /// Allocate and initialise a path [`PrimOpValue`].
651  ///
652  /// # Pure Evaluation
653  ///
654  /// This calls `nix_init_path_string`, which the Nix evaluator rejects for
655  /// absolute paths when running with `--pure-eval`.  If the path is a store
656  /// path your primop already added to the store, use
657  /// [`make_store_path`](Self::make_store_path) (requires the `shim` feature)
658  /// instead; it registers the path with the evaluator's allowlist so the
659  /// value is usable in pure mode.
660  ///
661  /// # Errors
662  ///
663  /// Returns an error if allocation, string conversion, or initialisation
664  /// fails, or Nix is running in pure evaluation mode and `p` is absolute.
665  pub fn make_path(&self, p: &str) -> Result<PrimOpValue<'a>> {
666    let v = PrimOpValue::alloc(self.ctx, self.state)?;
667    let p_c = CString::new(p)?;
668    unsafe {
669      check_err(
670        self.ctx,
671        sys::nix_init_path_string(self.ctx, self.state, v.inner, p_c.as_ptr()),
672      )?;
673    }
674    Ok(v)
675  }
676
677  /// Type-safe variant of [`make_store_path`](Self::make_store_path).
678  ///
679  /// Takes a parsed [`StorePath`](crate::store::StorePath) and renders it
680  /// to a canonical string via
681  /// [`Store::print_path`](crate::store::Store::print_path).
682  ///
683  /// Requires the `shim` and `store` features.
684  ///
685  /// # Errors
686  ///
687  /// Returns an error if rendering or initialisation fails.
688  #[cfg(all(feature = "shim", feature = "store"))]
689  pub fn make_store_path_typed(
690    &self,
691    store: &crate::store::Store,
692    path: &crate::store::StorePath,
693  ) -> Result<PrimOpValue<'a>> {
694    let rendered = store.print_path(path)?;
695    self.make_store_path(&rendered)
696  }
697
698  /// Allocate and initialise a store path [`PrimOpValue`], registering it as
699  /// accessible in the evaluator's allowlist first.
700  ///
701  /// Equivalent to [`set_store_path`](Self::set_store_path) but allocates and
702  /// returns a new [`PrimOpValue`] instead of writing into the return slot.
703  /// See [`set_store_path`](Self::set_store_path) for the full description of
704  /// the allowlist mechanism.
705  ///
706  /// The caller must ensure `p` is a canonical store path that already exists
707  /// in the store.
708  ///
709  /// # Errors
710  ///
711  /// Returns an error if allocation, string conversion, `p` is not a valid
712  /// store path, or initialisation fails.
713  #[cfg(feature = "shim")]
714  pub fn make_store_path(&self, p: &str) -> Result<PrimOpValue<'a>> {
715    let v = PrimOpValue::alloc(self.ctx, self.state)?;
716    let p_c = CString::new(p)?;
717    unsafe {
718      check_err(
719        self.ctx,
720        sys::nix_eval_state_allow_path(self.ctx, self.state, p_c.as_ptr()),
721      )?;
722      check_err(
723        self.ctx,
724        sys::nix_init_path_string(self.ctx, self.state, v.inner, p_c.as_ptr()),
725      )?;
726    }
727    Ok(v)
728  }
729}
730
731/// An owned Nix value used within a primop callback.
732///
733/// Unlike [`PrimOpArg`], this owns a GC reference to the underlying value
734/// and decrements it on drop. It is returned by [`ArgAttrs::get`] and
735/// [`ArgList::get`] when extracting child values from collections.
736///
737/// Methods mirror those of [`PrimOpArg`]: `value_type`, `force`, `as_int`,
738/// `as_float`, `as_bool`, `as_string`, `as_attrs`, and `as_list` are all
739/// available.
740pub struct PrimOpValue<'a> {
741  inner:    *mut sys::nix_value,
742  ctx:      *mut sys::nix_c_context,
743  state:    *mut sys::EvalState,
744  _phantom: PhantomData<&'a ()>,
745}
746
747// `PrimOpValue` holds a GC reference (released on Drop) plus raw context
748// and state pointers borrowed from the callback frame, expressed by
749// `'a`. The `PhantomData<&'a ()>` makes it `!Send + !Sync` via the
750// auto-trait. The wrapper must not outlive the trampoline frame: doing
751// so would dereference a dangling `EvalState*` on the next force/decref.
752
753impl NixValueRaw for PrimOpValue<'_> {
754  fn raw_ctx(&self) -> *mut sys::nix_c_context {
755    self.ctx
756  }
757
758  fn raw_state(&self) -> *mut sys::EvalState {
759    self.state
760  }
761
762  fn raw_inner(&self) -> *mut sys::nix_value {
763    self.inner
764  }
765}
766
767impl<'a> PrimOpValue<'a> {
768  fn alloc(
769    ctx: *mut sys::nix_c_context,
770    state: *mut sys::EvalState,
771  ) -> Result<Self> {
772    let inner = unsafe { sys::nix_alloc_value(ctx, state) };
773    if inner.is_null() {
774      return Err(Error::NullPointer);
775    }
776    Ok(PrimOpValue {
777      inner,
778      ctx,
779      state,
780      _phantom: PhantomData,
781    })
782  }
783
784  // Scalar accessors (value_type, force, as_int, as_float, as_bool,
785  // as_string, as_path) live on the NixValueOps trait; bring it into
786  // scope to use them.
787
788  /// Interpret this value as an attribute set.
789  ///
790  /// Automatically forces the value if it is a thunk.
791  ///
792  /// Returns an [`ArgAttrs`] wrapper that borrows from this value.
793  ///
794  /// # Errors
795  ///
796  /// Returns an error if forcing fails or the resolved value is not an
797  /// attribute set.
798  pub fn as_attrs(&self) -> Result<ArgAttrs<'_>> {
799    self.force()?;
800    if self.value_type() != ValueType::Attrs {
801      return Err(Error::InvalidType {
802        expected: "attrs",
803        actual:   self.value_type().to_string(),
804      });
805    }
806    Ok(ArgAttrs {
807      inner:    self.inner,
808      ctx:      self.ctx,
809      state:    self.state,
810      _phantom: PhantomData,
811    })
812  }
813
814  /// Interpret this value as a list.
815  ///
816  /// Automatically forces the value if it is a thunk.
817  ///
818  /// Returns an [`ArgList`] wrapper that borrows from this value.
819  ///
820  /// # Errors
821  ///
822  /// Returns an error if forcing fails or the resolved value is not a
823  /// list.
824  pub fn as_list(&self) -> Result<ArgList<'_>> {
825    self.force()?;
826    if self.value_type() != ValueType::List {
827      return Err(Error::InvalidType {
828        expected: "list",
829        actual:   self.value_type().to_string(),
830      });
831    }
832    Ok(ArgList {
833      inner:    self.inner,
834      ctx:      self.ctx,
835      state:    self.state,
836      _phantom: PhantomData,
837    })
838  }
839}
840
841impl Drop for PrimOpValue<'_> {
842  fn drop(&mut self) {
843    // SAFETY: ctx and inner are valid; we own a GC reference
844    unsafe {
845      sys::nix_value_decref(self.ctx, self.inner);
846    }
847  }
848}
849/// A borrowed Nix attribute set value.
850///
851/// Obtained from [`PrimOpArg::as_attrs`] or [`PrimOpValue::as_attrs`].
852/// Provides read access to the attribute set without owning any GC
853/// references itself.
854pub struct ArgAttrs<'a> {
855  inner:    *mut sys::nix_value,
856  ctx:      *mut sys::nix_c_context,
857  state:    *mut sys::EvalState,
858  _phantom: PhantomData<&'a ()>,
859}
860
861// `ArgAttrs` borrows from a callback-frame value through `'a`. Like
862// `PrimOpArg`, the raw context and state pointers are valid only while
863// the trampoline runs. We rely on the auto-trait derivation to keep the
864// type `!Send + !Sync`.
865
866impl<'a> ArgAttrs<'a> {
867  /// Get an attribute by name.
868  ///
869  /// Returns an owned [`PrimOpValue`] that holds a GC reference to the
870  /// attribute's value. The caller is responsible for the GC reference;
871  /// [`PrimOpValue`] releases it on drop. The value carries the same
872  /// callback-frame lifetime as this attribute-set wrapper.
873  ///
874  /// # Errors
875  ///
876  /// Returns [`Error::KeyNotFound`] if the key does not exist.
877  pub fn get(&self, key: &str) -> Result<PrimOpValue<'a>> {
878    let key_c = CString::new(key)?;
879    // SAFETY: ctx, value, and state are valid
880    let ptr = unsafe {
881      sys::nix_get_attr_byname(self.ctx, self.inner, self.state, key_c.as_ptr())
882    };
883    if ptr.is_null() {
884      return Err(Error::KeyNotFound(key.to_string()));
885    }
886    Ok(PrimOpValue {
887      inner:    ptr,
888      ctx:      self.ctx,
889      state:    self.state,
890      _phantom: PhantomData,
891    })
892  }
893
894  /// Check if an attribute exists.
895  ///
896  /// # Errors
897  ///
898  /// Returns an error if the lookup itself fails (not if the key is
899  /// absent).
900  pub fn has(&self, key: &str) -> Result<bool> {
901    let key_c = CString::new(key)?;
902    // SAFETY: ctx, value, and state are valid
903    let result = unsafe {
904      sys::nix_has_attr_byname(self.ctx, self.inner, self.state, key_c.as_ptr())
905    };
906    Ok(result)
907  }
908
909  /// Return all attribute keys in this set.
910  ///
911  /// # Errors
912  ///
913  /// Returns an error if iteration fails.
914  pub fn keys(&self) -> Result<Vec<String>> {
915    // SAFETY: ctx and value are valid
916    let count = unsafe { sys::nix_get_attrs_size(self.ctx, self.inner) };
917
918    let mut keys = Vec::with_capacity(count as usize);
919    for i in 0..count {
920      // Use the name-only API to avoid alloc/decref of an unused value.
921      let name_ptr = unsafe {
922        sys::nix_get_attr_name_byidx(self.ctx, self.inner, self.state, i)
923      };
924      if name_ptr.is_null() {
925        continue;
926      }
927      let name = unsafe {
928        std::ffi::CStr::from_ptr(name_ptr)
929          .to_string_lossy()
930          .into_owned()
931      };
932      keys.push(name);
933    }
934    Ok(keys)
935  }
936
937  /// Return the number of attributes in this set.
938  #[must_use]
939  pub fn len(&self) -> usize {
940    unsafe { sys::nix_get_attrs_size(self.ctx, self.inner) as usize }
941  }
942
943  /// Return `true` if the attribute set is empty.
944  #[must_use]
945  pub fn is_empty(&self) -> bool {
946    self.len() == 0
947  }
948}
949
950/// A borrowed Nix list value.
951///
952/// Obtained from [`PrimOpArg::as_list`] or [`PrimOpValue::as_list`].
953/// Provides read access to the list without owning any GC references
954/// itself.
955pub struct ArgList<'a> {
956  inner:    *mut sys::nix_value,
957  ctx:      *mut sys::nix_c_context,
958  state:    *mut sys::EvalState,
959  _phantom: PhantomData<&'a ()>,
960}
961
962// `ArgList` borrows from a callback-frame value through `'a`. Same
963// rationale as `ArgAttrs` and `PrimOpArg`: auto-trait leaves the type
964// `!Send + !Sync`, which prevents the user from escaping the trampoline
965// with a dangling state pointer.
966
967impl<'a> ArgList<'a> {
968  /// Get an element by index.
969  ///
970  /// Returns an owned [`PrimOpValue`] that holds a GC reference to the
971  /// element. The caller is responsible for the GC reference;
972  /// [`PrimOpValue`] releases it on drop. The value carries the same
973  /// callback-frame lifetime as this list wrapper.
974  ///
975  /// # Errors
976  ///
977  /// Returns [`Error::IndexOutOfBounds`] if `index >= self.len()`.
978  pub fn get(&self, index: usize) -> Result<PrimOpValue<'a>> {
979    let length = self.len();
980    if index >= length {
981      return Err(Error::IndexOutOfBounds { index, length });
982    }
983    // SAFETY: ctx, value, and state are valid; index is bounds-checked
984    let ptr = unsafe {
985      sys::nix_get_list_byidx(
986        self.ctx,
987        self.inner,
988        self.state,
989        index as std::os::raw::c_uint,
990      )
991    };
992    if ptr.is_null() {
993      return Err(Error::NullPointer);
994    }
995    Ok(PrimOpValue {
996      inner:    ptr,
997      ctx:      self.ctx,
998      state:    self.state,
999      _phantom: PhantomData,
1000    })
1001  }
1002
1003  /// Return the number of elements in this list.
1004  #[must_use]
1005  pub fn len(&self) -> usize {
1006    unsafe { sys::nix_get_list_size(self.ctx, self.inner) as usize }
1007  }
1008
1009  /// Return `true` if the list is empty.
1010  #[must_use]
1011  pub fn is_empty(&self) -> bool {
1012    self.len() == 0
1013  }
1014}
1015
1016/// A Nix primitive operation (primop) wrapping a Rust closure.
1017///
1018/// After construction with [`PrimOp::new`], the primop can be:
1019///
1020/// * Registered as a **global builtin** via [`PrimOp::register`] (must happen
1021///   before creating any [`EvalState`](crate::EvalState)).
1022/// * Embedded in a **value** via [`PrimOp::into_value`] for use as a
1023///   first-class function inside an [`EvalState`](crate::EvalState).
1024pub struct PrimOp {
1025  /// GC-owned pointer; we hold one reference until consumed.
1026  inner:      *mut sys::PrimOp,
1027  /// Keeps the context alive for the lifetime of the PrimOp.
1028  context:    Arc<Context>,
1029  /// The name passed to [`PrimOp::new`], retained for duplicate-registration
1030  /// diagnostics.
1031  name:       String,
1032  /// Set to `true` after `register()` so Drop skips the decref.
1033  registered: bool,
1034}
1035
1036/// Names of primops that have been registered as global builtins through
1037/// [`PrimOp::register`].  Used to fail loudly when the same name is offered
1038/// twice; the Nix C API silently accepts that today.
1039static REGISTERED_PRIMOPS: OnceLock<Mutex<HashSet<String>>> = OnceLock::new();
1040
1041// SAFETY: `PrimOp` owns its GC-allocated `*mut sys::PrimOp` until it is
1042// either consumed by `register` or `into_value`, or released by `Drop`.
1043// The GC-managed object itself is safe to hand off to another thread;
1044// the `ClosureData` it points at is reachable only via the GC pointer.
1045// `Arc<Context>` keeps the context alive on the destination thread.
1046//
1047// `Sync` is NOT implemented: `register` and `into_value` both mutate
1048// state through `Context`'s racy error buffer.
1049unsafe impl Send for PrimOp {}
1050
1051impl PrimOp {
1052  /// Create a new primop backed by the given Rust closure.
1053  ///
1054  /// # Arguments
1055  ///
1056  /// * `context`: the Nix context.
1057  /// * `name`: the name of the primop as it will appear in Nix.
1058  /// * `arity`: number of arguments the primop accepts.
1059  /// * `doc`: optional documentation string.
1060  /// * `f`: the Rust closure to invoke.
1061  ///
1062  /// # Errors
1063  ///
1064  /// Returns an error if the name or doc string contains an interior NUL
1065  /// byte, or if the underlying allocation fails.
1066  pub fn new<F>(
1067    context: &Arc<Context>,
1068    name: &str,
1069    arity: u32,
1070    doc: Option<&str>,
1071    f: F,
1072  ) -> Result<Self>
1073  where
1074    F: Fn(&[PrimOpArg<'_>], &mut PrimOpRet<'_>) -> Result<()>
1075      + Send
1076      + Sync
1077      + 'static,
1078  {
1079    let name_c = CString::new(name)?;
1080    let doc_c = doc.map(CString::new).transpose()?;
1081    // PrimOp::doc is std::optional<std::string> since Nix 2.34.
1082    // Passing null would throw "construction from null".
1083    let empty_doc;
1084    let doc_ptr = match doc_c {
1085      Some(ref c) => c.as_ptr(),
1086      None => {
1087        empty_doc = CString::default();
1088        empty_doc.as_ptr()
1089      },
1090    };
1091
1092    // Box the closure together with its arity for the trampoline.
1093    let data = Box::new(ClosureData {
1094      arity: arity as usize,
1095      f:     Box::new(f),
1096    });
1097    let data_raw = Box::into_raw(data) as *mut c_void;
1098
1099    // Allocate the GC-managed PrimOp.
1100    // SAFETY: context is valid; trampoline has the expected C signature.
1101    let primop_ptr = unsafe {
1102      sys::nix_alloc_primop(
1103        context.as_ptr(),
1104        Some(trampoline),
1105        arity as std::os::raw::c_int,
1106        name_c.as_ptr(),
1107        std::ptr::null_mut(), // arg names (optional)
1108        doc_ptr,
1109        data_raw,
1110      )
1111    };
1112
1113    if primop_ptr.is_null() {
1114      let _ = unsafe { Box::from_raw(data_raw as *mut ClosureData) };
1115      // SAFETY: context pointer is valid
1116      unsafe {
1117        check_err(context.as_ptr(), sys::nix_err_code(context.as_ptr()))?;
1118      }
1119      return Err(Error::NullPointer);
1120    }
1121
1122    // Register a GC finalizer so the closure is freed when the PrimOp GC
1123    // object is collected.
1124    // SAFETY: primop_ptr is a valid GC object; data_raw is a valid pointer.
1125    unsafe {
1126      sys::nix_gc_register_finalizer(
1127        primop_ptr as *mut c_void,
1128        data_raw,
1129        Some(drop_closure_finalizer),
1130      );
1131    }
1132
1133    Ok(PrimOp {
1134      inner:      primop_ptr,
1135      context:    Arc::clone(context),
1136      name:       name.to_string(),
1137      registered: false,
1138    })
1139  }
1140
1141  /// Register this primop as a global Nix builtin.
1142  ///
1143  /// After this call the primop will appear in `builtins.*` for all
1144  /// [`EvalState`](crate::EvalState) instances created **after** this call.
1145  ///
1146  /// This consumes `self`; the underlying pointer is transferred to the
1147  /// global registry and is no longer accessible.
1148  ///
1149  /// # Errors
1150  ///
1151  /// Returns an error if the registration fails.
1152  pub fn register(mut self, context: &Context) -> Result<()> {
1153    // Guard against duplicate-name registration. The Nix C API does not
1154    // diagnose this; the second registration silently shadows or is dropped
1155    // depending on internal ordering, and the user gets no feedback.
1156    {
1157      let names = REGISTERED_PRIMOPS.get_or_init(|| Mutex::new(HashSet::new()));
1158      let mut guard = names.lock().expect("REGISTERED_PRIMOPS poisoned");
1159      if !guard.insert(self.name.clone()) {
1160        return Err(Error::Unknown(format!(
1161          "primop '{}' is already registered globally",
1162          self.name
1163        )));
1164      }
1165    }
1166
1167    // SAFETY: context and inner are valid
1168    let err = unsafe { sys::nix_register_primop(context.as_ptr(), self.inner) };
1169    check_err(unsafe { self.context.as_ptr() }, err)?;
1170    // Mark as registered only after confirmed success so Drop still calls
1171    // nix_gc_decref if registration fails.
1172    self.registered = true;
1173    Ok(())
1174  }
1175
1176  /// Embed this primop in a Nix value, returning a callable
1177  /// [`Value`](crate::Value).
1178  ///
1179  /// This consumes `self`.  The returned value holds a GC reference to the
1180  /// primop, keeping it alive for the lifetime of the value.
1181  ///
1182  /// # Errors
1183  ///
1184  /// Returns an error if the value allocation or initialisation fails.
1185  pub fn into_value(
1186    self,
1187    state: &crate::EvalState,
1188  ) -> Result<crate::Value<'_>> {
1189    let v = state.alloc_value()?;
1190    // SAFETY: context, value, and primop pointer are valid
1191    unsafe {
1192      check_err(
1193        state.context.as_ptr(),
1194        sys::nix_init_primop(
1195          state.context.as_ptr(),
1196          v.inner.as_ptr(),
1197          self.inner,
1198        ),
1199      )?;
1200    }
1201    // `self` drops here; the value holds the GC ref via nix_init_primop so
1202    // our own reference can be released.
1203    Ok(v)
1204  }
1205}
1206
1207impl Drop for PrimOp {
1208  fn drop(&mut self) {
1209    if !self.registered && !self.inner.is_null() {
1210      // Release our GC reference.  The GC may still keep the object alive
1211      // until it is collected, at which point the finalizer frees the
1212      // closure.
1213      // SAFETY: ctx and inner are valid
1214      unsafe {
1215        let _ = sys::nix_gc_decref(
1216          self.context.as_ptr(),
1217          self.inner as *const c_void,
1218        );
1219      }
1220    }
1221  }
1222}
1223
1224#[cfg(test)]
1225mod tests {
1226  use std::sync::Arc;
1227
1228  use serial_test::serial;
1229
1230  use super::*;
1231  use crate::{Context, EvalStateBuilder, Store};
1232
1233  #[test]
1234  #[serial]
1235  fn test_primop_into_value() {
1236    let ctx = Arc::new(Context::new().expect("Failed to create context"));
1237    let store =
1238      Arc::new(Store::open(&ctx, None).expect("Failed to open store"));
1239    let state = EvalStateBuilder::new(&store)
1240      .expect("Failed to create builder")
1241      .build()
1242      .expect("Failed to build state");
1243
1244    // A primop that negates an integer
1245    let primop =
1246      PrimOp::new(&ctx, "negate", 1, Some("Negate an integer"), |args, ret| {
1247        let n = args[0].as_int()?;
1248        ret.set_int(-n)
1249      })
1250      .expect("Failed to create primop");
1251
1252    let func = primop
1253      .into_value(&state)
1254      .expect("Failed to embed primop as value");
1255
1256    let arg = state.make_int(7).unwrap();
1257    let result = func.call(&arg).expect("Failed to call primop");
1258    assert_eq!(result.as_int().unwrap(), -7);
1259  }
1260
1261  #[test]
1262  #[serial]
1263  fn test_primop_into_value_string() {
1264    let ctx = Arc::new(Context::new().expect("Failed to create context"));
1265    let store =
1266      Arc::new(Store::open(&ctx, None).expect("Failed to open store"));
1267    let state = EvalStateBuilder::new(&store)
1268      .expect("Failed to create builder")
1269      .build()
1270      .expect("Failed to build state");
1271
1272    // A primop that returns a constant string
1273    let primop = PrimOp::new(&ctx, "hello", 1, None, |_args, ret| {
1274      ret.set_string("hello from primop")
1275    })
1276    .expect("Failed to create primop");
1277
1278    let func = primop
1279      .into_value(&state)
1280      .expect("Failed to embed primop as value");
1281
1282    let arg = state.make_null().unwrap();
1283    let result = func.call(&arg).expect("Failed to call primop");
1284    assert_eq!(result.as_string().unwrap(), "hello from primop");
1285  }
1286
1287  #[test]
1288  #[serial]
1289  fn test_primop_path_roundtrip() {
1290    let ctx = Arc::new(Context::new().expect("Failed to create context"));
1291    let store =
1292      Arc::new(Store::open(&ctx, None).expect("Failed to open store"));
1293    let state = EvalStateBuilder::new(&store)
1294      .expect("Failed to create builder")
1295      .build()
1296      .expect("Failed to build state");
1297
1298    // Read the path arg with as_path, exercise make_path on PrimOpRet, then
1299    // write the result back via set_path. This covers all three new methods
1300    // in one call.
1301    let primop = PrimOp::new(&ctx, "echo_path", 1, None, |args, ret| {
1302      let p = args[0].as_path()?;
1303      assert_eq!(p, "/tmp/nix-bindings-path-in");
1304      // Exercise make_path; we don't actually use the value, just confirm it
1305      // round-trips through PrimOpValue::as_path.
1306      let v = ret.make_path("/tmp/nix-bindings-path-mid")?;
1307      assert_eq!(v.as_path()?, "/tmp/nix-bindings-path-mid");
1308      ret.set_path("/tmp/nix-bindings-path-out")
1309    })
1310    .expect("Failed to create primop");
1311
1312    let func = primop
1313      .into_value(&state)
1314      .expect("Failed to embed primop as value");
1315
1316    let arg = state
1317      .make_path("/tmp/nix-bindings-path-in")
1318      .expect("make_path failed");
1319    let result = func.call(&arg).expect("Failed to call primop");
1320    assert_eq!(result.value_type(), ValueType::Path);
1321    let out = result.as_path().expect("Value::as_path failed");
1322    assert_eq!(out.to_str(), Some("/tmp/nix-bindings-path-out"));
1323  }
1324
1325  #[test]
1326  #[serial]
1327  fn test_primop_arg_as_list() {
1328    let ctx = Arc::new(Context::new().expect("Failed to create context"));
1329    let store =
1330      Arc::new(Store::open(&ctx, None).expect("Failed to open store"));
1331    let state = EvalStateBuilder::new(&store)
1332      .expect("Failed to create builder")
1333      .build()
1334      .expect("Failed to build state");
1335
1336    let list_val = state
1337      .eval_from_string("[10 20 30]", "<eval>")
1338      .expect("Failed to evaluate list");
1339
1340    // A primop that reads list args and returns the sum
1341    let primop = PrimOp::new(&ctx, "list_sum", 1, None, |args, ret| {
1342      let list = args[0].as_list()?;
1343      let mut sum = 0i64;
1344      for i in 0..list.len() {
1345        sum += list.get(i)?.as_int()?;
1346      }
1347      ret.set_int(sum)
1348    })
1349    .expect("Failed to create primop");
1350
1351    let func = primop
1352      .into_value(&state)
1353      .expect("Failed to embed primop as value");
1354
1355    let result = func.call(&list_val).expect("Failed to call primop");
1356    assert_eq!(result.as_int().unwrap(), 60);
1357  }
1358
1359  #[test]
1360  #[serial]
1361  fn test_primop_arg_as_attrs() {
1362    let ctx = Arc::new(Context::new().expect("Failed to create context"));
1363    let store =
1364      Arc::new(Store::open(&ctx, None).expect("Failed to open store"));
1365    let state = EvalStateBuilder::new(&store)
1366      .expect("Failed to create builder")
1367      .build()
1368      .expect("Failed to build state");
1369
1370    let attrs_val = state
1371      .eval_from_string("{ foo = 42; bar = 13; }", "<eval>")
1372      .expect("Failed to evaluate attrs");
1373
1374    // A primop that sums two named attributes
1375    let primop = PrimOp::new(&ctx, "attr_sum", 1, None, |args, ret| {
1376      let attrs = args[0].as_attrs()?;
1377      let foo = attrs.get("foo")?.as_int()?;
1378      let bar = attrs.get("bar")?.as_int()?;
1379      ret.set_int(foo + bar)
1380    })
1381    .expect("Failed to create primop");
1382
1383    let func = primop
1384      .into_value(&state)
1385      .expect("Failed to embed primop as value");
1386
1387    let result = func.call(&attrs_val).expect("Failed to call primop");
1388    assert_eq!(result.as_int().unwrap(), 55);
1389  }
1390
1391  #[test]
1392  #[serial]
1393  fn test_primop_arg_attrs_has_and_keys() {
1394    let ctx = Arc::new(Context::new().expect("Failed to create context"));
1395    let store =
1396      Arc::new(Store::open(&ctx, None).expect("Failed to open store"));
1397    let state = EvalStateBuilder::new(&store)
1398      .expect("Failed to create builder")
1399      .build()
1400      .expect("Failed to build state");
1401
1402    let attrs_val = state
1403      .eval_from_string("{ a = 1; b = 2; c = 3; }", "<eval>")
1404      .expect("Failed to evaluate attrs");
1405
1406    let primop = PrimOp::new(&ctx, "check_attrs", 1, None, |args, ret| {
1407      let attrs = args[0].as_attrs()?;
1408      assert_eq!(attrs.len(), 3);
1409      assert!(!attrs.is_empty());
1410      assert!(attrs.has("a")?);
1411      assert!(attrs.has("b")?);
1412      assert!(attrs.has("c")?);
1413      assert!(!attrs.has("zzz")?);
1414      let keys = attrs.keys()?;
1415      assert_eq!(keys.len(), 3);
1416      ret.set_null()
1417    })
1418    .expect("Failed to create primop");
1419
1420    let func = primop
1421      .into_value(&state)
1422      .expect("Failed to embed primop as value");
1423
1424    func.call(&attrs_val).expect("Failed to call primop");
1425  }
1426
1427  #[test]
1428  #[serial]
1429  fn test_primop_empty_attrs_and_list() {
1430    let ctx = Arc::new(Context::new().expect("Failed to create context"));
1431    let store =
1432      Arc::new(Store::open(&ctx, None).expect("Failed to open store"));
1433    let state = EvalStateBuilder::new(&store)
1434      .expect("Failed to create builder")
1435      .build()
1436      .expect("Failed to build state");
1437
1438    let empty_attrs = state
1439      .eval_from_string("{}", "<eval>")
1440      .expect("Failed to evaluate empty attrs");
1441
1442    let primop = PrimOp::new(&ctx, "empty_check", 1, None, |args, ret| {
1443      match args[0].as_attrs() {
1444        Ok(a) => {
1445          assert!(a.is_empty());
1446          assert_eq!(a.len(), 0);
1447        },
1448        Err(_) => {
1449          let list = args[0].as_list()?;
1450          assert!(list.is_empty());
1451          assert_eq!(list.len(), 0);
1452        },
1453      }
1454      ret.set_null()
1455    })
1456    .expect("Failed to create primop");
1457
1458    let func = primop
1459      .into_value(&state)
1460      .expect("Failed to embed primop as value");
1461
1462    func
1463      .call(&empty_attrs)
1464      .expect("call with empty attrs failed");
1465
1466    let empty_list = state
1467      .eval_from_string("[]", "<eval>")
1468      .expect("Failed to evaluate empty list");
1469    func.call(&empty_list).expect("call with empty list failed");
1470  }
1471
1472  #[test]
1473  #[serial]
1474  fn test_primop_ret_set_attrs() {
1475    let ctx = Arc::new(Context::new().expect("Failed to create context"));
1476    let store =
1477      Arc::new(Store::open(&ctx, None).expect("Failed to open store"));
1478
1479    // Register a nullary primop that constructs and returns an attrset.
1480    PrimOp::new(&ctx, "mk_attrs_test", 0, None, |_args, ret| {
1481      let a = ret.make_int(100)?;
1482      let b = ret.make_string("hi")?;
1483      ret.set_attrs(&[("x", &a), ("y", &b)])
1484    })
1485    .expect("Failed to create primop")
1486    .register(&ctx)
1487    .expect("Failed to register primop");
1488
1489    let state = EvalStateBuilder::new(&store)
1490      .expect("Failed to create builder")
1491      .build()
1492      .expect("Failed to build state");
1493
1494    let result = state
1495      .eval_from_string("builtins.mk_attrs_test", "<eval>")
1496      .expect("Failed to evaluate expression");
1497    assert_eq!(result.value_type(), ValueType::Attrs);
1498    assert_eq!(result.attr_keys().unwrap().len(), 2);
1499    let x = result.get_attr("x").expect("missing x");
1500    assert_eq!(x.as_int().unwrap(), 100);
1501    let y = result.get_attr("y").expect("missing y");
1502    assert_eq!(y.as_string().unwrap(), "hi");
1503  }
1504
1505  #[test]
1506  #[serial]
1507  fn test_primop_ret_set_list() {
1508    let ctx = Arc::new(Context::new().expect("Failed to create context"));
1509    let store =
1510      Arc::new(Store::open(&ctx, None).expect("Failed to open store"));
1511
1512    // Register a nullary primop that constructs and returns a list.
1513    PrimOp::new(&ctx, "mk_list_test", 0, None, |_args, ret| {
1514      let a = ret.make_int(7)?;
1515      let b = ret.make_string("hi")?;
1516      let c = ret.make_bool(true)?;
1517      ret.set_list(&[&a, &b, &c])
1518    })
1519    .expect("Failed to create primop")
1520    .register(&ctx)
1521    .expect("Failed to register primop");
1522
1523    let state = EvalStateBuilder::new(&store)
1524      .expect("Failed to create builder")
1525      .build()
1526      .expect("Failed to build state");
1527
1528    let result = state
1529      .eval_from_string("builtins.mk_list_test", "<eval>")
1530      .expect("Failed to evaluate expression");
1531    assert_eq!(result.value_type(), ValueType::List);
1532    assert_eq!(result.list_len().unwrap(), 3);
1533
1534    let first = result.list_get(0).unwrap();
1535    assert_eq!(first.as_int().unwrap(), 7);
1536    let second = result.list_get(1).unwrap();
1537    assert_eq!(second.as_string().unwrap(), "hi");
1538    let third = result.list_get(2).unwrap();
1539    assert!(third.as_bool().unwrap());
1540  }
1541
1542  #[test]
1543  #[serial]
1544  fn test_primop_ret_make_types() {
1545    let ctx = Arc::new(Context::new().expect("Failed to create context"));
1546    let store =
1547      Arc::new(Store::open(&ctx, None).expect("Failed to open store"));
1548
1549    // Test each make_* method returns correct type and value.
1550    PrimOp::new(&ctx, "mk_types_test", 0, None, |_args, ret| {
1551      let int_val = ret.make_int(-42)?;
1552      assert_eq!(int_val.value_type(), ValueType::Int);
1553      assert_eq!(int_val.as_int()?, -42);
1554
1555      let float_val = ret.make_float(2.5)?;
1556      assert_eq!(float_val.value_type(), ValueType::Float);
1557      assert!((float_val.as_float()? - 2.5).abs() < 1e-9);
1558
1559      let bool_val = ret.make_bool(true)?;
1560      assert_eq!(bool_val.value_type(), ValueType::Bool);
1561      assert!(bool_val.as_bool()?);
1562
1563      let null_val = ret.make_null()?;
1564      assert_eq!(null_val.value_type(), ValueType::Null);
1565
1566      let str_val = ret.make_string("ok")?;
1567      assert_eq!(str_val.value_type(), ValueType::String);
1568      assert_eq!(str_val.as_string()?, "ok");
1569
1570      ret.set_int(0)
1571    })
1572    .expect("Failed to create primop")
1573    .register(&ctx)
1574    .expect("Failed to register primop");
1575
1576    let state = EvalStateBuilder::new(&store)
1577      .expect("Failed to create builder")
1578      .build()
1579      .expect("Failed to build state");
1580
1581    let result = state
1582      .eval_from_string("builtins.mk_types_test", "<eval>")
1583      .expect("Failed to evaluate expression");
1584    assert_eq!(result.as_int().unwrap(), 0);
1585  }
1586
1587  #[test]
1588  #[serial]
1589  fn test_primop_value_as_attrs_chained() {
1590    let ctx = Arc::new(Context::new().expect("Failed to create context"));
1591    let store =
1592      Arc::new(Store::open(&ctx, None).expect("Failed to open store"));
1593    let state = EvalStateBuilder::new(&store)
1594      .expect("Failed to create builder")
1595      .build()
1596      .expect("Failed to build state");
1597
1598    let nested = state
1599      .eval_from_string("{ inner = { x = 99; }; }", "<eval>")
1600      .expect("Failed to evaluate nested attrs");
1601
1602    let primop = PrimOp::new(&ctx, "nested_get", 1, None, |args, ret| {
1603      let outer = args[0].as_attrs()?;
1604      let inner = outer.get("inner")?;
1605      let inner_attrs = inner.as_attrs()?;
1606      let x = inner_attrs.get("x")?.as_int()?;
1607      ret.set_int(x)
1608    })
1609    .expect("Failed to create primop");
1610
1611    let func = primop
1612      .into_value(&state)
1613      .expect("Failed to embed primop as value");
1614
1615    let result = func.call(&nested).expect("Failed to call primop");
1616    assert_eq!(result.as_int().unwrap(), 99);
1617  }
1618
1619  #[test]
1620  #[serial]
1621  fn test_primop_unwritten_slot_is_diagnosed() {
1622    let ctx = Arc::new(Context::new().expect("Failed to create context"));
1623    let store =
1624      Arc::new(Store::open(&ctx, None).expect("Failed to open store"));
1625    let state = EvalStateBuilder::new(&store)
1626      .expect("Failed to create builder")
1627      .build()
1628      .expect("Failed to build state");
1629
1630    // Closure deliberately returns Ok(()) without calling any set_* on the
1631    // return slot. The trampoline must catch this and produce a Nix-side
1632    // eval error.
1633    let primop =
1634      PrimOp::new(&ctx, "broken_primop", 1, None, |_args, _ret| Ok(()))
1635        .expect("Failed to create primop");
1636    let func = primop
1637      .into_value(&state)
1638      .expect("Failed to embed primop as value");
1639    let arg = state.make_int(1).expect("Failed to make int");
1640    let result = func.call(&arg);
1641    assert!(
1642      result.is_err(),
1643      "Expected an error when the primop returns without writing the slot, \
1644       got {:?}",
1645      result.map(|v| v.value_type()),
1646    );
1647  }
1648}