From 312c86b8115eb81c4c4fe4136d2d9ba33abc8925 Mon Sep 17 00:00:00 2001 From: Robert Hensing Date: Fri, 19 Jul 2024 17:54:31 +0200 Subject: [PATCH] feat: PrimOp type (cherry picked from commit 67616c4a55b9d98d716384ffc07d3b3880dd76e4) --- rust/Cargo.lock | 11 ++ rust/nix-expr/Cargo.toml | 1 + rust/nix-expr/src/eval_state.rs | 184 +++++++++++++++++--------------- rust/nix-expr/src/lib.rs | 1 + rust/nix-expr/src/primop.rs | 120 +++++++++++++++++++++ 5 files changed, 233 insertions(+), 84 deletions(-) create mode 100644 rust/nix-expr/src/primop.rs diff --git a/rust/Cargo.lock b/rust/Cargo.lock index f9800e7..51e5dc9 100644 --- a/rust/Cargo.lock +++ b/rust/Cargo.lock @@ -72,6 +72,16 @@ dependencies = [ "libloading", ] +[[package]] +name = "cstr" +version = "0.2.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "68523903c8ae5aacfa32a0d9ae60cadeb764e1da14ee0d26b1f3089f13a54636" +dependencies = [ + "proc-macro2", + "quote", +] + [[package]] name = "ctor" version = "0.2.7" @@ -193,6 +203,7 @@ name = "nix-expr" version = "0.1.0" dependencies = [ "anyhow", + "cstr", "ctor", "lazy_static", "nix-c-raw", diff --git a/rust/nix-expr/Cargo.toml b/rust/nix-expr/Cargo.toml index 2733579..1de81e5 100644 --- a/rust/nix-expr/Cargo.toml +++ b/rust/nix-expr/Cargo.toml @@ -13,3 +13,4 @@ nix-c-raw = { path = "../nix-c-raw" } lazy_static = "1.4.0" ctor = "0.2.7" tempfile = "3.10.1" +cstr = "0.2.12" diff --git a/rust/nix-expr/src/eval_state.rs b/rust/nix-expr/src/eval_state.rs index ba4abc1..ca68676 100644 --- a/rust/nix-expr/src/eval_state.rs +++ b/rust/nix-expr/src/eval_state.rs @@ -1,3 +1,4 @@ +use crate::primop; use crate::value::{Int, Value, ValueType}; use anyhow::Context as _; use anyhow::{bail, Result}; @@ -8,8 +9,7 @@ use nix_store::store::{Store, StoreWeak}; use nix_util::context::Context; use nix_util::string_return::{callback_get_result_string, callback_get_result_string_data}; use nix_util::{check_call, check_call_opt_key, result_string_init}; -use std::ffi::{c_char, c_int, c_void, CString}; -use std::mem::ManuallyDrop; +use std::ffi::{c_char, CStr, CString}; use std::os::raw::c_uint; use std::ptr::{null, null_mut, NonNull}; use std::sync::{Arc, Weak}; @@ -78,7 +78,7 @@ impl Drop for EvalStateRef { pub struct EvalState { eval_state: Arc, store: Store, - context: Context, + pub(crate) context: Context, } impl EvalState { pub fn new<'a>(store: Store, lookup_path: impl IntoIterator) -> Result { @@ -426,9 +426,23 @@ impl EvalState { self.new_value_apply(&f, &f) } - /// Create a new function that is backed by a Rust function. + /// Create a new Nix function that is implemented by a Rust function. /// This is also known as a "primop" in Nix, short for primitive operation. - /// The `builtins.*` are examples of primops. + /// Most of the `builtins.*` values are examples of primops, but + /// `new_value_primop` does not affect `builtins`. + pub fn new_value_primop(&mut self, primop: primop::PrimOp) -> Result { + let value = self.new_value_uninitialized()?; + unsafe { + check_call!(raw::init_primop( + &mut self.context, + value.raw_ptr(), + primop.ptr + ))?; + }; + Ok(value) + } + + /// A worse quality shortcut for calling [new_value_primop]. pub fn new_value_function( &mut self, name: *const i8, @@ -443,43 +457,21 @@ impl EvalState { })); } - let mut args = Vec::new(); - for _ in 0..N { - args.push(FUNCTION_ANONYMOUS_ARG.as_ptr()); - } - args.push(null()); - // This leaks + let name = unsafe { CStr::from_ptr(name) }; - let user_data = { - // We'll be leaking this Box. - // TODO: Use the GC with finalizer, if possible. - let user_data = ManuallyDrop::new(Box::new(PrimOpContext { - arity: N, - function: Box::new(move |eval_state, args| { - let r = f(eval_state, args.try_into().unwrap()); - r - }), - eval_state: self.weak_ref(), - })); - user_data.as_ref() as *const PrimOpContext as *mut c_void - }; - let op = unsafe { - check_call!(raw::alloc_primop( - &mut self.context, - FUNCTION_ADAPTER, - N as c_int, + let args: [&CStr; N] = [FUNCTION_ANONYMOUS_ARG.as_ref(); N]; + + let prim = primop::PrimOp::new( + self, + primop::PrimOpMeta { name, - args.as_mut_ptr(), /* TODO add an extra const to bindings to avoid mut here. */ - FUNCTION_ANONYMOUS_DOC.as_ptr(), - user_data - ))? - }; - let value = self.new_value_uninitialized()?; - // Then use it in a value - unsafe { - check_call!(raw::init_primop(&mut self.context, value.raw_ptr(), op))?; - } - Ok(value) + args, + doc: FUNCTION_ANONYMOUS_DOC.as_ref(), + }, + f, + )?; + + self.new_value_primop(prim) } } @@ -535,50 +527,6 @@ impl Clone for EvalState { } } -/// The user_data for our Nix primops -struct PrimOpContext { - arity: usize, - // Something like Haskell's Dynamic - function: Box Result>, - eval_state: EvalStateWeak, -} - -unsafe extern "C" fn function_adapter( - user_data: *mut ::std::os::raw::c_void, - context_out: *mut raw::c_context, - _state: *mut raw::EvalState, - args: *mut *mut raw::Value, - ret: *mut raw::Value, -) { - let primop_info = (user_data as *const PrimOpContext).as_ref().unwrap(); - let mut eval_state = primop_info.eval_state.upgrade().unwrap_or_else(|| { - panic!("Nix primop called after EvalState was dropped"); - }); - let args_raw_slice = unsafe { std::slice::from_raw_parts(args, primop_info.arity) }; - let args_vec: Vec = args_raw_slice - .iter() - .map(|v| Value::new_borrowed(*v as *mut c_void)) - .collect(); - let args_slice = args_vec.as_slice(); - - let r = primop_info.function.as_ref()(&mut eval_state, args_slice); - - match r { - Ok(v) => unsafe { - raw::copy_value(context_out, ret, v.raw_ptr()); - }, - Err(e) => unsafe { - let cstr = CString::new(e.to_string()).unwrap_or_else(|_e| { - CString::new("") - .unwrap() - }); - raw::set_err_msg(context_out, raw::NIX_ERR_UNKNOWN, cstr.as_ptr()); - }, - } -} - -static FUNCTION_ADAPTER: raw::PrimOpFun = Some(function_adapter); - lazy_static! { pub static ref FUNCTION_ANONYMOUS: CString = CString::new("anonymous-primop").unwrap(); static ref FUNCTION_ANONYMOUS_ARG: CString = CString::new("x").unwrap(); @@ -604,6 +552,7 @@ pub fn test_init() { #[cfg(test)] mod tests { use super::*; + use cstr::cstr; use ctor::ctor; use std::collections::HashMap; use std::fs::read_dir; @@ -1498,4 +1447,71 @@ mod tests { }) .unwrap(); } + + #[test] + pub fn eval_state_primop_custom() { + gc_registering_current_thread(|| { + let store = Store::open("auto", []).unwrap(); + let mut es = EvalState::new(store, []).unwrap(); + let primop = primop::PrimOp::new( + &mut es, + primop::PrimOpMeta { + name: cstr!("frobnicate"), + doc: cstr!("Frobnicates widgets"), + args: [cstr!("x"), cstr!("y")], + }, + Box::new(|es, args| { + let a = es.require_int(&args[0])?; + let b = es.require_int(&args[1])?; + Ok(es.new_value_int(a + b)?) + }), + ) + .unwrap(); + let f = es.new_value_primop(primop).unwrap(); + let a = es.new_value_int(2).unwrap(); + let b = es.new_value_int(3).unwrap(); + let fa = es.call(f, a).unwrap(); + let fb = es.call(fa, b).unwrap(); + es.force(&fb).unwrap(); + let t = es.value_type(&fb).unwrap(); + assert!(t == ValueType::Int); + let i = es.require_int(&fb).unwrap(); + assert!(i == 5); + }) + .unwrap(); + } + + #[test] + pub fn eval_state_primop_custom_throw() { + gc_registering_current_thread(|| { + let store = Store::open("auto", []).unwrap(); + let mut es = EvalState::new(store, []).unwrap(); + let primop = primop::PrimOp::new( + &mut es, + primop::PrimOpMeta { + name: cstr!("frobnicate"), + doc: cstr!("Frobnicates widgets"), + args: [cstr!("x")], + }, + Box::new(|_es, _args| bail!("The frob unexpectedly fizzled")), + ) + .unwrap(); + let f = es.new_value_primop(primop).unwrap(); + let a = es.new_value_int(0).unwrap(); + match es.call(f, a) { + Ok(_) => panic!("expected an error"), + Err(e) => { + if !e.to_string().contains("The frob unexpectedly fizzled") { + eprintln!("unexpected error message: {}", e); + assert!(false); + } + if !e.to_string().contains("frobnicate") { + eprintln!("unexpected error message: {}", e); + assert!(false); + } + } + } + }) + .unwrap(); + } } diff --git a/rust/nix-expr/src/lib.rs b/rust/nix-expr/src/lib.rs index ead2024..41c4b39 100644 --- a/rust/nix-expr/src/lib.rs +++ b/rust/nix-expr/src/lib.rs @@ -1,2 +1,3 @@ pub mod eval_state; +pub mod primop; pub mod value; diff --git a/rust/nix-expr/src/primop.rs b/rust/nix-expr/src/primop.rs new file mode 100644 index 0000000..d2b2033 --- /dev/null +++ b/rust/nix-expr/src/primop.rs @@ -0,0 +1,120 @@ +use crate::eval_state::{EvalState, EvalStateWeak}; +use crate::value::Value; +use anyhow::Result; +use nix_c_raw as raw; +use nix_util::check_call; +use std::ffi::{c_int, c_void, CStr, CString}; +use std::mem::ManuallyDrop; +use std::ptr::{null, null_mut}; + +/// Metadata for a primop, used with `PrimOp::new`. +pub struct PrimOpMeta<'a, const N: usize> { + /// Name of the primop. Note that primops do not have to be registered as + /// builtins. Nonetheless, a name is required for documentation purposes, e.g. + /// :doc in the repl. + pub name: &'a CStr, + + /// Documentation for the primop. This is displayed in the repl when using + /// :doc. The format is markdown. + pub doc: &'a CStr, + + /// The number of arguments the function takes, as well as names for the + /// arguments, to be presented in the documentation (if applicable, e.g. + /// :doc in the repl). + pub args: [&'a CStr; N], +} + +pub struct PrimOp { + pub(crate) ptr: *mut raw::PrimOp, +} +impl Drop for PrimOp { + fn drop(&mut self) { + unsafe { + raw::gc_decref(null_mut(), self.ptr as *mut c_void); + } + } +} +impl PrimOp { + pub fn new( + eval_state: &mut EvalState, + meta: PrimOpMeta, + f: Box Result>, + ) -> Result { + let mut args = Vec::new(); + for arg in meta.args { + args.push(arg.as_ptr()); + } + args.push(null()); + + // Primops weren't meant to be dynamically created, as of writing. + // This leaks, and so do the primop fields in Nix internally. + let user_data = { + // We'll be leaking this Box. + // TODO: Use the GC with finalizer, if possible. + let user_data = ManuallyDrop::new(Box::new(PrimOpContext { + arity: N, + function: Box::new(move |eval_state, args| { + let r = f(eval_state, args.try_into().unwrap()); + r + }), + eval_state: eval_state.weak_ref(), + })); + user_data.as_ref() as *const PrimOpContext as *mut c_void + }; + let op = unsafe { + check_call!(raw::alloc_primop( + &mut eval_state.context, + FUNCTION_ADAPTER, + N as c_int, + meta.name.as_ptr(), + args.as_mut_ptr(), /* TODO add an extra const to bindings to avoid mut here. */ + meta.doc.as_ptr(), + user_data + ))? + }; + Ok(PrimOp { ptr: op }) + } +} + +/// The user_data for our Nix primops +struct PrimOpContext { + arity: usize, + function: Box Result>, + eval_state: EvalStateWeak, +} + +unsafe extern "C" fn function_adapter( + user_data: *mut ::std::os::raw::c_void, + context_out: *mut raw::c_context, + _state: *mut raw::EvalState, + args: *mut *mut raw::Value, + ret: *mut raw::Value, +) { + let primop_info = (user_data as *const PrimOpContext).as_ref().unwrap(); + let mut eval_state = primop_info.eval_state.upgrade().unwrap_or_else(|| { + panic!("Nix primop called after EvalState was dropped"); + }); + let args_raw_slice = unsafe { std::slice::from_raw_parts(args, primop_info.arity) }; + let args_vec: Vec = args_raw_slice + .iter() + .map(|v| Value::new_borrowed(*v)) + .collect(); + let args_slice = args_vec.as_slice(); + + let r = primop_info.function.as_ref()(&mut eval_state, args_slice); + + match r { + Ok(v) => unsafe { + raw::copy_value(context_out, ret, v.raw_ptr()); + }, + Err(e) => unsafe { + let cstr = CString::new(e.to_string()).unwrap_or_else(|_e| { + CString::new("") + .unwrap() + }); + raw::set_err_msg(context_out, raw::err_NIX_ERR_UNKNOWN, cstr.as_ptr()); + }, + } +} + +static FUNCTION_ADAPTER: raw::PrimOpFun = Some(function_adapter);