mirror of
https://github.com/denoland/deno.git
synced 2024-12-23 15:49:44 -05:00
perf(ext/ffi): optimize synchronous calls (#14945)
This commit is contained in:
parent
4e4d987486
commit
76d387fb93
2 changed files with 368 additions and 126 deletions
|
@ -253,8 +253,7 @@
|
|||
symbols = {};
|
||||
|
||||
constructor(path, symbols) {
|
||||
this.#rid = core.opSync("op_ffi_load", { path, symbols });
|
||||
|
||||
[this.#rid, this.symbols] = core.opSync("op_ffi_load", { path, symbols });
|
||||
for (const symbol in symbols) {
|
||||
if ("type" in symbols[symbol]) {
|
||||
const type = symbols[symbol].type;
|
||||
|
@ -285,48 +284,37 @@
|
|||
}
|
||||
|
||||
const isNonBlocking = symbols[symbol].nonblocking;
|
||||
const resultType = symbols[symbol].result;
|
||||
|
||||
let fn;
|
||||
if (isNonBlocking) {
|
||||
const resultType = symbols[symbol].result;
|
||||
const needsUnpacking = isReturnedAsBigInt(resultType);
|
||||
fn = (...parameters) => {
|
||||
const promise = core.opAsync(
|
||||
"op_ffi_call_nonblocking",
|
||||
this.#rid,
|
||||
symbol,
|
||||
parameters,
|
||||
);
|
||||
ObjectDefineProperty(
|
||||
this.symbols,
|
||||
symbol,
|
||||
{
|
||||
configurable: false,
|
||||
enumerable: true,
|
||||
value: (...parameters) => {
|
||||
const promise = core.opAsync(
|
||||
"op_ffi_call_nonblocking",
|
||||
this.#rid,
|
||||
symbol,
|
||||
parameters,
|
||||
);
|
||||
|
||||
if (needsUnpacking) {
|
||||
return PromisePrototypeThen(
|
||||
promise,
|
||||
(result) => unpackNonblockingReturnValue(resultType, result),
|
||||
);
|
||||
}
|
||||
if (needsUnpacking) {
|
||||
return PromisePrototypeThen(
|
||||
promise,
|
||||
(result) =>
|
||||
unpackNonblockingReturnValue(resultType, result),
|
||||
);
|
||||
}
|
||||
|
||||
return promise;
|
||||
};
|
||||
} else {
|
||||
fn = (...parameters) =>
|
||||
core.opSync(
|
||||
"op_ffi_call",
|
||||
this.#rid,
|
||||
symbol,
|
||||
parameters,
|
||||
);
|
||||
return promise;
|
||||
},
|
||||
writable: false,
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
ObjectDefineProperty(
|
||||
this.symbols,
|
||||
symbol,
|
||||
{
|
||||
configurable: false,
|
||||
enumerable: true,
|
||||
value: fn,
|
||||
writable: false,
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
428
ext/ffi/lib.rs
428
ext/ffi/lib.rs
|
@ -105,7 +105,7 @@ unsafe impl Sync for PtrSymbol {}
|
|||
|
||||
struct DynamicLibraryResource {
|
||||
lib: Library,
|
||||
symbols: HashMap<String, Symbol>,
|
||||
symbols: HashMap<String, Box<Symbol>>,
|
||||
}
|
||||
|
||||
impl Resource for DynamicLibraryResource {
|
||||
|
@ -119,50 +119,6 @@ impl Resource for DynamicLibraryResource {
|
|||
}
|
||||
|
||||
impl DynamicLibraryResource {
|
||||
fn register(
|
||||
&mut self,
|
||||
name: String,
|
||||
foreign_fn: ForeignFunction,
|
||||
) -> Result<(), AnyError> {
|
||||
let symbol = match &foreign_fn.name {
|
||||
Some(symbol) => symbol,
|
||||
None => &name,
|
||||
};
|
||||
// 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.
|
||||
let fn_ptr = match unsafe { self.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(),
|
||||
);
|
||||
|
||||
self.symbols.insert(
|
||||
name,
|
||||
Symbol {
|
||||
cif,
|
||||
ptr,
|
||||
parameter_types: foreign_fn.parameters,
|
||||
result_type: foreign_fn.result,
|
||||
},
|
||||
);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
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
|
||||
|
@ -196,7 +152,6 @@ pub fn init<P: FfiPermissions + 'static>(unstable: bool) -> Extension {
|
|||
.ops(vec![
|
||||
op_ffi_load::decl::<P>(),
|
||||
op_ffi_get_static::decl(),
|
||||
op_ffi_call::decl(),
|
||||
op_ffi_call_nonblocking::decl(),
|
||||
op_ffi_call_ptr::decl::<P>(),
|
||||
op_ffi_call_ptr_nonblocking::decl::<P>(),
|
||||
|
@ -378,6 +333,7 @@ impl NativeValue {
|
|||
}
|
||||
|
||||
// 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>,
|
||||
|
@ -474,6 +430,8 @@ struct ForeignFunction {
|
|||
name: Option<String>,
|
||||
parameters: Vec<NativeType>,
|
||||
result: NativeType,
|
||||
#[serde(rename = "nonblocking")]
|
||||
non_blocking: Option<bool>,
|
||||
}
|
||||
|
||||
// ForeignStatic's name and type fields are read and used by
|
||||
|
@ -575,11 +533,12 @@ pub(crate) fn format_error(e: dlopen::Error, path: String) -> String {
|
|||
}
|
||||
}
|
||||
|
||||
#[op]
|
||||
fn op_ffi_load<FP>(
|
||||
#[op(v8)]
|
||||
fn op_ffi_load<FP, 'scope>(
|
||||
scope: &mut v8::HandleScope<'scope>,
|
||||
state: &mut deno_core::OpState,
|
||||
args: FfiLoadArgs,
|
||||
) -> Result<ResourceId, AnyError>
|
||||
) -> Result<(ResourceId, serde_v8::Value<'scope>), AnyError>
|
||||
where
|
||||
FP: FfiPermissions + 'static,
|
||||
{
|
||||
|
@ -595,24 +554,118 @@ where
|
|||
format_error(e, path),
|
||||
))
|
||||
})?;
|
||||
|
||||
let mut resource = DynamicLibraryResource {
|
||||
lib,
|
||||
symbols: HashMap::new(),
|
||||
};
|
||||
let obj = v8::Object::new(scope);
|
||||
|
||||
for (symbol, foreign_symbol) in args.symbols {
|
||||
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) => {
|
||||
resource.register(symbol, 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.
|
||||
//
|
||||
// SAFETY: The obtained T symbol is the size of a pointer.
|
||||
let fn_ptr =
|
||||
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,
|
||||
});
|
||||
|
||||
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, Box::leak(sym));
|
||||
obj.set(scope, func_key.into(), function.into());
|
||||
}
|
||||
// This optimization is not yet supported for non-blocking calls.
|
||||
_ => {}
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(state.resource_table.add(resource))
|
||||
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: *mut Symbol,
|
||||
) -> v8::Local<'s, v8::Function> {
|
||||
let func = v8::Function::builder(
|
||||
|scope: &mut v8::HandleScope,
|
||||
args: v8::FunctionCallbackArguments,
|
||||
mut rv: v8::ReturnValue| {
|
||||
let external: v8::Local<v8::External> =
|
||||
args.data().unwrap().try_into().unwrap();
|
||||
// SAFETY: The pointer will not be deallocated until the function is
|
||||
// garbage collected.
|
||||
let symbol = unsafe { &*(external.value() as *const Symbol) };
|
||||
match ffi_call_sync(scope, args, symbol) {
|
||||
Ok(result) => {
|
||||
// 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 _).into())
|
||||
.build(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.
|
||||
unsafe { Box::from_raw(sym) };
|
||||
}),
|
||||
);
|
||||
|
||||
weak.to_local(scope).unwrap()
|
||||
}
|
||||
|
||||
fn ffi_parse_args<'scope>(
|
||||
|
@ -787,6 +840,240 @@ where
|
|||
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<NativeValue, AnyError>
|
||||
where
|
||||
'scope: 'scope,
|
||||
{
|
||||
let Symbol {
|
||||
parameter_types,
|
||||
result_type,
|
||||
cif,
|
||||
ptr: fun_ptr,
|
||||
} = symbol;
|
||||
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 as i32);
|
||||
match native_type {
|
||||
NativeType::Void => {
|
||||
unreachable!();
|
||||
}
|
||||
NativeType::U8 => {
|
||||
let value = value
|
||||
.uint32_value(scope)
|
||||
.ok_or_else(|| type_error("Invalid FFI u8 type, expected number"))?
|
||||
as u8;
|
||||
ffi_args.push(NativeValue { u8_value: value });
|
||||
}
|
||||
NativeType::I8 => {
|
||||
let value = value
|
||||
.int32_value(scope)
|
||||
.ok_or_else(|| type_error("Invalid FFI i8 type, expected number"))?
|
||||
as i8;
|
||||
|
||||
ffi_args.push(NativeValue { i8_value: value });
|
||||
}
|
||||
NativeType::U16 => {
|
||||
let value = value
|
||||
.uint32_value(scope)
|
||||
.ok_or_else(|| type_error("Invalid FFI u16 type, expected number"))?
|
||||
as u16;
|
||||
|
||||
ffi_args.push(NativeValue { u16_value: value });
|
||||
}
|
||||
NativeType::I16 => {
|
||||
let value = value
|
||||
.int32_value(scope)
|
||||
.ok_or_else(|| type_error("Invalid FFI i16 type, expected number"))?
|
||||
as i16;
|
||||
|
||||
ffi_args.push(NativeValue { i16_value: value });
|
||||
}
|
||||
NativeType::U32 => {
|
||||
let value = value
|
||||
.uint32_value(scope)
|
||||
.ok_or_else(|| type_error("Invalid FFI u32 type, expected number"))?
|
||||
as u32;
|
||||
|
||||
ffi_args.push(NativeValue { u32_value: value });
|
||||
}
|
||||
NativeType::I32 => {
|
||||
let value = value
|
||||
.int32_value(scope)
|
||||
.ok_or_else(|| type_error("Invalid FFI i32 type, expected number"))?
|
||||
as i32;
|
||||
|
||||
ffi_args.push(NativeValue { i32_value: value });
|
||||
}
|
||||
NativeType::U64 => {
|
||||
let value: u64 =
|
||||
if let Ok(value) = v8::Local::<v8::BigInt>::try_from(value) {
|
||||
value.u64_value().0
|
||||
} else {
|
||||
value.integer_value(scope).ok_or_else(|| {
|
||||
type_error("Invalid FFI u64 type, expected number")
|
||||
})? as u64
|
||||
};
|
||||
|
||||
ffi_args.push(NativeValue { u64_value: value });
|
||||
}
|
||||
NativeType::I64 => {
|
||||
let value: i64 =
|
||||
if let Ok(value) = v8::Local::<v8::BigInt>::try_from(value) {
|
||||
value.i64_value().0
|
||||
} else {
|
||||
value.integer_value(scope).ok_or_else(|| {
|
||||
type_error("Invalid FFI i64 type, expected number")
|
||||
})? as i64
|
||||
};
|
||||
|
||||
ffi_args.push(NativeValue { i64_value: value });
|
||||
}
|
||||
NativeType::USize => {
|
||||
let value: usize =
|
||||
if let Ok(value) = v8::Local::<v8::BigInt>::try_from(value) {
|
||||
value.u64_value().0 as usize
|
||||
} else {
|
||||
value.integer_value(scope).ok_or_else(|| {
|
||||
type_error("Invalid FFI usize type, expected number")
|
||||
})? as usize
|
||||
};
|
||||
|
||||
ffi_args.push(NativeValue { usize_value: value });
|
||||
}
|
||||
NativeType::ISize => {
|
||||
let value: isize =
|
||||
if let Ok(value) = v8::Local::<v8::BigInt>::try_from(value) {
|
||||
value.i64_value().0 as isize
|
||||
} else {
|
||||
value.integer_value(scope).ok_or_else(|| {
|
||||
type_error("Invalid FFI isize type, expected number")
|
||||
})? as isize
|
||||
};
|
||||
|
||||
ffi_args.push(NativeValue { isize_value: value });
|
||||
}
|
||||
NativeType::F32 => {
|
||||
let value = value
|
||||
.number_value(scope)
|
||||
.ok_or_else(|| type_error("Invalid FFI f32 type, expected number"))?
|
||||
as f32;
|
||||
|
||||
ffi_args.push(NativeValue { f32_value: value });
|
||||
}
|
||||
NativeType::F64 => {
|
||||
let value = value
|
||||
.number_value(scope)
|
||||
.ok_or_else(|| type_error("Invalid FFI f64 type, expected number"))?
|
||||
as f64;
|
||||
ffi_args.push(NativeValue { f64_value: value });
|
||||
}
|
||||
NativeType::Pointer => {
|
||||
if value.is_null() {
|
||||
let value: *const u8 = ptr::null();
|
||||
|
||||
ffi_args.push(NativeValue { pointer: value })
|
||||
} else if let Ok(value) = v8::Local::<v8::BigInt>::try_from(value) {
|
||||
let value = value.u64_value().0 as *const u8;
|
||||
|
||||
ffi_args.push(NativeValue { pointer: value });
|
||||
} else if let Ok(value) =
|
||||
v8::Local::<v8::ArrayBufferView>::try_from(value)
|
||||
{
|
||||
let byte_offset = value.byte_offset();
|
||||
let backing_store = value
|
||||
.buffer(scope)
|
||||
.ok_or_else(|| {
|
||||
type_error(
|
||||
"Invalid FFI ArrayBufferView, expected data in the buffer",
|
||||
)
|
||||
})?
|
||||
.get_backing_store();
|
||||
let pointer = &backing_store[byte_offset] as *const _ as *const u8;
|
||||
|
||||
ffi_args.push(NativeValue { pointer });
|
||||
} else if let Ok(value) = v8::Local::<v8::ArrayBuffer>::try_from(value)
|
||||
{
|
||||
let backing_store = value.get_backing_store();
|
||||
let pointer = &backing_store as *const _ as *const u8;
|
||||
|
||||
ffi_args.push(NativeValue { pointer });
|
||||
} else {
|
||||
return Err(type_error("Invalid FFI pointer type, expected null, BigInt, ArrayBuffer, or ArrayBufferView"));
|
||||
}
|
||||
}
|
||||
NativeType::Function => {
|
||||
if value.is_null() {
|
||||
let value: *const u8 = ptr::null();
|
||||
ffi_args.push(NativeValue { pointer: value })
|
||||
} else if let Ok(value) = v8::Local::<v8::BigInt>::try_from(value) {
|
||||
let value = value.u64_value().0 as *const u8;
|
||||
ffi_args.push(NativeValue { pointer: value });
|
||||
} else {
|
||||
return Err(type_error(
|
||||
"Invalid FFI function type, expected null, or BigInt",
|
||||
));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
let call_args: Vec<Arg> = 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::U8 => NativeValue {
|
||||
u8_value: cif.call::<u8>(*fun_ptr, &call_args),
|
||||
},
|
||||
NativeType::I8 => NativeValue {
|
||||
i8_value: cif.call::<i8>(*fun_ptr, &call_args),
|
||||
},
|
||||
NativeType::U16 => NativeValue {
|
||||
u16_value: cif.call::<u16>(*fun_ptr, &call_args),
|
||||
},
|
||||
NativeType::I16 => NativeValue {
|
||||
i16_value: cif.call::<i16>(*fun_ptr, &call_args),
|
||||
},
|
||||
NativeType::U32 => NativeValue {
|
||||
u32_value: cif.call::<u32>(*fun_ptr, &call_args),
|
||||
},
|
||||
NativeType::I32 => NativeValue {
|
||||
i32_value: cif.call::<i32>(*fun_ptr, &call_args),
|
||||
},
|
||||
NativeType::U64 => NativeValue {
|
||||
u64_value: cif.call::<u64>(*fun_ptr, &call_args),
|
||||
},
|
||||
NativeType::I64 => NativeValue {
|
||||
i64_value: cif.call::<i64>(*fun_ptr, &call_args),
|
||||
},
|
||||
NativeType::USize => NativeValue {
|
||||
usize_value: cif.call::<usize>(*fun_ptr, &call_args),
|
||||
},
|
||||
NativeType::ISize => NativeValue {
|
||||
isize_value: cif.call::<isize>(*fun_ptr, &call_args),
|
||||
},
|
||||
NativeType::F32 => NativeValue {
|
||||
f32_value: cif.call::<f32>(*fun_ptr, &call_args),
|
||||
},
|
||||
NativeType::F64 => NativeValue {
|
||||
f64_value: cif.call::<f64>(*fun_ptr, &call_args),
|
||||
},
|
||||
NativeType::Pointer | NativeType::Function => NativeValue {
|
||||
pointer: cif.call::<*const u8>(*fun_ptr, &call_args),
|
||||
},
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
fn ffi_call(
|
||||
call_args: Vec<NativeValue>,
|
||||
cif: &libffi::middle::Cif,
|
||||
|
@ -1404,39 +1691,6 @@ fn op_ffi_get_static<'scope>(
|
|||
})
|
||||
}
|
||||
|
||||
#[op(v8)]
|
||||
fn op_ffi_call<'scope>(
|
||||
scope: &mut v8::HandleScope<'scope>,
|
||||
state: Rc<RefCell<deno_core::OpState>>,
|
||||
rid: ResourceId,
|
||||
symbol: String,
|
||||
parameters: serde_v8::Value<'scope>,
|
||||
) -> Result<serde_v8::Value<'scope>, AnyError> {
|
||||
let symbol = {
|
||||
let state = &mut state.borrow();
|
||||
let resource = state.resource_table.get::<DynamicLibraryResource>(rid)?;
|
||||
|
||||
resource
|
||||
.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 = ffi_call(
|
||||
call_args,
|
||||
&symbol.cif,
|
||||
symbol.ptr,
|
||||
&symbol.parameter_types,
|
||||
symbol.result_type,
|
||||
)?;
|
||||
// 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) };
|
||||
Ok(result)
|
||||
}
|
||||
|
||||
/// A non-blocking FFI call.
|
||||
#[op(v8)]
|
||||
fn op_ffi_call_nonblocking<'scope>(
|
||||
|
@ -1450,7 +1704,7 @@ fn op_ffi_call_nonblocking<'scope>(
|
|||
let state = state.borrow();
|
||||
let resource = state.resource_table.get::<DynamicLibraryResource>(rid)?;
|
||||
let symbols = &resource.symbols;
|
||||
symbols
|
||||
*symbols
|
||||
.get(&symbol)
|
||||
.ok_or_else(|| type_error("Invalid FFI symbol name"))?
|
||||
.clone()
|
||||
|
|
Loading…
Reference in a new issue