// Copyright 2021 the Deno authors. All rights reserved. MIT license. use deno_core::error::bad_resource_id; use deno_core::error::AnyError; use deno_core::include_js_files; use deno_core::op_async; use deno_core::op_sync; use deno_core::serde_json::json; use deno_core::serde_json::Value; use deno_core::Extension; use deno_core::OpState; use deno_core::Resource; use deno_core::ResourceId; use deno_core::ZeroCopyBuf; use dlopen::raw::Library; use libffi::middle::Arg; use serde::Deserialize; use std::borrow::Cow; use std::cell::RefCell; use std::collections::HashMap; use std::ffi::c_void; use std::path::Path; use std::path::PathBuf; use std::rc::Rc; pub struct Unstable(pub bool); fn check_unstable(state: &OpState, api_name: &str) { let unstable = state.borrow::(); if !unstable.0 { eprintln!( "Unstable API '{}'. The --unstable flag must be provided.", api_name ); std::process::exit(70); } } pub trait FfiPermissions { fn check(&mut self, path: &Path) -> Result<(), AnyError>; } #[derive(Clone)] struct Symbol { cif: libffi::middle::Cif, ptr: libffi::middle::CodePtr, parameter_types: Vec, result_type: NativeType, } unsafe impl Send for Symbol {} unsafe impl Sync for Symbol {} struct DynamicLibraryResource { lib: Library, symbols: HashMap, } impl Resource for DynamicLibraryResource { fn name(&self) -> Cow { "dynamicLibrary".into() } fn close(self: Rc) { drop(self) } } impl DynamicLibraryResource { fn register( &mut self, symbol: String, foreign_fn: ForeignFunction, ) -> Result<(), AnyError> { let fn_ptr = unsafe { self.lib.symbol::<*const c_void>(&symbol) }?; 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( symbol, Symbol { cif, ptr, parameter_types: foreign_fn.parameters, result_type: foreign_fn.result, }, ); Ok(()) } } pub fn init(unstable: bool) -> Extension { Extension::builder() .js(include_js_files!( prefix "deno:ext/ffi", "00_ffi.js", )) .ops(vec![ ("op_ffi_load", op_sync(op_ffi_load::

)), ("op_ffi_call", op_sync(op_ffi_call)), ("op_ffi_call_nonblocking", op_async(op_ffi_call_nonblocking)), ]) .state(move |state| { // Stolen from deno_webgpu, is there a better option? state.put(Unstable(unstable)); Ok(()) }) .build() } #[derive(Clone, Copy, Debug, Deserialize, Eq, PartialEq)] #[serde(rename_all = "lowercase")] enum NativeType { Void, U8, I8, U16, I16, U32, I32, U64, I64, USize, ISize, F32, F64, Buffer, } impl From for libffi::middle::Type { fn from(native_type: NativeType) -> Self { match native_type { NativeType::Void => libffi::middle::Type::void(), NativeType::U8 => 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::Buffer => libffi::middle::Type::pointer(), } } } #[repr(C)] union NativeValue { void_value: (), 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, buffer: *const u8, } impl NativeValue { fn new(native_type: NativeType, value: Value) -> Self { match native_type { NativeType::Void => Self { void_value: () }, NativeType::U8 => Self { u8_value: value_as_uint::(value), }, NativeType::I8 => Self { i8_value: value_as_int::(value), }, NativeType::U16 => Self { u16_value: value_as_uint::(value), }, NativeType::I16 => Self { i16_value: value_as_int::(value), }, NativeType::U32 => Self { u32_value: value_as_uint::(value), }, NativeType::I32 => Self { i32_value: value_as_int::(value), }, NativeType::U64 => Self { u64_value: value_as_uint::(value), }, NativeType::I64 => Self { i64_value: value_as_int::(value), }, NativeType::USize => Self { usize_value: value_as_uint::(value), }, NativeType::ISize => Self { isize_value: value_as_int::(value), }, NativeType::F32 => Self { f32_value: value_as_f32(value), }, NativeType::F64 => Self { f64_value: value_as_f64(value), }, NativeType::Buffer => unreachable!(), } } fn buffer(ptr: *const u8) -> Self { Self { buffer: ptr } } unsafe fn as_arg(&self, native_type: NativeType) -> Arg { match native_type { NativeType::Void => Arg::new(&self.void_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::Buffer => Arg::new(&self.buffer), } } } fn value_as_uint>(value: Value) -> T { value .as_u64() .and_then(|v| T::try_from(v).ok()) .expect("Expected ffi arg value to be an unsigned integer") } fn value_as_int>(value: Value) -> T { value .as_i64() .and_then(|v| T::try_from(v).ok()) .expect("Expected ffi arg value to be a signed integer") } fn value_as_f32(value: Value) -> f32 { value_as_f64(value) as f32 } fn value_as_f64(value: Value) -> f64 { value .as_f64() .expect("Expected ffi arg value to be a float") } #[derive(Deserialize, Debug)] #[serde(rename_all = "camelCase")] struct ForeignFunction { parameters: Vec, result: NativeType, } #[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 { unsafe { let length = 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 { let err_num = 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(), } } fn op_ffi_load( state: &mut deno_core::OpState, args: FfiLoadArgs, _: (), ) -> Result where FP: FfiPermissions + 'static, { let path = args.path; check_unstable(state, "Deno.dlopen"); let permissions = state.borrow_mut::(); permissions.check(&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(), }; for (symbol, foreign_fn) in args.symbols { resource.register(symbol, foreign_fn)?; } Ok(state.resource_table.add(resource)) } #[derive(Deserialize)] #[serde(rename_all = "camelCase")] struct FfiCallArgs { rid: ResourceId, symbol: String, parameters: Vec, buffers: Vec, } fn ffi_call(args: FfiCallArgs, symbol: &Symbol) -> Result { let buffers: Vec<&[u8]> = args.buffers.iter().map(|buffer| &buffer[..]).collect(); let native_values = symbol .parameter_types .iter() .zip(args.parameters.into_iter()) .map(|(&native_type, value)| { if let NativeType::Buffer = native_type { let idx: usize = value_as_uint(value); let ptr = buffers[idx].as_ptr(); NativeValue::buffer(ptr) } else { NativeValue::new(native_type, value) } }) .collect::>(); let call_args = symbol .parameter_types .iter() .zip(native_values.iter()) .map(|(&native_type, native_value)| unsafe { native_value.as_arg(native_type) }) .collect::>(); Ok(match symbol.result_type { NativeType::Void => { json!(unsafe { symbol.cif.call::<()>(symbol.ptr, &call_args) }) } NativeType::U8 => { json!(unsafe { symbol.cif.call::(symbol.ptr, &call_args) }) } NativeType::I8 => { json!(unsafe { symbol.cif.call::(symbol.ptr, &call_args) }) } NativeType::U16 => { json!(unsafe { symbol.cif.call::(symbol.ptr, &call_args) }) } NativeType::I16 => { json!(unsafe { symbol.cif.call::(symbol.ptr, &call_args) }) } NativeType::U32 => { json!(unsafe { symbol.cif.call::(symbol.ptr, &call_args) }) } NativeType::I32 => { json!(unsafe { symbol.cif.call::(symbol.ptr, &call_args) }) } NativeType::U64 => { json!(unsafe { symbol.cif.call::(symbol.ptr, &call_args) }) } NativeType::I64 => { json!(unsafe { symbol.cif.call::(symbol.ptr, &call_args) }) } NativeType::USize => { json!(unsafe { symbol.cif.call::(symbol.ptr, &call_args) }) } NativeType::ISize => { json!(unsafe { symbol.cif.call::(symbol.ptr, &call_args) }) } NativeType::F32 => { json!(unsafe { symbol.cif.call::(symbol.ptr, &call_args) }) } NativeType::F64 => { json!(unsafe { symbol.cif.call::(symbol.ptr, &call_args) }) } NativeType::Buffer => unreachable!(), }) } fn op_ffi_call( state: &mut deno_core::OpState, args: FfiCallArgs, _: (), ) -> Result { let resource = state .resource_table .get::(args.rid)?; let symbol = resource .symbols .get(&args.symbol) .ok_or_else(bad_resource_id)?; ffi_call(args, symbol) } /// A non-blocking FFI call. async fn op_ffi_call_nonblocking( state: Rc>, args: FfiCallArgs, _: (), ) -> Result { let resource = state .borrow() .resource_table .get::(args.rid)?; let symbols = &resource.symbols; let symbol = symbols .get(&args.symbol) .ok_or_else(bad_resource_id)? .clone(); tokio::task::spawn_blocking(move || ffi_call(args, &symbol)) .await .unwrap() } #[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(), ); } }