Skip to content
Merged
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
2 changes: 1 addition & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[workspace]
resolver = "2"
members = ["metadata", "playground", "runtime", "runtime-binding-gen", "sbg", "nativescript", "typings-generator", "integration-tests", "runtime-devtools", "metadata-generator","tools/dotnet-tool", "windows-napi"]
members = ["metadata", "playground", "runtime", "runtime-binding-gen", "sbg", "nativescript", "typings-generator", "integration-tests", "runtime-devtools", "metadata-generator","tools/dotnet-tool", "windows-napi", "napi-v8-shim"]
# Excluded so their C builds / prebuilt-engine links don't run on normal `cargo` invocations.
exclude = ["packages/common", "packages/demo", "packages/windows-quickjs", "packages/windows-hermes", "packages/windows-jsc", "packages/windows-v8"]

Expand Down
38 changes: 32 additions & 6 deletions integration-tests/tests/new_features.rs
Original file line number Diff line number Diff line change
Expand Up @@ -137,6 +137,8 @@ fn url_relative_resolution() {
#[test]
fn raf_fires_callback_with_positive_timestamp() {
let mut rt = Runtime::new(".");
// What an app host does after creating the runtime; the frame pump finds the isolate here.
rt.register_delegate_isolate_ptr();
// Register the callback.
rt.run_script(
r#"
Expand All @@ -150,9 +152,8 @@ fn raf_fires_callback_with_positive_timestamp() {
"setup.js",
);

// Drain microtasks: __nsDwmFlush returns immediately on headless (no DWM)
// or waits one VSync on a live display. Either way the callback runs.
rt.run_script("", "pump.js");
// One frame: the host's pump (runtime_pump_timers) runs requested callbacks.
assert!(runtime::animation_frames::pump(), "no frame was requested");

assert_js(&mut rt, "_rafFired === true", "rAF callback not fired");
assert_js(&mut rt, "_rafTs >= 0", "rAF timestamp negative");
Expand All @@ -161,6 +162,8 @@ fn raf_fires_callback_with_positive_timestamp() {
#[test]
fn raf_callback_receives_increasing_timestamps() {
let mut rt = Runtime::new(".");
// What an app host does after creating the runtime; the frame pump finds the isolate here.
rt.register_delegate_isolate_ptr();
rt.run_script(
r#"
var _ts1 = -1, _ts2 = -1;
Expand All @@ -171,15 +174,38 @@ fn raf_callback_receives_increasing_timestamps() {
"#,
"setup.js",
);
rt.run_script("", "pump1.js");
rt.run_script("", "pump2.js");
runtime::animation_frames::pump();
// Requested during the first frame: runs in the second, not the first.
assert_js(&mut rt, "_ts2 === -1", "nested rAF ran in the same frame");
runtime::animation_frames::pump();
assert_js(&mut rt, "_ts1 >= 0", "first rAF timestamp invalid");
assert_js(&mut rt, "_ts2 >= _ts1", "second rAF not >= first");
}

#[test]
fn raf_does_not_run_without_a_frame() {
let mut rt = Runtime::new(".");
// What an app host does after creating the runtime; the frame pump finds the isolate here.
rt.register_delegate_isolate_ptr();
rt.run_script(
r#"
var _early = false;
requestAnimationFrame(function() { _early = true; });
Promise.resolve().then(function() {});
"#,
"setup.js",
);
// Microtasks drained, no pump yet: the UI thread was never held for a frame.
assert_js(&mut rt, "_early === false", "rAF ran before the frame");
runtime::animation_frames::pump();
assert_js(&mut rt, "_early === true", "rAF did not run on the frame");
}

#[test]
fn cancel_raf_prevents_callback() {
let mut rt = Runtime::new(".");
// What an app host does after creating the runtime; the frame pump finds the isolate here.
rt.register_delegate_isolate_ptr();
rt.run_script(
r#"
var _called = false;
Expand All @@ -188,7 +214,7 @@ fn cancel_raf_prevents_callback() {
"#,
"setup.js",
);
rt.run_script("", "pump.js");
runtime::animation_frames::pump();
assert_js(
&mut rt,
"_called === false",
Expand Down
13 changes: 13 additions & 0 deletions napi-v8-shim/Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
[package]
name = "napi-v8-shim"
version = "0.1.0"
edition = "2021"
description = "Node-API over the classic engine's V8: napi-android's v8-api.cpp (vendored in packages/windows-v8) compiled against rusty_v8, plus the env/registration glue the native-addon loader needs."

[dependencies]
# The shim's C++ calls V8; this keeps rusty_v8's static library (and its headers, which build.rs
# finds in the registry source) in the link.
v8 = "147"

[build-dependencies]
cc = "1"
71 changes: 71 additions & 0 deletions napi-v8-shim/build.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
//! Compiles napi-android's V8 Node-API shim (`packages/windows-v8/vendor/shim/v8-api.cpp`, shared
//! with the `windows-v8` engine package) and `csrc/env_ext.cpp` against the `v8` crate's V8 14.7
//! headers, with the same settings `packages/windows-v8/build.rs` validated.
//!
//! Linked whole-archive: the shim's `napi_*` functions are `__declspec(dllexport)`, and the
//! native-addon loader needs every one of them in `nativescript.dll`'s export table even though
//! Rust never names most of them.
use std::path::{Path, PathBuf};

fn find_v8_include() -> PathBuf {
let cargo_home = std::env::var("CARGO_HOME").map(PathBuf::from).unwrap_or_else(|_| {
let home = std::env::var("USERPROFILE").or_else(|_| std::env::var("HOME")).unwrap();
Path::new(&home).join(".cargo")
});
let src = cargo_home.join("registry").join("src");
if let Ok(indexes) = std::fs::read_dir(&src) {
for idx in indexes.flatten() {
if let Ok(crates) = std::fs::read_dir(idx.path()) {
let mut hits: Vec<PathBuf> = crates
.flatten()
.map(|e| e.path())
.filter(|p| {
p.file_name()
.and_then(|n| n.to_str())
.map(|n| n.starts_with("v8-147."))
.unwrap_or(false)
&& p.join("v8/include/v8.h").exists()
})
.collect();
hits.sort();
if let Some(p) = hits.pop() {
return p.join("v8/include");
}
}
}
}
panic!("could not locate the v8 crate's include dir (v8-147.x/v8/include) under {src:?}");
}

fn main() {
let manifest = PathBuf::from(std::env::var("CARGO_MANIFEST_DIR").unwrap());
let vendor = manifest.join("..").join("packages").join("windows-v8").join("vendor");

let mut b = cc::Build::new();
b.cpp(true)
.include(find_v8_include())
.include(vendor.join("shim"))
.include(vendor.join("napi"))
.include(vendor.join("compat"))
.file(vendor.join("shim/v8-api.cpp"))
.file(manifest.join("csrc/env_ext.cpp"))
.define("NAPI_VERSION", "8")
// V8 14.7 (>13): the shim's modern code paths (SetAccessorProperty etc.).
.define("__V8_13__", None)
.std("c++20")
.warnings(false)
.link_lib_modifier("+whole-archive");
if b.get_compiler().is_like_msvc() {
b.flag("/EHsc")
.flag("/Zc:__cplusplus")
.define("NOMINMAX", None)
.define("WIN32_LEAN_AND_MEAN", None)
.define("_SILENCE_CXX17_CODECVT_HEADER_DEPRECATION_WARNING", None)
.define("_CRT_SECURE_NO_WARNINGS", None);
}
b.compile("napi_v8_shim");

println!("cargo:rerun-if-changed=csrc/env_ext.cpp");
println!("cargo:rerun-if-changed=../packages/windows-v8/vendor/shim/v8-api.cpp");
println!("cargo:rerun-if-changed=../packages/windows-v8/vendor/shim/v8-api.h");
}
67 changes: 67 additions & 0 deletions napi-v8-shim/csrc/env_ext.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
// Glue between the classic engine (rusty_v8, Rust) and the vendored Node-API shim (v8-api.cpp):
// create an env over an existing context, run a module's init inside the env's call guard, and
// drain finalizers the way Node does (outside GC, at a point where calling into JS is safe).

#include <cstring>

#include "js_native_api.h"
#include "v8-api.h"

// rusty_v8 hands us a `v8::Local` as the pointer it wraps; that only round-trips if a Local is a
// single pointer (true without V8_ENABLE_DIRECT_HANDLE, which the v8 crate's default build lacks).
static_assert(sizeof(v8::Local<v8::Context>) == sizeof(void*), "v8::Local must be pointer-sized");

// From node_api_types.h, which the vendored headers don't carry.
typedef napi_value (*napi_addon_register_func)(napi_env env, napi_value exports);

extern "C" {

// `context` is a `v8::Local<v8::Context>` (as its underlying pointer) of the isolate current on
// this thread. `module_api_version` is the addon's declared Node-API version.
napi_env ns_napi_env_create(void* context, int32_t module_api_version) {
v8::Local<v8::Context> local;
std::memcpy(static_cast<void*>(&local), &context, sizeof(void*));
return new napi_env__(local, module_api_version);
}

// Calls `init(env, exports)` inside the env's call guard; an exception it throws is left pending
// on the isolate for the caller's TryCatch. Must run in a HandleScope with the env's context entered.
napi_value ns_napi_call_module_init(napi_env env, napi_addon_register_func init, napi_value exports) {
napi_value result = nullptr;
env->CallIntoModule([&](napi_env env) { result = init(env, exports); });
return result;
}

// Runs finalizers the GC queued for `env` (module API versions below "experimental" defer them;
// see napi_env__::InvokeFinalizerFromGC). Node's `DrainFinalizerQueue`.
void ns_napi_drain_finalizers(napi_env env) {
if (env->pending_finalizers.empty()) {
return;
}
v8::HandleScope handle_scope(env->isolate);
// env->context() aliases the env's persistent slot; take a real handle for the scope.
v8::Local<v8::Context> context = v8::Local<v8::Context>::New(env->isolate, env->context());
v8::Context::Scope context_scope(context);
while (!env->pending_finalizers.empty()) {
v8impl::RefTracker* tracker = *env->pending_finalizers.begin();
env->pending_finalizers.erase(tracker);
tracker->Finalize();
}
}

bool ns_napi_has_pending_finalizers(napi_env env) {
return !env->pending_finalizers.empty();
}

// Tears the env down: finalizes remaining references (running their finalizers) and frees it.
void ns_napi_env_teardown(napi_env env) {
v8::Isolate* isolate = env->isolate;
v8::HandleScope handle_scope(isolate);
// A real handle, not env->context(): that aliases the env's persistent slot, which DeleteMe
// frees before this scope exits.
v8::Local<v8::Context> context = v8::Local<v8::Context>::New(isolate, env->context());
v8::Context::Scope context_scope(context);
env->DeleteMe();
}

} // extern "C"
21 changes: 21 additions & 0 deletions napi-v8-shim/src/lib.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
//! Node-API over the classic engine's V8. The `napi_*` engine functions (js_native_api) come from
//! the compiled shim and are exported from the final DLL; the Node-specific ones (threadsafe
//! functions, async work, cleanup hooks, buffers, …) live in `runtime::node_api`.

use std::ffi::c_void;

// Keeps rusty_v8's static library in the link for the C++ shim.
extern crate v8;

pub type NapiEnv = *mut c_void;
pub type NapiValue = *mut c_void;
pub type AddonInit = unsafe extern "C" fn(NapiEnv, NapiValue) -> NapiValue;

extern "C" {
/// `context` is the pointer inside a `v8::Local<v8::Context>` of the current isolate.
pub fn ns_napi_env_create(context: *const c_void, module_api_version: i32) -> NapiEnv;
pub fn ns_napi_call_module_init(env: NapiEnv, init: AddonInit, exports: NapiValue) -> NapiValue;
pub fn ns_napi_drain_finalizers(env: NapiEnv);
pub fn ns_napi_has_pending_finalizers(env: NapiEnv) -> bool;
pub fn ns_napi_env_teardown(env: NapiEnv);
}
12 changes: 11 additions & 1 deletion nativescript/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -240,6 +240,8 @@ pub extern "C" fn runtime_init(app_root: *const c_char) -> i64 {
pub extern "C" fn runtime_deinit(runtime: i64) {
if runtime != 0 {
let _ = std::panic::catch_unwind(|| {
// Native addons' cleanup hooks and envs go first, while the isolate is still alive.
runtime::node_api::teardown();
let runtime: *mut Runtime = runtime as _;
let _ = unsafe { Box::from_raw(runtime) };
});
Expand Down Expand Up @@ -420,6 +422,10 @@ pub extern "C" fn runtime_devtools_pump(_runtime: i64) {
pub extern "C" fn runtime_pump_timers() {
let _ = std::panic::catch_unwind(|| {
runtime::timers::pump();
// Native addons: threadsafe-function calls, async-work completions, deferred finalizers.
runtime::node_api::drain();
// This frame's requestAnimationFrame callbacks, then their microtasks.
runtime::animation_frames::pump();
if runtime::ui_dispatcher::needs_win32_pump() {
runtime::pump_messages();
} else {
Expand All @@ -440,7 +446,11 @@ pub extern "C" fn runtime_pump_timers() {
/// Returns `true` if at least one Win32 message was dispatched.
#[no_mangle]
pub extern "C" fn runtime_pump_messages() -> bool {
std::panic::catch_unwind(|| runtime::pump_messages()).unwrap_or(false)
std::panic::catch_unwind(|| {
runtime::node_api::drain();
runtime::pump_messages()
})
.unwrap_or(false)
}

/// Free a string previously returned by `runtime_devtools_start`.
Expand Down
6 changes: 2 additions & 4 deletions packages/windows-v8/vendor/shim/v8-api-internals.h
Original file line number Diff line number Diff line change
Expand Up @@ -101,9 +101,7 @@ class PersistentToLocal {
#define CHECK_LE(a, b) CHECK((a) <= (b))
#endif

// [BABYLON-NATIVE-ADDITION]: Increase perf by using internal field instead of private property
// [windows port] V8 14.7 removed Context::GetIsolate(); the host has the isolate entered.
#define NAPI_PRIVATE_KEY(context) \
(v8::Private::New(v8::Isolate::GetCurrent()))
// [windows port] Type tags use napi_env__::TypeTagKey(): a stable private symbol. (This macro used
// to create a new, unique private on every call, so a tag could never be read back.)

#endif // SRC_JS_NATIVE_API_V8_INTERNALS_H_
4 changes: 2 additions & 2 deletions packages/windows-v8/vendor/shim/v8-api.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -2592,7 +2592,7 @@ napi_status NAPI_CDECL napi_type_tag_object(napi_env env,
CHECK_TO_OBJECT_WITH_PREAMBLE(env, context, obj, object);
CHECK_ARG_WITH_PREAMBLE(env, type_tag);

auto key = NAPI_PRIVATE_KEY(context);
auto key = env->TypeTagKey();
auto maybe_has = obj->HasPrivate(context, key);
CHECK_MAYBE_NOTHING_WITH_PREAMBLE(env, maybe_has, napi_generic_failure);
RETURN_STATUS_IF_FALSE_WITH_PREAMBLE(
Expand Down Expand Up @@ -2622,7 +2622,7 @@ napi_status NAPI_CDECL napi_check_object_type_tag(napi_env env,
CHECK_ARG_WITH_PREAMBLE(env, result);

auto maybe_value =
obj->GetPrivate(context, NAPI_PRIVATE_KEY(context));
obj->GetPrivate(context, env->TypeTagKey());
CHECK_MAYBE_EMPTY_WITH_PREAMBLE(env, maybe_value, napi_generic_failure);
v8::Local<v8::Value> val = maybe_value.ToLocalChecked();

Expand Down
10 changes: 10 additions & 0 deletions packages/windows-v8/vendor/shim/v8-api.h
Original file line number Diff line number Diff line change
Expand Up @@ -195,6 +195,16 @@ struct napi_env__ {
v8::Isolate* const isolate; // Shortcut for context()->GetIsolate()
v8impl::Persistent<v8::Context> context_persistent;

// The private symbol type tags are stored under. It must be the same symbol for tagging and
// checking, so it is created once (Private::ForApi) and kept for the env's lifetime.
v8::Global<v8::Private> type_tag_key;
inline v8::Local<v8::Private> TypeTagKey() {
if (type_tag_key.IsEmpty()) {
type_tag_key.Reset(isolate, v8::Private::ForApi(isolate, v8::String::NewFromUtf8Literal(isolate, "napi:type_tag")));
}
return type_tag_key.Get(isolate);
}

v8impl::Persistent<v8::Value> last_exception;
// Cache the template for NapiHostObject
v8::Persistent<v8::ObjectTemplate> host_object_template;
Expand Down
4 changes: 3 additions & 1 deletion runtime/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@ anyhow = "1.0"
libffi = "5.1.0"
# The classic engine (`classic` feature). The napi engines build without it and never link V8.
v8 = { version = "147", optional = true }
# Node-API over the classic engine's V8, for loading native addons (`runtime::native_addons`).
napi-v8-shim = { path = "../napi-v8-shim", optional = true }
chrono = "0.4"
serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0"
Expand Down Expand Up @@ -73,7 +75,7 @@ features = [
default = ["classic"]
# The classic engine: WinRT interop written against rusty_v8, which `nativescript.dll` is built on.
# The napi engine packages turn default features off, so they don't compile or link V8.
classic = ["dep:v8"]
classic = ["dep:v8", "dep:napi-v8-shim"]
devtools = ["classic", "runtime-devtools"]
napi_engine = ["dep:napi"]
# ES modules for napi engines without a native module loader (QuickJS, Hermes, JSC): modules are
Expand Down
Loading
Loading