nix-bindings 0.2347.2

Rust binding for Nix, the build tool
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
//! External Nix values backed by Rust data.
//!
//! - [`NixExternal`]: trait your data type must implement.
//! - [`EvalState::make_external`](crate::EvalState::make_external): wraps a
//!   value and returns an [`ExternalValueHandle`].
//! - [`ExternalValueHandle::as_external`]: downcasts back to a concrete Rust
//!   type.
//!
//! A [`TypeId`] is stored alongside each data pointer.
//! [`ExternalValueHandle::as_external`] checks it before returning a reference,
//! so a wrong-type downcast returns [`Error::InvalidType`] rather than UB.
//!
//! # Example
//!
//! ```rust,no_run
//! use std::sync::Arc;
//!
//! use nix_bindings::{Context, EvalStateBuilder, Store, external::NixExternal};
//!
//! struct MyData(i64);
//!
//! impl NixExternal for MyData {
//!   fn display(&self) -> String {
//!     format!("MyData({})", self.0)
//!   }
//!   fn type_name(&self) -> &'static str {
//!     "MyData"
//!   }
//! }
//!
//! fn main() -> Result<(), Box<dyn std::error::Error>> {
//!   let ctx = Arc::new(Context::new()?);
//!   let store = Arc::new(Store::open(&ctx, None)?);
//!   let state = EvalStateBuilder::new(&store)?.build()?;
//!
//!   let handle = state.make_external(MyData(42))?;
//!   let back = handle.as_external::<MyData>()?;
//!   assert_eq!(back.0, 42);
//!
//!   Ok(())
//! }
//! ```

use std::{any::TypeId, ffi::CString, ops::Deref};

use crate::{Error, EvalState, Result, Value, sys};

/// A Rust type that can be embedded as an external value in the Nix evaluator.
///
/// Implementing this trait is the only requirement for storing your type inside
/// Nix values via [`EvalState::make_external`]. All methods except
/// [`display`](NixExternal::display) and [`type_name`](NixExternal::type_name)
/// have default no-op implementations.
pub trait NixExternal: Send + Sync + 'static {
  /// Return a human-readable representation of the value.
  ///
  /// This is called when Nix prints the value (e.g. in the REPL).
  fn display(&self) -> String;

  /// Return the type name shown by `:t` in the Nix REPL.
  fn type_name(&self) -> &'static str;

  /// Try to coerce the value to a string.
  ///
  /// Return `Some(s)` to allow coercion; return `None` (the default) to have
  /// Nix throw an error when coercion is attempted.
  fn coerce_to_string(&self) -> Option<String> {
    None
  }

  /// Test equality with another external value of the same Rust type.
  ///
  /// The default implementation returns `false` (values are never equal).
  fn equal(&self, _other: &Self) -> bool {
    false
  }
}

/// Heap-allocated wrapper that combines a boxed `T` with its [`TypeId`].
///
/// This is the allocation stored behind the `void*` data pointer that is
/// passed to [`nix_create_external_value`](sys::nix_create_external_value).
struct ErasedPayload {
  type_id: TypeId,

  // The concrete data follows; we keep only a raw pointer into the T.
  data: *mut std::os::raw::c_void,

  // Destructor: how to drop the original Box<T>.
  drop_fn: unsafe fn(*mut std::os::raw::c_void),

  // display(): returns an owned String.
  display_fn: unsafe fn(*const std::os::raw::c_void) -> String,

  // type_name(): returns a &'static str.
  type_name_fn: unsafe fn(*const std::os::raw::c_void) -> &'static str,

  // coerce_to_string(): returns Option<String>.
  coerce_fn: unsafe fn(*const std::os::raw::c_void) -> Option<String>,

  // equal(other_data): compare two ErasedPayload.data pointers of the same T.
  equal_fn:
    unsafe fn(*const std::os::raw::c_void, *const std::os::raw::c_void) -> bool,
}

unsafe fn drop_erased<T>(ptr: *mut std::os::raw::c_void) {
  drop(unsafe { Box::from_raw(ptr.cast::<T>()) });
}

unsafe fn display_erased<T: NixExternal>(
  ptr: *const std::os::raw::c_void,
) -> String {
  let t = unsafe { &*(ptr as *const T) };
  t.display()
}

unsafe fn type_name_erased<T: NixExternal>(
  ptr: *const std::os::raw::c_void,
) -> &'static str {
  let t = unsafe { &*(ptr as *const T) };
  t.type_name()
}

unsafe fn coerce_erased<T: NixExternal>(
  ptr: *const std::os::raw::c_void,
) -> Option<String> {
  let t = unsafe { &*(ptr as *const T) };
  t.coerce_to_string()
}

unsafe fn equal_erased<T: NixExternal>(
  ptr: *const std::os::raw::c_void,
  other: *const std::os::raw::c_void,
) -> bool {
  let a = unsafe { &*(ptr as *const T) };
  let b = unsafe { &*(other as *const T) };
  a.equal(b)
}

impl ErasedPayload {
  fn new<T: NixExternal>(value: T) -> *mut Self {
    let data_box = Box::new(value);
    let data_ptr = Box::into_raw(data_box) as *mut std::os::raw::c_void;

    Box::into_raw(Box::new(ErasedPayload {
      type_id:      TypeId::of::<T>(),
      data:         data_ptr,
      drop_fn:      drop_erased::<T>,
      display_fn:   display_erased::<T>,
      type_name_fn: type_name_erased::<T>,
      coerce_fn:    coerce_erased::<T>,
      equal_fn:     equal_erased::<T>,
    }))
  }

  unsafe fn from_void<'a>(ptr: *mut std::os::raw::c_void) -> &'a Self {
    unsafe { &*(ptr as *const ErasedPayload) }
  }
}

impl Drop for ErasedPayload {
  fn drop(&mut self) {
    // SAFETY: self.data was created by Box::into_raw::<T> and drop_fn
    // knows its original concrete type T.
    unsafe { (self.drop_fn)(self.data) };
  }
}

/// The static vtable passed to `nix_create_external_value`.
///
/// We store only one global vtable because all per-type dispatch happens
/// through the function pointers embedded in [`ErasedPayload`].
static VTABLE: sys::NixCExternalValueDesc = {
  unsafe extern "C" fn print(
    self_: *mut std::os::raw::c_void,
    printer: *mut sys::nix_printer,
  ) {
    let payload = unsafe { ErasedPayload::from_void(self_) };
    let s = unsafe { (payload.display_fn)(payload.data) };
    if let Ok(cs) = CString::new(s) {
      unsafe {
        sys::nix_external_print(std::ptr::null_mut(), printer, cs.as_ptr());
      }
    }
  }

  unsafe extern "C" fn show_type(
    self_: *mut std::os::raw::c_void,
    res: *mut sys::nix_string_return,
  ) {
    let payload = unsafe { ErasedPayload::from_void(self_) };
    let name = unsafe { (payload.type_name_fn)(payload.data) };
    if let Ok(cs) = CString::new(name) {
      unsafe { sys::nix_set_string_return(res, cs.as_ptr()) };
    }
  }

  unsafe extern "C" fn type_of(
    _self: *mut std::os::raw::c_void,
    res: *mut sys::nix_string_return,
  ) {
    // builtins.typeOf for all external values returns "nix-external".
    if let Ok(cs) = CString::new("nix-external") {
      unsafe { sys::nix_set_string_return(res, cs.as_ptr()) };
    }
  }

  unsafe extern "C" fn coerce_to_string(
    self_: *mut std::os::raw::c_void,
    _c: *mut sys::nix_string_context,
    _coerce_more: std::os::raw::c_int,
    _copy_to_store: std::os::raw::c_int,
    res: *mut sys::nix_string_return,
  ) {
    let payload = unsafe { ErasedPayload::from_void(self_) };
    if let Some(s) = unsafe { (payload.coerce_fn)(payload.data) }
      && let Ok(cs) = CString::new(s)
    {
      unsafe { sys::nix_set_string_return(res, cs.as_ptr()) };
    }
  }

  unsafe extern "C" fn equal(
    self_: *mut std::os::raw::c_void,
    other: *mut std::os::raw::c_void,
  ) -> std::os::raw::c_int {
    let a = unsafe { ErasedPayload::from_void(self_) };
    let b = unsafe { ErasedPayload::from_void(other) };
    // Only compare if they share the same concrete type.
    if a.type_id != b.type_id {
      return 0;
    }
    if unsafe { (a.equal_fn)(a.data, b.data) } {
      1
    } else {
      0
    }
  }

  // JSON and XML printing default to not-implemented (None).
  sys::NixCExternalValueDesc {
    print:            Some(print),
    showType:         Some(show_type),
    typeOf:           Some(type_of),
    coerceToString:   Some(coerce_to_string),
    equal:            Some(equal),
    printValueAsJSON: None,
    printValueAsXML:  None,
  }
};

impl EvalState {
  /// Wrap a [`NixExternal`] value and return an [`ExternalValueHandle`].
  ///
  /// The handle carries the Nix [`Value`] and the raw `ExternalValue*` needed
  /// for downcasting via [`ExternalValueHandle::as_external`]. The value's
  /// lifetime is managed by the Nix GC.
  ///
  /// # Errors
  ///
  /// Returns an error if value allocation or external value creation fails.
  pub fn make_external<T: NixExternal>(
    &self,
    data: T,
  ) -> Result<ExternalValueHandle<'_>> {
    let payload_ptr = ErasedPayload::new(data);
    // Cast away the const on the vtable: the C API takes *mut but never
    // mutates the descriptor itself.
    let vtable_ptr = &VTABLE as *const sys::NixCExternalValueDesc
      as *mut sys::NixCExternalValueDesc;

    // Allocate the nix_value before touching the GC with the external pointer.
    let v = self.alloc_value()?;

    // SAFETY: vtable_ptr points to the static vtable; payload_ptr is a
    // freshly allocated ErasedPayload.
    let ext_ptr = unsafe {
      sys::nix_create_external_value(
        self.context.as_ptr(),
        vtable_ptr,
        payload_ptr.cast::<std::os::raw::c_void>(),
      )
    };

    if ext_ptr.is_null() {
      drop(unsafe { Box::from_raw(payload_ptr) });
      return Err(Error::NullPointer);
    }

    // SAFETY: context, value, and external value pointer are valid.
    unsafe {
      crate::check_err(
        self.context.as_ptr(),
        sys::nix_init_external(
          self.context.as_ptr(),
          v.inner.as_ptr(),
          ext_ptr,
        ),
      )?;
    }

    Ok(ExternalValueHandle { value: v, ext_ptr })
  }
}

/// A handle to a Nix external value that retains the `ExternalValue*`.
///
/// `nix_get_external` is broken in Nix 2.32.7; see the module-level note.
/// This type stores the `ExternalValue*` from `nix_create_external_value`
/// directly and uses `nix_get_external_value_content` for downcasting.
///
/// Derefs to [`Value`].
pub struct ExternalValueHandle<'s> {
  value:   Value<'s>,
  ext_ptr: *mut sys::ExternalValue,
}

// SAFETY: ExternalValue* is GC-managed. T: Send + Sync is enforced by
// NixExternal.
unsafe impl Send for ExternalValueHandle<'_> {}
unsafe impl Sync for ExternalValueHandle<'_> {}

impl<'s> Deref for ExternalValueHandle<'s> {
  type Target = Value<'s>;

  fn deref(&self) -> &Self::Target {
    &self.value
  }
}

impl ExternalValueHandle<'_> {
  /// Downcast to a concrete Rust type `T`.
  ///
  /// # Errors
  ///
  /// Returns [`Error::InvalidType`] if the stored
  /// [`TypeId`] does not match `T`.
  pub fn as_external<T: NixExternal>(&self) -> Result<&T> {
    // SAFETY: ext_ptr was returned by nix_create_external_value and is kept
    // alive by the nix_value that this handle owns.
    let void_ptr = unsafe {
      sys::nix_get_external_value_content(
        self.value.state.context.as_ptr(),
        self.ext_ptr,
      )
    };

    if void_ptr.is_null() {
      return Err(Error::NullPointer);
    }

    // SAFETY: void_ptr points to the ErasedPayload allocated in
    // ErasedPayload::new.
    let payload = unsafe { ErasedPayload::from_void(void_ptr) };

    if payload.type_id != TypeId::of::<T>() {
      return Err(Error::InvalidType {
        expected: std::any::type_name::<T>(),
        actual:   "external value of different type".to_string(),
      });
    }

    // SAFETY: type_id matches, so data is a valid *mut T.
    let t_ref = unsafe { &*(payload.data as *const T) };
    Ok(t_ref)
  }
}

#[cfg(test)]
mod tests {
  use std::sync::Arc;

  use serial_test::serial;

  use super::*;
  use crate::{Context, EvalStateBuilder, Store, ValueType};

  struct MyData(i64);

  impl NixExternal for MyData {
    fn display(&self) -> String {
      format!("MyData({})", self.0)
    }

    fn type_name(&self) -> &'static str {
      "MyData"
    }
  }

  fn make_eval_state() -> (Arc<Context>, Arc<Store>, crate::EvalState) {
    let ctx = Arc::new(Context::new().expect("Failed to create context"));
    let store =
      Arc::new(Store::open(&ctx, None).expect("Failed to open store"));
    let state = EvalStateBuilder::new(&store)
      .expect("Failed to create builder")
      .build()
      .expect("Failed to build state");
    (ctx, store, state)
  }

  #[test]
  #[serial]
  fn test_make_and_recover_external() {
    let (_ctx, _store, state) = make_eval_state();

    let handle = state
      .make_external(MyData(42))
      .expect("make_external failed");
    assert_eq!(handle.value_type(), ValueType::External);

    let back = handle.as_external::<MyData>().expect("as_external failed");
    assert_eq!(back.0, 42);
  }

  #[test]
  #[serial]
  fn test_wrong_type_returns_error() {
    let (_ctx, _store, state) = make_eval_state();

    struct OtherData;
    impl NixExternal for OtherData {
      fn display(&self) -> String {
        "OtherData".to_string()
      }

      fn type_name(&self) -> &'static str {
        "OtherData"
      }
    }

    let handle = state
      .make_external(MyData(1))
      .expect("make_external failed");
    let result = handle.as_external::<OtherData>();
    assert!(
      result.is_err(),
      "Downcasting to wrong type should return Err"
    );
  }

  #[test]
  #[serial]
  fn test_as_external_on_non_external_value() {
    let (_ctx, _store, state) = make_eval_state();

    let int_val = state.make_int(5).expect("make_int failed");
    // int_val is a Value, not an ExternalValueHandle; just confirm it's not
    // External type
    assert_ne!(int_val.value_type(), ValueType::External);
  }
}