GV

GeezerVison

The World Wide Web, restored to 800 pixels of glory.

Home|About|Gaming cabinet|Archives|How it works|Starter sites

On this site

  • Sign in
  • ActionsCI/CD and automation
  • CodespacesCloud development environments
  • All Features
  • Documentation
  • Training
  • Certifications
  • Build with Copilot
  • Customize Copilot
  • Integrations
  • Trending repos
  • GitHub Sponsors
  • Developer articles
  • Enterprises

This view

  • Original page
  • New address

GeezerVison is showing github.com — fix(runtime): generate named Java proxies at runtime when not precompiled by NathanWalker · Pull Request #2016 · NativeScript/android

Conversation

@NathanWalker

@NathanWalker NathanWalker commented Aug 20, 2026 •
edited
Loading

Uh oh!

There was an error while loading. Please reload this page.

Copy link
Copy Markdown
Contributor

Named proxies — Base.extend('a.b.C', {...}) and @javaproxy('a.b.C') — only
existed if the static binding generator had compiled them ahead of time:
ClassResolver routed com.tns.gen* names through DexFactory and threw for
everything else. SBG scans only assets/app, so a dev server that keeps
@nativescript/core off disk (Vite HMR serves it over HTTP) lost every named
proxy in core and crashed at boot with
LookedUpClassNotFound: Class "com.tns.FragmentClass" not found.

  • ClassResolver: on LookedUpClassNotFound, fall through to DexFactory and
    generate the proxy dex at runtime — classes and interfaces alike. Logs a
    warning per class so SBG coverage gaps stay visible.
  • DexFactory: tell named proxies apart from anonymous bindings so a dotted
    interface implementation keeps its requested name instead of the derived
    com.tns.gen one. Core's two @JavaProxy proxies implement
    ActivityLifecycleCallbacks and ComponentCallbacks2 — both interfaces.
  • ProxyGenerator: thumb-suffix dotted proxy file names. Unsuffixed files
    never matched getDexFile's probe (regenerated every launch), never
    matched purgeDexesByThumb, and jarFile.exists() reused their stale .jar
    across app versions.
  • ModuleInternal: anchor relative runModule paths to the app root in the ES
    module branch, as the require branch always has. A generated binding's
    @JavaScriptImplementation carries ./bundle.mjs — an app-root-relative
    module name by SBG convention — but the ESM branch stats it as a
    filesystem path from the process cwd (after the registry-key canonicalizer
    folds the ./ away), so it failed as "Cannot find module bundle.mjs".

Manifest-referenced classes (com.tns.NativeScriptActivity) resolve through
this path because the generated dex is injected into the app class loader
during entry evaluation — which the boot backstop holds until settled —
before the framework instantiates the activity. A precompiled class is
still the sturdier answer where a build step can provide one; this makes
the missing-class case survivable instead of fatal.

Sorry, something went wrong.

Uh oh!

There was an error while loading. Please reload this page.

All reactions
@NathanWalker
fix(runtime): generate named Java proxies at runtime when not precomp…
e49ac46
…iled

Named proxies — Base.extend('a.b.C', {...}) and @javaproxy('a.b.C') — only
existed if the static binding generator had compiled them ahead of time:
ClassResolver routed com.tns.gen* names through DexFactory and threw for
everything else. SBG scans only assets/app, so a dev server that keeps
@nativescript/core off disk (Vite HMR serves it over HTTP) lost every named
proxy in core and crashed at boot with
LookedUpClassNotFound: Class "com.tns.FragmentClass" not found.

- ClassResolver: on LookedUpClassNotFound, fall through to DexFactory and
  generate the proxy dex at runtime — classes and interfaces alike. Logs a
  warning per class so SBG coverage gaps stay visible.
- DexFactory: tell named proxies apart from anonymous bindings so a dotted
  interface implementation keeps its requested name instead of the derived
  com.tns.gen one. Core's two @javaproxy proxies implement
  ActivityLifecycleCallbacks and ComponentCallbacks2 — both interfaces.
- ProxyGenerator: thumb-suffix dotted proxy file names. Unsuffixed files
  never matched getDexFile's probe (regenerated every launch), never
  matched purgeDexesByThumb, and jarFile.exists() reused their stale .jar
  across app versions.
- ModuleInternal: anchor relative runModule paths to the app root in the ES
  module branch, as the require branch always has — a generated binding's
  @JavaScriptImplementation carries a project-relative path (./bundle.mjs)
  that java.io.File flattens, and the ESM branch went straight to the
  filesystem from the process cwd.

Manifest-referenced classes (com.tns.NativeScriptActivity) resolve through
this path because the generated dex is injected into the app class loader
during entry evaluation — which the boot backstop holds until settled —
before the framework instantiates the activity. A precompiled class is
still the sturdier answer where a build step can provide one; this makes
the missing-class case survivable instead of fatal.
@NathanWalker
NathanWalker requested a review from edusperoni August 20, 2026 00:57
@coderabbitai

coderabbitai Bot commented Aug 20, 2026 •
edited
Loading

Uh oh!

There was an error while loading. Please reload this page.

Copy link
Copy Markdown

Important

Review skipped

Auto reviews are disabled on base/target branches other than the default branch.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 00d15446-786b-428f-a99e-9cecae8379d2

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review

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.

❤️ Share
  • X
  • Mastodon
  • Reddit
  • LinkedIn

Comment @coderabbitai help to get the list of available commands.

All reactions

Sorry, something went wrong.

Uh oh!

There was an error while loading. Please reload this page.

NathanWalker and others added 3 commits August 19, 2026 18:14
@NathanWalker
chore: comment clarity
40dde17
@edusperoni
fix(runtime): resolve entry-module paths in Java before crossing JNI
d3bbd46
runModule now sends every entry through Module.resolveEntryPath: absolute
paths and URLs pass through, and a scheme-less relative name - the
@JavaScriptImplementation convention, ./bundle.mjs by SBG's stamp - goes
through the same resolution require uses, extension and directory probing
included, against the app root. A missing entry throws require's
"Failed to find module" from Java instead of a cwd-relative native stat
failure.

This replaces AnchorToAppRoot. Anchoring inside ModuleInternal::Load
resolved the load but not the identity: Runtime::RunModule probes the
boot backstop with the path it was handed ("Java resolves package.json's
main before handing the path over, so the entry the backstop probes is
the very one that was just evaluated"), so an anchored relative entry
evaluated under the app-root key while the backstop probed the unanchored
one - a registry miss that silently disarmed the hold and the rejection
fatal for exactly the dev-served entries this branch exists to support.
Resolving before the JNI crossing keeps one resolution seam and makes the
evaluated entry and the probed entry the same string by construction.
@edusperoni
fix(runtime): content-keyed proxy dex cache; keep the not-found error…
8eb0b86
… primary

The dex cache key was name + thumb, and the thumb only changes on
reinstall - so once the thumb suffix made the cache actually hit, an
HMR edit to a proxy's method overrides would silently keep loading the
previous dex. The file name now also carries a digest of everything that
shapes the generated proxy besides its name: base class, interface flag,
and the sorted override and interface lists, so JS enumeration order
cannot cause a spurious miss. The digest sits after the thumb, so the
thumb-keyed purge keeps matching old generations.

When runtime generation itself fails, ClassResolver now rethrows the
original LookedUpClassNotFound with the generation failure attached as a
suppressed exception, instead of letting a downstream ClassNotFound or
ASM error replace the one message that names the class the app actually
asked for.
@edusperoni edusperoni mentioned this pull request Aug 20, 2026
Proxy dex generation: thread-safety and latent naming bugs (follow-up to #2016) #2019
Open
@edusperoni

edusperoni commented Aug 20, 2026

Copy link
Copy Markdown
Collaborator

Reviewed this in depth alongside the loader work — the runtime-generation approach is sound, the !isNamedProxy guards are provably inert for every pre-existing path (anonymous bindings byte-identical), and the dotted-interface naming pipeline checks out end to end, nested $ interfaces included. Two commits landed on this branch from that review, plus one discovery worth writing down.

The thumb fix is bigger than the commit message says. desiredDexClassName for a plain anonymous class extend (android.app.Activity_MyActivity_59_56_) contains dots, so since 2018 every anonymous class extend has taken the dotted, un-thumbed filename branch: the getDexFile probe (…-<thumb>.dex) never matched, the dex regenerated every launch, and the freshly generated bytes were then discarded because the un-thumbed .jar from launch #1 already existed and was loaded instead — including across CLI livesync pushes, where the sentinel purge also never matched these files. The on-disk dex cache has only ever worked for anonymous interface implementations. This one-line suffix turns it on for class extends for the first time — a real cold-start win for extend-heavy apps. (One overstatement in my earlier analysis, corrected: stale jars did not survive store updates — dexDir is under code_cache, which the platform wipes on upgrade. The within-install reuse was the operative bug.)

Commit d3bbd46b — entry resolution moved to Java, replacing AnchorToAppRoot. The anchor resolved the load but not the identity: Runtime::RunModule probes the boot backstop with the path it was handed, so an anchored relative entry evaluated under the app-root registry key while the backstop probed the unanchored one — a silent registry miss that disarmed the hold and the rejection fatal for exactly the dev-served entries this branch targets (and this PR's own mechanism depends on the hold: the dex injection must complete before the framework instantiates the activity). Module.resolveEntryPath resolves scheme-less names through the same machinery require uses (extension/index probing included) before the JNI crossing, so the evaluated entry and the probed entry are the same string by construction, and the runtime keeps a single resolution seam.

Commit 8eb0b868 — two hardening fixes on the new path.

  • The cache key was name + thumb, and the thumb only changes on reinstall — so with the cache now live, an HMR edit to a named proxy's method overrides would keep loading the previous dex (named proxies get no file_line_col uniqueness). The filename now also carries a digest of the proxy's contents (base class, interface flag, sorted overrides/interfaces), placed after the thumb so the thumb-keyed purge still matches.
  • When runtime generation itself fails, ClassResolver now rethrows the original LookedUpClassNotFound with the generation failure attached as suppressed, instead of letting a downstream ClassNotFoundException/ASM error replace the one message naming the class the app asked for. (The previous if (clazz == null) throw notFound was unreachable — DexFactory.resolveClass throws rather than returning null.)

Both verified with the full suite on an API 33 emulator (1031/0).

Deferred items from the review — pre-existing defects this PR makes routine rather than theoretical (concurrent generation corrupting dexes via Dump's static StringBuffer, the jar exists()/setReadOnly() race, an unchecked short read, plus smaller naming nits) — are filed as #2019 to pick up after the ESM work lands.

All reactions

Sorry, something went wrong.

Uh oh!

There was an error while loading. Please reload this page.

@NathanWalker
NathanWalker merged commit 02044d6 into feat/hmr-dev-sessions Aug 20, 2026
4 checks passed

Uh oh!

There was an error while loading. Please reload this page.

@NathanWalker
NathanWalker deleted the feat/hmr-dev-sessions-javaproxy-handling branch August 20, 2026 03:24
@gustavotrott gustavotrott mentioned this pull request Aug 24, 2026
Static binding names never match at runtime since 9.0.0 — every extend() falls back to runtime dex generation #2024
Open
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Reviewers

@edusperoni edusperoni Awaiting requested review from edusperoni

Uh oh!

There was an error while loading. Please reload this page.

Assignees

No one assigned

Labels

None yet

Projects

None yet

Milestone

No milestone

Development

Successfully merging this pull request may close these issues.

Uh oh!

There was an error while loading. Please reload this page.

2 participants

@NathanWalker @edusperoni
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.
You are visitor 01402503 — GeezerVison does not store the pages you look up.
Best viewed with Netscape Navigator 4.7 or Internet Explorer 6 at 800×600. No cookies required.