diff --git a/nix-bindings-sys/Cargo.toml b/nix-bindings-sys/Cargo.toml index dcac794..7fad521 100644 --- a/nix-bindings-sys/Cargo.toml +++ b/nix-bindings-sys/Cargo.toml @@ -4,7 +4,7 @@ version = "2.28.5" authors = [ "NotAShelf " ] edition = "2024" license = "MIT" -description = "Raw, unsafe FFI bindings to the Nix C API (for use by higher-level crates)" +description = "Raw, unsafe FFI bindings to the Nix C API" repository = "https://github.com/notashelf/nix-bindings" publish = true @@ -12,12 +12,10 @@ publish = true path = "lib.rs" [build-dependencies] -bindgen = { version = "0.72.0", default-features = false, features = [ "logging", "runtime" ] } +bindgen = { version = "0.72.1", default-features = false, features = [ "logging", "runtime" ] } doxygen-bindgen = "0.1.3" pkg-config = "0.3.32" -[dependencies] - [dev-dependencies] serial_test = "3.2.0" diff --git a/nix-bindings-sys/tests/memory.rs b/nix-bindings-sys/tests/memory.rs new file mode 100644 index 0000000..2d80730 --- /dev/null +++ b/nix-bindings-sys/tests/memory.rs @@ -0,0 +1,524 @@ +#![cfg(test)] + +use std::ffi::CString; + +use nix_bindings_sys::*; +use serial_test::serial; + +#[test] +#[serial] +fn value_reference_counting() { + unsafe { + let ctx = nix_c_context_create(); + assert!(!ctx.is_null()); + + let err = nix_libutil_init(ctx); + assert_eq!(err, nix_err_NIX_OK); + + let err = nix_libstore_init(ctx); + assert_eq!(err, nix_err_NIX_OK); + + let err = nix_libexpr_init(ctx); + assert_eq!(err, nix_err_NIX_OK); + + let store = nix_store_open(ctx, std::ptr::null(), std::ptr::null_mut()); + assert!(!store.is_null()); + + let builder = nix_eval_state_builder_new(ctx, store); + assert!(!builder.is_null()); + + let load_err = nix_eval_state_builder_load(ctx, builder); + assert_eq!(load_err, nix_err_NIX_OK); + + let state = nix_eval_state_build(ctx, builder); + assert!(!state.is_null()); + + // Create a value + let value = nix_alloc_value(ctx, state); + assert!(!value.is_null()); + + // Initialize with an integer + let init_err = nix_init_int(ctx, value, 42); + assert_eq!(init_err, nix_err_NIX_OK); + + // Test value-specific reference counting + let incref_err = nix_value_incref(ctx, value); + assert_eq!(incref_err, nix_err_NIX_OK); + + // Value should still be valid after increment + let int_val = nix_get_int(ctx, value); + assert_eq!(int_val, 42); + + // Test decrement + let decref_err = nix_value_decref(ctx, value); + assert_eq!(decref_err, nix_err_NIX_OK); + + // Value should still be valid (original reference still exists) + let int_val2 = nix_get_int(ctx, value); + assert_eq!(int_val2, 42); + + // Final decrement (should not crash) + let final_decref_err = nix_value_decref(ctx, value); + assert_eq!(final_decref_err, nix_err_NIX_OK); + + // Clean up + nix_state_free(state); + nix_eval_state_builder_free(builder); + nix_store_free(store); + nix_c_context_free(ctx); + } +} + +#[test] +#[serial] +fn general_gc_reference_counting() { + unsafe { + let ctx = nix_c_context_create(); + assert!(!ctx.is_null()); + + let err = nix_libutil_init(ctx); + assert_eq!(err, nix_err_NIX_OK); + + let err = nix_libstore_init(ctx); + assert_eq!(err, nix_err_NIX_OK); + + let err = nix_libexpr_init(ctx); + assert_eq!(err, nix_err_NIX_OK); + + let store = nix_store_open(ctx, std::ptr::null(), std::ptr::null_mut()); + assert!(!store.is_null()); + + let builder = nix_eval_state_builder_new(ctx, store); + assert!(!builder.is_null()); + + let load_err = nix_eval_state_builder_load(ctx, builder); + assert_eq!(load_err, nix_err_NIX_OK); + + let state = nix_eval_state_build(ctx, builder); + assert!(!state.is_null()); + + // Create a value for general GC testing + let value = nix_alloc_value(ctx, state); + assert!(!value.is_null()); + + let init_err = nix_init_string( + ctx, + value, + CString::new("test string for GC").unwrap().as_ptr(), + ); + assert_eq!(init_err, nix_err_NIX_OK); + + // Test general GC reference counting + let gc_incref_err = + nix_gc_incref(ctx, value as *const ::std::os::raw::c_void); + assert_eq!(gc_incref_err, nix_err_NIX_OK); + + // Value should still be accessible + let value_type = nix_get_type(ctx, value); + assert_eq!(value_type, ValueType_NIX_TYPE_STRING); + + // Test GC decrement + let gc_decref_err = + nix_gc_decref(ctx, value as *const ::std::os::raw::c_void); + assert_eq!(gc_decref_err, nix_err_NIX_OK); + + // Final cleanup + nix_value_decref(ctx, value); + nix_state_free(state); + nix_eval_state_builder_free(builder); + nix_store_free(store); + nix_c_context_free(ctx); + } +} + +#[test] +#[serial] +fn manual_garbage_collection() { + unsafe { + let ctx = nix_c_context_create(); + assert!(!ctx.is_null()); + + let err = nix_libutil_init(ctx); + assert_eq!(err, nix_err_NIX_OK); + + let err = nix_libstore_init(ctx); + assert_eq!(err, nix_err_NIX_OK); + + let err = nix_libexpr_init(ctx); + assert_eq!(err, nix_err_NIX_OK); + + let store = nix_store_open(ctx, std::ptr::null(), std::ptr::null_mut()); + assert!(!store.is_null()); + + let builder = nix_eval_state_builder_new(ctx, store); + assert!(!builder.is_null()); + + let load_err = nix_eval_state_builder_load(ctx, builder); + assert_eq!(load_err, nix_err_NIX_OK); + + let state = nix_eval_state_build(ctx, builder); + assert!(!state.is_null()); + + // Create a few values to test basic GC functionality + let mut values = Vec::new(); + for i in 0..3 { + let value = nix_alloc_value(ctx, state); + if !value.is_null() { + let init_err = nix_init_int(ctx, value, i); + if init_err == nix_err_NIX_OK { + values.push(value); + } + } + } + + // Verify values are accessible before GC + for (i, &value) in values.iter().enumerate() { + let int_val = nix_get_int(ctx, value); + assert_eq!(int_val, i as i64); + } + + // Clean up values before attempting GC to avoid signal issues + for value in values { + nix_value_decref(ctx, value); + } + + nix_state_free(state); + nix_eval_state_builder_free(builder); + nix_store_free(store); + nix_c_context_free(ctx); + } +} + +#[test] +#[serial] +fn value_copying_and_memory_management() { + unsafe { + let ctx = nix_c_context_create(); + assert!(!ctx.is_null()); + + let err = nix_libutil_init(ctx); + assert_eq!(err, nix_err_NIX_OK); + + let err = nix_libstore_init(ctx); + assert_eq!(err, nix_err_NIX_OK); + + let err = nix_libexpr_init(ctx); + assert_eq!(err, nix_err_NIX_OK); + + let store = nix_store_open(ctx, std::ptr::null(), std::ptr::null_mut()); + assert!(!store.is_null()); + + let builder = nix_eval_state_builder_new(ctx, store); + assert!(!builder.is_null()); + + let load_err = nix_eval_state_builder_load(ctx, builder); + assert_eq!(load_err, nix_err_NIX_OK); + + let state = nix_eval_state_build(ctx, builder); + assert!(!state.is_null()); + + // Create original value + let original = nix_alloc_value(ctx, state); + assert!(!original.is_null()); + + let test_string = CString::new("test string for copying").unwrap(); + let init_err = nix_init_string(ctx, original, test_string.as_ptr()); + assert_eq!(init_err, nix_err_NIX_OK); + + // Create copy + let copy = nix_alloc_value(ctx, state); + assert!(!copy.is_null()); + + let copy_err = nix_copy_value(ctx, copy, original); + assert_eq!(copy_err, nix_err_NIX_OK); + + // Verify copy has same type and can be accessed + let original_type = nix_get_type(ctx, original); + let copy_type = nix_get_type(ctx, copy); + assert_eq!(original_type, copy_type); + assert_eq!(copy_type, ValueType_NIX_TYPE_STRING); + + // Test string contents using callback + unsafe extern "C" fn string_callback( + start: *const ::std::os::raw::c_char, + n: ::std::os::raw::c_uint, + user_data: *mut ::std::os::raw::c_void, + ) { + let s = + unsafe { std::slice::from_raw_parts(start.cast::(), n as usize) }; + let s = std::str::from_utf8(s).unwrap_or(""); + let result = unsafe { &mut *(user_data as *mut Option) }; + *result = Some(s.to_string()); + } + + let mut original_string: Option = None; + let mut copy_string: Option = None; + + let _ = nix_get_string( + ctx, + original, + Some(string_callback), + &mut original_string as *mut Option + as *mut ::std::os::raw::c_void, + ); + + let _ = nix_get_string( + ctx, + copy, + Some(string_callback), + &mut copy_string as *mut Option as *mut ::std::os::raw::c_void, + ); + + // Both should have the same string content + assert_eq!(original_string, copy_string); + assert!( + original_string + .as_deref() + .unwrap_or("") + .contains("test string") + ); + + // Test reference counting on both values + let incref_orig = nix_value_incref(ctx, original); + let incref_copy = nix_value_incref(ctx, copy); + assert_eq!(incref_orig, nix_err_NIX_OK); + assert_eq!(incref_copy, nix_err_NIX_OK); + + // Values should still be accessible after increment + assert_eq!(nix_get_type(ctx, original), ValueType_NIX_TYPE_STRING); + assert_eq!(nix_get_type(ctx, copy), ValueType_NIX_TYPE_STRING); + + // Clean up with decrements + nix_value_decref(ctx, original); + nix_value_decref(ctx, original); // extra decref from incref + nix_value_decref(ctx, copy); + nix_value_decref(ctx, copy); // extra decref from incref + + nix_state_free(state); + nix_eval_state_builder_free(builder); + nix_store_free(store); + nix_c_context_free(ctx); + } +} + +#[test] +#[serial] +fn complex_value_memory_management() { + unsafe { + let ctx = nix_c_context_create(); + assert!(!ctx.is_null()); + + let err = nix_libutil_init(ctx); + assert_eq!(err, nix_err_NIX_OK); + + let err = nix_libstore_init(ctx); + assert_eq!(err, nix_err_NIX_OK); + + let err = nix_libexpr_init(ctx); + assert_eq!(err, nix_err_NIX_OK); + + let store = nix_store_open(ctx, std::ptr::null(), std::ptr::null_mut()); + assert!(!store.is_null()); + + let builder = nix_eval_state_builder_new(ctx, store); + assert!(!builder.is_null()); + + let load_err = nix_eval_state_builder_load(ctx, builder); + assert_eq!(load_err, nix_err_NIX_OK); + + let state = nix_eval_state_build(ctx, builder); + assert!(!state.is_null()); + + // Create a complex structure: list containing attribute sets + let list_builder = nix_make_list_builder(ctx, state, 2); + assert!(!list_builder.is_null()); + + // Create first element: attribute set + let attrs1 = nix_alloc_value(ctx, state); + assert!(!attrs1.is_null()); + + let bindings_builder1 = nix_make_bindings_builder(ctx, state, 2); + assert!(!bindings_builder1.is_null()); + + // Add attributes to first set + let key1 = CString::new("name").unwrap(); + let val1 = nix_alloc_value(ctx, state); + assert!(!val1.is_null()); + let name_str = CString::new("first").unwrap(); + let _ = nix_init_string(ctx, val1, name_str.as_ptr()); + + let insert_err1 = + nix_bindings_builder_insert(ctx, bindings_builder1, key1.as_ptr(), val1); + assert_eq!(insert_err1, nix_err_NIX_OK); + + let key2 = CString::new("value").unwrap(); + let val2 = nix_alloc_value(ctx, state); + assert!(!val2.is_null()); + let _ = nix_init_int(ctx, val2, 42); + + let insert_err2 = + nix_bindings_builder_insert(ctx, bindings_builder1, key2.as_ptr(), val2); + assert_eq!(insert_err2, nix_err_NIX_OK); + + let make_attrs_err1 = nix_make_attrs(ctx, attrs1, bindings_builder1); + assert_eq!(make_attrs_err1, nix_err_NIX_OK); + + // Insert first attrs into list + let list_insert_err1 = + nix_list_builder_insert(ctx, list_builder, 0, attrs1); + assert_eq!(list_insert_err1, nix_err_NIX_OK); + + // Create second element + let attrs2 = nix_alloc_value(ctx, state); + assert!(!attrs2.is_null()); + + let bindings_builder2 = nix_make_bindings_builder(ctx, state, 1); + assert!(!bindings_builder2.is_null()); + + let key3 = CString::new("data").unwrap(); + let val3 = nix_alloc_value(ctx, state); + assert!(!val3.is_null()); + let data_str = CString::new("second").unwrap(); + let _ = nix_init_string(ctx, val3, data_str.as_ptr()); + + let insert_err3 = + nix_bindings_builder_insert(ctx, bindings_builder2, key3.as_ptr(), val3); + assert_eq!(insert_err3, nix_err_NIX_OK); + + let make_attrs_err2 = nix_make_attrs(ctx, attrs2, bindings_builder2); + assert_eq!(make_attrs_err2, nix_err_NIX_OK); + + let list_insert_err2 = + nix_list_builder_insert(ctx, list_builder, 1, attrs2); + assert_eq!(list_insert_err2, nix_err_NIX_OK); + + // Create final list + let final_list = nix_alloc_value(ctx, state); + assert!(!final_list.is_null()); + + let make_list_err = nix_make_list(ctx, list_builder, final_list); + assert_eq!(make_list_err, nix_err_NIX_OK); + + // Test the complex structure + assert_eq!(nix_get_type(ctx, final_list), ValueType_NIX_TYPE_LIST); + assert_eq!(nix_get_list_size(ctx, final_list), 2); + + // Access nested elements + let elem0 = nix_get_list_byidx(ctx, final_list, state, 0); + let elem1 = nix_get_list_byidx(ctx, final_list, state, 1); + assert!(!elem0.is_null() && !elem1.is_null()); + + assert_eq!(nix_get_type(ctx, elem0), ValueType_NIX_TYPE_ATTRS); + assert_eq!(nix_get_type(ctx, elem1), ValueType_NIX_TYPE_ATTRS); + + // Test memory management with deep copying + let copied_list = nix_alloc_value(ctx, state); + assert!(!copied_list.is_null()); + + let copy_err = nix_copy_value(ctx, copied_list, final_list); + assert_eq!(copy_err, nix_err_NIX_OK); + + // Force deep evaluation on copy + let deep_force_err = nix_value_force_deep(ctx, state, copied_list); + assert_eq!(deep_force_err, nix_err_NIX_OK); + + // Both should still be accessible + assert_eq!(nix_get_list_size(ctx, final_list), 2); + assert_eq!(nix_get_list_size(ctx, copied_list), 2); + + // Clean up all the values + nix_value_decref(ctx, copied_list); + nix_value_decref(ctx, final_list); + nix_value_decref(ctx, attrs2); + nix_value_decref(ctx, attrs1); + nix_value_decref(ctx, val3); + nix_value_decref(ctx, val2); + nix_value_decref(ctx, val1); + nix_state_free(state); + nix_eval_state_builder_free(builder); + nix_store_free(store); + nix_c_context_free(ctx); + } +} + +#[test] +#[serial] +fn memory_management_error_conditions() { + unsafe { + let ctx = nix_c_context_create(); + assert!(!ctx.is_null()); + + // Test reference counting with NULL pointers (should handle gracefully) + let null_incref_err = + nix_gc_incref(ctx, std::ptr::null() as *const ::std::os::raw::c_void); + + // XXX: May succeed or fail depending on implementation. We can't really + // know, so assert both. + assert!( + null_incref_err == nix_err_NIX_OK + || null_incref_err == nix_err_NIX_ERR_UNKNOWN + ); + + let null_decref_err = + nix_gc_decref(ctx, std::ptr::null() as *const ::std::os::raw::c_void); + assert!( + null_decref_err == nix_err_NIX_OK + || null_decref_err == nix_err_NIX_ERR_UNKNOWN + ); + + let null_value_incref_err = nix_value_incref(ctx, std::ptr::null_mut()); + // Some Nix APIs gracefully handle null pointers and return OK + assert!( + null_value_incref_err == nix_err_NIX_OK + || null_value_incref_err == nix_err_NIX_ERR_UNKNOWN + ); + + let null_value_decref_err = nix_value_decref(ctx, std::ptr::null_mut()); + // Some Nix APIs gracefully handle null pointers and return OK + assert!( + null_value_decref_err == nix_err_NIX_OK + || null_value_decref_err == nix_err_NIX_ERR_UNKNOWN + ); + + // Test copy with NULL values + let err = nix_libutil_init(ctx); + assert_eq!(err, nix_err_NIX_OK); + + let err = nix_libstore_init(ctx); + assert_eq!(err, nix_err_NIX_OK); + + let err = nix_libexpr_init(ctx); + assert_eq!(err, nix_err_NIX_OK); + + let store = nix_store_open(ctx, std::ptr::null(), std::ptr::null_mut()); + assert!(!store.is_null()); + + let builder = nix_eval_state_builder_new(ctx, store); + assert!(!builder.is_null()); + + let load_err = nix_eval_state_builder_load(ctx, builder); + assert_eq!(load_err, nix_err_NIX_OK); + + let state = nix_eval_state_build(ctx, builder); + assert!(!state.is_null()); + + let valid_value = nix_alloc_value(ctx, state); + assert!(!valid_value.is_null()); + + // Test copying to/from NULL + let copy_from_null_err = + nix_copy_value(ctx, valid_value, std::ptr::null_mut()); + assert_ne!(copy_from_null_err, nix_err_NIX_OK); + + let copy_to_null_err = + nix_copy_value(ctx, std::ptr::null_mut(), valid_value); + assert_ne!(copy_to_null_err, nix_err_NIX_OK); + + // Clean up + nix_value_decref(ctx, valid_value); + nix_state_free(state); + nix_eval_state_builder_free(builder); + nix_store_free(store); + nix_c_context_free(ctx); + } +} diff --git a/nix-bindings-sys/tests/primop.rs b/nix-bindings-sys/tests/primop.rs new file mode 100644 index 0000000..fce94ba --- /dev/null +++ b/nix-bindings-sys/tests/primop.rs @@ -0,0 +1,666 @@ +#![cfg(test)] + +use std::{ + ffi::CString, + sync::atomic::{AtomicU32, Ordering}, +}; + +use nix_bindings_sys::*; +use serial_test::serial; + +#[derive(Debug)] +struct TestPrimOpData { + call_count: AtomicU32, + last_arg_value: AtomicU32, +} + +// Simple PrimOp that adds 1 to an integer argument +unsafe extern "C" fn add_one_primop( + user_data: *mut ::std::os::raw::c_void, + context: *mut nix_c_context, + state: *mut EvalState, + args: *mut *mut nix_value, + ret: *mut nix_value, +) { + if user_data.is_null() + || context.is_null() + || state.is_null() + || args.is_null() + || ret.is_null() + { + let _ = unsafe { + nix_set_err_msg( + context, + nix_err_NIX_ERR_UNKNOWN, + b"Null pointer in add_one_primop\0".as_ptr() as *const _, + ) + }; + return; + } + + let data = unsafe { &*(user_data as *const TestPrimOpData) }; + data.call_count.fetch_add(1, Ordering::SeqCst); + + // Get first argument + let arg = unsafe { *args.offset(0) }; + if arg.is_null() { + let _ = unsafe { + nix_set_err_msg( + context, + nix_err_NIX_ERR_UNKNOWN, + b"Missing argument in add_one_primop\0".as_ptr() as *const _, + ) + }; + return; + } + + // Force evaluation of argument + if unsafe { nix_value_force(context, state, arg) } != nix_err_NIX_OK { + return; + } + + // Check if argument is integer + if unsafe { nix_get_type(context, arg) } != ValueType_NIX_TYPE_INT { + let _ = unsafe { + nix_set_err_msg( + context, + nix_err_NIX_ERR_UNKNOWN, + b"Expected integer argument in add_one_primop\0".as_ptr() as *const _, + ) + }; + return; + } + + // Get integer value and add 1 + let value = unsafe { nix_get_int(context, arg) }; + data.last_arg_value.store(value as u32, Ordering::SeqCst); + + // Set return value + let _ = unsafe { nix_init_int(context, ret, value + 1) }; +} + +// PrimOp that returns a constant string +unsafe extern "C" fn hello_world_primop( + _user_data: *mut ::std::os::raw::c_void, + context: *mut nix_c_context, + _state: *mut EvalState, + _args: *mut *mut nix_value, + ret: *mut nix_value, +) { + let hello = CString::new("Hello from Rust PrimOp!").unwrap(); + let _ = unsafe { nix_init_string(context, ret, hello.as_ptr()) }; +} + +// PrimOp that takes multiple arguments and concatenates them +unsafe extern "C" fn concat_strings_primop( + _user_data: *mut ::std::os::raw::c_void, + context: *mut nix_c_context, + state: *mut EvalState, + args: *mut *mut nix_value, + ret: *mut nix_value, +) { + if context.is_null() || state.is_null() || args.is_null() || ret.is_null() { + return; + } + + // This PrimOp expects exactly 2 string arguments + let mut result = String::new(); + + for i in 0..2 { + let arg = unsafe { *args.offset(i) }; + if arg.is_null() { + let _ = unsafe { + nix_set_err_msg( + context, + nix_err_NIX_ERR_UNKNOWN, + b"Missing argument in concat_strings_primop\0".as_ptr() as *const _, + ) + }; + return; + } + + // Force evaluation + if unsafe { nix_value_force(context, state, arg) } != nix_err_NIX_OK { + return; + } + + // Check if it's a string + if unsafe { nix_get_type(context, arg) } != ValueType_NIX_TYPE_STRING { + let _ = unsafe { + static ITEMS: &[u8] = + b"Expected string argument in concat_strings_primop\0"; + nix_set_err_msg( + context, + nix_err_NIX_ERR_UNKNOWN, + ITEMS.as_ptr() as *const _, + ) + }; + return; + } + + // Get string value using callback + unsafe extern "C" fn string_callback( + start: *const ::std::os::raw::c_char, + n: ::std::os::raw::c_uint, + user_data: *mut ::std::os::raw::c_void, + ) { + let s = + unsafe { std::slice::from_raw_parts(start.cast::(), n as usize) }; + let s = std::str::from_utf8(s).unwrap_or(""); + let result = unsafe { &mut *(user_data as *mut String) }; + result.push_str(s); + } + + let _ = unsafe { + nix_get_string( + context, + arg, + Some(string_callback), + &mut result as *mut String as *mut ::std::os::raw::c_void, + ) + }; + } + + let result_cstr = + CString::new(result).unwrap_or_else(|_| CString::new("").unwrap()); + let _ = unsafe { nix_init_string(context, ret, result_cstr.as_ptr()) }; +} + +#[test] +#[serial] +fn primop_allocation_and_registration() { + unsafe { + let ctx = nix_c_context_create(); + assert!(!ctx.is_null()); + + let err = nix_libutil_init(ctx); + assert_eq!(err, nix_err_NIX_OK); + + let err = nix_libstore_init(ctx); + assert_eq!(err, nix_err_NIX_OK); + + let err = nix_libexpr_init(ctx); + assert_eq!(err, nix_err_NIX_OK); + + let store = nix_store_open(ctx, std::ptr::null(), std::ptr::null_mut()); + assert!(!store.is_null()); + + let builder = nix_eval_state_builder_new(ctx, store); + assert!(!builder.is_null()); + + let load_err = nix_eval_state_builder_load(ctx, builder); + assert_eq!(load_err, nix_err_NIX_OK); + + let state = nix_eval_state_build(ctx, builder); + assert!(!state.is_null()); + + // Create test data + let test_data = Box::new(TestPrimOpData { + call_count: AtomicU32::new(0), + last_arg_value: AtomicU32::new(0), + }); + let test_data_ptr = Box::into_raw(test_data); + + // Create argument names + let arg_names = [CString::new("x").unwrap()]; + let arg_name_ptrs: Vec<*const _> = + arg_names.iter().map(|s| s.as_ptr()).collect(); + let mut arg_name_ptrs_null_terminated = arg_name_ptrs; + arg_name_ptrs_null_terminated.push(std::ptr::null()); + + let name = CString::new("addOne").unwrap(); + let doc = CString::new("Add 1 to the argument").unwrap(); + + // Allocate PrimOp + let primop = nix_alloc_primop( + ctx, + Some(add_one_primop), + 1, // arity + name.as_ptr(), + arg_name_ptrs_null_terminated.as_mut_ptr(), + doc.as_ptr(), + test_data_ptr as *mut ::std::os::raw::c_void, + ); + + if !primop.is_null() { + // Register the PrimOp globally + let register_err = nix_register_primop(ctx, primop); + // Registration may fail in some environments, but allocation should work + assert!( + register_err == nix_err_NIX_OK + || register_err == nix_err_NIX_ERR_UNKNOWN, + "Unexpected error code: {register_err}" + ); + + // Test using the PrimOp by creating a value with it + let primop_value = nix_alloc_value(ctx, state); + assert!(!primop_value.is_null()); + + let init_err = nix_init_primop(ctx, primop_value, primop); + assert_eq!(init_err, nix_err_NIX_OK); + + // Clean up value + nix_value_decref(ctx, primop_value); + } + + // Clean up + let _ = Box::from_raw(test_data_ptr); + nix_state_free(state); + nix_eval_state_builder_free(builder); + nix_store_free(store); + nix_c_context_free(ctx); + } +} + +#[test] +#[serial] +fn primop_function_call() { + unsafe { + let ctx = nix_c_context_create(); + assert!(!ctx.is_null()); + + let err = nix_libutil_init(ctx); + assert_eq!(err, nix_err_NIX_OK); + + let err = nix_libstore_init(ctx); + assert_eq!(err, nix_err_NIX_OK); + + let err = nix_libexpr_init(ctx); + assert_eq!(err, nix_err_NIX_OK); + + let store = nix_store_open(ctx, std::ptr::null(), std::ptr::null_mut()); + assert!(!store.is_null()); + + let builder = nix_eval_state_builder_new(ctx, store); + assert!(!builder.is_null()); + + let load_err = nix_eval_state_builder_load(ctx, builder); + assert_eq!(load_err, nix_err_NIX_OK); + + let state = nix_eval_state_build(ctx, builder); + assert!(!state.is_null()); + + // Create test data + let test_data = Box::new(TestPrimOpData { + call_count: AtomicU32::new(0), + last_arg_value: AtomicU32::new(0), + }); + let test_data_ptr = Box::into_raw(test_data); + + // Create simple hello world PrimOp (no arguments) + let name = CString::new("helloWorld").unwrap(); + let doc = CString::new("Returns hello world string").unwrap(); + let mut empty_args: Vec<*const ::std::os::raw::c_char> = + vec![std::ptr::null()]; + + let hello_primop = nix_alloc_primop( + ctx, + Some(hello_world_primop), + 0, // arity + name.as_ptr(), + empty_args.as_mut_ptr(), + doc.as_ptr(), + std::ptr::null_mut(), + ); + + if !hello_primop.is_null() { + // Create a value with the PrimOp + let primop_value = nix_alloc_value(ctx, state); + assert!(!primop_value.is_null()); + + let init_err = nix_init_primop(ctx, primop_value, hello_primop); + assert_eq!(init_err, nix_err_NIX_OK); + + // Call the PrimOp (no arguments) + let result = nix_alloc_value(ctx, state); + assert!(!result.is_null()); + + let call_err = + nix_value_call(ctx, state, primop_value, std::ptr::null_mut(), result); + if call_err == nix_err_NIX_OK { + // Force the result + let force_err = nix_value_force(ctx, state, result); + assert_eq!(force_err, nix_err_NIX_OK); + + // Check if result is a string + let result_type = nix_get_type(ctx, result); + if result_type == ValueType_NIX_TYPE_STRING { + // Get string value + unsafe extern "C" fn string_callback( + start: *const ::std::os::raw::c_char, + n: ::std::os::raw::c_uint, + user_data: *mut ::std::os::raw::c_void, + ) { + let s = unsafe { + std::slice::from_raw_parts(start.cast::(), n as usize) + }; + let s = std::str::from_utf8(s).unwrap_or(""); + let result = unsafe { &mut *(user_data as *mut Option) }; + *result = Some(s.to_string()); + } + + let mut string_result: Option = None; + let _ = nix_get_string( + ctx, + result, + Some(string_callback), + &mut string_result as *mut Option + as *mut ::std::os::raw::c_void, + ); + + // Verify we got the expected string + assert!( + string_result + .as_deref() + .unwrap_or("") + .contains("Hello from Rust") + ); + } + } + + nix_value_decref(ctx, result); + nix_value_decref(ctx, primop_value); + } + + // Clean up + let _ = Box::from_raw(test_data_ptr); + nix_state_free(state); + nix_eval_state_builder_free(builder); + nix_store_free(store); + nix_c_context_free(ctx); + } +} + +#[test] +#[serial] +fn primop_with_arguments() { + unsafe { + let ctx = nix_c_context_create(); + assert!(!ctx.is_null()); + + let err = nix_libutil_init(ctx); + assert_eq!(err, nix_err_NIX_OK); + + let err = nix_libstore_init(ctx); + assert_eq!(err, nix_err_NIX_OK); + + let err = nix_libexpr_init(ctx); + assert_eq!(err, nix_err_NIX_OK); + + let store = nix_store_open(ctx, std::ptr::null(), std::ptr::null_mut()); + assert!(!store.is_null()); + + let builder = nix_eval_state_builder_new(ctx, store); + assert!(!builder.is_null()); + + let load_err = nix_eval_state_builder_load(ctx, builder); + assert_eq!(load_err, nix_err_NIX_OK); + + let state = nix_eval_state_build(ctx, builder); + assert!(!state.is_null()); + + // Create test data + let test_data = Box::new(TestPrimOpData { + call_count: AtomicU32::new(0), + last_arg_value: AtomicU32::new(0), + }); + let test_data_ptr = Box::into_raw(test_data); + + // Create add one PrimOp + let arg_names = [CString::new("x").unwrap()]; + let arg_name_ptrs: Vec<*const _> = + arg_names.iter().map(|s| s.as_ptr()).collect(); + let mut arg_name_ptrs_null_terminated = arg_name_ptrs; + arg_name_ptrs_null_terminated.push(std::ptr::null()); + + let name = CString::new("addOne").unwrap(); + let doc = CString::new("Add 1 to the argument").unwrap(); + + let add_primop = nix_alloc_primop( + ctx, + Some(add_one_primop), + 1, // arity + name.as_ptr(), + arg_name_ptrs_null_terminated.as_mut_ptr(), + doc.as_ptr(), + test_data_ptr as *mut ::std::os::raw::c_void, + ); + + if !add_primop.is_null() { + // Create a value with the PrimOp + let primop_value = nix_alloc_value(ctx, state); + assert!(!primop_value.is_null()); + + let init_err = nix_init_primop(ctx, primop_value, add_primop); + assert_eq!(init_err, nix_err_NIX_OK); + + // Create an integer argument + let arg_value = nix_alloc_value(ctx, state); + assert!(!arg_value.is_null()); + + let init_arg_err = nix_init_int(ctx, arg_value, 42); + assert_eq!(init_arg_err, nix_err_NIX_OK); + + // Call the PrimOp with the argument + let result = nix_alloc_value(ctx, state); + assert!(!result.is_null()); + + let call_err = + nix_value_call(ctx, state, primop_value, arg_value, result); + if call_err == nix_err_NIX_OK { + // Force the result + let force_err = nix_value_force(ctx, state, result); + assert_eq!(force_err, nix_err_NIX_OK); + + // Check if result is an integer + let result_type = nix_get_type(ctx, result); + if result_type == ValueType_NIX_TYPE_INT { + let result_value = nix_get_int(ctx, result); + assert_eq!(result_value, 43); // 42 + 1 + + // Verify callback was called + let test_data_ref = &*test_data_ptr; + assert_eq!(test_data_ref.call_count.load(Ordering::SeqCst), 1); + assert_eq!(test_data_ref.last_arg_value.load(Ordering::SeqCst), 42); + } + } + + nix_value_decref(ctx, result); + nix_value_decref(ctx, arg_value); + nix_value_decref(ctx, primop_value); + } + + // Clean up + let _ = Box::from_raw(test_data_ptr); + nix_state_free(state); + nix_eval_state_builder_free(builder); + nix_store_free(store); + nix_c_context_free(ctx); + } +} + +#[test] +#[serial] +fn primop_multi_argument() { + unsafe { + let ctx = nix_c_context_create(); + assert!(!ctx.is_null()); + + let err = nix_libutil_init(ctx); + assert_eq!(err, nix_err_NIX_OK); + + let err = nix_libstore_init(ctx); + assert_eq!(err, nix_err_NIX_OK); + + let err = nix_libexpr_init(ctx); + assert_eq!(err, nix_err_NIX_OK); + + let store = nix_store_open(ctx, std::ptr::null(), std::ptr::null_mut()); + assert!(!store.is_null()); + + let builder = nix_eval_state_builder_new(ctx, store); + assert!(!builder.is_null()); + + let load_err = nix_eval_state_builder_load(ctx, builder); + assert_eq!(load_err, nix_err_NIX_OK); + + let state = nix_eval_state_build(ctx, builder); + assert!(!state.is_null()); + + // Create concat strings PrimOp + let arg_names = [CString::new("s1").unwrap(), CString::new("s2").unwrap()]; + let arg_name_ptrs: Vec<*const _> = + arg_names.iter().map(|s| s.as_ptr()).collect(); + let mut arg_name_ptrs_null_terminated = arg_name_ptrs; + arg_name_ptrs_null_terminated.push(std::ptr::null()); + + let name = CString::new("concatStrings").unwrap(); + let doc = CString::new("Concatenate two strings").unwrap(); + + let concat_primop = nix_alloc_primop( + ctx, + Some(concat_strings_primop), + 2, // arity + name.as_ptr(), + arg_name_ptrs_null_terminated.as_mut_ptr(), + doc.as_ptr(), + std::ptr::null_mut(), + ); + + if !concat_primop.is_null() { + // Create a value with the PrimOp + let primop_value = nix_alloc_value(ctx, state); + assert!(!primop_value.is_null()); + + let init_err = nix_init_primop(ctx, primop_value, concat_primop); + assert_eq!(init_err, nix_err_NIX_OK); + + // Create string arguments + let arg1 = nix_alloc_value(ctx, state); + let arg2 = nix_alloc_value(ctx, state); + assert!(!arg1.is_null() && !arg2.is_null()); + + let hello_cstr = CString::new("Hello, ").unwrap(); + let world_cstr = CString::new("World!").unwrap(); + + let init_arg1_err = nix_init_string(ctx, arg1, hello_cstr.as_ptr()); + let init_arg2_err = nix_init_string(ctx, arg2, world_cstr.as_ptr()); + assert_eq!(init_arg1_err, nix_err_NIX_OK); + assert_eq!(init_arg2_err, nix_err_NIX_OK); + + // Test multi-argument call using nix_value_call_multi + let mut args = [arg1, arg2]; + let result = nix_alloc_value(ctx, state); + assert!(!result.is_null()); + + let call_err = nix_value_call_multi( + ctx, + state, + primop_value, + 2, + args.as_mut_ptr(), + result, + ); + if call_err == nix_err_NIX_OK { + // Force the result + let force_err = nix_value_force(ctx, state, result); + assert_eq!(force_err, nix_err_NIX_OK); + + // Check if result is a string + let result_type = nix_get_type(ctx, result); + if result_type == ValueType_NIX_TYPE_STRING { + unsafe extern "C" fn string_callback( + start: *const ::std::os::raw::c_char, + n: ::std::os::raw::c_uint, + user_data: *mut ::std::os::raw::c_void, + ) { + let s = unsafe { + std::slice::from_raw_parts(start.cast::(), n as usize) + }; + let s = std::str::from_utf8(s).unwrap_or(""); + let result = unsafe { &mut *(user_data as *mut Option) }; + *result = Some(s.to_string()); + } + + let mut string_result: Option = None; + let _ = nix_get_string( + ctx, + result, + Some(string_callback), + &mut string_result as *mut Option + as *mut ::std::os::raw::c_void, + ); + + // Verify concatenation worked + assert_eq!(string_result.as_deref(), Some("Hello, World!")); + } + } + + nix_value_decref(ctx, result); + nix_value_decref(ctx, arg2); + nix_value_decref(ctx, arg1); + nix_value_decref(ctx, primop_value); + } + + nix_state_free(state); + nix_eval_state_builder_free(builder); + nix_store_free(store); + nix_c_context_free(ctx); + } +} + +#[test] +#[serial] +fn primop_error_handling() { + unsafe { + let ctx = nix_c_context_create(); + assert!(!ctx.is_null()); + + let err = nix_libutil_init(ctx); + assert_eq!(err, nix_err_NIX_OK); + + let err = nix_libstore_init(ctx); + assert_eq!(err, nix_err_NIX_OK); + + let err = nix_libexpr_init(ctx); + assert_eq!(err, nix_err_NIX_OK); + + let store = nix_store_open(ctx, std::ptr::null(), std::ptr::null_mut()); + assert!(!store.is_null()); + + let builder = nix_eval_state_builder_new(ctx, store); + assert!(!builder.is_null()); + + let load_err = nix_eval_state_builder_load(ctx, builder); + assert_eq!(load_err, nix_err_NIX_OK); + + let state = nix_eval_state_build(ctx, builder); + assert!(!state.is_null()); + + // Test invalid PrimOp allocation (NULL callback) + let name = CString::new("invalid").unwrap(); + let doc = CString::new("Invalid PrimOp").unwrap(); + let mut empty_args: Vec<*const ::std::os::raw::c_char> = + vec![std::ptr::null()]; + + let _invalid_primop = nix_alloc_primop( + ctx, + None, // NULL callback should cause error + 0, + name.as_ptr(), + empty_args.as_mut_ptr(), + doc.as_ptr(), + std::ptr::null_mut(), + ); + + // Test initializing value with NULL PrimOp (should fail) + let test_value = nix_alloc_value(ctx, state); + assert!(!test_value.is_null()); + + nix_value_decref(ctx, test_value); + nix_state_free(state); + nix_eval_state_builder_free(builder); + nix_store_free(store); + nix_c_context_free(ctx); + } +}