Skip to content

feat: support native ES classes with lazy registration#1983

Open
NathanWalker wants to merge 1 commit into
mainfrom
feat/native-es-classes
Open

feat: support native ES classes with lazy registration#1983
NathanWalker wants to merge 1 commit into
mainfrom
feat/native-es-classes

Conversation

@NathanWalker

@NathanWalker NathanWalker commented Jul 16, 2026

Copy link
Copy Markdown
Contributor

Parity with NativeScript/ios#403

Summary by CodeRabbit

  • New Features
    • Added support for extending native Java types using standard JavaScript class extends syntax, including overridden getters/methods, constructor forwarding, static members, and interface dispatch.
    • Improved JavaScript-to-Java marshalling so JS class constructors can be converted to java.lang.Class/Object correctly.
    • Added a no-op NativeClass decorator for compatibility with transpiled/annotated code.
  • Tests
    • Added an end-to-end test suite covering ES-native inheritance, proxy allocation rules, marshalling/identity, and error cases.
    • Extended the app test runner to include the new suite.

@coderabbitai

coderabbitai Bot commented Jul 16, 2026

Copy link
Copy Markdown

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

Run ID: 368970a6-3728-46cb-a572-148c431e5351

📥 Commits

Reviewing files that changed from the base of the PR and between 3b966c1 and 2b209a5.

📒 Files selected for processing (9)
  • test-app/app/src/main/assets/app/mainpage.js
  • test-app/app/src/main/assets/app/tests/testNativeESClasses.js
  • test-app/app/src/main/assets/internal/ts_helpers.js
  • test-app/runtime/src/main/cpp/CallbackHandlers.cpp
  • test-app/runtime/src/main/cpp/CallbackHandlers.h
  • test-app/runtime/src/main/cpp/JsArgConverter.cpp
  • test-app/runtime/src/main/cpp/JsArgToArrayConverter.cpp
  • test-app/runtime/src/main/cpp/MetadataNode.cpp
  • test-app/runtime/src/main/cpp/MetadataNode.h
🚧 Files skipped from review as they are similar to previous changes (8)
  • test-app/runtime/src/main/cpp/CallbackHandlers.h
  • test-app/app/src/main/assets/app/mainpage.js
  • test-app/app/src/main/assets/internal/ts_helpers.js
  • test-app/runtime/src/main/cpp/CallbackHandlers.cpp
  • test-app/runtime/src/main/cpp/MetadataNode.h
  • test-app/runtime/src/main/cpp/JsArgToArrayConverter.cpp
  • test-app/runtime/src/main/cpp/MetadataNode.cpp
  • test-app/app/src/main/assets/app/tests/testNativeESClasses.js

📝 Walkthrough

Walkthrough

Native ES classes extending native types now lazily generate Java proxy classes, support Java dispatch and interface implementation, marshal to java.lang.Class, preserve legacy behavior, and include runtime tests plus a no-op NativeClass helper.

Changes

Native ES class proxy support

Layer / File(s) Summary
Lazy ES proxy registration
test-app/runtime/src/main/cpp/MetadataNode.*, test-app/runtime/src/main/cpp/CallbackHandlers.*
Discovers ES-derived constructors, collects overrides and interfaces, generates deterministic proxy names, and resolves and caches Java proxy classes.
ES-derived construction and legacy boundaries
test-app/runtime/src/main/cpp/MetadataNode.cpp
Initializes ES-derived class and interface instances, registers them with the runtime, and rejects .extend(...) on ES syntax classes.
Constructor-to-java.lang.Class marshalling
test-app/runtime/src/main/cpp/JsArgConverter.cpp, test-app/runtime/src/main/cpp/JsArgToArrayConverter.cpp
Converts native constructor functions to Java class references for Class and Object targets, including array arguments and conversion failures.
Decorator wiring and behavioral coverage
test-app/app/src/main/assets/internal/ts_helpers.js, test-app/app/src/main/assets/app/mainpage.js, test-app/app/src/main/assets/app/tests/testNativeESClasses.js
Registers the no-op NativeClass helper, loads the test suite, and covers inheritance, dispatch, interfaces, identity, naming, statics, compatibility, and pure JavaScript classes.

Estimated code review effort: 4 (Complex) | ~60 minutes

Sequence Diagram(s)

sequenceDiagram
  participant ESConstructor
  participant MetadataNode
  participant CallbackHandlers
  participant JavaResolver
  ESConstructor->>MetadataNode: Resolve native class type
  MetadataNode->>MetadataNode: EnsureExtendedESClass
  MetadataNode->>CallbackHandlers: ResolveClass with overrides and interfaces
  CallbackHandlers->>JavaResolver: resolveClass
  JavaResolver-->>CallbackHandlers: Return generated proxy class
  CallbackHandlers-->>MetadataNode: Return cached class
  MetadataNode-->>ESConstructor: Attach TypeMetadata
Loading

Poem

A rabbit hops through classes bright,
And proxy stars appear at night.
Overrides dance, interfaces sing,
super calls make Java spring.
Tests thump softly: hop, hop, hooray!

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 16.67% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly matches the main change: adding lazy registration support for native ES classes.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch

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.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 3

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@test-app/runtime/src/main/cpp/JsArgConverter.cpp`:
- Around line 155-173: Update the failure message construction in
JsArgConverter’s function-conversion branch to use a bounded write matching
buff’s 1024-byte capacity, replacing the unbounded sprintf call while preserving
the existing message and index values.

In `@test-app/runtime/src/main/cpp/JsArgToArrayConverter.cpp`:
- Around line 137-154: Update the arg->IsFunction() handling in
JsArgToArrayConverter to permit native constructor marshalling only when the
target component type is java.lang.Class or java.lang.Object, matching the
scalar converter’s target-type check. Reject constructors for String,
interfaces, and other incompatible component types before SetConvertedObject,
while preserving successful conversion for Class[] and Object[] and the existing
error reporting.

In `@test-app/runtime/src/main/cpp/MetadataNode.cpp`:
- Around line 1304-1313: Update the deterministic name generation in the
ResolveClass path around HashESClassId to include the generated proxy shape,
specifically overridden methods and static interfaces, in the cache key
alongside scriptName, baseClassName, and className. Ensure equivalent shapes
remain stable while changed shapes produce distinct fullClassName values, and
add a regression covering cache reuse with the same class identity but a changed
override/interface set.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

Run ID: e8e4729c-31fa-43dd-af9f-83c5e85f7cb5

📥 Commits

Reviewing files that changed from the base of the PR and between 6bc84d4 and 3b966c1.

📒 Files selected for processing (9)
  • test-app/app/src/main/assets/app/mainpage.js
  • test-app/app/src/main/assets/app/tests/testNativeESClasses.js
  • test-app/app/src/main/assets/internal/ts_helpers.js
  • test-app/runtime/src/main/cpp/CallbackHandlers.cpp
  • test-app/runtime/src/main/cpp/CallbackHandlers.h
  • test-app/runtime/src/main/cpp/JsArgConverter.cpp
  • test-app/runtime/src/main/cpp/JsArgToArrayConverter.cpp
  • test-app/runtime/src/main/cpp/MetadataNode.cpp
  • test-app/runtime/src/main/cpp/MetadataNode.h

Comment on lines +155 to +173
} else if (arg->IsFunction() &&
(typeSignature == "Ljava/lang/Class;" || typeSignature == "Ljava/lang/Object;")) {
// a native type ctor (or a plain ES class extending one - registered lazily here)
// passed where Java expects a java.lang.Class
auto typeName = MetadataNode::TryResolveClassCtorTypeName(m_isolate, arg.As<Function>());

if (!typeName.empty()) {
JEnv env;
jclass clazz = env.FindClass(typeName);
success = clazz != nullptr;
if (success) {
// JEnv caches classes as global refs - mark as global so the dtor doesn't delete it
SetConvertedObject(index, clazz, true /* isGlobal */);
}
}

if (!success) {
sprintf(buff, "Cannot convert function to %s at index %d", typeSignature.c_str(), index);
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🔒 Security & Privacy | 🟠 Major | ⚡ Quick win

Bound the function-conversion error message write.

Line 171 copies the unbounded typeSignature into the fixed 1024-byte buff, allowing stack corruption. Use snprintf, consistent with the buffer size.

Proposed fix
-            sprintf(buff, "Cannot convert function to %s at index %d", typeSignature.c_str(), index);
+            snprintf(buff, sizeof(buff), "Cannot convert function to %s at index %d",
+                     typeSignature.c_str(), index);
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
} else if (arg->IsFunction() &&
(typeSignature == "Ljava/lang/Class;" || typeSignature == "Ljava/lang/Object;")) {
// a native type ctor (or a plain ES class extending one - registered lazily here)
// passed where Java expects a java.lang.Class
auto typeName = MetadataNode::TryResolveClassCtorTypeName(m_isolate, arg.As<Function>());
if (!typeName.empty()) {
JEnv env;
jclass clazz = env.FindClass(typeName);
success = clazz != nullptr;
if (success) {
// JEnv caches classes as global refs - mark as global so the dtor doesn't delete it
SetConvertedObject(index, clazz, true /* isGlobal */);
}
}
if (!success) {
sprintf(buff, "Cannot convert function to %s at index %d", typeSignature.c_str(), index);
}
} else if (arg->IsFunction() &&
(typeSignature == "Ljava/lang/Class;" || typeSignature == "Ljava/lang/Object;")) {
// a native type ctor (or a plain ES class extending one - registered lazily here)
// passed where Java expects a java.lang.Class
auto typeName = MetadataNode::TryResolveClassCtorTypeName(m_isolate, arg.As<Function>());
if (!typeName.empty()) {
JEnv env;
jclass clazz = env.FindClass(typeName);
success = clazz != nullptr;
if (success) {
// JEnv caches classes as global refs - mark as global so the dtor doesn't delete it
SetConvertedObject(index, clazz, true /* isGlobal */);
}
}
if (!success) {
snprintf(buff, sizeof(buff), "Cannot convert function to %s at index %d",
typeSignature.c_str(), index);
}
🧰 Tools
🪛 ast-grep (0.44.1)

[error] 171-171: Use of an unbounded buffer function that can overflow the destination; use a size-bounded equivalent (fgets, strncpy/strlcpy, strncat/strlcat, snprintf).
Context: sprintf(buff, "Cannot convert function to %s at index %d", typeSignature.c_str(), index)
Note: [CWE-120] Buffer Copy without Checking Size of Input ('Classic Buffer Overflow').

(dangerous-buffer-functions-cpp)

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@test-app/runtime/src/main/cpp/JsArgConverter.cpp` around lines 155 - 173,
Update the failure message construction in JsArgConverter’s function-conversion
branch to use a bounded write matching buff’s 1024-byte capacity, replacing the
unbounded sprintf call while preserving the existing message and index values.

Source: Linters/SAST tools

Comment on lines +137 to +154
} else if (arg->IsFunction()) {
// a native type ctor (or a plain ES class extending one - registered lazily here)
// marshals to its java.lang.Class
auto typeName = MetadataNode::TryResolveClassCtorTypeName(isolate, arg.As<Function>());

if (!typeName.empty()) {
jclass clazz = env.FindClass(typeName);
if (clazz != nullptr) {
// JEnv caches classes as global refs - mark as global so the dtor doesn't delete it
SetConvertedObject(env, index, clazz, true /* isGlobal */);
success = true;
}
}

if (!success) {
s << "Cannot marshal JavaScript function at index " << index
<< " to Java type. Only native type constructors can be marshalled (to java.lang.Class).";
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Restrict constructor marshalling to Class[] and Object[].

This branch converts every native constructor to a jclass, even when the target component is String, an interface, or another incompatible type. That reports conversion success and defers failure to JNI array storage. Mirror the scalar converter’s target-type check.

Proposed fix
-    } else if (arg->IsFunction()) {
+    } else if (arg->IsFunction() &&
+               (m_return_type == "Ljava/lang/Class;" ||
+                m_return_type == "Ljava/lang/Object;")) {

Please cover Class[], Object[], and rejection for an incompatible component type.

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
} else if (arg->IsFunction()) {
// a native type ctor (or a plain ES class extending one - registered lazily here)
// marshals to its java.lang.Class
auto typeName = MetadataNode::TryResolveClassCtorTypeName(isolate, arg.As<Function>());
if (!typeName.empty()) {
jclass clazz = env.FindClass(typeName);
if (clazz != nullptr) {
// JEnv caches classes as global refs - mark as global so the dtor doesn't delete it
SetConvertedObject(env, index, clazz, true /* isGlobal */);
success = true;
}
}
if (!success) {
s << "Cannot marshal JavaScript function at index " << index
<< " to Java type. Only native type constructors can be marshalled (to java.lang.Class).";
}
} else if (arg->IsFunction() &&
(m_return_type == "Ljava/lang/Class;" ||
m_return_type == "Ljava/lang/Object;")) {
// a native type ctor (or a plain ES class extending one - registered lazily here)
// marshals to its java.lang.Class
auto typeName = MetadataNode::TryResolveClassCtorTypeName(isolate, arg.As<Function>());
if (!typeName.empty()) {
jclass clazz = env.FindClass(typeName);
if (clazz != nullptr) {
// JEnv caches classes as global refs - mark as global so the dtor doesn't delete it
SetConvertedObject(env, index, clazz, true /* isGlobal */);
success = true;
}
}
if (!success) {
s << "Cannot marshal JavaScript function at index " << index
<< " to Java type. Only native type constructors can be marshalled (to java.lang.Class).";
}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@test-app/runtime/src/main/cpp/JsArgToArrayConverter.cpp` around lines 137 -
154, Update the arg->IsFunction() handling in JsArgToArrayConverter to permit
native constructor marshalling only when the target component type is
java.lang.Class or java.lang.Object, matching the scalar converter’s target-type
check. Reject constructors for String, interfaces, and other incompatible
component types before SetConvertedObject, while preserving successful
conversion for Class[] and Object[] and the existing error reporting.

Comment thread test-app/runtime/src/main/cpp/MetadataNode.cpp
@NathanWalker NathanWalker marked this pull request as draft July 16, 2026 19:04
@NathanWalker NathanWalker force-pushed the feat/native-es-classes branch from 3b966c1 to 2b209a5 Compare July 16, 2026 19:55
@NathanWalker NathanWalker marked this pull request as ready for review July 16, 2026 20:10
@NathanWalker NathanWalker requested a review from edusperoni July 16, 2026 20:12
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.

1 participant