Skip to content

fix(android): use $name when a Java package's global name is taken - #86

Open
NathanWalker wants to merge 2 commits into
android-react-nativefrom
fix/android-readonly-package-global
Open

NathanWalker wants to merge 2 commits into
android-react-nativefrom
fix/android-readonly-package-global

Conversation

@NathanWalker

@NathanWalker NathanWalker commented Oct 24, 2026 •

Copy link
Copy Markdown
Contributor

Important

Squash merge this PR. The first commit (f44d92e) describes a read-only-global fallback that review replaced with an existence check. Use this as the squash commit message (title + body), replacing GitHub's default list of commit messages:

fix(android): use $name when a Java package's global name is taken

If the global object already has a top-level Java package's name, expose
the package as `$name` and log a warning, in both the jsi and napi trees,
so an existing global is never overwritten and the package stays reachable.

Addresses the "Expo-specific special-casing in core runtime code" point in the #77 review.

Summary

Top-level Java package globals no longer overwrite, or depend on, a global the host already defined.

CreateTopLevelNamespaces
  for each top-level Java package
    name = package name, or "$name" for a JS keyword      (unchanged)
    if global already has `name`
      warn "Java package 'expo' is exposed as '$expo': the global 'expo' already exists."
      name = "$name"
    set global[name]
  • jsi (ffi/jni/jsi) used to try the assignment and fall back to $name only if it threw. It now checks for the name first.
  • napi (ffi/jni/napi) had no fallback, so a taken name made the package unreachable. It now runs the same check.
  • The warning goes to logcat (TNS.Runtime).
  • The rule is documented in the README PR (Android section).

Evidence

  • Both trees compile for arm64-v8a: ./gradlew :runtime:externalNativeBuildDebug -PonlyArm64 -PbindingLayer=napi, then the same with -PbindingLayer=jsi.
  • Nothing the runtime defines before CreateTopLevelNamespaces matches a Java package name. That set is global, self, Worker, and on workers postMessage, close, terminate and __ns__worker. A standalone app therefore gets exactly the names it got before.
  • Not verified at runtime: the fallback path. It needs a host that defines a matching global first, such as Expo's expo.

Merge Danger

Door: two-way

Blast Radius: top-level Java package globals

The only change is for a package whose name is already a global. Before, that package overwrote a writable global, or was unreachable on napi when the global was read-only. Now it appears as $name.

A host can own a top-level package name as a read-only global; Expo
defines `expo`. The jsi runtime already exposed such a package as
`$expo`, but did so silently. It now logs a warning naming both globals.

The napi runtime had no fallback: depending on the engine, the
assignment either failed or was ignored, and the package was not
reachable at all. It now gets the same `$name` fallback and warning.
@coderabbitai

coderabbitai Bot commented Oct 24, 2026 •

Copy link
Copy Markdown

Important

Review skipped

Auto reviews are disabled on this repository. 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
  • Source mode: Auto
  • Repository: Unavailable
  • Branch: Unavailable
  • File: No configuration file from this repository applied
  • Commit: Unavailable
  • Layers: Organization UI
  • Review profile: CHILL
  • Plan: Advanced
  • Run ID: 3ce7bc9c-c72c-4b01-8155-40c58b60750e

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
  • Autofix · Keep fixing CodeRabbit findings and required CI, and resolving merge conflicts

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

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

Comment on lines +1101 to +1153
@@ -1120,7 +1142,15 @@ void MetadataNode::CreateTopLevelNamespaces(napi_env env) {
if (IsJavascriptKeyword(nameSpace)) {
nameSpace = "$" + nameSpace;
}
NAPI_GUARD(napi_set_named_property(env, global, nameSpace.c_str(), packageObj)) {}
if (!SetGlobalIfWritable(env, global, nameSpace, packageObj)) {
// The host already owns the name as a read-only global (Expo's
// `expo`), so the package takes the same $ prefix as keywords.
string fallback = "$" + nameSpace;
__android_log_print(ANDROID_LOG_WARN, "TNS.Runtime",
"Java package '%s' is exposed as '%s': the global '%s' is read-only.",
nameSpace.c_str(), fallback.c_str(), nameSpace.c_str());
NAPI_GUARD(napi_set_named_property(env, global, fallback.c_str(), packageObj)) {}
}

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The SetGlobalIfWritable function is used only once hence move the function body inside CreateTopLevelNamespaces.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done. The helper is gone and the check is inline in CreateTopLevelNamespaces.

}
NAPI_GUARD(napi_set_named_property(env, global, nameSpace.c_str(), packageObj)) {}
if (!SetGlobalIfWritable(env, global, nameSpace, packageObj)) {
// The host already owns the name as a read-only global (Expo's

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Remove mention of expo or Expo from the comments.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done. No comment mentions Expo now.

Comment on lines +1103 to +1118
if (napi_set_named_property(env, global, name.c_str(), value) == napi_ok) {
napi_value assigned;
bool same = false;
if (napi_get_named_property(env, global, name.c_str(), &assigned) == napi_ok &&
napi_strict_equals(env, assigned, value, &same) == napi_ok && same) {
return true;
}
}
bool pending = false;
napi_is_exception_pending(env, &pending);
if (pending) {
napi_value error;
napi_get_and_clear_last_exception(env, &error);
}
return false;
}

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

All of this is probably not needed because we do not want to silently overwrite a value set on a global object even if it was not read-only I think. So A simple napi_has_named_property is enough.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Agreed. It now does a plain napi_has_named_property: if the global already has the name, the package goes on $name and a warning is logged, so nothing is overwritten. 0948f32

Comment on lines +891 to +900
try {
global.setProperty(rt, nameSpace.c_str(), packageObj);
} catch (JsError &) {
// A host can already own the name as a non-writable global (Expo
// defines `expo`), so fall back to the same $ prefix as keywords.
global.setProperty(rt, ("$" + nameSpace).c_str(), packageObj);
// The host already owns the name as a read-only global (Expo's
// `expo`), so the package takes the same $ prefix as keywords.
string fallback = "$" + nameSpace;
__android_log_print(ANDROID_LOG_WARN, "TNS.Runtime",
"Java package '%s' is exposed as '%s': the global '%s' is read-only.",
nameSpace.c_str(), fallback.c_str(), nameSpace.c_str());
global.setProperty(rt, fallback.c_str(), packageObj);

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do the same here as comments for the napi runtime. Check if the property already exists, if it does prefix with $. also remove the comment.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done. The jsi tree checks global.hasProperty first, takes the same $name fallback, and the comment is removed.

Check whether the global already has the package's name and, if it
does, expose the package as `$name`, logging a warning. Both the jsi
and napi trees now do the same check, so an existing global is never
overwritten and no engine-specific read-only handling is needed.
@NathanWalker NathanWalker changed the title fix(android): warn when a Java package global falls back to $name fix(android): use $name when a Java package's global name is taken Oct 25, 2026

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants