mirror of
https://github.com/denoland/deno.git
synced 2025-03-10 14:17:49 -04:00

Now that ArrayBuffer/ArrayBufferView is a generic Value type, we have to handle it being passed any value. To do this, thread FastApiCallbackOptions through the function, and add error raising logic. If we run conversion and the value is not valid, we return `isize::MAX`, and then in cranelift we use this value to know that we should branch to the error logic. An example compilation looks like this: ```rust extern "C" fn print_buffer(ptr: *const u8, len: usize); ``` ```clif function %print_buffer_wrapper(i64, i64, i64, i64) system_v { sig0 = (i64, i64) system_v sig1 = (i64) -> i64 system_v sig2 = (i64) system_v block0(v0: i64, v1: i64, v2: i64, v3: i64): v4 = iconst.i64 0x6525_9198_2d00 ; turbocall_ab_contents v5 = call_indirect sig1, v4(v1) v6 = iconst.i64 0x7fff_ffff_ffff_ffff v7 = icmp eq v5, v6 brif v7, block1, block2 block2: v8 = iconst.i64 0x7558_4c0c_0700 ; sym.ptr call_indirect sig0, v8(v5, v2) return block1 cold: v9 = iconst.i64 0x6525_9198_2d70 ; turbocall_raise call_indirect sig2, v9(v3) return } ``` Also cleaned up all the `unwrap`s and added some logging.
483 lines
14 KiB
Rust
483 lines
14 KiB
Rust
// Copyright 2018-2025 the Deno authors. MIT license.
|
|
|
|
use std::borrow::Cow;
|
|
use std::cell::RefCell;
|
|
use std::collections::HashMap;
|
|
use std::ffi::c_void;
|
|
use std::rc::Rc;
|
|
|
|
use deno_core::op2;
|
|
use deno_core::v8;
|
|
use deno_core::GarbageCollected;
|
|
use deno_core::OpState;
|
|
use deno_core::Resource;
|
|
use deno_error::JsErrorBox;
|
|
use deno_error::JsErrorClass;
|
|
use dlopen2::raw::Library;
|
|
use serde::Deserialize;
|
|
use serde_value::ValueDeserializer;
|
|
|
|
use crate::ir::out_buffer_as_ptr;
|
|
use crate::symbol::NativeType;
|
|
use crate::symbol::Symbol;
|
|
use crate::turbocall;
|
|
use crate::turbocall::Turbocall;
|
|
use crate::FfiPermissions;
|
|
|
|
deno_error::js_error_wrapper!(dlopen2::Error, JsDlopen2Error, |err| {
|
|
match err {
|
|
dlopen2::Error::NullCharacter(_) => "InvalidData".into(),
|
|
dlopen2::Error::OpeningLibraryError(e) => e.get_class(),
|
|
dlopen2::Error::SymbolGettingError(e) => e.get_class(),
|
|
dlopen2::Error::AddrNotMatchingDll(e) => e.get_class(),
|
|
dlopen2::Error::NullSymbol => "NotFound".into(),
|
|
}
|
|
});
|
|
|
|
#[derive(Debug, thiserror::Error, deno_error::JsError)]
|
|
pub enum DlfcnError {
|
|
#[class(generic)]
|
|
#[error("Failed to register symbol {symbol}: {error}")]
|
|
RegisterSymbol {
|
|
symbol: String,
|
|
#[source]
|
|
error: dlopen2::Error,
|
|
},
|
|
#[class(generic)]
|
|
#[error(transparent)]
|
|
Dlopen(#[from] dlopen2::Error),
|
|
#[class(inherit)]
|
|
#[error(transparent)]
|
|
Permission(#[from] deno_permissions::PermissionCheckError),
|
|
#[class(inherit)]
|
|
#[error(transparent)]
|
|
Other(#[from] JsErrorBox),
|
|
}
|
|
|
|
pub struct DynamicLibraryResource {
|
|
lib: Library,
|
|
pub symbols: HashMap<String, Box<Symbol>>,
|
|
}
|
|
|
|
impl Resource for DynamicLibraryResource {
|
|
fn name(&self) -> Cow<str> {
|
|
"dynamicLibrary".into()
|
|
}
|
|
|
|
fn close(self: Rc<Self>) {
|
|
drop(self)
|
|
}
|
|
}
|
|
|
|
impl DynamicLibraryResource {
|
|
pub fn get_static(&self, symbol: String) -> Result<*mut c_void, DlfcnError> {
|
|
// 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::<*mut c_void>(&symbol) } {
|
|
Ok(value) => Ok(Ok(value)),
|
|
Err(error) => Err(DlfcnError::RegisterSymbol { symbol, error }),
|
|
}?
|
|
}
|
|
}
|
|
|
|
#[derive(Deserialize, Debug)]
|
|
#[serde(rename_all = "camelCase")]
|
|
pub struct ForeignFunction {
|
|
name: Option<String>,
|
|
pub parameters: Vec<NativeType>,
|
|
pub result: NativeType,
|
|
#[serde(rename = "nonblocking")]
|
|
non_blocking: Option<bool>,
|
|
#[serde(rename = "optional")]
|
|
#[serde(default = "default_optional")]
|
|
optional: bool,
|
|
}
|
|
|
|
fn default_optional() -> 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<String>,
|
|
#[serde(rename(deserialize = "type"))]
|
|
_type: String,
|
|
}
|
|
|
|
#[derive(Debug)]
|
|
enum ForeignSymbol {
|
|
ForeignFunction(ForeignFunction),
|
|
ForeignStatic(#[allow(dead_code)] ForeignStatic),
|
|
}
|
|
|
|
impl<'de> Deserialize<'de> for ForeignSymbol {
|
|
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
|
|
where
|
|
D: serde::Deserializer<'de>,
|
|
{
|
|
let value = serde_value::Value::deserialize(deserializer)?;
|
|
|
|
// Probe a ForeignStatic and if that doesn't match, assume ForeignFunction to improve error messages
|
|
if let Ok(res) = ForeignStatic::deserialize(
|
|
ValueDeserializer::<D::Error>::new(value.clone()),
|
|
) {
|
|
Ok(ForeignSymbol::ForeignStatic(res))
|
|
} else {
|
|
ForeignFunction::deserialize(ValueDeserializer::<D::Error>::new(value))
|
|
.map(ForeignSymbol::ForeignFunction)
|
|
}
|
|
}
|
|
}
|
|
|
|
#[derive(Deserialize, Debug)]
|
|
pub struct FfiLoadArgs {
|
|
path: String,
|
|
symbols: HashMap<String, ForeignSymbol>,
|
|
}
|
|
|
|
#[op2(stack_trace)]
|
|
pub fn op_ffi_load<'scope, FP>(
|
|
scope: &mut v8::HandleScope<'scope>,
|
|
state: Rc<RefCell<OpState>>,
|
|
#[serde] args: FfiLoadArgs,
|
|
) -> Result<v8::Local<'scope, v8::Value>, DlfcnError>
|
|
where
|
|
FP: FfiPermissions + 'static,
|
|
{
|
|
let path = {
|
|
let mut state = state.borrow_mut();
|
|
let permissions = state.borrow_mut::<FP>();
|
|
permissions.check_partial_with_path(&args.path)?
|
|
};
|
|
|
|
let lib = Library::open(&path).map_err(|e| {
|
|
dlopen2::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) => 'register_symbol: {
|
|
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(error) => if foreign_fn.optional {
|
|
let null: v8::Local<v8::Value> = v8::null(scope).into();
|
|
let func_key = v8::String::new(scope, &symbol_key).unwrap();
|
|
obj.set(scope, func_key.into(), null);
|
|
break 'register_symbol;
|
|
} else {
|
|
Err(DlfcnError::RegisterSymbol {
|
|
symbol: symbol.to_owned(),
|
|
error,
|
|
})
|
|
},
|
|
}?;
|
|
|
|
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::try_from)
|
|
.collect::<Result<Vec<_>, _>>()?,
|
|
foreign_fn.result.clone().try_into()?,
|
|
);
|
|
|
|
let func_key = v8::String::new(scope, &symbol_key).unwrap();
|
|
let sym = Box::new(Symbol {
|
|
name: symbol_key.clone(),
|
|
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, sym);
|
|
obj.set(scope, func_key.into(), function.into());
|
|
}
|
|
// This optimization is not yet supported for non-blocking calls.
|
|
_ => {}
|
|
};
|
|
}
|
|
}
|
|
}
|
|
|
|
let mut state = state.borrow_mut();
|
|
let out = v8::Array::new(scope, 2);
|
|
let rid = state.resource_table.add(resource);
|
|
let rid_v8 = v8::Integer::new_from_unsigned(scope, rid);
|
|
out.set_index(scope, 0, rid_v8.into());
|
|
out.set_index(scope, 1, obj.into());
|
|
Ok(out.into())
|
|
}
|
|
|
|
struct FunctionData {
|
|
// Held in a box to keep memory while function is alive.
|
|
#[allow(unused)]
|
|
symbol: Box<Symbol>,
|
|
// Held in a box to keep inner data alive while function is alive.
|
|
#[allow(unused)]
|
|
turbocall: Option<Turbocall>,
|
|
}
|
|
|
|
impl GarbageCollected for FunctionData {}
|
|
|
|
// Create a JavaScript function for synchronous FFI call to
|
|
// the given symbol.
|
|
fn make_sync_fn<'s>(
|
|
scope: &mut v8::HandleScope<'s>,
|
|
symbol: Box<Symbol>,
|
|
) -> v8::Local<'s, v8::Function> {
|
|
let turbocall = if turbocall::is_compatible(&symbol) {
|
|
match turbocall::compile_trampoline(&symbol) {
|
|
Ok(trampoline) => {
|
|
let turbocall = turbocall::make_template(&symbol, trampoline);
|
|
Some(turbocall)
|
|
}
|
|
Err(e) => {
|
|
log::warn!("Failed to compile FFI turbocall: {e}");
|
|
None
|
|
}
|
|
}
|
|
} else {
|
|
None
|
|
};
|
|
|
|
let c_function = turbocall.as_ref().map(|turbocall| {
|
|
v8::fast_api::CFunction::new(
|
|
turbocall.trampoline.ptr(),
|
|
&turbocall.c_function_info,
|
|
)
|
|
});
|
|
|
|
let data = FunctionData { symbol, turbocall };
|
|
let data = deno_core::cppgc::make_cppgc_object(scope, data);
|
|
|
|
let builder = v8::FunctionTemplate::builder(sync_fn_impl).data(data.into());
|
|
|
|
let func = if let Some(c_function) = c_function {
|
|
builder.build_fast(scope, &[c_function])
|
|
} else {
|
|
builder.build(scope)
|
|
};
|
|
func.get_function(scope).unwrap()
|
|
}
|
|
|
|
fn sync_fn_impl<'s>(
|
|
scope: &mut v8::HandleScope<'s>,
|
|
args: v8::FunctionCallbackArguments<'s>,
|
|
mut rv: v8::ReturnValue,
|
|
) {
|
|
let data = deno_core::cppgc::try_unwrap_cppgc_object::<FunctionData>(
|
|
scope,
|
|
args.data(),
|
|
)
|
|
.unwrap();
|
|
let out_buffer = match data.symbol.result_type {
|
|
NativeType::Struct(_) => {
|
|
let argc = args.length();
|
|
out_buffer_as_ptr(
|
|
scope,
|
|
Some(
|
|
v8::Local::<v8::TypedArray>::try_from(args.get(argc - 1)).unwrap(),
|
|
),
|
|
)
|
|
}
|
|
_ => None,
|
|
};
|
|
match crate::call::ffi_call_sync(scope, args, &data.symbol, out_buffer) {
|
|
Ok(result) => {
|
|
let result =
|
|
// SAFETY: Same return type declared to libffi; trust user to have it right beyond that.
|
|
unsafe { result.to_v8(scope, data.symbol.result_type.clone()) };
|
|
rv.set(result);
|
|
}
|
|
Err(err) => deno_core::error::throw_js_error_class(scope, &err),
|
|
};
|
|
}
|
|
|
|
// `path` is only used on Windows.
|
|
#[allow(unused_variables)]
|
|
pub(crate) fn format_error(
|
|
e: dlopen2::Error,
|
|
path: &std::path::Path,
|
|
) -> 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
|
|
dlopen2::Error::OpeningLibraryError(e) => {
|
|
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 = path
|
|
.as_os_str()
|
|
.encode_wide()
|
|
.chain(Some(0))
|
|
.collect::<Vec<_>>();
|
|
|
|
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 {
|
|
use serde_json::json;
|
|
|
|
use super::ForeignFunction;
|
|
use super::ForeignSymbol;
|
|
use crate::symbol::NativeType;
|
|
|
|
#[cfg(target_os = "windows")]
|
|
#[test]
|
|
fn test_format_error() {
|
|
use super::format_error;
|
|
|
|
// BAD_EXE_FORMAT
|
|
let err = dlopen2::Error::OpeningLibraryError(
|
|
std::io::Error::from_raw_os_error(0x000000C1),
|
|
);
|
|
assert_eq!(
|
|
format_error(err, &std::path::PathBuf::from("foo.dll")),
|
|
"foo.dll is not a valid Win32 application.\r\n".to_string(),
|
|
);
|
|
}
|
|
|
|
/// Ensure that our custom serialize for ForeignSymbol is working using `serde_json`.
|
|
#[test]
|
|
fn test_serialize_foreign_symbol() {
|
|
let symbol: ForeignSymbol = serde_json::from_value(json! {{
|
|
"name": "test",
|
|
"type": "type is unused"
|
|
}})
|
|
.expect("Failed to parse");
|
|
assert!(matches!(symbol, ForeignSymbol::ForeignStatic(..)));
|
|
|
|
let symbol: ForeignSymbol = serde_json::from_value(json! {{
|
|
"name": "test",
|
|
"parameters": ["i64"],
|
|
"result": "bool"
|
|
}})
|
|
.expect("Failed to parse");
|
|
if let ForeignSymbol::ForeignFunction(ForeignFunction {
|
|
name: Some(expected_name),
|
|
parameters,
|
|
..
|
|
}) = symbol
|
|
{
|
|
assert_eq!(expected_name, "test");
|
|
assert_eq!(parameters, vec![NativeType::I64]);
|
|
} else {
|
|
panic!("Failed to parse ForeignFunction as expected");
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn test_serialize_foreign_symbol_failures() {
|
|
let error = serde_json::from_value::<ForeignSymbol>(json! {{
|
|
"name": "test",
|
|
"parameters": ["int"],
|
|
"result": "bool"
|
|
}})
|
|
.expect_err("Expected this to fail");
|
|
assert!(error.to_string().contains("expected one of"));
|
|
|
|
let error = serde_json::from_value::<ForeignSymbol>(json! {{
|
|
"name": "test",
|
|
"parameters": ["i64"],
|
|
"result": "int"
|
|
}})
|
|
.expect_err("Expected this to fail");
|
|
assert!(error.to_string().contains("expected one of"));
|
|
}
|
|
}
|