From a2db70a8d0820722695e9094c8dbc888bde1ffa3 Mon Sep 17 00:00:00 2001 From: Divy Srivastava Date: Mon, 12 Dec 2022 06:14:20 -0800 Subject: refactor(ext/ffi): split into multiple parts (#16950) - [x] `dlfcn.rs` - `dlopen()`-related code. - [x] `turbocall.rs` - Call trampoline JIT compiler. - [x] `repr.rs` - Pointer representation. Home of the UnsafePointerView ops. - [x] `symbol.rs` - Function symbol related code. - [x] `callback.rs` - Home of `Deno.UnsafeCallback` ops. - [x] `ir.rs` - Intermediate representation for values. Home of the `NativeValue` type. - [x] `call.rs` - Generic call ops. Home to everything related to calling FFI symbols. - [x] `static.rs` - static symbol support I find easier to work with this setup, I eventually want to expand TurboCall to unroll type conversion loop in generic calls, generate code for individual symbols (lazy function pointers), etc. --- ext/ffi/call.rs | 335 +++++++ ext/ffi/callback.rs | 562 ++++++++++++ ext/ffi/dlfcn.rs | 398 +++++++++ ext/ffi/fast_call.rs | 2065 ------------------------------------------ ext/ffi/ir.rs | 527 +++++++++++ ext/ffi/lib.rs | 2429 +------------------------------------------------- ext/ffi/repr.rs | 454 ++++++++++ ext/ffi/static.rs | 146 +++ ext/ffi/symbol.rs | 63 ++ ext/ffi/turbocall.rs | 2065 ++++++++++++++++++++++++++++++++++++++++++ 10 files changed, 4578 insertions(+), 4466 deletions(-) create mode 100644 ext/ffi/call.rs create mode 100644 ext/ffi/callback.rs create mode 100644 ext/ffi/dlfcn.rs delete mode 100644 ext/ffi/fast_call.rs create mode 100644 ext/ffi/ir.rs create mode 100644 ext/ffi/repr.rs create mode 100644 ext/ffi/static.rs create mode 100644 ext/ffi/symbol.rs create mode 100644 ext/ffi/turbocall.rs diff --git a/ext/ffi/call.rs b/ext/ffi/call.rs new file mode 100644 index 000000000..4a913c0e0 --- /dev/null +++ b/ext/ffi/call.rs @@ -0,0 +1,335 @@ +// Copyright 2018-2022 the Deno authors. All rights reserved. MIT license. + +use crate::callback::PtrSymbol; +use crate::check_unstable2; +use crate::dlfcn::DynamicLibraryResource; +use crate::ir::*; +use crate::symbol::NativeType; +use crate::symbol::Symbol; +use crate::FfiPermissions; +use crate::ForeignFunction; +use deno_core::anyhow::anyhow; +use deno_core::error::type_error; +use deno_core::error::AnyError; +use deno_core::op; +use deno_core::serde_json::Value; +use deno_core::serde_v8; +use deno_core::v8; +use deno_core::ResourceId; +use libffi::middle::Arg; +use std::cell::RefCell; +use std::ffi::c_void; +use std::future::Future; +use std::rc::Rc; + +// A one-off synchronous FFI call. +pub(crate) fn ffi_call_sync<'scope>( + scope: &mut v8::HandleScope<'scope>, + args: v8::FunctionCallbackArguments, + symbol: &Symbol, +) -> Result +where + 'scope: 'scope, +{ + let Symbol { + parameter_types, + result_type, + cif, + ptr: fun_ptr, + .. + } = symbol; + let mut ffi_args: Vec = + Vec::with_capacity(parameter_types.len()); + + for (index, native_type) in parameter_types.iter().enumerate() { + let value = args.get(index as i32); + match native_type { + NativeType::Bool => { + ffi_args.push(ffi_parse_bool_arg(value)?); + } + NativeType::U8 => { + ffi_args.push(ffi_parse_u8_arg(value)?); + } + NativeType::I8 => { + ffi_args.push(ffi_parse_i8_arg(value)?); + } + NativeType::U16 => { + ffi_args.push(ffi_parse_u16_arg(value)?); + } + NativeType::I16 => { + ffi_args.push(ffi_parse_i16_arg(value)?); + } + NativeType::U32 => { + ffi_args.push(ffi_parse_u32_arg(value)?); + } + NativeType::I32 => { + ffi_args.push(ffi_parse_i32_arg(value)?); + } + NativeType::U64 => { + ffi_args.push(ffi_parse_u64_arg(scope, value)?); + } + NativeType::I64 => { + ffi_args.push(ffi_parse_i64_arg(scope, value)?); + } + NativeType::USize => { + ffi_args.push(ffi_parse_usize_arg(scope, value)?); + } + NativeType::ISize => { + ffi_args.push(ffi_parse_isize_arg(scope, value)?); + } + NativeType::F32 => { + ffi_args.push(ffi_parse_f32_arg(value)?); + } + NativeType::F64 => { + ffi_args.push(ffi_parse_f64_arg(value)?); + } + NativeType::Buffer => { + ffi_args.push(ffi_parse_buffer_arg(scope, value)?); + } + NativeType::Pointer => { + ffi_args.push(ffi_parse_pointer_arg(scope, value)?); + } + NativeType::Function => { + ffi_args.push(ffi_parse_function_arg(scope, value)?); + } + NativeType::Void => { + unreachable!(); + } + } + } + let call_args: Vec = ffi_args.iter().map(Arg::new).collect(); + // SAFETY: types in the `Cif` match the actual calling convention and + // types of symbol. + unsafe { + Ok(match result_type { + NativeType::Void => NativeValue { + void_value: cif.call::<()>(*fun_ptr, &call_args), + }, + NativeType::Bool => NativeValue { + bool_value: cif.call::(*fun_ptr, &call_args), + }, + NativeType::U8 => NativeValue { + u8_value: cif.call::(*fun_ptr, &call_args), + }, + NativeType::I8 => NativeValue { + i8_value: cif.call::(*fun_ptr, &call_args), + }, + NativeType::U16 => NativeValue { + u16_value: cif.call::(*fun_ptr, &call_args), + }, + NativeType::I16 => NativeValue { + i16_value: cif.call::(*fun_ptr, &call_args), + }, + NativeType::U32 => NativeValue { + u32_value: cif.call::(*fun_ptr, &call_args), + }, + NativeType::I32 => NativeValue { + i32_value: cif.call::(*fun_ptr, &call_args), + }, + NativeType::U64 => NativeValue { + u64_value: cif.call::(*fun_ptr, &call_args), + }, + NativeType::I64 => NativeValue { + i64_value: cif.call::(*fun_ptr, &call_args), + }, + NativeType::USize => NativeValue { + usize_value: cif.call::(*fun_ptr, &call_args), + }, + NativeType::ISize => NativeValue { + isize_value: cif.call::(*fun_ptr, &call_args), + }, + NativeType::F32 => NativeValue { + f32_value: cif.call::(*fun_ptr, &call_args), + }, + NativeType::F64 => NativeValue { + f64_value: cif.call::(*fun_ptr, &call_args), + }, + NativeType::Pointer | NativeType::Function | NativeType::Buffer => { + NativeValue { + pointer: cif.call::<*mut c_void>(*fun_ptr, &call_args), + } + } + }) + } +} + +fn ffi_call( + call_args: Vec, + cif: &libffi::middle::Cif, + fun_ptr: libffi::middle::CodePtr, + parameter_types: &[NativeType], + result_type: NativeType, +) -> Result { + let call_args: Vec = call_args + .iter() + .enumerate() + .map(|(index, ffi_arg)| { + // SAFETY: the union field is initialized + unsafe { ffi_arg.as_arg(*parameter_types.get(index).unwrap()) } + }) + .collect(); + + // SAFETY: types in the `Cif` match the actual calling convention and + // types of symbol. + unsafe { + Ok(match result_type { + NativeType::Void => NativeValue { + void_value: cif.call::<()>(fun_ptr, &call_args), + }, + NativeType::Bool => NativeValue { + bool_value: cif.call::(fun_ptr, &call_args), + }, + NativeType::U8 => NativeValue { + u8_value: cif.call::(fun_ptr, &call_args), + }, + NativeType::I8 => NativeValue { + i8_value: cif.call::(fun_ptr, &call_args), + }, + NativeType::U16 => NativeValue { + u16_value: cif.call::(fun_ptr, &call_args), + }, + NativeType::I16 => NativeValue { + i16_value: cif.call::(fun_ptr, &call_args), + }, + NativeType::U32 => NativeValue { + u32_value: cif.call::(fun_ptr, &call_args), + }, + NativeType::I32 => NativeValue { + i32_value: cif.call::(fun_ptr, &call_args), + }, + NativeType::U64 => NativeValue { + u64_value: cif.call::(fun_ptr, &call_args), + }, + NativeType::I64 => NativeValue { + i64_value: cif.call::(fun_ptr, &call_args), + }, + NativeType::USize => NativeValue { + usize_value: cif.call::(fun_ptr, &call_args), + }, + NativeType::ISize => NativeValue { + isize_value: cif.call::(fun_ptr, &call_args), + }, + NativeType::F32 => NativeValue { + f32_value: cif.call::(fun_ptr, &call_args), + }, + NativeType::F64 => NativeValue { + f64_value: cif.call::(fun_ptr, &call_args), + }, + NativeType::Pointer | NativeType::Function | NativeType::Buffer => { + NativeValue { + pointer: cif.call::<*mut c_void>(fun_ptr, &call_args), + } + } + }) + } +} + +#[op(v8)] +pub fn op_ffi_call_ptr_nonblocking<'scope, FP>( + scope: &mut v8::HandleScope<'scope>, + state: Rc>, + pointer: usize, + def: ForeignFunction, + parameters: serde_v8::Value<'scope>, +) -> Result>, AnyError> +where + FP: FfiPermissions + 'static, +{ + check_unstable2(&state, "Deno.UnsafeFnPointer#call"); + { + let mut state = state.borrow_mut(); + let permissions = state.borrow_mut::(); + permissions.check(None)?; + }; + + let symbol = PtrSymbol::new(pointer, &def); + let call_args = ffi_parse_args(scope, parameters, &def.parameters)?; + + let join_handle = tokio::task::spawn_blocking(move || { + let PtrSymbol { cif, ptr } = symbol.clone(); + ffi_call(call_args, &cif, ptr, &def.parameters, def.result) + }); + + Ok(async move { + let result = join_handle + .await + .map_err(|err| anyhow!("Nonblocking FFI call failed: {}", err))??; + // SAFETY: Same return type declared to libffi; trust user to have it right beyond that. + Ok(unsafe { result.to_value(def.result) }) + }) +} + +/// A non-blocking FFI call. +#[op(v8)] +pub fn op_ffi_call_nonblocking<'scope>( + scope: &mut v8::HandleScope<'scope>, + state: Rc>, + rid: ResourceId, + symbol: String, + parameters: serde_v8::Value<'scope>, +) -> Result> + 'static, AnyError> { + let symbol = { + let state = state.borrow(); + let resource = state.resource_table.get::(rid)?; + let symbols = &resource.symbols; + *symbols + .get(&symbol) + .ok_or_else(|| type_error("Invalid FFI symbol name"))? + .clone() + }; + + let call_args = ffi_parse_args(scope, parameters, &symbol.parameter_types)?; + + let result_type = symbol.result_type; + let join_handle = tokio::task::spawn_blocking(move || { + let Symbol { + cif, + ptr, + parameter_types, + result_type, + .. + } = symbol.clone(); + ffi_call(call_args, &cif, ptr, ¶meter_types, result_type) + }); + + Ok(async move { + let result = join_handle + .await + .map_err(|err| anyhow!("Nonblocking FFI call failed: {}", err))??; + // SAFETY: Same return type declared to libffi; trust user to have it right beyond that. + Ok(unsafe { result.to_value(result_type) }) + }) +} + +#[op(v8)] +pub fn op_ffi_call_ptr( + scope: &mut v8::HandleScope<'scope>, + state: Rc>, + pointer: usize, + def: ForeignFunction, + parameters: serde_v8::Value<'scope>, +) -> Result, AnyError> +where + FP: FfiPermissions + 'static, +{ + check_unstable2(&state, "Deno.UnsafeFnPointer#call"); + { + let mut state = state.borrow_mut(); + let permissions = state.borrow_mut::(); + permissions.check(None)?; + }; + + let symbol = PtrSymbol::new(pointer, &def); + let call_args = ffi_parse_args(scope, parameters, &def.parameters)?; + + let result = ffi_call( + call_args, + &symbol.cif, + symbol.ptr, + &def.parameters, + def.result, + )?; + // SAFETY: Same return type declared to libffi; trust user to have it right beyond that. + let result = unsafe { result.to_v8(scope, def.result) }; + Ok(result) +} diff --git a/ext/ffi/callback.rs b/ext/ffi/callback.rs new file mode 100644 index 000000000..9b759a30e --- /dev/null +++ b/ext/ffi/callback.rs @@ -0,0 +1,562 @@ +// Copyright 2018-2022 the Deno authors. All rights reserved. MIT license. + +use crate::check_unstable; +use crate::symbol::NativeType; +use crate::FfiPermissions; +use crate::FfiState; +use crate::ForeignFunction; +use crate::PendingFfiAsyncWork; +use crate::LOCAL_ISOLATE_POINTER; +use crate::MAX_SAFE_INTEGER; +use crate::MIN_SAFE_INTEGER; +use deno_core::error::AnyError; +use deno_core::futures::channel::mpsc; +use deno_core::op; +use deno_core::serde_v8; +use deno_core::v8; +use deno_core::CancelFuture; +use deno_core::CancelHandle; +use deno_core::OpState; +use deno_core::Resource; +use deno_core::ResourceId; +use libffi::middle::Cif; +use serde::Deserialize; +use std::borrow::Cow; +use std::cell::RefCell; +use std::ffi::c_void; +use std::future::Future; +use std::future::IntoFuture; +use std::pin::Pin; +use std::ptr; +use std::ptr::NonNull; +use std::rc::Rc; +use std::sync::mpsc::sync_channel; +use std::task::Poll; +use std::task::Waker; +#[derive(Clone)] +pub struct PtrSymbol { + pub cif: libffi::middle::Cif, + pub ptr: libffi::middle::CodePtr, +} + +impl PtrSymbol { + pub fn new(fn_ptr: usize, def: &ForeignFunction) -> Self { + let ptr = libffi::middle::CodePtr::from_ptr(fn_ptr as _); + let cif = libffi::middle::Cif::new( + def + .parameters + .clone() + .into_iter() + .map(libffi::middle::Type::from), + def.result.into(), + ); + + Self { cif, ptr } + } +} + +#[allow(clippy::non_send_fields_in_send_ty)] +// SAFETY: unsafe trait must have unsafe implementation +unsafe impl Send for PtrSymbol {} +// SAFETY: unsafe trait must have unsafe implementation +unsafe impl Sync for PtrSymbol {} + +struct UnsafeCallbackResource { + cancel: Rc, + // Closure is never directly touched, but it keeps the C callback alive + // until `close()` method is called. + #[allow(dead_code)] + closure: libffi::middle::Closure<'static>, + info: *mut CallbackInfo, +} + +impl Resource for UnsafeCallbackResource { + fn name(&self) -> Cow { + "unsafecallback".into() + } + + fn close(self: Rc) { + self.cancel.cancel(); + // SAFETY: This drops the closure and the callback info associated with it. + // Any retained function pointers to the closure become dangling pointers. + // It is up to the user to know that it is safe to call the `close()` on the + // UnsafeCallback instance. + unsafe { + let info = Box::from_raw(self.info); + let isolate = info.isolate.as_mut().unwrap(); + let _ = v8::Global::from_raw(isolate, info.callback); + let _ = v8::Global::from_raw(isolate, info.context); + } + } +} + +struct CallbackInfo { + pub parameters: Vec, + pub result: NativeType, + pub async_work_sender: mpsc::UnboundedSender, + pub callback: NonNull, + pub context: NonNull, + pub isolate: *mut v8::Isolate, + pub waker: Option, +} + +impl Future for CallbackInfo { + type Output = (); + fn poll( + mut self: Pin<&mut Self>, + cx: &mut std::task::Context<'_>, + ) -> std::task::Poll { + // Always replace the waker to make sure it's bound to the proper Future. + self.waker.replace(cx.waker().clone()); + // The future for the CallbackInfo never resolves: It can only be canceled. + Poll::Pending + } +} +unsafe extern "C" fn deno_ffi_callback( + _cif: &libffi::low::ffi_cif, + result: &mut c_void, + args: *const *const c_void, + info: &CallbackInfo, +) { + LOCAL_ISOLATE_POINTER.with(|s| { + if ptr::eq(*s.borrow(), info.isolate) { + // Own isolate thread, okay to call directly + do_ffi_callback(info, result, args); + } else { + let async_work_sender = &info.async_work_sender; + // SAFETY: Safe as this function blocks until `do_ffi_callback` completes and a response message is received. + let result: &'static mut c_void = std::mem::transmute(result); + let info: &'static CallbackInfo = std::mem::transmute(info); + let (response_sender, response_receiver) = sync_channel::<()>(0); + let fut = Box::new(move || { + do_ffi_callback(info, result, args); + response_sender.send(()).unwrap(); + }); + async_work_sender.unbounded_send(fut).unwrap(); + if let Some(waker) = info.waker.as_ref() { + // Make sure event loop wakes up to receive our message before we start waiting for a response. + waker.wake_by_ref(); + } + response_receiver.recv().unwrap(); + } + }); +} + +unsafe fn do_ffi_callback( + info: &CallbackInfo, + result: &mut c_void, + args: *const *const c_void, +) { + let callback: NonNull = info.callback; + let context: NonNull = info.context; + let isolate: *mut v8::Isolate = info.isolate; + let isolate = &mut *isolate; + let callback = v8::Global::from_raw(isolate, callback); + let context = std::mem::transmute::< + NonNull, + v8::Local, + >(context); + // Call from main thread. If this callback is being triggered due to a + // function call coming from Deno itself, then this callback will build + // ontop of that stack. + // If this callback is being triggered outside of Deno (for example from a + // signal handler) then this will either create an empty new stack if + // Deno currently has nothing running and is waiting for promises to resolve, + // or will (very incorrectly) build ontop of whatever stack exists. + // The callback will even be called through from a `while (true)` liveloop, but + // it somehow cannot change the values that the loop sees, even if they both + // refer the same `let bool_value`. + let mut cb_scope = v8::CallbackScope::new(context); + let scope = &mut v8::HandleScope::new(&mut cb_scope); + let func = callback.open(scope); + let result = result as *mut c_void; + let vals: &[*const c_void] = + std::slice::from_raw_parts(args, info.parameters.len() as usize); + + let mut params: Vec> = vec![]; + for (native_type, val) in info.parameters.iter().zip(vals) { + let value: v8::Local = match native_type { + NativeType::Bool => { + let value = *((*val) as *const bool); + v8::Boolean::new(scope, value).into() + } + NativeType::F32 => { + let value = *((*val) as *const f32); + v8::Number::new(scope, value as f64).into() + } + NativeType::F64 => { + let value = *((*val) as *const f64); + v8::Number::new(scope, value).into() + } + NativeType::I8 => { + let value = *((*val) as *const i8); + v8::Integer::new(scope, value as i32).into() + } + NativeType::U8 => { + let value = *((*val) as *const u8); + v8::Integer::new_from_unsigned(scope, value as u32).into() + } + NativeType::I16 => { + let value = *((*val) as *const i16); + v8::Integer::new(scope, value as i32).into() + } + NativeType::U16 => { + let value = *((*val) as *const u16); + v8::Integer::new_from_unsigned(scope, value as u32).into() + } + NativeType::I32 => { + let value = *((*val) as *const i32); + v8::Integer::new(scope, value).into() + } + NativeType::U32 => { + let value = *((*val) as *const u32); + v8::Integer::new_from_unsigned(scope, value).into() + } + NativeType::I64 | NativeType::ISize => { + let result = *((*val) as *const i64); + if result > MAX_SAFE_INTEGER as i64 || result < MIN_SAFE_INTEGER as i64 + { + v8::BigInt::new_from_i64(scope, result).into() + } else { + v8::Number::new(scope, result as f64).into() + } + } + NativeType::U64 | NativeType::USize => { + let result = *((*val) as *const u64); + if result > MAX_SAFE_INTEGER as u64 { + v8::BigInt::new_from_u64(scope, result).into() + } else { + v8::Number::new(scope, result as f64).into() + } + } + NativeType::Pointer | NativeType::Buffer | NativeType::Function => { + let result = *((*val) as *const usize); + if result > MAX_SAFE_INTEGER as usize { + v8::BigInt::new_from_u64(scope, result as u64).into() + } else { + v8::Number::new(scope, result as f64).into() + } + } + NativeType::Void => unreachable!(), + }; + params.push(value); + } + + let recv = v8::undefined(scope); + let call_result = func.call(scope, recv.into(), ¶ms); + std::mem::forget(callback); + + if call_result.is_none() { + // JS function threw an exception. Set the return value to zero and return. + // The exception continue propagating up the call chain when the event loop + // resumes. + match info.result { + NativeType::Bool => { + *(result as *mut bool) = false; + } + NativeType::U32 | NativeType::I32 => { + // zero is equal for signed and unsigned alike + *(result as *mut u32) = 0; + } + NativeType::F32 => { + *(result as *mut f32) = 0.0; + } + NativeType::F64 => { + *(result as *mut f64) = 0.0; + } + NativeType::U8 | NativeType::I8 => { + // zero is equal for signed and unsigned alike + *(result as *mut u8) = 0; + } + NativeType::U16 | NativeType::I16 => { + // zero is equal for signed and unsigned alike + *(result as *mut u16) = 0; + } + NativeType::Pointer + | NativeType::Buffer + | NativeType::Function + | NativeType::U64 + | NativeType::I64 => { + *(result as *mut usize) = 0; + } + NativeType::Void => { + // nop + } + _ => { + unreachable!(); + } + }; + + return; + } + let value = call_result.unwrap(); + + match info.result { + NativeType::Bool => { + let value = if let Ok(value) = v8::Local::::try_from(value) { + value.is_true() + } else { + value.boolean_value(scope) + }; + *(result as *mut bool) = value; + } + NativeType::I32 => { + let value = if let Ok(value) = v8::Local::::try_from(value) { + value.value() as i32 + } else { + // Fallthrough, probably UB. + value + .int32_value(scope) + .expect("Unable to deserialize result parameter.") as i32 + }; + *(result as *mut i32) = value; + } + NativeType::F32 => { + let value = if let Ok(value) = v8::Local::::try_from(value) { + value.value() as f32 + } else { + // Fallthrough, probably UB. + value + .number_value(scope) + .expect("Unable to deserialize result parameter.") as f32 + }; + *(result as *mut f32) = value; + } + NativeType::F64 => { + let value = if let Ok(value) = v8::Local::::try_from(value) { + value.value() + } else { + // Fallthrough, probably UB. + value + .number_value(scope) + .expect("Unable to deserialize result parameter.") + }; + *(result as *mut f64) = value; + } + NativeType::Pointer | NativeType::Buffer | NativeType::Function => { + let pointer = if let Ok(value) = + v8::Local::::try_from(value) + { + let byte_offset = value.byte_offset(); + let backing_store = value + .buffer(scope) + .expect("Unable to deserialize result parameter.") + .get_backing_store(); + &backing_store[byte_offset..] as *const _ as *const u8 + } else if let Ok(value) = v8::Local::::try_from(value) { + value.u64_value().0 as usize as *const u8 + } else if let Ok(value) = v8::Local::::try_from(value) { + let backing_store = value.get_backing_store(); + &backing_store[..] as *const _ as *const u8 + } else if let Ok(value) = v8::Local::::try_from(value) { + value.value() as usize as *const u8 + } else if value.is_null() { + ptr::null() + } else { + // Fallthrough: Probably someone returned a number but this could + // also be eg. a string. This is essentially UB. + value + .integer_value(scope) + .expect("Unable to deserialize result parameter.") as usize + as *const u8 + }; + *(result as *mut *const u8) = pointer; + } + NativeType::I8 => { + let value = if let Ok(value) = v8::Local::::try_from(value) { + value.value() as i8 + } else { + // Fallthrough, essentially UB. + value + .int32_value(scope) + .expect("Unable to deserialize result parameter.") as i8 + }; + *(result as *mut i8) = value; + } + NativeType::U8 => { + let value = if let Ok(value) = v8::Local::::try_from(value) { + value.value() as u8 + } else { + // Fallthrough, essentially UB. + value + .uint32_value(scope) + .expect("Unable to deserialize result parameter.") as u8 + }; + *(result as *mut u8) = value; + } + NativeType::I16 => { + let value = if let Ok(value) = v8::Local::::try_from(value) { + value.value() as i16 + } else { + // Fallthrough, essentially UB. + value + .int32_value(scope) + .expect("Unable to deserialize result parameter.") as i16 + }; + *(result as *mut i16) = value; + } + NativeType::U16 => { + let value = if let Ok(value) = v8::Local::::try_from(value) { + value.value() as u16 + } else { + // Fallthrough, essentially UB. + value + .uint32_value(scope) + .expect("Unable to deserialize result parameter.") as u16 + }; + *(result as *mut u16) = value; + } + NativeType::U32 => { + let value = if let Ok(value) = v8::Local::::try_from(value) { + value.value() as u32 + } else { + // Fallthrough, essentially UB. + value + .uint32_value(scope) + .expect("Unable to deserialize result parameter.") + }; + *(result as *mut u32) = value; + } + NativeType::I64 => { + if let Ok(value) = v8::Local::::try_from(value) { + *(result as *mut i64) = value.i64_value().0; + } else if let Ok(value) = v8::Local::::try_from(value) { + *(result as *mut i64) = value.value(); + } else { + *(result as *mut i64) = value + .integer_value(scope) + .expect("Unable to deserialize result parameter.") + as i64; + } + } + NativeType::U64 => { + if let Ok(value) = v8::Local::::try_from(value) { + *(result as *mut u64) = value.u64_value().0; + } else if let Ok(value) = v8::Local::::try_from(value) { + *(result as *mut u64) = value.value() as u64; + } else { + *(result as *mut u64) = value + .integer_value(scope) + .expect("Unable to deserialize result parameter.") + as u64; + } + } + NativeType::Void => { + // nop + } + _ => { + unreachable!(); + } + }; +} + +#[op] +pub fn op_ffi_unsafe_callback_ref( + state: Rc>, + rid: ResourceId, +) -> Result>, AnyError> { + let state = state.borrow(); + let callback_resource = + state.resource_table.get::(rid)?; + + Ok(async move { + let info: &mut CallbackInfo = + // SAFETY: CallbackInfo pointer stays valid as long as the resource is still alive. + unsafe { callback_resource.info.as_mut().unwrap() }; + // Ignore cancellation rejection + let _ = info + .into_future() + .or_cancel(callback_resource.cancel.clone()) + .await; + Ok(()) + }) +} + +#[op(fast)] +pub fn op_ffi_unsafe_callback_unref( + state: &mut deno_core::OpState, + rid: u32, +) -> Result<(), AnyError> { + state + .resource_table + .get::(rid)? + .cancel + .cancel(); + Ok(()) +} + +#[derive(Deserialize)] +pub struct RegisterCallbackArgs { + parameters: Vec, + result: NativeType, +} + +#[op(v8)] +pub fn op_ffi_unsafe_callback_create( + state: &mut deno_core::OpState, + scope: &mut v8::HandleScope<'scope>, + args: RegisterCallbackArgs, + cb: serde_v8::Value<'scope>, +) -> Result, AnyError> +where + FP: FfiPermissions + 'static, +{ + check_unstable(state, "Deno.UnsafeCallback"); + let permissions = state.borrow_mut::(); + permissions.check(None)?; + + let v8_value = cb.v8_value; + let cb = v8::Local::::try_from(v8_value)?; + + let isolate: *mut v8::Isolate = &mut *scope as &mut v8::Isolate; + LOCAL_ISOLATE_POINTER.with(|s| { + if s.borrow().is_null() { + s.replace(isolate); + } + }); + + let async_work_sender = + state.borrow_mut::().async_work_sender.clone(); + let callback = v8::Global::new(scope, cb).into_raw(); + let current_context = scope.get_current_context(); + let context = v8::Global::new(scope, current_context).into_raw(); + + let info: *mut CallbackInfo = Box::leak(Box::new(CallbackInfo { + parameters: args.parameters.clone(), + result: args.result, + async_work_sender, + callback, + context, + isolate, + waker: None, + })); + let cif = Cif::new( + args.parameters.into_iter().map(libffi::middle::Type::from), + libffi::middle::Type::from(args.result), + ); + + // SAFETY: CallbackInfo is leaked, is not null and stays valid as long as the callback exists. + let closure = libffi::middle::Closure::new(cif, deno_ffi_callback, unsafe { + info.as_ref().unwrap() + }); + let ptr = *closure.code_ptr() as usize; + let resource = UnsafeCallbackResource { + cancel: CancelHandle::new_rc(), + closure, + info, + }; + let rid = state.resource_table.add(resource); + + let rid_local = v8::Integer::new_from_unsigned(scope, rid); + let ptr_local: v8::Local = if ptr > MAX_SAFE_INTEGER as usize { + v8::BigInt::new_from_u64(scope, ptr as u64).into() + } else { + v8::Number::new(scope, ptr as f64).into() + }; + let array = v8::Array::new(scope, 2); + array.set_index(scope, 0, rid_local.into()); + array.set_index(scope, 1, ptr_local); + let array_value: v8::Local = array.into(); + + Ok(array_value.into()) +} diff --git a/ext/ffi/dlfcn.rs b/ext/ffi/dlfcn.rs new file mode 100644 index 000000000..da47dcd47 --- /dev/null +++ b/ext/ffi/dlfcn.rs @@ -0,0 +1,398 @@ +// Copyright 2018-2022 the Deno authors. All rights reserved. MIT license. + +use crate::check_unstable; +use crate::symbol::NativeType; +use crate::symbol::Symbol; +use crate::turbocall; +use crate::FfiPermissions; +use deno_core::error::generic_error; +use deno_core::error::AnyError; +use deno_core::op; +use deno_core::serde_v8; +use deno_core::v8; +use deno_core::Resource; +use deno_core::ResourceId; +use dlopen::raw::Library; +use serde::Deserialize; +use std::borrow::Cow; +use std::collections::HashMap; +use std::ffi::c_void; +use std::path::PathBuf; +use std::rc::Rc; + +pub struct DynamicLibraryResource { + lib: Library, + pub symbols: HashMap>, +} + +impl Resource for DynamicLibraryResource { + fn name(&self) -> Cow { + "dynamicLibrary".into() + } + + fn close(self: Rc) { + drop(self) + } +} + +impl DynamicLibraryResource { + pub fn get_static(&self, symbol: String) -> Result<*const c_void, AnyError> { + // By default, Err returned by this function does not tell + // which symbol wasn't exported. So we'll modify the error + // message to include the name of symbol. + // + // SAFETY: The obtained T symbol is the size of a pointer. + match unsafe { self.lib.symbol::<*const c_void>(&symbol) } { + Ok(value) => Ok(Ok(value)), + Err(err) => Err(generic_error(format!( + "Failed to register symbol {}: {}", + symbol, err + ))), + }? + } +} + +pub fn needs_unwrap(rv: NativeType) -> bool { + matches!( + rv, + NativeType::Function + | NativeType::Pointer + | NativeType::Buffer + | NativeType::I64 + | NativeType::ISize + | NativeType::U64 + | NativeType::USize + ) +} + +fn is_i64(rv: NativeType) -> bool { + matches!(rv, NativeType::I64 | NativeType::ISize) +} + +#[derive(Deserialize, Debug)] +#[serde(rename_all = "camelCase")] +pub struct ForeignFunction { + name: Option, + pub parameters: Vec, + pub result: NativeType, + #[serde(rename = "nonblocking")] + non_blocking: Option, + #[serde(rename = "callback")] + #[serde(default = "default_callback")] + callback: bool, +} + +fn default_callback() -> bool { + false +} + +// ForeignStatic's name and type fields are read and used by +// serde_v8 to determine which variant a ForeignSymbol is. +// They are not used beyond that and are thus marked with underscores. +#[derive(Deserialize, Debug)] +struct ForeignStatic { + #[serde(rename(deserialize = "name"))] + _name: Option, + #[serde(rename(deserialize = "type"))] + _type: String, +} + +#[derive(Deserialize, Debug)] +#[serde(untagged)] +enum ForeignSymbol { + ForeignFunction(ForeignFunction), + ForeignStatic(ForeignStatic), +} + +#[derive(Deserialize, Debug)] +pub struct FfiLoadArgs { + path: String, + symbols: HashMap, +} + +#[op(v8)] +pub fn op_ffi_load( + scope: &mut v8::HandleScope<'scope>, + state: &mut deno_core::OpState, + args: FfiLoadArgs, +) -> Result<(ResourceId, serde_v8::Value<'scope>), AnyError> +where + FP: FfiPermissions + 'static, +{ + let path = args.path; + + check_unstable(state, "Deno.dlopen"); + let permissions = state.borrow_mut::(); + permissions.check(Some(&PathBuf::from(&path)))?; + + let lib = Library::open(&path).map_err(|e| { + dlopen::Error::OpeningLibraryError(std::io::Error::new( + std::io::ErrorKind::Other, + format_error(e, path), + )) + })?; + let mut resource = DynamicLibraryResource { + lib, + symbols: HashMap::new(), + }; + let obj = v8::Object::new(scope); + + for (symbol_key, foreign_symbol) in args.symbols { + match foreign_symbol { + ForeignSymbol::ForeignStatic(_) => { + // No-op: Statics will be handled separately and are not part of the Rust-side resource. + } + ForeignSymbol::ForeignFunction(foreign_fn) => { + let symbol = match &foreign_fn.name { + Some(symbol) => symbol, + None => &symbol_key, + }; + // By default, Err returned by this function does not tell + // which symbol wasn't exported. So we'll modify the error + // message to include the name of symbol. + let fn_ptr = + // SAFETY: The obtained T symbol is the size of a pointer. + match unsafe { resource.lib.symbol::<*const c_void>(symbol) } { + Ok(value) => Ok(value), + Err(err) => Err(generic_error(format!( + "Failed to register symbol {}: {}", + symbol, err + ))), + }?; + let ptr = libffi::middle::CodePtr::from_ptr(fn_ptr as _); + let cif = libffi::middle::Cif::new( + foreign_fn + .parameters + .clone() + .into_iter() + .map(libffi::middle::Type::from), + foreign_fn.result.into(), + ); + + let func_key = v8::String::new(scope, &symbol_key).unwrap(); + let sym = Box::new(Symbol { + cif, + ptr, + parameter_types: foreign_fn.parameters, + result_type: foreign_fn.result, + can_callback: foreign_fn.callback, + }); + + resource.symbols.insert(symbol_key, sym.clone()); + match foreign_fn.non_blocking { + // Generate functions for synchronous calls. + Some(false) | None => { + let function = make_sync_fn(scope, sym); + obj.set(scope, func_key.into(), function.into()); + } + // This optimization is not yet supported for non-blocking calls. + _ => {} + }; + } + } + } + + let rid = state.resource_table.add(resource); + Ok(( + rid, + serde_v8::Value { + v8_value: obj.into(), + }, + )) +} + +// Create a JavaScript function for synchronous FFI call to +// the given symbol. +fn make_sync_fn<'s>( + scope: &mut v8::HandleScope<'s>, + sym: Box, +) -> v8::Local<'s, v8::Function> { + let sym = Box::leak(sym); + let builder = v8::FunctionTemplate::builder( + |scope: &mut v8::HandleScope, + args: v8::FunctionCallbackArguments, + mut rv: v8::ReturnValue| { + let external: v8::Local = args.data().try_into().unwrap(); + // SAFETY: The pointer will not be deallocated until the function is + // garbage collected. + let symbol = unsafe { &*(external.value() as *const Symbol) }; + let needs_unwrap = match needs_unwrap(symbol.result_type) { + true => Some(args.get(symbol.parameter_types.len() as i32)), + false => None, + }; + match crate::call::ffi_call_sync(scope, args, symbol) { + Ok(result) => { + match needs_unwrap { + Some(v) => { + let view: v8::Local = v.try_into().unwrap(); + let backing_store = + view.buffer(scope).unwrap().get_backing_store(); + + if is_i64(symbol.result_type) { + // SAFETY: v8::SharedRef is similar to Arc<[u8]>, + // it points to a fixed continuous slice of bytes on the heap. + let bs = unsafe { + &mut *(&backing_store[..] as *const _ as *mut [u8] + as *mut i64) + }; + // SAFETY: We already checked that type == I64 + let value = unsafe { result.i64_value }; + *bs = value; + } else { + // SAFETY: v8::SharedRef is similar to Arc<[u8]>, + // it points to a fixed continuous slice of bytes on the heap. + let bs = unsafe { + &mut *(&backing_store[..] as *const _ as *mut [u8] + as *mut u64) + }; + // SAFETY: We checked that type == U64 + let value = unsafe { result.u64_value }; + *bs = value; + } + } + None => { + // SAFETY: Same return type declared to libffi; trust user to have it right beyond that. + let result = unsafe { result.to_v8(scope, symbol.result_type) }; + rv.set(result.v8_value); + } + } + } + Err(err) => { + deno_core::_ops::throw_type_error(scope, err.to_string()); + } + }; + }, + ) + .data(v8::External::new(scope, sym as *mut Symbol as *mut _).into()); + + let mut fast_call_alloc = None; + + let func = if turbocall::is_compatible(sym) { + let trampoline = turbocall::compile_trampoline(sym); + let func = builder.build_fast( + scope, + &turbocall::make_template(sym, &trampoline), + None, + ); + fast_call_alloc = Some(Box::into_raw(Box::new(trampoline))); + func + } else { + builder.build(scope) + }; + let func = func.get_function(scope).unwrap(); + + let weak = v8::Weak::with_finalizer( + scope, + func, + Box::new(move |_| { + // SAFETY: This is never called twice. pointer obtained + // from Box::into_raw, hence, satisfies memory layout requirements. + let _ = unsafe { Box::from_raw(sym) }; + if let Some(fast_call_ptr) = fast_call_alloc { + // fast-call compiled trampoline is unmapped when the MMAP handle is dropped + // SAFETY: This is never called twice. pointer obtained + // from Box::into_raw, hence, satisfies memory layout requirements. + let _ = unsafe { Box::from_raw(fast_call_ptr) }; + } + }), + ); + + weak.to_local(scope).unwrap() +} + +// `path` is only used on Windows. +#[allow(unused_variables)] +pub(crate) fn format_error(e: dlopen::Error, path: String) -> String { + match e { + #[cfg(target_os = "windows")] + // This calls FormatMessageW with library path + // as replacement for the insert sequences. + // Unlike libstd which passes the FORMAT_MESSAGE_IGNORE_INSERTS + // flag without any arguments. + // + // https://github.com/denoland/deno/issues/11632 + dlopen::Error::OpeningLibraryError(e) => { + use std::ffi::OsStr; + use std::os::windows::ffi::OsStrExt; + use winapi::shared::minwindef::DWORD; + use winapi::shared::winerror::ERROR_INSUFFICIENT_BUFFER; + use winapi::um::errhandlingapi::GetLastError; + use winapi::um::winbase::FormatMessageW; + use winapi::um::winbase::FORMAT_MESSAGE_ARGUMENT_ARRAY; + use winapi::um::winbase::FORMAT_MESSAGE_FROM_SYSTEM; + use winapi::um::winnt::LANG_SYSTEM_DEFAULT; + use winapi::um::winnt::MAKELANGID; + use winapi::um::winnt::SUBLANG_SYS_DEFAULT; + + let err_num = match e.raw_os_error() { + Some(err_num) => err_num, + // This should never hit unless dlopen changes its error type. + None => return e.to_string(), + }; + + // Language ID (0x0800) + let lang_id = + MAKELANGID(LANG_SYSTEM_DEFAULT, SUBLANG_SYS_DEFAULT) as DWORD; + + let mut buf = vec![0; 500]; + + let path = OsStr::new(&path) + .encode_wide() + .chain(Some(0).into_iter()) + .collect::>(); + + let arguments = [path.as_ptr()]; + + loop { + // SAFETY: + // winapi call to format the error message + let length = unsafe { + FormatMessageW( + FORMAT_MESSAGE_FROM_SYSTEM | FORMAT_MESSAGE_ARGUMENT_ARRAY, + std::ptr::null_mut(), + err_num as DWORD, + lang_id as DWORD, + buf.as_mut_ptr(), + buf.len() as DWORD, + arguments.as_ptr() as _, + ) + }; + + if length == 0 { + // SAFETY: + // winapi call to get the last error message + let err_num = unsafe { GetLastError() }; + if err_num == ERROR_INSUFFICIENT_BUFFER { + buf.resize(buf.len() * 2, 0); + continue; + } + + // Something went wrong, just return the original error. + return e.to_string(); + } + + let msg = String::from_utf16_lossy(&buf[..length as usize]); + return msg; + } + } + _ => e.to_string(), + } +} + +#[cfg(test)] +mod tests { + #[cfg(target_os = "windows")] + #[test] + fn test_format_error() { + use super::format_error; + + // BAD_EXE_FORMAT + let err = dlopen::Error::OpeningLibraryError( + std::io::Error::from_raw_os_error(0x000000C1), + ); + assert_eq!( + format_error(err, "foo.dll".to_string()), + "foo.dll is not a valid Win32 application.\r\n".to_string(), + ); + } +} diff --git a/ext/ffi/fast_call.rs b/ext/ffi/fast_call.rs deleted file mode 100644 index dc098a69a..000000000 --- a/ext/ffi/fast_call.rs +++ /dev/null @@ -1,2065 +0,0 @@ -// Copyright 2018-2022 the Deno authors. All rights reserved. MIT license. - -use std::cmp::max; -use std::ffi::c_void; -use std::iter::once; - -use deno_core::v8::fast_api; -use dynasmrt::dynasm; -use dynasmrt::DynasmApi; -use dynasmrt::ExecutableBuffer; - -use crate::needs_unwrap; -use crate::NativeType; -use crate::Symbol; - -pub(crate) fn is_compatible(sym: &Symbol) -> bool { - cfg!(any( - all(target_arch = "x86_64", target_family = "unix"), - all(target_arch = "x86_64", target_family = "windows"), - all(target_arch = "aarch64", target_vendor = "apple") - )) && !sym.can_callback -} - -pub(crate) fn compile_trampoline(sym: &Symbol) -> Trampoline { - #[cfg(all(target_arch = "x86_64", target_family = "unix"))] - return SysVAmd64::compile(sym); - #[cfg(all(target_arch = "x86_64", target_family = "windows"))] - return Win64::compile(sym); - #[cfg(all(target_arch = "aarch64", target_vendor = "apple"))] - return Aarch64Apple::compile(sym); - #[allow(unreachable_code)] - { - unimplemented!("fast API is not implemented for the current target"); - } -} - -pub(crate) fn make_template(sym: &Symbol, trampoline: &Trampoline) -> Template { - let mut params = once(fast_api::Type::V8Value) // Receiver - .chain(sym.parameter_types.iter().map(|t| t.into())) - .collect::>(); - - let ret = if needs_unwrap(sym.result_type) { - params.push(fast_api::Type::TypedArray(fast_api::CType::Int32)); - fast_api::Type::Void - } else { - fast_api::Type::from(&sym.result_type) - }; - - Template { - args: params.into_boxed_slice(), - ret: (&ret).into(), - symbol_ptr: trampoline.ptr(), - } -} - -/// Trampoline for fast-call FFI functions -/// -/// Calls the FFI function without the first argument (the receiver) -pub(crate) struct Trampoline(ExecutableBuffer); - -impl Trampoline { - fn ptr(&self) -> *const c_void { - &self.0[0] as *const u8 as *const c_void - } -} - -pub(crate) struct Template { - args: Box<[fast_api::Type]>, - ret: fast_api::CType, - symbol_ptr: *const c_void, -} - -impl fast_api::FastFunction for Template { - fn function(&self) -> *const c_void { - self.symbol_ptr - } - - fn args(&self) -> &'static [fast_api::Type] { - Box::leak(self.args.clone()) - } - - fn return_type(&self) -> fast_api::CType { - self.ret - } -} - -impl From<&NativeType> for fast_api::Type { - fn from(native_type: &NativeType) -> Self { - match native_type { - NativeType::Bool => fast_api::Type::Bool, - NativeType::U8 | NativeType::U16 | NativeType::U32 => { - fast_api::Type::Uint32 - } - NativeType::I8 | NativeType::I16 | NativeType::I32 => { - fast_api::Type::Int32 - } - NativeType::F32 => fast_api::Type::Float32, - NativeType::F64 => fast_api::Type::Float64, - NativeType::Void => fast_api::Type::Void, - NativeType::I64 => fast_api::Type::Int64, - NativeType::U64 => fast_api::Type::Uint64, - NativeType::ISize => fast_api::Type::Int64, - NativeType::USize | NativeType::Pointer | NativeType::Function => { - fast_api::Type::Uint64 - } - NativeType::Buffer => fast_api::Type::TypedArray(fast_api::CType::Uint8), - } - } -} - -macro_rules! x64 { - ($assembler:expr; $($tokens:tt)+) => { - dynasm!($assembler; .arch x64; $($tokens)+) - } -} - -macro_rules! aarch64 { - ($assembler:expr; $($tokens:tt)+) => { - dynasm!($assembler; .arch aarch64; $($tokens)+) - } -} - -struct SysVAmd64 { - // Reference: https://refspecs.linuxfoundation.org/elf/x86_64-abi-0.99.pdf - assmblr: dynasmrt::x64::Assembler, - // Parameter counters - integral_params: u32, - float_params: u32, - // Stack offset accumulators - offset_trampoline: u32, - offset_callee: u32, - allocated_stack: u32, - frame_pointer: u32, -} - -#[cfg_attr( - not(all(target_aarch = "x86_64", target_family = "unix")), - allow(dead_code) -)] -impl SysVAmd64 { - // Integral arguments go to the following GPR, in order: rdi, rsi, rdx, rcx, r8, r9 - const INTEGRAL_REGISTERS: u32 = 6; - // SSE arguments go to the first 8 SSE registers: xmm0-xmm7 - const FLOAT_REGISTERS: u32 = 8; - - fn new() -> Self { - Self { - assmblr: dynasmrt::x64::Assembler::new().unwrap(), - integral_params: 0, - float_params: 0, - // Start at 8 to account for trampoline caller's return address - offset_trampoline: 8, - // default to tail-call mode. If a new stack frame is allocated this becomes 0 - offset_callee: 8, - allocated_stack: 0, - frame_pointer: 0, - } - } - - fn compile(sym: &Symbol) -> Trampoline { - let mut compiler = Self::new(); - - let must_cast_return_value = - compiler.must_cast_return_value(sym.result_type); - let must_wrap_return_value = - compiler.must_wrap_return_value_in_typed_array(sym.result_type); - let must_save_preserved_register = must_wrap_return_value; - let cannot_tailcall = must_cast_return_value || must_wrap_return_value; - - if cannot_tailcall { - if must_save_preserved_register { - compiler.save_preserved_register_to_stack(); - } - compiler.allocate_stack(&sym.parameter_types); - } - - for param in sym.parameter_types.iter().copied() { - compiler.move_left(param) - } - if !compiler.is_recv_arg_overridden() { - // the receiver object should never be expected. Avoid its unexpected or deliberate leak - compiler.zero_first_arg(); - } - if must_wrap_return_value { - compiler.save_out_array_to_preserved_register(); - } - - if cannot_tailcall { - compiler.call(sym.ptr.as_ptr()); - if must_cast_return_value { - compiler.cast_return_value(sym.result_type); - } - if must_wrap_return_value { - compiler.wrap_return_value_in_out_array(); - } - compiler.deallocate_stack(); - if must_save_preserved_register { - compiler.recover_preserved_register(); - } - compiler.ret(); - } else { - compiler.tailcall(sym.ptr.as_ptr()); - } - - Trampoline(compiler.finalize()) - } - - fn move_left(&mut self, param: NativeType) { - // Section 3.2.3 of the SysV ABI spec, on argument classification: - // - INTEGER: - // > Arguments of types (signed and unsigned) _Bool, char, short, int, - // > long, long long, and pointers are in the INTEGER class. - // - SSE: - // > Arguments of types float, double, _Decimal32, _Decimal64 and - // > __m64 are in class SSE. - match param.into() { - Int(integral) => self.move_integral(integral), - Float(float) => self.move_float(float), - } - } - - fn move_float(&mut self, param: Floating) { - // Section 3.2.3 of the SysV AMD64 ABI: - // > If the class is SSE, the next available vector register is used, the registers - // > are taken in the order from %xmm0 to %xmm7. - // [...] - // > Once registers are assigned, the arguments passed in memory are pushed on - // > the stack in reversed (right-to-left) order - let param_i = self.float_params; - - let is_in_stack = param_i >= Self::FLOAT_REGISTERS; - // floats are only moved to accommodate integer movement in the stack - let stack_has_moved = self.allocated_stack > 0 - || self.integral_params >= Self::INTEGRAL_REGISTERS; - - if is_in_stack && stack_has_moved { - let s = &mut self.assmblr; - let ot = self.offset_trampoline as i32; - let oc = self.offset_callee as i32; - match param { - Single => x64!(s - ; movss xmm8, [rsp + ot] - ; movss [rsp + oc], xmm8 - ), - Double => x64!(s - ; movsd xmm8, [rsp + ot] - ; movsd [rsp + oc], xmm8 - ), - } - - // Section 3.2.3 of the SysV AMD64 ABI: - // > The size of each argument gets rounded up to eightbytes. [...] Therefore the stack will always be eightbyte aligned. - self.offset_trampoline += 8; - self.offset_callee += 8; - - debug_assert!( - self.allocated_stack == 0 || self.offset_callee <= self.allocated_stack - ); - } - self.float_params += 1; - } - - fn move_integral(&mut self, arg: Integral) { - // Section 3.2.3 of the SysV AMD64 ABI: - // > If the class is INTEGER, the next available register of the sequence %rdi, - // > %rsi, %rdx, %rcx, %r8 and %r9 is used - // [...] - // > Once registers are assigned, the arguments passed in memory are pushed on - // > the stack in reversed (right-to-left) order - let s = &mut self.assmblr; - let param_i = self.integral_params; - - // move each argument one position to the left. The first argument in the stack moves to the last integer register (r9). - // If the FFI function is called with a new stack frame, the arguments remaining in the stack are copied to the new stack frame. - // Otherwise, they are copied 8 bytes lower in the same frame - match (param_i, arg) { - // u8 and u16 parameters are defined as u32 parameters in the V8's fast API function. The trampoline takes care of the cast. - // Conventionally, many compilers expect 8 and 16 bit arguments to be sign/zero extended by the caller - // See https://stackoverflow.com/a/36760539/2623340 - (0, U(B)) => x64!(s; movzx edi, sil), - (0, I(B)) => x64!(s; movsx edi, sil), - (0, U(W)) => x64!(s; movzx edi, si), - (0, I(W)) => x64!(s; movsx edi, si), - (0, U(DW) | I(DW)) => x64!(s; mov edi, esi), - (0, U(QW) | I(QW)) => x64!(s; mov rdi, rsi), - // The fast API expects buffer arguments passed as a pointer to a FastApiTypedArray struct - // Here we blindly follow the layout of https://github.com/denoland/rusty_v8/blob/main/src/fast_api.rs#L190-L200 - // although that might be problematic: https://discord.com/channels/684898665143206084/956626010248478720/1009450940866252823 - (0, Buffer) => x64!(s; mov rdi, [rsi + 8]), - - (1, U(B)) => x64!(s; movzx esi, dl), - (1, I(B)) => x64!(s; movsx esi, dl), - (1, U(W)) => x64!(s; movzx esi, dx), - (1, I(W)) => x64!(s; movsx esi, dx), - (1, U(DW) | I(DW)) => x64!(s; mov esi, edx), - (1, U(QW) | I(QW)) => x64!(s; mov rsi, rdx), - (1, Buffer) => x64!(s; mov rsi, [rdx + 8]), - - (2, U(B)) => x64!(s; movzx edx, cl), - (2, I(B)) => x64!(s; movsx edx, cl), - (2, U(W)) => x64!(s; movzx edx, cx), - (2, I(W)) => x64!(s; movsx edx, cx), - (2, U(DW) | I(DW)) => x64!(s; mov edx, ecx), - (2, U(QW) | I(QW)) => x64!(s; mov rdx, rcx), - (2, Buffer) => x64!(s; mov rdx, [rcx + 8]), - - (3, U(B)) => x64!(s; movzx ecx, r8b), - (3, I(B)) => x64!(s; movsx ecx, r8b), - (3, U(W)) => x64!(s; movzx ecx, r8w), - (3, I(W)) => x64!(s; movsx ecx, r8w), - (3, U(DW) | I(DW)) => x64!(s; mov ecx, r8d), - (3, U(QW) | I(QW)) => x64!(s; mov rcx, r8), - (3, Buffer) => x64!(s; mov rcx, [r8 + 8]), - - (4, U(B)) => x64!(s; movzx r8d, r9b), - (4, I(B)) => x64!(s; movsx r8d, r9b), - (4, U(W)) => x64!(s; movzx r8d, r9w), - (4, I(W)) => x64!(s; movsx r8d, r9w), - (4, U(DW) | I(DW)) => x64!(s; mov r8d, r9d), - (4, U(QW) | I(QW)) => x64!(s; mov r8, r9), - (4, Buffer) => x64!(s; mov r8, [r9 + 8]), - - (5, param) => { - let ot = self.offset_trampoline as i32; - // First argument in stack goes to last register (r9) - match param { - U(B) => x64!(s; movzx r9d, BYTE [rsp + ot]), - I(B) => x64!(s; movsx r9d, BYTE [rsp + ot]), - U(W) => x64!(s; movzx r9d, WORD [rsp + ot]), - I(W) => x64!(s; movsx r9d, WORD [rsp + ot]), - U(DW) | I(DW) => x64!(s; mov r9d, [rsp + ot]), - U(QW) | I(QW) => x64!(s; mov r9, [rsp + ot]), - Buffer => x64!(s - ; mov r9, [rsp + ot] - ; mov r9, [r9 + 8] - ), - } - // Section 3.2.3 of the SysV AMD64 ABI: - // > The size of each argument gets rounded up to eightbytes. [...] Therefore the stack will always be eightbyte aligned. - self.offset_trampoline += 8; - } - - (6.., param) => { - let ot = self.offset_trampoline as i32; - let oc = self.offset_callee as i32; - match param { - U(B) => x64!(s - // TODO: optimize to [rsp] (without immediate) when offset is 0 - ; movzx eax, BYTE [rsp + ot] - ; mov [rsp + oc], eax - ), - I(B) => x64!(s - ; movsx eax, BYTE [rsp + ot] - ; mov [rsp + oc], eax - ), - U(W) => x64!(s - ; movzx eax, WORD [rsp + ot] - ; mov [rsp + oc], eax - ), - I(W) => x64!(s - ; movsx eax, WORD [rsp + ot] - ; mov [rsp + oc], eax - ), - U(DW) | I(DW) => x64!(s - ; mov eax, [rsp + ot] - ; mov [rsp + oc], eax - ), - U(QW) | I(QW) => x64!(s - ; mov rax, [rsp + ot] - ; mov [rsp + oc], rax - ), - Buffer => x64!(s - ; mov rax, [rsp + ot] - ; mov rax, [rax + 8] - ; mov [rsp + oc], rax - ), - } - // Section 3.2.3 of the SysV AMD64 ABI: - // > The size of each argument gets rounded up to eightbytes. [...] Therefore the stack will always be eightbyte aligned. - self.offset_trampoline += 8; - self.offset_callee += 8; - - debug_assert!( - self.allocated_stack == 0 - || self.offset_callee <= self.allocated_stack - ); - } - } - self.integral_params += 1; - } - - fn zero_first_arg(&mut self) { - debug_assert!( - self.integral_params == 0, - "the trampoline would zero the first argument after having overridden it with the second one" - ); - dynasm!(self.assmblr - ; .arch x64 - ; xor edi, edi - ); - } - - fn cast_return_value(&mut self, rv: NativeType) { - let s = &mut self.assmblr; - // V8 only supports 32bit integers. We support 8 and 16 bit integers casting them to 32bits. - // In SysV-AMD64 the convention dictates that the unused bits of the return value contain garbage, so we - // need to zero/sign extend the return value explicitly - match rv { - NativeType::U8 => x64!(s; movzx eax, al), - NativeType::I8 => x64!(s; movsx eax, al), - NativeType::U16 => x64!(s; movzx eax, ax), - NativeType::I16 => x64!(s; movsx eax, ax), - _ => (), - } - } - - fn save_out_array_to_preserved_register(&mut self) { - let s = &mut self.assmblr; - // functions returning 64 bit integers have the out array appended as their last parameter, - // and it is a *FastApiTypedArray - match self.integral_params { - // Trampoline's signature is (receiver, [param0, param1, ...], *FastApiTypedArray) - // self.integral_params account only for the original params [param0, param1, ...] - // and the out array has not been moved left - 0 => x64!(s; mov rbx, [rsi + 8]), - 1 => x64!(s; mov rbx, [rdx + 8]), - 2 => x64!(s; mov rbx, [rcx + 8]), - 3 => x64!(s; mov rbx, [r8 + 8]), - 4 => x64!(s; mov rbx, [r9 + 8]), - 5.. => { - x64!(s - ; mov rax, [rsp + self.offset_trampoline as i32] - ; mov rbx, [rax + 8] - ) - } - } - } - - fn wrap_return_value_in_out_array(&mut self) { - x64!(self.assmblr; mov [rbx], rax); - } - - fn save_preserved_register_to_stack(&mut self) { - x64!(self.assmblr; push rbx); - self.offset_trampoline += 8; - // stack pointer has been modified, and the callee stack parameters are expected at the top of the stack - self.offset_callee = 0; - self.frame_pointer += 8; - } - - fn recover_preserved_register(&mut self) { - debug_assert!( - self.frame_pointer >= 8, - "the trampoline would try to pop from the stack beyond its frame pointer" - ); - x64!(self.assmblr; pop rbx); - self.frame_pointer -= 8; - // parameter offsets are invalid once this method is called - } - - fn allocate_stack(&mut self, params: &[NativeType]) { - let mut int_params = 0u32; - let mut float_params = 0u32; - for param in params { - match param { - NativeType::F32 | NativeType::F64 => float_params += 1, - _ => int_params += 1, - } - } - let mut stack_size = (int_params.saturating_sub(Self::INTEGRAL_REGISTERS) - + float_params.saturating_sub(Self::FLOAT_REGISTERS)) - * 8; - - // Align new stack frame (accounting for the 8 byte of the trampoline caller's return address - // and any other potential addition to the stack prior to this allocation) - // Section 3.2.2 of the SysV AMD64 ABI: - // > The end of the input argument area shall be aligned on a 16 (32 or 64, if - // > __m256 or __m512 is passed on stack) byte boundary. In other words, the value - // > (%rsp + 8) is always a multiple of 16 (32 or 64) when control is transferred to - // > the function entry point. The stack pointer, %rsp, always points to the end of the - // > latest allocated stack frame. - stack_size += padding_to_align(16, self.frame_pointer + stack_size + 8); - - if stack_size > 0 { - x64!(self.assmblr; sub rsp, stack_size as i32); - self.offset_trampoline += stack_size; - // stack pointer has been modified, and the callee stack parameters are expected at the top of the stack - self.offset_callee = 0; - self.allocated_stack += stack_size; - self.frame_pointer += stack_size; - } - } - - fn deallocate_stack(&mut self) { - debug_assert!( - self.frame_pointer >= self.allocated_stack, - "the trampoline would try to deallocate stack beyond its frame pointer" - ); - if self.allocated_stack > 0 { - x64!(self.assmblr; add rsp, self.allocated_stack as i32); - - self.frame_pointer -= self.allocated_stack; - self.allocated_stack = 0; - } - } - - fn call(&mut self, ptr: *const c_void) { - // the stack has been aligned during stack allocation and/or pushing of preserved registers - debug_assert!( - (8 + self.frame_pointer) % 16 == 0, - "the trampoline would call the FFI function with an unaligned stack" - ); - x64!(self.assmblr - ; mov rax, QWORD ptr as _ - ; call rax - ); - } - - fn tailcall(&mut self, ptr: *const c_void) { - // stack pointer is never modified and remains aligned - // return address remains the one provided by the trampoline's caller (V8) - debug_assert!( - self.allocated_stack == 0, - "the trampoline would tail call the FFI function with an outstanding stack allocation" - ); - debug_assert!( - self.frame_pointer == 0, - "the trampoline would tail call the FFI function with outstanding locals in the frame" - ); - x64!(self.assmblr - ; mov rax, QWORD ptr as _ - ; jmp rax - ); - } - - fn ret(&mut self) { - debug_assert!( - self.allocated_stack == 0, - "the trampoline would return with an outstanding stack allocation" - ); - debug_assert!( - self.frame_pointer == 0, - "the trampoline would return with outstanding locals in the frame" - ); - x64!(self.assmblr; ret); - } - - fn is_recv_arg_overridden(&self) -> bool { - // V8 receiver is the first parameter of the trampoline function and is a pointer - self.integral_params > 0 - } - - fn must_cast_return_value(&self, rv: NativeType) -> bool { - // V8 only supports i32 and u32 return types for integers - // We support 8 and 16 bit integers by extending them to 32 bits in the trampoline before returning - matches!( - rv, - NativeType::U8 | NativeType::I8 | NativeType::U16 | NativeType::I16 - ) - } - - fn must_wrap_return_value_in_typed_array(&self, rv: NativeType) -> bool { - // V8 only supports i32 and u32 return types for integers - // We support 64 bit integers by wrapping them in a TypedArray out parameter - crate::needs_unwrap(rv) - } - - fn finalize(self) -> ExecutableBuffer { - self.assmblr.finalize().unwrap() - } -} - -struct Aarch64Apple { - // Reference https://github.com/ARM-software/abi-aa/blob/main/aapcs64/aapcs64.rst - assmblr: dynasmrt::aarch64::Assembler, - // Parameter counters - integral_params: u32, - float_params: u32, - // Stack offset accumulators - offset_trampoline: u32, - offset_callee: u32, - allocated_stack: u32, -} - -#[cfg_attr( - not(all(target_aarch = "aarch64", target_vendor = "apple")), - allow(dead_code) -)] -impl Aarch64Apple { - // Integral arguments go to the first 8 GPR: x0-x7 - const INTEGRAL_REGISTERS: u32 = 8; - // Floating-point arguments go to the first 8 SIMD & Floating-Point registers: v0-v1 - const FLOAT_REGISTERS: u32 = 8; - - fn new() -> Self { - Self { - assmblr: dynasmrt::aarch64::Assembler::new().unwrap(), - integral_params: 0, - float_params: 0, - offset_trampoline: 0, - offset_callee: 0, - allocated_stack: 0, - } - } - - fn compile(sym: &Symbol) -> Trampoline { - let mut compiler = Self::new(); - - let must_wrap_return_value = - compiler.must_wrap_return_value_in_typed_array(sym.result_type); - let must_save_preserved_register = must_wrap_return_value; - let cannot_tailcall = must_wrap_return_value; - - if cannot_tailcall { - compiler.allocate_stack(sym); - compiler.save_frame_record(); - if compiler.must_save_preserved_register_to_stack(sym) { - compiler.save_preserved_register_to_stack(); - } - } - - for param in sym.parameter_types.iter().copied() { - compiler.move_left(param) - } - if !compiler.is_recv_arg_overridden() { - // the receiver object should never be expected. Avoid its unexpected or deliberate leak - compiler.zero_first_arg(); - } - if compiler.must_wrap_return_value_in_typed_array(sym.result_type) { - compiler.save_out_array_to_preserved_register(); - } - - if cannot_tailcall { - compiler.call(sym.ptr.as_ptr()); - if must_wrap_return_value { - compiler.wrap_return_value_in_out_array(); - } - if must_save_preserved_register { - compiler.recover_preserved_register(); - } - compiler.recover_frame_record(); - compiler.deallocate_stack(); - compiler.ret(); - } else { - compiler.tailcall(sym.ptr.as_ptr()); - } - - Trampoline(compiler.finalize()) - } - - fn move_left(&mut self, param: NativeType) { - // Section 6.4.2 of the Aarch64 Procedure Call Standard (PCS), on argument classification: - // - INTEGRAL or POINTER: - // > If the argument is an Integral or Pointer Type, the size of the argument is less than or equal to 8 bytes - // > and the NGRN is less than 8, the argument is copied to the least significant bits in x[NGRN]. - // - // - Floating-Point or Vector: - // > If the argument is a Half-, Single-, Double- or Quad- precision Floating-point or short vector type - // > and the NSRN is less than 8, then the argument is allocated to the least significant bits of register v[NSRN] - match param.into() { - Int(integral) => self.move_integral(integral), - Float(float) => self.move_float(float), - } - } - - fn move_float(&mut self, param: Floating) { - // Section 6.4.2 of the Aarch64 PCS: - // > If the argument is a Half-, Single-, Double- or Quad- precision Floating-point or short vector type and the NSRN is less than 8, then the - // > argument is allocated to the least significant bits of register v[NSRN]. The NSRN is incremented by one. The argument has now been allocated. - // > [if NSRN is equal or more than 8] - // > The argument is copied to memory at the adjusted NSAA. The NSAA is incremented by the size of the argument. The argument has now been allocated. - let param_i = self.float_params; - - let is_in_stack = param_i >= Self::FLOAT_REGISTERS; - if is_in_stack { - // https://developer.apple.com/documentation/xcode/writing-arm64-code-for-apple-platforms: - // > Function arguments may consume slots on the stack that are not multiples of 8 bytes. - // (i.e. natural alignment instead of eightbyte alignment) - let padding_trampl = - (param.size() - self.offset_trampoline % param.size()) % param.size(); - let padding_callee = - (param.size() - self.offset_callee % param.size()) % param.size(); - - // floats are only moved to accommodate integer movement in the stack - let stack_has_moved = self.integral_params >= Self::INTEGRAL_REGISTERS; - if stack_has_moved { - let s = &mut self.assmblr; - let ot = self.offset_trampoline; - let oc = self.offset_callee; - match param { - Single => aarch64!(s - // 6.1.2 Aarch64 PCS: - // > Registers v8-v15 must be preserved by a callee across subroutine calls; - // > the remaining registers (v0-v7, v16-v31) do not need to be preserved (or should be preserved by the caller). - ; ldr s16, [sp, ot + padding_trampl] - ; str s16, [sp, oc + padding_callee] - ), - Double => aarch64!(s - ; ldr d16, [sp, ot + padding_trampl] - ; str d16, [sp, oc + padding_callee] - ), - } - } - self.offset_trampoline += padding_trampl + param.size(); - self.offset_callee += padding_callee + param.size(); - - debug_assert!( - self.allocated_stack == 0 || self.offset_callee <= self.allocated_stack - ); - } - self.float_params += 1; - } - - fn move_integral(&mut self, param: Integral) { - let s = &mut self.assmblr; - // Section 6.4.2 of the Aarch64 PCS: - // If the argument is an Integral or Pointer Type, the size of the argument is less than or - // equal to 8 bytes and the NGRN is less than 8, the argument is copied to the least - // significant bits in x[NGRN]. The NGRN is incremented by one. The argument has now been - // allocated. - // [if NGRN is equal or more than 8] - // The argument is copied to memory at the adjusted NSAA. The NSAA is incremented by the size - // of the argument. The argument has now been allocated. - let param_i = self.integral_params; - - // move each argument one position to the left. The first argument in the stack moves to the last integer register (x7). - match (param_i, param) { - // From https://developer.apple.com/documentation/xcode/writing-arm64-code-for-apple-platforms: - // > The caller of a function is responsible for signing or zero-extending any argument with fewer than 32 bits. - // > The standard ABI expects the callee to sign or zero-extend those arguments. - // (this applies to register parameters, as stack parameters are not eightbyte aligned in Apple) - (0, I(B)) => aarch64!(s; sxtb w0, w1), - (0, U(B)) => aarch64!(s; and w0, w1, 0xFF), - (0, I(W)) => aarch64!(s; sxth w0, w1), - (0, U(W)) => aarch64!(s; and w0, w1, 0xFFFF), - (0, I(DW) | U(DW)) => aarch64!(s; mov w0, w1), - (0, I(QW) | U(QW)) => aarch64!(s; mov x0, x1), - // The fast API expects buffer arguments passed as a pointer to a FastApiTypedArray struct - // Here we blindly follow the layout of https://github.com/denoland/rusty_v8/blob/main/src/fast_api.rs#L190-L200 - // although that might be problematic: https://discord.com/channels/684898665143206084/956626010248478720/1009450940866252823 - (0, Buffer) => aarch64!(s; ldr x0, [x1, 8]), - - (1, I(B)) => aarch64!(s; sxtb w1, w2), - (1, U(B)) => aarch64!(s; and w1, w2, 0xFF), - (1, I(W)) => aarch64!(s; sxth w1, w2), - (1, U(W)) => aarch64!(s; and w1, w2, 0xFFFF), - (1, I(DW) | U(DW)) => aarch64!(s; mov w1, w2), - (1, I(QW) | U(QW)) => aarch64!(s; mov x1, x2), - (1, Buffer) => aarch64!(s; ldr x1, [x2, 8]), - - (2, I(B)) => aarch64!(s; sxtb w2, w3), - (2, U(B)) => aarch64!(s; and w2, w3, 0xFF), - (2, I(W)) => aarch64!(s; sxth w2, w3), - (2, U(W)) => aarch64!(s; and w2, w3, 0xFFFF), - (2, I(DW) | U(DW)) => aarch64!(s; mov w2, w3), - (2, I(QW) | U(QW)) => aarch64!(s; mov x2, x3), - (2, Buffer) => aarch64!(s; ldr x2, [x3, 8]), - - (3, I(B)) => aarch64!(s; sxtb w3, w4), - (3, U(B)) => aarch64!(s; and w3, w4, 0xFF), - (3, I(W)) => aarch64!(s; sxth w3, w4), - (3, U(W)) => aarch64!(s; and w3, w4, 0xFFFF), - (3, I(DW) | U(DW)) => aarch64!(s; mov w3, w4), - (3, I(QW) | U(QW)) => aarch64!(s; mov x3, x4), - (3, Buffer) => aarch64!(s; ldr x3, [x4, 8]), - - (4, I(B)) => aarch64!(s; sxtb w4, w5), - (4, U(B)) => aarch64!(s; and w4, w5, 0xFF), - (4, I(W)) => aarch64!(s; sxth w4, w5), - (4, U(W)) => aarch64!(s; and w4, w5, 0xFFFF), - (4, I(DW) | U(DW)) => aarch64!(s; mov w4, w5), - (4, I(QW) | U(QW)) => aarch64!(s; mov x4, x5), - (4, Buffer) => aarch64!(s; ldr x4, [x5, 8]), - - (5, I(B)) => aarch64!(s; sxtb w5, w6), - (5, U(B)) => aarch64!(s; and w5, w6, 0xFF), - (5, I(W)) => aarch64!(s; sxth w5, w6), - (5, U(W)) => aarch64!(s; and w5, w6, 0xFFFF), - (5, I(DW) | U(DW)) => aarch64!(s; mov w5, w6), - (5, I(QW) | U(QW)) => aarch64!(s; mov x5, x6), - (5, Buffer) => aarch64!(s; ldr x5, [x6, 8]), - - (6, I(B)) => aarch64!(s; sxtb w6, w7), - (6, U(B)) => aarch64!(s; and w6, w7, 0xFF), - (6, I(W)) => aarch64!(s; sxth w6, w7), - (6, U(W)) => aarch64!(s; and w6, w7, 0xFFFF), - (6, I(DW) | U(DW)) => aarch64!(s; mov w6, w7), - (6, I(QW) | U(QW)) => aarch64!(s; mov x6, x7), - (6, Buffer) => aarch64!(s; ldr x6, [x7, 8]), - - (7, param) => { - let ot = self.offset_trampoline; - match param { - I(B) => { - aarch64!(s; ldrsb w7, [sp, ot]) - } - U(B) => { - // ldrb zero-extends the byte to fill the 32bits of the register - aarch64!(s; ldrb w7, [sp, ot]) - } - I(W) => { - aarch64!(s; ldrsh w7, [sp, ot]) - } - U(W) => { - // ldrh zero-extends the half-word to fill the 32bits of the register - aarch64!(s; ldrh w7, [sp, ot]) - } - I(DW) | U(DW) => { - aarch64!(s; ldr w7, [sp, ot]) - } - I(QW) | U(QW) => { - aarch64!(s; ldr x7, [sp, ot]) - } - Buffer => { - aarch64!(s - ; ldr x7, [sp, ot] - ; ldr x7, [x7, 8] - ) - } - } - // 16 and 8 bit integers are 32 bit integers in v8 - self.offset_trampoline += max(param.size(), 4); - } - - (8.., param) => { - // https://developer.apple.com/documentation/xcode/writing-arm64-code-for-apple-platforms: - // > Function arguments may consume slots on the stack that are not multiples of 8 bytes. - // (i.e. natural alignment instead of eightbyte alignment) - // - // N.B. V8 does not currently follow this Apple's policy, and instead aligns all arguments to 8 Byte boundaries. - // The current implementation follows the V8 incorrect calling convention for the sake of a seamless experience - // for the Deno users. Whenever upgrading V8 we should make sure that the bug has not been amended, and revert this - // workaround once it has been. The bug is being tracked in https://bugs.chromium.org/p/v8/issues/detail?id=13171 - let size_original = param.size(); - // 16 and 8 bit integers are 32 bit integers in v8 - // let size_trampl = max(size_original, 4); // <-- Apple alignment - let size_trampl = 8; // <-- V8 incorrect alignment - let padding_trampl = - padding_to_align(size_trampl, self.offset_trampoline); - let padding_callee = - padding_to_align(size_original, self.offset_callee); - let ot = self.offset_trampoline; - let oc = self.offset_callee; - match param { - I(B) | U(B) => aarch64!(s - ; ldr w8, [sp, ot + padding_trampl] - ; strb w8, [sp, oc + padding_callee] - ), - I(W) | U(W) => aarch64!(s - ; ldr w8, [sp, ot + padding_trampl] - ; strh w8, [sp, oc + padding_callee] - ), - I(DW) | U(DW) => aarch64!(s - ; ldr w8, [sp, ot + padding_trampl] - ; str w8, [sp, oc + padding_callee] - ), - I(QW) | U(QW) => aarch64!(s - ; ldr x8, [sp, ot + padding_trampl] - ; str x8, [sp, oc + padding_callee] - ), - Buffer => aarch64!(s - ; ldr x8, [sp, ot + padding_trampl] - ; ldr x8, [x8, 8] - ; str x8, [sp, oc + padding_callee] - ), - } - self.offset_trampoline += padding_trampl + size_trampl; - self.offset_callee += padding_callee + size_original; - - debug_assert!( - self.allocated_stack == 0 - || self.offset_callee <= self.allocated_stack - ); - } - }; - self.integral_params += 1; - } - - fn zero_first_arg(&mut self) { - debug_assert!( - self.integral_params == 0, - "the trampoline would zero the first argument after having overridden it with the second one" - ); - aarch64!(self.assmblr; mov x0, xzr); - } - - fn save_out_array_to_preserved_register(&mut self) { - let s = &mut self.assmblr; - // functions returning 64 bit integers have the out array appended as their last parameter, - // and it is a *FastApiTypedArray - match self.integral_params { - // x0 is always V8's receiver - 0 => aarch64!(s; ldr x19, [x1, 8]), - 1 => aarch64!(s; ldr x19, [x2, 8]), - 2 => aarch64!(s; ldr x19, [x3, 8]), - 3 => aarch64!(s; ldr x19, [x4, 8]), - 4 => aarch64!(s; ldr x19, [x5, 8]), - 5 => aarch64!(s; ldr x19, [x6, 8]), - 6 => aarch64!(s; ldr x19, [x7, 8]), - 7.. => { - aarch64!(s - ; ldr x19, [sp, self.offset_trampoline] - ; ldr x19, [x19, 8] - ) - } - } - } - - fn wrap_return_value_in_out_array(&mut self) { - aarch64!(self.assmblr; str x0, [x19]); - } - - fn save_frame_record(&mut self) { - debug_assert!( - self.allocated_stack >= 16, - "the trampoline would try to save the frame record to the stack without having allocated enough space for it" - ); - aarch64!(self.assmblr - // Frame record is stored at the bottom of the stack frame - ; stp x29, x30, [sp, self.allocated_stack - 16] - ; add x29, sp, self.allocated_stack - 16 - ) - } - - fn recover_frame_record(&mut self) { - // The stack cannot have been deallocated before the frame record is restored - debug_assert!( - self.allocated_stack >= 16, - "the trampoline would try to load the frame record from the stack, but it couldn't possibly contain it" - ); - // Frame record is stored at the bottom of the stack frame - aarch64!(self.assmblr; ldp x29, x30, [sp, self.allocated_stack - 16]) - } - - fn save_preserved_register_to_stack(&mut self) { - // If a preserved register needs to be used, we must have allocated at least 32 bytes in the stack - // 16 for the frame record, 8 for the preserved register, and 8 for 16-byte alignment. - debug_assert!( - self.allocated_stack >= 32, - "the trampoline would try to save a register to the stack without having allocated enough space for it" - ); - // preserved register is stored after frame record - aarch64!(self.assmblr; str x19, [sp, self.allocated_stack - 24]); - } - - fn recover_preserved_register(&mut self) { - // The stack cannot have been deallocated before the preserved register is restored - // 16 for the frame record, 8 for the preserved register, and 8 for 16-byte alignment. - debug_assert!( - self.allocated_stack >= 32, - "the trampoline would try to recover the value of a register from the stack, but it couldn't possibly contain it" - ); - // preserved register is stored after frame record - aarch64!(self.assmblr; ldr x19, [sp, self.allocated_stack - 24]); - } - - fn allocate_stack(&mut self, symbol: &Symbol) { - // https://developer.apple.com/documentation/xcode/writing-arm64-code-for-apple-platforms: - // > Function arguments may consume slots on the stack that are not multiples of 8 bytes. - // (i.e. natural alignment instead of eightbyte alignment) - let mut int_params = 0u32; - let mut float_params = 0u32; - let mut stack_size = 0u32; - for param in symbol.parameter_types.iter().copied() { - match param.into() { - Float(float_param) => { - float_params += 1; - if float_params > Self::FLOAT_REGISTERS { - stack_size += float_param.size(); - } - } - Int(integral_param) => { - int_params += 1; - if int_params > Self::INTEGRAL_REGISTERS { - stack_size += integral_param.size(); - } - } - } - } - - // Section 6.2.3 of the Aarch64 PCS: - // > Each frame shall link to the frame of its caller by means of a frame record of two 64-bit values on the stack - stack_size += 16; - - if self.must_save_preserved_register_to_stack(symbol) { - stack_size += 8; - } - - // Section 6.2.2 of Aarch64 PCS: - // > At any point at which memory is accessed via SP, the hardware requires that - // > - SP mod 16 = 0. The stack must be quad-word aligned. - // > The stack must also conform to the following constraint at a public interface: - // > - SP mod 16 = 0. The stack must be quad-word aligned. - stack_size += padding_to_align(16, stack_size); - - if stack_size > 0 { - aarch64!(self.assmblr; sub sp, sp, stack_size); - self.offset_trampoline += stack_size; - // stack pointer has been modified, and the callee stack parameters are expected at the top of the stack - self.offset_callee = 0; - self.allocated_stack += stack_size; - } - } - - fn deallocate_stack(&mut self) { - if self.allocated_stack > 0 { - aarch64!(self.assmblr; add sp, sp, self.allocated_stack); - self.allocated_stack = 0; - } - } - - fn call(&mut self, ptr: *const c_void) { - // the stack has been aligned during stack allocation - // Frame record has been stored in stack and frame pointer points to it - debug_assert!( - self.allocated_stack % 16 == 0, - "the trampoline would call the FFI function with an unaligned stack" - ); - debug_assert!( - self.allocated_stack >= 16, - "the trampoline would call the FFI function without allocating enough stack for the frame record" - ); - self.load_callee_address(ptr); - aarch64!(self.assmblr; blr x8); - } - - fn tailcall(&mut self, ptr: *const c_void) { - // stack pointer is never modified and remains aligned - // frame pointer and link register remain the one provided by the trampoline's caller (V8) - debug_assert!( - self.allocated_stack == 0, - "the trampoline would tail call the FFI function with an outstanding stack allocation" - ); - self.load_callee_address(ptr); - aarch64!(self.assmblr; br x8); - } - - fn ret(&mut self) { - debug_assert!( - self.allocated_stack == 0, - "the trampoline would return with an outstanding stack allocation" - ); - aarch64!(self.assmblr; ret); - } - - fn load_callee_address(&mut self, ptr: *const c_void) { - // Like all ARM instructions, move instructions are 32bit long and can fit at most 16bit immediates. - // bigger immediates are loaded in multiple steps applying a left-shift modifier - let mut address = ptr as u64; - let mut imm16 = address & 0xFFFF; - aarch64!(self.assmblr; movz x8, imm16 as u32); - address >>= 16; - let mut shift = 16; - while address > 0 { - imm16 = address & 0xFFFF; - if imm16 != 0 { - aarch64!(self.assmblr; movk x8, imm16 as u32, lsl shift); - } - address >>= 16; - shift += 16; - } - } - - fn is_recv_arg_overridden(&self) -> bool { - // V8 receiver is the first parameter of the trampoline function and is a pointer - self.integral_params > 0 - } - - fn must_save_preserved_register_to_stack(&mut self, symbol: &Symbol) -> bool { - self.must_wrap_return_value_in_typed_array(symbol.result_type) - } - - fn must_wrap_return_value_in_typed_array(&self, rv: NativeType) -> bool { - // V8 only supports i32 and u32 return types for integers - // We support 64 bit integers by wrapping them in a TypedArray out parameter - crate::needs_unwrap(rv) - } - - fn finalize(self) -> ExecutableBuffer { - self.assmblr.finalize().unwrap() - } -} - -struct Win64 { - // Reference: https://github.com/MicrosoftDocs/cpp-docs/blob/main/docs/build/x64-calling-convention.md - assmblr: dynasmrt::x64::Assembler, - // Params counter (Windows does not distinguish by type with regards to parameter position) - params: u32, - // Stack offset accumulators - offset_trampoline: u32, - offset_callee: u32, - allocated_stack: u32, - frame_pointer: u32, -} - -#[cfg_attr( - not(all(target_aarch = "x86_64", target_family = "windows")), - allow(dead_code) -)] -impl Win64 { - // Section "Parameter Passing" of the Windows x64 calling convention: - // > By default, the x64 calling convention passes the first four arguments to a function in registers. - const REGISTERS: u32 = 4; - - fn new() -> Self { - Self { - assmblr: dynasmrt::x64::Assembler::new().unwrap(), - params: 0, - // trampoline caller's return address + trampoline's shadow space - offset_trampoline: 8 + 32, - offset_callee: 8 + 32, - allocated_stack: 0, - frame_pointer: 0, - } - } - - fn compile(sym: &Symbol) -> Trampoline { - let mut compiler = Self::new(); - - let must_cast_return_value = - compiler.must_cast_return_value(sym.result_type); - let must_wrap_return_value = - compiler.must_wrap_return_value_in_typed_array(sym.result_type); - let must_save_preserved_register = must_wrap_return_value; - let cannot_tailcall = must_cast_return_value || must_wrap_return_value; - - if cannot_tailcall { - if must_save_preserved_register { - compiler.save_preserved_register_to_stack(); - } - compiler.allocate_stack(&sym.parameter_types); - } - - for param in sym.parameter_types.iter().copied() { - compiler.move_left(param) - } - if !compiler.is_recv_arg_overridden() { - // the receiver object should never be expected. Avoid its unexpected or deliberate leak - compiler.zero_first_arg(); - } - if must_wrap_return_value { - compiler.save_out_array_to_preserved_register(); - } - - if cannot_tailcall { - compiler.call(sym.ptr.as_ptr()); - if must_cast_return_value { - compiler.cast_return_value(sym.result_type); - } - if must_wrap_return_value { - compiler.wrap_return_value_in_out_array(); - } - compiler.deallocate_stack(); - if must_save_preserved_register { - compiler.recover_preserved_register(); - } - compiler.ret(); - } else { - compiler.tailcall(sym.ptr.as_ptr()); - } - - Trampoline(compiler.finalize()) - } - - fn move_left(&mut self, param: NativeType) { - // Section "Parameter Passing" of the Windows x64 calling convention: - // > By default, the x64 calling convention passes the first four arguments to a function in registers. - // > The registers used for these arguments depend on the position and type of the argument. - // > Remaining arguments get pushed on the stack in right-to-left order. - // > [...] - // > Integer valued arguments in the leftmost four positions are passed in left-to-right order in RCX, RDX, R8, and R9 - // > [...] - // > Any floating-point and double-precision arguments in the first four parameters are passed in XMM0 - XMM3, depending on position - let s = &mut self.assmblr; - let param_i = self.params; - - // move each argument one position to the left. The first argument in the stack moves to the last register (r9 or xmm3). - // If the FFI function is called with a new stack frame, the arguments remaining in the stack are copied to the new stack frame. - // Otherwise, they are copied 8 bytes lower in the same frame - match (param_i, param.into()) { - // Section "Parameter Passing" of the Windows x64 calling convention: - // > All integer arguments in registers are right-justified, so the callee can ignore the upper bits of the register - // > and access only the portion of the register necessary. - // (i.e. unlike in SysV or Aarch64-Apple, 8/16 bit integers are not expected to be zero/sign extended) - (0, Int(U(B | W | DW) | I(B | W | DW))) => x64!(s; mov ecx, edx), - (0, Int(U(QW) | I(QW))) => x64!(s; mov rcx, rdx), - // The fast API expects buffer arguments passed as a pointer to a FastApiTypedArray struct - // Here we blindly follow the layout of https://github.com/denoland/rusty_v8/blob/main/src/fast_api.rs#L190-L200 - // although that might be problematic: https://discord.com/channels/684898665143206084/956626010248478720/1009450940866252823 - (0, Int(Buffer)) => x64!(s; mov rcx, [rdx + 8]), - // Use movaps for singles and doubles, benefits of smaller encoding outweigh those of using the correct instruction for the type, - // which for doubles should technically be movapd - (0, Float(_)) => { - x64!(s; movaps xmm0, xmm1); - self.zero_first_arg(); - } - - (1, Int(U(B | W | DW) | I(B | W | DW))) => x64!(s; mov edx, r8d), - (1, Int(U(QW) | I(QW))) => x64!(s; mov rdx, r8), - (1, Int(Buffer)) => x64!(s; mov rdx, [r8 + 8]), - (1, Float(_)) => x64!(s; movaps xmm1, xmm2), - - (2, Int(U(B | W | DW) | I(B | W | DW))) => x64!(s; mov r8d, r9d), - (2, Int(U(QW) | I(QW))) => x64!(s; mov r8, r9), - (2, Int(Buffer)) => x64!(s; mov r8, [r9 + 8]), - (2, Float(_)) => x64!(s; movaps xmm2, xmm3), - - (3, param) => { - let ot = self.offset_trampoline as i32; - match param { - Int(U(B | W | DW) | I(B | W | DW)) => { - x64!(s; mov r9d, [rsp + ot]) - } - Int(U(QW) | I(QW)) => { - x64!(s; mov r9, [rsp + ot]) - } - Int(Buffer) => { - x64!(s - ; mov r9, [rsp + ot] - ; mov r9, [r9 + 8]) - } - Float(_) => { - // parameter 4 is always 16-byte aligned, so we can use movaps instead of movups - x64!(s; movaps xmm3, [rsp + ot]) - } - } - // Section "x64 Aggregate and Union layout" of the windows x64 software conventions doc: - // > The alignment of the beginning of a structure or a union is the maximum alignment of any individual member - // Ref: https://github.com/MicrosoftDocs/cpp-docs/blob/main/docs/build/x64-software-conventions.md#x64-aggregate-and-union-layout - self.offset_trampoline += 8; - } - (4.., param) => { - let ot = self.offset_trampoline as i32; - let oc = self.offset_callee as i32; - match param { - Int(U(B | W | DW) | I(B | W | DW)) => { - x64!(s - ; mov eax, [rsp + ot] - ; mov [rsp + oc], eax - ) - } - Int(U(QW) | I(QW)) => { - x64!(s - ; mov rax, [rsp + ot] - ; mov [rsp + oc], rax - ) - } - Int(Buffer) => { - x64!(s - ; mov rax, [rsp + ot] - ; mov rax, [rax + 8] - ; mov [rsp + oc], rax - ) - } - Float(_) => { - x64!(s - ; movups xmm4, [rsp + ot] - ; movups [rsp + oc], xmm4 - ) - } - } - // Section "x64 Aggregate and Union layout" of the windows x64 software conventions doc: - // > The alignment of the beginning of a structure or a union is the maximum alignment of any individual member - // Ref: https://github.com/MicrosoftDocs/cpp-docs/blob/main/docs/build/x64-software-conventions.md#x64-aggregate-and-union-layout - self.offset_trampoline += 8; - self.offset_callee += 8; - - debug_assert!( - self.allocated_stack == 0 - || self.offset_callee <= self.allocated_stack - ); - } - } - self.params += 1; - } - - fn zero_first_arg(&mut self) { - debug_assert!( - self.params == 0, - "the trampoline would zero the first argument after having overridden it with the second one" - ); - x64!(self.assmblr; xor ecx, ecx); - } - - fn cast_return_value(&mut self, rv: NativeType) { - let s = &mut self.assmblr; - // V8 only supports 32bit integers. We support 8 and 16 bit integers casting them to 32bits. - // Section "Return Values" of the Windows x64 Calling Convention doc: - // > The state of unused bits in the value returned in RAX or XMM0 is undefined. - match rv { - NativeType::U8 => x64!(s; movzx eax, al), - NativeType::I8 => x64!(s; movsx eax, al), - NativeType::U16 => x64!(s; movzx eax, ax), - NativeType::I16 => x64!(s; movsx eax, ax), - _ => (), - } - } - - fn save_out_array_to_preserved_register(&mut self) { - let s = &mut self.assmblr; - // functions returning 64 bit integers have the out array appended as their last parameter, - // and it is a *FastApiTypedArray - match self.params { - // rcx is always V8 receiver - 0 => x64!(s; mov rbx, [rdx + 8]), - 1 => x64!(s; mov rbx, [r8 + 8]), - 2 => x64!(s; mov rbx, [r9 + 8]), - 3.. => { - x64!(s - ; mov rax, [rsp + self.offset_trampoline as i32] - ; mov rbx, [rax + 8] - ) - } - } - } - - fn wrap_return_value_in_out_array(&mut self) { - x64!(self.assmblr; mov [rbx], rax) - } - - fn save_preserved_register_to_stack(&mut self) { - x64!(self.assmblr; push rbx); - self.offset_trampoline += 8; - // stack pointer has been modified, and the callee stack parameters are expected at the top of the stack - self.offset_callee = 0; - self.frame_pointer += 8; - } - - fn recover_preserved_register(&mut self) { - debug_assert!( - self.frame_pointer >= 8, - "the trampoline would try to pop from the stack beyond its frame pointer" - ); - x64!(self.assmblr; pop rbx); - self.frame_pointer -= 8; - // parameter offsets are invalid once this method is called - } - - fn allocate_stack(&mut self, params: &[NativeType]) { - let mut stack_size = 0; - // Section "Calling Convention Defaults" of the x64-calling-convention and Section "Stack Allocation" of the stack-usage docs: - // > The x64 Application Binary Interface (ABI) uses a four-register fast-call calling convention by default. - // > Space is allocated on the call stack as a shadow store for callees to save those registers. - // > [...] - // > Any parameters beyond the first four must be stored on the stack after the shadow store before the call - // > [...] - // > Even if the called function has fewer than 4 parameters, these 4 stack locations are effectively owned by the called function, - // > and may be used by the called function for other purposes besides saving parameter register values - stack_size += max(params.len() as u32, 4) * 8; - - // Align new stack frame (accounting for the 8 byte of the trampoline caller's return address - // and any other potential addition to the stack prior to this allocation) - // Section "Stack Allocation" of stack-usage docs: - // > The stack will always be maintained 16-byte aligned, except within the prolog (for example, after the return address is pushed) - stack_size += padding_to_align(16, self.frame_pointer + stack_size + 8); - - x64!(self.assmblr; sub rsp, stack_size as i32); - self.offset_trampoline += stack_size; - // stack pointer has been modified, and the callee stack parameters are expected at the top of the stack right after the shadow space - self.offset_callee = 32; - self.allocated_stack += stack_size; - self.frame_pointer += stack_size; - } - - fn deallocate_stack(&mut self) { - debug_assert!( - self.frame_pointer >= self.allocated_stack, - "the trampoline would try to deallocate stack beyond its frame pointer" - ); - x64!(self.assmblr; add rsp, self.allocated_stack as i32); - self.frame_pointer -= self.allocated_stack; - self.allocated_stack = 0; - } - - fn call(&mut self, ptr: *const c_void) { - // the stack has been aligned during stack allocation and/or pushing of preserved registers - debug_assert!( - (8 + self.frame_pointer) % 16 == 0, - "the trampoline would call the FFI function with an unaligned stack" - ); - x64!(self.assmblr - ; mov rax, QWORD ptr as _ - ; call rax - ); - } - - fn tailcall(&mut self, ptr: *const c_void) { - // stack pointer is never modified and remains aligned - // return address remains the one provided by the trampoline's caller (V8) - debug_assert!( - self.allocated_stack == 0, - "the trampoline would tail call the FFI function with an outstanding stack allocation" - ); - debug_assert!( - self.frame_pointer == 0, - "the trampoline would tail call the FFI function with outstanding locals in the frame" - ); - x64!(self.assmblr - ; mov rax, QWORD ptr as _ - ; jmp rax - ); - } - - fn ret(&mut self) { - debug_assert!( - self.allocated_stack == 0, - "the trampoline would return with an outstanding stack allocation" - ); - debug_assert!( - self.frame_pointer == 0, - "the trampoline would return with outstanding locals in the frame" - ); - x64!(self.assmblr; ret); - } - - fn is_recv_arg_overridden(&self) -> bool { - self.params > 0 - } - - fn must_cast_return_value(&self, rv: NativeType) -> bool { - // V8 only supports i32 and u32 return types for integers - // We support 8 and 16 bit integers by extending them to 32 bits in the trampoline before returning - matches!( - rv, - NativeType::U8 | NativeType::I8 | NativeType::U16 | NativeType::I16 - ) - } - - fn must_wrap_return_value_in_typed_array(&self, rv: NativeType) -> bool { - // V8 only supports i32 and u32 return types for integers - // We support 64 bit integers by wrapping them in a TypedArray out parameter - crate::needs_unwrap(rv) - } - - fn finalize(self) -> ExecutableBuffer { - self.assmblr.finalize().unwrap() - } -} - -fn padding_to_align(alignment: u32, size: u32) -> u32 { - (alignment - size % alignment) % alignment -} - -#[derive(Clone, Copy, Debug)] -enum Floating { - Single = 4, - Double = 8, -} - -impl Floating { - fn size(self) -> u32 { - self as u32 - } -} - -use Floating::*; - -#[derive(Clone, Copy, Debug)] -enum Integral { - I(Size), - U(Size), - Buffer, -} - -impl Integral { - fn size(self) -> u32 { - match self { - I(size) | U(size) => size as u32, - Buffer => 8, - } - } -} - -use Integral::*; - -#[derive(Clone, Copy, Debug)] -enum Size { - B = 1, - W = 2, - DW = 4, - QW = 8, -} -use Size::*; - -#[allow(clippy::enum_variant_names)] -#[derive(Clone, Copy, Debug)] -enum Param { - Int(Integral), - Float(Floating), -} - -use Param::*; - -impl From for Param { - fn from(native: NativeType) -> Self { - match native { - NativeType::F32 => Float(Single), - NativeType::F64 => Float(Double), - NativeType::Bool | NativeType::U8 => Int(U(B)), - NativeType::U16 => Int(U(W)), - NativeType::U32 | NativeType::Void => Int(U(DW)), - NativeType::U64 - | NativeType::USize - | NativeType::Pointer - | NativeType::Function => Int(U(QW)), - NativeType::I8 => Int(I(B)), - NativeType::I16 => Int(I(W)), - NativeType::I32 => Int(I(DW)), - NativeType::I64 | NativeType::ISize => Int(I(QW)), - NativeType::Buffer => Int(Buffer), - } - } -} - -#[cfg(test)] -mod tests { - use std::ptr::null_mut; - - use libffi::middle::Type; - - use crate::NativeType; - use crate::Symbol; - - fn symbol(parameters: Vec, ret: NativeType) -> Symbol { - Symbol { - cif: libffi::middle::Cif::new(vec![], Type::void()), - ptr: libffi::middle::CodePtr(null_mut()), - parameter_types: parameters, - result_type: ret, - can_callback: false, - } - } - - mod sysv_amd64 { - use std::ops::Deref; - - use dynasmrt::dynasm; - use dynasmrt::DynasmApi; - - use super::super::SysVAmd64; - use super::symbol; - use crate::NativeType::*; - - #[test] - fn tailcall() { - let trampoline = SysVAmd64::compile(&symbol( - vec![ - U8, U16, I16, I8, U32, U64, Buffer, Function, I64, I32, I16, I8, F32, - F32, F32, F32, F64, F64, F64, F64, F32, F64, - ], - Void, - )); - - let mut assembler = dynasmrt::x64::Assembler::new().unwrap(); - // See https://godbolt.org/z/KE9x1h9xq - dynasm!(assembler - ; .arch x64 - ; movzx edi, sil // u8 - ; movzx esi, dx // u16 - ; movsx edx, cx // i16 - ; movsx ecx, r8b // i8 - ; mov r8d, r9d // u32 - ; mov r9, [DWORD rsp + 8] // u64 - ; mov rax, [DWORD rsp + 16] // Buffer - ; mov rax, [rax + 8] // .. - ; mov [DWORD rsp + 8], rax // .. - ; mov rax, [DWORD rsp + 24] // Function - ; mov [DWORD rsp + 16], rax // .. - ; mov rax, [DWORD rsp + 32] // i64 - ; mov [DWORD rsp + 24], rax // .. - ; mov eax, [DWORD rsp + 40] // i32 - ; mov [DWORD rsp + 32], eax // .. - ; movsx eax, WORD [DWORD rsp + 48] // i16 - ; mov [DWORD rsp + 40], eax // .. - ; movsx eax, BYTE [DWORD rsp + 56] // i8 - ; mov [DWORD rsp + 48], eax // .. - ; movss xmm8, [DWORD rsp + 64] // f32 - ; movss [DWORD rsp + 56], xmm8 // .. - ; movsd xmm8, [DWORD rsp + 72] // f64 - ; movsd [DWORD rsp + 64], xmm8 // .. - ; mov rax, QWORD 0 - ; jmp rax - ); - let expected = assembler.finalize().unwrap(); - assert_eq!(trampoline.0.deref(), expected.deref()); - } - - #[test] - fn integer_casting() { - let trampoline = SysVAmd64::compile(&symbol( - vec![U8, U16, I8, I16, U8, U16, I8, I16, U8, U16, I8, I16], - I8, - )); - - let mut assembler = dynasmrt::x64::Assembler::new().unwrap(); - // See https://godbolt.org/z/qo59bPsfv - dynasm!(assembler - ; .arch x64 - ; sub rsp, DWORD 56 // stack allocation - ; movzx edi, sil // u8 - ; movzx esi, dx // u16 - ; movsx edx, cl // i8 - ; movsx ecx, r8w // i16 - ; movzx r8d, r9b // u8 - ; movzx r9d, WORD [DWORD rsp + 64] // u16 - ; movsx eax, BYTE [DWORD rsp + 72] // i8 - ; mov [DWORD rsp + 0], eax // .. - ; movsx eax, WORD [DWORD rsp + 80] // i16 - ; mov [DWORD rsp + 8], eax // .. - ; movzx eax, BYTE [DWORD rsp + 88] // u8 - ; mov [DWORD rsp + 16], eax // .. - ; movzx eax, WORD [DWORD rsp + 96] // u16 - ; mov [DWORD rsp + 24], eax // .. - ; movsx eax, BYTE [DWORD rsp + 104] // i8 - ; mov [DWORD rsp + 32], eax // .. - ; movsx eax, WORD [DWORD rsp + 112] // i16 - ; mov [DWORD rsp + 40], eax // .. - ; mov rax, QWORD 0 - ; call rax - ; movsx eax, al // return value cast - ; add rsp, DWORD 56 // stack deallocation - ; ret - ); - let expected = assembler.finalize().unwrap(); - assert_eq!(trampoline.0.deref(), expected.deref()); - } - - #[test] - fn buffer_parameters() { - let trampoline = SysVAmd64::compile(&symbol( - vec![ - Buffer, Buffer, Buffer, Buffer, Buffer, Buffer, Buffer, Buffer, - ], - Void, - )); - - let mut assembler = dynasmrt::x64::Assembler::new().unwrap(); - // See https://godbolt.org/z/hqv63M3Ko - dynasm!(assembler - ; .arch x64 - ; mov rdi, [rsi + 8] // Buffer - ; mov rsi, [rdx + 8] // Buffer - ; mov rdx, [rcx + 8] // Buffer - ; mov rcx, [r8 + 8] // Buffer - ; mov r8, [r9 + 8] // Buffer - ; mov r9, [DWORD rsp + 8] // Buffer - ; mov r9, [r9 + 8] // .. - ; mov rax, [DWORD rsp + 16] // Buffer - ; mov rax, [rax + 8] // .. - ; mov [DWORD rsp + 8], rax // .. - ; mov rax, [DWORD rsp + 24] // Buffer - ; mov rax, [rax + 8] // .. - ; mov [DWORD rsp + 16], rax // .. - ; mov rax, QWORD 0 - ; jmp rax - ); - let expected = assembler.finalize().unwrap(); - assert_eq!(trampoline.0.deref(), expected.deref()); - } - - #[test] - fn return_u64_in_register_typed_array() { - let trampoline = SysVAmd64::compile(&symbol(vec![], U64)); - - let mut assembler = dynasmrt::x64::Assembler::new().unwrap(); - // See https://godbolt.org/z/8G7a488o7 - dynasm!(assembler - ; .arch x64 - ; push rbx - ; xor edi, edi // recv - ; mov rbx, [rsi + 8] // save data array pointer to non-volatile register - ; mov rax, QWORD 0 - ; call rax - ; mov [rbx], rax // copy return value to data pointer address - ; pop rbx - ; ret - ); - let expected = assembler.finalize().unwrap(); - assert_eq!(trampoline.0.deref(), expected.deref()); - } - - #[test] - fn return_u64_in_stack_typed_array() { - let trampoline = SysVAmd64::compile(&symbol( - vec![U64, U64, U64, U64, U64, U64, U64], - U64, - )); - - let mut assembler = dynasmrt::x64::Assembler::new().unwrap(); - // See https://godbolt.org/z/cPnPYWdWq - dynasm!(assembler - ; .arch x64 - ; push rbx - ; sub rsp, DWORD 16 - ; mov rdi, rsi // u64 - ; mov rsi, rdx // u64 - ; mov rdx, rcx // u64 - ; mov rcx, r8 // u64 - ; mov r8, r9 // u64 - ; mov r9, [DWORD rsp + 32] // u64 - ; mov rax, [DWORD rsp + 40] // u64 - ; mov [DWORD rsp + 0], rax // .. - ; mov rax, [DWORD rsp + 48] // save data array pointer to non-volatile register - ; mov rbx, [rax + 8] // .. - ; mov rax, QWORD 0 - ; call rax - ; mov [rbx], rax // copy return value to data pointer address - ; add rsp, DWORD 16 - ; pop rbx - ; ret - ); - let expected = assembler.finalize().unwrap(); - assert_eq!(trampoline.0.deref(), expected.deref()); - } - } - - mod aarch64_apple { - use std::ops::Deref; - - use dynasmrt::dynasm; - - use super::super::Aarch64Apple; - use super::symbol; - use crate::NativeType::*; - - #[test] - fn tailcall() { - let trampoline = Aarch64Apple::compile(&symbol( - vec![ - U8, U16, I16, I8, U32, U64, Buffer, Function, I64, I32, I16, I8, F32, - F32, F32, F32, F64, F64, F64, F64, F32, F64, - ], - Void, - )); - - let mut assembler = dynasmrt::aarch64::Assembler::new().unwrap(); - // See https://godbolt.org/z/oefqYWT13 - dynasm!(assembler - ; .arch aarch64 - ; and w0, w1, 0xFF // u8 - ; and w1, w2, 0xFFFF // u16 - ; sxth w2, w3 // i16 - ; sxtb w3, w4 // i8 - ; mov w4, w5 // u32 - ; mov x5, x6 // u64 - ; ldr x6, [x7, 8] // Buffer - ; ldr x7, [sp] // Function - ; ldr x8, [sp, 8] // i64 - ; str x8, [sp] // .. - ; ldr w8, [sp, 16] // i32 - ; str w8, [sp, 8] // .. - ; ldr w8, [sp, 24] // i16 - ; strh w8, [sp, 12] // .. - ; ldr w8, [sp, 32] // i8 - ; strb w8, [sp, 14] // .. - ; ldr s16, [sp, 40] // f32 - ; str s16, [sp, 16] // .. - ; ldr d16, [sp, 48] // f64 - ; str d16, [sp, 24] // .. - ; movz x8, 0 - ; br x8 - ); - let expected = assembler.finalize().unwrap(); - assert_eq!(trampoline.0.deref(), expected.deref()); - } - - #[test] - fn integer_casting() { - let trampoline = Aarch64Apple::compile(&symbol( - vec![U8, U16, I8, I16, U8, U16, I8, I16, U8, U16, I8, I16], - I8, - )); - - let mut assembler = dynasmrt::aarch64::Assembler::new().unwrap(); - // See https://godbolt.org/z/7qfzbzobM - dynasm!(assembler - ; .arch aarch64 - ; and w0, w1, 0xFF // u8 - ; and w1, w2, 0xFFFF // u16 - ; sxtb w2, w3 // i8 - ; sxth w3, w4 // i16 - ; and w4, w5, 0xFF // u8 - ; and w5, w6, 0xFFFF // u16 - ; sxtb w6, w7 // i8 - ; ldrsh w7, [sp] // i16 - ; ldr w8, [sp, 8] // u8 - ; strb w8, [sp] // .. - ; ldr w8, [sp, 16] // u16 - ; strh w8, [sp, 2] // .. - ; ldr w8, [sp, 24] // i8 - ; strb w8, [sp, 4] // .. - ; ldr w8, [sp, 32] // i16 - ; strh w8, [sp, 6] // .. - ; movz x8, 0 - ; br x8 - ); - let expected = assembler.finalize().unwrap(); - assert_eq!(trampoline.0.deref(), expected.deref()); - } - - #[test] - fn buffer_parameters() { - let trampoline = Aarch64Apple::compile(&symbol( - vec![ - Buffer, Buffer, Buffer, Buffer, Buffer, Buffer, Buffer, Buffer, - Buffer, Buffer, - ], - Void, - )); - - let mut assembler = dynasmrt::aarch64::Assembler::new().unwrap(); - // See https://godbolt.org/z/obd6z6vsf - dynasm!(assembler - ; .arch aarch64 - ; ldr x0, [x1, 8] // Buffer - ; ldr x1, [x2, 8] // Buffer - ; ldr x2, [x3, 8] // Buffer - ; ldr x3, [x4, 8] // Buffer - ; ldr x4, [x5, 8] // Buffer - ; ldr x5, [x6, 8] // Buffer - ; ldr x6, [x7, 8] // Buffer - ; ldr x7, [sp] // Buffer - ; ldr x7, [x7, 8] // .. - ; ldr x8, [sp, 8] // Buffer - ; ldr x8, [x8, 8] // .. - ; str x8, [sp] // .. - ; ldr x8, [sp, 16] // Buffer - ; ldr x8, [x8, 8] // .. - ; str x8, [sp, 8] // .. - ; movz x8, 0 - ; br x8 - ); - let expected = assembler.finalize().unwrap(); - assert_eq!(trampoline.0.deref(), expected.deref()); - } - - #[test] - fn return_u64_in_register_typed_array() { - let trampoline = Aarch64Apple::compile(&symbol(vec![], U64)); - - let mut assembler = dynasmrt::aarch64::Assembler::new().unwrap(); - // See https://godbolt.org/z/47EvvYb83 - dynasm!(assembler - ; .arch aarch64 - ; sub sp, sp, 32 - ; stp x29, x30, [sp, 16] - ; add x29, sp, 16 - ; str x19, [sp, 8] - ; mov x0, xzr // recv - ; ldr x19, [x1, 8] // save data array pointer to non-volatile register - ; movz x8, 0 - ; blr x8 - ; str x0, [x19] // copy return value to data pointer address - ; ldr x19, [sp, 8] - ; ldp x29, x30, [sp, 16] - ; add sp, sp, 32 - ; ret - ); - let expected = assembler.finalize().unwrap(); - assert_eq!(trampoline.0.deref(), expected.deref()); - } - - #[test] - fn return_u64_in_stack_typed_array() { - let trampoline = Aarch64Apple::compile(&symbol( - vec![U64, U64, U64, U64, U64, U64, U64, U64, U8, U8], - U64, - )); - - let mut assembler = dynasmrt::aarch64::Assembler::new().unwrap(); - // See https://godbolt.org/z/PvYPbsE1b - dynasm!(assembler - ; .arch aarch64 - ; sub sp, sp, 32 - ; stp x29, x30, [sp, 16] - ; add x29, sp, 16 - ; str x19, [sp, 8] - ; mov x0, x1 // u64 - ; mov x1, x2 // u64 - ; mov x2, x3 // u64 - ; mov x3, x4 // u64 - ; mov x4, x5 // u64 - ; mov x5, x6 // u64 - ; mov x6, x7 // u64 - ; ldr x7, [sp, 32] // u64 - ; ldr w8, [sp, 40] // u8 - ; strb w8, [sp] // .. - ; ldr w8, [sp, 48] // u8 - ; strb w8, [sp, 1] // .. - ; ldr x19, [sp, 56] // save data array pointer to non-volatile register - ; ldr x19, [x19, 8] // .. - ; movz x8, 0 - ; blr x8 - ; str x0, [x19] // copy return value to data pointer address - ; ldr x19, [sp, 8] - ; ldp x29, x30, [sp, 16] - ; add sp, sp, 32 - ; ret - ); - let expected = assembler.finalize().unwrap(); - assert_eq!(trampoline.0.deref(), expected.deref()); - } - } - - mod x64_windows { - use std::ops::Deref; - - use dynasmrt::{dynasm, DynasmApi}; - - use super::super::Win64; - use super::symbol; - use crate::NativeType::*; - - #[test] - fn tailcall() { - let trampoline = - Win64::compile(&symbol(vec![U8, I16, F64, F32, U32, I8, Buffer], Void)); - - let mut assembler = dynasmrt::x64::Assembler::new().unwrap(); - // See https://godbolt.org/z/TYzqrf9aj - dynasm!(assembler - ; .arch x64 - ; mov ecx, edx // u8 - ; mov edx, r8d // i16 - ; movaps xmm2, xmm3 // f64 - ; movaps xmm3, [DWORD rsp + 40] // f32 - ; mov eax, [DWORD rsp + 48] // u32 - ; mov [DWORD rsp + 40], eax // .. - ; mov eax, [DWORD rsp + 56] // i8 - ; mov [DWORD rsp + 48], eax // .. - ; mov rax, [DWORD rsp + 64] // Buffer - ; mov rax, [rax + 8] // .. - ; mov [DWORD rsp + 56], rax // .. - ; mov rax, QWORD 0 - ; jmp rax - ); - let expected = assembler.finalize().unwrap(); - assert_eq!(trampoline.0.deref(), expected.deref()); - } - - #[test] - fn integer_casting() { - let trampoline = Win64::compile(&symbol( - vec![U8, U16, I8, I16, U8, U16, I8, I16, U8, U16, I8, I16], - I8, - )); - - let mut assembler = dynasmrt::x64::Assembler::new().unwrap(); - // See https://godbolt.org/z/KMx56KGTq - dynasm!(assembler - ; .arch x64 - ; sub rsp, DWORD 104 // stack allocation - ; mov ecx, edx // u8 - ; mov edx, r8d // u16 - ; mov r8d, r9d // i8 - ; mov r9d, [DWORD rsp + 144] // i16 - ; mov eax, [DWORD rsp + 152] // u8 - ; mov [DWORD rsp + 32], eax // .. - ; mov eax, [DWORD rsp + 160] // u16 - ; mov [DWORD rsp + 40], eax // u16 - ; mov eax, [DWORD rsp + 168] // i8 - ; mov [DWORD rsp + 48], eax // .. - ; mov eax, [DWORD rsp + 176] // i16 - ; mov [DWORD rsp + 56], eax // .. - ; mov eax, [DWORD rsp + 184] // u8 - ; mov [DWORD rsp + 64], eax // .. - ; mov eax, [DWORD rsp + 192] // u16 - ; mov [DWORD rsp + 72], eax // .. - ; mov eax, [DWORD rsp + 200] // i8 - ; mov [DWORD rsp + 80], eax // .. - ; mov eax, [DWORD rsp + 208] // i16 - ; mov [DWORD rsp + 88], eax // .. - ; mov rax, QWORD 0 - ; call rax - ; movsx eax, al // return value cast - ; add rsp, DWORD 104 // stack deallocation - ; ret - ); - let expected = assembler.finalize().unwrap(); - assert_eq!(trampoline.0.deref(), expected.deref()); - } - - #[test] - fn buffer_parameters() { - let trampoline = Win64::compile(&symbol( - vec![Buffer, Buffer, Buffer, Buffer, Buffer, Buffer], - Void, - )); - - let mut assembler = dynasmrt::x64::Assembler::new().unwrap(); - // See https://godbolt.org/z/TYzqrf9aj - dynasm!(assembler - ; .arch x64 - ; mov rcx, [rdx + 8] // Buffer - ; mov rdx, [r8 + 8] // Buffer - ; mov r8, [r9 + 8] // Buffer - ; mov r9, [DWORD rsp + 40] // Buffer - ; mov r9, [r9 + 8] // .. - ; mov rax, [DWORD rsp + 48] // Buffer - ; mov rax, [rax + 8] // .. - ; mov [DWORD rsp + 40], rax // .. - ; mov rax, [DWORD rsp + 56] // Buffer - ; mov rax, [rax + 8] // .. - ; mov [DWORD rsp + 48], rax // .. - ; mov rax, QWORD 0 - ; jmp rax - ); - let expected = assembler.finalize().unwrap(); - assert_eq!(trampoline.0.deref(), expected.deref()); - } - - #[test] - fn return_u64_in_register_typed_array() { - let trampoline = Win64::compile(&symbol(vec![], U64)); - - let mut assembler = dynasmrt::x64::Assembler::new().unwrap(); - // See https://godbolt.org/z/7EnPE7o3T - dynasm!(assembler - ; .arch x64 - ; push rbx - ; sub rsp, DWORD 32 - ; xor ecx, ecx // recv - ; mov rbx, [rdx + 8] // save data array pointer to non-volatile register - ; mov rax, QWORD 0 - ; call rax - ; mov [rbx], rax // copy return value to data pointer address - ; add rsp, DWORD 32 - ; pop rbx - ; ret - ); - let expected = assembler.finalize().unwrap(); - assert_eq!(trampoline.0.deref(), expected.deref()); - } - - #[test] - fn return_u64_in_stack_typed_array() { - let trampoline = - Win64::compile(&symbol(vec![U64, U64, U64, U64, U64], U64)); - - let mut assembler = dynasmrt::x64::Assembler::new().unwrap(); - // See https://godbolt.org/z/3966sfEex - dynasm!(assembler - ; .arch x64 - ; push rbx - ; sub rsp, DWORD 48 - ; mov rcx, rdx // u64 - ; mov rdx, r8 // u64 - ; mov r8, r9 // u64 - ; mov r9, [DWORD rsp + 96] // u64 - ; mov rax, [DWORD rsp + 104] // u64 - ; mov [DWORD rsp + 32], rax // .. - ; mov rax, [DWORD rsp + 112] // save data array pointer to non-volatile register - ; mov rbx, [rax + 8] // .. - ; mov rax, QWORD 0 - ; call rax - ; mov [rbx], rax // copy return value to data pointer address - ; add rsp, DWORD 48 - ; pop rbx - ; ret - ); - let expected = assembler.finalize().unwrap(); - assert_eq!(trampoline.0.deref(), expected.deref()); - } - } -} diff --git a/ext/ffi/ir.rs b/ext/ffi/ir.rs new file mode 100644 index 000000000..67c65b5b5 --- /dev/null +++ b/ext/ffi/ir.rs @@ -0,0 +1,527 @@ +// Copyright 2018-2022 the Deno authors. All rights reserved. MIT license. + +use crate::symbol::NativeType; +use crate::MAX_SAFE_INTEGER; +use crate::MIN_SAFE_INTEGER; +use deno_core::error::type_error; +use deno_core::error::AnyError; +use deno_core::serde_json::Value; +use deno_core::serde_v8; +use deno_core::v8; +use libffi::middle::Arg; +use std::ffi::c_void; +use std::ptr; + +/// Intermediate format for easy translation from NativeType + V8 value +/// to libffi argument types. +#[repr(C)] +pub union NativeValue { + pub void_value: (), + pub bool_value: bool, + pub u8_value: u8, + pub i8_value: i8, + pub u16_value: u16, + pub i16_value: i16, + pub u32_value: u32, + pub i32_value: i32, + pub u64_value: u64, + pub i64_value: i64, + pub usize_value: usize, + pub isize_value: isize, + pub f32_value: f32, + pub f64_value: f64, + pub pointer: *mut c_void, +} + +impl NativeValue { + pub unsafe fn as_arg(&self, native_type: NativeType) -> Arg { + match native_type { + NativeType::Void => unreachable!(), + NativeType::Bool => Arg::new(&self.bool_value), + NativeType::U8 => Arg::new(&self.u8_value), + NativeType::I8 => Arg::new(&self.i8_value), + NativeType::U16 => Arg::new(&self.u16_value), + NativeType::I16 => Arg::new(&self.i16_value), + NativeType::U32 => Arg::new(&self.u32_value), + NativeType::I32 => Arg::new(&self.i32_value), + NativeType::U64 => Arg::new(&self.u64_value), + NativeType::I64 => Arg::new(&self.i64_value), + NativeType::USize => Arg::new(&self.usize_value), + NativeType::ISize => Arg::new(&self.isize_value), + NativeType::F32 => Arg::new(&self.f32_value), + NativeType::F64 => Arg::new(&self.f64_value), + NativeType::Pointer | NativeType::Buffer | NativeType::Function => { + Arg::new(&self.pointer) + } + } + } + + // SAFETY: native_type must correspond to the type of value represented by the union field + pub unsafe fn to_value(&self, native_type: NativeType) -> Value { + match native_type { + NativeType::Void => Value::Null, + NativeType::Bool => Value::from(self.bool_value), + NativeType::U8 => Value::from(self.u8_value), + NativeType::I8 => Value::from(self.i8_value), + NativeType::U16 => Value::from(self.u16_value), + NativeType::I16 => Value::from(self.i16_value), + NativeType::U32 => Value::from(self.u32_value), + NativeType::I32 => Value::from(self.i32_value), + NativeType::U64 => Value::from(self.u64_value), + NativeType::I64 => Value::from(self.i64_value), + NativeType::USize => Value::from(self.usize_value), + NativeType::ISize => Value::from(self.isize_value), + NativeType::F32 => Value::from(self.f32_value), + NativeType::F64 => Value::from(self.f64_value), + NativeType::Pointer | NativeType::Function | NativeType::Buffer => { + Value::from(self.pointer as usize) + } + } + } + + // SAFETY: native_type must correspond to the type of value represented by the union field + #[inline] + pub unsafe fn to_v8<'scope>( + &self, + scope: &mut v8::HandleScope<'scope>, + native_type: NativeType, + ) -> serde_v8::Value<'scope> { + match native_type { + NativeType::Void => { + let local_value: v8::Local = v8::undefined(scope).into(); + local_value.into() + } + NativeType::Bool => { + let local_value: v8::Local = + v8::Boolean::new(scope, self.bool_value).into(); + local_value.into() + } + NativeType::U8 => { + let local_value: v8::Local = + v8::Integer::new_from_unsigned(scope, self.u8_value as u32).into(); + local_value.into() + } + NativeType::I8 => { + let local_value: v8::Local = + v8::Integer::new(scope, self.i8_value as i32).into(); + local_value.into() + } + NativeType::U16 => { + let local_value: v8::Local = + v8::Integer::new_from_unsigned(scope, self.u16_value as u32).into(); + local_value.into() + } + NativeType::I16 => { + let local_value: v8::Local = + v8::Integer::new(scope, self.i16_value as i32).into(); + local_value.into() + } + NativeType::U32 => { + let local_value: v8::Local = + v8::Integer::new_from_unsigned(scope, self.u32_value).into(); + local_value.into() + } + NativeType::I32 => { + let local_value: v8::Local = + v8::Integer::new(scope, self.i32_value).into(); + local_value.into() + } + NativeType::U64 => { + let value = self.u64_value; + let local_value: v8::Local = + if value > MAX_SAFE_INTEGER as u64 { + v8::BigInt::new_from_u64(scope, value).into() + } else { + v8::Number::new(scope, value as f64).into() + }; + local_value.into() + } + NativeType::I64 => { + let value = self.i64_value; + let local_value: v8::Local = + if value > MAX_SAFE_INTEGER as i64 || value < MIN_SAFE_INTEGER as i64 + { + v8::BigInt::new_from_i64(scope, self.i64_value).into() + } else { + v8::Number::new(scope, value as f64).into() + }; + local_value.into() + } + NativeType::USize => { + let value = self.usize_value; + let local_value: v8::Local = + if value > MAX_SAFE_INTEGER as usize { + v8::BigInt::new_from_u64(scope, value as u64).into() + } else { + v8::Number::new(scope, value as f64).into() + }; + local_value.into() + } + NativeType::ISize => { + let value = self.isize_value; + let local_value: v8::Local = + if !(MIN_SAFE_INTEGER..=MAX_SAFE_INTEGER).contains(&value) { + v8::BigInt::new_from_i64(scope, self.isize_value as i64).into() + } else { + v8::Number::new(scope, value as f64).into() + }; + local_value.into() + } + NativeType::F32 => { + let local_value: v8::Local = + v8::Number::new(scope, self.f32_value as f64).into(); + local_value.into() + } + NativeType::F64 => { + let local_value: v8::Local = + v8::Number::new(scope, self.f64_value).into(); + local_value.into() + } + NativeType::Pointer | NativeType::Buffer | NativeType::Function => { + let value = self.pointer as u64; + let local_value: v8::Local = + if value > MAX_SAFE_INTEGER as u64 { + v8::BigInt::new_from_u64(scope, value).into() + } else { + v8::Number::new(scope, value as f64).into() + }; + local_value.into() + } + } + } +} + +// SAFETY: unsafe trait must have unsafe implementation +unsafe impl Send for NativeValue {} + +#[inline] +pub fn ffi_parse_bool_arg( + arg: v8::Local, +) -> Result { + let bool_value = v8::Local::::try_from(arg) + .map_err(|_| type_error("Invalid FFI u8 type, expected boolean"))? + .is_true(); + Ok(NativeValue { bool_value }) +} + +#[inline] +pub fn ffi_parse_u8_arg( + arg: v8::Local, +) -> Result { + let u8_value = v8::Local::::try_from(arg) + .map_err(|_| type_error("Invalid FFI u8 type, expected unsigned integer"))? + .value() as u8; + Ok(NativeValue { u8_value }) +} + +#[inline] +pub fn ffi_parse_i8_arg( + arg: v8::Local, +) -> Result { + let i8_value = v8::Local::::try_from(arg) + .map_err(|_| type_error("Invalid FFI i8 type, expected integer"))? + .value() as i8; + Ok(NativeValue { i8_value }) +} + +#[inline] +pub fn ffi_parse_u16_arg( + arg: v8::Local, +) -> Result { + let u16_value = v8::Local::::try_from(arg) + .map_err(|_| type_error("Invalid FFI u16 type, expected unsigned integer"))? + .value() as u16; + Ok(NativeValue { u16_value }) +} + +#[inline] +pub fn ffi_parse_i16_arg( + arg: v8::Local, +) -> Result { + let i16_value = v8::Local::::try_from(arg) + .map_err(|_| type_error("Invalid FFI i16 type, expected integer"))? + .value() as i16; + Ok(NativeValue { i16_value }) +} + +#[inline] +pub fn ffi_parse_u32_arg( + arg: v8::Local, +) -> Result { + let u32_value = v8::Local::::try_from(arg) + .map_err(|_| type_error("Invalid FFI u32 type, expected unsigned integer"))? + .value() as u32; + Ok(NativeValue { u32_value }) +} + +#[inline] +pub fn ffi_parse_i32_arg( + arg: v8::Local, +) -> Result { + let i32_value = v8::Local::::try_from(arg) + .map_err(|_| type_error("Invalid FFI i32 type, expected integer"))? + .value() as i32; + Ok(NativeValue { i32_value }) +} + +#[inline] +pub fn ffi_parse_u64_arg( + scope: &mut v8::HandleScope, + arg: v8::Local, +) -> Result { + // Order of checking: + // 1. BigInt: Uncommon and not supported by Fast API, so optimise slow call for this case. + // 2. Number: Common, supported by Fast API, so let that be the optimal case. + let u64_value: u64 = if let Ok(value) = v8::Local::::try_from(arg) + { + value.u64_value().0 + } else if let Ok(value) = v8::Local::::try_from(arg) { + value.integer_value(scope).unwrap() as u64 + } else { + return Err(type_error( + "Invalid FFI u64 type, expected unsigned integer", + )); + }; + Ok(NativeValue { u64_value }) +} + +#[inline] +pub fn ffi_parse_i64_arg( + scope: &mut v8::HandleScope, + arg: v8::Local, +) -> Result { + // Order of checking: + // 1. BigInt: Uncommon and not supported by Fast API, so optimise slow call for this case. + // 2. Number: Common, supported by Fast API, so let that be the optimal case. + let i64_value: i64 = if let Ok(value) = v8::Local::::try_from(arg) + { + value.i64_value().0 + } else if let Ok(value) = v8::Local::::try_from(arg) { + value.integer_value(scope).unwrap() as i64 + } else { + return Err(type_error("Invalid FFI i64 type, expected integer")); + }; + Ok(NativeValue { i64_value }) +} + +#[inline] +pub fn ffi_parse_usize_arg( + scope: &mut v8::HandleScope, + arg: v8::Local, +) -> Result { + // Order of checking: + // 1. BigInt: Uncommon and not supported by Fast API, so optimise slow call for this case. + // 2. Number: Common, supported by Fast API, so let that be the optimal case. + let usize_value: usize = + if let Ok(value) = v8::Local::::try_from(arg) { + value.u64_value().0 as usize + } else if let Ok(value) = v8::Local::::try_from(arg) { + value.integer_value(scope).unwrap() as usize + } else { + return Err(type_error("Invalid FFI usize type, expected integer")); + }; + Ok(NativeValue { usize_value }) +} + +#[inline] +pub fn ffi_parse_isize_arg( + scope: &mut v8::HandleScope, + arg: v8::Local, +) -> Result { + // Order of checking: + // 1. BigInt: Uncommon and not supported by Fast API, so optimise slow call for this case. + // 2. Number: Common, supported by Fast API, so let that be the optimal case. + let isize_value: isize = + if let Ok(value) = v8::Local::::try_from(arg) { + value.i64_value().0 as isize + } else if let Ok(value) = v8::Local::::try_from(arg) { + value.integer_value(scope).unwrap() as isize + } else { + return Err(type_error("Invalid FFI isize type, expected integer")); + }; + Ok(NativeValue { isize_value }) +} + +#[inline] +pub fn ffi_parse_f32_arg( + arg: v8::Local, +) -> Result { + let f32_value = v8::Local::::try_from(arg) + .map_err(|_| type_error("Invalid FFI f32 type, expected number"))? + .value() as f32; + Ok(NativeValue { f32_value }) +} + +#[inline] +pub fn ffi_parse_f64_arg( + arg: v8::Local, +) -> Result { + let f64_value = v8::Local::::try_from(arg) + .map_err(|_| type_error("Invalid FFI f64 type, expected number"))? + .value() as f64; + Ok(NativeValue { f64_value }) +} + +#[inline] +pub fn ffi_parse_pointer_arg( + scope: &mut v8::HandleScope, + arg: v8::Local, +) -> Result { + // Order of checking: + // 1. BigInt: Uncommon and not supported by Fast API, optimise this case. + // 2. Number: Common and supported by Fast API. + // 3. Null: Very uncommon / can be represented by a 0. + let pointer = if let Ok(value) = v8::Local::::try_from(arg) { + value.u64_value().0 as usize as *mut c_void + } else if let Ok(value) = v8::Local::::try_from(arg) { + value.integer_value(scope).unwrap() as usize as *mut c_void + } else if arg.is_null() { + ptr::null_mut() + } else { + return Err(type_error( + "Invalid FFI pointer type, expected null, integer or BigInt", + )); + }; + Ok(NativeValue { pointer }) +} + +#[inline] +pub fn ffi_parse_buffer_arg( + scope: &mut v8::HandleScope, + arg: v8::Local, +) -> Result { + // Order of checking: + // 1. ArrayBuffer: Fairly common and not supported by Fast API, optimise this case. + // 2. ArrayBufferView: Common and supported by Fast API + // 5. Null: Very uncommon / can be represented by a 0. + + let pointer = if let Ok(value) = v8::Local::::try_from(arg) { + if let Some(non_null) = value.data() { + non_null.as_ptr() + } else { + ptr::null_mut() + } + } else if let Ok(value) = v8::Local::::try_from(arg) { + let byte_offset = value.byte_offset(); + let pointer = value + .buffer(scope) + .ok_or_else(|| { + type_error("Invalid FFI ArrayBufferView, expected data in the buffer") + })? + .data(); + if let Some(non_null) = pointer { + // SAFETY: Pointer is non-null, and V8 guarantees that the byte_offset + // is within the buffer backing store. + unsafe { non_null.as_ptr().add(byte_offset) } + } else { + ptr::null_mut() + } + } else if arg.is_null() { + ptr::null_mut() + } else { + return Err(type_error( + "Invalid FFI buffer type, expected null, ArrayBuffer, or ArrayBufferView", + )); + }; + Ok(NativeValue { pointer }) +} + +#[inline] +pub fn ffi_parse_function_arg( + scope: &mut v8::HandleScope, + arg: v8::Local, +) -> Result { + // Order of checking: + // 1. BigInt: Uncommon and not supported by Fast API, optimise this case. + // 2. Number: Common and supported by Fast API, optimise this case as second. + // 3. Null: Very uncommon / can be represented by a 0. + let pointer = if let Ok(value) = v8::Local::::try_from(arg) { + value.u64_value().0 as usize as *mut c_void + } else if let Ok(value) = v8::Local::::try_from(arg) { + value.integer_value(scope).unwrap() as usize as *mut c_void + } else if arg.is_null() { + ptr::null_mut() + } else { + return Err(type_error( + "Invalid FFI function type, expected null, integer, or BigInt", + )); + }; + Ok(NativeValue { pointer }) +} + +pub fn ffi_parse_args<'scope>( + scope: &mut v8::HandleScope<'scope>, + args: serde_v8::Value<'scope>, + parameter_types: &[NativeType], +) -> Result, AnyError> +where + 'scope: 'scope, +{ + if parameter_types.is_empty() { + return Ok(vec![]); + } + + let args = v8::Local::::try_from(args.v8_value) + .map_err(|_| type_error("Invalid FFI parameters, expected Array"))?; + let mut ffi_args: Vec = + Vec::with_capacity(parameter_types.len()); + + for (index, native_type) in parameter_types.iter().enumerate() { + let value = args.get_index(scope, index as u32).unwrap(); + match native_type { + NativeType::Bool => { + ffi_args.push(ffi_parse_bool_arg(value)?); + } + NativeType::U8 => { + ffi_args.push(ffi_parse_u8_arg(value)?); + } + NativeType::I8 => { + ffi_args.push(ffi_parse_i8_arg(value)?); + } + NativeType::U16 => { + ffi_args.push(ffi_parse_u16_arg(value)?); + } + NativeType::I16 => { + ffi_args.push(ffi_parse_i16_arg(value)?); + } + NativeType::U32 => { + ffi_args.push(ffi_parse_u32_arg(value)?); + } + NativeType::I32 => { + ffi_args.push(ffi_parse_i32_arg(value)?); + } + NativeType::U64 => { + ffi_args.push(ffi_parse_u64_arg(scope, value)?); + } + NativeType::I64 => { + ffi_args.push(ffi_parse_i64_arg(scope, value)?); + } + NativeType::USize => { + ffi_args.push(ffi_parse_usize_arg(scope, value)?); + } + NativeType::ISize => { + ffi_args.push(ffi_parse_isize_arg(scope, value)?); + } + NativeType::F32 => { + ffi_args.push(ffi_parse_f32_arg(value)?); + } + NativeType::F64 => { + ffi_args.push(ffi_parse_f64_arg(value)?); + } + NativeType::Buffer => { + ffi_args.push(ffi_parse_buffer_arg(scope, value)?); + } + NativeType::Pointer => { + ffi_args.push(ffi_parse_pointer_arg(scope, value)?); + } + NativeType::Function => { + ffi_args.push(ffi_parse_function_arg(scope, value)?); + } + NativeType::Void => { + unreachable!(); + } + } + } + + Ok(ffi_args) +} diff --git a/ext/ffi/lib.rs b/ext/ffi/lib.rs index 7e7756c93..f3a906505 100644 --- a/ext/ffi/lib.rs +++ b/ext/ffi/lib.rs @@ -1,47 +1,41 @@ // Copyright 2018-2022 the Deno authors. All rights reserved. MIT license. -use core::ptr::NonNull; -use deno_core::anyhow::anyhow; -use deno_core::error::generic_error; -use deno_core::error::range_error; -use deno_core::error::type_error; use deno_core::error::AnyError; use deno_core::futures::channel::mpsc; -use deno_core::futures::Future; use deno_core::include_js_files; -use deno_core::op; -use deno_core::serde_json::Value; -use deno_core::serde_v8; use deno_core::v8; -use deno_core::CancelFuture; -use deno_core::CancelHandle; use deno_core::Extension; use deno_core::OpState; -use deno_core::Resource; -use deno_core::ResourceId; -use dlopen::raw::Library; -use libffi::middle::Arg; -use libffi::middle::Cif; -use serde::Deserialize; -use std::borrow::Cow; + use std::cell::RefCell; -use std::collections::HashMap; -use std::ffi::c_void; -use std::ffi::CStr; -use std::future::IntoFuture; use std::mem::size_of; use std::os::raw::c_char; use std::os::raw::c_short; use std::path::Path; -use std::path::PathBuf; -use std::pin::Pin; use std::ptr; use std::rc::Rc; -use std::sync::mpsc::sync_channel; -use std::task::Poll; -use std::task::Waker; -mod fast_call; +mod call; +mod callback; +mod dlfcn; +mod ir; +mod repr; +mod r#static; +mod symbol; +mod turbocall; + +use call::op_ffi_call_nonblocking; +use call::op_ffi_call_ptr; +use call::op_ffi_call_ptr_nonblocking; +use callback::op_ffi_unsafe_callback_create; +use callback::op_ffi_unsafe_callback_ref; +use callback::op_ffi_unsafe_callback_unref; +use dlfcn::op_ffi_load; +use dlfcn::ForeignFunction; +use r#static::op_ffi_get_static; +use repr::*; +use symbol::NativeType; +use symbol::Symbol; #[cfg(not(target_pointer_width = "64"))] compile_error!("platform not supported"); @@ -56,8 +50,8 @@ thread_local! { static LOCAL_ISOLATE_POINTER: RefCell<*const v8::Isolate> = RefCell::new(ptr::null()); } -const MAX_SAFE_INTEGER: isize = 9007199254740991; -const MIN_SAFE_INTEGER: isize = -9007199254740991; +pub(crate) const MAX_SAFE_INTEGER: isize = 9007199254740991; +pub(crate) const MIN_SAFE_INTEGER: isize = -9007199254740991; pub struct Unstable(pub bool); @@ -82,86 +76,11 @@ pub trait FfiPermissions { fn check(&mut self, path: Option<&Path>) -> Result<(), AnyError>; } -#[derive(Clone)] -struct Symbol { - cif: libffi::middle::Cif, - ptr: libffi::middle::CodePtr, - parameter_types: Vec, - result_type: NativeType, - can_callback: bool, -} - -#[allow(clippy::non_send_fields_in_send_ty)] -// SAFETY: unsafe trait must have unsafe implementation -unsafe impl Send for Symbol {} -// SAFETY: unsafe trait must have unsafe implementation -unsafe impl Sync for Symbol {} - -#[derive(Clone)] -struct PtrSymbol { - cif: libffi::middle::Cif, - ptr: libffi::middle::CodePtr, -} - -impl PtrSymbol { - fn new(fn_ptr: usize, def: &ForeignFunction) -> Self { - let ptr = libffi::middle::CodePtr::from_ptr(fn_ptr as _); - let cif = libffi::middle::Cif::new( - def - .parameters - .clone() - .into_iter() - .map(libffi::middle::Type::from), - def.result.into(), - ); - - Self { cif, ptr } - } -} - -#[allow(clippy::non_send_fields_in_send_ty)] -// SAFETY: unsafe trait must have unsafe implementation -unsafe impl Send for PtrSymbol {} -// SAFETY: unsafe trait must have unsafe implementation -unsafe impl Sync for PtrSymbol {} - -struct DynamicLibraryResource { - lib: Library, - symbols: HashMap>, -} - -impl Resource for DynamicLibraryResource { - fn name(&self) -> Cow { - "dynamicLibrary".into() - } +pub(crate) type PendingFfiAsyncWork = Box; - fn close(self: Rc) { - drop(self) - } -} - -impl DynamicLibraryResource { - fn get_static(&self, symbol: String) -> Result<*const c_void, AnyError> { - // By default, Err returned by this function does not tell - // which symbol wasn't exported. So we'll modify the error - // message to include the name of symbol. - // - // SAFETY: The obtained T symbol is the size of a pointer. - match unsafe { self.lib.symbol::<*const c_void>(&symbol) } { - Ok(value) => Ok(Ok(value)), - Err(err) => Err(generic_error(format!( - "Failed to register symbol {}: {}", - symbol, err - ))), - }? - } -} - -type PendingFfiAsyncWork = Box; - -struct FfiState { - async_work_sender: mpsc::UnboundedSender, - async_work_receiver: mpsc::UnboundedReceiver, +pub(crate) struct FfiState { + pub(crate) async_work_sender: mpsc::UnboundedSender, + pub(crate) async_work_receiver: mpsc::UnboundedReceiver, } pub fn init(unstable: bool) -> Extension { @@ -237,2295 +156,3 @@ pub fn init(unstable: bool) -> Extension { }) .build() } - -/// Defines the accepted types that can be used as -/// parameters and return values in FFI. -#[derive(Clone, Copy, Debug, Deserialize, Eq, PartialEq)] -#[serde(rename_all = "lowercase")] -enum NativeType { - Void, - Bool, - U8, - I8, - U16, - I16, - U32, - I32, - U64, - I64, - USize, - ISize, - F32, - F64, - Pointer, - Buffer, - Function, -} - -impl From for libffi::middle::Type { - fn from(native_type: NativeType) -> Self { - match native_type { - NativeType::Void => libffi::middle::Type::void(), - NativeType::U8 | NativeType::Bool => libffi::middle::Type::u8(), - NativeType::I8 => libffi::middle::Type::i8(), - NativeType::U16 => libffi::middle::Type::u16(), - NativeType::I16 => libffi::middle::Type::i16(), - NativeType::U32 => libffi::middle::Type::u32(), - NativeType::I32 => libffi::middle::Type::i32(), - NativeType::U64 => libffi::middle::Type::u64(), - NativeType::I64 => libffi::middle::Type::i64(), - NativeType::USize => libffi::middle::Type::usize(), - NativeType::ISize => libffi::middle::Type::isize(), - NativeType::F32 => libffi::middle::Type::f32(), - NativeType::F64 => libffi::middle::Type::f64(), - NativeType::Pointer | NativeType::Buffer | NativeType::Function => { - libffi::middle::Type::pointer() - } - } - } -} - -/// Intermediate format for easy translation from NativeType + V8 value -/// to libffi argument types. -#[repr(C)] -union NativeValue { - void_value: (), - bool_value: bool, - u8_value: u8, - i8_value: i8, - u16_value: u16, - i16_value: i16, - u32_value: u32, - i32_value: i32, - u64_value: u64, - i64_value: i64, - usize_value: usize, - isize_value: isize, - f32_value: f32, - f64_value: f64, - pointer: *mut c_void, -} - -impl NativeValue { - unsafe fn as_arg(&self, native_type: NativeType) -> Arg { - match native_type { - NativeType::Void => unreachable!(), - NativeType::Bool => Arg::new(&self.bool_value), - NativeType::U8 => Arg::new(&self.u8_value), - NativeType::I8 => Arg::new(&self.i8_value), - NativeType::U16 => Arg::new(&self.u16_value), - NativeType::I16 => Arg::new(&self.i16_value), - NativeType::U32 => Arg::new(&self.u32_value), - NativeType::I32 => Arg::new(&self.i32_value), - NativeType::U64 => Arg::new(&self.u64_value), - NativeType::I64 => Arg::new(&self.i64_value), - NativeType::USize => Arg::new(&self.usize_value), - NativeType::ISize => Arg::new(&self.isize_value), - NativeType::F32 => Arg::new(&self.f32_value), - NativeType::F64 => Arg::new(&self.f64_value), - NativeType::Pointer | NativeType::Buffer | NativeType::Function => { - Arg::new(&self.pointer) - } - } - } - - // SAFETY: native_type must correspond to the type of value represented by the union field - unsafe fn to_value(&self, native_type: NativeType) -> Value { - match native_type { - NativeType::Void => Value::Null, - NativeType::Bool => Value::from(self.bool_value), - NativeType::U8 => Value::from(self.u8_value), - NativeType::I8 => Value::from(self.i8_value), - NativeType::U16 => Value::from(self.u16_value), - NativeType::I16 => Value::from(self.i16_value), - NativeType::U32 => Value::from(self.u32_value), - NativeType::I32 => Value::from(self.i32_value), - NativeType::U64 => Value::from(self.u64_value), - NativeType::I64 => Value::from(self.i64_value), - NativeType::USize => Value::from(self.usize_value), - NativeType::ISize => Value::from(self.isize_value), - NativeType::F32 => Value::from(self.f32_value), - NativeType::F64 => Value::from(self.f64_value), - NativeType::Pointer | NativeType::Function | NativeType::Buffer => { - Value::from(self.pointer as usize) - } - } - } - - // SAFETY: native_type must correspond to the type of value represented by the union field - #[inline] - unsafe fn to_v8<'scope>( - &self, - scope: &mut v8::HandleScope<'scope>, - native_type: NativeType, - ) -> serde_v8::Value<'scope> { - match native_type { - NativeType::Void => { - let local_value: v8::Local = v8::undefined(scope).into(); - local_value.into() - } - NativeType::Bool => { - let local_value: v8::Local = - v8::Boolean::new(scope, self.bool_value).into(); - local_value.into() - } - NativeType::U8 => { - let local_value: v8::Local = - v8::Integer::new_from_unsigned(scope, self.u8_value as u32).into(); - local_value.into() - } - NativeType::I8 => { - let local_value: v8::Local = - v8::Integer::new(scope, self.i8_value as i32).into(); - local_value.into() - } - NativeType::U16 => { - let local_value: v8::Local = - v8::Integer::new_from_unsigned(scope, self.u16_value as u32).into(); - local_value.into() - } - NativeType::I16 => { - let local_value: v8::Local = - v8::Integer::new(scope, self.i16_value as i32).into(); - local_value.into() - } - NativeType::U32 => { - let local_value: v8::Local = - v8::Integer::new_from_unsigned(scope, self.u32_value).into(); - local_value.into() - } - NativeType::I32 => { - let local_value: v8::Local = - v8::Integer::new(scope, self.i32_value).into(); - local_value.into() - } - NativeType::U64 => { - let value = self.u64_value; - let local_value: v8::Local = - if value > MAX_SAFE_INTEGER as u64 { - v8::BigInt::new_from_u64(scope, value).into() - } else { - v8::Number::new(scope, value as f64).into() - }; - local_value.into() - } - NativeType::I64 => { - let value = self.i64_value; - let local_value: v8::Local = - if value > MAX_SAFE_INTEGER as i64 || value < MIN_SAFE_INTEGER as i64 - { - v8::BigInt::new_from_i64(scope, self.i64_value).into() - } else { - v8::Number::new(scope, value as f64).into() - }; - local_value.into() - } - NativeType::USize => { - let value = self.usize_value; - let local_value: v8::Local = - if value > MAX_SAFE_INTEGER as usize { - v8::BigInt::new_from_u64(scope, value as u64).into() - } else { - v8::Number::new(scope, value as f64).into() - }; - local_value.into() - } - NativeType::ISize => { - let value = self.isize_value; - let local_value: v8::Local = - if !(MIN_SAFE_INTEGER..=MAX_SAFE_INTEGER).contains(&value) { - v8::BigInt::new_from_i64(scope, self.isize_value as i64).into() - } else { - v8::Number::new(scope, value as f64).into() - }; - local_value.into() - } - NativeType::F32 => { - let local_value: v8::Local = - v8::Number::new(scope, self.f32_value as f64).into(); - local_value.into() - } - NativeType::F64 => { - let local_value: v8::Local = - v8::Number::new(scope, self.f64_value).into(); - local_value.into() - } - NativeType::Pointer | NativeType::Buffer | NativeType::Function => { - let value = self.pointer as u64; - let local_value: v8::Local = - if value > MAX_SAFE_INTEGER as u64 { - v8::BigInt::new_from_u64(scope, value).into() - } else { - v8::Number::new(scope, value as f64).into() - }; - local_value.into() - } - } - } -} - -// SAFETY: unsafe trait must have unsafe implementation -unsafe impl Send for NativeValue {} - -#[derive(Deserialize, Debug)] -#[serde(rename_all = "camelCase")] -struct ForeignFunction { - name: Option, - parameters: Vec, - result: NativeType, - #[serde(rename = "nonblocking")] - non_blocking: Option, - #[serde(rename = "callback")] - #[serde(default = "default_callback")] - callback: bool, -} - -fn default_callback() -> bool { - false -} - -// ForeignStatic's name and type fields are read and used by -// serde_v8 to determine which variant a ForeignSymbol is. -// They are not used beyond that and are thus marked with underscores. -#[derive(Deserialize, Debug)] -struct ForeignStatic { - #[serde(rename(deserialize = "name"))] - _name: Option, - #[serde(rename(deserialize = "type"))] - _type: String, -} - -#[derive(Deserialize, Debug)] -#[serde(untagged)] -enum ForeignSymbol { - ForeignFunction(ForeignFunction), - ForeignStatic(ForeignStatic), -} - -#[derive(Deserialize, Debug)] -struct FfiLoadArgs { - path: String, - symbols: HashMap, -} - -// `path` is only used on Windows. -#[allow(unused_variables)] -pub(crate) fn format_error(e: dlopen::Error, path: String) -> String { - match e { - #[cfg(target_os = "windows")] - // This calls FormatMessageW with library path - // as replacement for the insert sequences. - // Unlike libstd which passes the FORMAT_MESSAGE_IGNORE_INSERTS - // flag without any arguments. - // - // https://github.com/denoland/deno/issues/11632 - dlopen::Error::OpeningLibraryError(e) => { - use std::ffi::OsStr; - use std::os::windows::ffi::OsStrExt; - use winapi::shared::minwindef::DWORD; - use winapi::shared::winerror::ERROR_INSUFFICIENT_BUFFER; - use winapi::um::errhandlingapi::GetLastError; - use winapi::um::winbase::FormatMessageW; - use winapi::um::winbase::FORMAT_MESSAGE_ARGUMENT_ARRAY; - use winapi::um::winbase::FORMAT_MESSAGE_FROM_SYSTEM; - use winapi::um::winnt::LANG_SYSTEM_DEFAULT; - use winapi::um::winnt::MAKELANGID; - use winapi::um::winnt::SUBLANG_SYS_DEFAULT; - - let err_num = match e.raw_os_error() { - Some(err_num) => err_num, - // This should never hit unless dlopen changes its error type. - None => return e.to_string(), - }; - - // Language ID (0x0800) - let lang_id = - MAKELANGID(LANG_SYSTEM_DEFAULT, SUBLANG_SYS_DEFAULT) as DWORD; - - let mut buf = vec![0; 500]; - - let path = OsStr::new(&path) - .encode_wide() - .chain(Some(0).into_iter()) - .collect::>(); - - let arguments = [path.as_ptr()]; - - loop { - // SAFETY: - // winapi call to format the error message - let length = unsafe { - FormatMessageW( - FORMAT_MESSAGE_FROM_SYSTEM | FORMAT_MESSAGE_ARGUMENT_ARRAY, - std::ptr::null_mut(), - err_num as DWORD, - lang_id as DWORD, - buf.as_mut_ptr(), - buf.len() as DWORD, - arguments.as_ptr() as _, - ) - }; - - if length == 0 { - // SAFETY: - // winapi call to get the last error message - let err_num = unsafe { GetLastError() }; - if err_num == ERROR_INSUFFICIENT_BUFFER { - buf.resize(buf.len() * 2, 0); - continue; - } - - // Something went wrong, just return the original error. - return e.to_string(); - } - - let msg = String::from_utf16_lossy(&buf[..length as usize]); - return msg; - } - } - _ => e.to_string(), - } -} - -#[op(v8)] -fn op_ffi_load( - scope: &mut v8::HandleScope<'scope>, - state: &mut deno_core::OpState, - args: FfiLoadArgs, -) -> Result<(ResourceId, serde_v8::Value<'scope>), AnyError> -where - FP: FfiPermissions + 'static, -{ - let path = args.path; - - check_unstable(state, "Deno.dlopen"); - let permissions = state.borrow_mut::(); - permissions.check(Some(&PathBuf::from(&path)))?; - - let lib = Library::open(&path).map_err(|e| { - dlopen::Error::OpeningLibraryError(std::io::Error::new( - std::io::ErrorKind::Other, - format_error(e, path), - )) - })?; - let mut resource = DynamicLibraryResource { - lib, - symbols: HashMap::new(), - }; - let obj = v8::Object::new(scope); - - for (symbol_key, foreign_symbol) in args.symbols { - match foreign_symbol { - ForeignSymbol::ForeignStatic(_) => { - // No-op: Statics will be handled separately and are not part of the Rust-side resource. - } - ForeignSymbol::ForeignFunction(foreign_fn) => { - let symbol = match &foreign_fn.name { - Some(symbol) => symbol, - None => &symbol_key, - }; - // By default, Err returned by this function does not tell - // which symbol wasn't exported. So we'll modify the error - // message to include the name of symbol. - let fn_ptr = - // SAFETY: The obtained T symbol is the size of a pointer. - match unsafe { resource.lib.symbol::<*const c_void>(symbol) } { - Ok(value) => Ok(value), - Err(err) => Err(generic_error(format!( - "Failed to register symbol {}: {}", - symbol, err - ))), - }?; - let ptr = libffi::middle::CodePtr::from_ptr(fn_ptr as _); - let cif = libffi::middle::Cif::new( - foreign_fn - .parameters - .clone() - .into_iter() - .map(libffi::middle::Type::from), - foreign_fn.result.into(), - ); - - let func_key = v8::String::new(scope, &symbol_key).unwrap(); - let sym = Box::new(Symbol { - cif, - ptr, - parameter_types: foreign_fn.parameters, - result_type: foreign_fn.result, - can_callback: foreign_fn.callback, - }); - - resource.symbols.insert(symbol_key, sym.clone()); - match foreign_fn.non_blocking { - // Generate functions for synchronous calls. - Some(false) | None => { - let function = make_sync_fn(scope, sym); - obj.set(scope, func_key.into(), function.into()); - } - // This optimization is not yet supported for non-blocking calls. - _ => {} - }; - } - } - } - - let rid = state.resource_table.add(resource); - Ok(( - rid, - serde_v8::Value { - v8_value: obj.into(), - }, - )) -} - -fn needs_unwrap(rv: NativeType) -> bool { - matches!( - rv, - NativeType::Function - | NativeType::Pointer - | NativeType::Buffer - | NativeType::I64 - | NativeType::ISize - | NativeType::U64 - | NativeType::USize - ) -} - -fn is_i64(rv: NativeType) -> bool { - matches!(rv, NativeType::I64 | NativeType::ISize) -} - -// Create a JavaScript function for synchronous FFI call to -// the given symbol. -fn make_sync_fn<'s>( - scope: &mut v8::HandleScope<'s>, - sym: Box, -) -> v8::Local<'s, v8::Function> { - let sym = Box::leak(sym); - let builder = v8::FunctionTemplate::builder( - |scope: &mut v8::HandleScope, - args: v8::FunctionCallbackArguments, - mut rv: v8::ReturnValue| { - let external: v8::Local = args.data().try_into().unwrap(); - // SAFETY: The pointer will not be deallocated until the function is - // garbage collected. - let symbol = unsafe { &*(external.value() as *const Symbol) }; - let needs_unwrap = match needs_unwrap(symbol.result_type) { - true => Some(args.get(symbol.parameter_types.len() as i32)), - false => None, - }; - match ffi_call_sync(scope, args, symbol) { - Ok(result) => { - match needs_unwrap { - Some(v) => { - let view: v8::Local = v.try_into().unwrap(); - let backing_store = - view.buffer(scope).unwrap().get_backing_store(); - - if is_i64(symbol.result_type) { - // SAFETY: v8::SharedRef is similar to Arc<[u8]>, - // it points to a fixed continuous slice of bytes on the heap. - let bs = unsafe { - &mut *(&backing_store[..] as *const _ as *mut [u8] - as *mut i64) - }; - // SAFETY: We already checked that type == I64 - let value = unsafe { result.i64_value }; - *bs = value; - } else { - // SAFETY: v8::SharedRef is similar to Arc<[u8]>, - // it points to a fixed continuous slice of bytes on the heap. - let bs = unsafe { - &mut *(&backing_store[..] as *const _ as *mut [u8] - as *mut u64) - }; - // SAFETY: We checked that type == U64 - let value = unsafe { result.u64_value }; - *bs = value; - } - } - None => { - // SAFETY: Same return type declared to libffi; trust user to have it right beyond that. - let result = unsafe { result.to_v8(scope, symbol.result_type) }; - rv.set(result.v8_value); - } - } - } - Err(err) => { - deno_core::_ops::throw_type_error(scope, err.to_string()); - } - }; - }, - ) - .data(v8::External::new(scope, sym as *mut Symbol as *mut _).into()); - - let mut fast_call_alloc = None; - - let func = if fast_call::is_compatible(sym) { - let trampoline = fast_call::compile_trampoline(sym); - let func = builder.build_fast( - scope, - &fast_call::make_template(sym, &trampoline), - None, - ); - fast_call_alloc = Some(Box::into_raw(Box::new(trampoline))); - func - } else { - builder.build(scope) - }; - let func = func.get_function(scope).unwrap(); - - let weak = v8::Weak::with_finalizer( - scope, - func, - Box::new(move |_| { - // SAFETY: This is never called twice. pointer obtained - // from Box::into_raw, hence, satisfies memory layout requirements. - let _ = unsafe { Box::from_raw(sym) }; - if let Some(fast_call_ptr) = fast_call_alloc { - // fast-call compiled trampoline is unmapped when the MMAP handle is dropped - // SAFETY: This is never called twice. pointer obtained - // from Box::into_raw, hence, satisfies memory layout requirements. - let _ = unsafe { Box::from_raw(fast_call_ptr) }; - } - }), - ); - - weak.to_local(scope).unwrap() -} - -#[inline] -fn ffi_parse_bool_arg( - arg: v8::Local, -) -> Result { - let bool_value = v8::Local::::try_from(arg) - .map_err(|_| type_error("Invalid FFI u8 type, expected boolean"))? - .is_true(); - Ok(NativeValue { bool_value }) -} - -#[inline] -fn ffi_parse_u8_arg( - arg: v8::Local, -) -> Result { - let u8_value = v8::Local::::try_from(arg) - .map_err(|_| type_error("Invalid FFI u8 type, expected unsigned integer"))? - .value() as u8; - Ok(NativeValue { u8_value }) -} - -#[inline] -fn ffi_parse_i8_arg( - arg: v8::Local, -) -> Result { - let i8_value = v8::Local::::try_from(arg) - .map_err(|_| type_error("Invalid FFI i8 type, expected integer"))? - .value() as i8; - Ok(NativeValue { i8_value }) -} - -#[inline] -fn ffi_parse_u16_arg( - arg: v8::Local, -) -> Result { - let u16_value = v8::Local::::try_from(arg) - .map_err(|_| type_error("Invalid FFI u16 type, expected unsigned integer"))? - .value() as u16; - Ok(NativeValue { u16_value }) -} - -#[inline] -fn ffi_parse_i16_arg( - arg: v8::Local, -) -> Result { - let i16_value = v8::Local::::try_from(arg) - .map_err(|_| type_error("Invalid FFI i16 type, expected integer"))? - .value() as i16; - Ok(NativeValue { i16_value }) -} - -#[inline] -fn ffi_parse_u32_arg( - arg: v8::Local, -) -> Result { - let u32_value = v8::Local::::try_from(arg) - .map_err(|_| type_error("Invalid FFI u32 type, expected unsigned integer"))? - .value() as u32; - Ok(NativeValue { u32_value }) -} - -#[inline] -fn ffi_parse_i32_arg( - arg: v8::Local, -) -> Result { - let i32_value = v8::Local::::try_from(arg) - .map_err(|_| type_error("Invalid FFI i32 type, expected integer"))? - .value() as i32; - Ok(NativeValue { i32_value }) -} - -#[inline] -fn ffi_parse_u64_arg( - scope: &mut v8::HandleScope, - arg: v8::Local, -) -> Result { - // Order of checking: - // 1. BigInt: Uncommon and not supported by Fast API, so optimise slow call for this case. - // 2. Number: Common, supported by Fast API, so let that be the optimal case. - let u64_value: u64 = if let Ok(value) = v8::Local::::try_from(arg) - { - value.u64_value().0 - } else if let Ok(value) = v8::Local::::try_from(arg) { - value.integer_value(scope).unwrap() as u64 - } else { - return Err(type_error( - "Invalid FFI u64 type, expected unsigned integer", - )); - }; - Ok(NativeValue { u64_value }) -} - -#[inline] -fn ffi_parse_i64_arg( - scope: &mut v8::HandleScope, - arg: v8::Local, -) -> Result { - // Order of checking: - // 1. BigInt: Uncommon and not supported by Fast API, so optimise slow call for this case. - // 2. Number: Common, supported by Fast API, so let that be the optimal case. - let i64_value: i64 = if let Ok(value) = v8::Local::::try_from(arg) - { - value.i64_value().0 - } else if let Ok(value) = v8::Local::::try_from(arg) { - value.integer_value(scope).unwrap() as i64 - } else { - return Err(type_error("Invalid FFI i64 type, expected integer")); - }; - Ok(NativeValue { i64_value }) -} - -#[inline] -fn ffi_parse_usize_arg( - scope: &mut v8::HandleScope, - arg: v8::Local, -) -> Result { - // Order of checking: - // 1. BigInt: Uncommon and not supported by Fast API, so optimise slow call for this case. - // 2. Number: Common, supported by Fast API, so let that be the optimal case. - let usize_value: usize = - if let Ok(value) = v8::Local::::try_from(arg) { - value.u64_value().0 as usize - } else if let Ok(value) = v8::Local::::try_from(arg) { - value.integer_value(scope).unwrap() as usize - } else { - return Err(type_error("Invalid FFI usize type, expected integer")); - }; - Ok(NativeValue { usize_value }) -} - -#[inline] -fn ffi_parse_isize_arg( - scope: &mut v8::HandleScope, - arg: v8::Local, -) -> Result { - // Order of checking: - // 1. BigInt: Uncommon and not supported by Fast API, so optimise slow call for this case. - // 2. Number: Common, supported by Fast API, so let that be the optimal case. - let isize_value: isize = - if let Ok(value) = v8::Local::::try_from(arg) { - value.i64_value().0 as isize - } else if let Ok(value) = v8::Local::::try_from(arg) { - value.integer_value(scope).unwrap() as isize - } else { - return Err(type_error("Invalid FFI isize type, expected integer")); - }; - Ok(NativeValue { isize_value }) -} - -#[inline] -fn ffi_parse_f32_arg( - arg: v8::Local, -) -> Result { - let f32_value = v8::Local::::try_from(arg) - .map_err(|_| type_error("Invalid FFI f32 type, expected number"))? - .value() as f32; - Ok(NativeValue { f32_value }) -} - -#[inline] -fn ffi_parse_f64_arg( - arg: v8::Local, -) -> Result { - let f64_value = v8::Local::::try_from(arg) - .map_err(|_| type_error("Invalid FFI f64 type, expected number"))? - .value() as f64; - Ok(NativeValue { f64_value }) -} - -#[inline] -fn ffi_parse_pointer_arg( - scope: &mut v8::HandleScope, - arg: v8::Local, -) -> Result { - // Order of checking: - // 1. BigInt: Uncommon and not supported by Fast API, optimise this case. - // 2. Number: Common and supported by Fast API. - // 3. Null: Very uncommon / can be represented by a 0. - let pointer = if let Ok(value) = v8::Local::::try_from(arg) { - value.u64_value().0 as usize as *mut c_void - } else if let Ok(value) = v8::Local::::try_from(arg) { - value.integer_value(scope).unwrap() as usize as *mut c_void - } else if arg.is_null() { - ptr::null_mut() - } else { - return Err(type_error( - "Invalid FFI pointer type, expected null, integer or BigInt", - )); - }; - Ok(NativeValue { pointer }) -} - -#[inline] -fn ffi_parse_buffer_arg( - scope: &mut v8::HandleScope, - arg: v8::Local, -) -> Result { - // Order of checking: - // 1. ArrayBuffer: Fairly common and not supported by Fast API, optimise this case. - // 2. ArrayBufferView: Common and supported by Fast API - // 5. Null: Very uncommon / can be represented by a 0. - - let pointer = if let Ok(value) = v8::Local::::try_from(arg) { - if let Some(non_null) = value.data() { - non_null.as_ptr() - } else { - ptr::null_mut() - } - } else if let Ok(value) = v8::Local::::try_from(arg) { - let byte_offset = value.byte_offset(); - let pointer = value - .buffer(scope) - .ok_or_else(|| { - type_error("Invalid FFI ArrayBufferView, expected data in the buffer") - })? - .data(); - if let Some(non_null) = pointer { - // SAFETY: Pointer is non-null, and V8 guarantees that the byte_offset - // is within the buffer backing store. - unsafe { non_null.as_ptr().add(byte_offset) } - } else { - ptr::null_mut() - } - } else if arg.is_null() { - ptr::null_mut() - } else { - return Err(type_error( - "Invalid FFI buffer type, expected null, ArrayBuffer, or ArrayBufferView", - )); - }; - Ok(NativeValue { pointer }) -} - -#[inline] -fn ffi_parse_function_arg( - scope: &mut v8::HandleScope, - arg: v8::Local, -) -> Result { - // Order of checking: - // 1. BigInt: Uncommon and not supported by Fast API, optimise this case. - // 2. Number: Common and supported by Fast API, optimise this case as second. - // 3. Null: Very uncommon / can be represented by a 0. - let pointer = if let Ok(value) = v8::Local::::try_from(arg) { - value.u64_value().0 as usize as *mut c_void - } else if let Ok(value) = v8::Local::::try_from(arg) { - value.integer_value(scope).unwrap() as usize as *mut c_void - } else if arg.is_null() { - ptr::null_mut() - } else { - return Err(type_error( - "Invalid FFI function type, expected null, integer, or BigInt", - )); - }; - Ok(NativeValue { pointer }) -} - -fn ffi_parse_args<'scope>( - scope: &mut v8::HandleScope<'scope>, - args: serde_v8::Value<'scope>, - parameter_types: &[NativeType], -) -> Result, AnyError> -where - 'scope: 'scope, -{ - if parameter_types.is_empty() { - return Ok(vec![]); - } - - let args = v8::Local::::try_from(args.v8_value) - .map_err(|_| type_error("Invalid FFI parameters, expected Array"))?; - let mut ffi_args: Vec = - Vec::with_capacity(parameter_types.len()); - - for (index, native_type) in parameter_types.iter().enumerate() { - let value = args.get_index(scope, index as u32).unwrap(); - match native_type { - NativeType::Bool => { - ffi_args.push(ffi_parse_bool_arg(value)?); - } - NativeType::U8 => { - ffi_args.push(ffi_parse_u8_arg(value)?); - } - NativeType::I8 => { - ffi_args.push(ffi_parse_i8_arg(value)?); - } - NativeType::U16 => { - ffi_args.push(ffi_parse_u16_arg(value)?); - } - NativeType::I16 => { - ffi_args.push(ffi_parse_i16_arg(value)?); - } - NativeType::U32 => { - ffi_args.push(ffi_parse_u32_arg(value)?); - } - NativeType::I32 => { - ffi_args.push(ffi_parse_i32_arg(value)?); - } - NativeType::U64 => { - ffi_args.push(ffi_parse_u64_arg(scope, value)?); - } - NativeType::I64 => { - ffi_args.push(ffi_parse_i64_arg(scope, value)?); - } - NativeType::USize => { - ffi_args.push(ffi_parse_usize_arg(scope, value)?); - } - NativeType::ISize => { - ffi_args.push(ffi_parse_isize_arg(scope, value)?); - } - NativeType::F32 => { - ffi_args.push(ffi_parse_f32_arg(value)?); - } - NativeType::F64 => { - ffi_args.push(ffi_parse_f64_arg(value)?); - } - NativeType::Buffer => { - ffi_args.push(ffi_parse_buffer_arg(scope, value)?); - } - NativeType::Pointer => { - ffi_args.push(ffi_parse_pointer_arg(scope, value)?); - } - NativeType::Function => { - ffi_args.push(ffi_parse_function_arg(scope, value)?); - } - NativeType::Void => { - unreachable!(); - } - } - } - - Ok(ffi_args) -} - -// A one-off synchronous FFI call. -fn ffi_call_sync<'scope>( - scope: &mut v8::HandleScope<'scope>, - args: v8::FunctionCallbackArguments, - symbol: &Symbol, -) -> Result -where - 'scope: 'scope, -{ - let Symbol { - parameter_types, - result_type, - cif, - ptr: fun_ptr, - .. - } = symbol; - let mut ffi_args: Vec = - Vec::with_capacity(parameter_types.len()); - - for (index, native_type) in parameter_types.iter().enumerate() { - let value = args.get(index as i32); - match native_type { - NativeType::Bool => { - ffi_args.push(ffi_parse_bool_arg(value)?); - } - NativeType::U8 => { - ffi_args.push(ffi_parse_u8_arg(value)?); - } - NativeType::I8 => { - ffi_args.push(ffi_parse_i8_arg(value)?); - } - NativeType::U16 => { - ffi_args.push(ffi_parse_u16_arg(value)?); - } - NativeType::I16 => { - ffi_args.push(ffi_parse_i16_arg(value)?); - } - NativeType::U32 => { - ffi_args.push(ffi_parse_u32_arg(value)?); - } - NativeType::I32 => { - ffi_args.push(ffi_parse_i32_arg(value)?); - } - NativeType::U64 => { - ffi_args.push(ffi_parse_u64_arg(scope, value)?); - } - NativeType::I64 => { - ffi_args.push(ffi_parse_i64_arg(scope, value)?); - } - NativeType::USize => { - ffi_args.push(ffi_parse_usize_arg(scope, value)?); - } - NativeType::ISize => { - ffi_args.push(ffi_parse_isize_arg(scope, value)?); - } - NativeType::F32 => { - ffi_args.push(ffi_parse_f32_arg(value)?); - } - NativeType::F64 => { - ffi_args.push(ffi_parse_f64_arg(value)?); - } - NativeType::Buffer => { - ffi_args.push(ffi_parse_buffer_arg(scope, value)?); - } - NativeType::Pointer => { - ffi_args.push(ffi_parse_pointer_arg(scope, value)?); - } - NativeType::Function => { - ffi_args.push(ffi_parse_function_arg(scope, value)?); - } - NativeType::Void => { - unreachable!(); - } - } - } - let call_args: Vec = ffi_args.iter().map(Arg::new).collect(); - // SAFETY: types in the `Cif` match the actual calling convention and - // types of symbol. - unsafe { - Ok(match result_type { - NativeType::Void => NativeValue { - void_value: cif.call::<()>(*fun_ptr, &call_args), - }, - NativeType::Bool => NativeValue { - bool_value: cif.call::(*fun_ptr, &call_args), - }, - NativeType::U8 => NativeValue { - u8_value: cif.call::(*fun_ptr, &call_args), - }, - NativeType::I8 => NativeValue { - i8_value: cif.call::(*fun_ptr, &call_args), - }, - NativeType::U16 => NativeValue { - u16_value: cif.call::(*fun_ptr, &call_args), - }, - NativeType::I16 => NativeValue { - i16_value: cif.call::(*fun_ptr, &call_args), - }, - NativeType::U32 => NativeValue { - u32_value: cif.call::(*fun_ptr, &call_args), - }, - NativeType::I32 => NativeValue { - i32_value: cif.call::(*fun_ptr, &call_args), - }, - NativeType::U64 => NativeValue { - u64_value: cif.call::(*fun_ptr, &call_args), - }, - NativeType::I64 => NativeValue { - i64_value: cif.call::(*fun_ptr, &call_args), - }, - NativeType::USize => NativeValue { - usize_value: cif.call::(*fun_ptr, &call_args), - }, - NativeType::ISize => NativeValue { - isize_value: cif.call::(*fun_ptr, &call_args), - }, - NativeType::F32 => NativeValue { - f32_value: cif.call::(*fun_ptr, &call_args), - }, - NativeType::F64 => NativeValue { - f64_value: cif.call::(*fun_ptr, &call_args), - }, - NativeType::Pointer | NativeType::Function | NativeType::Buffer => { - NativeValue { - pointer: cif.call::<*mut c_void>(*fun_ptr, &call_args), - } - } - }) - } -} - -fn ffi_call( - call_args: Vec, - cif: &libffi::middle::Cif, - fun_ptr: libffi::middle::CodePtr, - parameter_types: &[NativeType], - result_type: NativeType, -) -> Result { - let call_args: Vec = call_args - .iter() - .enumerate() - .map(|(index, ffi_arg)| { - // SAFETY: the union field is initialized - unsafe { ffi_arg.as_arg(*parameter_types.get(index).unwrap()) } - }) - .collect(); - - // SAFETY: types in the `Cif` match the actual calling convention and - // types of symbol. - unsafe { - Ok(match result_type { - NativeType::Void => NativeValue { - void_value: cif.call::<()>(fun_ptr, &call_args), - }, - NativeType::Bool => NativeValue { - bool_value: cif.call::(fun_ptr, &call_args), - }, - NativeType::U8 => NativeValue { - u8_value: cif.call::(fun_ptr, &call_args), - }, - NativeType::I8 => NativeValue { - i8_value: cif.call::(fun_ptr, &call_args), - }, - NativeType::U16 => NativeValue { - u16_value: cif.call::(fun_ptr, &call_args), - }, - NativeType::I16 => NativeValue { - i16_value: cif.call::(fun_ptr, &call_args), - }, - NativeType::U32 => NativeValue { - u32_value: cif.call::(fun_ptr, &call_args), - }, - NativeType::I32 => NativeValue { - i32_value: cif.call::(fun_ptr, &call_args), - }, - NativeType::U64 => NativeValue { - u64_value: cif.call::(fun_ptr, &call_args), - }, - NativeType::I64 => NativeValue { - i64_value: cif.call::(fun_ptr, &call_args), - }, - NativeType::USize => NativeValue { - usize_value: cif.call::(fun_ptr, &call_args), - }, - NativeType::ISize => NativeValue { - isize_value: cif.call::(fun_ptr, &call_args), - }, - NativeType::F32 => NativeValue { - f32_value: cif.call::(fun_ptr, &call_args), - }, - NativeType::F64 => NativeValue { - f64_value: cif.call::(fun_ptr, &call_args), - }, - NativeType::Pointer | NativeType::Function | NativeType::Buffer => { - NativeValue { - pointer: cif.call::<*mut c_void>(fun_ptr, &call_args), - } - } - }) - } -} - -struct UnsafeCallbackResource { - cancel: Rc, - // Closure is never directly touched, but it keeps the C callback alive - // until `close()` method is called. - #[allow(dead_code)] - closure: libffi::middle::Closure<'static>, - info: *mut CallbackInfo, -} - -impl Resource for UnsafeCallbackResource { - fn name(&self) -> Cow { - "unsafecallback".into() - } - - fn close(self: Rc) { - self.cancel.cancel(); - // SAFETY: This drops the closure and the callback info associated with it. - // Any retained function pointers to the closure become dangling pointers. - // It is up to the user to know that it is safe to call the `close()` on the - // UnsafeCallback instance. - unsafe { - let info = Box::from_raw(self.info); - let isolate = info.isolate.as_mut().unwrap(); - let _ = v8::Global::from_raw(isolate, info.callback); - let _ = v8::Global::from_raw(isolate, info.context); - } - } -} - -struct CallbackInfo { - pub parameters: Vec, - pub result: NativeType, - pub async_work_sender: mpsc::UnboundedSender, - pub callback: NonNull, - pub context: NonNull, - pub isolate: *mut v8::Isolate, - pub waker: Option, -} - -unsafe extern "C" fn deno_ffi_callback( - _cif: &libffi::low::ffi_cif, - result: &mut c_void, - args: *const *const c_void, - info: &CallbackInfo, -) { - LOCAL_ISOLATE_POINTER.with(|s| { - if ptr::eq(*s.borrow(), info.isolate) { - // Own isolate thread, okay to call directly - do_ffi_callback(info, result, args); - } else { - let async_work_sender = &info.async_work_sender; - // SAFETY: Safe as this function blocks until `do_ffi_callback` completes and a response message is received. - let result: &'static mut c_void = std::mem::transmute(result); - let info: &'static CallbackInfo = std::mem::transmute(info); - let (response_sender, response_receiver) = sync_channel::<()>(0); - let fut = Box::new(move || { - do_ffi_callback(info, result, args); - response_sender.send(()).unwrap(); - }); - async_work_sender.unbounded_send(fut).unwrap(); - if let Some(waker) = info.waker.as_ref() { - // Make sure event loop wakes up to receive our message before we start waiting for a response. - waker.wake_by_ref(); - } - response_receiver.recv().unwrap(); - } - }); -} - -unsafe fn do_ffi_callback( - info: &CallbackInfo, - result: &mut c_void, - args: *const *const c_void, -) { - let callback: NonNull = info.callback; - let context: NonNull = info.context; - let isolate: *mut v8::Isolate = info.isolate; - let isolate = &mut *isolate; - let callback = v8::Global::from_raw(isolate, callback); - let context = std::mem::transmute::< - NonNull, - v8::Local, - >(context); - // Call from main thread. If this callback is being triggered due to a - // function call coming from Deno itself, then this callback will build - // ontop of that stack. - // If this callback is being triggered outside of Deno (for example from a - // signal handler) then this will either create an empty new stack if - // Deno currently has nothing running and is waiting for promises to resolve, - // or will (very incorrectly) build ontop of whatever stack exists. - // The callback will even be called through from a `while (true)` liveloop, but - // it somehow cannot change the values that the loop sees, even if they both - // refer the same `let bool_value`. - let mut cb_scope = v8::CallbackScope::new(context); - let scope = &mut v8::HandleScope::new(&mut cb_scope); - let func = callback.open(scope); - let result = result as *mut c_void; - let vals: &[*const c_void] = - std::slice::from_raw_parts(args, info.parameters.len() as usize); - - let mut params: Vec> = vec![]; - for (native_type, val) in info.parameters.iter().zip(vals) { - let value: v8::Local = match native_type { - NativeType::Bool => { - let value = *((*val) as *const bool); - v8::Boolean::new(scope, value).into() - } - NativeType::F32 => { - let value = *((*val) as *const f32); - v8::Number::new(scope, value as f64).into() - } - NativeType::F64 => { - let value = *((*val) as *const f64); - v8::Number::new(scope, value).into() - } - NativeType::I8 => { - let value = *((*val) as *const i8); - v8::Integer::new(scope, value as i32).into() - } - NativeType::U8 => { - let value = *((*val) as *const u8); - v8::Integer::new_from_unsigned(scope, value as u32).into() - } - NativeType::I16 => { - let value = *((*val) as *const i16); - v8::Integer::new(scope, value as i32).into() - } - NativeType::U16 => { - let value = *((*val) as *const u16); - v8::Integer::new_from_unsigned(scope, value as u32).into() - } - NativeType::I32 => { - let value = *((*val) as *const i32); - v8::Integer::new(scope, value).into() - } - NativeType::U32 => { - let value = *((*val) as *const u32); - v8::Integer::new_from_unsigned(scope, value).into() - } - NativeType::I64 | NativeType::ISize => { - let result = *((*val) as *const i64); - if result > MAX_SAFE_INTEGER as i64 || result < MIN_SAFE_INTEGER as i64 - { - v8::BigInt::new_from_i64(scope, result).into() - } else { - v8::Number::new(scope, result as f64).into() - } - } - NativeType::U64 | NativeType::USize => { - let result = *((*val) as *const u64); - if result > MAX_SAFE_INTEGER as u64 { - v8::BigInt::new_from_u64(scope, result).into() - } else { - v8::Number::new(scope, result as f64).into() - } - } - NativeType::Pointer | NativeType::Buffer | NativeType::Function => { - let result = *((*val) as *const usize); - if result > MAX_SAFE_INTEGER as usize { - v8::BigInt::new_from_u64(scope, result as u64).into() - } else { - v8::Number::new(scope, result as f64).into() - } - } - NativeType::Void => unreachable!(), - }; - params.push(value); - } - - let recv = v8::undefined(scope); - let call_result = func.call(scope, recv.into(), ¶ms); - std::mem::forget(callback); - - if call_result.is_none() { - // JS function threw an exception. Set the return value to zero and return. - // The exception continue propagating up the call chain when the event loop - // resumes. - match info.result { - NativeType::Bool => { - *(result as *mut bool) = false; - } - NativeType::U32 | NativeType::I32 => { - // zero is equal for signed and unsigned alike - *(result as *mut u32) = 0; - } - NativeType::F32 => { - *(result as *mut f32) = 0.0; - } - NativeType::F64 => { - *(result as *mut f64) = 0.0; - } - NativeType::U8 | NativeType::I8 => { - // zero is equal for signed and unsigned alike - *(result as *mut u8) = 0; - } - NativeType::U16 | NativeType::I16 => { - // zero is equal for signed and unsigned alike - *(result as *mut u16) = 0; - } - NativeType::Pointer - | NativeType::Buffer - | NativeType::Function - | NativeType::U64 - | NativeType::I64 => { - *(result as *mut usize) = 0; - } - NativeType::Void => { - // nop - } - _ => { - unreachable!(); - } - }; - - return; - } - let value = call_result.unwrap(); - - match info.result { - NativeType::Bool => { - let value = if let Ok(value) = v8::Local::::try_from(value) { - value.is_true() - } else { - value.boolean_value(scope) - }; - *(result as *mut bool) = value; - } - NativeType::I32 => { - let value = if let Ok(value) = v8::Local::::try_from(value) { - value.value() as i32 - } else { - // Fallthrough, probably UB. - value - .int32_value(scope) - .expect("Unable to deserialize result parameter.") as i32 - }; - *(result as *mut i32) = value; - } - NativeType::F32 => { - let value = if let Ok(value) = v8::Local::::try_from(value) { - value.value() as f32 - } else { - // Fallthrough, probably UB. - value - .number_value(scope) - .expect("Unable to deserialize result parameter.") as f32 - }; - *(result as *mut f32) = value; - } - NativeType::F64 => { - let value = if let Ok(value) = v8::Local::::try_from(value) { - value.value() - } else { - // Fallthrough, probably UB. - value - .number_value(scope) - .expect("Unable to deserialize result parameter.") - }; - *(result as *mut f64) = value; - } - NativeType::Pointer | NativeType::Buffer | NativeType::Function => { - let pointer = if let Ok(value) = - v8::Local::::try_from(value) - { - let byte_offset = value.byte_offset(); - let backing_store = value - .buffer(scope) - .expect("Unable to deserialize result parameter.") - .get_backing_store(); - &backing_store[byte_offset..] as *const _ as *const u8 - } else if let Ok(value) = v8::Local::::try_from(value) { - value.u64_value().0 as usize as *const u8 - } else if let Ok(value) = v8::Local::::try_from(value) { - let backing_store = value.get_backing_store(); - &backing_store[..] as *const _ as *const u8 - } else if let Ok(value) = v8::Local::::try_from(value) { - value.value() as usize as *const u8 - } else if value.is_null() { - ptr::null() - } else { - // Fallthrough: Probably someone returned a number but this could - // also be eg. a string. This is essentially UB. - value - .integer_value(scope) - .expect("Unable to deserialize result parameter.") as usize - as *const u8 - }; - *(result as *mut *const u8) = pointer; - } - NativeType::I8 => { - let value = if let Ok(value) = v8::Local::::try_from(value) { - value.value() as i8 - } else { - // Fallthrough, essentially UB. - value - .int32_value(scope) - .expect("Unable to deserialize result parameter.") as i8 - }; - *(result as *mut i8) = value; - } - NativeType::U8 => { - let value = if let Ok(value) = v8::Local::::try_from(value) { - value.value() as u8 - } else { - // Fallthrough, essentially UB. - value - .uint32_value(scope) - .expect("Unable to deserialize result parameter.") as u8 - }; - *(result as *mut u8) = value; - } - NativeType::I16 => { - let value = if let Ok(value) = v8::Local::::try_from(value) { - value.value() as i16 - } else { - // Fallthrough, essentially UB. - value - .int32_value(scope) - .expect("Unable to deserialize result parameter.") as i16 - }; - *(result as *mut i16) = value; - } - NativeType::U16 => { - let value = if let Ok(value) = v8::Local::::try_from(value) { - value.value() as u16 - } else { - // Fallthrough, essentially UB. - value - .uint32_value(scope) - .expect("Unable to deserialize result parameter.") as u16 - }; - *(result as *mut u16) = value; - } - NativeType::U32 => { - let value = if let Ok(value) = v8::Local::::try_from(value) { - value.value() as u32 - } else { - // Fallthrough, essentially UB. - value - .uint32_value(scope) - .expect("Unable to deserialize result parameter.") - }; - *(result as *mut u32) = value; - } - NativeType::I64 => { - if let Ok(value) = v8::Local::::try_from(value) { - *(result as *mut i64) = value.i64_value().0; - } else if let Ok(value) = v8::Local::::try_from(value) { - *(result as *mut i64) = value.value(); - } else { - *(result as *mut i64) = value - .integer_value(scope) - .expect("Unable to deserialize result parameter.") - as i64; - } - } - NativeType::U64 => { - if let Ok(value) = v8::Local::::try_from(value) { - *(result as *mut u64) = value.u64_value().0; - } else if let Ok(value) = v8::Local::::try_from(value) { - *(result as *mut u64) = value.value() as u64; - } else { - *(result as *mut u64) = value - .integer_value(scope) - .expect("Unable to deserialize result parameter.") - as u64; - } - } - NativeType::Void => { - // nop - } - _ => { - unreachable!(); - } - }; -} - -#[derive(Deserialize)] -struct RegisterCallbackArgs { - parameters: Vec, - result: NativeType, -} - -#[op(v8)] -fn op_ffi_unsafe_callback_create( - state: &mut deno_core::OpState, - scope: &mut v8::HandleScope<'scope>, - args: RegisterCallbackArgs, - cb: serde_v8::Value<'scope>, -) -> Result, AnyError> -where - FP: FfiPermissions + 'static, -{ - check_unstable(state, "Deno.UnsafeCallback"); - let permissions = state.borrow_mut::(); - permissions.check(None)?; - - let v8_value = cb.v8_value; - let cb = v8::Local::::try_from(v8_value)?; - - let isolate: *mut v8::Isolate = &mut *scope as &mut v8::Isolate; - LOCAL_ISOLATE_POINTER.with(|s| { - if s.borrow().is_null() { - s.replace(isolate); - } - }); - - let async_work_sender = - state.borrow_mut::().async_work_sender.clone(); - let callback = v8::Global::new(scope, cb).into_raw(); - let current_context = scope.get_current_context(); - let context = v8::Global::new(scope, current_context).into_raw(); - - let info: *mut CallbackInfo = Box::leak(Box::new(CallbackInfo { - parameters: args.parameters.clone(), - result: args.result, - async_work_sender, - callback, - context, - isolate, - waker: None, - })); - let cif = Cif::new( - args.parameters.into_iter().map(libffi::middle::Type::from), - libffi::middle::Type::from(args.result), - ); - - // SAFETY: CallbackInfo is leaked, is not null and stays valid as long as the callback exists. - let closure = libffi::middle::Closure::new(cif, deno_ffi_callback, unsafe { - info.as_ref().unwrap() - }); - let ptr = *closure.code_ptr() as usize; - let resource = UnsafeCallbackResource { - cancel: CancelHandle::new_rc(), - closure, - info, - }; - let rid = state.resource_table.add(resource); - - let rid_local = v8::Integer::new_from_unsigned(scope, rid); - let ptr_local: v8::Local = if ptr > MAX_SAFE_INTEGER as usize { - v8::BigInt::new_from_u64(scope, ptr as u64).into() - } else { - v8::Number::new(scope, ptr as f64).into() - }; - let array = v8::Array::new(scope, 2); - array.set_index(scope, 0, rid_local.into()); - array.set_index(scope, 1, ptr_local); - let array_value: v8::Local = array.into(); - - Ok(array_value.into()) -} - -#[op(v8)] -fn op_ffi_call_ptr( - scope: &mut v8::HandleScope<'scope>, - state: Rc>, - pointer: usize, - def: ForeignFunction, - parameters: serde_v8::Value<'scope>, -) -> Result, AnyError> -where - FP: FfiPermissions + 'static, -{ - check_unstable2(&state, "Deno.UnsafeFnPointer#call"); - { - let mut state = state.borrow_mut(); - let permissions = state.borrow_mut::(); - permissions.check(None)?; - }; - - let symbol = PtrSymbol::new(pointer, &def); - let call_args = ffi_parse_args(scope, parameters, &def.parameters)?; - - let result = ffi_call( - call_args, - &symbol.cif, - symbol.ptr, - &def.parameters, - def.result, - )?; - // SAFETY: Same return type declared to libffi; trust user to have it right beyond that. - let result = unsafe { result.to_v8(scope, def.result) }; - Ok(result) -} - -impl Future for CallbackInfo { - type Output = (); - fn poll( - mut self: Pin<&mut Self>, - cx: &mut std::task::Context<'_>, - ) -> std::task::Poll { - // Always replace the waker to make sure it's bound to the proper Future. - self.waker.replace(cx.waker().clone()); - // The future for the CallbackInfo never resolves: It can only be canceled. - Poll::Pending - } -} - -#[op] -fn op_ffi_unsafe_callback_ref( - state: Rc>, - rid: ResourceId, -) -> Result>, AnyError> { - let state = state.borrow(); - let callback_resource = - state.resource_table.get::(rid)?; - - Ok(async move { - let info: &mut CallbackInfo = - // SAFETY: CallbackInfo pointer stays valid as long as the resource is still alive. - unsafe { callback_resource.info.as_mut().unwrap() }; - // Ignore cancellation rejection - let _ = info - .into_future() - .or_cancel(callback_resource.cancel.clone()) - .await; - Ok(()) - }) -} - -#[op(fast)] -fn op_ffi_unsafe_callback_unref( - state: &mut deno_core::OpState, - rid: u32, -) -> Result<(), AnyError> { - state - .resource_table - .get::(rid)? - .cancel - .cancel(); - Ok(()) -} - -#[op(v8)] -fn op_ffi_call_ptr_nonblocking<'scope, FP>( - scope: &mut v8::HandleScope<'scope>, - state: Rc>, - pointer: usize, - def: ForeignFunction, - parameters: serde_v8::Value<'scope>, -) -> Result>, AnyError> -where - FP: FfiPermissions + 'static, -{ - check_unstable2(&state, "Deno.UnsafeFnPointer#call"); - { - let mut state = state.borrow_mut(); - let permissions = state.borrow_mut::(); - permissions.check(None)?; - }; - - let symbol = PtrSymbol::new(pointer, &def); - let call_args = ffi_parse_args(scope, parameters, &def.parameters)?; - - let join_handle = tokio::task::spawn_blocking(move || { - let PtrSymbol { cif, ptr } = symbol.clone(); - ffi_call(call_args, &cif, ptr, &def.parameters, def.result) - }); - - Ok(async move { - let result = join_handle - .await - .map_err(|err| anyhow!("Nonblocking FFI call failed: {}", err))??; - // SAFETY: Same return type declared to libffi; trust user to have it right beyond that. - Ok(unsafe { result.to_value(def.result) }) - }) -} - -#[op(v8)] -fn op_ffi_get_static<'scope>( - scope: &mut v8::HandleScope<'scope>, - state: &mut deno_core::OpState, - rid: ResourceId, - name: String, - static_type: NativeType, -) -> Result, AnyError> { - let resource = state.resource_table.get::(rid)?; - - let data_ptr = resource.get_static(name)?; - - Ok(match static_type { - NativeType::Void => { - return Err(type_error("Invalid FFI static type 'void'")); - } - NativeType::Bool => { - // SAFETY: ptr is user provided - let result = unsafe { ptr::read_unaligned(data_ptr as *const bool) }; - let boolean: v8::Local = - v8::Boolean::new(scope, result).into(); - boolean.into() - } - NativeType::U8 => { - // SAFETY: ptr is user provided - let result = unsafe { ptr::read_unaligned(data_ptr as *const u8) }; - let number: v8::Local = - v8::Integer::new_from_unsigned(scope, result as u32).into(); - number.into() - } - NativeType::I8 => { - // SAFETY: ptr is user provided - let result = unsafe { ptr::read_unaligned(data_ptr as *const i8) }; - let number: v8::Local = - v8::Integer::new(scope, result as i32).into(); - number.into() - } - NativeType::U16 => { - // SAFETY: ptr is user provided - let result = unsafe { ptr::read_unaligned(data_ptr as *const u16) }; - let number: v8::Local = - v8::Integer::new_from_unsigned(scope, result as u32).into(); - number.into() - } - NativeType::I16 => { - // SAFETY: ptr is user provided - let result = unsafe { ptr::read_unaligned(data_ptr as *const i16) }; - let number: v8::Local = - v8::Integer::new(scope, result as i32).into(); - number.into() - } - NativeType::U32 => { - // SAFETY: ptr is user provided - let result = unsafe { ptr::read_unaligned(data_ptr as *const u32) }; - let number: v8::Local = - v8::Integer::new_from_unsigned(scope, result).into(); - number.into() - } - NativeType::I32 => { - // SAFETY: ptr is user provided - let result = unsafe { ptr::read_unaligned(data_ptr as *const i32) }; - let number: v8::Local = v8::Integer::new(scope, result).into(); - number.into() - } - NativeType::U64 => { - // SAFETY: ptr is user provided - let result = unsafe { ptr::read_unaligned(data_ptr as *const u64) }; - let integer: v8::Local = if result > MAX_SAFE_INTEGER as u64 { - v8::BigInt::new_from_u64(scope, result).into() - } else { - v8::Number::new(scope, result as f64).into() - }; - integer.into() - } - NativeType::I64 => { - // SAFETY: ptr is user provided - let result = unsafe { ptr::read_unaligned(data_ptr as *const i64) }; - let integer: v8::Local = if result > MAX_SAFE_INTEGER as i64 - || result < MIN_SAFE_INTEGER as i64 - { - v8::BigInt::new_from_i64(scope, result).into() - } else { - v8::Number::new(scope, result as f64).into() - }; - integer.into() - } - NativeType::USize => { - // SAFETY: ptr is user provided - let result = unsafe { ptr::read_unaligned(data_ptr as *const usize) }; - let integer: v8::Local = if result > MAX_SAFE_INTEGER as usize - { - v8::BigInt::new_from_u64(scope, result as u64).into() - } else { - v8::Number::new(scope, result as f64).into() - }; - integer.into() - } - NativeType::ISize => { - // SAFETY: ptr is user provided - let result = unsafe { ptr::read_unaligned(data_ptr as *const isize) }; - let integer: v8::Local = - if !(MIN_SAFE_INTEGER..=MAX_SAFE_INTEGER).contains(&result) { - v8::BigInt::new_from_i64(scope, result as i64).into() - } else { - v8::Number::new(scope, result as f64).into() - }; - integer.into() - } - NativeType::F32 => { - // SAFETY: ptr is user provided - let result = unsafe { ptr::read_unaligned(data_ptr as *const f32) }; - let number: v8::Local = - v8::Number::new(scope, result as f64).into(); - number.into() - } - NativeType::F64 => { - // SAFETY: ptr is user provided - let result = unsafe { ptr::read_unaligned(data_ptr as *const f64) }; - let number: v8::Local = v8::Number::new(scope, result).into(); - number.into() - } - NativeType::Pointer | NativeType::Function | NativeType::Buffer => { - let result = data_ptr as u64; - let integer: v8::Local = if result > MAX_SAFE_INTEGER as u64 { - v8::BigInt::new_from_u64(scope, result).into() - } else { - v8::Number::new(scope, result as f64).into() - }; - integer.into() - } - }) -} - -/// A non-blocking FFI call. -#[op(v8)] -fn op_ffi_call_nonblocking<'scope>( - scope: &mut v8::HandleScope<'scope>, - state: Rc>, - rid: ResourceId, - symbol: String, - parameters: serde_v8::Value<'scope>, -) -> Result> + 'static, AnyError> { - let symbol = { - let state = state.borrow(); - let resource = state.resource_table.get::(rid)?; - let symbols = &resource.symbols; - *symbols - .get(&symbol) - .ok_or_else(|| type_error("Invalid FFI symbol name"))? - .clone() - }; - - let call_args = ffi_parse_args(scope, parameters, &symbol.parameter_types)?; - - let result_type = symbol.result_type; - let join_handle = tokio::task::spawn_blocking(move || { - let Symbol { - cif, - ptr, - parameter_types, - result_type, - .. - } = symbol.clone(); - ffi_call(call_args, &cif, ptr, ¶meter_types, result_type) - }); - - Ok(async move { - let result = join_handle - .await - .map_err(|err| anyhow!("Nonblocking FFI call failed: {}", err))??; - // SAFETY: Same return type declared to libffi; trust user to have it right beyond that. - Ok(unsafe { result.to_value(result_type) }) - }) -} - -#[op(fast)] -fn op_ffi_ptr_of( - state: &mut deno_core::OpState, - buf: *const u8, - out: &mut [u32], -) -> Result<(), AnyError> -where - FP: FfiPermissions + 'static, -{ - check_unstable(state, "Deno.UnsafePointer#of"); - let permissions = state.borrow_mut::(); - permissions.check(None)?; - - let outptr = out.as_ptr() as *mut usize; - let length = out.len(); - assert!( - length >= (std::mem::size_of::() / std::mem::size_of::()) - ); - assert_eq!(outptr as usize % std::mem::size_of::(), 0); - - // SAFETY: Out buffer was asserted to be at least large enough to hold a usize, and properly aligned. - let out = unsafe { &mut *outptr }; - *out = buf as usize; - - Ok(()) -} - -unsafe extern "C" fn noop_deleter_callback( - _data: *mut c_void, - _byte_length: usize, - _deleter_data: *mut c_void, -) { -} - -#[op(v8)] -fn op_ffi_get_buf( - scope: &mut v8::HandleScope<'scope>, - state: &mut deno_core::OpState, - ptr: usize, - offset: usize, - len: usize, -) -> Result, AnyError> -where - FP: FfiPermissions + 'static, -{ - check_unstable(state, "Deno.UnsafePointerView#arrayBuffer"); - - let permissions = state.borrow_mut::(); - permissions.check(None)?; - - let ptr = ptr as *mut c_void; - - if ptr.is_null() { - return Err(type_error("Invalid FFI pointer value, got nullptr")); - } - - // SAFETY: Offset is user defined. - let ptr = unsafe { ptr.add(offset) }; - - // SAFETY: Trust the user to have provided a real pointer, and a valid matching size to it. Since this is a foreign pointer, we should not do any deletion. - let backing_store = unsafe { - v8::ArrayBuffer::new_backing_store_from_ptr( - ptr, - len, - noop_deleter_callback, - std::ptr::null_mut(), - ) - } - .make_shared(); - let array_buffer: v8::Local = - v8::ArrayBuffer::with_backing_store(scope, &backing_store).into(); - Ok(array_buffer.into()) -} - -#[op(fast)] -fn op_ffi_buf_copy_into( - state: &mut deno_core::OpState, - src: usize, - offset: usize, - dst: &mut [u8], - len: usize, -) -> Result<(), AnyError> -where - FP: FfiPermissions + 'static, -{ - check_unstable(state, "Deno.UnsafePointerView#copyInto"); - - let permissions = state.borrow_mut::(); - permissions.check(None)?; - - if dst.len() < len { - Err(range_error( - "Destination length is smaller than source length", - )) - } else { - let src = src as *const c_void; - - // SAFETY: Offset is user defined. - let src = unsafe { src.add(offset) as *const u8 }; - - // SAFETY: src is user defined. - // dest is properly aligned and is valid for writes of len * size_of::() bytes. - unsafe { ptr::copy::(src, dst.as_mut_ptr(), len) }; - Ok(()) - } -} - -#[op(v8)] -fn op_ffi_cstr_read( - scope: &mut v8::HandleScope<'scope>, - state: &mut deno_core::OpState, - ptr: usize, - offset: usize, -) -> Result, AnyError> -where - FP: FfiPermissions + 'static, -{ - check_unstable(state, "Deno.UnsafePointerView#getCString"); - - let permissions = state.borrow_mut::(); - permissions.check(None)?; - - let ptr = ptr as *const c_void; - - if ptr.is_null() { - return Err(type_error("Invalid CString pointer, pointer is null")); - } - - // SAFETY: Offset is user defined. - let ptr = unsafe { ptr.add(offset) }; - - // SAFETY: Pointer is user provided. - let cstr = unsafe { CStr::from_ptr(ptr as *const c_char) } - .to_str() - .map_err(|_| type_error("Invalid CString pointer, not valid UTF-8"))?; - let value: v8::Local = v8::String::new(scope, cstr) - .ok_or_else(|| { - type_error("Invalid CString pointer, string exceeds max length") - })? - .into(); - Ok(value.into()) -} - -#[op(fast)] -fn op_ffi_read_bool( - state: &mut deno_core::OpState, - ptr: usize, - offset: usize, -) -> Result -where - FP: FfiPermissions + 'static, -{ - check_unstable(state, "Deno.UnsafePointerView#getBool"); - - let permissions = state.borrow_mut::(); - permissions.check(None)?; - - let ptr = ptr as *const c_void; - - if ptr.is_null() { - return Err(type_error("Invalid bool pointer, pointer is null")); - } - - // SAFETY: ptr and offset are user provided. - Ok(unsafe { ptr::read_unaligned::(ptr.add(offset) as *const bool) }) -} - -#[op(fast)] -fn op_ffi_read_u8( - state: &mut deno_core::OpState, - ptr: usize, - offset: usize, -) -> Result -where - FP: FfiPermissions + 'static, -{ - check_unstable(state, "Deno.UnsafePointerView#getUint8"); - - let permissions = state.borrow_mut::(); - permissions.check(None)?; - - let ptr = ptr as *const c_void; - - if ptr.is_null() { - return Err(type_error("Invalid u8 pointer, pointer is null")); - } - - // SAFETY: ptr and offset are user provided. - Ok(unsafe { ptr::read_unaligned::(ptr.add(offset) as *const u8) as u32 }) -} - -#[op(fast)] -fn op_ffi_read_i8( - state: &mut deno_core::OpState, - ptr: usize, - offset: usize, -) -> Result -where - FP: FfiPermissions + 'static, -{ - check_unstable(state, "Deno.UnsafePointerView#getInt8"); - - let permissions = state.borrow_mut::(); - permissions.check(None)?; - - let ptr = ptr as *const c_void; - - if ptr.is_null() { - return Err(type_error("Invalid i8 pointer, pointer is null")); - } - - // SAFETY: ptr and offset are user provided. - Ok(unsafe { ptr::read_unaligned::(ptr.add(offset) as *const i8) as i32 }) -} - -#[op(fast)] -fn op_ffi_read_u16( - state: &mut deno_core::OpState, - ptr: usize, - offset: usize, -) -> Result -where - FP: FfiPermissions + 'static, -{ - check_unstable(state, "Deno.UnsafePointerView#getUint16"); - - let permissions = state.borrow_mut::(); - permissions.check(None)?; - - let ptr = ptr as *const c_void; - - if ptr.is_null() { - return Err(type_error("Invalid u16 pointer, pointer is null")); - } - - // SAFETY: ptr and offset are user provided. - Ok(unsafe { - ptr::read_unaligned::(ptr.add(offset) as *const u16) as u32 - }) -} - -#[op(fast)] -fn op_ffi_read_i16( - state: &mut deno_core::OpState, - ptr: usize, - offset: usize, -) -> Result -where - FP: FfiPermissions + 'static, -{ - check_unstable(state, "Deno.UnsafePointerView#getInt16"); - - let permissions = state.borrow_mut::(); - permissions.check(None)?; - - let ptr = ptr as *const c_void; - - if ptr.is_null() { - return Err(type_error("Invalid i16 pointer, pointer is null")); - } - - // SAFETY: ptr and offset are user provided. - Ok(unsafe { - ptr::read_unaligned::(ptr.add(offset) as *const i16) as i32 - }) -} - -#[op(fast)] -fn op_ffi_read_u32( - state: &mut deno_core::OpState, - ptr: usize, - offset: usize, -) -> Result -where - FP: FfiPermissions + 'static, -{ - check_unstable(state, "Deno.UnsafePointerView#getUint32"); - - let permissions = state.borrow_mut::(); - permissions.check(None)?; - - let ptr = ptr as *const c_void; - - if ptr.is_null() { - return Err(type_error("Invalid u32 pointer, pointer is null")); - } - - // SAFETY: ptr and offset are user provided. - Ok(unsafe { - ptr::read_unaligned::(ptr.add(offset) as *const u32) as u32 - }) -} - -#[op(fast)] -fn op_ffi_read_i32( - state: &mut deno_core::OpState, - ptr: usize, - offset: usize, -) -> Result -where - FP: FfiPermissions + 'static, -{ - check_unstable(state, "Deno.UnsafePointerView#getInt32"); - - let permissions = state.borrow_mut::(); - permissions.check(None)?; - - let ptr = ptr as *const c_void; - - if ptr.is_null() { - return Err(type_error("Invalid i32 pointer, pointer is null")); - } - - // SAFETY: ptr and offset are user provided. - Ok(unsafe { - ptr::read_unaligned::(ptr.add(offset) as *const i32) as i32 - }) -} - -#[op] -fn op_ffi_read_u64( - state: &mut deno_core::OpState, - ptr: usize, - offset: usize, - out: &mut [u32], -) -> Result<(), AnyError> -where - FP: FfiPermissions + 'static, -{ - check_unstable(state, "Deno.UnsafePointerView#getBigUint64"); - - let permissions = state.borrow_mut::(); - permissions.check(None)?; - - let outptr = out.as_mut_ptr() as *mut u64; - - assert!( - out.len() >= (std::mem::size_of::() / std::mem::size_of::()) - ); - assert_eq!((outptr as usize % std::mem::size_of::()), 0); - - let ptr = ptr as *const c_void; - - if ptr.is_null() { - return Err(type_error("Invalid u64 pointer, pointer is null")); - } - - let value = - // SAFETY: ptr and offset are user provided. - unsafe { ptr::read_unaligned::(ptr.add(offset) as *const u64) }; - - // SAFETY: Length and alignment of out slice were asserted to be correct. - unsafe { *outptr = value }; - Ok(()) -} - -#[op(fast)] -fn op_ffi_read_i64( - state: &mut deno_core::OpState, - ptr: usize, - offset: usize, - out: &mut [u32], -) -> Result<(), AnyError> -where - FP: FfiPermissions + 'static, -{ - check_unstable(state, "Deno.UnsafePointerView#getBigUint64"); - - let permissions = state.borrow_mut::(); - permissions.check(None)?; - - let outptr = out.as_mut_ptr() as *mut i64; - - assert!( - out.len() >= (std::mem::size_of::() / std::mem::size_of::()) - ); - assert_eq!((outptr as usize % std::mem::size_of::()), 0); - - let ptr = ptr as *const c_void; - - if ptr.is_null() { - return Err(type_error("Invalid i64 pointer, pointer is null")); - } - - let value = - // SAFETY: ptr and offset are user provided. - unsafe { ptr::read_unaligned::(ptr.add(offset) as *const i64) }; - // SAFETY: Length and alignment of out slice were asserted to be correct. - unsafe { *outptr = value }; - Ok(()) -} - -#[op(fast)] -fn op_ffi_read_f32( - state: &mut deno_core::OpState, - ptr: usize, - offset: usize, -) -> Result -where - FP: FfiPermissions + 'static, -{ - check_unstable(state, "Deno.UnsafePointerView#getFloat32"); - - let permissions = state.borrow_mut::(); - permissions.check(None)?; - - let ptr = ptr as *const c_void; - - if ptr.is_null() { - return Err(type_error("Invalid f32 pointer, pointer is null")); - } - - // SAFETY: ptr and offset are user provided. - Ok(unsafe { ptr::read_unaligned::(ptr.add(offset) as *const f32) }) -} - -#[op(fast)] -fn op_ffi_read_f64( - state: &mut deno_core::OpState, - ptr: usize, - offset: usize, -) -> Result -where - FP: FfiPermissions + 'static, -{ - check_unstable(state, "Deno.UnsafePointerView#getFloat64"); - - let permissions = state.borrow_mut::(); - permissions.check(None)?; - - let ptr = ptr as *const c_void; - - if ptr.is_null() { - return Err(type_error("Invalid f64 pointer, pointer is null")); - } - - // SAFETY: ptr and offset are user provided. - Ok(unsafe { ptr::read_unaligned::(ptr.add(offset) as *const f64) }) -} - -#[cfg(test)] -mod tests { - #[cfg(target_os = "windows")] - #[test] - fn test_format_error() { - use super::format_error; - - // BAD_EXE_FORMAT - let err = dlopen::Error::OpeningLibraryError( - std::io::Error::from_raw_os_error(0x000000C1), - ); - assert_eq!( - format_error(err, "foo.dll".to_string()), - "foo.dll is not a valid Win32 application.\r\n".to_string(), - ); - } -} diff --git a/ext/ffi/repr.rs b/ext/ffi/repr.rs new file mode 100644 index 000000000..22cf03a6b --- /dev/null +++ b/ext/ffi/repr.rs @@ -0,0 +1,454 @@ +// Copyright 2018-2022 the Deno authors. All rights reserved. MIT license. + +use crate::check_unstable; +use crate::FfiPermissions; +use deno_core::error::range_error; +use deno_core::error::type_error; +use deno_core::error::AnyError; +use deno_core::op; +use deno_core::serde_v8; +use deno_core::v8; +use std::ffi::c_char; +use std::ffi::c_void; +use std::ffi::CStr; +use std::ptr; + +#[op(fast)] +pub fn op_ffi_ptr_of( + state: &mut deno_core::OpState, + buf: *const u8, + out: &mut [u32], +) -> Result<(), AnyError> +where + FP: FfiPermissions + 'static, +{ + check_unstable(state, "Deno.UnsafePointer#of"); + let permissions = state.borrow_mut::(); + permissions.check(None)?; + + let outptr = out.as_ptr() as *mut usize; + let length = out.len(); + assert!( + length >= (std::mem::size_of::() / std::mem::size_of::()) + ); + assert_eq!(outptr as usize % std::mem::size_of::(), 0); + + // SAFETY: Out buffer was asserted to be at least large enough to hold a usize, and properly aligned. + let out = unsafe { &mut *outptr }; + *out = buf as usize; + + Ok(()) +} + +unsafe extern "C" fn noop_deleter_callback( + _data: *mut c_void, + _byte_length: usize, + _deleter_data: *mut c_void, +) { +} + +#[op(v8)] +pub fn op_ffi_get_buf( + scope: &mut v8::HandleScope<'scope>, + state: &mut deno_core::OpState, + ptr: usize, + offset: usize, + len: usize, +) -> Result, AnyError> +where + FP: FfiPermissions + 'static, +{ + check_unstable(state, "Deno.UnsafePointerView#arrayBuffer"); + + let permissions = state.borrow_mut::(); + permissions.check(None)?; + + let ptr = ptr as *mut c_void; + + if ptr.is_null() { + return Err(type_error("Invalid FFI pointer value, got nullptr")); + } + + // SAFETY: Offset is user defined. + let ptr = unsafe { ptr.add(offset) }; + + // SAFETY: Trust the user to have provided a real pointer, and a valid matching size to it. Since this is a foreign pointer, we should not do any deletion. + let backing_store = unsafe { + v8::ArrayBuffer::new_backing_store_from_ptr( + ptr, + len, + noop_deleter_callback, + std::ptr::null_mut(), + ) + } + .make_shared(); + let array_buffer: v8::Local = + v8::ArrayBuffer::with_backing_store(scope, &backing_store).into(); + Ok(array_buffer.into()) +} + +#[op(fast)] +pub fn op_ffi_buf_copy_into( + state: &mut deno_core::OpState, + src: usize, + offset: usize, + dst: &mut [u8], + len: usize, +) -> Result<(), AnyError> +where + FP: FfiPermissions + 'static, +{ + check_unstable(state, "Deno.UnsafePointerView#copyInto"); + + let permissions = state.borrow_mut::(); + permissions.check(None)?; + + if dst.len() < len { + Err(range_error( + "Destination length is smaller than source length", + )) + } else { + let src = src as *const c_void; + + // SAFETY: Offset is user defined. + let src = unsafe { src.add(offset) as *const u8 }; + + // SAFETY: src is user defined. + // dest is properly aligned and is valid for writes of len * size_of::() bytes. + unsafe { ptr::copy::(src, dst.as_mut_ptr(), len) }; + Ok(()) + } +} + +#[op(v8)] +pub fn op_ffi_cstr_read( + scope: &mut v8::HandleScope<'scope>, + state: &mut deno_core::OpState, + ptr: usize, + offset: usize, +) -> Result, AnyError> +where + FP: FfiPermissions + 'static, +{ + check_unstable(state, "Deno.UnsafePointerView#getCString"); + + let permissions = state.borrow_mut::(); + permissions.check(None)?; + + let ptr = ptr as *const c_void; + + if ptr.is_null() { + return Err(type_error("Invalid CString pointer, pointer is null")); + } + + // SAFETY: Offset is user defined. + let ptr = unsafe { ptr.add(offset) }; + + // SAFETY: Pointer is user provided. + let cstr = unsafe { CStr::from_ptr(ptr as *const c_char) } + .to_str() + .map_err(|_| type_error("Invalid CString pointer, not valid UTF-8"))?; + let value: v8::Local = v8::String::new(scope, cstr) + .ok_or_else(|| { + type_error("Invalid CString pointer, string exceeds max length") + })? + .into(); + Ok(value.into()) +} + +#[op(fast)] +pub fn op_ffi_read_bool( + state: &mut deno_core::OpState, + ptr: usize, + offset: usize, +) -> Result +where + FP: FfiPermissions + 'static, +{ + check_unstable(state, "Deno.UnsafePointerView#getBool"); + + let permissions = state.borrow_mut::(); + permissions.check(None)?; + + let ptr = ptr as *const c_void; + + if ptr.is_null() { + return Err(type_error("Invalid bool pointer, pointer is null")); + } + + // SAFETY: ptr and offset are user provided. + Ok(unsafe { ptr::read_unaligned::(ptr.add(offset) as *const bool) }) +} + +#[op(fast)] +pub fn op_ffi_read_u8( + state: &mut deno_core::OpState, + ptr: usize, + offset: usize, +) -> Result +where + FP: FfiPermissions + 'static, +{ + check_unstable(state, "Deno.UnsafePointerView#getUint8"); + + let permissions = state.borrow_mut::(); + permissions.check(None)?; + + let ptr = ptr as *const c_void; + + if ptr.is_null() { + return Err(type_error("Invalid u8 pointer, pointer is null")); + } + + // SAFETY: ptr and offset are user provided. + Ok(unsafe { ptr::read_unaligned::(ptr.add(offset) as *const u8) as u32 }) +} + +#[op(fast)] +pub fn op_ffi_read_i8( + state: &mut deno_core::OpState, + ptr: usize, + offset: usize, +) -> Result +where + FP: FfiPermissions + 'static, +{ + check_unstable(state, "Deno.UnsafePointerView#getInt8"); + + let permissions = state.borrow_mut::(); + permissions.check(None)?; + + let ptr = ptr as *const c_void; + + if ptr.is_null() { + return Err(type_error("Invalid i8 pointer, pointer is null")); + } + + // SAFETY: ptr and offset are user provided. + Ok(unsafe { ptr::read_unaligned::(ptr.add(offset) as *const i8) as i32 }) +} + +#[op(fast)] +pub fn op_ffi_read_u16( + state: &mut deno_core::OpState, + ptr: usize, + offset: usize, +) -> Result +where + FP: FfiPermissions + 'static, +{ + check_unstable(state, "Deno.UnsafePointerView#getUint16"); + + let permissions = state.borrow_mut::(); + permissions.check(None)?; + + let ptr = ptr as *const c_void; + + if ptr.is_null() { + return Err(type_error("Invalid u16 pointer, pointer is null")); + } + + // SAFETY: ptr and offset are user provided. + Ok(unsafe { + ptr::read_unaligned::(ptr.add(offset) as *const u16) as u32 + }) +} + +#[op(fast)] +pub fn op_ffi_read_i16( + state: &mut deno_core::OpState, + ptr: usize, + offset: usize, +) -> Result +where + FP: FfiPermissions + 'static, +{ + check_unstable(state, "Deno.UnsafePointerView#getInt16"); + + let permissions = state.borrow_mut::(); + permissions.check(None)?; + + let ptr = ptr as *const c_void; + + if ptr.is_null() { + return Err(type_error("Invalid i16 pointer, pointer is null")); + } + + // SAFETY: ptr and offset are user provided. + Ok(unsafe { + ptr::read_unaligned::(ptr.add(offset) as *const i16) as i32 + }) +} + +#[op(fast)] +pub fn op_ffi_read_u32( + state: &mut deno_core::OpState, + ptr: usize, + offset: usize, +) -> Result +where + FP: FfiPermissions + 'static, +{ + check_unstable(state, "Deno.UnsafePointerView#getUint32"); + + let permissions = state.borrow_mut::(); + permissions.check(None)?; + + let ptr = ptr as *const c_void; + + if ptr.is_null() { + return Err(type_error("Invalid u32 pointer, pointer is null")); + } + + // SAFETY: ptr and offset are user provided. + Ok(unsafe { + ptr::read_unaligned::(ptr.add(offset) as *const u32) as u32 + }) +} + +#[op(fast)] +pub fn op_ffi_read_i32( + state: &mut deno_core::OpState, + ptr: usize, + offset: usize, +) -> Result +where + FP: FfiPermissions + 'static, +{ + check_unstable(state, "Deno.UnsafePointerView#getInt32"); + + let permissions = state.borrow_mut::(); + permissions.check(None)?; + + let ptr = ptr as *const c_void; + + if ptr.is_null() { + return Err(type_error("Invalid i32 pointer, pointer is null")); + } + + // SAFETY: ptr and offset are user provided. + Ok(unsafe { + ptr::read_unaligned::(ptr.add(offset) as *const i32) as i32 + }) +} + +#[op] +pub fn op_ffi_read_u64( + state: &mut deno_core::OpState, + ptr: usize, + offset: usize, + out: &mut [u32], +) -> Result<(), AnyError> +where + FP: FfiPermissions + 'static, +{ + check_unstable(state, "Deno.UnsafePointerView#getBigUint64"); + + let permissions = state.borrow_mut::(); + permissions.check(None)?; + + let outptr = out.as_mut_ptr() as *mut u64; + + assert!( + out.len() >= (std::mem::size_of::() / std::mem::size_of::()) + ); + assert_eq!((outptr as usize % std::mem::size_of::()), 0); + + let ptr = ptr as *const c_void; + + if ptr.is_null() { + return Err(type_error("Invalid u64 pointer, pointer is null")); + } + + let value = + // SAFETY: ptr and offset are user provided. + unsafe { ptr::read_unaligned::(ptr.add(offset) as *const u64) }; + + // SAFETY: Length and alignment of out slice were asserted to be correct. + unsafe { *outptr = value }; + Ok(()) +} + +#[op(fast)] +pub fn op_ffi_read_i64( + state: &mut deno_core::OpState, + ptr: usize, + offset: usize, + out: &mut [u32], +) -> Result<(), AnyError> +where + FP: FfiPermissions + 'static, +{ + check_unstable(state, "Deno.UnsafePointerView#getBigUint64"); + + let permissions = state.borrow_mut::(); + permissions.check(None)?; + + let outptr = out.as_mut_ptr() as *mut i64; + + assert!( + out.len() >= (std::mem::size_of::() / std::mem::size_of::()) + ); + assert_eq!((outptr as usize % std::mem::size_of::()), 0); + + let ptr = ptr as *const c_void; + + if ptr.is_null() { + return Err(type_error("Invalid i64 pointer, pointer is null")); + } + + let value = + // SAFETY: ptr and offset are user provided. + unsafe { ptr::read_unaligned::(ptr.add(offset) as *const i64) }; + // SAFETY: Length and alignment of out slice were asserted to be correct. + unsafe { *outptr = value }; + Ok(()) +} + +#[op(fast)] +pub fn op_ffi_read_f32( + state: &mut deno_core::OpState, + ptr: usize, + offset: usize, +) -> Result +where + FP: FfiPermissions + 'static, +{ + check_unstable(state, "Deno.UnsafePointerView#getFloat32"); + + let permissions = state.borrow_mut::(); + permissions.check(None)?; + + let ptr = ptr as *const c_void; + + if ptr.is_null() { + return Err(type_error("Invalid f32 pointer, pointer is null")); + } + + // SAFETY: ptr and offset are user provided. + Ok(unsafe { ptr::read_unaligned::(ptr.add(offset) as *const f32) }) +} + +#[op(fast)] +pub fn op_ffi_read_f64( + state: &mut deno_core::OpState, + ptr: usize, + offset: usize, +) -> Result +where + FP: FfiPermissions + 'static, +{ + check_unstable(state, "Deno.UnsafePointerView#getFloat64"); + + let permissions = state.borrow_mut::(); + permissions.check(None)?; + + let ptr = ptr as *const c_void; + + if ptr.is_null() { + return Err(type_error("Invalid f64 pointer, pointer is null")); + } + + // SAFETY: ptr and offset are user provided. + Ok(unsafe { ptr::read_unaligned::(ptr.add(offset) as *const f64) }) +} diff --git a/ext/ffi/static.rs b/ext/ffi/static.rs new file mode 100644 index 000000000..aa0bb325c --- /dev/null +++ b/ext/ffi/static.rs @@ -0,0 +1,146 @@ +// Copyright 2018-2022 the Deno authors. All rights reserved. MIT license. + +use crate::dlfcn::DynamicLibraryResource; +use crate::symbol::NativeType; +use crate::MAX_SAFE_INTEGER; +use crate::MIN_SAFE_INTEGER; +use deno_core::error::type_error; +use deno_core::error::AnyError; +use deno_core::op; +use deno_core::serde_v8; +use deno_core::v8; +use deno_core::ResourceId; +use std::ptr; + +#[op(v8)] +pub fn op_ffi_get_static<'scope>( + scope: &mut v8::HandleScope<'scope>, + state: &mut deno_core::OpState, + rid: ResourceId, + name: String, + static_type: NativeType, +) -> Result, AnyError> { + let resource = state.resource_table.get::(rid)?; + + let data_ptr = resource.get_static(name)?; + + Ok(match static_type { + NativeType::Void => { + return Err(type_error("Invalid FFI static type 'void'")); + } + NativeType::Bool => { + // SAFETY: ptr is user provided + let result = unsafe { ptr::read_unaligned(data_ptr as *const bool) }; + let boolean: v8::Local = + v8::Boolean::new(scope, result).into(); + boolean.into() + } + NativeType::U8 => { + // SAFETY: ptr is user provided + let result = unsafe { ptr::read_unaligned(data_ptr as *const u8) }; + let number: v8::Local = + v8::Integer::new_from_unsigned(scope, result as u32).into(); + number.into() + } + NativeType::I8 => { + // SAFETY: ptr is user provided + let result = unsafe { ptr::read_unaligned(data_ptr as *const i8) }; + let number: v8::Local = + v8::Integer::new(scope, result as i32).into(); + number.into() + } + NativeType::U16 => { + // SAFETY: ptr is user provided + let result = unsafe { ptr::read_unaligned(data_ptr as *const u16) }; + let number: v8::Local = + v8::Integer::new_from_unsigned(scope, result as u32).into(); + number.into() + } + NativeType::I16 => { + // SAFETY: ptr is user provided + let result = unsafe { ptr::read_unaligned(data_ptr as *const i16) }; + let number: v8::Local = + v8::Integer::new(scope, result as i32).into(); + number.into() + } + NativeType::U32 => { + // SAFETY: ptr is user provided + let result = unsafe { ptr::read_unaligned(data_ptr as *const u32) }; + let number: v8::Local = + v8::Integer::new_from_unsigned(scope, result).into(); + number.into() + } + NativeType::I32 => { + // SAFETY: ptr is user provided + let result = unsafe { ptr::read_unaligned(data_ptr as *const i32) }; + let number: v8::Local = v8::Integer::new(scope, result).into(); + number.into() + } + NativeType::U64 => { + // SAFETY: ptr is user provided + let result = unsafe { ptr::read_unaligned(data_ptr as *const u64) }; + let integer: v8::Local = if result > MAX_SAFE_INTEGER as u64 { + v8::BigInt::new_from_u64(scope, result).into() + } else { + v8::Number::new(scope, result as f64).into() + }; + integer.into() + } + NativeType::I64 => { + // SAFETY: ptr is user provided + let result = unsafe { ptr::read_unaligned(data_ptr as *const i64) }; + let integer: v8::Local = if result > MAX_SAFE_INTEGER as i64 + || result < MIN_SAFE_INTEGER as i64 + { + v8::BigInt::new_from_i64(scope, result).into() + } else { + v8::Number::new(scope, result as f64).into() + }; + integer.into() + } + NativeType::USize => { + // SAFETY: ptr is user provided + let result = unsafe { ptr::read_unaligned(data_ptr as *const usize) }; + let integer: v8::Local = if result > MAX_SAFE_INTEGER as usize + { + v8::BigInt::new_from_u64(scope, result as u64).into() + } else { + v8::Number::new(scope, result as f64).into() + }; + integer.into() + } + NativeType::ISize => { + // SAFETY: ptr is user provided + let result = unsafe { ptr::read_unaligned(data_ptr as *const isize) }; + let integer: v8::Local = + if !(MIN_SAFE_INTEGER..=MAX_SAFE_INTEGER).contains(&result) { + v8::BigInt::new_from_i64(scope, result as i64).into() + } else { + v8::Number::new(scope, result as f64).into() + }; + integer.into() + } + NativeType::F32 => { + // SAFETY: ptr is user provided + let result = unsafe { ptr::read_unaligned(data_ptr as *const f32) }; + let number: v8::Local = + v8::Number::new(scope, result as f64).into(); + number.into() + } + NativeType::F64 => { + // SAFETY: ptr is user provided + let result = unsafe { ptr::read_unaligned(data_ptr as *const f64) }; + let number: v8::Local = v8::Number::new(scope, result).into(); + number.into() + } + NativeType::Pointer | NativeType::Function | NativeType::Buffer => { + let result = data_ptr as u64; + let integer: v8::Local = if result > MAX_SAFE_INTEGER as u64 { + v8::BigInt::new_from_u64(scope, result).into() + } else { + v8::Number::new(scope, result as f64).into() + }; + integer.into() + } + }) +} diff --git a/ext/ffi/symbol.rs b/ext/ffi/symbol.rs new file mode 100644 index 000000000..0248c1fff --- /dev/null +++ b/ext/ffi/symbol.rs @@ -0,0 +1,63 @@ +// Copyright 2018-2022 the Deno authors. All rights reserved. MIT license. + +/// Defines the accepted types that can be used as +/// parameters and return values in FFI. +#[derive(Clone, Copy, Debug, serde::Deserialize, Eq, PartialEq)] +#[serde(rename_all = "lowercase")] +pub enum NativeType { + Void, + Bool, + U8, + I8, + U16, + I16, + U32, + I32, + U64, + I64, + USize, + ISize, + F32, + F64, + Pointer, + Buffer, + Function, +} + +impl From for libffi::middle::Type { + fn from(native_type: NativeType) -> Self { + match native_type { + NativeType::Void => libffi::middle::Type::void(), + NativeType::U8 | NativeType::Bool => libffi::middle::Type::u8(), + NativeType::I8 => libffi::middle::Type::i8(), + NativeType::U16 => libffi::middle::Type::u16(), + NativeType::I16 => libffi::middle::Type::i16(), + NativeType::U32 => libffi::middle::Type::u32(), + NativeType::I32 => libffi::middle::Type::i32(), + NativeType::U64 => libffi::middle::Type::u64(), + NativeType::I64 => libffi::middle::Type::i64(), + NativeType::USize => libffi::middle::Type::usize(), + NativeType::ISize => libffi::middle::Type::isize(), + NativeType::F32 => libffi::middle::Type::f32(), + NativeType::F64 => libffi::middle::Type::f64(), + NativeType::Pointer | NativeType::Buffer | NativeType::Function => { + libffi::middle::Type::pointer() + } + } + } +} + +#[derive(Clone)] +pub struct Symbol { + pub cif: libffi::middle::Cif, + pub ptr: libffi::middle::CodePtr, + pub parameter_types: Vec, + pub result_type: NativeType, + pub can_callback: bool, +} + +#[allow(clippy::non_send_fields_in_send_ty)] +// SAFETY: unsafe trait must have unsafe implementation +unsafe impl Send for Symbol {} +// SAFETY: unsafe trait must have unsafe implementation +unsafe impl Sync for Symbol {} diff --git a/ext/ffi/turbocall.rs b/ext/ffi/turbocall.rs new file mode 100644 index 000000000..79ec814b4 --- /dev/null +++ b/ext/ffi/turbocall.rs @@ -0,0 +1,2065 @@ +// Copyright 2018-2022 the Deno authors. All rights reserved. MIT license. + +use std::cmp::max; +use std::ffi::c_void; +use std::iter::once; + +use deno_core::v8::fast_api; +use dynasmrt::dynasm; +use dynasmrt::DynasmApi; +use dynasmrt::ExecutableBuffer; + +use crate::dlfcn::needs_unwrap; +use crate::NativeType; +use crate::Symbol; + +pub(crate) fn is_compatible(sym: &Symbol) -> bool { + cfg!(any( + all(target_arch = "x86_64", target_family = "unix"), + all(target_arch = "x86_64", target_family = "windows"), + all(target_arch = "aarch64", target_vendor = "apple") + )) && !sym.can_callback +} + +pub(crate) fn compile_trampoline(sym: &Symbol) -> Trampoline { + #[cfg(all(target_arch = "x86_64", target_family = "unix"))] + return SysVAmd64::compile(sym); + #[cfg(all(target_arch = "x86_64", target_family = "windows"))] + return Win64::compile(sym); + #[cfg(all(target_arch = "aarch64", target_vendor = "apple"))] + return Aarch64Apple::compile(sym); + #[allow(unreachable_code)] + { + unimplemented!("fast API is not implemented for the current target"); + } +} + +pub(crate) fn make_template(sym: &Symbol, trampoline: &Trampoline) -> Template { + let mut params = once(fast_api::Type::V8Value) // Receiver + .chain(sym.parameter_types.iter().map(|t| t.into())) + .collect::>(); + + let ret = if needs_unwrap(sym.result_type) { + params.push(fast_api::Type::TypedArray(fast_api::CType::Int32)); + fast_api::Type::Void + } else { + fast_api::Type::from(&sym.result_type) + }; + + Template { + args: params.into_boxed_slice(), + ret: (&ret).into(), + symbol_ptr: trampoline.ptr(), + } +} + +/// Trampoline for fast-call FFI functions +/// +/// Calls the FFI function without the first argument (the receiver) +pub(crate) struct Trampoline(ExecutableBuffer); + +impl Trampoline { + fn ptr(&self) -> *const c_void { + &self.0[0] as *const u8 as *const c_void + } +} + +pub(crate) struct Template { + args: Box<[fast_api::Type]>, + ret: fast_api::CType, + symbol_ptr: *const c_void, +} + +impl fast_api::FastFunction for Template { + fn function(&self) -> *const c_void { + self.symbol_ptr + } + + fn args(&self) -> &'static [fast_api::Type] { + Box::leak(self.args.clone()) + } + + fn return_type(&self) -> fast_api::CType { + self.ret + } +} + +impl From<&NativeType> for fast_api::Type { + fn from(native_type: &NativeType) -> Self { + match native_type { + NativeType::Bool => fast_api::Type::Bool, + NativeType::U8 | NativeType::U16 | NativeType::U32 => { + fast_api::Type::Uint32 + } + NativeType::I8 | NativeType::I16 | NativeType::I32 => { + fast_api::Type::Int32 + } + NativeType::F32 => fast_api::Type::Float32, + NativeType::F64 => fast_api::Type::Float64, + NativeType::Void => fast_api::Type::Void, + NativeType::I64 => fast_api::Type::Int64, + NativeType::U64 => fast_api::Type::Uint64, + NativeType::ISize => fast_api::Type::Int64, + NativeType::USize | NativeType::Pointer | NativeType::Function => { + fast_api::Type::Uint64 + } + NativeType::Buffer => fast_api::Type::TypedArray(fast_api::CType::Uint8), + } + } +} + +macro_rules! x64 { + ($assembler:expr; $($tokens:tt)+) => { + dynasm!($assembler; .arch x64; $($tokens)+) + } +} + +macro_rules! aarch64 { + ($assembler:expr; $($tokens:tt)+) => { + dynasm!($assembler; .arch aarch64; $($tokens)+) + } +} + +struct SysVAmd64 { + // Reference: https://refspecs.linuxfoundation.org/elf/x86_64-abi-0.99.pdf + assmblr: dynasmrt::x64::Assembler, + // Parameter counters + integral_params: u32, + float_params: u32, + // Stack offset accumulators + offset_trampoline: u32, + offset_callee: u32, + allocated_stack: u32, + frame_pointer: u32, +} + +#[cfg_attr( + not(all(target_aarch = "x86_64", target_family = "unix")), + allow(dead_code) +)] +impl SysVAmd64 { + // Integral arguments go to the following GPR, in order: rdi, rsi, rdx, rcx, r8, r9 + const INTEGRAL_REGISTERS: u32 = 6; + // SSE arguments go to the first 8 SSE registers: xmm0-xmm7 + const FLOAT_REGISTERS: u32 = 8; + + fn new() -> Self { + Self { + assmblr: dynasmrt::x64::Assembler::new().unwrap(), + integral_params: 0, + float_params: 0, + // Start at 8 to account for trampoline caller's return address + offset_trampoline: 8, + // default to tail-call mode. If a new stack frame is allocated this becomes 0 + offset_callee: 8, + allocated_stack: 0, + frame_pointer: 0, + } + } + + fn compile(sym: &Symbol) -> Trampoline { + let mut compiler = Self::new(); + + let must_cast_return_value = + compiler.must_cast_return_value(sym.result_type); + let must_wrap_return_value = + compiler.must_wrap_return_value_in_typed_array(sym.result_type); + let must_save_preserved_register = must_wrap_return_value; + let cannot_tailcall = must_cast_return_value || must_wrap_return_value; + + if cannot_tailcall { + if must_save_preserved_register { + compiler.save_preserved_register_to_stack(); + } + compiler.allocate_stack(&sym.parameter_types); + } + + for param in sym.parameter_types.iter().copied() { + compiler.move_left(param) + } + if !compiler.is_recv_arg_overridden() { + // the receiver object should never be expected. Avoid its unexpected or deliberate leak + compiler.zero_first_arg(); + } + if must_wrap_return_value { + compiler.save_out_array_to_preserved_register(); + } + + if cannot_tailcall { + compiler.call(sym.ptr.as_ptr()); + if must_cast_return_value { + compiler.cast_return_value(sym.result_type); + } + if must_wrap_return_value { + compiler.wrap_return_value_in_out_array(); + } + compiler.deallocate_stack(); + if must_save_preserved_register { + compiler.recover_preserved_register(); + } + compiler.ret(); + } else { + compiler.tailcall(sym.ptr.as_ptr()); + } + + Trampoline(compiler.finalize()) + } + + fn move_left(&mut self, param: NativeType) { + // Section 3.2.3 of the SysV ABI spec, on argument classification: + // - INTEGER: + // > Arguments of types (signed and unsigned) _Bool, char, short, int, + // > long, long long, and pointers are in the INTEGER class. + // - SSE: + // > Arguments of types float, double, _Decimal32, _Decimal64 and + // > __m64 are in class SSE. + match param.into() { + Int(integral) => self.move_integral(integral), + Float(float) => self.move_float(float), + } + } + + fn move_float(&mut self, param: Floating) { + // Section 3.2.3 of the SysV AMD64 ABI: + // > If the class is SSE, the next available vector register is used, the registers + // > are taken in the order from %xmm0 to %xmm7. + // [...] + // > Once registers are assigned, the arguments passed in memory are pushed on + // > the stack in reversed (right-to-left) order + let param_i = self.float_params; + + let is_in_stack = param_i >= Self::FLOAT_REGISTERS; + // floats are only moved to accommodate integer movement in the stack + let stack_has_moved = self.allocated_stack > 0 + || self.integral_params >= Self::INTEGRAL_REGISTERS; + + if is_in_stack && stack_has_moved { + let s = &mut self.assmblr; + let ot = self.offset_trampoline as i32; + let oc = self.offset_callee as i32; + match param { + Single => x64!(s + ; movss xmm8, [rsp + ot] + ; movss [rsp + oc], xmm8 + ), + Double => x64!(s + ; movsd xmm8, [rsp + ot] + ; movsd [rsp + oc], xmm8 + ), + } + + // Section 3.2.3 of the SysV AMD64 ABI: + // > The size of each argument gets rounded up to eightbytes. [...] Therefore the stack will always be eightbyte aligned. + self.offset_trampoline += 8; + self.offset_callee += 8; + + debug_assert!( + self.allocated_stack == 0 || self.offset_callee <= self.allocated_stack + ); + } + self.float_params += 1; + } + + fn move_integral(&mut self, arg: Integral) { + // Section 3.2.3 of the SysV AMD64 ABI: + // > If the class is INTEGER, the next available register of the sequence %rdi, + // > %rsi, %rdx, %rcx, %r8 and %r9 is used + // [...] + // > Once registers are assigned, the arguments passed in memory are pushed on + // > the stack in reversed (right-to-left) order + let s = &mut self.assmblr; + let param_i = self.integral_params; + + // move each argument one position to the left. The first argument in the stack moves to the last integer register (r9). + // If the FFI function is called with a new stack frame, the arguments remaining in the stack are copied to the new stack frame. + // Otherwise, they are copied 8 bytes lower in the same frame + match (param_i, arg) { + // u8 and u16 parameters are defined as u32 parameters in the V8's fast API function. The trampoline takes care of the cast. + // Conventionally, many compilers expect 8 and 16 bit arguments to be sign/zero extended by the caller + // See https://stackoverflow.com/a/36760539/2623340 + (0, U(B)) => x64!(s; movzx edi, sil), + (0, I(B)) => x64!(s; movsx edi, sil), + (0, U(W)) => x64!(s; movzx edi, si), + (0, I(W)) => x64!(s; movsx edi, si), + (0, U(DW) | I(DW)) => x64!(s; mov edi, esi), + (0, U(QW) | I(QW)) => x64!(s; mov rdi, rsi), + // The fast API expects buffer arguments passed as a pointer to a FastApiTypedArray struct + // Here we blindly follow the layout of https://github.com/denoland/rusty_v8/blob/main/src/fast_api.rs#L190-L200 + // although that might be problematic: https://discord.com/channels/684898665143206084/956626010248478720/1009450940866252823 + (0, Buffer) => x64!(s; mov rdi, [rsi + 8]), + + (1, U(B)) => x64!(s; movzx esi, dl), + (1, I(B)) => x64!(s; movsx esi, dl), + (1, U(W)) => x64!(s; movzx esi, dx), + (1, I(W)) => x64!(s; movsx esi, dx), + (1, U(DW) | I(DW)) => x64!(s; mov esi, edx), + (1, U(QW) | I(QW)) => x64!(s; mov rsi, rdx), + (1, Buffer) => x64!(s; mov rsi, [rdx + 8]), + + (2, U(B)) => x64!(s; movzx edx, cl), + (2, I(B)) => x64!(s; movsx edx, cl), + (2, U(W)) => x64!(s; movzx edx, cx), + (2, I(W)) => x64!(s; movsx edx, cx), + (2, U(DW) | I(DW)) => x64!(s; mov edx, ecx), + (2, U(QW) | I(QW)) => x64!(s; mov rdx, rcx), + (2, Buffer) => x64!(s; mov rdx, [rcx + 8]), + + (3, U(B)) => x64!(s; movzx ecx, r8b), + (3, I(B)) => x64!(s; movsx ecx, r8b), + (3, U(W)) => x64!(s; movzx ecx, r8w), + (3, I(W)) => x64!(s; movsx ecx, r8w), + (3, U(DW) | I(DW)) => x64!(s; mov ecx, r8d), + (3, U(QW) | I(QW)) => x64!(s; mov rcx, r8), + (3, Buffer) => x64!(s; mov rcx, [r8 + 8]), + + (4, U(B)) => x64!(s; movzx r8d, r9b), + (4, I(B)) => x64!(s; movsx r8d, r9b), + (4, U(W)) => x64!(s; movzx r8d, r9w), + (4, I(W)) => x64!(s; movsx r8d, r9w), + (4, U(DW) | I(DW)) => x64!(s; mov r8d, r9d), + (4, U(QW) | I(QW)) => x64!(s; mov r8, r9), + (4, Buffer) => x64!(s; mov r8, [r9 + 8]), + + (5, param) => { + let ot = self.offset_trampoline as i32; + // First argument in stack goes to last register (r9) + match param { + U(B) => x64!(s; movzx r9d, BYTE [rsp + ot]), + I(B) => x64!(s; movsx r9d, BYTE [rsp + ot]), + U(W) => x64!(s; movzx r9d, WORD [rsp + ot]), + I(W) => x64!(s; movsx r9d, WORD [rsp + ot]), + U(DW) | I(DW) => x64!(s; mov r9d, [rsp + ot]), + U(QW) | I(QW) => x64!(s; mov r9, [rsp + ot]), + Buffer => x64!(s + ; mov r9, [rsp + ot] + ; mov r9, [r9 + 8] + ), + } + // Section 3.2.3 of the SysV AMD64 ABI: + // > The size of each argument gets rounded up to eightbytes. [...] Therefore the stack will always be eightbyte aligned. + self.offset_trampoline += 8; + } + + (6.., param) => { + let ot = self.offset_trampoline as i32; + let oc = self.offset_callee as i32; + match param { + U(B) => x64!(s + // TODO: optimize to [rsp] (without immediate) when offset is 0 + ; movzx eax, BYTE [rsp + ot] + ; mov [rsp + oc], eax + ), + I(B) => x64!(s + ; movsx eax, BYTE [rsp + ot] + ; mov [rsp + oc], eax + ), + U(W) => x64!(s + ; movzx eax, WORD [rsp + ot] + ; mov [rsp + oc], eax + ), + I(W) => x64!(s + ; movsx eax, WORD [rsp + ot] + ; mov [rsp + oc], eax + ), + U(DW) | I(DW) => x64!(s + ; mov eax, [rsp + ot] + ; mov [rsp + oc], eax + ), + U(QW) | I(QW) => x64!(s + ; mov rax, [rsp + ot] + ; mov [rsp + oc], rax + ), + Buffer => x64!(s + ; mov rax, [rsp + ot] + ; mov rax, [rax + 8] + ; mov [rsp + oc], rax + ), + } + // Section 3.2.3 of the SysV AMD64 ABI: + // > The size of each argument gets rounded up to eightbytes. [...] Therefore the stack will always be eightbyte aligned. + self.offset_trampoline += 8; + self.offset_callee += 8; + + debug_assert!( + self.allocated_stack == 0 + || self.offset_callee <= self.allocated_stack + ); + } + } + self.integral_params += 1; + } + + fn zero_first_arg(&mut self) { + debug_assert!( + self.integral_params == 0, + "the trampoline would zero the first argument after having overridden it with the second one" + ); + dynasm!(self.assmblr + ; .arch x64 + ; xor edi, edi + ); + } + + fn cast_return_value(&mut self, rv: NativeType) { + let s = &mut self.assmblr; + // V8 only supports 32bit integers. We support 8 and 16 bit integers casting them to 32bits. + // In SysV-AMD64 the convention dictates that the unused bits of the return value contain garbage, so we + // need to zero/sign extend the return value explicitly + match rv { + NativeType::U8 => x64!(s; movzx eax, al), + NativeType::I8 => x64!(s; movsx eax, al), + NativeType::U16 => x64!(s; movzx eax, ax), + NativeType::I16 => x64!(s; movsx eax, ax), + _ => (), + } + } + + fn save_out_array_to_preserved_register(&mut self) { + let s = &mut self.assmblr; + // functions returning 64 bit integers have the out array appended as their last parameter, + // and it is a *FastApiTypedArray + match self.integral_params { + // Trampoline's signature is (receiver, [param0, param1, ...], *FastApiTypedArray) + // self.integral_params account only for the original params [param0, param1, ...] + // and the out array has not been moved left + 0 => x64!(s; mov rbx, [rsi + 8]), + 1 => x64!(s; mov rbx, [rdx + 8]), + 2 => x64!(s; mov rbx, [rcx + 8]), + 3 => x64!(s; mov rbx, [r8 + 8]), + 4 => x64!(s; mov rbx, [r9 + 8]), + 5.. => { + x64!(s + ; mov rax, [rsp + self.offset_trampoline as i32] + ; mov rbx, [rax + 8] + ) + } + } + } + + fn wrap_return_value_in_out_array(&mut self) { + x64!(self.assmblr; mov [rbx], rax); + } + + fn save_preserved_register_to_stack(&mut self) { + x64!(self.assmblr; push rbx); + self.offset_trampoline += 8; + // stack pointer has been modified, and the callee stack parameters are expected at the top of the stack + self.offset_callee = 0; + self.frame_pointer += 8; + } + + fn recover_preserved_register(&mut self) { + debug_assert!( + self.frame_pointer >= 8, + "the trampoline would try to pop from the stack beyond its frame pointer" + ); + x64!(self.assmblr; pop rbx); + self.frame_pointer -= 8; + // parameter offsets are invalid once this method is called + } + + fn allocate_stack(&mut self, params: &[NativeType]) { + let mut int_params = 0u32; + let mut float_params = 0u32; + for param in params { + match param { + NativeType::F32 | NativeType::F64 => float_params += 1, + _ => int_params += 1, + } + } + let mut stack_size = (int_params.saturating_sub(Self::INTEGRAL_REGISTERS) + + float_params.saturating_sub(Self::FLOAT_REGISTERS)) + * 8; + + // Align new stack frame (accounting for the 8 byte of the trampoline caller's return address + // and any other potential addition to the stack prior to this allocation) + // Section 3.2.2 of the SysV AMD64 ABI: + // > The end of the input argument area shall be aligned on a 16 (32 or 64, if + // > __m256 or __m512 is passed on stack) byte boundary. In other words, the value + // > (%rsp + 8) is always a multiple of 16 (32 or 64) when control is transferred to + // > the function entry point. The stack pointer, %rsp, always points to the end of the + // > latest allocated stack frame. + stack_size += padding_to_align(16, self.frame_pointer + stack_size + 8); + + if stack_size > 0 { + x64!(self.assmblr; sub rsp, stack_size as i32); + self.offset_trampoline += stack_size; + // stack pointer has been modified, and the callee stack parameters are expected at the top of the stack + self.offset_callee = 0; + self.allocated_stack += stack_size; + self.frame_pointer += stack_size; + } + } + + fn deallocate_stack(&mut self) { + debug_assert!( + self.frame_pointer >= self.allocated_stack, + "the trampoline would try to deallocate stack beyond its frame pointer" + ); + if self.allocated_stack > 0 { + x64!(self.assmblr; add rsp, self.allocated_stack as i32); + + self.frame_pointer -= self.allocated_stack; + self.allocated_stack = 0; + } + } + + fn call(&mut self, ptr: *const c_void) { + // the stack has been aligned during stack allocation and/or pushing of preserved registers + debug_assert!( + (8 + self.frame_pointer) % 16 == 0, + "the trampoline would call the FFI function with an unaligned stack" + ); + x64!(self.assmblr + ; mov rax, QWORD ptr as _ + ; call rax + ); + } + + fn tailcall(&mut self, ptr: *const c_void) { + // stack pointer is never modified and remains aligned + // return address remains the one provided by the trampoline's caller (V8) + debug_assert!( + self.allocated_stack == 0, + "the trampoline would tail call the FFI function with an outstanding stack allocation" + ); + debug_assert!( + self.frame_pointer == 0, + "the trampoline would tail call the FFI function with outstanding locals in the frame" + ); + x64!(self.assmblr + ; mov rax, QWORD ptr as _ + ; jmp rax + ); + } + + fn ret(&mut self) { + debug_assert!( + self.allocated_stack == 0, + "the trampoline would return with an outstanding stack allocation" + ); + debug_assert!( + self.frame_pointer == 0, + "the trampoline would return with outstanding locals in the frame" + ); + x64!(self.assmblr; ret); + } + + fn is_recv_arg_overridden(&self) -> bool { + // V8 receiver is the first parameter of the trampoline function and is a pointer + self.integral_params > 0 + } + + fn must_cast_return_value(&self, rv: NativeType) -> bool { + // V8 only supports i32 and u32 return types for integers + // We support 8 and 16 bit integers by extending them to 32 bits in the trampoline before returning + matches!( + rv, + NativeType::U8 | NativeType::I8 | NativeType::U16 | NativeType::I16 + ) + } + + fn must_wrap_return_value_in_typed_array(&self, rv: NativeType) -> bool { + // V8 only supports i32 and u32 return types for integers + // We support 64 bit integers by wrapping them in a TypedArray out parameter + crate::dlfcn::needs_unwrap(rv) + } + + fn finalize(self) -> ExecutableBuffer { + self.assmblr.finalize().unwrap() + } +} + +struct Aarch64Apple { + // Reference https://github.com/ARM-software/abi-aa/blob/main/aapcs64/aapcs64.rst + assmblr: dynasmrt::aarch64::Assembler, + // Parameter counters + integral_params: u32, + float_params: u32, + // Stack offset accumulators + offset_trampoline: u32, + offset_callee: u32, + allocated_stack: u32, +} + +#[cfg_attr( + not(all(target_aarch = "aarch64", target_vendor = "apple")), + allow(dead_code) +)] +impl Aarch64Apple { + // Integral arguments go to the first 8 GPR: x0-x7 + const INTEGRAL_REGISTERS: u32 = 8; + // Floating-point arguments go to the first 8 SIMD & Floating-Point registers: v0-v1 + const FLOAT_REGISTERS: u32 = 8; + + fn new() -> Self { + Self { + assmblr: dynasmrt::aarch64::Assembler::new().unwrap(), + integral_params: 0, + float_params: 0, + offset_trampoline: 0, + offset_callee: 0, + allocated_stack: 0, + } + } + + fn compile(sym: &Symbol) -> Trampoline { + let mut compiler = Self::new(); + + let must_wrap_return_value = + compiler.must_wrap_return_value_in_typed_array(sym.result_type); + let must_save_preserved_register = must_wrap_return_value; + let cannot_tailcall = must_wrap_return_value; + + if cannot_tailcall { + compiler.allocate_stack(sym); + compiler.save_frame_record(); + if compiler.must_save_preserved_register_to_stack(sym) { + compiler.save_preserved_register_to_stack(); + } + } + + for param in sym.parameter_types.iter().copied() { + compiler.move_left(param) + } + if !compiler.is_recv_arg_overridden() { + // the receiver object should never be expected. Avoid its unexpected or deliberate leak + compiler.zero_first_arg(); + } + if compiler.must_wrap_return_value_in_typed_array(sym.result_type) { + compiler.save_out_array_to_preserved_register(); + } + + if cannot_tailcall { + compiler.call(sym.ptr.as_ptr()); + if must_wrap_return_value { + compiler.wrap_return_value_in_out_array(); + } + if must_save_preserved_register { + compiler.recover_preserved_register(); + } + compiler.recover_frame_record(); + compiler.deallocate_stack(); + compiler.ret(); + } else { + compiler.tailcall(sym.ptr.as_ptr()); + } + + Trampoline(compiler.finalize()) + } + + fn move_left(&mut self, param: NativeType) { + // Section 6.4.2 of the Aarch64 Procedure Call Standard (PCS), on argument classification: + // - INTEGRAL or POINTER: + // > If the argument is an Integral or Pointer Type, the size of the argument is less than or equal to 8 bytes + // > and the NGRN is less than 8, the argument is copied to the least significant bits in x[NGRN]. + // + // - Floating-Point or Vector: + // > If the argument is a Half-, Single-, Double- or Quad- precision Floating-point or short vector type + // > and the NSRN is less than 8, then the argument is allocated to the least significant bits of register v[NSRN] + match param.into() { + Int(integral) => self.move_integral(integral), + Float(float) => self.move_float(float), + } + } + + fn move_float(&mut self, param: Floating) { + // Section 6.4.2 of the Aarch64 PCS: + // > If the argument is a Half-, Single-, Double- or Quad- precision Floating-point or short vector type and the NSRN is less than 8, then the + // > argument is allocated to the least significant bits of register v[NSRN]. The NSRN is incremented by one. The argument has now been allocated. + // > [if NSRN is equal or more than 8] + // > The argument is copied to memory at the adjusted NSAA. The NSAA is incremented by the size of the argument. The argument has now been allocated. + let param_i = self.float_params; + + let is_in_stack = param_i >= Self::FLOAT_REGISTERS; + if is_in_stack { + // https://developer.apple.com/documentation/xcode/writing-arm64-code-for-apple-platforms: + // > Function arguments may consume slots on the stack that are not multiples of 8 bytes. + // (i.e. natural alignment instead of eightbyte alignment) + let padding_trampl = + (param.size() - self.offset_trampoline % param.size()) % param.size(); + let padding_callee = + (param.size() - self.offset_callee % param.size()) % param.size(); + + // floats are only moved to accommodate integer movement in the stack + let stack_has_moved = self.integral_params >= Self::INTEGRAL_REGISTERS; + if stack_has_moved { + let s = &mut self.assmblr; + let ot = self.offset_trampoline; + let oc = self.offset_callee; + match param { + Single => aarch64!(s + // 6.1.2 Aarch64 PCS: + // > Registers v8-v15 must be preserved by a callee across subroutine calls; + // > the remaining registers (v0-v7, v16-v31) do not need to be preserved (or should be preserved by the caller). + ; ldr s16, [sp, ot + padding_trampl] + ; str s16, [sp, oc + padding_callee] + ), + Double => aarch64!(s + ; ldr d16, [sp, ot + padding_trampl] + ; str d16, [sp, oc + padding_callee] + ), + } + } + self.offset_trampoline += padding_trampl + param.size(); + self.offset_callee += padding_callee + param.size(); + + debug_assert!( + self.allocated_stack == 0 || self.offset_callee <= self.allocated_stack + ); + } + self.float_params += 1; + } + + fn move_integral(&mut self, param: Integral) { + let s = &mut self.assmblr; + // Section 6.4.2 of the Aarch64 PCS: + // If the argument is an Integral or Pointer Type, the size of the argument is less than or + // equal to 8 bytes and the NGRN is less than 8, the argument is copied to the least + // significant bits in x[NGRN]. The NGRN is incremented by one. The argument has now been + // allocated. + // [if NGRN is equal or more than 8] + // The argument is copied to memory at the adjusted NSAA. The NSAA is incremented by the size + // of the argument. The argument has now been allocated. + let param_i = self.integral_params; + + // move each argument one position to the left. The first argument in the stack moves to the last integer register (x7). + match (param_i, param) { + // From https://developer.apple.com/documentation/xcode/writing-arm64-code-for-apple-platforms: + // > The caller of a function is responsible for signing or zero-extending any argument with fewer than 32 bits. + // > The standard ABI expects the callee to sign or zero-extend those arguments. + // (this applies to register parameters, as stack parameters are not eightbyte aligned in Apple) + (0, I(B)) => aarch64!(s; sxtb w0, w1), + (0, U(B)) => aarch64!(s; and w0, w1, 0xFF), + (0, I(W)) => aarch64!(s; sxth w0, w1), + (0, U(W)) => aarch64!(s; and w0, w1, 0xFFFF), + (0, I(DW) | U(DW)) => aarch64!(s; mov w0, w1), + (0, I(QW) | U(QW)) => aarch64!(s; mov x0, x1), + // The fast API expects buffer arguments passed as a pointer to a FastApiTypedArray struct + // Here we blindly follow the layout of https://github.com/denoland/rusty_v8/blob/main/src/fast_api.rs#L190-L200 + // although that might be problematic: https://discord.com/channels/684898665143206084/956626010248478720/1009450940866252823 + (0, Buffer) => aarch64!(s; ldr x0, [x1, 8]), + + (1, I(B)) => aarch64!(s; sxtb w1, w2), + (1, U(B)) => aarch64!(s; and w1, w2, 0xFF), + (1, I(W)) => aarch64!(s; sxth w1, w2), + (1, U(W)) => aarch64!(s; and w1, w2, 0xFFFF), + (1, I(DW) | U(DW)) => aarch64!(s; mov w1, w2), + (1, I(QW) | U(QW)) => aarch64!(s; mov x1, x2), + (1, Buffer) => aarch64!(s; ldr x1, [x2, 8]), + + (2, I(B)) => aarch64!(s; sxtb w2, w3), + (2, U(B)) => aarch64!(s; and w2, w3, 0xFF), + (2, I(W)) => aarch64!(s; sxth w2, w3), + (2, U(W)) => aarch64!(s; and w2, w3, 0xFFFF), + (2, I(DW) | U(DW)) => aarch64!(s; mov w2, w3), + (2, I(QW) | U(QW)) => aarch64!(s; mov x2, x3), + (2, Buffer) => aarch64!(s; ldr x2, [x3, 8]), + + (3, I(B)) => aarch64!(s; sxtb w3, w4), + (3, U(B)) => aarch64!(s; and w3, w4, 0xFF), + (3, I(W)) => aarch64!(s; sxth w3, w4), + (3, U(W)) => aarch64!(s; and w3, w4, 0xFFFF), + (3, I(DW) | U(DW)) => aarch64!(s; mov w3, w4), + (3, I(QW) | U(QW)) => aarch64!(s; mov x3, x4), + (3, Buffer) => aarch64!(s; ldr x3, [x4, 8]), + + (4, I(B)) => aarch64!(s; sxtb w4, w5), + (4, U(B)) => aarch64!(s; and w4, w5, 0xFF), + (4, I(W)) => aarch64!(s; sxth w4, w5), + (4, U(W)) => aarch64!(s; and w4, w5, 0xFFFF), + (4, I(DW) | U(DW)) => aarch64!(s; mov w4, w5), + (4, I(QW) | U(QW)) => aarch64!(s; mov x4, x5), + (4, Buffer) => aarch64!(s; ldr x4, [x5, 8]), + + (5, I(B)) => aarch64!(s; sxtb w5, w6), + (5, U(B)) => aarch64!(s; and w5, w6, 0xFF), + (5, I(W)) => aarch64!(s; sxth w5, w6), + (5, U(W)) => aarch64!(s; and w5, w6, 0xFFFF), + (5, I(DW) | U(DW)) => aarch64!(s; mov w5, w6), + (5, I(QW) | U(QW)) => aarch64!(s; mov x5, x6), + (5, Buffer) => aarch64!(s; ldr x5, [x6, 8]), + + (6, I(B)) => aarch64!(s; sxtb w6, w7), + (6, U(B)) => aarch64!(s; and w6, w7, 0xFF), + (6, I(W)) => aarch64!(s; sxth w6, w7), + (6, U(W)) => aarch64!(s; and w6, w7, 0xFFFF), + (6, I(DW) | U(DW)) => aarch64!(s; mov w6, w7), + (6, I(QW) | U(QW)) => aarch64!(s; mov x6, x7), + (6, Buffer) => aarch64!(s; ldr x6, [x7, 8]), + + (7, param) => { + let ot = self.offset_trampoline; + match param { + I(B) => { + aarch64!(s; ldrsb w7, [sp, ot]) + } + U(B) => { + // ldrb zero-extends the byte to fill the 32bits of the register + aarch64!(s; ldrb w7, [sp, ot]) + } + I(W) => { + aarch64!(s; ldrsh w7, [sp, ot]) + } + U(W) => { + // ldrh zero-extends the half-word to fill the 32bits of the register + aarch64!(s; ldrh w7, [sp, ot]) + } + I(DW) | U(DW) => { + aarch64!(s; ldr w7, [sp, ot]) + } + I(QW) | U(QW) => { + aarch64!(s; ldr x7, [sp, ot]) + } + Buffer => { + aarch64!(s + ; ldr x7, [sp, ot] + ; ldr x7, [x7, 8] + ) + } + } + // 16 and 8 bit integers are 32 bit integers in v8 + self.offset_trampoline += max(param.size(), 4); + } + + (8.., param) => { + // https://developer.apple.com/documentation/xcode/writing-arm64-code-for-apple-platforms: + // > Function arguments may consume slots on the stack that are not multiples of 8 bytes. + // (i.e. natural alignment instead of eightbyte alignment) + // + // N.B. V8 does not currently follow this Apple's policy, and instead aligns all arguments to 8 Byte boundaries. + // The current implementation follows the V8 incorrect calling convention for the sake of a seamless experience + // for the Deno users. Whenever upgrading V8 we should make sure that the bug has not been amended, and revert this + // workaround once it has been. The bug is being tracked in https://bugs.chromium.org/p/v8/issues/detail?id=13171 + let size_original = param.size(); + // 16 and 8 bit integers are 32 bit integers in v8 + // let size_trampl = max(size_original, 4); // <-- Apple alignment + let size_trampl = 8; // <-- V8 incorrect alignment + let padding_trampl = + padding_to_align(size_trampl, self.offset_trampoline); + let padding_callee = + padding_to_align(size_original, self.offset_callee); + let ot = self.offset_trampoline; + let oc = self.offset_callee; + match param { + I(B) | U(B) => aarch64!(s + ; ldr w8, [sp, ot + padding_trampl] + ; strb w8, [sp, oc + padding_callee] + ), + I(W) | U(W) => aarch64!(s + ; ldr w8, [sp, ot + padding_trampl] + ; strh w8, [sp, oc + padding_callee] + ), + I(DW) | U(DW) => aarch64!(s + ; ldr w8, [sp, ot + padding_trampl] + ; str w8, [sp, oc + padding_callee] + ), + I(QW) | U(QW) => aarch64!(s + ; ldr x8, [sp, ot + padding_trampl] + ; str x8, [sp, oc + padding_callee] + ), + Buffer => aarch64!(s + ; ldr x8, [sp, ot + padding_trampl] + ; ldr x8, [x8, 8] + ; str x8, [sp, oc + padding_callee] + ), + } + self.offset_trampoline += padding_trampl + size_trampl; + self.offset_callee += padding_callee + size_original; + + debug_assert!( + self.allocated_stack == 0 + || self.offset_callee <= self.allocated_stack + ); + } + }; + self.integral_params += 1; + } + + fn zero_first_arg(&mut self) { + debug_assert!( + self.integral_params == 0, + "the trampoline would zero the first argument after having overridden it with the second one" + ); + aarch64!(self.assmblr; mov x0, xzr); + } + + fn save_out_array_to_preserved_register(&mut self) { + let s = &mut self.assmblr; + // functions returning 64 bit integers have the out array appended as their last parameter, + // and it is a *FastApiTypedArray + match self.integral_params { + // x0 is always V8's receiver + 0 => aarch64!(s; ldr x19, [x1, 8]), + 1 => aarch64!(s; ldr x19, [x2, 8]), + 2 => aarch64!(s; ldr x19, [x3, 8]), + 3 => aarch64!(s; ldr x19, [x4, 8]), + 4 => aarch64!(s; ldr x19, [x5, 8]), + 5 => aarch64!(s; ldr x19, [x6, 8]), + 6 => aarch64!(s; ldr x19, [x7, 8]), + 7.. => { + aarch64!(s + ; ldr x19, [sp, self.offset_trampoline] + ; ldr x19, [x19, 8] + ) + } + } + } + + fn wrap_return_value_in_out_array(&mut self) { + aarch64!(self.assmblr; str x0, [x19]); + } + + fn save_frame_record(&mut self) { + debug_assert!( + self.allocated_stack >= 16, + "the trampoline would try to save the frame record to the stack without having allocated enough space for it" + ); + aarch64!(self.assmblr + // Frame record is stored at the bottom of the stack frame + ; stp x29, x30, [sp, self.allocated_stack - 16] + ; add x29, sp, self.allocated_stack - 16 + ) + } + + fn recover_frame_record(&mut self) { + // The stack cannot have been deallocated before the frame record is restored + debug_assert!( + self.allocated_stack >= 16, + "the trampoline would try to load the frame record from the stack, but it couldn't possibly contain it" + ); + // Frame record is stored at the bottom of the stack frame + aarch64!(self.assmblr; ldp x29, x30, [sp, self.allocated_stack - 16]) + } + + fn save_preserved_register_to_stack(&mut self) { + // If a preserved register needs to be used, we must have allocated at least 32 bytes in the stack + // 16 for the frame record, 8 for the preserved register, and 8 for 16-byte alignment. + debug_assert!( + self.allocated_stack >= 32, + "the trampoline would try to save a register to the stack without having allocated enough space for it" + ); + // preserved register is stored after frame record + aarch64!(self.assmblr; str x19, [sp, self.allocated_stack - 24]); + } + + fn recover_preserved_register(&mut self) { + // The stack cannot have been deallocated before the preserved register is restored + // 16 for the frame record, 8 for the preserved register, and 8 for 16-byte alignment. + debug_assert!( + self.allocated_stack >= 32, + "the trampoline would try to recover the value of a register from the stack, but it couldn't possibly contain it" + ); + // preserved register is stored after frame record + aarch64!(self.assmblr; ldr x19, [sp, self.allocated_stack - 24]); + } + + fn allocate_stack(&mut self, symbol: &Symbol) { + // https://developer.apple.com/documentation/xcode/writing-arm64-code-for-apple-platforms: + // > Function arguments may consume slots on the stack that are not multiples of 8 bytes. + // (i.e. natural alignment instead of eightbyte alignment) + let mut int_params = 0u32; + let mut float_params = 0u32; + let mut stack_size = 0u32; + for param in symbol.parameter_types.iter().copied() { + match param.into() { + Float(float_param) => { + float_params += 1; + if float_params > Self::FLOAT_REGISTERS { + stack_size += float_param.size(); + } + } + Int(integral_param) => { + int_params += 1; + if int_params > Self::INTEGRAL_REGISTERS { + stack_size += integral_param.size(); + } + } + } + } + + // Section 6.2.3 of the Aarch64 PCS: + // > Each frame shall link to the frame of its caller by means of a frame record of two 64-bit values on the stack + stack_size += 16; + + if self.must_save_preserved_register_to_stack(symbol) { + stack_size += 8; + } + + // Section 6.2.2 of Aarch64 PCS: + // > At any point at which memory is accessed via SP, the hardware requires that + // > - SP mod 16 = 0. The stack must be quad-word aligned. + // > The stack must also conform to the following constraint at a public interface: + // > - SP mod 16 = 0. The stack must be quad-word aligned. + stack_size += padding_to_align(16, stack_size); + + if stack_size > 0 { + aarch64!(self.assmblr; sub sp, sp, stack_size); + self.offset_trampoline += stack_size; + // stack pointer has been modified, and the callee stack parameters are expected at the top of the stack + self.offset_callee = 0; + self.allocated_stack += stack_size; + } + } + + fn deallocate_stack(&mut self) { + if self.allocated_stack > 0 { + aarch64!(self.assmblr; add sp, sp, self.allocated_stack); + self.allocated_stack = 0; + } + } + + fn call(&mut self, ptr: *const c_void) { + // the stack has been aligned during stack allocation + // Frame record has been stored in stack and frame pointer points to it + debug_assert!( + self.allocated_stack % 16 == 0, + "the trampoline would call the FFI function with an unaligned stack" + ); + debug_assert!( + self.allocated_stack >= 16, + "the trampoline would call the FFI function without allocating enough stack for the frame record" + ); + self.load_callee_address(ptr); + aarch64!(self.assmblr; blr x8); + } + + fn tailcall(&mut self, ptr: *const c_void) { + // stack pointer is never modified and remains aligned + // frame pointer and link register remain the one provided by the trampoline's caller (V8) + debug_assert!( + self.allocated_stack == 0, + "the trampoline would tail call the FFI function with an outstanding stack allocation" + ); + self.load_callee_address(ptr); + aarch64!(self.assmblr; br x8); + } + + fn ret(&mut self) { + debug_assert!( + self.allocated_stack == 0, + "the trampoline would return with an outstanding stack allocation" + ); + aarch64!(self.assmblr; ret); + } + + fn load_callee_address(&mut self, ptr: *const c_void) { + // Like all ARM instructions, move instructions are 32bit long and can fit at most 16bit immediates. + // bigger immediates are loaded in multiple steps applying a left-shift modifier + let mut address = ptr as u64; + let mut imm16 = address & 0xFFFF; + aarch64!(self.assmblr; movz x8, imm16 as u32); + address >>= 16; + let mut shift = 16; + while address > 0 { + imm16 = address & 0xFFFF; + if imm16 != 0 { + aarch64!(self.assmblr; movk x8, imm16 as u32, lsl shift); + } + address >>= 16; + shift += 16; + } + } + + fn is_recv_arg_overridden(&self) -> bool { + // V8 receiver is the first parameter of the trampoline function and is a pointer + self.integral_params > 0 + } + + fn must_save_preserved_register_to_stack(&mut self, symbol: &Symbol) -> bool { + self.must_wrap_return_value_in_typed_array(symbol.result_type) + } + + fn must_wrap_return_value_in_typed_array(&self, rv: NativeType) -> bool { + // V8 only supports i32 and u32 return types for integers + // We support 64 bit integers by wrapping them in a TypedArray out parameter + crate::dlfcn::needs_unwrap(rv) + } + + fn finalize(self) -> ExecutableBuffer { + self.assmblr.finalize().unwrap() + } +} + +struct Win64 { + // Reference: https://github.com/MicrosoftDocs/cpp-docs/blob/main/docs/build/x64-calling-convention.md + assmblr: dynasmrt::x64::Assembler, + // Params counter (Windows does not distinguish by type with regards to parameter position) + params: u32, + // Stack offset accumulators + offset_trampoline: u32, + offset_callee: u32, + allocated_stack: u32, + frame_pointer: u32, +} + +#[cfg_attr( + not(all(target_aarch = "x86_64", target_family = "windows")), + allow(dead_code) +)] +impl Win64 { + // Section "Parameter Passing" of the Windows x64 calling convention: + // > By default, the x64 calling convention passes the first four arguments to a function in registers. + const REGISTERS: u32 = 4; + + fn new() -> Self { + Self { + assmblr: dynasmrt::x64::Assembler::new().unwrap(), + params: 0, + // trampoline caller's return address + trampoline's shadow space + offset_trampoline: 8 + 32, + offset_callee: 8 + 32, + allocated_stack: 0, + frame_pointer: 0, + } + } + + fn compile(sym: &Symbol) -> Trampoline { + let mut compiler = Self::new(); + + let must_cast_return_value = + compiler.must_cast_return_value(sym.result_type); + let must_wrap_return_value = + compiler.must_wrap_return_value_in_typed_array(sym.result_type); + let must_save_preserved_register = must_wrap_return_value; + let cannot_tailcall = must_cast_return_value || must_wrap_return_value; + + if cannot_tailcall { + if must_save_preserved_register { + compiler.save_preserved_register_to_stack(); + } + compiler.allocate_stack(&sym.parameter_types); + } + + for param in sym.parameter_types.iter().copied() { + compiler.move_left(param) + } + if !compiler.is_recv_arg_overridden() { + // the receiver object should never be expected. Avoid its unexpected or deliberate leak + compiler.zero_first_arg(); + } + if must_wrap_return_value { + compiler.save_out_array_to_preserved_register(); + } + + if cannot_tailcall { + compiler.call(sym.ptr.as_ptr()); + if must_cast_return_value { + compiler.cast_return_value(sym.result_type); + } + if must_wrap_return_value { + compiler.wrap_return_value_in_out_array(); + } + compiler.deallocate_stack(); + if must_save_preserved_register { + compiler.recover_preserved_register(); + } + compiler.ret(); + } else { + compiler.tailcall(sym.ptr.as_ptr()); + } + + Trampoline(compiler.finalize()) + } + + fn move_left(&mut self, param: NativeType) { + // Section "Parameter Passing" of the Windows x64 calling convention: + // > By default, the x64 calling convention passes the first four arguments to a function in registers. + // > The registers used for these arguments depend on the position and type of the argument. + // > Remaining arguments get pushed on the stack in right-to-left order. + // > [...] + // > Integer valued arguments in the leftmost four positions are passed in left-to-right order in RCX, RDX, R8, and R9 + // > [...] + // > Any floating-point and double-precision arguments in the first four parameters are passed in XMM0 - XMM3, depending on position + let s = &mut self.assmblr; + let param_i = self.params; + + // move each argument one position to the left. The first argument in the stack moves to the last register (r9 or xmm3). + // If the FFI function is called with a new stack frame, the arguments remaining in the stack are copied to the new stack frame. + // Otherwise, they are copied 8 bytes lower in the same frame + match (param_i, param.into()) { + // Section "Parameter Passing" of the Windows x64 calling convention: + // > All integer arguments in registers are right-justified, so the callee can ignore the upper bits of the register + // > and access only the portion of the register necessary. + // (i.e. unlike in SysV or Aarch64-Apple, 8/16 bit integers are not expected to be zero/sign extended) + (0, Int(U(B | W | DW) | I(B | W | DW))) => x64!(s; mov ecx, edx), + (0, Int(U(QW) | I(QW))) => x64!(s; mov rcx, rdx), + // The fast API expects buffer arguments passed as a pointer to a FastApiTypedArray struct + // Here we blindly follow the layout of https://github.com/denoland/rusty_v8/blob/main/src/fast_api.rs#L190-L200 + // although that might be problematic: https://discord.com/channels/684898665143206084/956626010248478720/1009450940866252823 + (0, Int(Buffer)) => x64!(s; mov rcx, [rdx + 8]), + // Use movaps for singles and doubles, benefits of smaller encoding outweigh those of using the correct instruction for the type, + // which for doubles should technically be movapd + (0, Float(_)) => { + x64!(s; movaps xmm0, xmm1); + self.zero_first_arg(); + } + + (1, Int(U(B | W | DW) | I(B | W | DW))) => x64!(s; mov edx, r8d), + (1, Int(U(QW) | I(QW))) => x64!(s; mov rdx, r8), + (1, Int(Buffer)) => x64!(s; mov rdx, [r8 + 8]), + (1, Float(_)) => x64!(s; movaps xmm1, xmm2), + + (2, Int(U(B | W | DW) | I(B | W | DW))) => x64!(s; mov r8d, r9d), + (2, Int(U(QW) | I(QW))) => x64!(s; mov r8, r9), + (2, Int(Buffer)) => x64!(s; mov r8, [r9 + 8]), + (2, Float(_)) => x64!(s; movaps xmm2, xmm3), + + (3, param) => { + let ot = self.offset_trampoline as i32; + match param { + Int(U(B | W | DW) | I(B | W | DW)) => { + x64!(s; mov r9d, [rsp + ot]) + } + Int(U(QW) | I(QW)) => { + x64!(s; mov r9, [rsp + ot]) + } + Int(Buffer) => { + x64!(s + ; mov r9, [rsp + ot] + ; mov r9, [r9 + 8]) + } + Float(_) => { + // parameter 4 is always 16-byte aligned, so we can use movaps instead of movups + x64!(s; movaps xmm3, [rsp + ot]) + } + } + // Section "x64 Aggregate and Union layout" of the windows x64 software conventions doc: + // > The alignment of the beginning of a structure or a union is the maximum alignment of any individual member + // Ref: https://github.com/MicrosoftDocs/cpp-docs/blob/main/docs/build/x64-software-conventions.md#x64-aggregate-and-union-layout + self.offset_trampoline += 8; + } + (4.., param) => { + let ot = self.offset_trampoline as i32; + let oc = self.offset_callee as i32; + match param { + Int(U(B | W | DW) | I(B | W | DW)) => { + x64!(s + ; mov eax, [rsp + ot] + ; mov [rsp + oc], eax + ) + } + Int(U(QW) | I(QW)) => { + x64!(s + ; mov rax, [rsp + ot] + ; mov [rsp + oc], rax + ) + } + Int(Buffer) => { + x64!(s + ; mov rax, [rsp + ot] + ; mov rax, [rax + 8] + ; mov [rsp + oc], rax + ) + } + Float(_) => { + x64!(s + ; movups xmm4, [rsp + ot] + ; movups [rsp + oc], xmm4 + ) + } + } + // Section "x64 Aggregate and Union layout" of the windows x64 software conventions doc: + // > The alignment of the beginning of a structure or a union is the maximum alignment of any individual member + // Ref: https://github.com/MicrosoftDocs/cpp-docs/blob/main/docs/build/x64-software-conventions.md#x64-aggregate-and-union-layout + self.offset_trampoline += 8; + self.offset_callee += 8; + + debug_assert!( + self.allocated_stack == 0 + || self.offset_callee <= self.allocated_stack + ); + } + } + self.params += 1; + } + + fn zero_first_arg(&mut self) { + debug_assert!( + self.params == 0, + "the trampoline would zero the first argument after having overridden it with the second one" + ); + x64!(self.assmblr; xor ecx, ecx); + } + + fn cast_return_value(&mut self, rv: NativeType) { + let s = &mut self.assmblr; + // V8 only supports 32bit integers. We support 8 and 16 bit integers casting them to 32bits. + // Section "Return Values" of the Windows x64 Calling Convention doc: + // > The state of unused bits in the value returned in RAX or XMM0 is undefined. + match rv { + NativeType::U8 => x64!(s; movzx eax, al), + NativeType::I8 => x64!(s; movsx eax, al), + NativeType::U16 => x64!(s; movzx eax, ax), + NativeType::I16 => x64!(s; movsx eax, ax), + _ => (), + } + } + + fn save_out_array_to_preserved_register(&mut self) { + let s = &mut self.assmblr; + // functions returning 64 bit integers have the out array appended as their last parameter, + // and it is a *FastApiTypedArray + match self.params { + // rcx is always V8 receiver + 0 => x64!(s; mov rbx, [rdx + 8]), + 1 => x64!(s; mov rbx, [r8 + 8]), + 2 => x64!(s; mov rbx, [r9 + 8]), + 3.. => { + x64!(s + ; mov rax, [rsp + self.offset_trampoline as i32] + ; mov rbx, [rax + 8] + ) + } + } + } + + fn wrap_return_value_in_out_array(&mut self) { + x64!(self.assmblr; mov [rbx], rax) + } + + fn save_preserved_register_to_stack(&mut self) { + x64!(self.assmblr; push rbx); + self.offset_trampoline += 8; + // stack pointer has been modified, and the callee stack parameters are expected at the top of the stack + self.offset_callee = 0; + self.frame_pointer += 8; + } + + fn recover_preserved_register(&mut self) { + debug_assert!( + self.frame_pointer >= 8, + "the trampoline would try to pop from the stack beyond its frame pointer" + ); + x64!(self.assmblr; pop rbx); + self.frame_pointer -= 8; + // parameter offsets are invalid once this method is called + } + + fn allocate_stack(&mut self, params: &[NativeType]) { + let mut stack_size = 0; + // Section "Calling Convention Defaults" of the x64-calling-convention and Section "Stack Allocation" of the stack-usage docs: + // > The x64 Application Binary Interface (ABI) uses a four-register fast-call calling convention by default. + // > Space is allocated on the call stack as a shadow store for callees to save those registers. + // > [...] + // > Any parameters beyond the first four must be stored on the stack after the shadow store before the call + // > [...] + // > Even if the called function has fewer than 4 parameters, these 4 stack locations are effectively owned by the called function, + // > and may be used by the called function for other purposes besides saving parameter register values + stack_size += max(params.len() as u32, 4) * 8; + + // Align new stack frame (accounting for the 8 byte of the trampoline caller's return address + // and any other potential addition to the stack prior to this allocation) + // Section "Stack Allocation" of stack-usage docs: + // > The stack will always be maintained 16-byte aligned, except within the prolog (for example, after the return address is pushed) + stack_size += padding_to_align(16, self.frame_pointer + stack_size + 8); + + x64!(self.assmblr; sub rsp, stack_size as i32); + self.offset_trampoline += stack_size; + // stack pointer has been modified, and the callee stack parameters are expected at the top of the stack right after the shadow space + self.offset_callee = 32; + self.allocated_stack += stack_size; + self.frame_pointer += stack_size; + } + + fn deallocate_stack(&mut self) { + debug_assert!( + self.frame_pointer >= self.allocated_stack, + "the trampoline would try to deallocate stack beyond its frame pointer" + ); + x64!(self.assmblr; add rsp, self.allocated_stack as i32); + self.frame_pointer -= self.allocated_stack; + self.allocated_stack = 0; + } + + fn call(&mut self, ptr: *const c_void) { + // the stack has been aligned during stack allocation and/or pushing of preserved registers + debug_assert!( + (8 + self.frame_pointer) % 16 == 0, + "the trampoline would call the FFI function with an unaligned stack" + ); + x64!(self.assmblr + ; mov rax, QWORD ptr as _ + ; call rax + ); + } + + fn tailcall(&mut self, ptr: *const c_void) { + // stack pointer is never modified and remains aligned + // return address remains the one provided by the trampoline's caller (V8) + debug_assert!( + self.allocated_stack == 0, + "the trampoline would tail call the FFI function with an outstanding stack allocation" + ); + debug_assert!( + self.frame_pointer == 0, + "the trampoline would tail call the FFI function with outstanding locals in the frame" + ); + x64!(self.assmblr + ; mov rax, QWORD ptr as _ + ; jmp rax + ); + } + + fn ret(&mut self) { + debug_assert!( + self.allocated_stack == 0, + "the trampoline would return with an outstanding stack allocation" + ); + debug_assert!( + self.frame_pointer == 0, + "the trampoline would return with outstanding locals in the frame" + ); + x64!(self.assmblr; ret); + } + + fn is_recv_arg_overridden(&self) -> bool { + self.params > 0 + } + + fn must_cast_return_value(&self, rv: NativeType) -> bool { + // V8 only supports i32 and u32 return types for integers + // We support 8 and 16 bit integers by extending them to 32 bits in the trampoline before returning + matches!( + rv, + NativeType::U8 | NativeType::I8 | NativeType::U16 | NativeType::I16 + ) + } + + fn must_wrap_return_value_in_typed_array(&self, rv: NativeType) -> bool { + // V8 only supports i32 and u32 return types for integers + // We support 64 bit integers by wrapping them in a TypedArray out parameter + crate::dlfcn::needs_unwrap(rv) + } + + fn finalize(self) -> ExecutableBuffer { + self.assmblr.finalize().unwrap() + } +} + +fn padding_to_align(alignment: u32, size: u32) -> u32 { + (alignment - size % alignment) % alignment +} + +#[derive(Clone, Copy, Debug)] +enum Floating { + Single = 4, + Double = 8, +} + +impl Floating { + fn size(self) -> u32 { + self as u32 + } +} + +use Floating::*; + +#[derive(Clone, Copy, Debug)] +enum Integral { + I(Size), + U(Size), + Buffer, +} + +impl Integral { + fn size(self) -> u32 { + match self { + I(size) | U(size) => size as u32, + Buffer => 8, + } + } +} + +use Integral::*; + +#[derive(Clone, Copy, Debug)] +enum Size { + B = 1, + W = 2, + DW = 4, + QW = 8, +} +use Size::*; + +#[allow(clippy::enum_variant_names)] +#[derive(Clone, Copy, Debug)] +enum Param { + Int(Integral), + Float(Floating), +} + +use Param::*; + +impl From for Param { + fn from(native: NativeType) -> Self { + match native { + NativeType::F32 => Float(Single), + NativeType::F64 => Float(Double), + NativeType::Bool | NativeType::U8 => Int(U(B)), + NativeType::U16 => Int(U(W)), + NativeType::U32 | NativeType::Void => Int(U(DW)), + NativeType::U64 + | NativeType::USize + | NativeType::Pointer + | NativeType::Function => Int(U(QW)), + NativeType::I8 => Int(I(B)), + NativeType::I16 => Int(I(W)), + NativeType::I32 => Int(I(DW)), + NativeType::I64 | NativeType::ISize => Int(I(QW)), + NativeType::Buffer => Int(Buffer), + } + } +} + +#[cfg(test)] +mod tests { + use std::ptr::null_mut; + + use libffi::middle::Type; + + use crate::NativeType; + use crate::Symbol; + + fn symbol(parameters: Vec, ret: NativeType) -> Symbol { + Symbol { + cif: libffi::middle::Cif::new(vec![], Type::void()), + ptr: libffi::middle::CodePtr(null_mut()), + parameter_types: parameters, + result_type: ret, + can_callback: false, + } + } + + mod sysv_amd64 { + use std::ops::Deref; + + use dynasmrt::dynasm; + use dynasmrt::DynasmApi; + + use super::super::SysVAmd64; + use super::symbol; + use crate::NativeType::*; + + #[test] + fn tailcall() { + let trampoline = SysVAmd64::compile(&symbol( + vec![ + U8, U16, I16, I8, U32, U64, Buffer, Function, I64, I32, I16, I8, F32, + F32, F32, F32, F64, F64, F64, F64, F32, F64, + ], + Void, + )); + + let mut assembler = dynasmrt::x64::Assembler::new().unwrap(); + // See https://godbolt.org/z/KE9x1h9xq + dynasm!(assembler + ; .arch x64 + ; movzx edi, sil // u8 + ; movzx esi, dx // u16 + ; movsx edx, cx // i16 + ; movsx ecx, r8b // i8 + ; mov r8d, r9d // u32 + ; mov r9, [DWORD rsp + 8] // u64 + ; mov rax, [DWORD rsp + 16] // Buffer + ; mov rax, [rax + 8] // .. + ; mov [DWORD rsp + 8], rax // .. + ; mov rax, [DWORD rsp + 24] // Function + ; mov [DWORD rsp + 16], rax // .. + ; mov rax, [DWORD rsp + 32] // i64 + ; mov [DWORD rsp + 24], rax // .. + ; mov eax, [DWORD rsp + 40] // i32 + ; mov [DWORD rsp + 32], eax // .. + ; movsx eax, WORD [DWORD rsp + 48] // i16 + ; mov [DWORD rsp + 40], eax // .. + ; movsx eax, BYTE [DWORD rsp + 56] // i8 + ; mov [DWORD rsp + 48], eax // .. + ; movss xmm8, [DWORD rsp + 64] // f32 + ; movss [DWORD rsp + 56], xmm8 // .. + ; movsd xmm8, [DWORD rsp + 72] // f64 + ; movsd [DWORD rsp + 64], xmm8 // .. + ; mov rax, QWORD 0 + ; jmp rax + ); + let expected = assembler.finalize().unwrap(); + assert_eq!(trampoline.0.deref(), expected.deref()); + } + + #[test] + fn integer_casting() { + let trampoline = SysVAmd64::compile(&symbol( + vec![U8, U16, I8, I16, U8, U16, I8, I16, U8, U16, I8, I16], + I8, + )); + + let mut assembler = dynasmrt::x64::Assembler::new().unwrap(); + // See https://godbolt.org/z/qo59bPsfv + dynasm!(assembler + ; .arch x64 + ; sub rsp, DWORD 56 // stack allocation + ; movzx edi, sil // u8 + ; movzx esi, dx // u16 + ; movsx edx, cl // i8 + ; movsx ecx, r8w // i16 + ; movzx r8d, r9b // u8 + ; movzx r9d, WORD [DWORD rsp + 64] // u16 + ; movsx eax, BYTE [DWORD rsp + 72] // i8 + ; mov [DWORD rsp + 0], eax // .. + ; movsx eax, WORD [DWORD rsp + 80] // i16 + ; mov [DWORD rsp + 8], eax // .. + ; movzx eax, BYTE [DWORD rsp + 88] // u8 + ; mov [DWORD rsp + 16], eax // .. + ; movzx eax, WORD [DWORD rsp + 96] // u16 + ; mov [DWORD rsp + 24], eax // .. + ; movsx eax, BYTE [DWORD rsp + 104] // i8 + ; mov [DWORD rsp + 32], eax // .. + ; movsx eax, WORD [DWORD rsp + 112] // i16 + ; mov [DWORD rsp + 40], eax // .. + ; mov rax, QWORD 0 + ; call rax + ; movsx eax, al // return value cast + ; add rsp, DWORD 56 // stack deallocation + ; ret + ); + let expected = assembler.finalize().unwrap(); + assert_eq!(trampoline.0.deref(), expected.deref()); + } + + #[test] + fn buffer_parameters() { + let trampoline = SysVAmd64::compile(&symbol( + vec![ + Buffer, Buffer, Buffer, Buffer, Buffer, Buffer, Buffer, Buffer, + ], + Void, + )); + + let mut assembler = dynasmrt::x64::Assembler::new().unwrap(); + // See https://godbolt.org/z/hqv63M3Ko + dynasm!(assembler + ; .arch x64 + ; mov rdi, [rsi + 8] // Buffer + ; mov rsi, [rdx + 8] // Buffer + ; mov rdx, [rcx + 8] // Buffer + ; mov rcx, [r8 + 8] // Buffer + ; mov r8, [r9 + 8] // Buffer + ; mov r9, [DWORD rsp + 8] // Buffer + ; mov r9, [r9 + 8] // .. + ; mov rax, [DWORD rsp + 16] // Buffer + ; mov rax, [rax + 8] // .. + ; mov [DWORD rsp + 8], rax // .. + ; mov rax, [DWORD rsp + 24] // Buffer + ; mov rax, [rax + 8] // .. + ; mov [DWORD rsp + 16], rax // .. + ; mov rax, QWORD 0 + ; jmp rax + ); + let expected = assembler.finalize().unwrap(); + assert_eq!(trampoline.0.deref(), expected.deref()); + } + + #[test] + fn return_u64_in_register_typed_array() { + let trampoline = SysVAmd64::compile(&symbol(vec![], U64)); + + let mut assembler = dynasmrt::x64::Assembler::new().unwrap(); + // See https://godbolt.org/z/8G7a488o7 + dynasm!(assembler + ; .arch x64 + ; push rbx + ; xor edi, edi // recv + ; mov rbx, [rsi + 8] // save data array pointer to non-volatile register + ; mov rax, QWORD 0 + ; call rax + ; mov [rbx], rax // copy return value to data pointer address + ; pop rbx + ; ret + ); + let expected = assembler.finalize().unwrap(); + assert_eq!(trampoline.0.deref(), expected.deref()); + } + + #[test] + fn return_u64_in_stack_typed_array() { + let trampoline = SysVAmd64::compile(&symbol( + vec![U64, U64, U64, U64, U64, U64, U64], + U64, + )); + + let mut assembler = dynasmrt::x64::Assembler::new().unwrap(); + // See https://godbolt.org/z/cPnPYWdWq + dynasm!(assembler + ; .arch x64 + ; push rbx + ; sub rsp, DWORD 16 + ; mov rdi, rsi // u64 + ; mov rsi, rdx // u64 + ; mov rdx, rcx // u64 + ; mov rcx, r8 // u64 + ; mov r8, r9 // u64 + ; mov r9, [DWORD rsp + 32] // u64 + ; mov rax, [DWORD rsp + 40] // u64 + ; mov [DWORD rsp + 0], rax // .. + ; mov rax, [DWORD rsp + 48] // save data array pointer to non-volatile register + ; mov rbx, [rax + 8] // .. + ; mov rax, QWORD 0 + ; call rax + ; mov [rbx], rax // copy return value to data pointer address + ; add rsp, DWORD 16 + ; pop rbx + ; ret + ); + let expected = assembler.finalize().unwrap(); + assert_eq!(trampoline.0.deref(), expected.deref()); + } + } + + mod aarch64_apple { + use std::ops::Deref; + + use dynasmrt::dynasm; + + use super::super::Aarch64Apple; + use super::symbol; + use crate::NativeType::*; + + #[test] + fn tailcall() { + let trampoline = Aarch64Apple::compile(&symbol( + vec![ + U8, U16, I16, I8, U32, U64, Buffer, Function, I64, I32, I16, I8, F32, + F32, F32, F32, F64, F64, F64, F64, F32, F64, + ], + Void, + )); + + let mut assembler = dynasmrt::aarch64::Assembler::new().unwrap(); + // See https://godbolt.org/z/oefqYWT13 + dynasm!(assembler + ; .arch aarch64 + ; and w0, w1, 0xFF // u8 + ; and w1, w2, 0xFFFF // u16 + ; sxth w2, w3 // i16 + ; sxtb w3, w4 // i8 + ; mov w4, w5 // u32 + ; mov x5, x6 // u64 + ; ldr x6, [x7, 8] // Buffer + ; ldr x7, [sp] // Function + ; ldr x8, [sp, 8] // i64 + ; str x8, [sp] // .. + ; ldr w8, [sp, 16] // i32 + ; str w8, [sp, 8] // .. + ; ldr w8, [sp, 24] // i16 + ; strh w8, [sp, 12] // .. + ; ldr w8, [sp, 32] // i8 + ; strb w8, [sp, 14] // .. + ; ldr s16, [sp, 40] // f32 + ; str s16, [sp, 16] // .. + ; ldr d16, [sp, 48] // f64 + ; str d16, [sp, 24] // .. + ; movz x8, 0 + ; br x8 + ); + let expected = assembler.finalize().unwrap(); + assert_eq!(trampoline.0.deref(), expected.deref()); + } + + #[test] + fn integer_casting() { + let trampoline = Aarch64Apple::compile(&symbol( + vec![U8, U16, I8, I16, U8, U16, I8, I16, U8, U16, I8, I16], + I8, + )); + + let mut assembler = dynasmrt::aarch64::Assembler::new().unwrap(); + // See https://godbolt.org/z/7qfzbzobM + dynasm!(assembler + ; .arch aarch64 + ; and w0, w1, 0xFF // u8 + ; and w1, w2, 0xFFFF // u16 + ; sxtb w2, w3 // i8 + ; sxth w3, w4 // i16 + ; and w4, w5, 0xFF // u8 + ; and w5, w6, 0xFFFF // u16 + ; sxtb w6, w7 // i8 + ; ldrsh w7, [sp] // i16 + ; ldr w8, [sp, 8] // u8 + ; strb w8, [sp] // .. + ; ldr w8, [sp, 16] // u16 + ; strh w8, [sp, 2] // .. + ; ldr w8, [sp, 24] // i8 + ; strb w8, [sp, 4] // .. + ; ldr w8, [sp, 32] // i16 + ; strh w8, [sp, 6] // .. + ; movz x8, 0 + ; br x8 + ); + let expected = assembler.finalize().unwrap(); + assert_eq!(trampoline.0.deref(), expected.deref()); + } + + #[test] + fn buffer_parameters() { + let trampoline = Aarch64Apple::compile(&symbol( + vec![ + Buffer, Buffer, Buffer, Buffer, Buffer, Buffer, Buffer, Buffer, + Buffer, Buffer, + ], + Void, + )); + + let mut assembler = dynasmrt::aarch64::Assembler::new().unwrap(); + // See https://godbolt.org/z/obd6z6vsf + dynasm!(assembler + ; .arch aarch64 + ; ldr x0, [x1, 8] // Buffer + ; ldr x1, [x2, 8] // Buffer + ; ldr x2, [x3, 8] // Buffer + ; ldr x3, [x4, 8] // Buffer + ; ldr x4, [x5, 8] // Buffer + ; ldr x5, [x6, 8] // Buffer + ; ldr x6, [x7, 8] // Buffer + ; ldr x7, [sp] // Buffer + ; ldr x7, [x7, 8] // .. + ; ldr x8, [sp, 8] // Buffer + ; ldr x8, [x8, 8] // .. + ; str x8, [sp] // .. + ; ldr x8, [sp, 16] // Buffer + ; ldr x8, [x8, 8] // .. + ; str x8, [sp, 8] // .. + ; movz x8, 0 + ; br x8 + ); + let expected = assembler.finalize().unwrap(); + assert_eq!(trampoline.0.deref(), expected.deref()); + } + + #[test] + fn return_u64_in_register_typed_array() { + let trampoline = Aarch64Apple::compile(&symbol(vec![], U64)); + + let mut assembler = dynasmrt::aarch64::Assembler::new().unwrap(); + // See https://godbolt.org/z/47EvvYb83 + dynasm!(assembler + ; .arch aarch64 + ; sub sp, sp, 32 + ; stp x29, x30, [sp, 16] + ; add x29, sp, 16 + ; str x19, [sp, 8] + ; mov x0, xzr // recv + ; ldr x19, [x1, 8] // save data array pointer to non-volatile register + ; movz x8, 0 + ; blr x8 + ; str x0, [x19] // copy return value to data pointer address + ; ldr x19, [sp, 8] + ; ldp x29, x30, [sp, 16] + ; add sp, sp, 32 + ; ret + ); + let expected = assembler.finalize().unwrap(); + assert_eq!(trampoline.0.deref(), expected.deref()); + } + + #[test] + fn return_u64_in_stack_typed_array() { + let trampoline = Aarch64Apple::compile(&symbol( + vec![U64, U64, U64, U64, U64, U64, U64, U64, U8, U8], + U64, + )); + + let mut assembler = dynasmrt::aarch64::Assembler::new().unwrap(); + // See https://godbolt.org/z/PvYPbsE1b + dynasm!(assembler + ; .arch aarch64 + ; sub sp, sp, 32 + ; stp x29, x30, [sp, 16] + ; add x29, sp, 16 + ; str x19, [sp, 8] + ; mov x0, x1 // u64 + ; mov x1, x2 // u64 + ; mov x2, x3 // u64 + ; mov x3, x4 // u64 + ; mov x4, x5 // u64 + ; mov x5, x6 // u64 + ; mov x6, x7 // u64 + ; ldr x7, [sp, 32] // u64 + ; ldr w8, [sp, 40] // u8 + ; strb w8, [sp] // .. + ; ldr w8, [sp, 48] // u8 + ; strb w8, [sp, 1] // .. + ; ldr x19, [sp, 56] // save data array pointer to non-volatile register + ; ldr x19, [x19, 8] // .. + ; movz x8, 0 + ; blr x8 + ; str x0, [x19] // copy return value to data pointer address + ; ldr x19, [sp, 8] + ; ldp x29, x30, [sp, 16] + ; add sp, sp, 32 + ; ret + ); + let expected = assembler.finalize().unwrap(); + assert_eq!(trampoline.0.deref(), expected.deref()); + } + } + + mod x64_windows { + use std::ops::Deref; + + use dynasmrt::{dynasm, DynasmApi}; + + use super::super::Win64; + use super::symbol; + use crate::NativeType::*; + + #[test] + fn tailcall() { + let trampoline = + Win64::compile(&symbol(vec![U8, I16, F64, F32, U32, I8, Buffer], Void)); + + let mut assembler = dynasmrt::x64::Assembler::new().unwrap(); + // See https://godbolt.org/z/TYzqrf9aj + dynasm!(assembler + ; .arch x64 + ; mov ecx, edx // u8 + ; mov edx, r8d // i16 + ; movaps xmm2, xmm3 // f64 + ; movaps xmm3, [DWORD rsp + 40] // f32 + ; mov eax, [DWORD rsp + 48] // u32 + ; mov [DWORD rsp + 40], eax // .. + ; mov eax, [DWORD rsp + 56] // i8 + ; mov [DWORD rsp + 48], eax // .. + ; mov rax, [DWORD rsp + 64] // Buffer + ; mov rax, [rax + 8] // .. + ; mov [DWORD rsp + 56], rax // .. + ; mov rax, QWORD 0 + ; jmp rax + ); + let expected = assembler.finalize().unwrap(); + assert_eq!(trampoline.0.deref(), expected.deref()); + } + + #[test] + fn integer_casting() { + let trampoline = Win64::compile(&symbol( + vec![U8, U16, I8, I16, U8, U16, I8, I16, U8, U16, I8, I16], + I8, + )); + + let mut assembler = dynasmrt::x64::Assembler::new().unwrap(); + // See https://godbolt.org/z/KMx56KGTq + dynasm!(assembler + ; .arch x64 + ; sub rsp, DWORD 104 // stack allocation + ; mov ecx, edx // u8 + ; mov edx, r8d // u16 + ; mov r8d, r9d // i8 + ; mov r9d, [DWORD rsp + 144] // i16 + ; mov eax, [DWORD rsp + 152] // u8 + ; mov [DWORD rsp + 32], eax // .. + ; mov eax, [DWORD rsp + 160] // u16 + ; mov [DWORD rsp + 40], eax // u16 + ; mov eax, [DWORD rsp + 168] // i8 + ; mov [DWORD rsp + 48], eax // .. + ; mov eax, [DWORD rsp + 176] // i16 + ; mov [DWORD rsp + 56], eax // .. + ; mov eax, [DWORD rsp + 184] // u8 + ; mov [DWORD rsp + 64], eax // .. + ; mov eax, [DWORD rsp + 192] // u16 + ; mov [DWORD rsp + 72], eax // .. + ; mov eax, [DWORD rsp + 200] // i8 + ; mov [DWORD rsp + 80], eax // .. + ; mov eax, [DWORD rsp + 208] // i16 + ; mov [DWORD rsp + 88], eax // .. + ; mov rax, QWORD 0 + ; call rax + ; movsx eax, al // return value cast + ; add rsp, DWORD 104 // stack deallocation + ; ret + ); + let expected = assembler.finalize().unwrap(); + assert_eq!(trampoline.0.deref(), expected.deref()); + } + + #[test] + fn buffer_parameters() { + let trampoline = Win64::compile(&symbol( + vec![Buffer, Buffer, Buffer, Buffer, Buffer, Buffer], + Void, + )); + + let mut assembler = dynasmrt::x64::Assembler::new().unwrap(); + // See https://godbolt.org/z/TYzqrf9aj + dynasm!(assembler + ; .arch x64 + ; mov rcx, [rdx + 8] // Buffer + ; mov rdx, [r8 + 8] // Buffer + ; mov r8, [r9 + 8] // Buffer + ; mov r9, [DWORD rsp + 40] // Buffer + ; mov r9, [r9 + 8] // .. + ; mov rax, [DWORD rsp + 48] // Buffer + ; mov rax, [rax + 8] // .. + ; mov [DWORD rsp + 40], rax // .. + ; mov rax, [DWORD rsp + 56] // Buffer + ; mov rax, [rax + 8] // .. + ; mov [DWORD rsp + 48], rax // .. + ; mov rax, QWORD 0 + ; jmp rax + ); + let expected = assembler.finalize().unwrap(); + assert_eq!(trampoline.0.deref(), expected.deref()); + } + + #[test] + fn return_u64_in_register_typed_array() { + let trampoline = Win64::compile(&symbol(vec![], U64)); + + let mut assembler = dynasmrt::x64::Assembler::new().unwrap(); + // See https://godbolt.org/z/7EnPE7o3T + dynasm!(assembler + ; .arch x64 + ; push rbx + ; sub rsp, DWORD 32 + ; xor ecx, ecx // recv + ; mov rbx, [rdx + 8] // save data array pointer to non-volatile register + ; mov rax, QWORD 0 + ; call rax + ; mov [rbx], rax // copy return value to data pointer address + ; add rsp, DWORD 32 + ; pop rbx + ; ret + ); + let expected = assembler.finalize().unwrap(); + assert_eq!(trampoline.0.deref(), expected.deref()); + } + + #[test] + fn return_u64_in_stack_typed_array() { + let trampoline = + Win64::compile(&symbol(vec![U64, U64, U64, U64, U64], U64)); + + let mut assembler = dynasmrt::x64::Assembler::new().unwrap(); + // See https://godbolt.org/z/3966sfEex + dynasm!(assembler + ; .arch x64 + ; push rbx + ; sub rsp, DWORD 48 + ; mov rcx, rdx // u64 + ; mov rdx, r8 // u64 + ; mov r8, r9 // u64 + ; mov r9, [DWORD rsp + 96] // u64 + ; mov rax, [DWORD rsp + 104] // u64 + ; mov [DWORD rsp + 32], rax // .. + ; mov rax, [DWORD rsp + 112] // save data array pointer to non-volatile register + ; mov rbx, [rax + 8] // .. + ; mov rax, QWORD 0 + ; call rax + ; mov [rbx], rax // copy return value to data pointer address + ; add rsp, DWORD 48 + ; pop rbx + ; ret + ); + let expected = assembler.finalize().unwrap(); + assert_eq!(trampoline.0.deref(), expected.deref()); + } + } +} -- cgit v1.2.3