Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
425 changes: 208 additions & 217 deletions Cargo.standalone.lock

Large diffs are not rendered by default.

11 changes: 10 additions & 1 deletion Cargo.standalone.toml
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,12 @@ edition = "2024"
license = "MIT"
resolver = "2"

[package.metadata.napi]
# The pinned public Wasmer SDK predates LinearMemory::do_wait_interruptible.
# build.rs uses this marker so the compatibility path cannot be changed by
# --no-default-features or --all-features.
standalone_legacy_wait = true

[features]
js = [
"dep:js-sys",
Expand Down Expand Up @@ -33,7 +39,9 @@ cli = [
anyhow = "1"
# Standalone builds use one immutable Wasmer source revision. The N-API
# instantiation hook and shared-memory bridge are developed together on the SDK
# branch, so mixing this source with crates.io lacks required APIs.
# branch, so mixing this source with crates.io lacks required APIs. This public
# revision also carries the APIs needed by the wasm32+js backend. Edge's WASIX
# child-reap integration uses its separately reviewed Wasmer source.
wasmer = { git = "https://github.com/wasmerio/wasmer.git", rev = "c83ba6d315d051739339636da7080ed5987845de", default-features = false }

# This dependencies are ONLY needed for testing napi in Wasm locally
Expand All @@ -46,6 +54,7 @@ virtual-fs = { git = "https://github.com/wasmerio/wasmer.git", rev = "c83ba6d315
tokio = { version = "1", features = ["rt-multi-thread"], optional = true }
enum-iterator = "2.3.0"
offset-allocator = "0.2.0"
parking_lot = "0.12"

[target.'cfg(target_arch = "wasm32")'.dependencies]
js-sys = { version = "0.3", optional = true }
Expand Down
1 change: 1 addition & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@ virtual-fs = { version = "=0.704.0", path = "../virtual-fs", optional = true }
tokio = { version = "1", features = ["rt-multi-thread"], optional = true }
enum-iterator = "2.3.0"
offset-allocator = "0.2.0"
parking_lot = "0.12"

[target.'cfg(target_arch = "wasm32")'.dependencies]
js-sys = { version = "0.3", optional = true }
Expand Down
69 changes: 68 additions & 1 deletion build.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
use std::{
env,
env, fs,
io::Read,
path::{Path, PathBuf},
};
Expand Down Expand Up @@ -37,6 +37,12 @@ enum ExtraLink {
}

fn main() {
println!("cargo:rustc-check-cfg=cfg(napi_standalone_legacy_wait)");
println!("cargo:rerun-if-changed=Cargo.toml");
if standalone_legacy_wait() {
println!("cargo:rustc-cfg=napi_standalone_legacy_wait");
}

let target_arch = env::var("CARGO_CFG_TARGET_ARCH").unwrap_or_default();
if target_arch == "wasm32" {
if env::var_os("CARGO_FEATURE_JS").is_none() {
Expand Down Expand Up @@ -85,6 +91,24 @@ fn main() {

let v8_defines = read_env_value("V8_DEFINES", &["NAPI_V8_DEFINES", "NAPI_V8_V8_DEFINES"])
.unwrap_or_else(|| "V8_COMPRESS_POINTERS".to_string());
let sandbox_define = v8_defines
.split(&[';', ',', ' '][..])
.any(|define| matches!(define, "V8_ENABLE_SANDBOX" | "V8_ENABLE_SANDBOX=1"));
if sandbox_define {
panic!(
"V8_ENABLE_SANDBOX is unsupported: N-API backing stores can reside outside the V8 sandbox"
);
}
// The bridge exposes Wasmer linear memory and other embedder-owned
// allocations as V8 ArrayBuffer backing stores. V8's sandbox requires
// every such store to reside inside its sandbox address range. Until the
// bridge has a sandbox-resident backing-store implementation, accepting
// this archive would make ordinary guest code abort the host process.
if read_sandbox_build_flag(&v8.library_path) == Some(true) {
panic!(
"sandbox-enabled V8 archives are unsupported: N-API backing stores can reside outside the V8 sandbox"
);
}

let mut build = cc::Build::new();
build
Expand Down Expand Up @@ -225,6 +249,49 @@ fn main() {
}
}

fn standalone_legacy_wait() -> bool {
let manifest_dir =
PathBuf::from(env::var("CARGO_MANIFEST_DIR").expect("CARGO_MANIFEST_DIR is not set"));
let manifest = fs::read_to_string(manifest_dir.join("Cargo.toml"))
.expect("failed to read N-API Cargo.toml");
let mut in_napi_metadata = false;
for line in manifest.lines() {
let line = line.trim();
if line.starts_with('[') {
in_napi_metadata = line == "[package.metadata.napi]";
} else if in_napi_metadata {
let declaration = line.split('#').next().unwrap_or("").trim();
if let Some((key, value)) = declaration.split_once('=')
&& key.trim() == "standalone_legacy_wait"
{
return value.trim() == "true";
}
}
}
false
}

fn read_sandbox_build_flag(library_path: &Path) -> Option<bool> {
let config_path = library_path.parent()?.parent()?.join("build-config.txt");
println!("cargo:rerun-if-changed={}", config_path.display());
let config = match std::fs::read_to_string(&config_path) {
Ok(config) => config,
Err(err) if err.kind() == std::io::ErrorKind::NotFound => return None,
Err(err) => panic!("failed to read {}: {err}", config_path.display()),
};
match config
.lines()
.find_map(|line| line.strip_prefix("v8_enable_sandbox="))
{
Some("true") => Some(true),
Some("false") => Some(false),
_ => panic!(
"{} lacks a valid v8_enable_sandbox flag",
config_path.display()
),
}
}

fn resolve_v8_config() -> Result<V8Config, String> {
let include_override = read_env_value(
"NAPI_V8_INCLUDE_DIR",
Expand Down
1 change: 1 addition & 0 deletions cargo-standalone.sh
Original file line number Diff line number Diff line change
Expand Up @@ -71,4 +71,5 @@ for entry in build.rs include lib src tests v8; do
ln -s "$repo_root/$entry" "$standalone_work_dir/$entry"
done

export NAPI_SOURCE_ROOT="$repo_root"
"${cargo_cmd[@]}" "$subcommand" --manifest-path "$standalone_work_dir/Cargo.toml" "$@"
56 changes: 56 additions & 0 deletions quickjs/src/internal/napi_module_wrap.cc
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
#include <new>
#include <sstream>
#include <string>
#include <unordered_set>
#include <utility>
#include <vector>

Expand Down Expand Up @@ -217,6 +218,19 @@ void napi_module_wrap__::free_record(record *entry)
if (entry == nullptr)
return;

// Links store record pointers for duplicate-request checks. Clear incoming
// links before freeing a record so a parent cannot instantiate through a
// destroyed dependency or mistake a later allocation for the old record.
for (record *dependent : records_)
{
if (dependent == entry)
continue;
for (record *&linked : dependent->linked_records)
{
if (linked == entry)
linked = nullptr;
}
}
remove(entry);
for (auto &request : entry->requests)
JS_FreeValue(ctx_, request.attributes);
Expand Down Expand Up @@ -689,11 +703,44 @@ napi_status napi_module_wrap__::link(unofficial_napi_module module,
return napi_ok;
}

napi_status napi_module_wrap__::validate_linked_graph(record *root)
{
// QuickJS can link implicitly while evaluating a module or a require()
// facade. Verify the whole reachable, not-yet-linked graph before any of
// those entry points follow request pointers. Keep already-linked modules
// usable after their wrapper handles have been destroyed.
std::unordered_set<record *> live(records_.begin(), records_.end());
std::unordered_set<record *> visited;
std::vector<record *> pending{root};
while (!pending.empty())
{
record *entry = pending.back();
pending.pop_back();
if (!live.contains(entry))
return throw_error("ERR_VM_MODULE_LINK_FAILURE", "Linked module is invalid");
if (!visited.insert(entry).second ||
JS_GetModuleStatus(ctx_, entry->module) != JS_MODULE_STATUS_UNLINKED)
continue;
if (entry->linked_records.size() != entry->requests.size())
return throw_error("ERR_VM_MODULE_LINK_FAILURE", "Linked module is incomplete");
for (record *linked : entry->linked_records)
{
if (!live.contains(linked))
return throw_error("ERR_VM_MODULE_LINK_FAILURE", "Linked module is invalid");
pending.push_back(linked);
}
}
return napi_ok;
}

napi_status napi_module_wrap__::instantiate(unofficial_napi_module module)
{
record *entry = find(module);
if (entry == nullptr)
return napi_util__::invalid_arg(env_);
napi_status validation = validate_linked_graph(entry);
if (validation != napi_ok)
return validation;
if (JS_LinkModule(ctx_, entry->module) < 0)
return return_pending_exception("Module linking failed");
return napi_ok;
Expand All @@ -709,6 +756,9 @@ napi_status napi_module_wrap__::evaluate(unofficial_napi_module module,
record *entry = find(module);
if (entry == nullptr || result_out == nullptr)
return napi_util__::invalid_arg(env_);
napi_status validation = validate_linked_graph(entry);
if (validation != napi_ok)
return validation;
JSValue promise = JS_EvaluateModule(ctx_, entry->module);
if (JS_IsException(promise))
return return_pending_exception("Failed to evaluate module");
Expand All @@ -725,6 +775,9 @@ napi_status napi_module_wrap__::evaluate_sync(unofficial_napi_module module,
record *entry = find(module);
if (entry == nullptr || result_out == nullptr)
return napi_util__::invalid_arg(env_);
napi_status validation = validate_linked_graph(entry);
if (validation != napi_ok)
return validation;

JSValue promise = JS_EvaluateModule(ctx_, entry->module);
if (JS_IsException(promise))
Expand Down Expand Up @@ -1013,6 +1066,9 @@ napi_status napi_module_wrap__::create_required_module_facade(unofficial_napi_mo
record *entry = find(module);
if (entry == nullptr || result_out == nullptr)
return napi_util__::invalid_arg(env_);
napi_status validation = validate_linked_graph(entry);
if (validation != napi_ok)
return validation;

std::string filename = "<required-module-facade:" +
std::to_string(++facade_counter_) + ">";
Expand Down
1 change: 1 addition & 0 deletions quickjs/src/internal/napi_module_wrap.h
Original file line number Diff line number Diff line change
Expand Up @@ -104,6 +104,7 @@ class napi_module_wrap__
record *find_by_wrapper(JSValueConst wrapper) const;
void remove(record *entry);
void free_record(record *entry);
napi_status validate_linked_graph(record *root);

JSValue get_host_defined_option_symbol() const;
JSValue get_symbols_binding_property(const char *name) const;
Expand Down
2 changes: 1 addition & 1 deletion rust-toolchain.toml
Original file line number Diff line number Diff line change
@@ -1,2 +1,2 @@
[toolchain]
channel = "1.95"
channel = "1.96"
Loading
Loading