1
0
Fork 0
mirror of https://github.com/denoland/deno.git synced 2024-12-27 09:39:08 -05:00
denoland-deno/ext/ffi/ir.rs
Divy Srivastava 53606de634
BREAKING(ffi/unstable): always return u64 as bigint (#23981)
The mixed `number | bigint` representation was useful optimization for
pointers. Now, pointers are represented as V8 externals. As part of the
FFI stabilization effort we want to make `bigint` the only
representation for `u64` and `i64`.

BigInt representation performance is almost on par with mixed
representation with the added benefit that its less confusing and users
don't need manual checks and conversions for doing operations on the
value.

```
cpu: AMD Ryzen 5 7530U with Radeon Graphics
runtime: deno 1.43.6+92a8d09 (x86_64-unknown-linux-gnu)

file:///home/divy/gh/ffi/main.ts
benchmark                 time (avg)        iter/s             (min … max)       p75       p99      p995
-------------------------------------------------------------------------- -----------------------------
nop                        4.01 ns/iter 249,533,690.5     (3.97 ns … 10.8 ns) 3.97 ns 4.36 ns 9.03 ns
ret bigint                 7.74 ns/iter 129,127,186.8    (7.72 ns … 10.46 ns) 7.72 ns 8.11 ns 8.82 ns
ret i32                    7.81 ns/iter 128,087,100.5    (7.77 ns … 12.72 ns) 7.78 ns 8.57 ns 9.75 ns
ret bigint (add op)       15.02 ns/iter  66,588,253.2   (14.64 ns … 24.99 ns) 14.76 ns 19.13 ns 19.44 ns
ret i32    (add op)       12.02 ns/iter  83,209,131.8   (11.95 ns … 18.18 ns) 11.98 ns 13.11 ns 14.5 ns
```
2024-05-28 09:31:09 +05:30

488 lines
15 KiB
Rust

// Copyright 2018-2024 the Deno authors. All rights reserved. MIT license.
use crate::symbol::NativeType;
use deno_core::error::type_error;
use deno_core::error::AnyError;
use deno_core::v8;
use libffi::middle::Arg;
use std::ffi::c_void;
use std::ptr;
pub struct OutBuffer(pub *mut u8, pub usize);
// SAFETY: OutBuffer is allocated by us in 00_ffi.js and is guaranteed to be
// only used for the purpose of writing return value of structs.
unsafe impl Send for OutBuffer {}
// SAFETY: See above
unsafe impl Sync for OutBuffer {}
pub fn out_buffer_as_ptr(
scope: &mut v8::HandleScope,
out_buffer: Option<v8::Local<v8::TypedArray>>,
) -> Option<OutBuffer> {
match out_buffer {
Some(out_buffer) => {
let ab = out_buffer.buffer(scope).unwrap();
let len = ab.byte_length();
ab.data()
.map(|non_null| OutBuffer(non_null.as_ptr() as *mut u8, len))
}
None => None,
}
}
/// 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)
}
NativeType::Struct(_) => Arg::new(&*self.pointer),
}
}
// 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,
) -> v8::Local<'scope, v8::Value> {
match native_type {
NativeType::Void => v8::undefined(scope).into(),
NativeType::Bool => v8::Boolean::new(scope, self.bool_value).into(),
NativeType::U8 => {
v8::Integer::new_from_unsigned(scope, self.u8_value as u32).into()
}
NativeType::I8 => v8::Integer::new(scope, self.i8_value as i32).into(),
NativeType::U16 => {
v8::Integer::new_from_unsigned(scope, self.u16_value as u32).into()
}
NativeType::I16 => v8::Integer::new(scope, self.i16_value as i32).into(),
NativeType::U32 => {
v8::Integer::new_from_unsigned(scope, self.u32_value).into()
}
NativeType::I32 => v8::Integer::new(scope, self.i32_value).into(),
NativeType::U64 => v8::BigInt::new_from_u64(scope, self.u64_value).into(),
NativeType::I64 => v8::BigInt::new_from_i64(scope, self.i64_value).into(),
NativeType::USize => {
v8::BigInt::new_from_u64(scope, self.usize_value as u64).into()
}
NativeType::ISize => {
v8::BigInt::new_from_i64(scope, self.isize_value as i64).into()
}
NativeType::F32 => v8::Number::new(scope, self.f32_value as f64).into(),
NativeType::F64 => v8::Number::new(scope, self.f64_value).into(),
NativeType::Pointer | NativeType::Buffer | NativeType::Function => {
let local_value: v8::Local<v8::Value> = if self.pointer.is_null() {
v8::null(scope).into()
} else {
v8::External::new(scope, self.pointer).into()
};
local_value
}
NativeType::Struct(_) => v8::null(scope).into(),
}
}
}
// SAFETY: unsafe trait must have unsafe implementation
unsafe impl Send for NativeValue {}
#[inline]
pub fn ffi_parse_bool_arg(
arg: v8::Local<v8::Value>,
) -> Result<NativeValue, AnyError> {
let bool_value = v8::Local::<v8::Boolean>::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<v8::Value>,
) -> Result<NativeValue, AnyError> {
let u8_value = v8::Local::<v8::Uint32>::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<v8::Value>,
) -> Result<NativeValue, AnyError> {
let i8_value = v8::Local::<v8::Int32>::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<v8::Value>,
) -> Result<NativeValue, AnyError> {
let u16_value = v8::Local::<v8::Uint32>::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<v8::Value>,
) -> Result<NativeValue, AnyError> {
let i16_value = v8::Local::<v8::Int32>::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<v8::Value>,
) -> Result<NativeValue, AnyError> {
let u32_value = v8::Local::<v8::Uint32>::try_from(arg)
.map_err(|_| type_error("Invalid FFI u32 type, expected unsigned integer"))?
.value();
Ok(NativeValue { u32_value })
}
#[inline]
pub fn ffi_parse_i32_arg(
arg: v8::Local<v8::Value>,
) -> Result<NativeValue, AnyError> {
let i32_value = v8::Local::<v8::Int32>::try_from(arg)
.map_err(|_| type_error("Invalid FFI i32 type, expected integer"))?
.value();
Ok(NativeValue { i32_value })
}
#[inline]
pub fn ffi_parse_u64_arg(
scope: &mut v8::HandleScope,
arg: v8::Local<v8::Value>,
) -> Result<NativeValue, AnyError> {
// 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::<v8::BigInt>::try_from(arg)
{
value.u64_value().0
} else if let Ok(value) = v8::Local::<v8::Number>::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<v8::Value>,
) -> Result<NativeValue, AnyError> {
// 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::<v8::BigInt>::try_from(arg)
{
value.i64_value().0
} else if let Ok(value) = v8::Local::<v8::Number>::try_from(arg) {
value.integer_value(scope).unwrap()
} 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<v8::Value>,
) -> Result<NativeValue, AnyError> {
// 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::<v8::BigInt>::try_from(arg) {
value.u64_value().0 as usize
} else if let Ok(value) = v8::Local::<v8::Number>::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<v8::Value>,
) -> Result<NativeValue, AnyError> {
// 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::<v8::BigInt>::try_from(arg) {
value.i64_value().0 as isize
} else if let Ok(value) = v8::Local::<v8::Number>::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<v8::Value>,
) -> Result<NativeValue, AnyError> {
let f32_value = v8::Local::<v8::Number>::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<v8::Value>,
) -> Result<NativeValue, AnyError> {
let f64_value = v8::Local::<v8::Number>::try_from(arg)
.map_err(|_| type_error("Invalid FFI f64 type, expected number"))?
.value();
Ok(NativeValue { f64_value })
}
#[inline]
pub fn ffi_parse_pointer_arg(
_scope: &mut v8::HandleScope,
arg: v8::Local<v8::Value>,
) -> Result<NativeValue, AnyError> {
let pointer = if let Ok(value) = v8::Local::<v8::External>::try_from(arg) {
value.value()
} else if arg.is_null() {
ptr::null_mut()
} else {
return Err(type_error(
"Invalid FFI pointer type, expected null, or External",
));
};
Ok(NativeValue { pointer })
}
#[inline]
pub fn ffi_parse_buffer_arg(
scope: &mut v8::HandleScope,
arg: v8::Local<v8::Value>,
) -> Result<NativeValue, AnyError> {
// 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::<v8::ArrayBuffer>::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::<v8::ArrayBufferView>::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_struct_arg(
scope: &mut v8::HandleScope,
arg: v8::Local<v8::Value>,
) -> Result<NativeValue, AnyError> {
// Order of checking:
// 1. ArrayBuffer: Fairly common and not supported by Fast API, optimise this case.
// 2. ArrayBufferView: Common and supported by Fast API
let pointer = if let Ok(value) = v8::Local::<v8::ArrayBuffer>::try_from(arg) {
if let Some(non_null) = value.data() {
non_null.as_ptr()
} else {
return Err(type_error(
"Invalid FFI ArrayBuffer, expected data in buffer",
));
}
} else if let Ok(value) = v8::Local::<v8::ArrayBufferView>::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 {
return Err(type_error(
"Invalid FFI ArrayBufferView, expected data in buffer",
));
}
} else {
return Err(type_error(
"Invalid FFI struct type, expected ArrayBuffer, or ArrayBufferView",
));
};
Ok(NativeValue { pointer })
}
#[inline]
pub fn ffi_parse_function_arg(
_scope: &mut v8::HandleScope,
arg: v8::Local<v8::Value>,
) -> Result<NativeValue, AnyError> {
let pointer = if let Ok(value) = v8::Local::<v8::External>::try_from(arg) {
value.value()
} else if arg.is_null() {
ptr::null_mut()
} else {
return Err(type_error(
"Invalid FFI function type, expected null, or External",
));
};
Ok(NativeValue { pointer })
}
pub fn ffi_parse_args<'scope>(
scope: &mut v8::HandleScope<'scope>,
args: v8::Local<v8::Array>,
parameter_types: &[NativeType],
) -> Result<Vec<NativeValue>, AnyError>
where
'scope: 'scope,
{
if parameter_types.is_empty() {
return Ok(vec![]);
}
let mut ffi_args: Vec<NativeValue> =
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::Struct(_) => {
ffi_args.push(ffi_parse_struct_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)
}