--- a/bindings/jsonnet/Cargo.toml +++ b/bindings/jsonnet/Cargo.toml @@ -1,5 +1,5 @@ [package] -name = "jsonnet" +name = "libjsonnet" description = "Rust implementation of libjsonnet.so" version = "0.4.2" authors = ["Yaroslav Bolyukin "] @@ -14,9 +14,11 @@ jrsonnet-gcmodule = { version = "0.3.4" } [lib] +name = "jsonnet" crate-type = ["cdylib"] [features] +# Export additional functions for native integration, i.e ability to set custom trace format interop = [] experimental = ["exp-preserve-order", "exp-destruct"] exp-preserve-order = ["jrsonnet-evaluator/exp-preserve-order"] --- a/bindings/jsonnet/src/lib.rs +++ b/bindings/jsonnet/src/lib.rs @@ -10,51 +10,101 @@ use std::{ alloc::Layout, - env, - ffi::{CStr, CString}, + borrow::Cow, + ffi::{CStr, CString, OsStr}, os::raw::{c_char, c_double, c_int, c_uint}, + path::Path, }; -use import::NativeImportResolver; -use jrsonnet_evaluator::{IStr, ManifestFormat, State, Val}; +use jrsonnet_evaluator::{ + trace::PathResolver, FileImportResolver, IStr, ManifestFormat, State, Val, +}; /// WASM stub #[cfg(target_arch = "wasm32")] #[no_mangle] pub extern "C" fn _start() {} +/// Return the version string of the Jsonnet interpreter. Conforms to semantic versioning +/// http://semver.org/ If this does not match LIB_JSONNET_VERSION then there is a mismatch between +/// header and compiled library. #[no_mangle] pub extern "C" fn jsonnet_version() -> &'static [u8; 8] { b"v0.16.0\0" } +unsafe fn parse_path(input: &CStr) -> Cow { + #[cfg(target_family = "unix")] + { + use std::os::unix::ffi::OsStrExt; + let str = OsStr::from_bytes(input.to_bytes()); + Cow::Borrowed(Path::new(str)) + } + #[cfg(target_family = "windows")] + { + use std::os::windows::ffi::OsStringExt; + let str = input.to_str().expect("input is not utf8"); + let wide = str.encode_utf16().collect::>(); + let wide = OsString::from_wide(&wide); + Cow::Owned(PathBuf::new(wide)) + } + #[cfg(not(any(target_family = "unix", target_family = "windows")))] + { + compile_error!("unsupported os") + } +} + +unsafe fn unparse_path(input: &Path) -> Cow { + #[cfg(target_family = "unix")] + { + use std::os::unix::ffi::OsStrExt; + let str = CString::new(input.as_os_str().as_bytes()).expect("input has zero byte in it"); + Cow::Owned(str) + } + #[cfg(not(any(target_family = "unix", target_family = "windows")))] + { + compile_error!("unsupported os") + } +} + +/// Create a new Jsonnet virtual machine. #[no_mangle] pub extern "C" fn jsonnet_make() -> *mut State { let state = State::default(); - state.settings_mut().import_resolver = Box::new(NativeImportResolver::default()); - state.settings_mut().context_initializer = - Box::new(jrsonnet_stdlib::ContextInitializer::new(state.clone())); + state.settings_mut().import_resolver = Box::new(FileImportResolver::default()); + state.settings_mut().context_initializer = Box::new(jrsonnet_stdlib::ContextInitializer::new( + state.clone(), + PathResolver::new_cwd_fallback(), + )); Box::into_raw(Box::new(state)) } -/// # Safety +/// Complement of `jsonnet_vm_make` #[no_mangle] #[allow(clippy::boxed_local)] -pub unsafe extern "C" fn jsonnet_destroy(vm: *mut State) { - drop(Box::from_raw(vm)); +pub extern "C" fn jsonnet_destroy(vm: Box) { + drop(vm); } +/// Set the maximum stack depth. #[no_mangle] pub extern "C" fn jsonnet_max_stack(vm: &State, v: c_uint) { vm.settings_mut().max_stack = v as usize; } -// jrsonnet currently have no GC, so these functions is no-op +/// Set the number of objects required before a garbage collection cycle is allowed. +/// +/// No-op for now #[no_mangle] pub extern "C" fn jsonnet_gc_min_objects(_vm: &State, _v: c_uint) {} + +/// Run the garbage collector after this amount of growth in the number of objects +/// +/// No-op for now #[no_mangle] pub extern "C" fn jsonnet_gc_growth_trigger(_vm: &State, _v: c_double) {} +/// Expect a string as output and don't JSON encode it. #[no_mangle] pub extern "C" fn jsonnet_string_output(vm: &State, v: c_int) { match v { @@ -68,13 +118,20 @@ } } +/// Allocate, resize, or free a buffer. This will abort if the memory cannot be allocated. It will +/// only return NULL if sz was zero. +/// /// # Safety /// +/// `buf` should be either previosly allocated by this library, or NULL +/// /// This function is most definitely broken, but it works somehow, see TODO inside #[no_mangle] pub unsafe extern "C" fn jsonnet_realloc(_vm: &State, buf: *mut u8, sz: usize) -> *mut u8 { if buf.is_null() { - assert!(sz != 0); + if sz == 0 { + return std::ptr::null_mut(); + } return std::alloc::alloc(Layout::from_size_align(sz, std::mem::align_of::()).unwrap()); } // TODO: Somehow store size of allocation, because its real size is probally not 16 :D @@ -89,33 +146,37 @@ std::alloc::realloc(buf, old_layout, sz) } -/// # Safety +/// Clean up a JSON subtree. +/// +/// This is useful if you want to abort with an error mid-way through building a complex value. #[no_mangle] #[allow(clippy::boxed_local)] -pub unsafe extern "C" fn jsonnet_json_destroy(_vm: &State, v: *mut Val) { - drop(Box::from_raw(v)); +pub extern "C" fn jsonnet_json_destroy(_vm: &State, v: Box) { + drop(v); } +/// Set the number of lines of stack trace to display (0 for all of them). #[no_mangle] pub extern "C" fn jsonnet_max_trace(vm: &State, v: c_uint) { vm.set_max_trace(v as usize) } +/// Evaluate a file containing Jsonnet code, return a JSON string. +/// +/// The returned string should be cleaned up with jsonnet_realloc. +/// /// # Safety /// -/// This function is safe, if received v is a pointer to normal C string +/// `filename` should be a \0-terminated string #[no_mangle] pub unsafe extern "C" fn jsonnet_evaluate_file( vm: &State, filename: *const c_char, error: &mut c_int, ) -> *const c_char { - let filename = CStr::from_ptr(filename); + let filename = parse_path(CStr::from_ptr(filename)); match vm - .import( - &env::current_dir().expect("cwd"), - filename.to_str().unwrap(), - ) + .import(&filename) .and_then(|v| vm.with_tla(v)) .and_then(|v| vm.manifest(v)) { @@ -131,9 +192,13 @@ } } +/// Evaluate a string containing Jsonnet code, return a JSON string. +/// +/// The returned string should be cleaned up with jsonnet_realloc. +/// /// # Safety /// -/// This function is safe, if received v is a pointer to normal C string +/// `filename`, `snippet` should be a \0-terminated strings #[no_mangle] pub unsafe extern "C" fn jsonnet_evaluate_snippet( vm: &State, @@ -144,7 +209,7 @@ let filename = CStr::from_ptr(filename); let snippet = CStr::from_ptr(snippet); match vm - .evaluate_snippet(filename.to_str().unwrap().into(), snippet.to_str().unwrap()) + .evaluate_snippet(filename.to_str().unwrap(), snippet.to_str().unwrap()) .and_then(|v| vm.with_tla(v)) .and_then(|v| vm.manifest(v)) { @@ -184,12 +249,9 @@ filename: *const c_char, error: &mut c_int, ) -> *const c_char { - let filename = CStr::from_ptr(filename); + let filename = parse_path(CStr::from_ptr(filename)); match vm - .import( - &env::current_dir().expect("cwd"), - filename.to_str().unwrap(), - ) + .import(&filename) .and_then(|v| vm.with_tla(v)) .and_then(|v| vm.manifest_multi(v)) { @@ -216,7 +278,7 @@ let filename = CStr::from_ptr(filename); let snippet = CStr::from_ptr(snippet); match vm - .evaluate_snippet(filename.to_str().unwrap().into(), snippet.to_str().unwrap()) + .evaluate_snippet(filename.to_str().unwrap(), snippet.to_str().unwrap()) .and_then(|v| vm.with_tla(v)) .and_then(|v| vm.manifest_multi(v)) { @@ -254,12 +316,9 @@ filename: *const c_char, error: &mut c_int, ) -> *const c_char { - let filename = CStr::from_ptr(filename); + let filename = parse_path(CStr::from_ptr(filename)); match vm - .import( - &env::current_dir().expect("cwd"), - filename.to_str().unwrap(), - ) + .import(&filename) .and_then(|v| vm.with_tla(v)) .and_then(|v| vm.manifest_stream(v)) { @@ -270,7 +329,9 @@ Err(e) => { *error = 1; let out = vm.stringify_err(&e); - CString::new(&out as &str).unwrap().into_raw() + CString::new(&out as &str) + .expect("there should be no \\0 in the error string") + .into_raw() } } } @@ -286,7 +347,10 @@ let filename = CStr::from_ptr(filename); let snippet = CStr::from_ptr(snippet); match vm - .evaluate_snippet(filename.to_str().unwrap().into(), snippet.to_str().unwrap()) + .evaluate_snippet( + filename.to_str().expect("filename is not utf-8"), + snippet.to_str().expect("snippet is not utf-8"), + ) .and_then(|v| vm.with_tla(v)) .and_then(|v| vm.manifest_stream(v)) { @@ -297,7 +361,9 @@ Err(e) => { *error = 1; let out = vm.stringify_err(&e); - CString::new(&out as &str).unwrap().into_raw() + CString::new(&out as &str) + .expect("there should be no \\0 in the error string") + .into_raw() } } } --- a/bindings/jsonnet/src/native.rs +++ b/bindings/jsonnet/src/native.rs @@ -12,6 +12,15 @@ }; use jrsonnet_gcmodule::Cc; +/// The returned JsonnetJsonValue* should be allocated with jsonnet_realloc. It will be cleaned up +/// along with the objects rooted at argv by libjsonnet when no-longer needed. Return a string upon +/// failure, which will appear in Jsonnet as an error. The argv pointer is an array whose size +/// matches the array of parameters supplied when the native callback was originally registered. +/// +/// - `ctx` User pointer, given in jsonnet_native_callback. +/// - `argv` Array of arguments from Jsonnet code. +/// - `param` success Set this byref param to 1 to indicate success and 0 for failure. +/// Returns the content of the imported file, or an error message. type JsonnetNativeCallback = unsafe extern "C" fn( ctx: *const c_void, argv: *const *const Val, @@ -44,13 +53,20 @@ if success == 1 { Ok(v) } else { - let e = IStr::from_untyped(v, s).expect("error msg"); + let e = IStr::from_untyped(v, s).expect("error msg should be a string"); Err(Error::RuntimeError(e).into()) } } } +/// Callback to provide native extensions to Jsonnet. +/// /// # Safety +/// +/// `vm` should be a vm allocated by `jsonnet_make` +/// `cb` should be a correct function pointer +/// `raw_params` should point to a NULL-terminated string array +/// `name`, `raw_params` elements should be a \0-terminated strings #[no_mangle] pub unsafe extern "C" fn jsonnet_native_callback( vm: &State, @@ -59,13 +75,18 @@ ctx: *const c_void, mut raw_params: *const *const c_char, ) { - let name = CStr::from_ptr(name).to_str().expect("utf8 name").into(); + let name = CStr::from_ptr(name) + .to_str() + .expect("name is not utf-8") + .into(); let mut params = Vec::new(); loop { if (*raw_params).is_null() { break; } - let param = CStr::from_ptr(*raw_params).to_str().expect("not utf8"); + let param = CStr::from_ptr(*raw_params) + .to_str() + .expect("param name is not utf-8"); params.push(BuiltinParam { name: param.into(), has_default: false, --- a/bindings/jsonnet/src/val_extract.rs +++ b/bindings/jsonnet/src/val_extract.rs @@ -7,6 +7,7 @@ use jrsonnet_evaluator::{State, Val}; +/// If the value is a string, return it as UTF8 otherwise return NULL. #[no_mangle] pub extern "C" fn jsonnet_json_extract_string(_vm: &State, v: &Val) -> *mut c_char { match v { @@ -14,6 +15,8 @@ _ => std::ptr::null_mut(), } } + +/// If the value is a number, return 1 and store the number in out, otherwise return 0. #[no_mangle] pub extern "C" fn jsonnet_json_extract_number(_vm: &State, v: &Val, out: &mut c_double) -> c_int { match v { @@ -24,6 +27,8 @@ _ => 0, } } + +/// Return 0 if the value is false, 1 if it is true, and 2 if it is not a bool. #[no_mangle] pub extern "C" fn jsonnet_json_extract_bool(_vm: &State, v: &Val) -> c_int { match v { @@ -32,6 +37,8 @@ _ => 2, } } + +/// Return 1 if the value is null, else 0. #[no_mangle] pub extern "C" fn jsonnet_json_extract_null(_vm: &State, v: &Val) -> c_int { match v { --- a/bindings/jsonnet/src/val_make.rs +++ b/bindings/jsonnet/src/val_make.rs @@ -8,37 +8,46 @@ use jrsonnet_evaluator::{val::ArrValue, ObjValue, State, Val}; use jrsonnet_gcmodule::Cc; +/// Convert the given UTF8 string to a JsonnetJsonValue. +/// /// # Safety /// -/// This function is safe, if received v is a pointer to normal C string +/// `v` should be a \0-terminated string #[no_mangle] -pub unsafe extern "C" fn jsonnet_json_make_string(_vm: &State, v: *const c_char) -> *mut Val { - let cstr = CStr::from_ptr(v); - let str = cstr.to_str().unwrap(); - Box::into_raw(Box::new(Val::Str(str.into()))) +pub unsafe extern "C" fn jsonnet_json_make_string(_vm: &State, val: *const c_char) -> *mut Val { + let val = CStr::from_ptr(val); + let val = val.to_str().expect("string is not utf-8"); + Box::into_raw(Box::new(Val::Str(val.into()))) } +/// Convert the given double to a JsonnetJsonValue. #[no_mangle] pub extern "C" fn jsonnet_json_make_number(_vm: &State, v: c_double) -> *mut Val { Box::into_raw(Box::new(Val::Num(v))) } +/// Convert the given bool (1 or 0) to a JsonnetJsonValue. #[no_mangle] pub extern "C" fn jsonnet_json_make_bool(_vm: &State, v: c_int) -> *mut Val { - assert!(v == 0 || v == 1); + assert!(v == 0 || v == 1, "bad boolean value"); Box::into_raw(Box::new(Val::Bool(v == 1))) } +/// Make a JsonnetJsonValue representing null. #[no_mangle] pub extern "C" fn jsonnet_json_make_null(_vm: &State) -> *mut Val { Box::into_raw(Box::new(Val::Null)) } +/// Make a JsonnetJsonValue representing an array. +/// +/// Assign elements with jsonnet_json_array_append. #[no_mangle] pub extern "C" fn jsonnet_json_make_array(_vm: &State) -> *mut Val { Box::into_raw(Box::new(Val::Arr(ArrValue::Eager(Cc::new(Vec::new()))))) } +/// Make a JsonnetJsonValue representing an object. #[no_mangle] pub extern "C" fn jsonnet_json_make_object(_vm: &State) -> *mut Val { Box::into_raw(Box::new(Val::Obj(ObjValue::new_empty()))) --- a/bindings/jsonnet/src/val_modify.rs +++ b/bindings/jsonnet/src/val_modify.rs @@ -7,9 +7,12 @@ use jrsonnet_evaluator::{val::ArrValue, State, Thunk, Val}; use jrsonnet_gcmodule::Cc; +/// Add value to the end of the array arr +/// /// # Safety /// -/// Received arr value should be correct pointer to array allocated by make_array +/// `arr` should be correct pointer to array value allocated by make_array, or returned by other library call +/// `val` should be correct pointer to value allocated using this library #[no_mangle] pub unsafe extern "C" fn jsonnet_json_array_append(_vm: &State, arr: &mut Val, val: &Val) { match arr { @@ -26,9 +29,14 @@ } } +/// Add the field to the object, bound to value. +/// +/// This shadows any previous binding of the field. +/// /// # Safety /// -/// This function is safe if passed name is ok +/// `obj` should be pointer to object value allocated by make_object, or returned by other library call +/// `name` should be \0-terminated string #[no_mangle] pub unsafe extern "C" fn jsonnet_json_object_append( _vm: &State, --- a/bindings/jsonnet/src/vars_tlas.rs +++ b/bindings/jsonnet/src/vars_tlas.rs @@ -4,52 +4,84 @@ use jrsonnet_evaluator::State; +/// Bind a Jsonnet external var to the given string. +/// +/// Argument values are copied so memory should be managed by caller. +/// /// # Safety +/// +/// Caller should pass correct pointers as `name` and `code`, they need to be \0-terminated strings #[no_mangle] pub unsafe extern "C" fn jsonnet_ext_var(vm: &State, name: *const c_char, value: *const c_char) { let name = CStr::from_ptr(name); let value = CStr::from_ptr(value); - let any_resolver = vm.context_initializer(); - any_resolver + let any_initializer = vm.context_initializer(); + any_initializer .as_any() .downcast_ref::() .expect("only stdlib context initializer supported") .add_ext_str( - name.to_str().unwrap().into(), - value.to_str().unwrap().into(), + name.to_str().expect("name is not utf-8").into(), + value.to_str().expect("value is not utf-8").into(), ) } +/// Bind a Jsonnet external var to the given code. +/// +/// Argument values are copied so memory should be managed by caller. +/// /// # Safety +/// +/// Caller should pass correct pointers as `name` and `code`, they need to be \0-terminated strings #[no_mangle] -pub unsafe extern "C" fn jsonnet_ext_code(vm: &State, name: *const c_char, value: *const c_char) { +pub unsafe extern "C" fn jsonnet_ext_code(vm: &State, name: *const c_char, code: *const c_char) { let name = CStr::from_ptr(name); - let value = CStr::from_ptr(value); + let code = CStr::from_ptr(code); - let any_resolver = vm.context_initializer(); - any_resolver + let any_initializer = vm.context_initializer(); + any_initializer .as_any() .downcast_ref::() .expect("only stdlib context initializer supported") - .add_ext_code(name.to_str().unwrap(), value.to_str().unwrap()) - .unwrap() + .add_ext_code( + name.to_str().expect("name is not utf-8"), + code.to_str().expect("code is not utf-8"), + ) + .expect("can't parse ext code") } + +/// Bind a string top-level argument for a top-level parameter. +/// +/// Argument values are copied so memory should be managed by caller. +/// /// # Safety +/// +/// Caller should pass correct pointers as `name` and `value`, they need to be \0-terminated strings #[no_mangle] pub unsafe extern "C" fn jsonnet_tla_var(vm: &State, name: *const c_char, value: *const c_char) { let name = CStr::from_ptr(name); let value = CStr::from_ptr(value); vm.add_tla_str( - name.to_str().unwrap().into(), - value.to_str().unwrap().into(), + name.to_str().expect("name is not utf-8").into(), + value.to_str().expect("value is not utf-8").into(), ) } + +/// Bind a code top-level argument for a top-level parameter. +/// +/// Argument values are copied so memory should be managed by caller. +/// /// # Safety +/// +/// Caller should pass correct pointers as `name` and `code`, they need to be \0-terminated strings #[no_mangle] -pub unsafe extern "C" fn jsonnet_tla_code(vm: &State, name: *const c_char, value: *const c_char) { +pub unsafe extern "C" fn jsonnet_tla_code(vm: &State, name: *const c_char, code: *const c_char) { let name = CStr::from_ptr(name); - let value = CStr::from_ptr(value); - vm.add_tla_code(name.to_str().unwrap().into(), value.to_str().unwrap()) - .unwrap() + let code = CStr::from_ptr(code); + vm.add_tla_code( + name.to_str().expect("name is not utf-8").into(), + code.to_str().expect("code is not utf-8"), + ) + .expect("can't parse tla code") }