use std::{
any::TypeId,
ffi::CString,
ops::Deref,
panic::{self, AssertUnwindSafe},
};
use crate::{Error, EvalState, Result, Value, sys};
pub trait NixExternal: Send + Sync + 'static {
fn display(&self) -> String;
fn type_name(&self) -> &'static str;
fn coerce_to_string(&self) -> Option<String> {
None
}
fn equal(&self, _other: &Self) -> bool {
false
}
}
const ERASED_PAYLOAD_MAGIC: u64 = 0x4E49585F455854;
#[repr(C)]
struct ErasedPayload {
magic: u64,
type_id: TypeId,
data: *mut std::os::raw::c_void,
drop_fn: unsafe fn(*mut std::os::raw::c_void),
display_fn: unsafe fn(*const std::os::raw::c_void) -> String,
type_name_fn: unsafe fn(*const std::os::raw::c_void) -> &'static str,
coerce_fn: unsafe fn(*const std::os::raw::c_void) -> Option<String>,
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 {
magic: ERASED_PAYLOAD_MAGIC,
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 try_from_void<'a>(
ptr: *mut std::os::raw::c_void,
) -> Option<&'a Self> {
if ptr.is_null() {
return None;
}
let candidate = unsafe { &*(ptr as *const ErasedPayload) };
if candidate.magic != ERASED_PAYLOAD_MAGIC {
return None;
}
Some(candidate)
}
unsafe fn from_void<'a>(ptr: *mut std::os::raw::c_void) -> &'a Self {
unsafe { Self::try_from_void(ptr) }
.expect("ErasedPayload sentinel mismatch")
}
}
impl Drop for ErasedPayload {
fn drop(&mut self) {
unsafe { (self.drop_fn)(self.data) };
}
}
static VTABLE: sys::NixCExternalValueDesc = {
unsafe extern "C" fn print(
self_: *mut std::os::raw::c_void,
printer: *mut sys::nix_printer,
) {
let _ = panic::catch_unwind(AssertUnwindSafe(|| {
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 _ = panic::catch_unwind(AssertUnwindSafe(|| {
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,
) {
let _ = panic::catch_unwind(AssertUnwindSafe(|| {
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 _ = panic::catch_unwind(AssertUnwindSafe(|| {
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 result = panic::catch_unwind(AssertUnwindSafe(|| {
let a = match unsafe { ErasedPayload::try_from_void(self_) } {
Some(p) => p,
None => return 0,
};
let b = match unsafe { ErasedPayload::try_from_void(other) } {
Some(p) => p,
None => return 0,
};
if a.type_id != b.type_id {
return 0;
}
if unsafe { (a.equal_fn)(a.data, b.data) } {
1
} else {
0
}
}));
result.unwrap_or(0)
}
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 {
pub fn make_external<T: NixExternal>(
&self,
data: T,
) -> Result<ExternalValueHandle<'_>> {
let payload_ptr = ErasedPayload::new(data);
let vtable_ptr = &VTABLE as *const sys::NixCExternalValueDesc
as *mut sys::NixCExternalValueDesc;
let v = self.alloc_value()?;
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);
}
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 })
}
}
pub struct ExternalValueHandle<'s> {
value: Value<'s>,
ext_ptr: *mut sys::ExternalValue,
}
unsafe impl Send for ExternalValueHandle<'_> {}
impl<'s> Deref for ExternalValueHandle<'s> {
type Target = Value<'s>;
fn deref(&self) -> &Self::Target {
&self.value
}
}
impl ExternalValueHandle<'_> {
pub fn as_external<T: NixExternal>(&self) -> Result<&T> {
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);
}
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(),
});
}
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");
assert_ne!(int_val.value_type(), ValueType::External);
}
}