Skip to main content

nix_bindings/
external.rs

1//! External Nix values backed by Rust data.
2//!
3//! - [`NixExternal`]: trait your data type must implement.
4//! - [`EvalState::make_external`](crate::EvalState::make_external): wraps a
5//!   value and returns an [`ExternalValueHandle`].
6//! - [`ExternalValueHandle::as_external`]: downcasts back to a concrete Rust
7//!   type.
8//!
9//! A [`TypeId`] is stored alongside each data pointer.
10//! [`ExternalValueHandle::as_external`] checks it before returning a reference,
11//! so a wrong-type downcast returns [`Error::InvalidType`] rather than UB.
12//!
13//! # Example
14//!
15//! ```rust,no_run
16//! use std::sync::Arc;
17//!
18//! use nix_bindings::{Context, EvalStateBuilder, Store, external::NixExternal};
19//!
20//! struct MyData(i64);
21//!
22//! impl NixExternal for MyData {
23//!   fn display(&self) -> String {
24//!     format!("MyData({})", self.0)
25//!   }
26//!   fn type_name(&self) -> &'static str {
27//!     "MyData"
28//!   }
29//! }
30//!
31//! fn main() -> Result<(), Box<dyn std::error::Error>> {
32//!   let ctx = Arc::new(Context::new()?);
33//!   let store = Arc::new(Store::open(&ctx, None)?);
34//!   let state = EvalStateBuilder::new(&store)?.build()?;
35//!
36//!   let handle = state.make_external(MyData(42))?;
37//!   let back = handle.as_external::<MyData>()?;
38//!   assert_eq!(back.0, 42);
39//!
40//!   Ok(())
41//! }
42//! ```
43
44use std::{
45  any::TypeId,
46  ffi::CString,
47  ops::Deref,
48  panic::{self, AssertUnwindSafe},
49};
50
51use crate::{Error, EvalState, Result, Value, sys};
52
53/// A Rust type that can be embedded as an external value in the Nix evaluator.
54///
55/// Implementing this trait is the only requirement for storing your type inside
56/// Nix values via [`EvalState::make_external`]. All methods except
57/// [`display`](NixExternal::display) and [`type_name`](NixExternal::type_name)
58/// have default no-op implementations.
59pub trait NixExternal: Send + Sync + 'static {
60  /// Return a human-readable representation of the value.
61  ///
62  /// This is called when Nix prints the value (e.g. in the REPL).
63  fn display(&self) -> String;
64
65  /// Return the type name shown by `:t` in the Nix REPL.
66  fn type_name(&self) -> &'static str;
67
68  /// Try to coerce the value to a string.
69  ///
70  /// Return `Some(s)` to allow coercion; return `None` (the default) to have
71  /// Nix throw an error when coercion is attempted.
72  fn coerce_to_string(&self) -> Option<String> {
73    None
74  }
75
76  /// Test equality with another external value of the same Rust type.
77  ///
78  /// The default implementation returns `false` (values are never equal).
79  fn equal(&self, _other: &Self) -> bool {
80    false
81  }
82}
83
84/// Magic sentinel that prefixes every [`ErasedPayload`].
85///
86/// The `equal` vtable callback is invoked with two raw `void*`s from Nix and
87/// has no protocol-level way to know whether the second pointer originated
88/// from this crate. Reading `type_id` from an unknown payload would be
89/// undefined behaviour, so we check this byte pattern first and bail if it
90/// does not match.
91const ERASED_PAYLOAD_MAGIC: u64 = 0x4E49585F455854; // "NIX_EXT"
92
93/// Heap-allocated wrapper that combines a boxed `T` with its [`TypeId`].
94///
95/// This is the allocation stored behind the `void*` data pointer that is
96/// passed to [`nix_create_external_value`](sys::nix_create_external_value).
97#[repr(C)]
98struct ErasedPayload {
99  magic:   u64,
100  type_id: TypeId,
101
102  // The concrete data follows; we keep only a raw pointer into the T.
103  data: *mut std::os::raw::c_void,
104
105  // Destructor: how to drop the original Box<T>.
106  drop_fn: unsafe fn(*mut std::os::raw::c_void),
107
108  // display(): returns an owned String.
109  display_fn: unsafe fn(*const std::os::raw::c_void) -> String,
110
111  // type_name(): returns a &'static str.
112  type_name_fn: unsafe fn(*const std::os::raw::c_void) -> &'static str,
113
114  // coerce_to_string(): returns Option<String>.
115  coerce_fn: unsafe fn(*const std::os::raw::c_void) -> Option<String>,
116
117  // equal(other_data): compare two ErasedPayload.data pointers of the same T.
118  equal_fn:
119    unsafe fn(*const std::os::raw::c_void, *const std::os::raw::c_void) -> bool,
120}
121
122unsafe fn drop_erased<T>(ptr: *mut std::os::raw::c_void) {
123  drop(unsafe { Box::from_raw(ptr.cast::<T>()) });
124}
125
126unsafe fn display_erased<T: NixExternal>(
127  ptr: *const std::os::raw::c_void,
128) -> String {
129  let t = unsafe { &*(ptr as *const T) };
130  t.display()
131}
132
133unsafe fn type_name_erased<T: NixExternal>(
134  ptr: *const std::os::raw::c_void,
135) -> &'static str {
136  let t = unsafe { &*(ptr as *const T) };
137  t.type_name()
138}
139
140unsafe fn coerce_erased<T: NixExternal>(
141  ptr: *const std::os::raw::c_void,
142) -> Option<String> {
143  let t = unsafe { &*(ptr as *const T) };
144  t.coerce_to_string()
145}
146
147unsafe fn equal_erased<T: NixExternal>(
148  ptr: *const std::os::raw::c_void,
149  other: *const std::os::raw::c_void,
150) -> bool {
151  let a = unsafe { &*(ptr as *const T) };
152  let b = unsafe { &*(other as *const T) };
153  a.equal(b)
154}
155
156impl ErasedPayload {
157  fn new<T: NixExternal>(value: T) -> *mut Self {
158    let data_box = Box::new(value);
159    let data_ptr = Box::into_raw(data_box) as *mut std::os::raw::c_void;
160
161    Box::into_raw(Box::new(ErasedPayload {
162      magic:        ERASED_PAYLOAD_MAGIC,
163      type_id:      TypeId::of::<T>(),
164      data:         data_ptr,
165      drop_fn:      drop_erased::<T>,
166      display_fn:   display_erased::<T>,
167      type_name_fn: type_name_erased::<T>,
168      coerce_fn:    coerce_erased::<T>,
169      equal_fn:     equal_erased::<T>,
170    }))
171  }
172
173  /// Validate the magic sentinel before dereferencing as `ErasedPayload`.
174  ///
175  /// Returns `None` if the pointer is null or the sentinel does not match,
176  /// meaning the payload was not produced by this crate.
177  unsafe fn try_from_void<'a>(
178    ptr: *mut std::os::raw::c_void,
179  ) -> Option<&'a Self> {
180    if ptr.is_null() {
181      return None;
182    }
183    let candidate = unsafe { &*(ptr as *const ErasedPayload) };
184    if candidate.magic != ERASED_PAYLOAD_MAGIC {
185      return None;
186    }
187    Some(candidate)
188  }
189
190  /// Same as [`try_from_void`](Self::try_from_void) but panics if the sentinel
191  /// is wrong. Use only inside callbacks that own the payload (`print`,
192  /// `show_type`, `coerce_to_string`) where `self_` was produced by us.
193  unsafe fn from_void<'a>(ptr: *mut std::os::raw::c_void) -> &'a Self {
194    unsafe { Self::try_from_void(ptr) }
195      .expect("ErasedPayload sentinel mismatch")
196  }
197}
198
199impl Drop for ErasedPayload {
200  fn drop(&mut self) {
201    // SAFETY: self.data was created by Box::into_raw::<T> and drop_fn
202    // knows its original concrete type T.
203    unsafe { (self.drop_fn)(self.data) };
204  }
205}
206
207/// The static vtable passed to `nix_create_external_value`.
208///
209/// We store only one global vtable because all per-type dispatch happens
210/// through the function pointers embedded in [`ErasedPayload`].
211static VTABLE: sys::NixCExternalValueDesc = {
212  unsafe extern "C" fn print(
213    self_: *mut std::os::raw::c_void,
214    printer: *mut sys::nix_printer,
215  ) {
216    let _ = panic::catch_unwind(AssertUnwindSafe(|| {
217      let payload = unsafe { ErasedPayload::from_void(self_) };
218      let s = unsafe { (payload.display_fn)(payload.data) };
219      if let Ok(cs) = CString::new(s) {
220        unsafe {
221          sys::nix_external_print(std::ptr::null_mut(), printer, cs.as_ptr());
222        }
223      }
224    }));
225  }
226
227  unsafe extern "C" fn show_type(
228    self_: *mut std::os::raw::c_void,
229    res: *mut sys::nix_string_return,
230  ) {
231    let _ = panic::catch_unwind(AssertUnwindSafe(|| {
232      let payload = unsafe { ErasedPayload::from_void(self_) };
233      let name = unsafe { (payload.type_name_fn)(payload.data) };
234      if let Ok(cs) = CString::new(name) {
235        unsafe { sys::nix_set_string_return(res, cs.as_ptr()) };
236      }
237    }));
238  }
239
240  unsafe extern "C" fn type_of(
241    _self: *mut std::os::raw::c_void,
242    res: *mut sys::nix_string_return,
243  ) {
244    let _ = panic::catch_unwind(AssertUnwindSafe(|| {
245      // builtins.typeOf for all external values returns "nix-external".
246      if let Ok(cs) = CString::new("nix-external") {
247        unsafe { sys::nix_set_string_return(res, cs.as_ptr()) };
248      }
249    }));
250  }
251
252  unsafe extern "C" fn coerce_to_string(
253    self_: *mut std::os::raw::c_void,
254    _c: *mut sys::nix_string_context,
255    _coerce_more: std::os::raw::c_int,
256    _copy_to_store: std::os::raw::c_int,
257    res: *mut sys::nix_string_return,
258  ) {
259    let _ = panic::catch_unwind(AssertUnwindSafe(|| {
260      let payload = unsafe { ErasedPayload::from_void(self_) };
261      if let Some(s) = unsafe { (payload.coerce_fn)(payload.data) }
262        && let Ok(cs) = CString::new(s)
263      {
264        unsafe { sys::nix_set_string_return(res, cs.as_ptr()) };
265      }
266    }));
267  }
268
269  unsafe extern "C" fn equal(
270    self_: *mut std::os::raw::c_void,
271    other: *mut std::os::raw::c_void,
272  ) -> std::os::raw::c_int {
273    let result = panic::catch_unwind(AssertUnwindSafe(|| {
274      let a = match unsafe { ErasedPayload::try_from_void(self_) } {
275        Some(p) => p,
276        None => return 0,
277      };
278      let b = match unsafe { ErasedPayload::try_from_void(other) } {
279        Some(p) => p,
280        None => return 0,
281      };
282      // Only compare if they share the same concrete type.
283      if a.type_id != b.type_id {
284        return 0;
285      }
286      if unsafe { (a.equal_fn)(a.data, b.data) } {
287        1
288      } else {
289        0
290      }
291    }));
292    result.unwrap_or(0)
293  }
294
295  // JSON and XML printing default to not-implemented (None).
296  sys::NixCExternalValueDesc {
297    print:            Some(print),
298    showType:         Some(show_type),
299    typeOf:           Some(type_of),
300    coerceToString:   Some(coerce_to_string),
301    equal:            Some(equal),
302    printValueAsJSON: None,
303    printValueAsXML:  None,
304  }
305};
306
307impl EvalState {
308  /// Wrap a [`NixExternal`] value and return an [`ExternalValueHandle`].
309  ///
310  /// The handle carries the Nix [`Value`] and the raw `ExternalValue*` needed
311  /// for downcasting via [`ExternalValueHandle::as_external`]. The value's
312  /// lifetime is managed by the Nix GC.
313  ///
314  /// # Errors
315  ///
316  /// Returns an error if value allocation or external value creation fails.
317  pub fn make_external<T: NixExternal>(
318    &self,
319    data: T,
320  ) -> Result<ExternalValueHandle<'_>> {
321    let payload_ptr = ErasedPayload::new(data);
322    // Cast away the const on the vtable: the C API takes *mut but never
323    // mutates the descriptor itself.
324    let vtable_ptr = &VTABLE as *const sys::NixCExternalValueDesc
325      as *mut sys::NixCExternalValueDesc;
326
327    // Allocate the nix_value before touching the GC with the external pointer.
328    let v = self.alloc_value()?;
329
330    // SAFETY: vtable_ptr points to the static vtable; payload_ptr is a
331    // freshly allocated ErasedPayload.
332    let ext_ptr = unsafe {
333      sys::nix_create_external_value(
334        self.context.as_ptr(),
335        vtable_ptr,
336        payload_ptr.cast::<std::os::raw::c_void>(),
337      )
338    };
339
340    if ext_ptr.is_null() {
341      drop(unsafe { Box::from_raw(payload_ptr) });
342      return Err(Error::NullPointer);
343    }
344
345    // SAFETY: context, value, and external value pointer are valid.
346    unsafe {
347      crate::check_err(
348        self.context.as_ptr(),
349        sys::nix_init_external(
350          self.context.as_ptr(),
351          v.inner.as_ptr(),
352          ext_ptr,
353        ),
354      )?;
355    }
356
357    Ok(ExternalValueHandle { value: v, ext_ptr })
358  }
359}
360
361/// A handle to a Nix external value that retains the `ExternalValue*`.
362///
363/// `nix_get_external` is broken in Nix 2.32.7; see the module-level note.
364/// This type stores the `ExternalValue*` from `nix_create_external_value`
365/// directly and uses `nix_get_external_value_content` for downcasting.
366///
367/// Derefs to [`Value`].
368pub struct ExternalValueHandle<'s> {
369  value:   Value<'s>,
370  ext_ptr: *mut sys::ExternalValue,
371}
372
373// SAFETY: `ExternalValueHandle` wraps an owned `Value<'s>` plus the raw
374// `*mut sys::ExternalValue` it was constructed from. The Nix GC owns the
375// external value; we hold a single reference through the inner `Value`.
376// The user-provided payload requires `T: NixExternal: Send + Sync`, so
377// moving the wrapper to another thread does not expose any
378// thread-affine Rust data. `Sync` is NOT implemented because
379// `as_external` calls into `Context` to fetch the payload, racing on
380// its error buffer.
381unsafe impl Send for ExternalValueHandle<'_> {}
382
383impl<'s> Deref for ExternalValueHandle<'s> {
384  type Target = Value<'s>;
385
386  fn deref(&self) -> &Self::Target {
387    &self.value
388  }
389}
390
391impl ExternalValueHandle<'_> {
392  /// Downcast to a concrete Rust type `T`.
393  ///
394  /// # Errors
395  ///
396  /// Returns [`Error::InvalidType`] if the stored
397  /// [`TypeId`] does not match `T`.
398  pub fn as_external<T: NixExternal>(&self) -> Result<&T> {
399    // SAFETY: ext_ptr was returned by nix_create_external_value and is kept
400    // alive by the nix_value that this handle owns.
401    let void_ptr = unsafe {
402      sys::nix_get_external_value_content(
403        self.value.state.context.as_ptr(),
404        self.ext_ptr,
405      )
406    };
407
408    if void_ptr.is_null() {
409      return Err(Error::NullPointer);
410    }
411
412    // SAFETY: void_ptr points to the ErasedPayload allocated in
413    // ErasedPayload::new.
414    let payload = unsafe { ErasedPayload::from_void(void_ptr) };
415
416    if payload.type_id != TypeId::of::<T>() {
417      return Err(Error::InvalidType {
418        expected: std::any::type_name::<T>(),
419        actual:   "external value of different type".to_string(),
420      });
421    }
422
423    // SAFETY: type_id matches, so data is a valid *mut T.
424    let t_ref = unsafe { &*(payload.data as *const T) };
425    Ok(t_ref)
426  }
427}
428
429#[cfg(test)]
430mod tests {
431  use std::sync::Arc;
432
433  use serial_test::serial;
434
435  use super::*;
436  use crate::{Context, EvalStateBuilder, Store, ValueType};
437
438  struct MyData(i64);
439
440  impl NixExternal for MyData {
441    fn display(&self) -> String {
442      format!("MyData({})", self.0)
443    }
444
445    fn type_name(&self) -> &'static str {
446      "MyData"
447    }
448  }
449
450  fn make_eval_state() -> (Arc<Context>, Arc<Store>, crate::EvalState) {
451    let ctx = Arc::new(Context::new().expect("Failed to create context"));
452    let store =
453      Arc::new(Store::open(&ctx, None).expect("Failed to open store"));
454    let state = EvalStateBuilder::new(&store)
455      .expect("Failed to create builder")
456      .build()
457      .expect("Failed to build state");
458    (ctx, store, state)
459  }
460
461  #[test]
462  #[serial]
463  fn test_make_and_recover_external() {
464    let (_ctx, _store, state) = make_eval_state();
465
466    let handle = state
467      .make_external(MyData(42))
468      .expect("make_external failed");
469    assert_eq!(handle.value_type(), ValueType::External);
470
471    let back = handle.as_external::<MyData>().expect("as_external failed");
472    assert_eq!(back.0, 42);
473  }
474
475  #[test]
476  #[serial]
477  fn test_wrong_type_returns_error() {
478    let (_ctx, _store, state) = make_eval_state();
479
480    struct OtherData;
481    impl NixExternal for OtherData {
482      fn display(&self) -> String {
483        "OtherData".to_string()
484      }
485
486      fn type_name(&self) -> &'static str {
487        "OtherData"
488      }
489    }
490
491    let handle = state
492      .make_external(MyData(1))
493      .expect("make_external failed");
494    let result = handle.as_external::<OtherData>();
495    assert!(
496      result.is_err(),
497      "Downcasting to wrong type should return Err"
498    );
499  }
500
501  #[test]
502  #[serial]
503  fn test_as_external_on_non_external_value() {
504    let (_ctx, _store, state) = make_eval_state();
505
506    let int_val = state.make_int(5).expect("make_int failed");
507    // int_val is a Value, not an ExternalValueHandle; just confirm it's not
508    // External type
509    assert_ne!(int_val.value_type(), ValueType::External);
510  }
511}