mashin_ffi 0.1.0

FFI engine for mashin
Documentation
// Copyright 2018-2023 the Deno authors. All rights reserved. MIT license.

use crate::check_unstable;
use crate::ir::out_buffer_as_ptr;
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::OpState;
use deno_core::Resource;
use deno_core::ResourceId;
use dlopen::raw::Library;
use serde::Deserialize;
use serde_value::ValueDeserializer;
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<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, 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::<*mut 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::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<String>,
    pub parameters: Vec<NativeType>,
    pub result: NativeType,
    #[serde(rename = "nonblocking")]
    non_blocking: Option<bool>,
    #[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<String>,
    #[serde(rename(deserialize = "type"))]
    _type: String,
}

#[derive(Debug)]
enum ForeignSymbol {
    ForeignFunction(ForeignFunction),
    ForeignStatic(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>,
}

#[op(v8)]
pub fn op_ffi_load<FP, 'scope>(
    scope: &mut v8::HandleScope<'scope>,
    state: &mut 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::<FP>();
    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::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 {
                    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<Symbol>,
) -> 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<v8::External> = 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,
            };
            let out_buffer = match 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, symbol, out_buffer) {
                Ok(result) => {
                    match needs_unwrap {
                        Some(v) => {
                            let view: v8::Local<v8::ArrayBufferView> = v.try_into().unwrap();
                            let pointer =
                                view.buffer(scope).unwrap().data().unwrap().as_ptr() as *mut u8;

                            if is_i64(&symbol.result_type) {
                                // SAFETY: v8::SharedRef<v8::BackingStore> is similar to Arc<[u8]>,
                                // it points to a fixed continuous slice of bytes on the heap.
                                let bs = unsafe { &mut *(pointer as *mut i64) };
                                // SAFETY: We already checked that type == I64
                                let value = unsafe { result.i64_value };
                                *bs = value;
                            } else {
                                // SAFETY: v8::SharedRef<v8::BackingStore> is similar to Arc<[u8]>,
                                // it points to a fixed continuous slice of bytes on the heap.
                                let bs = unsafe { &mut *(pointer as *mut u64) };
                                // SAFETY: We checked that type == U64
                                let value = unsafe { result.u64_value };
                                *bs = value;
                            }
                        }
                        None => {
                            let result =
                // SAFETY: Same return type declared to libffi; trust user to have it right beyond that.
                unsafe { result.to_v8(scope, symbol.result_type.clone()) };
                            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,
            None,
            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::<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 super::ForeignFunction;
    use super::ForeignSymbol;
    use crate::symbol::NativeType;
    use serde_json::json;

    #[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(),
        );
    }

    /// 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"));
    }
}