1
0
Fork 0
mirror of https://github.com/denoland/deno.git synced 2024-11-21 15:04:11 -05:00

feat: ffi to replace plugins (#11152)

This commit removes implementation of "native plugins" and replaces
it with FFI API.

Effectively "Deno.openPlugin" API was replaced with "Deno.dlopen" API.
This commit is contained in:
Elias Sjögreen 2021-08-06 23:28:10 +02:00 committed by GitHub
parent 0d1a522a03
commit 33c8d790c3
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
44 changed files with 860 additions and 511 deletions

View file

@ -164,9 +164,11 @@ jobs:
sudo apt-get update
sudo apt-get install debootstrap
# Note: git, nc, strace, and time, are needed to run the benchmarks.
# `file` and `make` are needed to build libffi-sys.
# `curl` is needed to build rusty_v8.
# `git`, `nc`, `strace`, and `time`, are needed to run the benchmarks.
sudo debootstrap \
--include=ca-certificates,curl,git,netcat-openbsd,strace,time \
--include=ca-certificates,curl,file,git,make,netcat-openbsd,strace,time \
--no-merged-usr --variant=minbase bionic /sysroot \
http://azure.archive.ubuntu.com/ubuntu
sudo mount --rbind /dev /sysroot/dev

52
Cargo.lock generated
View file

@ -12,6 +12,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 = "1.0.2"
@ -568,6 +574,7 @@ dependencies = [
"deno_crypto",
"deno_doc",
"deno_fetch",
"deno_ffi",
"deno_http",
"deno_lint",
"deno_net",
@ -629,6 +636,27 @@ dependencies = [
"winres",
]
[[package]]
name = "deno-libffi"
version = "0.0.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3a844ceea9e6233005c62dfe0bb7c1adab786ea78bab4ac1e5ea5cd2a5d47761"
dependencies = [
"abort_on_panic",
"deno-libffi-sys",
"libc",
]
[[package]]
name = "deno-libffi-sys"
version = "0.0.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "15a12b5205cc4f3944cefa192e851a0ee15c226b41733468b2660d4ab2bdf555"
dependencies = [
"cc",
"make-cmd",
]
[[package]]
name = "deno_bench_util"
version = "0.7.0"
@ -725,6 +753,16 @@ dependencies = [
"tokio-util",
]
[[package]]
name = "deno_ffi"
version = "0.1.0"
dependencies = [
"deno-libffi",
"deno_core",
"dlopen",
"serde",
]
[[package]]
name = "deno_http"
version = "0.4.0"
@ -786,6 +824,7 @@ dependencies = [
"deno_core",
"deno_crypto",
"deno_fetch",
"deno_ffi",
"deno_http",
"deno_net",
"deno_timers",
@ -2106,6 +2145,12 @@ dependencies = [
"syn 1.0.65",
]
[[package]]
name = "make-cmd"
version = "0.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a8ca8afbe8af1785e09636acb5a41e08a765f5f0340568716c18a8700ba3c0d3"
[[package]]
name = "malloc_buf"
version = "0.0.6"
@ -3831,12 +3876,9 @@ dependencies = [
]
[[package]]
name = "test_plugin"
version = "0.0.1"
name = "test_ffi"
version = "0.1.0"
dependencies = [
"deno_core",
"futures",
"serde",
"test_util",
]

View file

@ -6,12 +6,13 @@ members = [
"cli",
"core",
"runtime",
"test_plugin",
"test_ffi",
"test_util",
"extensions/broadcast_channel",
"extensions/console",
"extensions/crypto",
"extensions/fetch",
"extensions/ffi",
"extensions/http",
"extensions/net",
"extensions/timers",

View file

@ -25,6 +25,7 @@ deno_console = { version = "0.13.0", path = "../extensions/console" }
deno_core = { version = "0.95.0", path = "../core" }
deno_crypto = { version = "0.27.0", path = "../extensions/crypto" }
deno_fetch = { version = "0.36.0", path = "../extensions/fetch" }
deno_ffi = { version = "0.1.0", path = "../extensions/ffi" }
deno_http = { version = "0.4.0", path = "../extensions/http" }
deno_net = { version = "0.4.0", path = "../extensions/net" }
deno_timers = { version = "0.11.0", path = "../extensions/timers" }

View file

@ -54,7 +54,7 @@ const UNSTABLE_DENO_PROPS: &[&str] = &[
"listen",
"listenDatagram",
"loadavg",
"openPlugin",
"dlopen",
"osRelease",
"ppid",
"resolveDns",

View file

@ -2131,7 +2131,7 @@ declare namespace Deno {
| "write"
| "net"
| "env"
| "plugin"
| "ffi"
| "hrtime";
/** The current status of the permission. */
@ -2167,8 +2167,8 @@ declare namespace Deno {
variable?: string;
}
export interface PluginPermissionDescriptor {
name: "plugin";
export interface FFIPermissionDescriptor {
name: "ffi";
}
export interface HrtimePermissionDescriptor {
@ -2183,7 +2183,7 @@ declare namespace Deno {
| WritePermissionDescriptor
| NetPermissionDescriptor
| EnvPermissionDescriptor
| PluginPermissionDescriptor
| FFIPermissionDescriptor
| HrtimePermissionDescriptor;
export interface PermissionStatusEventMap {

View file

@ -107,36 +107,44 @@ declare namespace Deno {
swapFree: number;
}
/** **UNSTABLE**: new API, yet to be vetted.
/** All possible types for interfacing with foreign functions */
export type NativeType =
| "void"
| "u8"
| "i8"
| "u16"
| "i16"
| "u32"
| "i32"
| "u64"
| "i64"
| "usize"
| "isize"
| "f32"
| "f64";
/** A foreign function as defined by its parameter and result types */
export interface ForeignFunction {
parameters: NativeType[];
result: NativeType;
}
/** A dynamic library resource */
export interface DynamicLibrary<S extends Record<string, ForeignFunction>> {
/** All of the registered symbols along with functions for calling them */
symbols: { [K in keyof S]: (...args: unknown[]) => unknown };
close(): void;
}
/** **UNSTABLE**: new API
*
* Open and initialize a plugin.
*
* ```ts
* import { assert } from "https://deno.land/std/testing/asserts.ts";
* const rid = Deno.openPlugin("./path/to/some/plugin.so");
*
* // The Deno.core namespace is needed to interact with plugins, but this is
* // internal so we use ts-ignore to skip type checking these calls.
* // @ts-ignore
* const { op_test_sync, op_test_async } = Deno.core.ops();
*
* assert(op_test_sync);
* assert(op_test_async);
*
* // @ts-ignore
* const result = Deno.core.opSync("op_test_sync");
*
* // @ts-ignore
* const result = await Deno.core.opAsync("op_test_sync");
* ```
*
* Requires `allow-plugin` permission.
*
* The plugin system is not stable and will change in the future, hence the
* lack of docs. For now take a look at the example
* https://github.com/denoland/deno/tree/main/test_plugin
* Opens a dynamic library and registers symbols
*/
export function openPlugin(filename: string): number;
export function dlopen<S extends Record<string, ForeignFunction>>(
filename: string,
symbols: S,
): DynamicLibrary<S>;
/** The log category for a diagnostic message. */
export enum DiagnosticCategory {
@ -1043,14 +1051,14 @@ declare namespace Deno {
*/
net?: "inherit" | boolean | string[];
/** Specifies if the `plugin` permission should be requested or revoked.
* If set to `"inherit"`, the current `plugin` permission will be inherited.
* If set to `true`, the global `plugin` permission will be requested.
* If set to `false`, the global `plugin` permission will be revoked.
/** Specifies if the `ffi` permission should be requested or revoked.
* If set to `"inherit"`, the current `ffi` permission will be inherited.
* If set to `true`, the global `ffi` permission will be requested.
* If set to `false`, the global `ffi` permission will be revoked.
*
* Defaults to "inherit".
*/
plugin?: "inherit" | boolean;
ffi?: "inherit" | boolean;
/** Specifies if the `read` permission should be requested or revoked.
* If set to `"inherit"`, the current `read` permission will be inherited.
@ -1137,7 +1145,7 @@ declare interface WorkerOptions {
* For example: `["https://deno.land", "localhost:8080"]`.
*/
net?: "inherit" | boolean | string[];
plugin?: "inherit" | boolean;
ffi?: "inherit" | boolean;
read?: "inherit" | boolean | Array<string | URL>;
run?: "inherit" | boolean | Array<string | URL>;
write?: "inherit" | boolean | Array<string | URL>;

View file

@ -133,7 +133,7 @@ pub struct Flags {
pub allow_env: Option<Vec<String>>,
pub allow_hrtime: bool,
pub allow_net: Option<Vec<String>>,
pub allow_plugin: bool,
pub allow_ffi: Option<Vec<String>>,
pub allow_read: Option<Vec<PathBuf>>,
pub allow_run: Option<Vec<String>>,
pub allow_write: Option<Vec<PathBuf>>,
@ -235,8 +235,15 @@ impl Flags {
_ => {}
}
if self.allow_plugin {
args.push("--allow-plugin".to_string());
match &self.allow_ffi {
Some(ffi_allowlist) if ffi_allowlist.is_empty() => {
args.push("--allow-ffi".to_string());
}
Some(ffi_allowlist) => {
let s = format!("--allow-ffi={}", ffi_allowlist.join(","));
args.push(s);
}
_ => {}
}
if self.allow_hrtime {
@ -253,7 +260,7 @@ impl From<Flags> for PermissionsOptions {
allow_env: flags.allow_env,
allow_hrtime: flags.allow_hrtime,
allow_net: flags.allow_net,
allow_plugin: flags.allow_plugin,
allow_ffi: flags.allow_ffi,
allow_read: flags.allow_read,
allow_run: flags.allow_run,
allow_write: flags.allow_write,
@ -1228,9 +1235,13 @@ fn permission_args<'a, 'b>(app: App<'a, 'b>) -> App<'a, 'b> {
.help("Allow running subprocesses"),
)
.arg(
Arg::with_name("allow-plugin")
.long("allow-plugin")
.help("Allow loading plugins"),
Arg::with_name("allow-ffi")
.long("allow-ffi")
.min_values(0)
.takes_value(true)
.use_delimiter(true)
.require_equals(true)
.help("Allow loading dynamic libraries"),
)
.arg(
Arg::with_name("allow-hrtime")
@ -1577,7 +1588,7 @@ fn eval_parse(flags: &mut Flags, matches: &clap::ArgMatches) {
flags.allow_run = Some(vec![]);
flags.allow_read = Some(vec![]);
flags.allow_write = Some(vec![]);
flags.allow_plugin = true;
flags.allow_ffi = Some(vec![]);
flags.allow_hrtime = true;
// TODO(@satyarohith): remove this flag in 2.0.
let as_typescript = matches.is_present("ts");
@ -1696,7 +1707,7 @@ fn repl_parse(flags: &mut Flags, matches: &clap::ArgMatches) {
flags.allow_run = Some(vec![]);
flags.allow_read = Some(vec![]);
flags.allow_write = Some(vec![]);
flags.allow_plugin = true;
flags.allow_ffi = Some(vec![]);
flags.allow_hrtime = true;
}
@ -1876,9 +1887,12 @@ fn permission_args_parse(flags: &mut Flags, matches: &clap::ArgMatches) {
debug!("run allowlist: {:#?}", &flags.allow_run);
}
if matches.is_present("allow-plugin") {
flags.allow_plugin = true;
if let Some(ffi_wl) = matches.values_of("allow-ffi") {
let ffi_allowlist: Vec<String> = ffi_wl.map(ToString::to_string).collect();
flags.allow_ffi = Some(ffi_allowlist);
debug!("ffi allowlist: {:#?}", &flags.allow_ffi);
}
if matches.is_present("allow-hrtime") {
flags.allow_hrtime = true;
}
@ -1888,7 +1902,7 @@ fn permission_args_parse(flags: &mut Flags, matches: &clap::ArgMatches) {
flags.allow_net = Some(vec![]);
flags.allow_run = Some(vec![]);
flags.allow_write = Some(vec![]);
flags.allow_plugin = true;
flags.allow_ffi = Some(vec![]);
flags.allow_hrtime = true;
}
if matches.is_present("prompt") {
@ -2227,7 +2241,7 @@ mod tests {
allow_run: Some(vec![]),
allow_read: Some(vec![]),
allow_write: Some(vec![]),
allow_plugin: true,
allow_ffi: Some(vec![]),
allow_hrtime: true,
..Flags::default()
}
@ -2564,7 +2578,7 @@ mod tests {
allow_run: Some(vec![]),
allow_read: Some(vec![]),
allow_write: Some(vec![]),
allow_plugin: true,
allow_ffi: Some(vec![]),
allow_hrtime: true,
..Flags::default()
}
@ -2587,7 +2601,7 @@ mod tests {
allow_run: Some(vec![]),
allow_read: Some(vec![]),
allow_write: Some(vec![]),
allow_plugin: true,
allow_ffi: Some(vec![]),
allow_hrtime: true,
..Flags::default()
}
@ -2611,7 +2625,7 @@ mod tests {
allow_run: Some(vec![]),
allow_read: Some(vec![]),
allow_write: Some(vec![]),
allow_plugin: true,
allow_ffi: Some(vec![]),
allow_hrtime: true,
..Flags::default()
}
@ -2648,7 +2662,7 @@ mod tests {
allow_run: Some(vec![]),
allow_read: Some(vec![]),
allow_write: Some(vec![]),
allow_plugin: true,
allow_ffi: Some(vec![]),
allow_hrtime: true,
..Flags::default()
}
@ -2678,7 +2692,7 @@ mod tests {
allow_run: Some(vec![]),
allow_read: Some(vec![]),
allow_write: Some(vec![]),
allow_plugin: true,
allow_ffi: Some(vec![]),
allow_hrtime: true,
..Flags::default()
}
@ -2698,7 +2712,7 @@ mod tests {
allow_run: Some(vec![]),
allow_read: Some(vec![]),
allow_write: Some(vec![]),
allow_plugin: true,
allow_ffi: Some(vec![]),
allow_hrtime: true,
..Flags::default()
}
@ -2732,7 +2746,7 @@ mod tests {
allow_run: Some(vec![]),
allow_read: Some(vec![]),
allow_write: Some(vec![]),
allow_plugin: true,
allow_ffi: Some(vec![]),
allow_hrtime: true,
..Flags::default()
}

View file

@ -195,7 +195,7 @@ fn metadata_to_flags(metadata: &Metadata) -> Flags {
allow_env: permissions.allow_env,
allow_hrtime: permissions.allow_hrtime,
allow_net: permissions.allow_net,
allow_plugin: permissions.allow_plugin,
allow_ffi: permissions.allow_ffi,
allow_read: permissions.allow_read,
allow_run: permissions.allow_run,
allow_write: permissions.allow_write,

View file

@ -502,7 +502,7 @@ fn lsp_hover_unstable_disabled() {
"uri": "file:///a/file.ts",
"languageId": "typescript",
"version": 1,
"text": "console.log(Deno.openPlugin);\n"
"text": "console.log(Deno.dlopen);\n"
}
}),
);
@ -537,7 +537,7 @@ fn lsp_hover_unstable_disabled() {
},
"end": {
"line": 0,
"character": 27
"character": 23
}
}
}))
@ -555,7 +555,7 @@ fn lsp_hover_unstable_enabled() {
"uri": "file:///a/file.ts",
"languageId": "typescript",
"version": 1,
"text": "console.log(Deno.openPlugin);\n"
"text": "console.log(Deno.ppid);\n"
}
}),
);
@ -580,9 +580,9 @@ fn lsp_hover_unstable_enabled() {
"contents":[
{
"language":"typescript",
"value":"function Deno.openPlugin(filename: string): number"
"value":"const Deno.ppid: number"
},
"**UNSTABLE**: new API, yet to be vetted.\n\nOpen and initialize a plugin.\n\n```ts\nimport { assert } from \"https://deno.land/std/testing/asserts.ts\";\nconst rid = Deno.openPlugin(\"./path/to/some/plugin.so\");\n\n// The Deno.core namespace is needed to interact with plugins, but this is\n// internal so we use ts-ignore to skip type checking these calls.\n// @ts-ignore\nconst { op_test_sync, op_test_async } = Deno.core.ops();\n\nassert(op_test_sync);\nassert(op_test_async);\n\n// @ts-ignore\nconst result = Deno.core.opSync(\"op_test_sync\");\n\n// @ts-ignore\nconst result = await Deno.core.opAsync(\"op_test_sync\");\n```\n\nRequires `allow-plugin` permission.\n\nThe plugin system is not stable and will change in the future, hence the\nlack of docs. For now take a look at the example\nhttps://github.com/denoland/deno/tree/main/test_plugin"
"The pid of the current process's parent."
],
"range":{
"start":{
@ -591,7 +591,7 @@ fn lsp_hover_unstable_enabled() {
},
"end":{
"line":0,
"character":27
"character":21
}
}
}))

View file

@ -10,8 +10,8 @@ test env false ... ok [WILDCARD]
test env true ... ok [WILDCARD]
test run false ... ok [WILDCARD]
test run true ... ok [WILDCARD]
test plugin false ... ok [WILDCARD]
test plugin true ... ok [WILDCARD]
test ffi false ... ok [WILDCARD]
test ffi true ... ok [WILDCARD]
test hrtime false ... ok [WILDCARD]
test hrtime true ... ok [WILDCARD]

View file

@ -6,7 +6,7 @@ const permissions: Deno.PermissionName[] = [
"net",
"env",
"run",
"plugin",
"ffi",
"hrtime",
];

View file

@ -5,7 +5,7 @@ test write ... FAILED [WILDCARD]
test net ... FAILED [WILDCARD]
test env ... FAILED [WILDCARD]
test run ... FAILED [WILDCARD]
test plugin ... FAILED [WILDCARD]
test ffi ... FAILED [WILDCARD]
test hrtime ... FAILED [WILDCARD]
failures:
@ -30,7 +30,7 @@ run
PermissionDenied: Can't escalate parent thread permissions
[WILDCARD]
plugin
ffi
PermissionDenied: Can't escalate parent thread permissions
[WILDCARD]
@ -45,7 +45,7 @@ failures:
net
env
run
plugin
ffi
hrtime
test result: FAILED. 0 passed; 7 failed; 0 ignored; 0 measured; 0 filtered out [WILDCARD]

View file

@ -6,7 +6,7 @@ const permissions: Deno.PermissionName[] = [
"net",
"env",
"run",
"plugin",
"ffi",
"hrtime",
];

View file

@ -6,7 +6,7 @@ Deno.test({
net: true,
env: true,
run: true,
plugin: true,
ffi: true,
hrtime: true,
},
ignore: true,

View file

@ -32,7 +32,7 @@ interface UnitTestPermissions {
net?: boolean;
env?: boolean;
run?: boolean;
plugin?: boolean;
ffi?: boolean;
hrtime?: boolean;
}
@ -84,7 +84,7 @@ export function unitTest(
net: false,
env: false,
run: false,
plugin: false,
ffi: false,
hrtime: false,
}, options.perms),
};

View file

@ -1,14 +1,14 @@
self.onmessage = async () => {
const hrtime = await Deno.permissions.query({ name: "hrtime" });
const net = await Deno.permissions.query({ name: "net" });
const plugin = await Deno.permissions.query({ name: "plugin" });
const ffi = await Deno.permissions.query({ name: "ffi" });
const read = await Deno.permissions.query({ name: "read" });
const run = await Deno.permissions.query({ name: "run" });
const write = await Deno.permissions.query({ name: "write" });
self.postMessage(
hrtime.state === "denied" &&
net.state === "denied" &&
plugin.state === "denied" &&
ffi.state === "denied" &&
read.state === "denied" &&
run.state === "denied" &&
write.state === "denied",

View file

@ -201,7 +201,7 @@ pub fn compile_to_runtime_flags(
allow_env: flags.allow_env,
allow_hrtime: flags.allow_hrtime,
allow_net: flags.allow_net,
allow_plugin: flags.allow_plugin,
allow_ffi: flags.allow_ffi,
allow_read: flags.allow_read,
allow_run: flags.allow_run,
allow_write: flags.allow_write,

30
extensions/ffi/00_ffi.js Normal file
View file

@ -0,0 +1,30 @@
// Copyright 2018-2021 the Deno authors. All rights reserved. MIT license.
"use strict";
((window) => {
const core = window.Deno.core;
class DynamicLibrary {
#rid;
symbols = {};
constructor(path, symbols) {
this.#rid = core.opSync("op_ffi_load", { path, symbols });
for (const symbol in symbols) {
this.symbols[symbol] = (...parameters) =>
core.opSync("op_ffi_call", { rid: this.#rid, symbol, parameters });
}
}
close() {
core.close(this.#rid);
}
}
function dlopen(path, symbols) {
return new DynamicLibrary(path, symbols);
}
window.__bootstrap.ffi = { dlopen };
})(this);

20
extensions/ffi/Cargo.toml Normal file
View file

@ -0,0 +1,20 @@
# Copyright 2018-2021 the Deno authors. All rights reserved. MIT license.
[package]
name = "deno_ffi"
version = "0.1.0"
authors = ["the Deno authors"]
edition = "2018"
license = "MIT"
readme = "README.md"
repository = "https://github.com/denoland/deno"
description = "Dynamic library ffi for deno"
[lib]
path = "lib.rs"
[dependencies]
deno_core = { version = "0.95.0", path = "../../core" }
dlopen = "0.1.8"
libffi = { version = "0.0.3", package = "deno-libffi" }
serde = { version = "1.0.125", features = ["derive"] }

3
extensions/ffi/README.md Normal file
View file

@ -0,0 +1,3 @@
# deno_ffi
This crate implements dynamic library ffi.

397
extensions/ffi/lib.rs Normal file
View file

@ -0,0 +1,397 @@
// 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_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 dlopen::raw::Library;
use libffi::middle::Arg;
use serde::Deserialize;
use std::borrow::Cow;
use std::collections::HashMap;
use std::convert::TryFrom;
use std::ffi::c_void;
use std::rc::Rc;
pub struct Unstable(pub bool);
fn check_unstable(state: &OpState, api_name: &str) {
let unstable = state.borrow::<Unstable>();
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: &str) -> Result<(), AnyError>;
}
pub struct NoFfiPermissions;
impl FfiPermissions for NoFfiPermissions {
fn check(&mut self, _path: &str) -> Result<(), AnyError> {
Ok(())
}
}
struct Symbol {
cif: libffi::middle::Cif,
ptr: libffi::middle::CodePtr,
parameter_types: Vec<NativeType>,
result_type: NativeType,
}
struct DynamicLibraryResource {
lib: Library,
symbols: HashMap<String, Symbol>,
}
impl Resource for DynamicLibraryResource {
fn name(&self) -> Cow<str> {
"dynamicLibrary".into()
}
fn close(self: Rc<Self>) {
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 parameter_types =
foreign_fn.parameters.into_iter().map(NativeType::from);
let result_type = NativeType::from(foreign_fn.result);
let cif = libffi::middle::Cif::new(
parameter_types.clone().map(libffi::middle::Type::from),
result_type.into(),
);
self.symbols.insert(
symbol,
Symbol {
cif,
ptr,
parameter_types: parameter_types.collect(),
result_type,
},
);
Ok(())
}
}
pub fn init<P: FfiPermissions + 'static>(unstable: bool) -> Extension {
Extension::builder()
.js(include_js_files!(
prefix "deno:extensions/ffi",
"00_ffi.js",
))
.ops(vec![
("op_ffi_load", op_sync(op_ffi_load::<P>)),
("op_ffi_call", op_sync(op_ffi_call)),
])
.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,
}
impl From<NativeType> 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(),
}
}
}
impl From<String> for NativeType {
fn from(string: String) -> Self {
match string.as_str() {
"void" => NativeType::Void,
"u8" => NativeType::U8,
"i8" => NativeType::I8,
"u16" => NativeType::U16,
"i16" => NativeType::I16,
"u32" => NativeType::U32,
"i32" => NativeType::I32,
"u64" => NativeType::U64,
"i64" => NativeType::I64,
"usize" => NativeType::USize,
"isize" => NativeType::ISize,
"f32" => NativeType::F32,
"f64" => NativeType::F64,
_ => unimplemented!(),
}
}
}
#[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,
}
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::<u8>(value),
},
NativeType::I8 => Self {
i8_value: value_as_int::<i8>(value),
},
NativeType::U16 => Self {
u16_value: value_as_uint::<u16>(value),
},
NativeType::I16 => Self {
i16_value: value_as_int::<i16>(value),
},
NativeType::U32 => Self {
u32_value: value_as_uint::<u32>(value),
},
NativeType::I32 => Self {
i32_value: value_as_int::<i32>(value),
},
NativeType::U64 => Self {
u64_value: value_as_uint::<u64>(value),
},
NativeType::I64 => Self {
i64_value: value_as_int::<i64>(value),
},
NativeType::USize => Self {
usize_value: value_as_uint::<usize>(value),
},
NativeType::ISize => Self {
isize_value: value_as_int::<isize>(value),
},
NativeType::F32 => Self {
f32_value: value_as_f32(value),
},
NativeType::F64 => Self {
f64_value: value_as_f64(value),
},
}
}
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),
}
}
}
fn value_as_uint<T: TryFrom<u64>>(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<T: TryFrom<i64>>(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)]
struct ForeignFunction {
parameters: Vec<String>,
result: String,
}
#[derive(Deserialize, Debug)]
struct FfiLoadArgs {
path: String,
symbols: HashMap<String, ForeignFunction>,
}
fn op_ffi_load<FP>(
state: &mut deno_core::OpState,
args: FfiLoadArgs,
_: (),
) -> Result<ResourceId, AnyError>
where
FP: FfiPermissions + 'static,
{
check_unstable(state, "Deno.dlopen");
let permissions = state.borrow_mut::<FP>();
permissions.check(&args.path)?;
let lib = Library::open(args.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, Debug)]
#[serde(rename_all = "camelCase")]
struct FfiCallArgs {
rid: ResourceId,
symbol: String,
parameters: Vec<Value>,
}
fn op_ffi_call(
state: &mut deno_core::OpState,
args: FfiCallArgs,
_: (),
) -> Result<Value, AnyError> {
let resource = state
.resource_table
.get::<DynamicLibraryResource>(args.rid)
.ok_or_else(bad_resource_id)?;
let symbol = resource
.symbols
.get(&args.symbol)
.ok_or_else(bad_resource_id)?;
let native_values = symbol
.parameter_types
.iter()
.zip(args.parameters.into_iter())
.map(|(&native_type, value)| NativeValue::new(native_type, value))
.collect::<Vec<_>>();
let call_args = symbol
.parameter_types
.iter()
.zip(native_values.iter())
.map(|(&native_type, native_value)| unsafe {
native_value.as_arg(native_type)
})
.collect::<Vec<_>>();
Ok(match symbol.result_type {
NativeType::Void => {
json!(unsafe { symbol.cif.call::<()>(symbol.ptr, &call_args) })
}
NativeType::U8 => {
json!(unsafe { symbol.cif.call::<u8>(symbol.ptr, &call_args) })
}
NativeType::I8 => {
json!(unsafe { symbol.cif.call::<i8>(symbol.ptr, &call_args) })
}
NativeType::U16 => {
json!(unsafe { symbol.cif.call::<u16>(symbol.ptr, &call_args) })
}
NativeType::I16 => {
json!(unsafe { symbol.cif.call::<i16>(symbol.ptr, &call_args) })
}
NativeType::U32 => {
json!(unsafe { symbol.cif.call::<u32>(symbol.ptr, &call_args) })
}
NativeType::I32 => {
json!(unsafe { symbol.cif.call::<i32>(symbol.ptr, &call_args) })
}
NativeType::U64 => {
json!(unsafe { symbol.cif.call::<u64>(symbol.ptr, &call_args) })
}
NativeType::I64 => {
json!(unsafe { symbol.cif.call::<i64>(symbol.ptr, &call_args) })
}
NativeType::USize => {
json!(unsafe { symbol.cif.call::<usize>(symbol.ptr, &call_args) })
}
NativeType::ISize => {
json!(unsafe { symbol.cif.call::<isize>(symbol.ptr, &call_args) })
}
NativeType::F32 => {
json!(unsafe { symbol.cif.call::<f32>(symbol.ptr, &call_args) })
}
NativeType::F64 => {
json!(unsafe { symbol.cif.call::<f64>(symbol.ptr, &call_args) })
}
})
}

View file

@ -23,6 +23,7 @@ deno_console = { version = "0.13.0", path = "../extensions/console" }
deno_core = { version = "0.95.0", path = "../core" }
deno_crypto = { version = "0.27.0", path = "../extensions/crypto" }
deno_fetch = { version = "0.36.0", path = "../extensions/fetch" }
deno_ffi = { version = "0.1.0", path = "../extensions/ffi" }
deno_http = { version = "0.4.0", path = "../extensions/http" }
deno_net = { version = "0.4.0", path = "../extensions/net" }
deno_timers = { version = "0.11.0", path = "../extensions/timers" }
@ -43,6 +44,7 @@ deno_console = { version = "0.13.0", path = "../extensions/console" }
deno_core = { version = "0.95.0", path = "../core" }
deno_crypto = { version = "0.27.0", path = "../extensions/crypto" }
deno_fetch = { version = "0.36.0", path = "../extensions/fetch" }
deno_ffi = { version = "0.1.0", path = "../extensions/ffi" }
deno_http = { version = "0.4.0", path = "../extensions/http" }
deno_net = { version = "0.4.0", path = "../extensions/net" }
deno_timers = { version = "0.11.0", path = "../extensions/timers" }

View file

@ -60,6 +60,7 @@ fn create_runtime_snapshot(snapshot_path: &Path, files: Vec<PathBuf>) {
deno_broadcast_channel::InMemoryBroadcastChannel::default(),
false, // No --unstable.
),
deno_ffi::init::<deno_ffi::NoFfiPermissions>(false),
deno_net::init::<deno_net::NoNetPermissions>(None, false), // No --unstable.
deno_http::init(),
];

View file

@ -119,7 +119,7 @@
env = "inherit",
hrtime = "inherit",
net = "inherit",
plugin = "inherit",
ffi = "inherit",
read = "inherit",
run = "inherit",
write = "inherit",
@ -128,7 +128,7 @@
env: parseUnitPermission(env, "env"),
hrtime: parseUnitPermission(hrtime, "hrtime"),
net: parseArrayPermission(net, "net"),
plugin: parseUnitPermission(plugin, "plugin"),
ffi: parseUnitPermission(ffi, "ffi"),
read: parseArrayPermission(read, "read"),
run: parseUnitPermission(run, "run"),
write: parseArrayPermission(write, "write"),
@ -175,7 +175,7 @@
env: false,
hrtime: false,
net: false,
plugin: false,
ffi: false,
read: false,
run: false,
write: false,

View file

@ -28,14 +28,14 @@
* @property {PermissionStatus} status
*/
/** @type {ReadonlyArray<"read" | "write" | "net" | "env" | "run" | "plugin" | "hrtime">} */
/** @type {ReadonlyArray<"read" | "write" | "net" | "env" | "run" | "ffi" | "hrtime">} */
const permissionNames = [
"read",
"write",
"net",
"env",
"run",
"plugin",
"ffi",
"hrtime",
];

View file

@ -1,16 +0,0 @@
// Copyright 2018-2021 the Deno authors. All rights reserved. MIT license.
"use strict";
((window) => {
const core = window.Deno.core;
function openPlugin(filename) {
const rid = core.opSync("op_open_plugin", filename);
core.syncOpsCache();
return rid;
}
window.__bootstrap.plugins = {
openPlugin,
};
})(this);

View file

@ -110,7 +110,6 @@
Signal: __bootstrap.signals.Signal,
SignalStream: __bootstrap.signals.SignalStream,
emit: __bootstrap.compilerApi.emit,
openPlugin: __bootstrap.plugins.openPlugin,
kill: __bootstrap.process.kill,
setRaw: __bootstrap.tty.setRaw,
consoleSize: __bootstrap.tty.consoleSize,
@ -136,5 +135,6 @@
HttpClient: __bootstrap.fetch.HttpClient,
createHttpClient: __bootstrap.fetch.createHttpClient,
http: __bootstrap.http,
dlopen: __bootstrap.ffi.dlopen,
};
})(this);

View file

@ -4,6 +4,7 @@ pub use deno_broadcast_channel;
pub use deno_console;
pub use deno_crypto;
pub use deno_fetch;
pub use deno_ffi;
pub use deno_http;
pub use deno_net;
pub use deno_timers;

View file

@ -6,7 +6,6 @@ pub mod http;
pub mod io;
pub mod os;
pub mod permissions;
pub mod plugin;
pub mod process;
pub mod runtime;
pub mod signal;

View file

@ -28,6 +28,7 @@ pub struct PermissionArgs {
host: Option<String>,
variable: Option<String>,
command: Option<String>,
library: Option<String>,
}
pub fn op_query_permission(
@ -49,7 +50,7 @@ pub fn op_query_permission(
),
"env" => permissions.env.query(args.variable.as_deref()),
"run" => permissions.run.query(args.command.as_deref()),
"plugin" => permissions.plugin.query(),
"ffi" => permissions.ffi.query(args.library.as_deref()),
"hrtime" => permissions.hrtime.query(),
n => {
return Err(custom_error(
@ -80,7 +81,7 @@ pub fn op_revoke_permission(
),
"env" => permissions.env.revoke(args.variable.as_deref()),
"run" => permissions.run.revoke(args.command.as_deref()),
"plugin" => permissions.plugin.revoke(),
"ffi" => permissions.ffi.revoke(args.library.as_deref()),
"hrtime" => permissions.hrtime.revoke(),
n => {
return Err(custom_error(
@ -111,7 +112,7 @@ pub fn op_request_permission(
),
"env" => permissions.env.request(args.variable.as_deref()),
"run" => permissions.run.request(args.command.as_deref()),
"plugin" => permissions.plugin.request(),
"ffi" => permissions.ffi.request(args.library.as_deref()),
"hrtime" => permissions.hrtime.request(),
n => {
return Err(custom_error(

View file

@ -1,86 +0,0 @@
// Copyright 2018-2021 the Deno authors. All rights reserved. MIT license.
use crate::permissions::Permissions;
use deno_core::error::AnyError;
use deno_core::op_sync;
use deno_core::Extension;
use deno_core::OpState;
use deno_core::Resource;
use deno_core::ResourceId;
use dlopen::symbor::Library;
use log::debug;
use std::borrow::Cow;
use std::mem;
use std::path::PathBuf;
use std::rc::Rc;
/// A default `init` function for plugins which mimics the way the internal
/// extensions are initalized. Plugins currently do not support all extension
/// features and are most likely not going to in the future. Currently only
/// `init_state` and `init_ops` are supported while `init_middleware` and `init_js`
/// are not. Currently the `PluginResource` does not support being closed due to
/// certain risks in unloading the dynamic library without unloading dependent
/// functions and resources.
pub type InitFn = fn() -> Extension;
pub fn init() -> Extension {
Extension::builder()
.ops(vec![("op_open_plugin", op_sync(op_open_plugin))])
.build()
}
pub fn op_open_plugin(
state: &mut OpState,
filename: String,
_: (),
) -> Result<ResourceId, AnyError> {
let filename = PathBuf::from(&filename);
super::check_unstable(state, "Deno.openPlugin");
let permissions = state.borrow_mut::<Permissions>();
permissions.plugin.check()?;
debug!("Loading Plugin: {:#?}", filename);
let plugin_lib = Library::open(filename).map(Rc::new)?;
let plugin_resource = PluginResource::new(&plugin_lib);
// Forgets the plugin_lib value to prevent segfaults when the process exits
mem::forget(plugin_lib);
let init = *unsafe { plugin_resource.0.symbol::<InitFn>("init") }?;
let rid = state.resource_table.add(plugin_resource);
let mut extension = init();
if !extension.init_js().is_empty() {
panic!("Plugins do not support loading js");
}
if extension.init_middleware().is_some() {
panic!("Plugins do not support middleware");
}
extension.init_state(state)?;
let ops = extension.init_ops().unwrap_or_default();
for (name, opfn) in ops {
state.op_table.register_op(name, opfn);
}
Ok(rid)
}
struct PluginResource(Rc<Library>);
impl Resource for PluginResource {
fn name(&self) -> Cow<str> {
"plugin".into()
}
fn close(self: Rc<Self>) {
unimplemented!();
}
}
impl PluginResource {
fn new(lib: &Rc<Library>) -> Self {
Self(lib.clone())
}
}

View file

@ -3,6 +3,7 @@
use crate::permissions::resolve_read_allowlist;
use crate::permissions::resolve_write_allowlist;
use crate::permissions::EnvDescriptor;
use crate::permissions::FfiDescriptor;
use crate::permissions::NetDescriptor;
use crate::permissions::PermissionState;
use crate::permissions::Permissions;
@ -218,6 +219,26 @@ fn merge_run_permission(
Ok(main)
}
fn merge_ffi_permission(
mut main: UnaryPermission<FfiDescriptor>,
worker: Option<UnaryPermission<FfiDescriptor>>,
) -> Result<UnaryPermission<FfiDescriptor>, AnyError> {
if let Some(worker) = worker {
if (worker.global_state < main.global_state)
|| !worker.granted_list.iter().all(|x| main.check(&x.0).is_ok())
{
return Err(custom_error(
"PermissionDenied",
"Can't escalate parent thread permissions",
));
} else {
main.global_state = worker.global_state;
main.granted_list = worker.granted_list;
}
}
Ok(main)
}
pub fn create_worker_permissions(
main_perms: Permissions,
worker_perms: PermissionsArg,
@ -226,7 +247,7 @@ pub fn create_worker_permissions(
env: merge_env_permission(main_perms.env, worker_perms.env)?,
hrtime: merge_boolean_permission(main_perms.hrtime, worker_perms.hrtime)?,
net: merge_net_permission(main_perms.net, worker_perms.net)?,
plugin: merge_boolean_permission(main_perms.plugin, worker_perms.plugin)?,
ffi: merge_ffi_permission(main_perms.ffi, worker_perms.ffi)?,
read: merge_read_permission(main_perms.read, worker_perms.read)?,
run: merge_run_permission(main_perms.run, worker_perms.run)?,
write: merge_write_permission(main_perms.write, worker_perms.write)?,
@ -241,8 +262,8 @@ pub struct PermissionsArg {
hrtime: Option<PermissionState>,
#[serde(default, deserialize_with = "as_unary_net_permission")]
net: Option<UnaryPermission<NetDescriptor>>,
#[serde(default, deserialize_with = "as_permission_state")]
plugin: Option<PermissionState>,
#[serde(default, deserialize_with = "as_unary_ffi_permission")]
ffi: Option<UnaryPermission<FfiDescriptor>>,
#[serde(default, deserialize_with = "as_unary_read_permission")]
read: Option<UnaryPermission<ReadDescriptor>>,
#[serde(default, deserialize_with = "as_unary_run_permission")]
@ -414,6 +435,22 @@ where
}))
}
fn as_unary_ffi_permission<'de, D>(
deserializer: D,
) -> Result<Option<UnaryPermission<FfiDescriptor>>, D::Error>
where
D: Deserializer<'de>,
{
let value: UnaryPermissionBase =
deserializer.deserialize_any(ParseBooleanOrStringVec)?;
Ok(Some(UnaryPermission::<FfiDescriptor> {
global_state: value.global_state,
granted_list: value.paths.into_iter().map(FfiDescriptor).collect(),
..Default::default()
}))
}
#[derive(Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct CreateWorkerArgs {

View file

@ -202,6 +202,9 @@ pub struct EnvDescriptor(pub String);
#[derive(Clone, Eq, PartialEq, Hash, Debug, Default, Deserialize)]
pub struct RunDescriptor(pub String);
#[derive(Clone, Eq, PartialEq, Hash, Debug, Default, Deserialize)]
pub struct FfiDescriptor(pub String);
impl UnaryPermission<ReadDescriptor> {
pub fn query(&self, path: Option<&Path>) -> PermissionState {
let path = path.map(|p| resolve_from_cwd(p).unwrap());
@ -787,6 +790,104 @@ impl UnaryPermission<RunDescriptor> {
}
}
impl UnaryPermission<FfiDescriptor> {
pub fn query(&self, lib: Option<&str>) -> PermissionState {
if self.global_state == PermissionState::Denied
&& match lib {
None => true,
Some(lib) => self.denied_list.iter().any(|lib_| lib_.0 == lib),
}
{
PermissionState::Denied
} else if self.global_state == PermissionState::Granted
|| match lib {
None => false,
Some(lib) => self.granted_list.iter().any(|lib_| lib_.0 == lib),
}
{
PermissionState::Granted
} else {
PermissionState::Prompt
}
}
pub fn request(&mut self, lib: Option<&str>) -> PermissionState {
if let Some(lib) = lib {
let state = self.query(Some(lib));
if state == PermissionState::Prompt {
if permission_prompt(&format!("ffi access to \"{}\"", lib)) {
self.granted_list.retain(|lib_| lib_.0 != lib);
self.granted_list.insert(FfiDescriptor(lib.to_string()));
PermissionState::Granted
} else {
self.denied_list.retain(|lib_| lib_.0 != lib);
self.denied_list.insert(FfiDescriptor(lib.to_string()));
self.global_state = PermissionState::Denied;
PermissionState::Denied
}
} else {
state
}
} else {
let state = self.query(None);
if state == PermissionState::Prompt {
if permission_prompt("ffi access") {
self.granted_list.clear();
self.global_state = PermissionState::Granted;
PermissionState::Granted
} else {
self.global_state = PermissionState::Denied;
PermissionState::Denied
}
} else {
state
}
}
}
pub fn revoke(&mut self, lib: Option<&str>) -> PermissionState {
if let Some(lib) = lib {
self.granted_list.retain(|lib_| lib_.0 != lib);
} else {
self.granted_list.clear();
if self.global_state == PermissionState::Granted {
self.global_state = PermissionState::Prompt;
}
}
self.query(lib)
}
pub fn check(&mut self, lib: &str) -> Result<(), AnyError> {
let (result, prompted) = self.query(Some(lib)).check(
self.name,
Some(&format!("\"{}\"", lib)),
self.prompt,
);
if prompted {
if result.is_ok() {
self.granted_list.insert(FfiDescriptor(lib.to_string()));
} else {
self.denied_list.insert(FfiDescriptor(lib.to_string()));
self.global_state = PermissionState::Denied;
}
}
result
}
pub fn check_all(&mut self) -> Result<(), AnyError> {
let (result, prompted) =
self.query(None).check(self.name, Some("all"), self.prompt);
if prompted {
if result.is_ok() {
self.global_state = PermissionState::Granted;
} else {
self.global_state = PermissionState::Denied;
}
}
result
}
}
#[derive(Clone, Debug, Default, PartialEq)]
pub struct Permissions {
pub read: UnaryPermission<ReadDescriptor>,
@ -794,7 +895,7 @@ pub struct Permissions {
pub net: UnaryPermission<NetDescriptor>,
pub env: UnaryPermission<EnvDescriptor>,
pub run: UnaryPermission<RunDescriptor>,
pub plugin: UnitPermission,
pub ffi: UnaryPermission<FfiDescriptor>,
pub hrtime: UnitPermission,
}
@ -803,7 +904,7 @@ pub struct PermissionsOptions {
pub allow_env: Option<Vec<String>>,
pub allow_hrtime: bool,
pub allow_net: Option<Vec<String>>,
pub allow_plugin: bool,
pub allow_ffi: Option<Vec<String>>,
pub allow_read: Option<Vec<PathBuf>>,
pub allow_run: Option<Vec<String>>,
pub allow_write: Option<Vec<PathBuf>>,
@ -904,8 +1005,21 @@ impl Permissions {
}
}
pub fn new_plugin(state: bool, prompt: bool) -> UnitPermission {
unit_permission_from_flag_bool(state, "plugin", "open a plugin", prompt)
pub fn new_ffi(
state: &Option<Vec<String>>,
prompt: bool,
) -> UnaryPermission<FfiDescriptor> {
UnaryPermission::<FfiDescriptor> {
name: "ffi",
description: "load a dynamic library",
global_state: global_state_from_option(state),
granted_list: state
.as_ref()
.map(|v| v.iter().map(|x| FfiDescriptor(x.clone())).collect())
.unwrap_or_else(HashSet::new),
denied_list: Default::default(),
prompt,
}
}
pub fn new_hrtime(state: bool, prompt: bool) -> UnitPermission {
@ -924,7 +1038,7 @@ impl Permissions {
net: Permissions::new_net(&opts.allow_net, opts.prompt),
env: Permissions::new_env(&opts.allow_env, opts.prompt),
run: Permissions::new_run(&opts.allow_run, opts.prompt),
plugin: Permissions::new_plugin(opts.allow_plugin, opts.prompt),
ffi: Permissions::new_ffi(&opts.allow_ffi, opts.prompt),
hrtime: Permissions::new_hrtime(opts.allow_hrtime, opts.prompt),
}
}
@ -936,7 +1050,7 @@ impl Permissions {
net: Permissions::new_net(&Some(vec![]), false),
env: Permissions::new_env(&Some(vec![]), false),
run: Permissions::new_run(&Some(vec![]), false),
plugin: Permissions::new_plugin(true, false),
ffi: Permissions::new_ffi(&Some(vec![]), false),
hrtime: Permissions::new_hrtime(true, false),
}
}
@ -1005,6 +1119,12 @@ impl deno_websocket::WebSocketPermissions for Permissions {
}
}
impl deno_ffi::FfiPermissions for Permissions {
fn check(&mut self, path: &str) -> Result<(), AnyError> {
self.ffi.check(path)
}
}
fn unit_permission_from_flag_bool(
flag: bool,
name: &'static str,
@ -1457,9 +1577,9 @@ mod tests {
global_state: PermissionState::Prompt,
..Permissions::new_run(&Some(svec!["deno"]), false)
},
plugin: UnitPermission {
state: PermissionState::Prompt,
..Default::default()
ffi: UnaryPermission {
global_state: PermissionState::Prompt,
..Permissions::new_ffi(&Some(svec!["deno"]), false)
},
hrtime: UnitPermission {
state: PermissionState::Prompt,
@ -1490,8 +1610,10 @@ mod tests {
assert_eq!(perms1.run.query(Some(&"deno".to_string())), PermissionState::Granted);
assert_eq!(perms2.run.query(None), PermissionState::Prompt);
assert_eq!(perms2.run.query(Some(&"deno".to_string())), PermissionState::Granted);
assert_eq!(perms1.plugin.query(), PermissionState::Granted);
assert_eq!(perms2.plugin.query(), PermissionState::Prompt);
assert_eq!(perms1.ffi.query(None), PermissionState::Granted);
assert_eq!(perms1.ffi.query(Some(&"deno".to_string())), PermissionState::Granted);
assert_eq!(perms2.ffi.query(None), PermissionState::Prompt);
assert_eq!(perms2.ffi.query(Some(&"deno".to_string())), PermissionState::Granted);
assert_eq!(perms1.hrtime.query(), PermissionState::Granted);
assert_eq!(perms2.hrtime.query(), PermissionState::Prompt);
};
@ -1528,9 +1650,10 @@ mod tests {
set_prompt_result(false);
assert_eq!(perms.run.request(Some(&"deno".to_string())), PermissionState::Granted);
set_prompt_result(true);
assert_eq!(perms.plugin.request(), PermissionState::Granted);
assert_eq!(perms.ffi.request(Some(&"deno".to_string())), PermissionState::Granted);
assert_eq!(perms.ffi.query(None), PermissionState::Prompt);
set_prompt_result(false);
assert_eq!(perms.plugin.request(), PermissionState::Granted);
assert_eq!(perms.ffi.request(Some(&"deno".to_string())), PermissionState::Granted);
set_prompt_result(false);
assert_eq!(perms.hrtime.request(), PermissionState::Denied);
set_prompt_result(true);
@ -1561,9 +1684,9 @@ mod tests {
global_state: PermissionState::Prompt,
..Permissions::new_run(&Some(svec!["deno"]), false)
},
plugin: UnitPermission {
state: PermissionState::Prompt,
..Default::default()
ffi: UnaryPermission {
global_state: PermissionState::Prompt,
..Permissions::new_ffi(&Some(svec!["deno"]), false)
},
hrtime: UnitPermission {
state: PermissionState::Denied,
@ -1582,7 +1705,7 @@ mod tests {
assert_eq!(perms.net.revoke(Some(&("127.0.0.1", None))), PermissionState::Prompt);
assert_eq!(perms.env.revoke(Some(&"HOME".to_string())), PermissionState::Prompt);
assert_eq!(perms.run.revoke(Some(&"deno".to_string())), PermissionState::Prompt);
assert_eq!(perms.plugin.revoke(), PermissionState::Prompt);
assert_eq!(perms.ffi.revoke(Some(&"deno".to_string())), PermissionState::Prompt);
assert_eq!(perms.hrtime.revoke(), PermissionState::Denied);
};
}
@ -1595,7 +1718,7 @@ mod tests {
net: Permissions::new_net(&None, true),
env: Permissions::new_env(&None, true),
run: Permissions::new_run(&None, true),
plugin: Permissions::new_plugin(false, true),
ffi: Permissions::new_ffi(&None, true),
hrtime: Permissions::new_hrtime(false, true),
};
@ -1648,7 +1771,7 @@ mod tests {
net: Permissions::new_net(&None, true),
env: Permissions::new_env(&None, true),
run: Permissions::new_run(&None, true),
plugin: Permissions::new_plugin(false, true),
ffi: Permissions::new_ffi(&None, true),
hrtime: Permissions::new_hrtime(false, true),
};

View file

@ -315,6 +315,8 @@ impl WebWorker {
deno_crypto::init(options.seed),
deno_webgpu::init(options.unstable),
deno_timers::init::<Permissions>(),
// ffi
deno_ffi::init::<Permissions>(options.unstable),
// Metrics
metrics::init(),
// Permissions ext (worker specific state)
@ -340,7 +342,6 @@ impl WebWorker {
),
ops::os::init(),
ops::permissions::init(),
ops::plugin::init(),
ops::process::init(),
ops::signal::init(),
ops::tty::init(),

View file

@ -115,6 +115,8 @@ impl MainWorker {
),
deno_webgpu::init(options.unstable),
deno_timers::init::<Permissions>(),
// ffi
deno_ffi::init::<Permissions>(options.unstable),
// Metrics
metrics::init(),
// Runtime ops
@ -127,7 +129,6 @@ impl MainWorker {
deno_net::init::<Permissions>(options.ca_data.clone(), options.unstable),
ops::os::init(),
ops::permissions::init(),
ops::plugin::init(),
ops::process::init(),
ops::signal::init(),
ops::tty::init(),

View file

@ -1,8 +1,8 @@
# Copyright 2018-2021 the Deno authors. All rights reserved. MIT license.
[package]
name = "test_plugin"
version = "0.0.1"
name = "test_ffi"
version = "0.1.0"
authors = ["the deno authors"]
edition = "2018"
publish = false
@ -10,10 +10,5 @@ publish = false
[lib]
crate-type = ["cdylib"]
[dependencies]
deno_core = { path = "../core" }
futures = "0.3.15"
serde = "1"
[dev-dependencies]
test_util = { path = "../test_util" }

1
test_ffi/README.md Normal file
View file

@ -0,0 +1 @@
# `test_ffi` crate

9
test_ffi/src/lib.rs Normal file
View file

@ -0,0 +1,9 @@
#[no_mangle]
pub extern "C" fn print_something() {
println!("something");
}
#[no_mangle]
pub extern "C" fn add(a: u32, b: u32) -> u32 {
a + b
}

View file

@ -13,7 +13,7 @@ const BUILD_VARIANT: &str = "release";
fn basic() {
let mut build_plugin_base = Command::new("cargo");
let mut build_plugin =
build_plugin_base.arg("build").arg("-p").arg("test_plugin");
build_plugin_base.arg("build").arg("-p").arg("test_ffi");
if BUILD_VARIANT == "release" {
build_plugin = build_plugin.arg("--release");
}
@ -21,10 +21,11 @@ fn basic() {
assert!(build_plugin_output.status.success());
let output = deno_cmd()
.arg("run")
.arg("--allow-plugin")
.arg("--allow-ffi")
.arg("--allow-read")
.arg("--unstable")
.arg("tests/test.js")
.arg(BUILD_VARIANT)
.env("NO_COLOR", "1")
.output()
.unwrap();
let stdout = std::str::from_utf8(&output.stdout).unwrap();
@ -36,23 +37,9 @@ fn basic() {
println!("{:?}", output.status);
assert!(output.status.success());
let expected = "\
Plugin rid: 3\n\
Hello from sync plugin op.\n\
args: TestArgs { val: \"1\" }\n\
zero_copy: test\n\
op_test_sync returned: test\n\
Hello from async plugin op.\n\
args: TestArgs { val: \"1\" }\n\
zero_copy: 123\n\
op_test_async returned: test\n\
Hello from resource_table.add plugin op.\n\
TestResource rid: 4\n\
Hello from resource_table.get plugin op.\n\
TestResource get value: hello plugin!\n\
Hello from sync plugin op.\n\
args: TestArgs { val: \"1\" }\n\
Ops completed count is correct!\n\
Ops dispatched count is correct!\n";
something\n\
579\n\
Correct number of resources\n";
assert_eq!(stdout, expected);
assert_eq!(stderr, "");
}

33
test_ffi/tests/test.js Normal file
View file

@ -0,0 +1,33 @@
// Copyright 2018-2021 the Deno authors. All rights reserved. MIT license.
// deno-lint-ignore-file
const targetDir = Deno.execPath().replace(/[^\/\\]+$/, "");
const [libPrefix, libSuffix] = {
darwin: ["lib", "dylib"],
linux: ["lib", "so"],
windows: ["", "dll"],
}[Deno.build.os];
const libPath = `${targetDir}/${libPrefix}test_ffi.${libSuffix}`;
const resourcesPre = Deno.resources();
const dylib = Deno.dlopen(libPath, {
"print_something": { parameters: [], result: "void" },
"add": { parameters: ["u32", "u32"], result: "u32" },
});
dylib.symbols.print_something();
console.log(dylib.symbols.add(123, 456));
dylib.close();
const resourcesPost = Deno.resources();
const preStr = JSON.stringify(resourcesPre, null, 2);
const postStr = JSON.stringify(resourcesPost, null, 2);
if (preStr !== postStr) {
throw new Error(
`Difference in open resources before dlopen and after closing:
Before: ${preStr}
After: ${postStr}`,
);
}
console.log("Correct number of resources");

View file

@ -1,9 +0,0 @@
# `test_plugin` crate
## To run this test manually
```
cd test_plugin
../target/debug/deno run --unstable --allow-plugin tests/test.js debug
```

View file

@ -1,114 +0,0 @@
// Copyright 2018-2021 the Deno authors. All rights reserved. MIT license.
use std::borrow::Cow;
use std::cell::RefCell;
use std::rc::Rc;
use deno_core::error::bad_resource_id;
use deno_core::error::AnyError;
use deno_core::op_async;
use deno_core::op_sync;
use deno_core::Extension;
use deno_core::OpState;
use deno_core::Resource;
use deno_core::ResourceId;
use deno_core::ZeroCopyBuf;
use serde::Deserialize;
#[no_mangle]
pub fn init() -> Extension {
Extension::builder()
.ops(vec![
("op_test_sync", op_sync(op_test_sync)),
("op_test_async", op_async(op_test_async)),
(
"op_test_resource_table_add",
op_sync(op_test_resource_table_add),
),
(
"op_test_resource_table_get",
op_sync(op_test_resource_table_get),
),
])
.build()
}
#[derive(Debug, Deserialize)]
struct TestArgs {
val: String,
}
fn op_test_sync(
_state: &mut OpState,
args: TestArgs,
zero_copy: Option<ZeroCopyBuf>,
) -> Result<String, AnyError> {
println!("Hello from sync plugin op.");
println!("args: {:?}", args);
if let Some(buf) = zero_copy {
let buf_str = std::str::from_utf8(&buf[..])?;
println!("zero_copy: {}", buf_str);
}
Ok("test".to_string())
}
async fn op_test_async(
_state: Rc<RefCell<OpState>>,
args: TestArgs,
zero_copy: Option<ZeroCopyBuf>,
) -> Result<String, AnyError> {
println!("Hello from async plugin op.");
println!("args: {:?}", args);
if let Some(buf) = zero_copy {
let buf_str = std::str::from_utf8(&buf[..])?;
println!("zero_copy: {}", buf_str);
}
let (tx, rx) = futures::channel::oneshot::channel::<Result<(), ()>>();
std::thread::spawn(move || {
std::thread::sleep(std::time::Duration::from_secs(1));
tx.send(Ok(())).unwrap();
});
assert!(rx.await.is_ok());
Ok("test".to_string())
}
struct TestResource(String);
impl Resource for TestResource {
fn name(&self) -> Cow<str> {
"TestResource".into()
}
}
fn op_test_resource_table_add(
state: &mut OpState,
text: String,
_: (),
) -> Result<u32, AnyError> {
println!("Hello from resource_table.add plugin op.");
Ok(state.resource_table.add(TestResource(text)))
}
fn op_test_resource_table_get(
state: &mut OpState,
rid: ResourceId,
_: (),
) -> Result<String, AnyError> {
println!("Hello from resource_table.get plugin op.");
Ok(
state
.resource_table
.get::<TestResource>(rid)
.ok_or_else(bad_resource_id)?
.0
.clone(),
)
}

View file

@ -1,135 +0,0 @@
// Copyright 2018-2021 the Deno authors. All rights reserved. MIT license.
// deno-lint-ignore-file
const filenameBase = "test_plugin";
let filenameSuffix = ".so";
let filenamePrefix = "lib";
if (Deno.build.os === "windows") {
filenameSuffix = ".dll";
filenamePrefix = "";
} else if (Deno.build.os === "darwin") {
filenameSuffix = ".dylib";
}
const filename = `../target/${
Deno.args[0]
}/${filenamePrefix}${filenameBase}${filenameSuffix}`;
const resourcesPre = Deno.resources();
const pluginRid = Deno.openPlugin(filename);
console.log(`Plugin rid: ${pluginRid}`);
const {
op_test_sync,
op_test_async,
op_test_resource_table_add,
op_test_resource_table_get,
} = Deno.core.ops();
if (
op_test_sync === null ||
op_test_async === null ||
op_test_resource_table_add === null ||
op_test_resource_table_get === null
) {
throw new Error("Not all expected ops were registered");
}
function runTestSync() {
const result = Deno.core.opSync(
"op_test_sync",
{ val: "1" },
new Uint8Array([116, 101, 115, 116]),
);
console.log(`op_test_sync returned: ${result}`);
if (result !== "test") {
throw new Error("op_test_sync returned an unexpected value!");
}
}
async function runTestAsync() {
const promise = Deno.core.opAsync(
"op_test_async",
{ val: "1" },
new Uint8Array([49, 50, 51]),
);
if (!(promise instanceof Promise)) {
throw new Error("Expected promise!");
}
const result = await promise;
console.log(`op_test_async returned: ${result}`);
if (result !== "test") {
throw new Error("op_test_async promise resolved to an unexpected value!");
}
}
function runTestResourceTable() {
const expect = "hello plugin!";
const testRid = Deno.core.opSync("op_test_resource_table_add", expect);
console.log(`TestResource rid: ${testRid}`);
if (testRid === null || Deno.resources()[testRid] !== "TestResource") {
throw new Error("TestResource was not found!");
}
const testValue = Deno.core.opSync("op_test_resource_table_get", testRid);
console.log(`TestResource get value: ${testValue}`);
if (testValue !== expect) {
throw new Error("Did not get correct resource value!");
}
Deno.close(testRid);
}
function runTestOpCount() {
const start = Deno.metrics();
Deno.core.opSync("op_test_sync", { val: "1" });
const end = Deno.metrics();
if (end.opsCompleted - start.opsCompleted !== 1) {
throw new Error("The opsCompleted metric is not correct!");
}
console.log("Ops completed count is correct!");
if (end.opsDispatched - start.opsDispatched !== 1) {
throw new Error("The opsDispatched metric is not correct!");
}
console.log("Ops dispatched count is correct!");
}
function runTestPluginClose() {
// Closing does not yet work
Deno.close(pluginRid);
const resourcesPost = Deno.resources();
const preStr = JSON.stringify(resourcesPre, null, 2);
const postStr = JSON.stringify(resourcesPost, null, 2);
if (preStr !== postStr) {
throw new Error(
`Difference in open resources before openPlugin and after Plugin.close():
Before: ${preStr}
After: ${postStr}`,
);
}
console.log("Correct number of resources");
}
runTestSync();
await runTestAsync();
runTestResourceTable();
runTestOpCount();
// runTestPluginClose();