From 81ddc194a4bf5dae2217b7ace7a340a86213a034 Mon Sep 17 00:00:00 2001 From: Yaroslav Bolyukin Date: Sat, 22 Nov 2025 22:06:56 +0000 Subject: [PATCH] feat: basic primop interface --- --- a/crates/nix-eval/src/lib.rs +++ b/crates/nix-eval/src/lib.rs @@ -1,12 +1,13 @@ use std::borrow::Cow; use std::cell::RefCell; use std::ffi::{CStr, CString, c_char, c_int, c_uint, c_void}; -use std::ptr::null_mut; +use std::ptr::{null, null_mut}; use std::sync::LazyLock; +use std::{array, fmt, slice}; use std::{collections::HashMap, path::PathBuf}; -use std::{fmt, slice}; use anyhow::{Context, anyhow, bail}; +use itertools::Itertools; use serde::Serialize; use serde::de::DeserializeOwned; @@ -18,25 +19,26 @@ use self::nix_raw::{ BindingsBuilder as c_bindings_builder, EvalState as c_eval_state, GC_SUCCESS, GC_allow_register_threads, GC_get_stack_base, GC_register_my_thread, GC_stack_base, - GC_thread_is_registered, GC_unregister_my_thread, ListBuilder as c_list_builder, - Store as c_store, StorePath as c_store_path, alloc_value, bindings_builder_free, - bindings_builder_insert, c_context, c_context_create, c_context_free, clear_err, err_code, - err_info_msg, err_msg, eval_state_build, eval_state_builder_load, eval_state_builder_new, - eval_state_builder_set_eval_setting, expr_eval_from_string, fetchers_settings, - fetchers_settings_free, fetchers_settings_new, flake_lock, flake_lock_flags, - flake_lock_flags_free, flake_lock_flags_new, flake_reference, + GC_thread_is_registered, GC_unregister_my_thread, ListBuilder as c_list_builder, PrimOp, + PrimOpFun, Store as c_store, StorePath as c_store_path, alloc_primop, alloc_value, + bindings_builder_free, bindings_builder_insert, c_context, c_context_create, c_context_free, + clear_err, copy_value, err_code, err_info_msg, err_msg, eval_state_build, + eval_state_builder_load, eval_state_builder_new, eval_state_builder_set_eval_setting, + expr_eval_from_string, fetchers_settings, fetchers_settings_free, fetchers_settings_new, + flake_lock, flake_lock_flags, flake_lock_flags_free, flake_lock_flags_new, flake_reference, flake_reference_and_fragment_from_string, flake_reference_parse_flags, flake_reference_parse_flags_free, flake_reference_parse_flags_new, flake_reference_parse_flags_set_base_directory, flake_settings, flake_settings_free, flake_settings_new, gc_now as gc_now_raw, get_attr_byname, get_attr_name_byidx, get_attrs_size, get_list_byidx, get_list_size, get_string, get_type, has_attr_byname, init_bool, init_int, - init_string, libexpr_init, libstore_init, libutil_init, list_builder_free, list_builder_insert, - locked_flake, locked_flake_free, locked_flake_get_output_attrs, make_attrs, - make_bindings_builder, make_list, make_list_builder, realised_string, realised_string_free, - realised_string_get_buffer_size, realised_string_get_buffer_start, - realised_string_get_store_path, realised_string_get_store_path_count, set_err_msg, setting_set, - state_free, store_open, store_parse_path, store_path_free, store_path_name, string_realise, - value, value_call, value_decref, value_incref, + init_primop, init_string, libexpr_init, libstore_init, libutil_init, list_builder_free, + list_builder_insert, locked_flake, locked_flake_free, locked_flake_get_output_attrs, + make_attrs, make_bindings_builder, make_list, make_list_builder, realised_string, + realised_string_free, realised_string_get_buffer_size, realised_string_get_buffer_start, + realised_string_get_store_path, realised_string_get_store_path_count, register_primop, + set_err_msg, setting_set, state_free, store_copy_closure, store_get_fs_closure, store_open, + store_parse_path, store_path_free, store_path_name, string_realise, value, value_call, + value_decref, value_incref, }; // Contains macros helpers @@ -166,6 +168,7 @@ } } +#[repr(transparent)] pub struct NixContext(*mut c_context); impl NixContext { pub fn set_err(&mut self, err: NixErrorKind, msg: &CStr) { @@ -540,6 +543,7 @@ } } +#[repr(transparent)] pub struct Value(*mut value); unsafe impl Send for Value {} @@ -626,6 +630,12 @@ } impl Value { + pub fn new_primop(v: NativeFn) -> Self { + let out = Self::new_uninit(); + with_default_context(|c, _| unsafe { init_primop(c, out.0, v.0) }) + .expect("primop initialization should not fail"); + out + } pub fn new_attrs(v: HashMap<&str, Value>) -> Self { let out = Self::new_uninit(); let mut b = AttrsBuilder::new(v.len()); @@ -913,6 +923,70 @@ nix_logging_cxx::apply_tracing_logger(); } +unsafe extern "C" fn nix_primop_closure_adapter( + user_data: *mut c_void, + context: *mut c_context, + state: *mut nix_raw::EvalState, + args: *mut *mut value, + ret: *mut value, +) { + let user_closure: &UserClosure = unsafe { &*user_data.cast_const().cast() }; + let args: [&Value; N] = array::from_fn(|i| { + let v: &Value = unsafe { &*args.add(i).cast_const().cast() }; + v + }); + let ctx: &mut NixContext = unsafe { &mut *context.cast() }; + + match user_closure(args) { + Ok(v) => { + unsafe { copy_value(context, ret, v.0) }; + } + Err(e) => { + ctx.set_err( + NixErrorKind::Unknown, + &CString::new(e.to_string()).expect("error should not contain internal nuls"), + ); + } + } +} + +type UserClosure = Box Result>; + +struct NativeFn(*mut PrimOp); +impl NativeFn { + pub fn new( + name: &'static CStr, + doc: &'static CStr, + args: [&'static CStr; N], + f: impl Fn([&Value; N]) -> Result + 'static, + ) -> Self { + // Double-boxing to make it thin pointer, as vtable gets outside of first Box + let closure: Box> = Box::new(Box::new(f)); + let f: PrimOpFun = Some(nix_primop_closure_adapter::); + let mut args = args.into_iter().map(|v| v.as_ptr()).collect_vec(); + args.push(null()); + let args = args.as_mut_ptr(); + let primop = with_default_context(|c, _| unsafe { + alloc_primop( + c, + f, + N as i32, + name.as_ptr(), + args, + doc.as_ptr(), + Box::into_raw(closure).cast(), + ) + }) + .expect("primop allocation should not fail"); + + Self(primop) + } + pub fn register(self) { + with_default_context(|c, _| unsafe { register_primop(c, self.0) }) + .expect("primop registration should not fail"); + } +} + struct StorePath(*mut c_store_path); impl StorePath {} @@ -949,6 +1023,20 @@ let s = nix_go!(attrs.packages["x86_64-linux"].fleet.drvPath); let s = CString::new(s.to_string()?).expect("path str is cstring"); + let uppercase_suffix = Value::new_primop(NativeFn::new( + c"uppercase_suffix", + c"make string uppercase and add suffix", + [c"str", c"suffix"], + |[str, suffix]: [&Value; 2]| { + let str = str.to_string()?; + let suffix = suffix.to_string()?; + Ok(Value::new_str(&format!("{}{suffix}", str.to_uppercase()))) + }, + )); + + let test_result: String = nix_go_json!(test_data.testPrimop(uppercase_suffix)); + assert_eq!(test_result, "PREFIX_BODY_SUFFIX"); + let nix_ctx = NixContext::new(); let store = GLOBAL_STATE.store.parse_path(s.as_c_str())?; --- a/flake.lock +++ b/flake.lock @@ -2,11 +2,10 @@ "nodes": { "crane": { "locked": { - "lastModified": 1762538466, - "narHash": "sha256-8zrIPl6J+wLm9MH5ksHcW7BUHo7jSNOu0/hA0ohOOaM=", + "lastModified": 1763511871, "owner": "ipetkov", "repo": "crane", - "rev": "0cea393fffb39575c46b7a0318386467272182fe", + "rev": "099f9014bc8d0cd6e445470ea1df0fd691d5a548", "type": "github" }, "original": { @@ -38,11 +37,10 @@ ] }, "locked": { - "lastModified": 1762810396, - "narHash": "sha256-dxFVgQPG+R72dkhXTtqUm7KpxElw3u6E+YlQ2WaDgt8=", + "lastModified": 1763759067, "owner": "hercules-ci", "repo": "flake-parts", - "rev": "0bdadb1b265fb4143a75bd1ec7d8c915898a9923", + "rev": "2cccadc7357c0ba201788ae99c4dfa90728ef5e0", "type": "github" }, "original": { @@ -51,26 +49,6 @@ "type": "github" } }, - "flake-parts_2": { - "inputs": { - "nixpkgs-lib": [ - "nix", - "nixpkgs" - ] - }, - "locked": { - "lastModified": 1748821116, - "narHash": "sha256-F82+gS044J1APL0n4hH50GYdPRv/5JWm34oCJYmVKdE=", - "rev": "49f0870db23e8c1ca0b5259734a02cd9e1e371a1", - "revCount": 377, - "type": "tarball", - "url": "https://api.flakehub.com/f/pinned/hercules-ci/flake-parts/0.1.377%2Brev-49f0870db23e8c1ca0b5259734a02cd9e1e371a1/01972f28-554a-73f8-91f4-d488cc502f08/source.tar.gz" - }, - "original": { - "type": "tarball", - "url": "https://flakehub.com/f/hercules-ci/flake-parts/0.1" - } - }, "fleet-tf": { "inputs": { "flake-parts": [ @@ -85,7 +63,6 @@ }, "locked": { "lastModified": 1759080490, - "narHash": "sha256-6eog70ItEoiusftwCp1vjY/7kA1+BDTUuRwg4KmszUs=", "owner": "CertainLach", "repo": "fleet-tf", "rev": "878bd8c23933d628bf750378bbe527b841901c3d", @@ -123,18 +100,21 @@ }, "nix": { "inputs": { - "flake-parts": "flake-parts_2", + "flake-parts": [ + "flake-parts" + ], "git-hooks-nix": "git-hooks-nix", - "nixpkgs": "nixpkgs", + "nixpkgs": [ + "nixpkgs" + ], "nixpkgs-23-11": "nixpkgs-23-11", "nixpkgs-regression": "nixpkgs-regression" }, "locked": { - "lastModified": 1762216145, - "narHash": "sha256-ff2SX5zKVsOAvN84AT+81TmWob5B5dxlkMcFexPZHAA=", + "lastModified": 1763844027, "owner": "deltarocks", "repo": "nix", - "rev": "dece43d5bae876aa475033d5a5a389b478b1644e", + "rev": "6b07ab20f1a65bd4aa54ec1418ff6f179dde64ac", "type": "github" }, "original": { @@ -146,16 +126,17 @@ }, "nixpkgs": { "locked": { - "lastModified": 1755922037, - "narHash": "sha256-wY1+2JPH0ZZC4BQefoZw/k+3+DowFyfOxv17CN/idKs=", - "rev": "b1b3291469652d5a2edb0becc4ef0246fff97a7c", - "revCount": 808723, - "type": "tarball", - "url": "https://api.flakehub.com/f/pinned/NixOS/nixpkgs/0.2505.808723%2Brev-b1b3291469652d5a2edb0becc4ef0246fff97a7c/0198daf7-011a-7703-95d7-57146e794342/source.tar.gz" + "lastModified": 1763844746, + "owner": "nixos", + "repo": "nixpkgs", + "rev": "d20f025296df84f7d97dee7c1a069bea8b17c8f2", + "type": "github" }, "original": { - "type": "tarball", - "url": "https://flakehub.com/f/NixOS/nixpkgs/0.2505" + "owner": "nixos", + "ref": "release-25.05", + "repo": "nixpkgs", + "type": "github" } }, "nixpkgs-23-11": { @@ -190,29 +171,13 @@ "type": "github" } }, - "nixpkgs_2": { - "locked": { - "lastModified": 1762881507, - "narHash": "sha256-P59mF2fC4/ijf5ZoCQA+wcaN6b0OgKKA3BQZpfPqHtY=", - "owner": "nixos", - "repo": "nixpkgs", - "rev": "2bbfb713196ae4cf5428530e8387cadcf95d3e2f", - "type": "github" - }, - "original": { - "owner": "nixos", - "ref": "release-25.05", - "repo": "nixpkgs", - "type": "github" - } - }, "root": { "inputs": { "crane": "crane", "flake-parts": "flake-parts", "fleet-tf": "fleet-tf", "nix": "nix", - "nixpkgs": "nixpkgs_2", + "nixpkgs": "nixpkgs", "rust-overlay": "rust-overlay", "shelly": "shelly", "treefmt-nix": "treefmt-nix" @@ -225,11 +190,10 @@ ] }, "locked": { - "lastModified": 1762828736, - "narHash": "sha256-RxtFHWZpKwVcWHhx88E2NhWuBbgYVqIoIDynGs5FoJs=", + "lastModified": 1763778964, "owner": "oxalica", "repo": "rust-overlay", - "rev": "8d5baa5628f6dbd7ce6beca3c299bae27755204c", + "rev": "7f3aa46dfa230ec2a4ca9281186a24771650ccd1", "type": "github" }, "original": { @@ -241,7 +205,6 @@ "shelly": { "locked": { "lastModified": 1756323923, - "narHash": "sha256-sKUaQrgnYVBmG5cGmGoFYXc61g74ufWpPYGPoJia58k=", "owner": "CertainLach", "repo": "shelly", "rev": "b5dd29a500db04f54a9f1c2bf81cdd84df8b0cd7", @@ -260,11 +223,10 @@ ] }, "locked": { - "lastModified": 1762410071, - "narHash": "sha256-aF5fvoZeoXNPxT0bejFUBXeUjXfHLSL7g+mjR/p5TEg=", + "lastModified": 1762938485, "owner": "numtide", "repo": "treefmt-nix", - "rev": "97a30861b13c3731a84e09405414398fbf3e109f", + "rev": "5b4ee75aeefd1e2d5a1cc43cf6ba65eba75e83e4", "type": "github" }, "original": { --- a/flake.nix +++ b/flake.nix @@ -25,7 +25,11 @@ }; # DeterminateSystem's nix fork is controversial, but I don't mind it, # and it has lazy-trees support which is useful for fleet. - nix.url = "github:deltarocks/nix/fleet"; + nix = { + url = "github:deltarocks/nix/fleet"; + inputs.nixpkgs.follows = "nixpkgs"; + inputs.flake-parts.follows = "flake-parts"; + }; }; outputs = inputs: @@ -56,6 +60,7 @@ v = "Hello"; }; testString = "hello"; + testPrimop = op: "PREFIX_" + (op "body" "_SUFFIX"); }; # To be used with https://github.com/NixOS/nix/pull/8892 -- gitstuff