diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index cb3a9e440ffcdd..f5f9b79ac50b61 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -137,6 +137,24 @@ jobs: service_account_key: ${{ secrets.GCP_SA_KEY }} export_default_credentials: true + - name: Setup GNU Tools (Linux) + uses: actions-rs/toolchain@v1 + if: startsWith(matrix.os, 'ubuntu') + with: + toolchain: stable + target: x86_64-unknown-linux-gnu + + - name: Setup GNU Tools (Windows) + uses: actions-rs/toolchain@v1 + if: startsWith(matrix.os, 'windows') + with: + toolchain: stable + target: x86_64-pc-windows-gnu + + - name: Setup GNU Tools (MacOS) + if: startsWith(matrix.os, 'macOS') + run: brew install autoconf automake libtool libffi + - name: Configure canary build if: | matrix.kind == 'test_release' && diff --git a/Cargo.lock b/Cargo.lock index ab09055d5c69ec..72f0d238bb305b 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" @@ -558,6 +564,7 @@ dependencies = [ "indexmap", "lazy_static", "libc", + "libffi", "log", "nix", "notify", @@ -1370,6 +1377,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 = "linked-hash-map" version = "0.5.4" @@ -1452,6 +1480,12 @@ dependencies = [ "syn 1.0.58", ] +[[package]] +name = "make-cmd" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a8ca8afbe8af1785e09636acb5a41e08a765f5f0340568716c18a8700ba3c0d3" + [[package]] name = "match_cfg" version = "0.1.0" @@ -2920,6 +2954,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 c73703368ac69d..bee699d79f4016 100644 --- a/cli/dts/lib.deno.unstable.d.ts +++ b/cli/dts/lib.deno.unstable.d.ts @@ -1341,6 +1341,58 @@ declare namespace Deno { * ``` */ export function sleepSync(millis: number): Promise; + + export type DyLibraryDataType = + | "i8" + | "i16" + | "i32" + | "i64" + | "u8" + | "u16" + | "u32" + | "u64" + | "f32" + | "f64" + | "cstr"; + + 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 1ee338733fdc5a..2e58636048e82f 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 bafa1a1b51870b..be80f9aabeb112 100644 --- a/runtime/js/90_deno_ns.js +++ b/runtime/js/90_deno_ns.js @@ -131,5 +131,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..f7630527dfdf5b --- /dev/null +++ b/runtime/ops/ffi.rs @@ -0,0 +1,250 @@ +// Copyright 2018-2020 the Deno authors. All rights reserved. MIT license. + +use std::{ + any::Any, + borrow::Cow, + ffi::{c_void, CString}, + path::PathBuf, + rc::Rc, +}; + +use deno_core::{ + error::AnyError, + serde_json::{self, json, Value}, + OpState, Resource, ZeroCopyBuf, +}; +use dlopen::symbor::Library; +use libc::c_char; +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())); + } + "u16" => { + let val = param.value.as_u64().unwrap() as u16; + 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())); + } + "cstr" => { + let val = param.value.as_str().unwrap(); + let cstr = val.as_ptr() as *const c_char; + call_args.push((Box::new(cstr), 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) + } + "cstr" => { + let v = val.downcast_ref::<*const c_char>().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()) }) + } + "cstr" => { + let val = unsafe { + let ptr = ffi::call::<*mut c_char>(fn_code_ptr, params.as_slice()); + CString::from_raw(ptr) + }; + json!(val.into_string().unwrap()) + } + _ => { + 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..5bca8c7df1470f 100644 --- a/runtime/ops/mod.rs +++ b/runtime/ops/mod.rs @@ -5,6 +5,7 @@ pub use dispatch_minimal::MinimalOp; pub mod crypto; pub mod fetch; +pub mod ffi; pub mod fs; pub mod fs_events; pub mod io; 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..968301dfbf7c62 --- /dev/null +++ b/test_ffi/src/lib.rs @@ -0,0 +1,67 @@ +use libc::c_char; +use std::ffi::{CStr, CString}; + +#[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 +} + +#[no_mangle] +/// # Safety +pub unsafe extern "C" fn concat_string(str: *const c_char) -> *const c_char { + let str = CStr::from_ptr(str); + let mut hello = String::from("hello "); + hello.push_str(str.to_str().unwrap()); + let str = CString::new(hello).unwrap(); + str.into_raw() +} 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..d28da34bb08f83 --- /dev/null +++ b/test_ffi/tests/test.js @@ -0,0 +1,118 @@ +// 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", + }), +); + +console.log( + "concat_string", + lib.call("concat_string", { + params: [{ typeName: "cstr", value: "Deno" }], + returnType: "cstr", + }), +); + +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..06ddaae1a844e5 --- /dev/null +++ b/test_ffi/tests/test.out @@ -0,0 +1,15 @@ +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 2 +add_one_u32 2 +add_one_u64 2 +add_one_f32 3.5 +add_one_f64 3.14 +concat_string hello Deno +Deno.resources(): { "0": "stdin", "1": "stdout", "2": "stderr" }