--- /dev/null +++ b/bindings/c/.gitignore @@ -0,0 +1 @@ +libjsonnet_test_file --- a/bindings/js/index.js +++ b/bindings/js/index.js @@ -1,16 +1,48 @@ const fs = require('fs'); +const path = require('path'); const { WASI } = require('wasi'); const wasi = new WASI({ args: process.argv, env: process.env, preopens: {}, }); -const importObject = { wasi_snapshot_preview1: wasi.wasiImport }; class JsonnetVM { constructor(wasm, vm) { this.wasm = wasm; this.vm = vm; + this.wasm.exports.jrsonnet_set_trace_format(this.vm, 1); + + this.setImportCallback((from, to) => { + const resolved = path.resolve(from, to); + return { + value: fs.readFileSync(resolved).toString('utf-8'), + foundHere: resolved, + }; + }) + } + + /** + * @param {(from: string, to: string) => {foundHere: string, value: string}} cb + */ + setImportCallback(cb) { + this.wasm.importCbs.set(this.vm, (base, rel, foundHere, success) => { + const baseStr = this.wasm.readString(base); + const relStr = this.wasm.readString(rel); + try { + const value = cb(baseStr, relStr); + this.wasm.memorySlice32Len(foundHere, 1)[0] = this.allocateString(value.foundHere); + this.wasm.memorySlice32Len(success, 1)[0] = 1; + return this.allocateString(value.value); + } catch (e) { + this.wasm.memorySlice32Len(success, 1)[0] = 0; + return this.allocateString(e.stack) + } + }); + this.wasm.exports.jrsonnet_apply_static_import_callback( + this.vm, + this.vm, + ); } alloc(length) { @@ -18,7 +50,7 @@ } allocateString(string) { const byteLength = new TextEncoder().encode(string).length; - const addr = this.alloc(byteLength); + const addr = this.alloc(byteLength + 1); this.wasm.writeString(addr, string); return addr; } @@ -36,7 +68,7 @@ const result = this.wasm.readString(resultAddr).trim(); this.dealloc(resultAddr); if (resultCode[0] === 1) { - const error = new Error(this.normalizeErrorString(result)); + const error = new Error(result); throw error; } else { return result; @@ -54,31 +86,46 @@ const result = this.wasm.readString(resultAddr); this.dealloc(resultAddr); if (resultCode[0] === 1) { - const error = new Error(this.normalizeErrorString(result)); + const error = new Error(result); throw error; } else { return result; } } - normalizeErrorString(str) { - str = str.trim(); - const newLine = str.indexOf('\n'); - if (newLine === -1) return str; - let message = str.slice(0, newLine); - let trace = str.slice(newLine + 1).split('\n').map(s => s.split(' ---- ')).map(([p, v]) => ` at ${v} (${p})`).join('\n'); - return `${message}\n${trace}`; + + /** + * Destroys vm, any future call to this object will fail, and all resources will be freed + */ + destroy() { + this.wasm.exports.jsonnet_destroy(this.vm); + this.wasm.importCbs.delete(this.vm); } } class JsonnetWASM { - constructor() { } + constructor() { + this.importCbs = new Map(); + } async init(buf) { const wasm = await WebAssembly.compile(buf); - const instance = await WebAssembly.instantiate(wasm, importObject); + const instance = await WebAssembly.instantiate(wasm, { + wasi_snapshot_preview1: wasi.wasiImport, + env: { + _jrsonnet_static_import_callback: (ctx, base, rel, found_here, success) => { + if (!this.importCbs.has(ctx)) { + throw new Error(`Got unknown ctx callback: ${ctx}`); + } + return this.importCbs.get(ctx)(base, rel, found_here, success); + } + } + }); wasi.start(instance); this.instance = instance; } + /** + * @type Record + */ get exports() { return this.instance.exports; } @@ -91,6 +138,9 @@ memorySliceLen(start, length) { return new Uint8Array(this.memoryBuffer, start, length); } + memorySlice32Len(start, length) { + return new Uint32Array(this.memoryBuffer, start, length); + } memorySlice(start, end) { return new Uint8Array(this.memoryBuffer, start, start && end && (end - start)); } @@ -119,15 +169,13 @@ (async () => { try { const jsonnet = new JsonnetWASM(); - await jsonnet.init(fs.readFileSync(`${__dirname}/../../target/wasm32-wasi/release/jsonnet.wasi.wasm`)); + await jsonnet.init(fs.readFileSync(`${__dirname}/../../target/wasm32-wasi/release/jsonnet.wasm`)); console.log(`Version = ${jsonnet.version()}`); const vm = jsonnet.newVM(); console.log(vm.evaluateSnippet('./snip.jsonnet', ` - local a(b) = error "sad" + b; - local c() = a(2 + 2); - c() - `)) + 2+2 + `)); console.log(vm.evaluateFile('./test.jsonnet')); } catch (e) { console.log(e.stack); --- a/bindings/jsonnet/Cargo.toml +++ b/bindings/jsonnet/Cargo.toml @@ -9,7 +9,6 @@ [dependencies] jrsonnet-evaluator = { path = "../../crates/jrsonnet-evaluator", version = "1.0.0" } jrsonnet-parser = { path = "../../crates/jrsonnet-parser", version = "1.0.0" } -libc = "0.2.71" [lib] crate-type = ["cdylib"] --- /dev/null +++ b/bindings/jsonnet/src/interop.rs @@ -0,0 +1,36 @@ +//! Jrsonnet specific additional binding helpers + +use crate::import::jsonnet_import_callback; +use jrsonnet_evaluator::EvaluationState; +use std::{ + ffi::c_void, + os::raw::{c_char, c_int}, +}; + +extern "C" { + pub fn _jrsonnet_static_import_callback( + ctx: *mut c_void, + base: *const c_char, + rel: *const c_char, + found_here: *mut *const c_char, + success: &mut c_int, + ) -> *const c_char; +} + +/// # Safety +#[no_mangle] +pub unsafe extern "C" fn jrsonnet_apply_static_import_callback( + vm: &EvaluationState, + ctx: *mut c_void, +) { + jsonnet_import_callback(vm, _jrsonnet_static_import_callback, ctx) +} + +#[no_mangle] +pub extern "C" fn jrsonnet_set_trace_format(vm: &EvaluationState, format: u8) { + use jrsonnet_evaluator::trace::JSFormat; + match format { + 1 => vm.set_trace_format(Box::new(JSFormat)), + _ => panic!("unknown trace format"), + } +} --- a/bindings/jsonnet/src/lib.rs +++ b/bindings/jsonnet/src/lib.rs @@ -1,23 +1,24 @@ -use jrsonnet_evaluator::{ - create_error, create_error_result, Error, EvaluationState, ImportResolver, LazyBinding, - LazyVal, ObjMember, ObjValue, Result, Val, -}; -use jrsonnet_parser::Visibility; -use libc::{c_char, c_double, c_int, c_uint}; +#![feature(custom_inner_attributes)] + +pub mod import; +pub mod interop; +pub mod val_extract; +pub mod val_make; +pub mod val_modify; +pub mod vars_tlas; + +use import::NativeImportResolver; +use jrsonnet_evaluator::{EvaluationState, ManifestFormat, Val}; use std::{ alloc::Layout, - any::Any, - cell::RefCell, - collections::HashMap, ffi::{CStr, CString}, - fs::File, - io::Read, + os::raw::{c_char, c_double, c_int, c_uint}, path::PathBuf, rc::Rc, }; +/// WASM stub #[no_mangle] -#[cfg(target = "wasm32-wasi")] pub extern "C" fn _start() {} #[no_mangle] @@ -25,45 +26,6 @@ b"v0.16.0\0" } -#[derive(Default)] -struct NativeImportResolver { - library_paths: RefCell>, -} -impl NativeImportResolver { - fn add_jpath(&self, path: PathBuf) { - self.library_paths.borrow_mut().push(path); - } -} -impl ImportResolver for NativeImportResolver { - fn resolve_file(&self, from: &PathBuf, path: &PathBuf) -> Result> { - let mut new_path = from.clone(); - new_path.push(path); - if new_path.exists() { - Ok(Rc::new(new_path)) - } else { - for library_path in self.library_paths.borrow().iter() { - let mut cloned = library_path.clone(); - cloned.push(path); - if cloned.exists() { - return Ok(Rc::new(cloned)); - } - } - create_error_result(Error::ImportFileNotFound(from.clone(), path.clone())) - } - } - fn load_file_contents(&self, id: &PathBuf) -> Result> { - let mut file = - File::open(id).map_err(|_e| create_error(Error::ResolvedFileNotFound(id.clone())))?; - let mut out = String::new(); - file.read_to_string(&mut out) - .map_err(|_e| create_error(Error::ImportBadFileUtf8(id.clone())))?; - Ok(out.into()) - } - unsafe fn as_any(&self) -> &dyn Any { - self - } -} - #[no_mangle] pub extern "C" fn jsonnet_make() -> *mut EvaluationState { let state = EvaluationState::default(); @@ -89,134 +51,18 @@ pub extern "C" fn jsonnet_gc_min_objects(_vm: &EvaluationState, _v: c_uint) {} #[no_mangle] pub extern "C" fn jsonnet_gc_growth_trigger(_vm: &EvaluationState, _v: c_double) {} - -// TODO -#[no_mangle] -pub extern "C" fn jsonnet_string_output(_vm: &EvaluationState, _v: c_int) { - todo!() -} #[no_mangle] -pub extern "C" fn jsonnet_json_extract_string(_vm: &EvaluationState, v: &Val) -> *mut c_char { - match v.unwrap_if_lazy().unwrap() { - Val::Str(s) => CString::new(&*s as &str).unwrap().into_raw(), - _ => std::ptr::null_mut(), - } -} -#[no_mangle] -pub extern "C" fn jsonnet_json_extract_number( - _vm: &EvaluationState, - v: &Val, - out: &mut c_double, -) -> c_int { - match v.unwrap_if_lazy().unwrap() { - Val::Num(n) => { - *out = n; - 1 - } - _ => 0, - } -} -#[no_mangle] -pub extern "C" fn jsonnet_json_extract_bool(_vm: &EvaluationState, v: &Val) -> c_int { - match v.unwrap_if_lazy().unwrap() { - Val::Bool(false) => 0, - Val::Bool(true) => 1, - _ => 2, - } -} -#[no_mangle] -pub extern "C" fn jsonnet_json_extract_null(_vm: &EvaluationState, v: &Val) -> c_int { - match v.unwrap_if_lazy().unwrap() { - Val::Null => 1, - _ => 0, - } -} - -/// # Safety -/// -/// This function is safe, if received v is a pointer to normal C string -#[no_mangle] -pub unsafe extern "C" fn jsonnet_json_make_string( - _vm: &EvaluationState, - 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()))) -} - -#[no_mangle] -pub extern "C" fn jsonnet_json_make_number(_vm: &EvaluationState, v: c_double) -> *mut Val { - Box::into_raw(Box::new(Val::Num(v))) -} - -#[no_mangle] -pub extern "C" fn jsonnet_json_make_bool(_vm: &EvaluationState, v: c_int) -> *mut Val { - assert!(v == 0 || v == 1); - Box::into_raw(Box::new(Val::Bool(v == 1))) -} - -#[no_mangle] -pub extern "C" fn jsonnet_json_make_null(_vm: &EvaluationState) -> *mut Val { - Box::into_raw(Box::new(Val::Null)) -} - -#[no_mangle] -pub extern "C" fn jsonnet_json_make_array(_vm: &EvaluationState) -> *mut Val { - Box::into_raw(Box::new(Val::Arr(Rc::new(Vec::new())))) -} - -#[no_mangle] -pub extern "C" fn jsonnet_json_array_append(_vm: &EvaluationState, arr: &mut Val, val: &Val) { - match arr { - Val::Arr(old) => { - // TODO: Mutate array, instead of recreating them - let mut new = Vec::new(); - new.extend(old.iter().cloned()); - new.push(val.clone()); - *arr = Val::Arr(Rc::new(new)); - } - _ => panic!("should receive array"), +pub extern "C" fn jsonnet_string_output(vm: &EvaluationState, v: c_int) { + match v { + 1 => vm.set_manifest_format(ManifestFormat::None), + 0 => vm.set_manifest_format(ManifestFormat::Json(4)), + _ => panic!("incorrect output format"), } -} - -#[no_mangle] -pub extern "C" fn jsonnet_json_make_object(_vm: &EvaluationState) -> *mut Val { - Box::into_raw(Box::new(Val::Obj(ObjValue::new_empty()))) } /// # Safety /// -/// This function is safe if passed name is ok -#[no_mangle] -pub unsafe extern "C" fn jsonnet_json_object_append( - _vm: &EvaluationState, - obj: &mut Val, - name: *const c_char, - val: &Val, -) { - match obj { - Val::Obj(old) => { - let mut new = HashMap::new(); - new.insert( - CStr::from_ptr(name).to_str().unwrap().into(), - ObjMember { - add: false, - visibility: Visibility::Normal, - invoke: LazyBinding::Bound(LazyVal::new_resolved(val.clone())), - location: None, - }, - ); - let new_obj = ObjValue::new(Some(old.clone()), Rc::new(new)); - *obj = Val::Obj(new_obj); - } - _ => panic!("should receive array"), - } -} - -/// # Safety -/// /// This function is most definitely broken, but it works somehow, see TODO inside #[no_mangle] pub unsafe extern "C" fn jsonnet_realloc( @@ -247,48 +93,14 @@ Box::from_raw(v); } -#[no_mangle] -pub extern "C" fn jsonnet_import_callback() { - todo!() -} #[no_mangle] pub extern "C" fn jsonnet_native_callback() { - todo!() -} -#[no_mangle] -pub extern "C" fn jsonnet_ext_var() { todo!() } -#[no_mangle] -pub extern "C" fn jsonnet_ext_code() { - todo!() -} -#[no_mangle] -pub extern "C" fn jsonnet_tla_var() { - todo!() -} -#[no_mangle] -pub extern "C" fn jsonnet_tla_code() { - todo!() -} -#[no_mangle] -pub extern "C" fn jsonnet_max_trace() { - todo!() -} -/// # Safety -/// -/// This function is safe, if received v is a pointer to normal C string #[no_mangle] -pub unsafe extern "C" fn jsonnet_jpath_add(vm: &EvaluationState, v: *const c_char) { - let cstr = CStr::from_ptr(v); - let path = PathBuf::from(cstr.to_str().unwrap()); - let any_resolver = &vm.settings().import_resolver; - let resolver = any_resolver - .as_any() - .downcast_ref::() - .unwrap(); - resolver.add_jpath(path); +pub extern "C" fn jsonnet_max_trace(vm: &EvaluationState, v: c_uint) { + vm.set_max_trace(v as usize) } /// # Safety @@ -301,20 +113,19 @@ error: &mut c_int, ) -> *const c_char { vm.run_in_state(|| { - use std::fmt::Write; let filename = CStr::from_ptr(filename); - match vm.evaluate_file_to_json(&PathBuf::from(filename.to_str().unwrap())) { + match vm + .evaluate_file_raw_nocwd(&PathBuf::from(filename.to_str().unwrap())) + .and_then(|v| vm.with_tla(v)) + .and_then(|v| vm.manifest(v)) + { Ok(v) => { *error = 0; CString::new(&*v as &str).unwrap().into_raw() } Err(e) => { *error = 1; - let mut out = String::new(); - writeln!(out, "{:?}", e.0).unwrap(); - for i in (e.1).0.iter() { - writeln!(out, "{:?}", i.0).unwrap(); - } + let out = vm.stringify_err(&e); CString::new(&out as &str).unwrap().into_raw() } } @@ -332,24 +143,23 @@ error: &mut c_int, ) -> *const c_char { vm.run_in_state(|| { - use std::fmt::Write; let filename = CStr::from_ptr(filename); let snippet = CStr::from_ptr(snippet); - match vm.evaluate_snippet_to_json( - &PathBuf::from(filename.to_str().unwrap()), - &snippet.to_str().unwrap(), - ) { + match vm + .evaluate_snippet_raw( + Rc::new(PathBuf::from(filename.to_str().unwrap())), + snippet.to_str().unwrap().into(), + ) + .and_then(|v| vm.with_tla(v)) + .and_then(|v| vm.manifest(v)) + { Ok(v) => { *error = 0; CString::new(&*v as &str).unwrap().into_raw() } Err(e) => { *error = 1; - let mut out = String::new(); - writeln!(out, "{:?}", e.0).unwrap(); - for i in (e.1).0.iter() { - writeln!(out, "{:?} ---- {}", i.0, i.1).unwrap(); - } + let out = vm.stringify_err(&e); CString::new(&out as &str).unwrap().into_raw() } } --- /dev/null +++ b/bindings/jsonnet/src/val_extract.rs @@ -0,0 +1,45 @@ +//! Extract values from VM + +use jrsonnet_evaluator::{EvaluationState, Val}; + +use std::{ + ffi::CString, + os::raw::{c_char, c_double, c_int}, +}; + +#[no_mangle] +pub extern "C" fn jsonnet_json_extract_string(_vm: &EvaluationState, v: &Val) -> *mut c_char { + match v.unwrap_if_lazy().unwrap() { + Val::Str(s) => CString::new(&*s as &str).unwrap().into_raw(), + _ => std::ptr::null_mut(), + } +} +#[no_mangle] +pub extern "C" fn jsonnet_json_extract_number( + _vm: &EvaluationState, + v: &Val, + out: &mut c_double, +) -> c_int { + match v.unwrap_if_lazy().unwrap() { + Val::Num(n) => { + *out = n; + 1 + } + _ => 0, + } +} +#[no_mangle] +pub extern "C" fn jsonnet_json_extract_bool(_vm: &EvaluationState, v: &Val) -> c_int { + match v.unwrap_if_lazy().unwrap() { + Val::Bool(false) => 0, + Val::Bool(true) => 1, + _ => 2, + } +} +#[no_mangle] +pub extern "C" fn jsonnet_json_extract_null(_vm: &EvaluationState, v: &Val) -> c_int { + match v.unwrap_if_lazy().unwrap() { + Val::Null => 1, + _ => 0, + } +} --- /dev/null +++ b/bindings/jsonnet/src/val_make.rs @@ -0,0 +1,47 @@ +//! Create values in VM + +use jrsonnet_evaluator::{EvaluationState, ObjValue, Val}; +use std::{ + ffi::CStr, + os::raw::{c_char, c_double, c_int}, + rc::Rc, +}; + +/// # Safety +/// +/// This function is safe, if received v is a pointer to normal C string +#[no_mangle] +pub unsafe extern "C" fn jsonnet_json_make_string( + _vm: &EvaluationState, + 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()))) +} + +#[no_mangle] +pub extern "C" fn jsonnet_json_make_number(_vm: &EvaluationState, v: c_double) -> *mut Val { + Box::into_raw(Box::new(Val::Num(v))) +} + +#[no_mangle] +pub extern "C" fn jsonnet_json_make_bool(_vm: &EvaluationState, v: c_int) -> *mut Val { + assert!(v == 0 || v == 1); + Box::into_raw(Box::new(Val::Bool(v == 1))) +} + +#[no_mangle] +pub extern "C" fn jsonnet_json_make_null(_vm: &EvaluationState) -> *mut Val { + Box::into_raw(Box::new(Val::Null)) +} + +#[no_mangle] +pub extern "C" fn jsonnet_json_make_array(_vm: &EvaluationState) -> *mut Val { + Box::into_raw(Box::new(Val::Arr(Rc::new(Vec::new())))) +} + +#[no_mangle] +pub extern "C" fn jsonnet_json_make_object(_vm: &EvaluationState) -> *mut Val { + Box::into_raw(Box::new(Val::Obj(ObjValue::new_empty()))) +} --- /dev/null +++ b/bindings/jsonnet/src/val_modify.rs @@ -0,0 +1,55 @@ +//! Modify VM values +//! Only tested with variables, which haven't altered by code before appearing here +//! In jrsonnet every value is immutable, and this code is probally broken + +use jrsonnet_evaluator::{EvaluationState, LazyBinding, LazyVal, ObjMember, ObjValue, Val}; +use jrsonnet_parser::Visibility; +use std::{collections::HashMap, ffi::CStr, os::raw::c_char, rc::Rc}; + +/// # Safety +/// +/// Received arr value should be correct pointer to array allocated by make_array +#[no_mangle] +pub unsafe extern "C" fn jsonnet_json_array_append( + _vm: &EvaluationState, + arr: *mut Val, + val: &Val, +) { + match *Box::from_raw(arr) { + Val::Arr(old) => { + let mut new = Rc::try_unwrap(old).expect("arr with no refs"); + new.push(val.clone()); + *arr = Val::Arr(Rc::new(new)); + } + _ => panic!("should receive array"), + } +} + +/// # Safety +/// +/// This function is safe if passed name is ok +#[no_mangle] +pub unsafe extern "C" fn jsonnet_json_object_append( + _vm: &EvaluationState, + obj: &mut Val, + name: *const c_char, + val: &Val, +) { + match obj { + Val::Obj(old) => { + let mut new = HashMap::new(); + new.insert( + CStr::from_ptr(name).to_str().unwrap().into(), + ObjMember { + add: false, + visibility: Visibility::Normal, + invoke: LazyBinding::Bound(LazyVal::new_resolved(val.clone())), + location: None, + }, + ); + let new_obj = ObjValue::new(Some(old.clone()), Rc::new(new)); + *obj = Val::Obj(new_obj); + } + _ => panic!("should receive object"), + } +} --- /dev/null +++ b/bindings/jsonnet/src/vars_tlas.rs @@ -0,0 +1,64 @@ +//! Manipulate external variables and top level arguments + +use jrsonnet_evaluator::EvaluationState; +use std::{ffi::CStr, os::raw::c_char}; + +/// # Safety +#[no_mangle] +pub unsafe extern "C" fn jsonnet_ext_var( + vm: &EvaluationState, + name: *const c_char, + value: *const c_char, +) { + let name = CStr::from_ptr(name); + let value = CStr::from_ptr(value); + vm.add_ext_str( + name.to_str().unwrap().into(), + value.to_str().unwrap().into(), + ) +} + +/// # Safety +#[no_mangle] +pub unsafe extern "C" fn jsonnet_ext_code( + vm: &EvaluationState, + name: *const c_char, + value: *const c_char, +) { + let name = CStr::from_ptr(name); + let value = CStr::from_ptr(value); + vm.add_ext_code( + name.to_str().unwrap().into(), + value.to_str().unwrap().into(), + ) + .unwrap() +} +/// # Safety +#[no_mangle] +pub unsafe extern "C" fn jsonnet_tla_var( + vm: &EvaluationState, + 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(), + ) +} +/// # Safety +#[no_mangle] +pub unsafe extern "C" fn jsonnet_tla_code( + vm: &EvaluationState, + name: *const c_char, + value: *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().into(), + ) + .unwrap() +} --- a/bindings/test.jsonnet +++ /dev/null @@ -1 +0,0 @@ -2 + 2 --- a/crates/jrsonnet-evaluator/src/error.rs +++ b/crates/jrsonnet-evaluator/src/error.rs @@ -56,10 +56,17 @@ StackOverflow, FractionalIndex, DivisionByZero, + + StringManifestOutputIsNotAString, + + ImportCallbackError(String), } #[derive(Clone, Debug)] -pub struct StackTraceElement(pub ExprLocation, pub String); +pub struct StackTraceElement { + pub location: ExprLocation, + pub desc: String, +} #[derive(Debug, Clone)] pub struct StackTrace(pub Vec);