refactor: move embedded runtime JS to real .js files (js2c) + enum eval caching - #411
Draft
edusperoni wants to merge 9 commits into
Draft
refactor: move embedded runtime JS to real .js files (js2c) + enum eval caching#411edusperoni wants to merge 9 commits into
edusperoni wants to merge 9 commits into
Conversation
Register SetPromiseRejectCallback and track rejected promises without handlers in a per-isolate PromiseRejectionTracker (owned by Caches). A kCFRunLoopBeforeWaiting observer drains the pending list once per runloop turn and reports each rejection through the same __onUncaughtError/__onDiscardedError machinery as uncaught sync exceptions (log-prefixed 'Unhandled promise rejection:'). A handler attached before the drain cancels the report. Worker isolates give the worker-global onerror a chance first, then forward to the main isolate's worker.onerror like uncaught worker errors. OnUncaughtError's reporting tail is refactored into the shared ReportToJsHandlersAndLog helper (behavior preserved); the observer polls an atomic pending count since rejections can arrive from any thread holding the v8::Locker, and the drain catches NSExceptions so they cannot unwind through the observer's live V8 scopes.
Add a JS bootstrap (InitErrorEvents, evaluated for main and worker isolates right after PromiseProxy::Init) providing spec-shaped Event, EventTarget, ErrorEvent and PromiseRejectionEvent constructors, the EventTarget methods on globalThis, and a global reportError(). Uncaught exceptions now dispatch a cancelable 'error' ErrorEvent and unhandled rejections a cancelable 'unhandledrejection' PromiseRejectionEvent before reporting; preventDefault() fully handles the error (no __on* shim, no fatal log, no modal). Unprevented errors keep the existing behavior byte-for-byte, so NativeScript core's __onUncaughtError/__onDiscardedError hooks continue to work unchanged. A handler attached after reporting fires 'rejectionhandled' as a task on the next drain turn; already-reported promises are held phantom-weak so GC'd promises drop out. Native code dispatches through closures returned by the bootstrap IIFE (stashed as Persistents in Caches), so the events keep firing even if app code overwrites globalThis.dispatchEvent. Listener-thrown errors route to the fatal tail without recursive dispatch.
Preserve the original NSException when a native call throws: the JS error now carries name/message from the exception and the wrapped original as error.nativeException (re-enables the long-dormant Throw_ObjC_exceptions_to_JavaScript test everywhere - libffi 3.7.1 fixed simulator unwinding). Add interop.escapeException(x): returns a JS Error branded with an isolate-private symbol carrying either the original NSException or synthesis info. When a branded throw reaches a JS-to-native boundary (overridden methods and JS-backed blocks via ArgConverter's MethodCallback, property accessors, adapters), the boundary converts it to a real @throw into the native caller - always after every V8 scope has destructed (pendingThrow captured before the scopes, thrown after the inner block). Unbranded throws keep today's semantics: ReThrow to the message listener in MethodCallback, and report-once-plus-defined- default at the former tns::Assert abort sites (property getters/ setters, Array/Dictionary/FastEnumeration adapters no longer abort the process on a JS throw). Add opt-in crashOnUncaughtJsExceptions (default off): unprevented fatal errors schedule an uncaught @throw on the runtime loop from a clean, V8-scope-free frame so crash reporters capture a real native crash with the JS stack in userInfo. Branded escapes reaching the fatal tail (e.g. thrown from an error-event listener) always take that path.
…jectionhandled reason)
- GetEscapeExceptionBrand now checks Caches::IsValid() — Caches::Get
returns a dummy cache (never null) after isolate disposal, so the
nullptr check could not honor the documented empty-handle contract.
- The property setter's fallback ObjCDataWrapper is now released via
DeleteWrapperIfUnused, mirroring the getter (pre-existing asymmetry).
- rejectionhandled now carries the original rejection reason:
reportedOutstanding_/pendingRejectionHandled_ retain {promise, reason}
pairs (promise phantom-weak, so a GC'd promise still drops the entry
and its reason), per PromiseRejectionEvent semantics.
…tException
Event and EventTarget are general-purpose web primitives, not exception
machinery. The generic bootstrap (Event, EventTarget, the globalThis
mixin and the internal backing target, now stashed in
Caches::GlobalEventTarget for future native consumers like AbortSignal)
moves to Events.{h,cpp}; the error layer (ErrorEvent,
PromiseRejectionEvent, reportError and the native dispatch closures)
moves to ErrorEvents.{h,cpp}. NativeScriptException keeps only the
reporting machinery and the rejection tracker.
Pure code motion aside from the cross-layer handoff: the error layer
installs the listener-error reporter into the Events closure through a
one-shot _installListenerErrorReporter hook (the backing target is
reachable from app code via event.target, so the hook removes itself
after runtime init). No behavior change; both new .cpp files are
registered in the Xcode project.
interop.escapeException now captures the escape-site JS stack alongside the wrapping error's own stack, and every NSException leaving the escape/crash machinery carries the JavaScript context: - synthesized escapes keep the stack in the reason and under the documented userInfo keys (JavaScriptStack, plus JavaScriptEscapeStack when the escape site differs from the origin); - rethrown ORIGINAL NSExceptions get a combined 'JS stack / Escaped at' string attached as an associated object, preserving the exception's identity and userInfo for parent @catch matching; - the crashOnUncaughtJsExceptions fatal exception is attached the same way. New NSException (NativeScript) category exposes it uniformly to native code and crash-SDK hooks via -[NSException tns_javascriptStackTrace] (associated object first, userInfo fallback); the userInfo keys are exported as TNSJavaScriptStackTraceKey/TNSJavaScriptEscapeStackTraceKey and are the stable integration contract. Non-Error escapes (escapeException("boom")) now carry the escape-site stack instead of nothing.
New docs folder with the first runtime feature doc: the WHATWG error model (error/unhandledrejection/rejectionhandled events, reportError), what lands on the events for each throw shape, catching native exceptions in JS via error.nativeException, forwarding JS throws to native with interop.escapeException, JS stacks on NSException (tns_javascriptStackTrace and the userInfo keys), configuration flags, crash-reporter integration on both layers, legacy hook status and the behavioral invariants.
…js2c The ~26KB of JavaScript previously embedded as C++ string literals now lives in NativeScript/runtime/js/*.js. A "Generate RuntimeBuiltins" build phase runs tools/js2c.mjs to emit a generated source table, and call sites go through BuiltinLoader::RunBuiltin, which sets proper internal/<name>.js script origins and shares an in-process bytecode cache across isolates so workers stop re-parsing the builtins. The placeholder-module proxy is now a static builtin parameterized with the error message. The legacy inline proxy source never compiled (the spliced message's quotes made it a guaranteed SyntaxError), so the first require() of a missing optional module always threw via the leaked pending exception; CreatePlaceholderModule now throws an explicit "Cannot find module" error to keep that contract.
Interop::WriteValue recompiled and re-ran an enum wrapper's __tsEnum snippet on every FFI marshal; the value is now memoized on the EnumDataWrapper. GlobalPropertyGetter did the same on every read of a JsCode global; the evaluated result is now defined as a real own property on the global (the interceptor is kNonMasking, so later reads bypass it entirely), with a reentrancy guard because CreateDataProperty re-enters the interceptor before the property exists.
|
Important Review skippedDraft detected. Please check the settings in the CodeRabbit UI or the ⚙️ Run configurationConfiguration used: Repository UI Review profile: CHILL Plan: Pro Plus Run ID: You can disable this status message by setting the Use the checkbox below for a quick retry:
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What
Moves the ~26KB of JavaScript that was embedded as C++ string literals into real, version-controlled
.jsfiles underNativeScript/runtime/js/, compiled into the framework at build time (Node-style js2c), and adds two steady-state perf fixes for jitless mode.Stacked on #409; draft until that lands.
Extraction & build
tools/js2c.mjs(Node CLI) convertsruntime/js/*.jsinto a generatedRuntimeBuiltins.{h,cpp}table (byte arrays, deterministic output). Supports--minifyvia esbuild (whitespace+syntax only) — implemented but not enabled in any configuration.NativeScript/runtime/generated/is gitignored.Loader
BuiltinLoader::RunBuiltinis the single compile-and-run path: properinternal/<name>.jsscript origins (runtime frames are now identifiable in stack traces) and a process-wide bytecode cache — first isolate compiles withkEagerCompile, workers/subsequent isolates consume viakConsumeCodeCache(with rejected-cache fallback), so the ~26KB is parsed once per process instead of once per isolate.placeholder-module.js) parameterized with the error message instead of splicing it into source text.Perf (jitless steady-state)
Interop::WriteValueno longer recompiles + re-runs an enum's__tsEnumsnippet on every FFI marshal — the numeric value is memoized onEnumDataWrapper.GlobalPropertyGetterno longer recompiles a JsCode global's snippet on every read — the evaluated result is defined as a real own property on the global (interceptor iskNonMasking, so later reads bypass it entirely). Includes a reentrancy guard:CreateDataPropertyre-enters the interceptor before the property exists.Behavior notes
require()of a missing optional module always threw via the leaked pending exception (which the shared require tests rely on).CreatePlaceholderModulenow keeps that contract explicitly with a properError: Cannot find module '<name>'; repeat requires still get the cached placeholder, whose exports now actually throw on access as originally designed.internal/<name>.jsorigins instead of anonymous<eval>frames.Testing
.jsinput is touched.