Skip to content

Commit 2bfb275

Browse files
authored
feat(runtime): add node:vm engine parity and CLI memory semantics coverage (#30)
* feat(runtime): add node:vm and harden QuickJS/V8 runtime parity Implement a NativeScript-backed node:vm builtin and land the runtime fixes needed to keep both V8 and QuickJS aligned across CommonJS, ESM, and runtime execution paths. Node vm support - add the NativeScript-backed vm runtime module and expose require("vm") / require("node:vm") from the internal Node loader - implement vm.createContext(), vm.isContext(), runInContext(), runInNewContext(), runInThisContext(), Script, compileFunction(), SourceTextModule, SyntheticModule, measureMemory(), and vm.constants - preserve caller-provided receivers in vm.compileFunction() while keeping plain calls strict so they do not leak the ambient global object into parsing contexts ESM and module loader parity - add builtin ESM bridges for node:vm in both the V8 and QuickJS loaders so dynamic import("node:vm") resolves the same surface as require("node:vm") - initialize the QuickJS ES module loader during module bootstrap and resolve builtin/native modules through virtual module sources with import.meta support - extend vm CLI coverage to assert compileFunction receiver semantics and dynamic import("node:vm") on both engines - move the dynamic import coverage earlier in the CLI test to avoid a separate QuickJS logging quirk triggered after the module-evaluation block QuickJS runtime and N-API fixes - preserve exact script length when evaluating strings with embedded NUL bytes - fix plain-call constructor handling in napi_define_class() - switch internal wrap/type-tag storage to symbol-backed properties - fix uint32/uint64 creation paths and string extraction behavior - retain weakref targets until the host job completes - normalize QuickJS property access errors to modern message text Objective-C bridge and marshalling fixes - thread napi_env through Closure construction - harden bridge object lifetime/ref management and FunctionReference branding - add Float16 conversion support and correct metadata encoding for UInt8 URL and web runtime fixes - fix URL-backed URLSearchParams creation so V8 method dispatch works correctly - reject invalid fetch inputs via promise rejection and clean up abort listeners Verification - macOS CLI vm test on V8: pass - macOS CLI vm test on QuickJS: pass - full runtime suites were previously exercised on macOS/iOS for both engines * feat(runtime): add Hermes parity for vm and runtime tests - integrate prebuilt Hermes artifact downloads into the local build flow and macOS test harness - switch the Hermes N-API runtime over to the thread-safe runtime path and drain microtasks fully - harden bridge constructors, pointer/reference handling, closure teardown, timers, URL constructors, and object conversion for Hermes semantics - add Hermes-compatible node:vm shims, keep compileFunction receiver behavior, and expose node:vm through the builtin ESM bridge - extend runtime tests for vm import/receiver coverage and relax engine-specific expectations where Hermes lifetime behavior differs but runtime state is verified * fix jsc coverage regressions Attach struct type encodings directly to generated struct constructors so JSC can resolve record types consistently during reference construction. Stabilize the timer cleanup coverage by exposing the native active timer count and using that on iOS JSC instead of relying on weak reference collection timing. Also replace NSString version comparison in VersionDiffTests with engine-agnostic numeric parsing so the coverage path no longer depends on platform-specific compare helpers. * feat runtime engine parity and memory semantics coverage Add the remaining runtime engine work across JSC, QuickJS, and the Objective-C bridge, including the node:vm implementation, builtin module resolution updates, object/class/reference marshalling fixes, and engine-specific Node-API hardening. Also add the CLI memory semantics suite and harness updates covering weak references, finalization, Objective-C ownership rules, block callbacks, C function pointers, pointer buffers, and reference lifecycle behavior so the runtime’s memory model is exercised from JavaScript.
1 parent 09dc282 commit 2bfb275

72 files changed

Lines changed: 9839 additions & 5860 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

NativeScript/CMakeLists.txt

Lines changed: 19 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,8 @@ set(CMAKE_CXX_STANDARD 20)
1111

1212
set(BUILD_FRAMEWORK TRUE)
1313

14+
set(HERMES_XCFRAMEWORK "${CMAKE_SOURCE_DIR}/../Frameworks/hermes.xcframework")
15+
1416
set(COMMON_FLAGS "-O3 -Wno-shorten-64-to-32")
1517

1618
set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} ${COMMON_FLAGS}")
@@ -48,6 +50,9 @@ elseif(TARGET_PLATFORM STREQUAL "ios-sim")
4850
set(SDK_NAME "iphonesimulator")
4951
set(CMAKE_OSX_ARCHITECTURES "arm64")
5052
set(TARGET_PLATFORM_SPEC "ios-arm64-simulator")
53+
if(EXISTS "${HERMES_XCFRAMEWORK}/ios-arm64_x86_64-simulator")
54+
set(TARGET_PLATFORM_SPEC "ios-arm64_x86_64-simulator")
55+
endif()
5156

5257
elseif(TARGET_PLATFORM STREQUAL "macos")
5358
set(CMAKE_XCODE_ATTRIBUTE_MACOSX_DEPLOYMENT_TARGET "13.3")
@@ -184,13 +189,24 @@ if(ENABLE_JS_RUNTIME)
184189
)
185190

186191
elseif(TARGET_ENGINE_HERMES)
192+
set(HERMES_HEADERS_DIR "${CMAKE_CURRENT_SOURCE_DIR}/../Frameworks/hermes-headers")
193+
187194
include_directories(
188195
napi/hermes
189-
napi/hermes/include
190-
napi/hermes/include/hermes
191-
napi/hermes/include/jsi
192196
)
193197

198+
if(EXISTS "${HERMES_HEADERS_DIR}/jsi/jsi.h" AND EXISTS "${HERMES_HEADERS_DIR}/hermes/hermes.h")
199+
include_directories(
200+
${HERMES_HEADERS_DIR}
201+
)
202+
else()
203+
include_directories(
204+
napi/hermes/include
205+
napi/hermes/include/hermes
206+
napi/hermes/include/jsi
207+
)
208+
endif()
209+
194210
set(SOURCE_FILES
195211
${SOURCE_FILES}
196212
napi/hermes/jsr.cpp

NativeScript/ffi/Block.mm

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -231,7 +231,7 @@ id registerBlock(napi_env env, Closure* closure, napi_value callback) {
231231
if (napiSupportsThreadsafeFunctions(bridgeState->self_dl)) {
232232
napi_value workName;
233233
napi_create_string_utf8(env, "Block", NAPI_AUTO_LENGTH, &workName);
234-
napi_create_threadsafe_function(env, callback, nullptr, workName, 0, 1, nullptr, nullptr,
234+
napi_create_threadsafe_function(env, nullptr, nullptr, workName, 0, 1, nullptr, nullptr,
235235
closure, Closure::callBlockFromMainThread, &closure->tsfn);
236236
if (closure->tsfn) napi_unref_threadsafe_function(env, closure->tsfn);
237237
}
@@ -301,8 +301,7 @@ bool isObjCBlockObject(id obj) {
301301

302302
napi_value callback = argv[1];
303303

304-
auto closure = new Closure(enc, true);
305-
closure->env = env;
304+
auto closure = new Closure(env, enc, true);
306305
registerBlock(env, closure, callback);
307306

308307
return callback;
@@ -412,6 +411,9 @@ bool isObjCBlockObject(id obj) {
412411

413412
void FunctionPointer::finalize(napi_env env, void* finalize_data, void* finalize_hint) {
414413
auto ref = (FunctionPointer*)finalize_data;
414+
if (ref == nullptr) {
415+
return;
416+
}
415417
if (ref->ownsCif && ref->cif != nullptr) {
416418
delete ref->cif;
417419
ref->cif = nullptr;

NativeScript/ffi/CFunction.mm

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -164,8 +164,7 @@ inline napi_value tryCallCompatLibdispatchFunction(napi_env env, napi_callback_i
164164
return nullptr;
165165
}
166166

167-
auto closure = new Closure(std::string("v"), true);
168-
closure->env = env;
167+
auto closure = new Closure(env, std::string("v"), true);
169168
id block = registerBlock(env, closure, argv[1]);
170169
dispatch_block_t dispatchBlock = (dispatch_block_t)block;
171170

NativeScript/ffi/Cif.mm

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,8 @@ inline bool typeRequiresSlowGeneratedNapiDispatch(const std::shared_ptr<TypeConv
6464
switch (type->kind) {
6565
case mdTypeUChar:
6666
case mdTypeUInt8:
67+
case mdTypeBlock:
68+
case mdTypeFunctionPointer:
6769
return true;
6870
default:
6971
return false;

NativeScript/ffi/Class.mm

Lines changed: 189 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -246,7 +246,13 @@ void setupObjCClassDecorator(napi_env env) {
246246
napi_get_cb_info(env, cbinfo, nullptr, nullptr, &jsThis, nullptr);
247247

248248
Class currentClass = nil;
249-
napi_unwrap(env, jsThis, (void**)&currentClass);
249+
auto bridgeState = ObjCBridgeState::InstanceData(env);
250+
if (bridgeState != nullptr && jsThis != nullptr) {
251+
bridgeState->tryResolveBridgedClassConstructor(env, jsThis, &currentClass);
252+
}
253+
if (currentClass == nil) {
254+
napi_unwrap(env, jsThis, (void**)&currentClass);
255+
}
250256
if (currentClass == nil) {
251257
return nullptr;
252258
}
@@ -256,7 +262,6 @@ void setupObjCClassDecorator(napi_env env) {
256262
return nullptr;
257263
}
258264

259-
auto bridgeState = ObjCBridgeState::InstanceData(env);
260265
auto find = bridgeState->classesByPointer.find(superClass);
261266
if (find != bridgeState->classesByPointer.end()) {
262267
return get_ref_value(env, find->second->constructor);
@@ -268,12 +273,103 @@ void setupObjCClassDecorator(napi_env env) {
268273
return get_ref_value(env, bridgedClass->constructor);
269274
}
270275

276+
const char* runtimeName = class_getName(superClass);
277+
if (runtimeName != nullptr && runtimeName[0] != '\0') {
278+
napi_value global = nullptr;
279+
napi_value constructor = nullptr;
280+
bool hasGlobal = false;
281+
napi_get_global(env, &global);
282+
if (napi_has_named_property(env, global, runtimeName, &hasGlobal) == napi_ok && hasGlobal &&
283+
napi_get_named_property(env, global, runtimeName, &constructor) == napi_ok &&
284+
constructor != nullptr) {
285+
return constructor;
286+
}
287+
}
288+
271289
return bridgeState->getObject(env, (id)superClass, kUnownedObject, 0, nullptr);
272290
}
273291

292+
NAPI_FUNCTION(classHasInstance) {
293+
size_t argc = 1;
294+
napi_value argv[1] = {nullptr};
295+
napi_value jsThis = nullptr;
296+
napi_get_cb_info(env, cbinfo, &argc, argv, &jsThis, nullptr);
297+
298+
Class expectedClass = nil;
299+
auto bridgeState = ObjCBridgeState::InstanceData(env);
300+
if (bridgeState != nullptr && jsThis != nullptr) {
301+
bridgeState->tryResolveBridgedClassConstructor(env, jsThis, &expectedClass);
302+
}
303+
if (expectedClass == nil) {
304+
napi_unwrap(env, jsThis, (void**)&expectedClass);
305+
}
306+
307+
bool isInstance = false;
308+
if (expectedClass != nil && argc > 0 && argv[0] != nullptr) {
309+
napi_valuetype valueType = napi_undefined;
310+
if (napi_typeof(env, argv[0], &valueType) == napi_ok &&
311+
(valueType == napi_object || valueType == napi_function)) {
312+
id instance = nil;
313+
if (napi_unwrap(env, argv[0], (void**)&instance) != napi_ok || instance == nil) {
314+
napi_value nativePointer = nullptr;
315+
if (napi_get_named_property(env, argv[0], "__ns_native_ptr", &nativePointer) == napi_ok &&
316+
Pointer::isInstance(env, nativePointer)) {
317+
Pointer* pointer = Pointer::unwrap(env, nativePointer);
318+
instance = pointer != nullptr ? static_cast<id>(pointer->data) : nil;
319+
}
320+
}
321+
322+
if (instance != nil) {
323+
Class currentClass = object_getClass(instance);
324+
while (currentClass != nil) {
325+
if (currentClass == expectedClass) {
326+
isInstance = true;
327+
break;
328+
}
329+
currentClass = class_getSuperclass(currentClass);
330+
}
331+
}
332+
}
333+
}
334+
335+
napi_value result = nullptr;
336+
napi_get_boolean(env, isInstance, &result);
337+
return result;
338+
}
339+
274340
NAPI_FUNCTION(BridgedConstructor) {
275341
NAPI_CALLBACK_BEGIN(16)
276342

343+
napi_value newTarget = nullptr;
344+
napi_get_new_target(env, cbinfo, &newTarget);
345+
346+
napi_valuetype thisType = napi_undefined;
347+
if (jsThis == nullptr || napi_typeof(env, jsThis, &thisType) != napi_ok ||
348+
(thisType != napi_object && thisType != napi_function)) {
349+
napi_create_object(env, &jsThis);
350+
351+
napi_value prototypeOwner = newTarget;
352+
if (prototypeOwner == nullptr || napi_typeof(env, prototypeOwner, &thisType) != napi_ok ||
353+
(thisType != napi_function && thisType != napi_object)) {
354+
prototypeOwner = nullptr;
355+
}
356+
357+
if (prototypeOwner != nullptr) {
358+
napi_value prototype = nullptr;
359+
if (napi_get_named_property(env, prototypeOwner, "prototype", &prototype) == napi_ok &&
360+
prototype != nullptr) {
361+
napi_value global = nullptr;
362+
napi_value objectCtor = nullptr;
363+
napi_value setPrototypeOf = nullptr;
364+
napi_get_global(env, &global);
365+
napi_get_named_property(env, global, "Object", &objectCtor);
366+
napi_get_named_property(env, objectCtor, "setPrototypeOf", &setPrototypeOf);
367+
napi_value setPrototypeArgs[2] = {jsThis, prototype};
368+
napi_call_function(env, objectCtor, setPrototypeOf, 2, setPrototypeArgs, nullptr);
369+
}
370+
}
371+
}
372+
277373
napi_valuetype jsType = napi_undefined;
278374
if (argc > 0) {
279375
napi_typeof(env, argv[0], &jsType);
@@ -282,14 +378,32 @@ void setupObjCClassDecorator(napi_env env) {
282378
id object = nil;
283379

284380
ObjCBridgeState* bridgeState = ObjCBridgeState::InstanceData(env);
381+
auto ensureWrappedThis = [&](id nativeObject) {
382+
if (jsThis == nullptr || nativeObject == nil) {
383+
return;
384+
}
385+
386+
void* existingWrapped = nullptr;
387+
if (napi_unwrap(env, jsThis, &existingWrapped) == napi_ok && existingWrapped != nullptr) {
388+
return;
389+
}
390+
391+
napi_wrap(env, jsThis, nativeObject, nullptr, nullptr, nullptr);
392+
};
285393

286394
Class cls = (Class)data;
287395

288-
if (jsThis != nullptr) {
289-
napi_value constructor;
396+
napi_value constructor = newTarget;
397+
if (constructor == nullptr && jsThis != nullptr) {
290398
napi_get_named_property(env, jsThis, "constructor", &constructor);
399+
}
400+
401+
if (constructor != nullptr) {
291402
Class newTargetCls = nil;
292-
napi_unwrap(env, constructor, (void**)&newTargetCls);
403+
if (!(bridgeState != nullptr &&
404+
bridgeState->tryResolveBridgedClassConstructor(env, constructor, &newTargetCls))) {
405+
napi_unwrap(env, constructor, (void**)&newTargetCls);
406+
}
293407

294408
if (newTargetCls != nil) {
295409
cls = newTargetCls;
@@ -315,6 +429,7 @@ void setupObjCClassDecorator(napi_env env) {
315429
return existing;
316430
}
317431

432+
ensureWrappedThis(object);
318433
jsThis = bridgeState->proxyNativeObject(env, jsThis, object);
319434
napi_wrap(env, jsThis, object, nullptr, nullptr, nullptr);
320435
return jsThis;
@@ -332,6 +447,7 @@ void setupObjCClassDecorator(napi_env env) {
332447
// JS "init" method so constructor arguments participate in selector
333448
// matching (including Swift-style token objects).
334449
object = [cls alloc];
450+
ensureWrappedThis(object);
335451
jsThis = bridgeState->proxyNativeObject(env, jsThis, object);
336452
}
337453

@@ -642,6 +758,20 @@ void defineProtocolMembers(napi_env env, ObjCClassMemberMap& members, napi_value
642758
superclass = nullptr;
643759
}
644760

761+
napi_value constructorNameValue = nullptr;
762+
napi_create_string_utf8(env, jsConstructorName.c_str(), jsConstructorName.length(),
763+
&constructorNameValue);
764+
napi_property_descriptor constructorNameProp = {
765+
.utf8name = "name",
766+
.method = nullptr,
767+
.getter = nullptr,
768+
.setter = nullptr,
769+
.value = constructorNameValue,
770+
.attributes = napi_default,
771+
.data = nullptr,
772+
};
773+
napi_define_properties(env, constructor, 1, &constructorNameProp);
774+
645775
this->constructor = make_ref(env, constructor);
646776
this->prototype = make_ref(env, prototype);
647777

@@ -752,6 +882,38 @@ void defineProtocolMembers(napi_env env, ObjCClassMemberMap& members, napi_value
752882
return hasOwn;
753883
};
754884

885+
if (!hasOwnNamedProperty(constructor, "name")) {
886+
napi_value classNameValue = nullptr;
887+
napi_create_string_utf8(env, jsConstructorName.c_str(), NAPI_AUTO_LENGTH, &classNameValue);
888+
napi_property_descriptor nameProperty = {
889+
.utf8name = "name",
890+
.name = nil,
891+
.method = nil,
892+
.getter = nil,
893+
.setter = nil,
894+
.value = classNameValue,
895+
.attributes = (napi_property_attributes)(napi_configurable),
896+
.data = nil,
897+
};
898+
napi_define_properties(env, constructor, 1, &nameProperty);
899+
}
900+
901+
if (!hasOwnNamedProperty(constructor, "length")) {
902+
napi_value zeroValue = nullptr;
903+
napi_create_int32(env, 0, &zeroValue);
904+
napi_property_descriptor lengthProperty = {
905+
.utf8name = "length",
906+
.name = nil,
907+
.method = nil,
908+
.getter = nil,
909+
.setter = nil,
910+
.value = zeroValue,
911+
.attributes = (napi_property_attributes)(napi_configurable),
912+
.data = nil,
913+
};
914+
napi_define_properties(env, constructor, 1, &lengthProperty);
915+
}
916+
755917
if (!hasOwnNamedProperty(constructor, "alloc")) {
756918
napi_property_descriptor allocProperty = {
757919
.utf8name = "alloc",
@@ -796,6 +958,28 @@ void defineProtocolMembers(napi_env env, ObjCClassMemberMap& members, napi_value
796958
napi_define_properties(env, constructor, 2, slots);
797959
}
798960

961+
napi_value global = nullptr;
962+
napi_value symbolCtor = nullptr;
963+
napi_value hasInstanceSymbol = nullptr;
964+
napi_get_global(env, &global);
965+
napi_get_named_property(env, global, "Symbol", &symbolCtor);
966+
napi_get_named_property(env, symbolCtor, "hasInstance", &hasInstanceSymbol);
967+
bool hasOwnHasInstance = false;
968+
napi_has_own_property(env, constructor, hasInstanceSymbol, &hasOwnHasInstance);
969+
if (!hasOwnHasInstance) {
970+
napi_property_descriptor hasInstanceProperty = {
971+
.utf8name = nil,
972+
.name = hasInstanceSymbol,
973+
.method = JS_classHasInstance,
974+
.getter = nil,
975+
.setter = nil,
976+
.value = nil,
977+
.attributes = (napi_property_attributes)(napi_configurable),
978+
.data = nil,
979+
};
980+
napi_define_properties(env, constructor, 1, &hasInstanceProperty);
981+
}
982+
799983
if (!hasOwnNamedProperty(prototype, "toString")) {
800984
napi_property_descriptor toStringProperty = {
801985
.utf8name = "toString",

NativeScript/ffi/ClassBuilder.h

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ class ObjCProtocol;
1515

1616
class ClassBuilder : public ObjCClass {
1717
public:
18-
ClassBuilder(napi_env env, napi_value constructor);
18+
ClassBuilder(napi_env env, napi_value constructor, Class explicitSuperClass = nullptr);
1919
~ClassBuilder();
2020

2121
void addProtocol(ObjCProtocol* protocol);

0 commit comments

Comments
 (0)