feat(ext/ffi): Implement FFI fast-call trampoline with Dynasmrt (#15305)

This commit is contained in:
Arnau Orriols 2022-09-07 08:53:56 +02:00 committed by GitHub
parent 5819fef2d5
commit 8bdc3c2baf
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
15 changed files with 2282 additions and 601 deletions

4
.gitmodules vendored
View File

@ -9,6 +9,4 @@
[submodule "test_util/wpt"]
path = test_util/wpt
url = https://github.com/web-platform-tests/wpt.git
[submodule "ext/ffi/tinycc"]
path = ext/ffi/tinycc
url = https://github.com/TinyCC/tinycc

37
Cargo.lock generated
View File

@ -1035,6 +1035,7 @@ version = "0.54.0"
dependencies = [
"deno_core",
"dlopen",
"dynasmrt",
"libffi",
"serde",
"tokio",
@ -1467,6 +1468,32 @@ version = "1.0.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "21e50f3adc76d6a43f5ed73b698a87d0760ca74617f60f7c3b879003536fdd28"
[[package]]
name = "dynasm"
version = "1.2.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "add9a102807b524ec050363f09e06f1504214b0e1c7797f64261c891022dce8b"
dependencies = [
"bitflags",
"byteorder",
"lazy_static",
"proc-macro-error",
"proc-macro2 1.0.39",
"quote 1.0.18",
"syn 1.0.96",
]
[[package]]
name = "dynasmrt"
version = "1.2.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "64fba5a42bd76a17cad4bfa00de168ee1cbfa06a5e8ce992ae880218c05641a9"
dependencies = [
"byteorder",
"dynasm",
"memmap2",
]
[[package]]
name = "ecdsa"
version = "0.14.1"
@ -2657,6 +2684,15 @@ version = "2.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2dffe52ecf27772e601905b7522cb4ef790d2cc203488bbd0e2fe85fcb74566d"
[[package]]
name = "memmap2"
version = "0.5.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3a79b39c93a7a5a27eeaf9a23b5ff43f1b9e0ad6b1cdd441140ae53c35613fc7"
dependencies = [
"libc",
]
[[package]]
name = "memoffset"
version = "0.6.5"
@ -4590,6 +4626,7 @@ dependencies = [
name = "test_ffi"
version = "0.1.0"
dependencies = [
"pretty_assertions",
"test_util",
]

View File

@ -16,6 +16,7 @@ path = "lib.rs"
[dependencies]
deno_core = { version = "0.149.0", path = "../../core" }
dlopen = "0.1.8"
dynasmrt = "1.2.3"
libffi = "3.0.0"
serde = { version = "1.0.129", features = ["derive"] }
tokio = { version = "1.17", features = ["full"] }

View File

@ -1,70 +0,0 @@
// Copyright 2018-2022 the Deno authors. All rights reserved. MIT license.
#[cfg(not(target_os = "windows"))]
fn build_tcc() {
use std::env;
{
// TODO(@littledivy): Windows support for fast call.
// let tcc_path = root
// .parent()
// .unwrap()
// .to_path_buf()
// .parent()
// .unwrap()
// .to_path_buf()
// .join("third_party")
// .join("prebuilt")
// .join("win");
// println!("cargo:rustc-link-search=native={}", tcc_path.display());
}
#[cfg(not(target_os = "windows"))]
{
use std::path::PathBuf;
use std::process::exit;
use std::process::Command;
let root = PathBuf::from(concat!(env!("CARGO_MANIFEST_DIR")));
let tcc_src = root.join("tinycc");
dbg!(&tcc_src);
let out_dir = PathBuf::from(env::var("OUT_DIR").unwrap());
let mut configure = Command::new(tcc_src.join("configure"));
configure.current_dir(&out_dir);
configure.args(&["--enable-static", "--extra-cflags=-fPIC -O3 -g -static"]);
let status = configure.status().unwrap();
if !status.success() {
eprintln!("Fail to configure: {:?}", status);
exit(1);
}
let mut make = Command::new("make");
make.current_dir(&out_dir).arg(format!(
"-j{}",
env::var("NUM_JOBS").unwrap_or_else(|_| String::from("1"))
));
make.args(&["libtcc.a"]);
let status = make.status().unwrap();
if !status.success() {
eprintln!("Fail to make: {:?}", status);
exit(1);
}
println!("cargo:rustc-link-search=native={}", out_dir.display());
println!("cargo:rerun-if-changed={}", tcc_src.display());
}
}
#[cfg(target_os = "windows")]
fn main() {}
#[cfg(not(target_os = "windows"))]
fn main() {
use std::env;
if let Ok(tcc_path) = env::var("TCC_PATH") {
println!("cargo:rustc-link-search=native={}", tcc_path);
} else {
build_tcc();
}
println!("cargo:rustc-link-lib=static=tcc");
}

2065
ext/ffi/fast_call.rs Normal file

File diff suppressed because it is too large Load Diff

View File

@ -1,263 +0,0 @@
// Copyright 2018-2022 the Deno authors. All rights reserved. MIT license.
use crate::NativeType;
use crate::{tcc::Compiler, Symbol};
use std::ffi::c_void;
use std::ffi::CString;
use std::fmt::Write as _;
use std::mem::size_of;
const _: () = assert!(size_of::<fn()>() == size_of::<usize>());
pub(crate) struct Allocation {
pub addr: *mut c_void,
_ctx: Compiler,
_sym: Box<Symbol>,
}
macro_rules! cstr {
($st:expr) => {
&CString::new($st).unwrap()
};
}
fn native_arg_to_c(ty: &NativeType) -> &'static str {
match ty {
NativeType::Bool => "bool",
NativeType::U8 | NativeType::U16 | NativeType::U32 => "uint32_t",
NativeType::I8 | NativeType::I16 | NativeType::I32 => "int32_t",
NativeType::Void => "void",
NativeType::F32 => "float",
NativeType::F64 => "double",
NativeType::U64 => "uint64_t",
NativeType::I64 => "int64_t",
NativeType::ISize => "intptr_t",
NativeType::USize => "uintptr_t",
NativeType::Buffer => "struct FastApiTypedArray*",
NativeType::Function | NativeType::Pointer => "void*",
}
}
fn native_to_c(ty: &NativeType) -> &'static str {
match ty {
NativeType::Bool => "bool",
NativeType::U8 => "uint8_t",
NativeType::U16 => "uint16_t",
NativeType::U32 => "uint32_t",
NativeType::I8 => "int8_t",
NativeType::I16 => "uint16_t",
NativeType::I32 => "int32_t",
NativeType::Void => "void",
NativeType::F32 => "float",
NativeType::F64 => "double",
NativeType::U64 => "uint64_t",
NativeType::I64 => "int64_t",
NativeType::ISize => "intptr_t",
NativeType::USize => "uintptr_t",
NativeType::Pointer | NativeType::Buffer | NativeType::Function => "void*",
}
}
pub(crate) fn codegen(sym: &crate::Symbol) -> String {
let mut c = String::from(include_str!("prelude.h"));
let needs_unwrap = crate::needs_unwrap(sym.result_type);
// Return type of the FFI call.
let ffi_ret = native_to_c(&sym.result_type);
// Return type of the trampoline.
let ret = if needs_unwrap { "void" } else { ffi_ret };
// extern <return_type> func(
let _ = write!(c, "\nextern {ffi_ret} func(");
// <param_type> p0, <param_type> p1, ...);
for (i, ty) in sym.parameter_types.iter().enumerate() {
if i > 0 {
c += ", ";
}
c += native_to_c(ty);
let _ = write!(c, " p{i}");
}
c += ");\n\n";
// void* recv, <param_type> p0, <param_type> p1, ...);
c += ret;
c += " func_trampoline(";
c += "void* recv";
for (i, ty) in sym.parameter_types.iter().enumerate() {
c += ", ";
c += native_arg_to_c(ty);
let _ = write!(c, " p{i}");
}
if needs_unwrap {
let _ = write!(c, ", struct FastApiTypedArray* const p_ret");
}
c += ") {\n";
// func(p0, p1, ...);
let mut call_s = String::from("func(");
{
for (i, ty) in sym.parameter_types.iter().enumerate() {
if i > 0 {
call_s += ", ";
}
if matches!(ty, NativeType::Buffer) {
let _ = write!(call_s, "p{i}->data");
} else {
let _ = write!(call_s, "p{i}");
}
}
call_s += ");\n";
}
if needs_unwrap {
// <return_type> r = func(p0, p1, ...);
// ((<return_type>*)p_ret->data)[0] = r;
let _ = write!(c, " {ffi_ret} r = {call_s}");
let _ = writeln!(c, " (({ffi_ret}*)p_ret->data)[0] = r;");
} else {
// return func(p0, p1, ...);
let _ = write!(c, " return {call_s}");
}
c += "}\n\n";
c
}
pub(crate) fn gen_trampoline(
sym: Box<crate::Symbol>,
) -> Result<Box<Allocation>, ()> {
let mut ctx = Compiler::new()?;
ctx.set_options(cstr!("-nostdlib"));
// SAFETY: symbol satisfies ABI requirement.
unsafe { ctx.add_symbol(cstr!("func"), sym.ptr.0 as *const c_void) };
let c = codegen(&sym);
ctx.compile_string(cstr!(c))?;
let alloc = Allocation {
addr: ctx.relocate_and_get_symbol(cstr!("func_trampoline"))?,
_ctx: ctx,
_sym: sym,
};
Ok(Box::new(alloc))
}
#[cfg(test)]
mod tests {
use super::*;
use libffi::middle::Type;
use std::ptr::null_mut;
fn codegen(parameters: Vec<NativeType>, ret: NativeType) -> String {
let sym = Box::new(crate::Symbol {
cif: libffi::middle::Cif::new(vec![], Type::void()),
ptr: libffi::middle::CodePtr(null_mut()),
parameter_types: parameters,
result_type: ret,
can_callback: false,
});
super::codegen(&sym)
}
const PRELUDE: &str = include_str!("prelude.h");
fn assert_codegen(expected: String, actual: &str) {
assert_eq!(expected, format!("{PRELUDE}\n{}", actual))
}
#[test]
fn test_gen_trampoline() {
assert_codegen(
codegen(vec![], NativeType::Void),
"extern void func();\n\n\
void func_trampoline(void* recv) {\
\n return func();\n\
}\n\n",
);
assert_codegen(
codegen(vec![NativeType::U32, NativeType::U32], NativeType::U32),
"extern uint32_t func(uint32_t p0, uint32_t p1);\n\n\
uint32_t func_trampoline(void* recv, uint32_t p0, uint32_t p1) {\
\n return func(p0, p1);\n\
}\n\n",
);
assert_codegen(
codegen(vec![NativeType::I32, NativeType::I32], NativeType::I32),
"extern int32_t func(int32_t p0, int32_t p1);\n\n\
int32_t func_trampoline(void* recv, int32_t p0, int32_t p1) {\
\n return func(p0, p1);\n\
}\n\n",
);
assert_codegen(
codegen(vec![NativeType::F32, NativeType::F32], NativeType::F32),
"extern float func(float p0, float p1);\n\n\
float func_trampoline(void* recv, float p0, float p1) {\
\n return func(p0, p1);\n\
}\n\n",
);
assert_codegen(
codegen(vec![NativeType::F64, NativeType::F64], NativeType::F64),
"extern double func(double p0, double p1);\n\n\
double func_trampoline(void* recv, double p0, double p1) {\
\n return func(p0, p1);\n\
}\n\n",
);
assert_codegen(
codegen(vec![NativeType::Buffer, NativeType::U32], NativeType::U32),
"extern uint32_t func(void* p0, uint32_t p1);\n\n\
uint32_t func_trampoline(void* recv, struct FastApiTypedArray* p0, uint32_t p1) {\
\n return func(p0->data, p1);\n\
}\n\n",
);
assert_codegen(
codegen(vec![NativeType::Buffer, NativeType::Buffer], NativeType::U32),
"extern uint32_t func(void* p0, void* p1);\n\n\
uint32_t func_trampoline(void* recv, struct FastApiTypedArray* p0, struct FastApiTypedArray* p1) {\
\n return func(p0->data, p1->data);\n\
}\n\n",
);
assert_codegen(
codegen(vec![], NativeType::U64),
"extern uint64_t func();\n\n\
void func_trampoline(void* recv, struct FastApiTypedArray* const p_ret) {\
\n uint64_t r = func();\
\n ((uint64_t*)p_ret->data)[0] = r;\n\
}\n\n",
);
assert_codegen(
codegen(vec![NativeType::Buffer, NativeType::Buffer], NativeType::U64),
"extern uint64_t func(void* p0, void* p1);\n\n\
void func_trampoline(void* recv, struct FastApiTypedArray* p0, struct FastApiTypedArray* p1, struct FastApiTypedArray* const p_ret) {\
\n uint64_t r = func(p0->data, p1->data);\
\n ((uint64_t*)p_ret->data)[0] = r;\n\
}\n\n",
);
assert_codegen(
codegen(vec![NativeType::Pointer, NativeType::Pointer], NativeType::U64),
"extern uint64_t func(void* p0, void* p1);\n\n\
void func_trampoline(void* recv, void* p0, void* p1, struct FastApiTypedArray* const p_ret) {\
\n uint64_t r = func(p0, p1);\
\n ((uint64_t*)p_ret->data)[0] = r;\n\
}\n\n",
);
}
#[test]
fn test_gen_trampoline_implicit_cast() {
assert_codegen(
codegen(vec![NativeType::I8, NativeType::U8], NativeType::I8),
"extern int8_t func(int8_t p0, uint8_t p1);\n\n\
int8_t func_trampoline(void* recv, int32_t p0, uint32_t p1) {\
\n return func(p0, p1);\n\
}\n\n",
);
assert_codegen(
codegen(vec![NativeType::ISize, NativeType::U64], NativeType::Void),
"extern void func(intptr_t p0, uint64_t p1);\n\n\
void func_trampoline(void* recv, intptr_t p0, uint64_t p1) {\
\n return func(p0, p1);\n\
}\n\n",
);
assert_codegen(
codegen(vec![NativeType::USize, NativeType::USize], NativeType::U32),
"extern uint32_t func(uintptr_t p0, uintptr_t p1);\n\n\
uint32_t func_trampoline(void* recv, uintptr_t p0, uintptr_t p1) {\
\n return func(p0, p1);\n\
}\n\n",
);
}
}

View File

@ -14,7 +14,6 @@ use deno_core::serde_json::json;
use deno_core::serde_json::Value;
use deno_core::serde_v8;
use deno_core::v8;
use deno_core::v8::fast_api;
use deno_core::Extension;
use deno_core::OpState;
use deno_core::Resource;
@ -39,15 +38,11 @@ use std::ptr;
use std::rc::Rc;
use std::sync::mpsc::sync_channel;
#[cfg(not(target_os = "windows"))]
mod jit_trampoline;
#[cfg(not(target_os = "windows"))]
mod tcc;
mod fast_call;
#[cfg(not(target_pointer_width = "64"))]
compile_error!("platform not supported");
// Assert assumptions made in `prelude.h`
const _: () = {
assert!(size_of::<c_char>() == 1);
assert!(size_of::<c_short>() == 2);
@ -90,8 +85,6 @@ struct Symbol {
ptr: libffi::middle::CodePtr,
parameter_types: Vec<NativeType>,
result_type: NativeType,
// This is dead code only on Windows
#[allow(dead_code)]
can_callback: bool,
}
@ -729,50 +722,6 @@ where
))
}
pub struct FfiFastCallTemplate {
args: Box<[fast_api::Type]>,
ret: fast_api::CType,
symbol_ptr: *const c_void,
}
impl fast_api::FastFunction for FfiFastCallTemplate {
fn function(&self) -> *const c_void {
self.symbol_ptr
}
fn args(&self) -> &'static [fast_api::Type] {
Box::leak(self.args.clone())
}
fn return_type(&self) -> fast_api::CType {
self.ret
}
}
impl From<&NativeType> for fast_api::Type {
fn from(native_type: &NativeType) -> Self {
match native_type {
NativeType::Bool => fast_api::Type::Bool,
NativeType::U8 | NativeType::U16 | NativeType::U32 => {
fast_api::Type::Uint32
}
NativeType::I8 | NativeType::I16 | NativeType::I32 => {
fast_api::Type::Int32
}
NativeType::F32 => fast_api::Type::Float32,
NativeType::F64 => fast_api::Type::Float64,
NativeType::Void => fast_api::Type::Void,
NativeType::I64 => fast_api::Type::Int64,
NativeType::U64 => fast_api::Type::Uint64,
NativeType::ISize => fast_api::Type::Int64,
NativeType::USize | NativeType::Pointer | NativeType::Function => {
fast_api::Type::Uint64
}
NativeType::Buffer => fast_api::Type::TypedArray(fast_api::CType::Uint8),
}
}
}
fn needs_unwrap(rv: NativeType) -> bool {
matches!(
rv,
@ -796,42 +745,6 @@ fn make_sync_fn<'s>(
scope: &mut v8::HandleScope<'s>,
sym: Box<Symbol>,
) -> v8::Local<'s, v8::Function> {
#[cfg(not(target_os = "windows"))]
let mut fast_ffi_templ: Option<FfiFastCallTemplate> = None;
#[cfg(target_os = "windows")]
let fast_ffi_templ: Option<FfiFastCallTemplate> = None;
#[cfg(not(target_os = "windows"))]
let mut fast_allocations: Option<*mut ()> = None;
#[cfg(not(target_os = "windows"))]
if !sym.can_callback {
let needs_unwrap = needs_unwrap(sym.result_type);
let ret = match needs_unwrap {
true => fast_api::Type::Void,
false => fast_api::Type::from(&sym.result_type),
};
let mut args = sym
.parameter_types
.iter()
.map(|t| t.into())
.collect::<Vec<_>>();
// recv
args.insert(0, fast_api::Type::V8Value);
if needs_unwrap {
args.push(fast_api::Type::TypedArray(fast_api::CType::Int32));
}
let symbol_trampoline =
jit_trampoline::gen_trampoline(sym.clone()).expect("gen_trampoline");
fast_ffi_templ = Some(FfiFastCallTemplate {
args: args.into_boxed_slice(),
ret: (&ret).into(),
symbol_ptr: symbol_trampoline.addr,
});
fast_allocations = Some(Box::into_raw(symbol_trampoline) as *mut ());
}
let sym = Box::leak(sym);
let builder = v8::FunctionTemplate::builder(
|scope: &mut v8::HandleScope,
@ -891,8 +804,17 @@ fn make_sync_fn<'s>(
)
.data(v8::External::new(scope, sym as *mut Symbol as *mut _).into());
let func = if let Some(fast_ffi_templ) = fast_ffi_templ {
builder.build_fast(scope, &fast_ffi_templ, None)
let mut fast_call_alloc = None;
let func = if fast_call::is_compatible(sym) {
let trampoline = fast_call::compile_trampoline(sym);
let func = builder.build_fast(
scope,
&fast_call::make_template(sym, &trampoline),
None,
);
fast_call_alloc = Some(Box::into_raw(Box::new(trampoline)));
func
} else {
builder.build(scope)
};
@ -904,12 +826,12 @@ fn make_sync_fn<'s>(
Box::new(move |_| {
// SAFETY: This is never called twice. pointer obtained
// from Box::into_raw, hence, satisfies memory layout requirements.
unsafe {
Box::from_raw(sym);
#[cfg(not(target_os = "windows"))]
if let Some(fast_allocations) = fast_allocations {
Box::from_raw(fast_allocations as *mut jit_trampoline::Allocation);
}
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) };
}
}),
);

View File

@ -1,36 +0,0 @@
// Copyright 2018-2022 the Deno authors. All rights reserved. MIT license.
/* Boolean type */
#ifndef _STDBOOL_H
#define _STDBOOL_H
#define bool _Bool
#define true 1
#define false 0
#endif
/* Exact integral types. */
/* Signed. */
typedef signed char int8_t;
typedef short int int16_t;
typedef int int32_t;
typedef long int int64_t;
/* Unsigned. */
typedef unsigned char uint8_t;
typedef unsigned short int uint16_t;
typedef unsigned int uint32_t;
typedef unsigned long int uint64_t;
/* Types for `void *' pointers. */
typedef long int intptr_t;
typedef unsigned long int uintptr_t;
// https://source.chromium.org/chromium/chromium/src/+/main:v8/include/v8-fast-api-calls.h;l=336
struct FastApiTypedArray {
uintptr_t length_;
void* data;
};

View File

@ -1,116 +0,0 @@
// Copyright 2018-2022 the Deno authors. All rights reserved. MIT license.
use std::{
ffi::CStr,
marker::PhantomData,
os::raw::{c_char, c_int, c_void},
ptr::null_mut,
};
#[repr(C)]
#[derive(Debug)]
pub struct TCCState {
_unused: [u8; 0],
}
pub const TCC_OUTPUT_MEMORY: i32 = 1;
extern "C" {
pub fn tcc_new() -> *mut TCCState;
pub fn tcc_delete(s: *mut TCCState);
pub fn tcc_set_options(s: *mut TCCState, str: *const c_char);
pub fn tcc_compile_string(s: *mut TCCState, buf: *const c_char) -> c_int;
pub fn tcc_add_symbol(
s: *mut TCCState,
name: *const c_char,
val: *const c_void,
) -> c_int;
pub fn tcc_set_output_type(s: *mut TCCState, output_type: c_int) -> c_int;
pub fn tcc_relocate(s1: *mut TCCState, ptr: *mut c_void) -> c_int;
pub fn tcc_get_symbol(s: *mut TCCState, name: *const c_char) -> *mut c_void;
}
/// Compilation context.
pub struct Compiler {
inner: *mut TCCState,
_phantom: PhantomData<TCCState>,
pub bin: Option<Vec<u8>>,
}
impl Compiler {
pub fn new() -> Result<Self, ()> {
// SAFETY: There is one context per thread.
let inner = unsafe { tcc_new() };
if inner.is_null() {
Err(())
} else {
let ret =
// SAFETY: set output to memory.
unsafe { tcc_set_output_type(inner, TCC_OUTPUT_MEMORY as c_int) };
assert_eq!(ret, 0);
Ok(Self {
inner,
_phantom: PhantomData,
bin: None,
})
}
}
pub fn set_options(&mut self, option: &CStr) -> &mut Self {
// SAFETY: option is a null-terminated C string.
unsafe {
tcc_set_options(self.inner, option.as_ptr());
}
self
}
pub fn compile_string(&mut self, p: &CStr) -> Result<(), ()> {
// SAFETY: p is a null-terminated C string.
let ret = unsafe { tcc_compile_string(self.inner, p.as_ptr()) };
if ret == 0 {
Ok(())
} else {
Err(())
}
}
/// # Safety
/// Symbol need satisfy ABI requirement.
pub unsafe fn add_symbol(&mut self, sym: &CStr, val: *const c_void) {
// SAFETY: sym is a null-terminated C string.
let ret = tcc_add_symbol(self.inner, sym.as_ptr(), val);
assert_eq!(ret, 0);
}
pub fn relocate_and_get_symbol(
&mut self,
sym: &CStr,
) -> Result<*mut c_void, ()> {
// SAFETY: pass null ptr to get required length
let len = unsafe { tcc_relocate(self.inner, null_mut()) };
if len == -1 {
return Err(());
};
let mut bin = Vec::with_capacity(len as usize);
let ret =
// SAFETY: bin is allocated up to len.
unsafe { tcc_relocate(self.inner, bin.as_mut_ptr() as *mut c_void) };
if ret != 0 {
return Err(());
}
// SAFETY: if ret == 0, bin is initialized.
unsafe {
bin.set_len(len as usize);
}
self.bin = Some(bin);
// SAFETY: sym is a null-terminated C string.
let addr = unsafe { tcc_get_symbol(self.inner, sym.as_ptr()) };
Ok(addr)
}
}
impl Drop for Compiler {
fn drop(&mut self) {
// SAFETY: delete state from tcc_new()
unsafe { tcc_delete(self.inner) };
}
}

@ -1 +0,0 @@
Subproject commit afc136262e93ae85fb3643005b36dbfc30d99c42

View File

@ -11,4 +11,5 @@ publish = false
crate-type = ["cdylib"]
[dev-dependencies]
pretty_assertions = "1.2.1"
test_util = { path = "../test_util" }

View File

@ -258,6 +258,60 @@ pub extern "C" fn call_stored_function_thread_safe_and_log() {
});
}
#[no_mangle]
pub extern "C" fn log_many_parameters(
a: u8,
b: u16,
c: u32,
d: u64,
e: f64,
f: f32,
g: i64,
h: i32,
i: i16,
j: i8,
k: isize,
l: usize,
m: f64,
n: f32,
o: f64,
p: f32,
q: f64,
r: f32,
s: f64,
) {
println!("{a} {b} {c} {d} {e} {f} {g} {h} {i} {j} {k} {l} {m} {n} {o} {p} {q} {r} {s}");
}
#[no_mangle]
pub extern "C" fn cast_u8_u32(x: u8) -> u32 {
x as u32
}
#[no_mangle]
pub extern "C" fn cast_u32_u8(x: u32) -> u8 {
x as u8
}
#[no_mangle]
pub extern "C" fn add_many_u16(
a: u16,
b: u16,
c: u16,
d: u16,
e: u16,
f: u16,
g: u16,
h: u16,
i: u16,
j: u16,
k: u16,
l: u16,
m: u16,
) -> u16 {
a + b + c + d + e + f + g + h + i + j + k + l + m
}
// FFI performance helper functions
#[no_mangle]
pub extern "C" fn nop() {}

View File

@ -80,6 +80,10 @@ fn basic() {
579.912\n\
true\n\
false\n\
579.9119873046875\n\
579.9119873046875\n\
579.912\n\
579.912\n\
579\n\
8589934590\n\
-8589934590\n\
@ -105,6 +109,14 @@ fn basic() {
buf: [1, 2, 3, 4, 5, 6, 7, 8]\n\
logCallback\n\
30\n\
255 65535 4294967295 4294967296 123.456 789.876 -1 -2 -3 -4 -1000 1000 12345.67891 12345.679 12345.67891 12345.679 12345.67891 12345.679 12345.67891\n\
255 65535 4294967295 4294967296 123.456 789.876 -1 -2 -3 -4 -1000 1000 12345.67891 12345.679 12345.67891 12345.679 12345.67891 12345.679 12345.67891\n\
0\n\
0\n\
0\n\
0\n\
78\n\
78\n\
STORED_FUNCTION cleared\n\
STORED_FUNCTION_2 cleared\n\
Thread safe call counter: 0\n\
@ -120,6 +132,8 @@ fn basic() {
uint32Array[0]: 42\n\
uint32Array[0] after mutation: 55\n\
Static ptr value after mutation: 55\n\
2264956937\n\
2264956937\n\
Correct number of resources\n";
assert_eq!(stdout, expected);
assert_eq!(stderr, "");

View File

@ -6,6 +6,7 @@
import { assertEquals } from "https://deno.land/std@0.149.0/testing/asserts.ts";
import {
assertThrows,
assert,
} from "../../test_util/std/testing/asserts.ts";
const targetDir = Deno.execPath().replace(/[^\/\\]+$/, "");
@ -175,6 +176,22 @@ const dylib = Deno.dlopen(libPath, {
result: "void",
callback: true,
},
log_many_parameters: {
parameters: ["u8", "u16", "u32", "u64", "f64", "f32", "i64", "i32", "i16", "i8", "isize", "usize", "f64", "f32", "f64", "f32", "f64", "f32", "f64"],
result: "void",
},
cast_u8_u32: {
parameters: ["u8"],
result: "u32",
},
cast_u32_u8: {
parameters: ["u32"],
result: "u8",
},
add_many_u16: {
parameters: ["u16", "u16", "u16", "u16", "u16", "u16", "u16", "u16", "u16", "u16", "u16", "u16", "u16"],
result: "u16",
},
// Statics
"static_u32": {
type: "u32",
@ -191,6 +208,7 @@ const dylib = Deno.dlopen(libPath, {
"static_char": {
type: "pointer",
},
"hash": { parameters: ["buffer", "u32"], result: "u32" },
});
const { symbols } = dylib;
@ -210,11 +228,7 @@ function returnBuffer() { return return_buffer(); };
returnBuffer();
%OptimizeFunctionOnNextCall(returnBuffer);
const ptr0 = returnBuffer();
const status = %GetOptimizationStatus(returnBuffer);
if (!(status & (1 << 4))) {
throw new Error("returnBuffer is not optimized");
}
assertIsOptimized(returnBuffer);
dylib.symbols.print_pointer(ptr0, 8);
const ptrView = new Deno.UnsafePointerView(ptr0);
@ -266,17 +280,10 @@ const { add_u32, add_usize_fast } = symbols;
function addU32Fast(a, b) {
return add_u32(a, b);
};
%PrepareFunctionForOptimization(addU32Fast);
console.log(addU32Fast(123, 456));
%OptimizeFunctionOnNextCall(addU32Fast);
console.log(addU32Fast(123, 456));
testOptimized(addU32Fast, () => addU32Fast(123, 456));
function addU64Fast(a, b) { return add_usize_fast(a, b); };
%PrepareFunctionForOptimization(addU64Fast);
console.log(addU64Fast(2, 3));
%OptimizeFunctionOnNextCall(addU64Fast);
console.log(addU64Fast(2, 3));
testOptimized(addU64Fast, () => addU64Fast(2, 3));
console.log(dylib.symbols.add_i32(123, 456));
console.log(dylib.symbols.add_u64(0xffffffffn, 0xffffffffn));
@ -294,6 +301,16 @@ console.log(dylib.symbols.add_f64(123.123, 456.789));
console.log(dylib.symbols.and(true, true));
console.log(dylib.symbols.and(true, false));
function addF32Fast(a, b) {
return dylib.symbols.add_f32(a, b);
};
testOptimized(addF32Fast, () => addF32Fast(123.123, 456.789));
function addF64Fast(a, b) {
return dylib.symbols.add_f64(a, b);
};
testOptimized(addF64Fast, () => addF64Fast(123.123, 456.789));
// Test adders as nonblocking calls
console.log(await dylib.symbols.add_i32_nonblocking(123, 456));
console.log(await dylib.symbols.add_u64_nonblocking(0xffffffffn, 0xffffffffn));
@ -437,6 +454,39 @@ call_stored_function();
dylib.symbols.store_function_2(add10Callback.pointer);
dylib.symbols.call_stored_function_2(20);
function logManyParametersFast(a, b, c, d, e, f, g, h, i, j, k, l, m, n, o, p, q, r, s) {
return symbols.log_many_parameters(a, b, c, d, e, f, g, h, i, j, k, l, m, n, o, p, q, r, s);
};
testOptimized(
logManyParametersFast,
() => logManyParametersFast(
255, 65535, 4294967295, 4294967296, 123.456, 789.876, -1, -2, -3, -4, -1000, 1000,
12345.678910, 12345.678910, 12345.678910, 12345.678910, 12345.678910, 12345.678910, 12345.678910
)
);
// Some ABIs rely on the convention to zero/sign-extend arguments by the caller to optimize the callee function.
// If the trampoline did not zero/sign-extend arguments, this would return 256 instead of the expected 0 (in optimized builds)
function castU8U32Fast(x) { return symbols.cast_u8_u32(x); };
testOptimized(castU8U32Fast, () => castU8U32Fast(256));
// Some ABIs rely on the convention to expect garbage in the bits beyond the size of the return value to optimize the callee function.
// If the trampoline did not zero/sign-extend the return value, this would return 256 instead of the expected 0 (in optimized builds)
function castU32U8Fast(x) { return symbols.cast_u32_u8(x); };
testOptimized(castU32U8Fast, () => castU32U8Fast(256));
// Generally the trampoline tail-calls into the FFI function, but in certain cases (e.g. when returning 8 or 16 bit integers)
// the tail call is not possible and a new stack frame must be created. We need enough parameters to have some on the stack
function addManyU16Fast(a, b, c, d, e, f, g, h, i, j, k, l, m) {
return symbols.add_many_u16(a, b, c, d, e, f, g, h, i, j, k, l, m);
};
// N.B. V8 does not currently follow Aarch64 Apple's calling convention.
// The current implementation of the JIT trampoline follows the V8 incorrect calling convention. This test covers the use-case
// and is expected to fail once Deno uses a V8 version with the bug fixed.
// The V8 bug is being tracked in https://bugs.chromium.org/p/v8/issues/detail?id=13171
testOptimized(addManyU16Fast, () => addManyU16Fast(0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12));
const nestedCallback = new Deno.UnsafeCallback(
{ parameters: [], result: "void" },
() => {
@ -502,6 +552,12 @@ try {
console.log("Invalid UTF-8 characters to `v8::String`:", charView.getCString());
}
const bytes = new Uint8Array([0, 1, 2, 3, 4, 5, 6, 7, 8, 9]);
function hash() { return dylib.symbols.hash(bytes, bytes.byteLength); };
testOptimized(hash, () => hash());
(function cleanup() {
dylib.close();
throwCallback.close();
@ -526,4 +582,23 @@ After: ${postStr}`,
}
console.log("Correct number of resources");
})();
})();
function assertIsOptimized(fn) {
const status = % GetOptimizationStatus(fn);
assert(status & (1 << 4), `expected ${fn.name} to be optimized, but wasn't`);
}
function testOptimized(fn, callback) {
%PrepareFunctionForOptimization(fn);
const r1 = callback();
if (r1 !== undefined) {
console.log(r1);
}
%OptimizeFunctionOnNextCall(fn);
const r2 = callback();
if (r2 !== undefined) {
console.log(r2);
}
assertIsOptimized(fn);
}

View File

@ -40,4 +40,4 @@ tokio-tungstenite = "0.16"
pty = "0.2.2"
[target.'cfg(windows)'.dependencies]
winapi = { version = "0.3.9", features = ["consoleapi", "handleapi", "namedpipeapi", "winbase", "winerror"] }
winapi = { version = "0.3.9", features = ["consoleapi", "synchapi", "handleapi", "namedpipeapi", "winbase", "winerror"] }