#![allow(clippy::missing_safety_doc)]
#![allow(non_camel_case_types)]
mod async_ops;
mod buffers;
mod error;
mod functions;
mod object_lifetime;
mod objects;
mod runtime;
mod script;
mod state;
mod values;
use std::{
ffi::{c_char, c_int, c_void, CStr, CString},
mem, ptr, result, slice,
sync::{
atomic::{AtomicUsize, Ordering},
Mutex,
},
};
use libc::{dlerror, dlopen, dlsym, RTLD_LAZY, RTLD_LOCAL};
use rquickjs::{
function::{Args, Rest, This},
qjs, Array, Ctx, Error, Exception, Function, Object, Result, Value,
};
use self::{
async_ops::*, buffers::*, error::*, functions::*, object_lifetime::*, objects::*, runtime::*,
script::*, state::*, values::*,
};
type V = Value<'static>;
type napi_status = c_int;
type napi_env = *mut Env;
type napi_value = *mut V;
type napi_callback_info = *mut c_void;
type napi_callback = unsafe extern "C" fn(napi_env, napi_callback_info) -> napi_value;
type napi_finalize = unsafe extern "C" fn(napi_env, *mut c_void, *mut c_void);
type napi_cleanup_hook = unsafe extern "C" fn(*mut c_void);
type napi_async_execute = unsafe extern "C" fn(napi_env, *mut c_void);
type napi_async_complete = unsafe extern "C" fn(napi_env, napi_status, *mut c_void);
type napi_addon_init = unsafe extern "C" fn(napi_env, napi_value) -> napi_value;
const NAPI_OK: napi_status = 0;
const NAPI_INVALID_ARG: napi_status = 1;
const NAPI_OBJECT_EXPECTED: napi_status = 2;
const NAPI_STRING_EXPECTED: napi_status = 3;
const NAPI_FUNCTION_EXPECTED: napi_status = 5;
const NAPI_NUMBER_EXPECTED: napi_status = 6;
const NAPI_BOOLEAN_EXPECTED: napi_status = 7;
const NAPI_ARRAY_EXPECTED: napi_status = 8;
const NAPI_GENERIC_FAILURE: napi_status = 9;
const NAPI_PENDING_EXCEPTION: napi_status = 10;
const NAPI_CANCELLED: napi_status = 11;
const NAPI_ESCAPE_CALLED_TWICE: napi_status = 12;
const NAPI_STATIC: c_int = 1 << 10;
type Status<T> = result::Result<T, napi_status>;
const NAPI_AUTO_LENGTH: usize = usize::MAX;
static ADDON_LOAD_LOCK: Mutex<()> = Mutex::new(());
static NEXT_ENV_ANCHOR: AtomicUsize = AtomicUsize::new(1);
#[repr(C)]
pub struct napi_extended_error_info {
error_message: *const c_char,
engine_reserved: *mut c_void,
engine_error_code: u32,
error_code: napi_status,
}
#[repr(C)]
pub struct napi_property_descriptor {
utf8name: *const c_char,
name: napi_value,
method: Option<napi_callback>,
getter: Option<napi_callback>,
setter: Option<napi_callback>,
value: napi_value,
attributes: c_int,
data: *mut c_void,
}
pub struct Env {
ctx: ptr::NonNull<qjs::JSContext>,
#[allow(clippy::vec_box)]
handles: Vec<Box<V>>,
exception: Option<V>,
last_error: napi_extended_error_info,
instance_data: *mut c_void,
instance_finalize_cb: Option<napi_finalize>,
instance_finalize_hint: usize,
cleanup_hooks: Mutex<Vec<CleanupHook>>,
}
#[derive(Clone, Copy)]
struct CleanupHook {
callback: usize,
arg: usize,
}
unsafe extern "C" fn env_finalizer(_rt: *mut qjs::JSRuntime, value: qjs::JSValue) {
let mut class_id = 0;
let opaque = qjs::JS_GetAnyOpaque(value, &mut class_id);
if opaque.is_null() {
return;
}
qjs::JS_SetOpaque(value, ptr::null_mut());
let env = opaque as napi_env;
loop {
let hook = match (*env).cleanup_hooks.lock() {
Ok(mut hooks) => hooks.pop(),
Err(poisoned) => {
eprintln!("Recovering poisoned N-API cleanup hook list");
poisoned.into_inner().pop()
}
};
let Some(hook) = hook else {
break;
};
let callback: napi_cleanup_hook = mem::transmute(hook.callback);
callback(hook.arg as *mut c_void);
}
if let Some(callback) = (*env).instance_finalize_cb.take() {
let mark = (*env).handles.len();
callback(
env,
(*env).instance_data,
(*env).instance_finalize_hint as *mut c_void,
);
(*env).handles.truncate(mark);
(*env).exception.take();
}
drop(Box::from_raw(env));
}
fn anchor_env(ctx: &Ctx<'_>, env: Box<Env>) -> Result<napi_env> {
let ctx_ptr = ctx.as_raw().as_ptr();
let runtime = unsafe { qjs::JS_GetRuntime(ctx_ptr) };
let mut class_id = 0;
unsafe { qjs::JS_NewClassID(runtime, &mut class_id) };
let class_def = qjs::JSClassDef {
class_name: c"RquickjsNapiEnv".as_ptr(),
finalizer: Some(env_finalizer),
gc_mark: None,
call: None,
exotic: ptr::null_mut(),
};
if unsafe { qjs::JS_NewClass(runtime, class_id, &class_def) } < 0 {
return Err(Error::Unknown);
}
let anchor = unsafe { qjs::JS_NewObjectClass(ctx_ptr, class_id) };
if unsafe { qjs::JS_IsException(anchor) } {
return Err(Error::Exception);
}
let env = Box::into_raw(env);
if unsafe { qjs::JS_SetOpaque(anchor, env.cast()) } != 0 {
unsafe {
qjs::JS_SetOpaque(anchor, ptr::null_mut());
drop(Box::from_raw(env));
qjs::JS_FreeValue(ctx_ptr, anchor);
}
return Err(Error::Unknown);
}
let global = unsafe { qjs::JS_GetGlobalObject(ctx_ptr) };
let anchor_id = NEXT_ENV_ANCHOR.fetch_add(1, Ordering::Relaxed);
let name = CString::new(format!("__rquickjs_napi_env_{anchor_id}"))
.expect("generated anchor name has no NUL bytes");
let status = unsafe {
qjs::JS_DefinePropertyValueStr(
ctx_ptr,
global,
name.as_ptr(),
qjs::JS_DupValue(ctx_ptr, anchor),
0,
)
};
if status <= 0 {
unsafe {
qjs::JS_SetOpaque(anchor, ptr::null_mut());
qjs::JS_FreeValue(ctx_ptr, anchor);
qjs::JS_FreeValue(ctx_ptr, global);
drop(Box::from_raw(env));
}
return Err(if status < 0 {
Error::Exception
} else {
Error::Unknown
});
}
unsafe {
qjs::JS_FreeValue(ctx_ptr, anchor);
qjs::JS_FreeValue(ctx_ptr, global);
}
Ok(env as napi_env)
}
unsafe fn extend<'js>(v: Value<'js>) -> V {
mem::transmute::<Value<'js>, V>(v)
}
unsafe fn narrow<'js>(v: V) -> Value<'js> {
mem::transmute::<V, Value<'js>>(v)
}
fn push(env: napi_env, v: V) -> napi_value {
unsafe {
let mut boxed = Box::new(v);
let p: napi_value = &mut *boxed;
(*env).handles.push(boxed);
p
}
}
fn ctx(env: napi_env) -> Ctx<'static> {
unsafe { Ctx::from_raw((*env).ctx) }
}
fn run(env: napi_env, f: impl FnOnce() -> Status<()>) -> napi_status {
if env.is_null() {
return NAPI_INVALID_ARG;
}
let status = f().err().unwrap_or(NAPI_OK);
unsafe { (*env).last_error.error_code = status };
status
}
fn run_js(env: napi_env, f: impl FnOnce() -> Status<()>) -> napi_status {
if !env.is_null() && unsafe { (*env).exception.is_some() } {
unsafe { (*env).last_error.error_code = NAPI_PENDING_EXCEPTION };
return NAPI_PENDING_EXCEPTION;
}
run(env, f)
}
fn js<T>(env: napi_env, r: Result<T>) -> Status<T> {
r.map_err(|err| {
let c = ctx(env);
let exception = if matches!(err, Error::Exception) {
c.catch()
} else {
Exception::from_message(c.clone(), &err.to_string())
.map(|e| e.into_value())
.unwrap_or_else(|_| Value::new_undefined(c))
};
unsafe { (*env).exception = Some(extend(exception)) };
NAPI_PENDING_EXCEPTION
})
}
fn qjs_status(env: napi_env, status: c_int) -> Status<bool> {
if status < 0 {
unsafe {
(*env).exception = Some(extend(ctx(env).catch()));
}
Err(NAPI_PENDING_EXCEPTION)
} else {
Ok(status != 0)
}
}
unsafe fn get<'a>(v: napi_value) -> Status<&'a V> {
v.as_ref().ok_or(NAPI_INVALID_ARG)
}
unsafe fn out<T>(p: *mut T, v: T) -> Status<()> {
if p.is_null() {
return Err(NAPI_INVALID_ARG);
}
*p = v;
Ok(())
}
unsafe fn cstr(p: *const c_char, len: usize) -> Status<String> {
if p.is_null() {
return if len == 0 {
Ok(String::new())
} else {
Err(NAPI_INVALID_ARG)
};
}
let bytes = if len == NAPI_AUTO_LENGTH {
CStr::from_ptr(p).to_bytes()
} else {
slice::from_raw_parts(p as *const u8, len)
};
Ok(String::from_utf8_lossy(bytes).into_owned())
}
pub(crate) fn load_addon<'js>(ctx: &Ctx<'js>, path: &str) -> Result<Value<'js>> {
runtime::retain_exported_symbols();
let throw = |msg: String, code: &str| {
Exception::from_message(ctx.clone(), &msg)
.and_then(|e| {
e.set("code", code)?;
Ok(e.throw())
})
.unwrap_or_else(|e| e)
};
let c_path =
CString::new(path).map_err(|_| throw("Invalid path".into(), "ERR_DLOPEN_FAILED"))?;
let (_handle, init) = {
let _load_guard = ADDON_LOAD_LOCK
.lock()
.map_err(|_| throw("Addon loader lock is poisoned".into(), "ERR_DLOPEN_FAILED"))?;
PENDING_MODULE.store(ptr::null_mut(), Ordering::Relaxed);
let handle = unsafe { dlopen(c_path.as_ptr(), RTLD_LAZY | RTLD_LOCAL) };
if handle.is_null() {
let err = unsafe {
let e = dlerror();
if e.is_null() {
"unknown error".to_string()
} else {
CStr::from_ptr(e).to_string_lossy().into_owned()
}
};
return Err(throw(err, "ERR_DLOPEN_FAILED"));
}
let symbol = unsafe { dlsym(handle, c"napi_register_module_v1".as_ptr()) };
let init: napi_addon_init = if !symbol.is_null() {
unsafe { mem::transmute::<*mut c_void, napi_addon_init>(symbol) }
} else {
let module = PENDING_MODULE.load(Ordering::Acquire);
match unsafe { module.as_ref().and_then(|m| m.nm_register_func) } {
Some(init) => init,
None => {
return Err(throw(
format!("Module did not self-register: '{}'.", path),
"ERR_DLOPEN_FAILED",
))
}
}
};
(handle, init)
};
let env = Box::new(Env {
ctx: ctx.as_raw(),
handles: Vec::new(),
exception: None,
instance_data: ptr::null_mut(),
instance_finalize_cb: None,
instance_finalize_hint: 0,
cleanup_hooks: Mutex::new(Vec::new()),
last_error: napi_extended_error_info {
error_message: ptr::null(),
engine_reserved: ptr::null_mut(),
engine_error_code: 0,
error_code: NAPI_OK,
},
});
let env = anchor_env(ctx, env)?;
let exports_value = Object::new(ctx.clone())?.into_value();
let exports_handle = push(env, unsafe { extend(exports_value.clone()) });
let ret = unsafe { init(env, exports_handle) };
let result = unsafe { ret.as_ref().map(|v| narrow::<'js>(v.clone())) }.unwrap_or(exports_value);
let exception = unsafe {
(*env).handles.clear();
(*env).exception.take()
};
match exception {
Some(e) => Err(ctx.throw(unsafe { narrow(e) })),
None => Ok(result),
}
}