use std::cell::{Cell, RefCell}; use std::collections::HashMap; use std::ffi::c_void; use std::path::{Path, PathBuf}; use std::rc::Rc; use std::sync::Arc; use avril_caps::{Capabilities, PathTarget}; use avril_core::serde_v8::ToJsBuffer; use avril_core::{Extension, Op, PublicModule, v8}; use futures::channel::oneshot; use libffi::middle::{Arg, Cif, CodePtr, Ret, Type}; #[cfg(not(all(target_pointer_width = "64", target_endian = "little")))] compile_error!("avril-ffi currently requires a 64-bit little-endian target"); // Resource IDs cross into JS as Numbers, so they must stay exact safe integers. const MAX_ID: u64 = 9_007_199_254_740_991; #[derive(Clone, Copy, PartialEq, Eq, Debug)] enum Kind { Void, Bool, I8, U8, I16, U16, I32, U32, I64, U64, Isize, Usize, F32, F64, Pointer, Buffer, Function, } impl Kind { fn parse(name: &str) -> Result { Ok(match name { "void" => Self::Void, "bool" => Self::Bool, "i8" => Self::I8, "u8" => Self::U8, "i16" => Self::I16, "u16" => Self::U16, "i32" => Self::I32, "u32" => Self::U32, "i64" => Self::I64, "u64" => Self::U64, "isize" => Self::Isize, "usize" => Self::Usize, "f32" => Self::F32, "f64" => Self::F64, "pointer" => Self::Pointer, "buffer" => Self::Buffer, "function" => Self::Function, _ => return Err(format!("Unknown native type: {name}")), }) } fn ffi_type(self) -> Type { match self { Self::Void => Type::void(), Self::Bool | Self::U8 => Type::u8(), Self::I8 => Type::i8(), Self::U16 => Type::u16(), Self::I16 => Type::i16(), Self::U32 => Type::u32(), Self::I32 => Type::i32(), Self::U64 => Type::u64(), Self::I64 => Type::i64(), Self::Usize => Type::usize(), Self::Isize => Type::isize(), Self::F32 => Type::f32(), Self::F64 => Type::f64(), Self::Pointer | Self::Buffer | Self::Function => Type::pointer(), } } } #[derive(Clone, Debug)] struct Def { params: Vec, result: Kind, } #[derive(serde::Deserialize)] struct DefWire { symbol: String, params: Vec, result: String, nonblocking: bool, } struct SymbolState { address: usize, def: Def, } struct LibraryState { _library: libloading::Library, symbols: Vec, } #[derive(Clone, Debug)] enum Value { Void, Bool(bool), I64(i64), U64(u64), F32(f32), F64(f64), Ptr(u64), } struct Completed { outcome: Result, buffers: Vec>, originals: Vec>, targets: Vec>, } struct PendingTargets { targets: Vec>, originals: Vec>, } type CallReceiver = oneshot::Receiver>), String>>; struct Registry { caps: Arc, libraries: RefCell>>, next_library: Cell, receivers: RefCell>, targets: RefCell>, completed: Rc>>, next_call: Cell, } enum CallError { Type(String), Error(String), } impl CallError { fn type_(message: impl Into) -> Self { Self::Type(message.into()) } fn error(message: impl Into) -> Self { Self::Error(message.into()) } } type CallResult = Result; fn throw(scope: &mut v8::PinScope, error: &CallError) -> Option { let (message, type_error) = match error { CallError::Type(message) => (message, true), CallError::Error(message) => (message, false), }; let message = v8::String::new(scope, message)?; let exception = if type_error { v8::Exception::type_error(scope, message) } else { v8::Exception::error(scope, message) }; scope.throw_exception(exception); None } fn bigint_u64(scope: &mut v8::PinScope, value: v8::Local) -> CallResult { if !value.is_big_int() { return Err(CallError::type_("Expected a bigint")); } let bigint = value.to_big_int(scope).unwrap(); let (value, lossless) = bigint.u64_value(); if lossless { return Ok(value); } Err(CallError::type_("Bigint is out of range")) } fn bigint_i64(scope: &mut v8::PinScope, value: v8::Local) -> CallResult { if !value.is_big_int() { return Err(CallError::type_("Expected a bigint")); } let bigint = value.to_big_int(scope).unwrap(); let (value, lossless) = bigint.i64_value(); if lossless { return Ok(value); } Err(CallError::type_("Bigint is out of range")) } fn number(scope: &mut v8::PinScope, value: v8::Local) -> CallResult { if !value.is_number() { return Err(CallError::type_("Expected a number")); } value .number_value(scope) .ok_or_else(|| CallError::type_("Expected a number")) } fn integer( scope: &mut v8::PinScope, value: v8::Local, min: f64, max: f64, ) -> CallResult { if value.is_big_int() { let value = bigint_i64(scope, value)?; if (value as f64) < min || (value as f64) > max { return Err(CallError::Type("Integer argument is out of range".into())); } return Ok(value); } let value = number(scope, value)?; if value.fract() != 0.0 || value < min || value > max { return Err(CallError::Type("Integer argument is out of range".into())); } Ok(value as i64) } fn view_bytes( scope: &mut v8::PinScope, value: v8::Local, ) -> CallResult<(*mut u8, usize)> { if let Ok(view) = v8::Local::::try_from(value) { return Ok((view.data() as *mut u8, view.byte_length())); } if let Ok(buffer) = v8::Local::::try_from(value) { let pointer = buffer .data() .map(|pointer| pointer.as_ptr() as *mut u8) .unwrap_or(std::ptr::null_mut()); return Ok((pointer, buffer.byte_length())); } let _ = scope; Err(CallError::type_( "Expected an ArrayBuffer or an ArrayBuffer view", )) } // One 8-byte slot per argument; libffi reads only the bytes of the slot's type. #[derive(Clone, Copy)] struct Slot(u64); impl Slot { fn from_i64(value: i64) -> Self { Self(value as u64) } fn from_u64(value: u64) -> Self { Self(value) } fn from_f32(value: f32) -> Self { Self(f32::to_bits(value) as u64) } fn from_f64(value: f64) -> Self { Self(f64::to_bits(value)) } fn from_ptr(value: *mut c_void) -> Self { Self(value as usize as u64) } } fn convert_arg( scope: &mut v8::PinScope, value: v8::Local, kind: Kind, nonblocking: bool, buffers: &mut Vec>, ) -> CallResult { Ok(match kind { Kind::Bool => { if !value.is_boolean() { return Err(CallError::type_("Expected a boolean")); } Slot::from_u64(value.boolean_value(scope) as u64) } Kind::I8 => Slot::from_i64(integer(scope, value, i8::MIN as f64, i8::MAX as f64)?), Kind::U8 => Slot::from_i64(integer(scope, value, 0.0, u8::MAX as f64)?), Kind::I16 => Slot::from_i64(integer(scope, value, i16::MIN as f64, i16::MAX as f64)?), Kind::U16 => Slot::from_i64(integer(scope, value, 0.0, u16::MAX as f64)?), Kind::I32 => Slot::from_i64(integer(scope, value, i32::MIN as f64, i32::MAX as f64)?), Kind::U32 => Slot::from_i64(integer(scope, value, 0.0, u32::MAX as f64)?), Kind::I64 | Kind::Isize => { if value.is_big_int() { Slot::from_i64(bigint_i64(scope, value)?) } else { Slot::from_i64(integer( scope, value, -9_007_199_254_740_992.0, 9_007_199_254_740_992.0, )?) } } Kind::U64 | Kind::Usize => { if value.is_big_int() { Slot::from_u64(bigint_u64(scope, value)?) } else { Slot::from_i64(integer(scope, value, 0.0, 9_007_199_254_740_992.0)?) } } Kind::F32 => Slot::from_f32(number(scope, value)? as f32), Kind::F64 => Slot::from_f64(number(scope, value)?), Kind::Pointer | Kind::Function => { if value.is_null_or_undefined() { Slot::from_ptr(std::ptr::null_mut()) } else if value.is_big_int() { Slot::from_u64(bigint_u64(scope, value)?) } else if let Ok((pointer, _)) = view_bytes(scope, value) { if nonblocking { return Err(CallError::type_( "A pointer into a view cannot be used in a nonblocking call; pass an UnsafePointer allocation", )); } Slot::from_ptr(pointer as *mut c_void) } else { return Err(CallError::type_("Expected a pointer")); } } Kind::Buffer => { let (pointer, length) = view_bytes(scope, value)?; if nonblocking { let copy = if length == 0 { Vec::new() } else { // SAFETY: pointer/length describe this view's live bytes; we copy them out now. unsafe { std::slice::from_raw_parts(pointer, length) }.to_vec() }; buffers.push(copy); let copy = buffers.last_mut().unwrap(); Slot::from_ptr(copy.as_mut_ptr() as *mut c_void) } else { Slot::from_ptr(pointer as *mut c_void) } } Kind::Void => return Err(CallError::type_("void is not a parameter type")), }) } fn call_native(address: usize, def: &Def, slots: &mut [Slot]) -> Value { let cif = Cif::new( def.params.iter().map(|kind| kind.ffi_type()), def.result.ffi_type(), ); let args: Vec = slots.iter().map(|slot| Arg::new(&slot.0)).collect(); let function = CodePtr::from_ptr(address as *const c_void); // SAFETY: the caller checked address against def when the library (or the // UnsafeFnPointer's creator) was opened; slot bytes match def's types. unsafe { match def.result { Kind::Void => { cif.call_return_into(function, &args, Ret::void()); Value::Void } Kind::Bool => Value::Bool(cif.call::(function, &args) != 0), Kind::I8 => Value::I64(cif.call::(function, &args) as i64), Kind::U8 => Value::U64(cif.call::(function, &args) as u64), Kind::I16 => Value::I64(cif.call::(function, &args) as i64), Kind::U16 => Value::U64(cif.call::(function, &args) as u64), Kind::I32 => Value::I64(cif.call::(function, &args) as i64), Kind::U32 => Value::U64(cif.call::(function, &args) as u64), Kind::I64 | Kind::Isize => Value::I64(cif.call::(function, &args)), Kind::U64 | Kind::Usize => Value::U64(cif.call::(function, &args)), Kind::F32 => Value::F32(cif.call::(function, &args)), Kind::F64 => Value::F64(cif.call::(function, &args)), Kind::Pointer | Kind::Function => { Value::Ptr(cif.call::<*mut c_void>(function, &args) as usize as u64) } Kind::Buffer => Value::Ptr(cif.call::<*mut c_void>(function, &args) as usize as u64), } } } fn value_to_v8<'s>( scope: &mut v8::PinScope<'s, '_>, value: &Value, result: Kind, ) -> Option> { Some(match (value, result) { (Value::Void, _) => v8::undefined(scope).into(), (Value::Bool(value), _) => v8::Boolean::new(scope, *value).into(), (Value::I64(value), Kind::I64 | Kind::Isize) => { v8::BigInt::new_from_i64(scope, *value).into() } (Value::U64(value), Kind::U64 | Kind::Usize) => { v8::BigInt::new_from_u64(scope, *value).into() } (Value::I64(value), _) => v8::Number::new(scope, *value as f64).into(), (Value::U64(value), _) => v8::Number::new(scope, *value as f64).into(), (Value::F32(value), _) => v8::Number::new(scope, *value as f64).into(), (Value::F64(value), _) => v8::Number::new(scope, *value).into(), (Value::Ptr(value), _) => v8::BigInt::new_from_u64(scope, *value).into(), }) } fn parse_def(wire: DefWire) -> Result<(String, Def), String> { let params = wire .params .iter() .map(|name| Kind::parse(name)) .collect::, _>>()?; if params.contains(&Kind::Void) { return Err("void is not a parameter type".into()); } let result = Kind::parse(&wire.result)?; if result == Kind::Buffer { return Err("buffer is not a result type".into()); } let _ = wire.nonblocking; Ok((wire.symbol, Def { params, result })) } fn resolve_library(caps: &Capabilities, specifier: &str) -> Result { // A bare name goes through the loader's search path; only unrestricted // ffi authority covers names whose resolution we cannot check beforehand. let bare = !specifier.contains(['/', '\\']) && Path::new(specifier).components().count() == 1; if let Ok(url) = url::Url::parse(specifier) && url.scheme() == "file" { let path = url .to_file_path() .map_err(|_| CallError::error("Invalid file URL"))?; return load_checked(caps, &path); } if bare && !specifier.starts_with('.') { if !caps.unrestricted_ffi() { return Err(CallError::error(format!( "PermissionDenied: ffi {specifier}" ))); } // SAFETY: loading a library runs its initializers; unrestricted ffi // authority is exactly permission to run native code in-process. return unsafe { libloading::Library::new(specifier) } .map_err(|error| CallError::error(error.to_string())); } let path = if Path::new(specifier).is_absolute() { PathBuf::from(specifier) } else { caps.cwd().join(specifier) }; load_checked(caps, &path) } fn load_checked(caps: &Capabilities, path: &Path) -> Result { let target = PathTarget::for_executable(path).map_err(|error| CallError::error(error.to_string()))?; caps.check_ffi(&target) .map_err(|error| CallError::error(error.to_string()))?; // SAFETY: see resolve_library. unsafe { libloading::Library::new(target.as_path()) } .map_err(|error| CallError::error(error.to_string())) } fn id(scope: &mut v8::PinScope, value: v8::Local) -> CallResult where T: TryFrom, { let number = number(scope, value).map_err(|_| CallError::type_("Expected an id"))?; T::try_from(number as u64).map_err(|_| CallError::type_("Id is out of range")) } fn snapshot_args<'s>( scope: &mut v8::PinScope<'s, '_>, values: &v8::Local<'s, v8::Array>, ) -> CallResult>> { (0..values.length()) .map(|index| values.get_index(scope, index)) .collect::>>() .ok_or_else(|| CallError::type_("Could not read the arguments array")) } fn authorize_any(registry: &Registry) -> CallResult<()> { registry .caps .check_ffi_any() .map_err(|error| CallError::error(error.to_string())) } pub fn init(caps: Arc) -> Extension { let registry = Rc::new(Registry { caps, libraries: RefCell::new(HashMap::new()), next_library: Cell::new(1), receivers: RefCell::new(HashMap::new()), targets: RefCell::new(HashMap::new()), completed: Rc::new(RefCell::new(HashMap::new())), next_call: Cell::new(1), }); let dlopen_registry = registry.clone(); let close_registry = registry.clone(); let call_registry = registry.clone(); let begin_registry = registry.clone(); let await_registry = registry.clone(); let finish_registry = registry.clone(); let ptr_registry = registry.clone(); let address_registry = registry.clone(); let read_registry = registry.clone(); let write_registry = registry.clone(); let bytes_registry = registry.clone(); let write_bytes_registry = registry.clone(); let cstring_registry = registry.clone(); Extension { name: "ext:ffi", dependencies: &[], ops: vec![ Op::raw("op_ffi_dlopen", move |scope, args| { let specifier = args.get(0).to_rust_string_lossy(scope); let wires: Vec = match avril_core::serde_v8::from_v8(scope, args.get(1)) { Ok(wires) => wires, Err(error) => { return throw(scope, &CallError::type_(error.to_string())); } }; let library = match resolve_library(&dlopen_registry.caps, &specifier) { Ok(library) => library, Err(error) => return throw(scope, &error), }; let mut symbols = Vec::new(); for wire in wires { let (name, def) = match parse_def(wire) { Ok(parsed) => parsed, Err(error) => return throw(scope, &CallError::type_(error)), }; // SAFETY: we only record the symbol's address; it is // invoked later through libffi with def's signature. let address = unsafe { match library.get::<*const c_void>(name.as_bytes()) { Ok(symbol) => match symbol.try_as_raw_ptr() { Some(address) if !address.is_null() => address as usize, Some(_) => { return throw( scope, &CallError::error("Symbol resolved to a null address"), ); } None => { return throw( scope, &CallError::error("Could not resolve symbol"), ); } }, Err(error) => { return throw(scope, &CallError::error(error.to_string())); } } }; symbols.push(SymbolState { address, def }); } let library_id = dlopen_registry.next_library.get(); if library_id > MAX_ID { return throw(scope, &CallError::error("FFI library IDs exhausted")); } dlopen_registry.next_library.set(library_id + 1); dlopen_registry.libraries.borrow_mut().insert( library_id, Arc::new(LibraryState { _library: library, symbols }), ); Some(v8::Number::new(scope, library_id as f64).into()) }), Op::raw("op_ffi_close", move |scope, args| { if let Ok(library_id) = id::(scope, args.get(0)) { close_registry.libraries.borrow_mut().remove(&library_id); } Some(v8::undefined(scope).into()) }), Op::raw("op_ffi_call", move |scope, args| { let result = (|| -> CallResult> { let library_id = id::(scope, args.get(0))?; let index = id::(scope, args.get(1))?; let library = call_registry .libraries .borrow() .get(&library_id) .cloned() .ok_or_else(|| CallError::error("Library is closed"))?; let symbol = library .symbols .get(index) .ok_or_else(|| CallError::error("Unknown symbol"))?; let values = v8::Local::::try_from(args.get(2)) .map_err(|_| CallError::type_("Expected an arguments array"))?; if values.length() as usize != symbol.def.params.len() { return Err(CallError::type_("Wrong number of arguments")); } // Read every element before converting any: an accessor // could run JS, and no view address may be captured yet. let elements = snapshot_args(scope, &values)?; let mut buffers = Vec::new(); let mut slots = Vec::new(); for (value, kind) in elements.iter().zip(&symbol.def.params) { slots.push(convert_arg(scope, *value, *kind, false, &mut buffers)?); } let value = call_native(symbol.address, &symbol.def, &mut slots); value_to_v8(scope, &value, symbol.def.result) .ok_or_else(|| CallError::error("Could not convert the result")) })(); match result { Ok(value) => Some(value), Err(error) => throw(scope, &error), } }), Op::raw("op_ffi_call_ptr", move |scope, args| { let result = (|| -> CallResult> { authorize_any(&ptr_registry)?; let address = bigint_u64(scope, args.get(0))? as usize; let wire: DefWire = avril_core::serde_v8::from_v8(scope, args.get(1)) .map_err(|error| CallError::type_(error.to_string()))?; let (_, def) = parse_def(wire).map_err(CallError::type_)?; let values = v8::Local::::try_from(args.get(2)) .map_err(|_| CallError::type_("Expected an arguments array"))?; if values.length() as usize != def.params.len() { return Err(CallError::type_("Wrong number of arguments")); } let elements = snapshot_args(scope, &values)?; let mut buffers = Vec::new(); let mut slots = Vec::new(); for (value, kind) in elements.iter().zip(&def.params) { slots.push(convert_arg(scope, *value, *kind, false, &mut buffers)?); } let value = call_native(address, &def, &mut slots); value_to_v8(scope, &value, def.result) .ok_or_else(|| CallError::error("Could not convert the result")) })(); match result { Ok(value) => Some(value), Err(error) => throw(scope, &error), } }), Op::raw("op_ffi_call_async_begin", move |scope, args| { let result = (|| -> CallResult { let library_id = id::(scope, args.get(0))?; let index = id::(scope, args.get(1))?; let library = begin_registry .libraries .borrow() .get(&library_id) .cloned() .ok_or_else(|| CallError::error("Library is closed"))?; let symbol = library .symbols .get(index) .ok_or_else(|| CallError::error("Unknown symbol"))?; let values = v8::Local::::try_from(args.get(2)) .map_err(|_| CallError::type_("Expected an arguments array"))?; if values.length() as usize != symbol.def.params.len() { return Err(CallError::type_("Wrong number of arguments")); } let elements = snapshot_args(scope, &values)?; let mut buffers: Vec> = Vec::new(); let mut targets = Vec::new(); let mut originals = Vec::new(); let mut slots = Vec::new(); for (value, kind) in elements.iter().zip(&symbol.def.params) { if *kind == Kind::Buffer { targets.push(v8::Global::new(scope, *value)); } slots.push(convert_arg(scope, *value, *kind, true, &mut buffers)?); if *kind == Kind::Buffer { originals.push(buffers.last().unwrap().clone()); } } let call_id = begin_registry.next_call.get(); if call_id > MAX_ID { return Err(CallError::error("FFI call IDs exhausted")); } begin_registry.next_call.set(call_id + 1); let (sender, receiver) = oneshot::channel(); begin_registry .receivers .borrow_mut() .insert(call_id, receiver); begin_registry .targets .borrow_mut() .insert(call_id, PendingTargets { targets, originals }); let address = symbol.address; let def = symbol.def.clone(); let library = library.clone(); let spawned = std::thread::Builder::new() .name("avril:ffi".into()) .spawn(move || { // Keeps the library loaded for the call's duration. let _library = &library; let mut slots = slots; let outcome = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| { (call_native(address, &def, &mut slots), buffers) })); let result = match outcome { Ok((value, buffers)) => Ok((value, buffers)), Err(_) => Err("Native call panicked".to_string()), }; let _ = sender.send(result); }); if let Err(error) = spawned { begin_registry.receivers.borrow_mut().remove(&call_id); begin_registry.targets.borrow_mut().remove(&call_id); return Err(CallError::error(error.to_string())); } Ok(call_id) })(); match result { Ok(call_id) => Some(v8::Number::new(scope, call_id as f64).into()), Err(error) => throw(scope, &error), } }), Op::asynchronous("op_ffi_await", move |call_id: u64| { let receiver = await_registry.receivers.borrow_mut().remove(&call_id); let pending = await_registry.targets.borrow_mut().remove(&call_id); let (targets, originals) = match pending { Some(pending) => (pending.targets, pending.originals), None => (Vec::new(), Vec::new()), }; let completed = await_registry.completed.clone(); async move { let outcome = match receiver { Some(receiver) => match receiver.await { Ok(Ok((value, buffers))) => Ok((value, buffers)), Ok(Err(error)) => Err(error), Err(_) => Err("Native call was dropped".to_string()), }, None => Err("Unknown call".to_string()), }; let (outcome, buffers) = match outcome { Ok((value, buffers)) => (Ok(value), buffers), Err(error) => (Err(error), Vec::new()), }; completed.borrow_mut().insert( call_id, Completed { outcome, buffers, originals, targets, }, ); Ok::<_, std::io::Error>(call_id) } }), Op::raw("op_ffi_call_async_finish", move |scope, args| { let result = (|| -> CallResult> { let call_id = id::(scope, args.get(0))?; let result_kind = Kind::parse(&args.get(1).to_rust_string_lossy(scope)) .map_err(CallError::type_)?; let completed = finish_registry .completed .borrow_mut() .remove(&call_id) .ok_or_else(|| CallError::error("Call is not complete"))?; for ((target, bytes), original) in completed .targets .iter() .zip(&completed.buffers) .zip(&completed.originals) { let target = v8::Local::new(scope, target); let (pointer, length) = view_bytes(scope, target)?; if length != bytes.len() { return Err(CallError::error( "Buffer changed size during a nonblocking call", )); } // Only bytes the native call changed are written // back, so JS writes to other bytes during the call // are not clobbered. SAFETY: pointer/length describe // the target view, kept alive via this Global. for (index, (byte, before)) in bytes.iter().zip(original).enumerate() { if byte != before { unsafe { *pointer.add(index) = *byte }; } } } let value = completed.outcome.map_err(CallError::error)?; value_to_v8(scope, &value, result_kind) .ok_or_else(|| CallError::error("Could not convert the result")) })(); match result { Ok(value) => Some(value), Err(error) => throw(scope, &error), } }), Op::raw("op_ffi_address_of", move |scope, args| { if let Err(error) = authorize_any(&address_registry) { return throw(scope, &error); } match view_bytes(scope, args.get(0)) { Ok((pointer, _)) => { Some(v8::BigInt::new_from_u64(scope, pointer as usize as u64).into()) } Err(error) => throw(scope, &error), } }), Op::raw("op_ffi_read", move |scope, args| { let result = (|| -> CallResult> { authorize_any(&read_registry)?; let address = bigint_u64(scope, args.get(0))? as usize; let kind = Kind::parse(&args.get(1).to_rust_string_lossy(scope)) .map_err(CallError::type_)?; let pointer = address as *const u8; // SAFETY: the caller asserts address is readable for kind; // that is the documented unsafety of pointer reads. let value = unsafe { match kind { Kind::Bool => Value::Bool(*pointer != 0), Kind::I8 => Value::I64(pointer.cast::().read_unaligned() as i64), Kind::U8 => Value::U64(*pointer as u64), Kind::I16 => Value::I64(pointer.cast::().read_unaligned() as i64), Kind::U16 => Value::U64(pointer.cast::().read_unaligned() as u64), Kind::I32 => Value::I64(pointer.cast::().read_unaligned() as i64), Kind::U32 => Value::U64(pointer.cast::().read_unaligned() as u64), Kind::I64 | Kind::Isize => { Value::I64(pointer.cast::().read_unaligned()) } Kind::U64 | Kind::Usize => { Value::U64(pointer.cast::().read_unaligned()) } Kind::F32 => Value::F32(pointer.cast::().read_unaligned()), Kind::F64 => Value::F64(pointer.cast::().read_unaligned()), Kind::Pointer | Kind::Function => { Value::Ptr(pointer.cast::().read_unaligned() as u64) } _ => { return Err(CallError::type_( "Type cannot be read through a pointer", )); } } }; value_to_v8(scope, &value, kind) .ok_or_else(|| CallError::error("Could not convert the result")) })(); match result { Ok(value) => Some(value), Err(error) => throw(scope, &error), } }), Op::raw("op_ffi_write", move |scope, args| { let result = (|| -> CallResult> { authorize_any(&write_registry)?; let address = bigint_u64(scope, args.get(0))? as usize; let kind = Kind::parse(&args.get(1).to_rust_string_lossy(scope)) .map_err(CallError::type_)?; let mut buffers = Vec::new(); let slot = convert_arg(scope, args.get(2), kind, false, &mut buffers)?; let size = match kind { Kind::Bool | Kind::I8 | Kind::U8 => 1, Kind::I16 | Kind::U16 => 2, Kind::I32 | Kind::U32 | Kind::F32 => 4, Kind::I64 | Kind::U64 | Kind::Isize | Kind::Usize | Kind::F64 | Kind::Pointer | Kind::Function => 8, _ => { return Err(CallError::type_( "Type cannot be written through a pointer", )); } }; // SAFETY: as op_ffi_read, with the caller's writable assert. unsafe { std::ptr::copy_nonoverlapping( (&slot.0 as *const u64).cast::(), address as *mut u8, size, ); } Ok(v8::undefined(scope).into()) })(); match result { Ok(value) => Some(value), Err(error) => throw(scope, &error), } }), Op::raw("op_ffi_read_bytes", move |scope, args| { let result = (|| -> CallResult> { authorize_any(&bytes_registry)?; let address = bigint_u64(scope, args.get(0))? as usize; let length = id::(scope, args.get(1))?; let bytes = if length == 0 { Vec::new() } else { // SAFETY: the caller asserts address..address+length is readable. unsafe { std::slice::from_raw_parts(address as *const u8, length) }.to_vec() }; avril_core::serde_v8::to_v8(scope, ToJsBuffer::from(bytes)) .map_err(|error| CallError::error(error.to_string())) })(); match result { Ok(value) => Some(value), Err(error) => throw(scope, &error), } }), Op::raw("op_ffi_write_bytes", move |scope, args| { let result = (|| -> CallResult> { authorize_any(&write_bytes_registry)?; let address = bigint_u64(scope, args.get(0))? as usize; let (pointer, length) = view_bytes(scope, args.get(1))?; // SAFETY: source is a live view; the caller asserts the // destination is writable for length bytes. Ranges may // overlap, hence ptr::copy. if length > 0 { unsafe { std::ptr::copy(pointer, address as *mut u8, length) }; } Ok(v8::undefined(scope).into()) })(); match result { Ok(value) => Some(value), Err(error) => throw(scope, &error), } }), Op::raw("op_ffi_read_cstring", move |scope, args| { let result = (|| -> CallResult> { authorize_any(&cstring_registry)?; let address = bigint_u64(scope, args.get(0))? as usize; let max = id::(scope, args.get(1))?; let mut bytes = Vec::new(); for offset in 0..max { // SAFETY: the caller asserts a NUL-terminated string // at address; we stop at max bytes regardless. let byte = unsafe { *(address as *const u8).add(offset) }; if byte == 0 { break; } bytes.push(byte); } avril_core::serde_v8::to_v8(scope, ToJsBuffer::from(bytes)) .map_err(|error| CallError::error(error.to_string())) })(); match result { Ok(value) => Some(value), Err(error) => throw(scope, &error), } }), ], modules: const { &[PUBLIC_MODULES[0].module] }, entry_point: Some("ext:ffi/main.js"), } } pub const PUBLIC_MODULES: &[PublicModule] = &[avril_core::public_module!("avril:ffi", "ext:ffi", "main")];