use anyhow::{Context, Result, bail};
use std::collections::{HashMap, VecDeque};
use std::sync::{
Arc, Mutex,
atomic::{AtomicUsize, Ordering},
};
use wasmer::{ExternType, FunctionEnv, Imports, Instance, Module, StoreMut, Table, Value};
use crate::{
NAPI_EXTENSION_WASMER_MODULE_NAME, NAPI_EXTENSION_WASMER_MODULE_PREFIX, NAPI_MODULE_NAME,
NapiEnv, NapiVersion, NapiWasmerExtensionVersion,
guest::napi::{is_known_napi_import, register_env_imports, register_napi_imports},
};
#[derive(Debug, Clone, Default)]
pub struct NapiLimits {
pub max_sessions: Option<usize>,
pub max_envs: Option<usize>,
pub max_total_external_memory: Option<u64>,
pub max_total_heap_bytes: Option<u64>,
}
#[derive(Debug, Default)]
pub struct NapiCtxBuilder {
limits: NapiLimits,
}
#[derive(Clone, Debug)]
pub struct NapiCtx {
inner: Arc<NapiCtxInner>,
}
#[derive(Clone)]
pub struct NapiSession {
inner: Arc<NapiSessionInner>,
}
#[derive(Clone, Debug)]
pub struct NapiRuntimeHooks {
ctx: NapiCtx,
sessions: Arc<Mutex<HashMap<usize, VecDeque<NapiSession>>>>,
}
#[derive(Debug)]
struct NapiCtxInner {
limits: NapiLimits,
active_sessions: AtomicUsize,
}
struct NapiSessionInner {
ctx: Arc<NapiCtxInner>,
imported_memory_type: Option<wasmer::MemoryType>,
imported_table_type: Option<wasmer::TableType>,
func_env: Mutex<Option<FunctionEnv<NapiEnv>>>,
}
impl std::fmt::Debug for NapiSession {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("NapiSession").finish_non_exhaustive()
}
}
impl Drop for NapiSessionInner {
fn drop(&mut self) {
self.ctx.active_sessions.fetch_sub(1, Ordering::AcqRel);
}
}
impl NapiCtxBuilder {
pub fn max_sessions(mut self, max_sessions: usize) -> Self {
self.limits.max_sessions = Some(max_sessions);
self
}
pub fn max_envs(mut self, max_envs: usize) -> Self {
self.limits.max_envs = Some(max_envs);
self
}
pub fn max_total_external_memory(mut self, bytes: u64) -> Self {
self.limits.max_total_external_memory = Some(bytes);
self
}
pub fn max_total_heap_bytes(mut self, bytes: u64) -> Self {
self.limits.max_total_heap_bytes = Some(bytes);
self
}
pub fn build(self) -> NapiCtx {
NapiCtx {
inner: Arc::new(NapiCtxInner {
limits: self.limits,
active_sessions: AtomicUsize::new(0),
}),
}
}
}
impl Default for NapiCtx {
fn default() -> Self {
Self::builder().build()
}
}
impl NapiCtx {
pub fn builder() -> NapiCtxBuilder {
NapiCtxBuilder::default()
}
pub fn limits(&self) -> &NapiLimits {
&self.inner.limits
}
pub fn active_sessions(&self) -> usize {
self.inner.active_sessions.load(Ordering::Acquire)
}
pub fn prepare_module(&self, module: &Module) -> Result<NapiSession> {
self.new_session(module)
}
pub fn module_needs_napi(
module: &Module,
) -> (Option<NapiVersion>, Option<NapiWasmerExtensionVersion>) {
let mut napi_version = None;
let mut napi_extension_version = None;
for import in module.imports() {
if import.module() == NAPI_MODULE_NAME {
napi_version = Some(match napi_version {
Some(NapiVersion::Unknown) => NapiVersion::Unknown,
_ if is_known_napi_import(import.name()) => NapiVersion::V10,
_ => NapiVersion::Unknown,
});
continue;
}
let Some(detected_extension_version) =
napi_wasmer_extension_version_from_namespace(import.module())
else {
continue;
};
napi_extension_version = Some(match napi_extension_version {
None => detected_extension_version,
Some(existing) if existing == detected_extension_version => existing,
Some(_) => NapiWasmerExtensionVersion::Unknown,
});
}
(napi_version, napi_extension_version)
}
pub fn runtime_hooks(&self) -> NapiRuntimeHooks {
NapiRuntimeHooks {
ctx: self.clone(),
sessions: Arc::new(Mutex::new(HashMap::new())),
}
}
pub fn new_session(&self, module: &Module) -> Result<NapiSession> {
let previous = self.inner.active_sessions.fetch_add(1, Ordering::AcqRel);
if let Some(max_sessions) = self.inner.limits.max_sessions
&& previous >= max_sessions
{
self.inner.active_sessions.fetch_sub(1, Ordering::AcqRel);
bail!("refusing to create more than {max_sessions} active N-API sessions");
}
let imported_memory_type = module.imports().find_map(|import| {
if import.module() == "env"
&& import.name() == "memory"
&& let ExternType::Memory(ty) = import.ty()
{
return Some(*ty);
}
None
});
let imported_table_type = module.imports().find_map(|import| {
if import.module() == "env"
&& import.name() == "__indirect_function_table"
&& let ExternType::Table(ty) = import.ty()
{
return Some(*ty);
}
None
});
Ok(NapiSession {
inner: Arc::new(NapiSessionInner {
ctx: Arc::clone(&self.inner),
imported_memory_type,
imported_table_type,
func_env: Mutex::new(None),
}),
})
}
}
impl NapiRuntimeHooks {
fn module_key(module: &Module) -> usize {
module as *const Module as usize
}
pub fn additional_imports(&self, module: &Module, store: &mut StoreMut<'_>) -> Result<Imports> {
let (napi_version, napi_extension_version) = NapiCtx::module_needs_napi(module);
if napi_version.is_none() && napi_extension_version.is_none() {
return Ok(Imports::new());
}
if let Some(version) = napi_version
&& !NapiVersion::V10.is_compatible_with(version)
{
bail!("unsupported N-API import version: {version:?}");
}
if let Some(version) = napi_extension_version
&& !NapiWasmerExtensionVersion::V0.is_compatible_with(version)
{
bail!("unsupported Wasmer N-API extension version: {version:?}");
}
let session = self.ctx.prepare_module(module)?;
let imports = session.create_imports(store)?;
let mut sessions = self
.sessions
.lock()
.expect("poisoned NapiRuntimeHooks session queue");
sessions
.entry(Self::module_key(module))
.or_default()
.push_back(session);
Ok(imports)
}
pub fn configure_instance(
&self,
module: &Module,
store: &mut StoreMut<'_>,
instance: &Instance,
imported_memory: Option<&wasmer::Memory>,
) -> Result<()> {
let (napi_version, napi_extension_version) = NapiCtx::module_needs_napi(module);
if napi_version.is_none() && napi_extension_version.is_none() {
return Ok(());
}
let session = {
let mut sessions = self
.sessions
.lock()
.expect("poisoned NapiRuntimeHooks session queue");
let key = Self::module_key(module);
let Some(queue) = sessions.get_mut(&key) else {
bail!("missing pending N-API session for module instance setup");
};
let session = queue
.pop_front()
.context("missing queued N-API session for module instance setup")?;
if queue.is_empty() {
sessions.remove(&key);
}
session
};
session.configure_instance(store, instance, imported_memory)
}
}
impl NapiSession {
pub fn create_imports(&self, store: &mut StoreMut<'_>) -> Result<Imports> {
let mut import_object = Imports::new();
register_env_imports(store, &mut import_object);
let func_env = FunctionEnv::new(store, NapiEnv::default());
{
let mut guard = self
.inner
.func_env
.lock()
.expect("poisoned NapiSession mutex");
*guard = Some(func_env.clone());
}
register_napi_imports(store, &func_env, &mut import_object);
if let Some(memory_type) = self.inner.imported_memory_type {
let memory = wasmer::Memory::new(&mut *store, memory_type)?;
import_object.define("env", "memory", memory.clone());
func_env.as_mut(&mut *store).memory = Some(memory);
}
if let Some(table_type) = self.inner.imported_table_type {
let table = Table::new(&mut *store, table_type, Value::FuncRef(None))?;
import_object.define("env", "__indirect_function_table", table.clone());
func_env.as_mut(&mut *store).table = Some(table);
}
Ok(import_object)
}
pub fn configure_instance(
&self,
store: &mut StoreMut<'_>,
instance: &Instance,
imported_memory: Option<&wasmer::Memory>,
) -> Result<()> {
let func_env = {
let guard = self
.inner
.func_env
.lock()
.expect("poisoned NapiSession mutex");
guard
.clone()
.context("missing runtime function env during instance setup")?
};
if let Some(memory) = imported_memory {
func_env.as_mut(&mut *store).memory = Some(memory.clone());
}
for export_name in ["unofficial_napi_guest_malloc", "malloc"] {
if let Ok(malloc) = instance
.exports
.get_typed_function::<i32, i32>(&store, export_name)
{
func_env.as_mut(&mut *store).malloc_fn = Some(malloc);
break;
}
}
if let Ok(table) = instance.exports.get_table("__indirect_function_table") {
func_env.as_mut(&mut *store).table = Some(table.clone());
}
Ok(())
}
}
fn napi_wasmer_extension_version_from_namespace(
namespace: &str,
) -> Option<NapiWasmerExtensionVersion> {
if namespace == NAPI_EXTENSION_WASMER_MODULE_NAME {
return Some(NapiWasmerExtensionVersion::V0);
}
let suffix = namespace.strip_prefix(NAPI_EXTENSION_WASMER_MODULE_PREFIX)?;
Some(match suffix {
"0" => NapiWasmerExtensionVersion::V0,
_ => NapiWasmerExtensionVersion::Unknown,
})
}
#[cfg(test)]
mod tests {
use super::NapiCtx;
use crate::{NapiVersion, NapiWasmerExtensionVersion};
use wasmer::{Module, Store};
use wat::parse_str;
const EMPTY_WASM_MODULE: &[u8] = b"\0asm\x01\0\0\0";
#[test]
fn max_sessions_limit_is_enforced() {
let store = Store::default();
let module = Module::new(&store, EMPTY_WASM_MODULE).expect("empty wasm module compiles");
let ctx = NapiCtx::builder().max_sessions(1).build();
let first = ctx
.prepare_module(&module)
.expect("first session should be created");
assert_eq!(ctx.active_sessions(), 1);
assert!(ctx.prepare_module(&module).is_err());
drop(first);
assert_eq!(ctx.active_sessions(), 0);
let _second = ctx
.prepare_module(&module)
.expect("session slot should be released after drop");
assert_eq!(ctx.active_sessions(), 1);
}
fn compile_wat(store: &Store, wat: &str) -> Module {
let wasm = parse_str(wat).expect("wat module parses");
Module::new(store, wasm).expect("wat module compiles")
}
#[test]
fn module_needs_napi_detects_none() {
let store = Store::default();
let module = Module::new(&store, EMPTY_WASM_MODULE).expect("empty wasm module compiles");
assert_eq!(NapiCtx::module_needs_napi(&module), (None, None));
}
#[test]
fn module_needs_napi_detects_core_napi_v10() {
let store = Store::default();
let module = compile_wat(
&store,
r#"(module
(import "napi" "napi_get_undefined" (func))
)"#,
);
assert_eq!(
NapiCtx::module_needs_napi(&module),
(Some(NapiVersion::V10), None)
);
}
#[test]
fn module_needs_napi_detects_unknown_core_napi() {
let store = Store::default();
let module = compile_wat(
&store,
r#"(module
(import "napi" "napi_future_function" (func))
)"#,
);
assert_eq!(
NapiCtx::module_needs_napi(&module),
(Some(NapiVersion::Unknown), None)
);
}
#[test]
fn module_needs_napi_detects_extension_v0() {
let store = Store::default();
let module = compile_wat(
&store,
r#"(module
(import "napi_extension_wasmer_v0" "unofficial_napi_get_hash_seed" (func))
)"#,
);
assert_eq!(
NapiCtx::module_needs_napi(&module),
(None, Some(NapiWasmerExtensionVersion::V0))
);
}
#[test]
fn module_needs_napi_detects_unknown_extension_version() {
let store = Store::default();
let module = compile_wat(
&store,
r#"(module
(import "napi_extension_wasmer_v1" "unofficial_napi_get_hash_seed" (func))
)"#,
);
assert_eq!(
NapiCtx::module_needs_napi(&module),
(None, Some(NapiWasmerExtensionVersion::Unknown))
);
}
#[test]
fn module_needs_napi_detects_mixed_namespaces() {
let store = Store::default();
let module = compile_wat(
&store,
r#"(module
(import "napi" "napi_get_undefined" (func))
(import "napi_extension_wasmer_v0" "unofficial_napi_get_hash_seed" (func))
)"#,
);
assert_eq!(
NapiCtx::module_needs_napi(&module),
(Some(NapiVersion::V10), Some(NapiWasmerExtensionVersion::V0))
);
}
#[test]
fn napi_version_compatibility_is_additive() {
assert!(NapiVersion::V10.is_compatible_with(NapiVersion::V10));
assert!(!NapiVersion::V10.is_compatible_with(NapiVersion::Unknown));
assert!(NapiVersion::Unknown.is_compatible_with(NapiVersion::V10));
assert!(!NapiVersion::Unknown.is_compatible_with(NapiVersion::Unknown));
}
#[test]
fn napi_wasmer_extension_version_compatibility_is_strict() {
assert!(NapiWasmerExtensionVersion::V0.is_compatible_with(NapiWasmerExtensionVersion::V0));
assert!(
!NapiWasmerExtensionVersion::V0.is_compatible_with(NapiWasmerExtensionVersion::Unknown)
);
assert!(
!NapiWasmerExtensionVersion::Unknown.is_compatible_with(NapiWasmerExtensionVersion::V0)
);
assert!(
!NapiWasmerExtensionVersion::Unknown
.is_compatible_with(NapiWasmerExtensionVersion::Unknown)
);
}
}