diff --git a/Cargo.lock b/Cargo.lock index 350702abc8df16..3af0fe03933750 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -10,6 +10,12 @@ dependencies = [ "regex", ] +[[package]] +name = "abort_on_panic" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "955f37ac58af2416bac687c8ab66a4ccba282229bd7422a28d2281a5e66a6116" + [[package]] name = "adler" version = "0.2.3" @@ -549,6 +555,7 @@ dependencies = [ "indexmap", "lazy_static", "libc", + "libffi", "log", "nix", "notify", @@ -1306,6 +1313,27 @@ version = "0.2.82" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "89203f3fba0a3795506acaad8ebce3c80c0af93f994d5a1d7a0b1eeb23271929" +[[package]] +name = "libffi" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bafef83ee22d51c27348aaf6b2da007a32b9f5004809d09271432e5ea2a795dd" +dependencies = [ + "abort_on_panic", + "libc", + "libffi-sys", +] + +[[package]] +name = "libffi-sys" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b6d65142f1c3b06ca3f4216da4d32b3124d14d932cef8dfd8792037acd2160b" +dependencies = [ + "cc", + "make-cmd", +] + [[package]] name = "lock_api" version = "0.4.2" @@ -1373,6 +1401,12 @@ dependencies = [ "syn 1.0.56", ] +[[package]] +name = "make-cmd" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a8ca8afbe8af1785e09636acb5a41e08a765f5f0340568716c18a8700ba3c0d3" + [[package]] name = "matches" version = "0.1.8" @@ -2789,6 +2823,14 @@ dependencies = [ "winapi-util", ] +[[package]] +name = "test_ffi" +version = "0.0.1" +dependencies = [ + "libc", + "test_util", +] + [[package]] name = "test_plugin" version = "0.0.1" diff --git a/Cargo.toml b/Cargo.toml index 9c6cde3f9f66d5..a95d71d85081d1 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -6,6 +6,7 @@ members = [ "core", "runtime", "test_plugin", + "test_ffi", "test_util", "op_crates/fetch", "op_crates/web", diff --git a/cli/dts/lib.deno.unstable.d.ts b/cli/dts/lib.deno.unstable.d.ts index 1de7ed8cc92d9a..55d20da6503f1e 100644 --- a/cli/dts/lib.deno.unstable.d.ts +++ b/cli/dts/lib.deno.unstable.d.ts @@ -1255,6 +1255,57 @@ declare namespace Deno { * ``` */ export function sleepSync(millis: number): Promise; + + export type DyLibraryDataType = + | "i8" + | "i16" + | "i32" + | "i64" + | "u8" + | "u16" + | "u32" + | "u64" + | "f32" + | "f64"; + + export interface CallDylibraryOptions { + params?: { + typeName: DyLibraryDataType; + value: any; + }[]; + returnType?: DyLibraryDataType; + } + + export class DyLibrary { + /** Call dynamic library function + * + * ```ts + * const lib = await Deno.loadLibrary("./libtest.dylib"); + * const rval = lib.call("some_func", { + * params: [{ typeName: "i32", value: 10 }] + * returnType: "i32" + * }); + * console.log(rval); + * ``` + */ + call(name: string, options?: CallDylibraryOptions): T; + + /** Unload dynamic library */ + close(): void; + } + + /** **UNSTABLE**: new API, yet to be vetted. + * + * Load a dynamic library from the given file name, + * + * ```ts + * const lib = await Deno.loadLibrary("./libtest.dylib"); + * lib.call("some_func"); + * ``` + * + * Requires `allow-all` permission. + */ + export function loadLibrary(filename: string): DyLibrary; } declare function fetch( diff --git a/runtime/Cargo.toml b/runtime/Cargo.toml index b1718ee9545117..e18895defc00e6 100644 --- a/runtime/Cargo.toml +++ b/runtime/Cargo.toml @@ -47,6 +47,7 @@ lazy_static = "1.4.0" libc = "0.2.82" log = "0.4.13" notify = "5.0.0-pre.4" +libffi = "1.0.0" percent-encoding = "2.1.0" regex = "1.4.3" ring = "0.16.19" diff --git a/runtime/js/40_ffi.js b/runtime/js/40_ffi.js new file mode 100644 index 00000000000000..d40d26d0573efa --- /dev/null +++ b/runtime/js/40_ffi.js @@ -0,0 +1,35 @@ +// Copyright 2018-2020 the Deno authors. All rights reserved. MIT license. + +((window) => { + const core = window.Deno.core; + + class DyLibaray { + #rid = 0; + constructor(rid) { + this.#rid = rid; + } + + call(name, options) { + const { params = [], returnType = "" } = options ?? {}; + return core.jsonOpSync("op_call_libaray_ffi", { + rid: this.#rid, + name, + params, + returnType, + }); + } + + close() { + core.close(this.#rid); + } + } + + function loadLibrary(filename) { + const rid = core.jsonOpSync("op_load_libaray", { filename }); + return new DyLibaray(rid); + } + + window.__bootstrap.ffi = { + loadLibrary, + }; +})(this); diff --git a/runtime/js/90_deno_ns.js b/runtime/js/90_deno_ns.js index c4205a1a919eaa..a2f8e01a7d84ca 100644 --- a/runtime/js/90_deno_ns.js +++ b/runtime/js/90_deno_ns.js @@ -130,5 +130,6 @@ symlinkSync: __bootstrap.fs.symlinkSync, HttpClient: __bootstrap.fetch.HttpClient, createHttpClient: __bootstrap.fetch.createHttpClient, + loadLibrary: __bootstrap.ffi.loadLibrary, }; })(this); diff --git a/runtime/ops/ffi.rs b/runtime/ops/ffi.rs new file mode 100644 index 00000000000000..0ee89aa21c529d --- /dev/null +++ b/runtime/ops/ffi.rs @@ -0,0 +1,223 @@ +// Copyright 2018-2020 the Deno authors. All rights reserved. MIT license. + +use std::{any::Any, borrow::Cow, ffi::c_void, path::PathBuf, rc::Rc}; + +use deno_core::{ + error::AnyError, + serde_json::{self, json, Value}, + OpState, Resource, ZeroCopyBuf, +}; +use dlopen::symbor::Library; +use libffi::high as ffi; +use serde::Deserialize; + +#[derive(Deserialize)] +#[serde(rename_all = "camelCase")] +struct LoadLibarayArgs { + filename: String, +} + +#[derive(Deserialize)] +#[serde(rename_all = "camelCase")] +struct CallParam { + type_name: String, + value: Value, +} + +#[derive(Deserialize)] +#[serde(rename_all = "camelCase")] +struct CallLibarayFfiArgs { + rid: u32, + name: String, + params: Vec, + return_type: String, +} + +struct DylibResource { + lib: Rc, +} + +impl Resource for DylibResource { + fn name(&self) -> Cow { + "dylib".into() + } +} + +impl DylibResource { + fn new(lib: &Rc) -> Self { + Self { lib: lib.clone() } + } +} + +pub fn init(rt: &mut deno_core::JsRuntime) { + super::reg_json_sync(rt, "op_load_libaray", op_load_libaray); + super::reg_json_sync(rt, "op_call_libaray_ffi", op_call_libaray_ffi); +} + +fn op_load_libaray( + state: &mut OpState, + args: Value, + _zero_copy: &mut [ZeroCopyBuf], +) -> Result { + let args: LoadLibarayArgs = serde_json::from_value(args)?; + let filename = PathBuf::from(&args.filename); + + debug!("Loading Libaray: {:#?}", filename); + let dy_lib = Library::open(filename).map(Rc::new)?; + let dylib_resource = DylibResource::new(&dy_lib); + + let rid = state.resource_table.add(dylib_resource); + + Ok(json!(rid)) +} + +fn op_call_libaray_ffi( + state: &mut OpState, + args: Value, + _zero_copy: &mut [ZeroCopyBuf], +) -> Result { + let args: CallLibarayFfiArgs = serde_json::from_value(args)?; + let mut call_args: Vec<(Box, String)> = vec![]; + + args + .params + .iter() + .for_each(|param| match param.type_name.as_str() { + "i8" => { + let val = param.value.as_i64().unwrap() as i8; + call_args.push((Box::new(val), param.type_name.clone())); + } + "i16" => { + let val = param.value.as_i64().unwrap() as i16; + call_args.push((Box::new(val), param.type_name.clone())); + } + "i32" => { + let val = param.value.as_i64().unwrap() as i32; + call_args.push((Box::new(val), param.type_name.clone())); + } + "i64" => { + let val = param.value.as_i64().unwrap(); + call_args.push((Box::new(val), param.type_name.clone())); + } + "u8" => { + let val = param.value.as_u64().unwrap() as u8; + call_args.push((Box::new(val), param.type_name.clone())); + } + "u32" => { + let val = param.value.as_u64().unwrap() as u32; + call_args.push((Box::new(val), param.type_name.clone())); + } + "u64" => { + let val = param.value.as_u64().unwrap(); + call_args.push((Box::new(val), param.type_name.clone())); + } + "f32" => { + let val = param.value.as_f64().unwrap() as f32; + call_args.push((Box::new(val), param.type_name.clone())); + } + "f64" => { + let val = param.value.as_f64().unwrap(); + call_args.push((Box::new(val), param.type_name.clone())); + } + _ => {} + }); + + let params: Vec = call_args + .iter() + .map(|param| { + let (val, name) = param; + match name.as_str() { + "i8" => { + let v = val.downcast_ref::().unwrap(); + ffi::arg(&*v) + } + "i16" => { + let v = val.downcast_ref::().unwrap(); + ffi::arg(&*v) + } + "i32" => { + let v = val.downcast_ref::().unwrap(); + ffi::arg(&*v) + } + "i64" => { + let v = val.downcast_ref::().unwrap(); + ffi::arg(&*v) + } + "u8" => { + let v = val.downcast_ref::().unwrap(); + ffi::arg(&*v) + } + "u16" => { + let v = val.downcast_ref::().unwrap(); + ffi::arg(&*v) + } + "u32" => { + let v = val.downcast_ref::().unwrap(); + ffi::arg(&*v) + } + "u64" => { + let v = val.downcast_ref::().unwrap(); + ffi::arg(&*v) + } + "f32" => { + let v = val.downcast_ref::().unwrap(); + ffi::arg(&*v) + } + "f64" => { + let v = val.downcast_ref::().unwrap(); + ffi::arg(&*v) + } + _ => ffi::arg(&()), + } + }) + .collect(); + + let lib = state + .resource_table + .get::(args.rid) + .unwrap() + .lib + .clone(); + + let fn_ptr: *const c_void = *unsafe { lib.symbol(&args.name) }?; + let fn_code_ptr = ffi::CodePtr::from_ptr(fn_ptr); + + let ret = match args.return_type.as_str() { + "i8" => { + json!(unsafe { ffi::call::(fn_code_ptr, params.as_slice()) }) + } + "i16" => { + json!(unsafe { ffi::call::(fn_code_ptr, params.as_slice()) }) + } + "i32" => { + json!(unsafe { ffi::call::(fn_code_ptr, params.as_slice()) }) + } + "i64" => { + json!(unsafe { ffi::call::(fn_code_ptr, params.as_slice()) }) + } + "u8" => { + json!(unsafe { ffi::call::(fn_code_ptr, params.as_slice()) }) + } + "u16" => { + json!(unsafe { ffi::call::(fn_code_ptr, params.as_slice()) }) + } + "u32" => { + json!(unsafe { ffi::call::(fn_code_ptr, params.as_slice()) }) + } + "u64" => { + json!(unsafe { ffi::call::(fn_code_ptr, params.as_slice()) }) + } + "f32" => { + json!(unsafe { ffi::call::(fn_code_ptr, params.as_slice()) }) + } + "f64" => { + json!(unsafe { ffi::call::(fn_code_ptr, params.as_slice()) }) + } + _ => { + unsafe { ffi::call::<()>(fn_code_ptr, params.as_slice()) }; + json!(null) + } + }; + + Ok(ret) +} diff --git a/runtime/ops/mod.rs b/runtime/ops/mod.rs index 0da4b977198679..d126963dfc49e6 100644 --- a/runtime/ops/mod.rs +++ b/runtime/ops/mod.rs @@ -23,6 +23,7 @@ pub mod tty; pub mod web_worker; pub mod websocket; pub mod worker_host; +pub mod ffi; use crate::metrics::metrics_op; use deno_core::error::AnyError; diff --git a/runtime/worker.rs b/runtime/worker.rs index a619ecc4c7d47a..9f1b65b6225eee 100644 --- a/runtime/worker.rs +++ b/runtime/worker.rs @@ -147,6 +147,7 @@ impl MainWorker { options.user_agent.clone(), options.ca_data.clone(), ); + ops::ffi::init(js_runtime); } { let op_state = js_runtime.op_state(); diff --git a/test_ffi/Cargo.toml b/test_ffi/Cargo.toml new file mode 100644 index 00000000000000..256fef81991899 --- /dev/null +++ b/test_ffi/Cargo.toml @@ -0,0 +1,17 @@ + +# Copyright 2018-2020 the Deno authors. All rights reserved. MIT license. + +[package] +name = "test_ffi" +version = "0.0.1" +authors = ["the deno authors"] +edition = "2018" + +[lib] +crate-type = ["cdylib"] + +[dependencies] +libc = "0.2.80" + +[dev-dependencies] +test_util = { path = "../test_util" } \ No newline at end of file diff --git a/test_ffi/src/lib.rs b/test_ffi/src/lib.rs new file mode 100644 index 00000000000000..23e10a215cdb52 --- /dev/null +++ b/test_ffi/src/lib.rs @@ -0,0 +1,54 @@ +#[no_mangle] +pub fn print_something() { + println!("something"); +} + +#[no_mangle] +pub fn add_one_i8(arg: i8) -> i8 { + arg + 1 +} + +#[no_mangle] +pub fn add_one_i16(arg: i16) -> i16 { + arg + 1 +} + +#[no_mangle] +pub fn add_one_i32(arg: i32) -> i32 { + arg + 1 +} + +#[no_mangle] +pub fn add_one_i64(arg: i64) -> i64 { + arg + 1 +} + +#[no_mangle] +pub fn add_one_u8(arg: u8) -> u8 { + arg + 1 +} + +#[no_mangle] +pub fn add_one_u16(arg: u16) -> u16 { + arg + 1 +} + +#[no_mangle] +pub fn add_one_u32(arg: u32) -> u32 { + arg + 1 +} + +#[no_mangle] +pub fn add_one_u64(arg: u64) -> u64 { + arg + 1 +} + +#[no_mangle] +pub fn add_one_f32(arg: f32) -> f32 { + arg + 1.0 +} + +#[no_mangle] +pub fn add_one_f64(arg: f64) -> f64 { + arg + 1.0 +} diff --git a/test_ffi/tests/integration_tests.rs b/test_ffi/tests/integration_tests.rs new file mode 100644 index 00000000000000..0f115fff1cb66a --- /dev/null +++ b/test_ffi/tests/integration_tests.rs @@ -0,0 +1,45 @@ +// Copyright 2018-2020 the Deno authors. All rights reserved. MIT license. +// To run this test manually: +// cargo test ffi_tests + +use std::{io::Read, process::Command}; +use test_util::deno_cmd; + +#[cfg(debug_assertions)] +const BUILD_VARIANT: &str = "debug"; + +#[cfg(not(debug_assertions))] +const BUILD_VARIANT: &str = "release"; + +#[test] +fn ffi_tests() { + let mut build_dylib_base = Command::new("cargo"); + let mut build_dylib = build_dylib_base.arg("build").arg("-p").arg("test_ffi"); + if BUILD_VARIANT == "release" { + build_dylib = build_dylib.arg("--release"); + } + let build_dylib_output = build_dylib.output().unwrap(); + assert!(build_dylib_output.status.success()); + let output = deno_cmd() + .arg("run") + .arg("--allow-all") + .arg("--unstable") + .arg("tests/test.js") + .arg(BUILD_VARIANT) + .env("NO_COLOR", "1") + .output() + .unwrap(); + let stdout = std::str::from_utf8(&output.stdout).unwrap(); + let stderr = std::str::from_utf8(&output.stderr).unwrap(); + if !output.status.success() { + println!("stdout {}", stdout); + println!("stderr {}", stderr); + } + assert!(output.status.success()); + let mut file = std::fs::File::open("tests/test.out").unwrap(); + let mut expected = String::new(); + file.read_to_string(&mut expected).unwrap(); + println!("{}", stdout); + assert_eq!(stdout, expected); + assert_eq!(stderr, ""); +} diff --git a/test_ffi/tests/test.js b/test_ffi/tests/test.js new file mode 100644 index 00000000000000..0c3ad1e4ac6196 --- /dev/null +++ b/test_ffi/tests/test.js @@ -0,0 +1,108 @@ +// Copyright 2018-2020 the Deno authors. All rights reserved. MIT license. + +const filenameBase = "test_ffi"; + +let filenameSuffix = ".so"; +let filenamePrefix = "lib"; + +if (Deno.build.os === "windows") { + filenameSuffix = ".dll"; + filenamePrefix = ""; +} +if (Deno.build.os === "darwin") { + filenameSuffix = ".dylib"; +} + +const filename = `../target/${Deno.args[0]}/${filenamePrefix}${filenameBase}${filenameSuffix}`; + +console.log("Deno.resources():", Deno.resources()); + +const lib = Deno.loadLibrary(filename); + +console.log("Deno.resources():", Deno.resources()); + +lib.call("print_something"); + +console.log( + "add_one_i8", + lib.call("add_one_i8", { + params: [{ typeName: "i8", value: 1 }], + returnType: "i8", + }) +); + +console.log( + "add_one_i16", + lib.call("add_one_i16", { + params: [{ typeName: "i16", value: 1 }], + returnType: "i16", + }) +); + +console.log( + "add_one_i32", + lib.call("add_one_i32", { + params: [{ typeName: "i32", value: 1 }], + returnType: "i32", + }) +); + +console.log( + "add_one_i64", + lib.call("add_one_i64", { + params: [{ typeName: "i64", value: 1 }], + returnType: "i64", + }) +); + +console.log( + "add_one_u8", + lib.call("add_one_u8", { + params: [{ typeName: "u8", value: 1 }], + returnType: "u8", + }) +); + +console.log( + "add_one_u16", + lib.call("add_one_u16", { + params: [{ typeName: "u16", value: 1 }], + returnType: "u16", + }) +); + +console.log( + "add_one_u32", + lib.call("add_one_u32", { + params: [{ typeName: "u32", value: 1 }], + returnType: "u32", + }) +); + +console.log( + "add_one_u64", + lib.call("add_one_i64", { + params: [{ typeName: "i64", value: 1 }], + returnType: "i64", + }) +); + +console.log( + "add_one_f32", + lib.call("add_one_f32", { + params: [{ typeName: "f32", value: 2.5 }], + returnType: "f32", + }) +); + +console.log( + "add_one_f64", + lib.call("add_one_f64", { + params: [{ typeName: "f64", value: 2.14 }], + returnType: "f64", + }) +); + +lib.close(); + +console.log("Deno.resources():", Deno.resources()); diff --git a/test_ffi/tests/test.out b/test_ffi/tests/test.out new file mode 100644 index 00000000000000..a362f23112afab --- /dev/null +++ b/test_ffi/tests/test.out @@ -0,0 +1,14 @@ +Deno.resources(): { "0": "stdin", "1": "stdout", "2": "stderr" } +Deno.resources(): { "0": "stdin", "1": "stdout", "2": "stderr", "3": "dylib" } +something +add_one_i8 2 +add_one_i16 2 +add_one_i32 2 +add_one_i64 2 +add_one_u8 2 +add_one_u16 9 +add_one_u32 2 +add_one_u64 2 +add_one_f32 3.5 +add_one_f64 3.14 +Deno.resources(): { "0": "stdin", "1": "stdout", "2": "stderr" }