Skip to content

Commit 7d150a7

Browse files
committed
chore: pr feedback
1 parent 033b682 commit 7d150a7

6 files changed

Lines changed: 147 additions & 22 deletions

File tree

‎test-app/app/src/main/assets/app/tests/testNativeESClasses.js‎

Lines changed: 75 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -202,6 +202,31 @@ describe("Tests native ES class extensions (class X extends NativeType)", functi
202202
expect(runCount).toBe(1);
203203
});
204204

205+
it("When_two_es_classes_implement_the_same_interface_js_instances_should_stay_distinct", function () {
206+
var aCount = 0;
207+
var bCount = 0;
208+
209+
class EsRunnableA extends java.lang.Runnable {
210+
run() {
211+
aCount++;
212+
}
213+
}
214+
215+
class EsRunnableB extends java.lang.Runnable {
216+
run() {
217+
bCount++;
218+
}
219+
}
220+
221+
new java.lang.Thread(new EsRunnableA()).run();
222+
new java.lang.Thread(new EsRunnableB()).run();
223+
224+
expect(aCount).toBe(1);
225+
expect(bCount).toBe(1);
226+
// DexFactory shares one interface proxy; JS identity is per instance.
227+
expect(EsRunnableA.class.equals(EsRunnableB.class)).toBe(true);
228+
});
229+
205230
it("When_declaring_static_interfaces_the_proxy_should_implement_them", function () {
206231
var ran = { value: false };
207232

@@ -348,6 +373,30 @@ describe("Tests native ES class extensions (class X extends NativeType)", functi
348373
expect(ESPrivateAllocObject.class.newInstance().someMethod()).toBe(1);
349374
});
350375

376+
it("When_java_instantiates_an_es_class_nested_native_construction_should_not_steal_adopt", function () {
377+
class ESNestedAdoptObject extends java.lang.Object {
378+
constructor() {
379+
// Valid before super(): must not consume the pending adopt id
380+
// that belongs to ESNestedAdoptObject.
381+
var list = new java.util.ArrayList();
382+
list.add("nested");
383+
super();
384+
this.list = list;
385+
}
386+
}
387+
388+
var allocated = ESNestedAdoptObject.class.newInstance();
389+
expect(allocated instanceof ESNestedAdoptObject).toBe(true);
390+
expect(allocated.list instanceof java.util.ArrayList).toBe(true);
391+
expect(allocated.list.size()).toBe(1);
392+
expect(allocated.list.get(0)).toBe("nested");
393+
expect(allocated.getClass().getName()).toContain("ESNestedAdoptObject");
394+
395+
var constructed = new ESNestedAdoptObject();
396+
expect(constructed.list.get(0)).toBe("nested");
397+
expect(constructed.getClass().equals(allocated.getClass())).toBe(true);
398+
});
399+
351400
it("When_java_instantiates_an_es_class_super_args_should_not_construct_again", function () {
352401
class ESAdoptOnceObject extends com.tns.tests.DummyClass {
353402
constructor() {
@@ -438,6 +487,17 @@ describe("Tests native ES class extensions (class X extends NativeType)", functi
438487
expect(new ESEagerNamed() instanceof ESEagerNamed).toBe(true);
439488
});
440489

490+
it("When_NativeClass_sets_an_unqualified_android_name_it_should_throw", function () {
491+
expect(function () {
492+
global.NativeClass({
493+
android: {
494+
name: "UnqualifiedName"
495+
}
496+
})(class UnqualifiedNativeClass extends java.lang.Object {
497+
});
498+
}).toThrow();
499+
});
500+
441501
it("When_NativeClass_runs_on_a_worker_it_should_be_a_noop", function (done) {
442502
var worker = new Worker("../shared/Workers/EvalWorker.js");
443503
worker.onmessage = function (msg) {
@@ -462,16 +522,22 @@ describe("Tests native ES class extensions (class X extends NativeType)", functi
462522
});
463523

464524
it("When_anonymous_es_classes_extend_native_types_each_should_get_a_distinct_proxy", function () {
465-
var First = class extends java.lang.Object {
466-
toString() {
467-
return "first anonymous";
468-
}
469-
};
470-
var Second = class extends java.lang.Object {
471-
toString() {
472-
return "second anonymous";
525+
// Array-literal class expressions stay anonymous (no inferred name),
526+
// so both hash as ESClass and exercise the _2 suffix collision path.
527+
var classes = [
528+
class extends java.lang.Object {
529+
toString() {
530+
return "first anonymous";
531+
}
532+
},
533+
class extends java.lang.Object {
534+
toString() {
535+
return "second anonymous";
536+
}
473537
}
474-
};
538+
];
539+
var First = classes[0];
540+
var Second = classes[1];
475541

476542
var firstInstance = new First();
477543
var secondInstance = new Second();

‎test-app/app/src/main/assets/internal/ts_helpers.js‎

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -177,9 +177,18 @@
177177
var name = android && android.name;
178178

179179
if (interfaces && interfaces.length > 0) {
180-
target.interfaces = (target.interfaces && target.interfaces instanceof Array ? target.interfaces.concat(interfaces) : interfaces.slice());
180+
var merged = (target.interfaces && target.interfaces instanceof Array ? target.interfaces.concat(interfaces) : interfaces.slice());
181+
target.interfaces = merged;
182+
// Legacy `.extend()` reads interfaces from the implementation object
183+
// (the prototype). Keep both so downleveled ES5 targets still work.
184+
if (target.prototype) {
185+
target.prototype.interfaces = merged;
186+
}
181187
}
182188
if (name) {
189+
if (name.indexOf(".") === -1) {
190+
throw new Error("NativeClass android.name must be a fully qualified Java class name.");
191+
}
183192
target.nativeClassName = name;
184193
// Accessing `.class` lazily registers the proxy under the explicit name.
185194
void target.class;

‎test-app/runtime/src/main/cpp/CallbackHandlers.cpp‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -95,7 +95,7 @@ bool CallbackHandlers::RegisterInstance(Isolate *isolate, const Local<Object> &j
9595
auto objectManager = runtime->GetObjectManager();
9696

9797
int adoptObjectId = -1;
98-
if (MetadataNode::TryConsumePendingESAdopt(isolate, adoptObjectId)) {
98+
if (MetadataNode::TryConsumePendingESAdopt(isolate, fullClassName, adoptObjectId)) {
9999
// Adopt path: Java already created this object. Bind it to the ES
100100
// construct and do not NewObject again (that would be a second
101101
// instance, or recurse through initInstance).

‎test-app/runtime/src/main/cpp/JsArgConverter.cpp‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -166,7 +166,7 @@ bool JsArgConverter::ConvertArg(const Local<Value> &arg, int index) {
166166
// JEnv caches classes as global refs - mark as global so the dtor doesn't delete it
167167
SetConvertedObject(index, clazz, true /* isGlobal */);
168168
} else {
169-
sprintf(buff, "Cannot convert function to %s at index %d", typeSignature.c_str(), index);
169+
snprintf(buff, sizeof(buff), "Cannot convert function to %s at index %d", typeSignature.c_str(), index);
170170
}
171171
} else if (arg->IsObject()) {
172172
auto context = m_isolate->GetCurrentContext();

‎test-app/runtime/src/main/cpp/MetadataNode.cpp‎

Lines changed: 56 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1178,7 +1178,7 @@ MetadataNode::TypeMetadata* MetadataNode::TryGetTypeMetadata(Isolate* isolate, c
11781178
return nullptr;
11791179
}
11801180

1181-
return reinterpret_cast<TypeMetadata*>(hiddenVal.As<External>()->Value());
1181+
return reinterpret_cast<TypeMetadata*>(hiddenVal.As<External>()->Value(v8::kExternalPointerTypeTagDefault));
11821182
}
11831183

11841184
std::string MetadataNode::TryResolveClassCtorTypeName(Isolate* isolate, const Local<Function>& func) {
@@ -1221,20 +1221,51 @@ std::string SanitizeESClassNamePart(const std::string& name) {
12211221
std::string result;
12221222
result.reserve(name.size());
12231223
for (char c : name) {
1224-
bool isValid = isalpha(c) || isdigit(c) || c == '_';
1224+
auto uc = static_cast<unsigned char>(c);
1225+
bool isValid = isalnum(uc) || c == '_';
12251226
result += isValid ? c : '_';
12261227
}
12271228
return result;
12281229
}
12291230

1231+
std::string BuildESClassProxyIdentity(const std::string& scriptName, const std::string& baseClassName, const std::string& className,
1232+
const std::vector<std::string>& methodOverrides, const std::vector<std::string>& implementedInterfaces) {
1233+
auto sortedMethods = methodOverrides;
1234+
auto sortedInterfaces = implementedInterfaces;
1235+
std::sort(sortedMethods.begin(), sortedMethods.end());
1236+
std::sort(sortedInterfaces.begin(), sortedInterfaces.end());
1237+
1238+
std::string identity = scriptName + "|" + baseClassName + "|" + className;
1239+
for (const auto& method : sortedMethods) {
1240+
identity += "|m:" + method;
1241+
}
1242+
for (const auto& iface : sortedInterfaces) {
1243+
identity += "|i:" + iface;
1244+
}
1245+
return identity;
1246+
}
1247+
1248+
bool TryGetConstructorPrototype(Isolate* isolate, const Local<Function>& ctorFunc, Local<Object>& out) {
1249+
auto context = isolate->GetCurrentContext();
1250+
Local<Value> protoValue;
1251+
if (!ctorFunc->Get(context, V8StringConstants::GetPrototype(isolate)).ToLocal(&protoValue)
1252+
|| protoValue.IsEmpty() || !protoValue->IsObject()) {
1253+
return false;
1254+
}
1255+
out = protoValue.As<Object>();
1256+
return true;
1257+
}
1258+
12301259
// True only for genuine `class` syntax constructors. Function source text is the reliable
12311260
// discriminator: per spec, Function.prototype.toString for a class constructor reproduces the
12321261
// `class` declaration/expression source (possibly behind leading comments/whitespace, which V8
12331262
// does not emit for the class case - the text starts with "class").
12341263
bool IsESClassConstructor(v8::Isolate* isolate, const v8::Local<v8::Function>& func) {
1264+
v8::TryCatch tc(isolate);
12351265
auto context = isolate->GetCurrentContext();
12361266
v8::Local<v8::String> sourceText;
12371267
if (!func->FunctionProtoToString(context).ToLocal(&sourceText)) {
1268+
tc.Reset();
12381269
return false;
12391270
}
12401271

@@ -1431,7 +1462,7 @@ MetadataNode::TypeMetadata* MetadataNode::EnsureExtendedESClass(Isolate* isolate
14311462
scriptName = ArgConverter::ConvertToString(resourceName.As<String>());
14321463
}
14331464

1434-
string extendNameAndLocation = "es" + HashESClassId(scriptName + "|" + baseClassName + "|" + className) + "_" + className;
1465+
string extendNameAndLocation = "es" + HashESClassId(BuildESClassProxyIdentity(scriptName, baseClassName, className, methodOverrides, implementedInterfaces)) + "_" + className;
14351466
string candidate = TNS_PREFIX + CreateFullClassName(baseClassName, extendNameAndLocation);
14361467

14371468
// collision handling for distinct classes that produce the same deterministic name
@@ -1492,17 +1523,26 @@ bool MetadataNode::TryConstructESDerivedInstance(Isolate* isolate, const string&
14921523
return false;
14931524
}
14941525

1526+
// DexFactory collapses every interface extension onto one shared
1527+
// com.tns.gen.<interface> proxy. That name does not identify a single ES
1528+
// class, so Java-born instances stay on the legacy wrapper path.
1529+
if (cacheData.node != nullptr && cacheData.node->IsNodeTypeInterface()) {
1530+
return false;
1531+
}
1532+
14951533
Local<Function> ctor = Local<Function>::New(isolate, *cacheData.extendedCtorFunction);
14961534
auto typeMetadata = TryGetTypeMetadata(isolate, ctor);
14971535
if (typeMetadata == nullptr || !typeMetadata->isESDerived) {
14981536
return false;
14991537
}
15001538

15011539
cache->PendingESAdoptObjectId = javaObjectID;
1540+
cache->PendingESAdoptClassName = proxyClassName;
15021541
TryCatch tc(isolate);
15031542
auto context = isolate->GetCurrentContext();
15041543
bool ok = !ctor->CallAsConstructor(context, 0, nullptr).IsEmpty();
15051544
cache->PendingESAdoptObjectId = -1;
1545+
cache->PendingESAdoptClassName.clear();
15061546
if (!ok) {
15071547
throw NativeScriptException(tc, "Failed to construct ES class for native instance");
15081548
}
@@ -1517,14 +1557,18 @@ bool MetadataNode::TryConstructESDerivedInstance(Isolate* isolate, const string&
15171557
return true;
15181558
}
15191559

1520-
bool MetadataNode::TryConsumePendingESAdopt(Isolate* isolate, int& javaObjectID) {
1560+
bool MetadataNode::TryConsumePendingESAdopt(Isolate* isolate, const string& fullClassName, int& javaObjectID) {
15211561
auto cache = GetMetadataNodeCache(isolate);
15221562
if (cache->PendingESAdoptObjectId == -1) {
15231563
return false;
15241564
}
1565+
if (cache->PendingESAdoptClassName != fullClassName) {
1566+
return false;
1567+
}
15251568

15261569
javaObjectID = cache->PendingESAdoptObjectId;
15271570
cache->PendingESAdoptObjectId = -1;
1571+
cache->PendingESAdoptClassName.clear();
15281572
return true;
15291573
}
15301574

@@ -1618,7 +1662,10 @@ void MetadataNode::InterfaceConstructorCallback(const v8::FunctionCallbackInfo<v
16181662
}
16191663

16201664
if (typeMetadata != nullptr && typeMetadata->isESDerived) {
1621-
auto esImplementationObject = newTargetFunc->Get(context, V8StringConstants::GetPrototype(isolate)).ToLocalChecked().As<Object>();
1665+
Local<Object> esImplementationObject;
1666+
if (!TryGetConstructorPrototype(isolate, newTargetFunc, esImplementationObject)) {
1667+
throw NativeScriptException(string("Cannot resolve the prototype of the ES class constructor."));
1668+
}
16221669

16231670
SetInstanceMetadata(isolate, thiz, node);
16241671
thiz->SetInternalField(static_cast<int>(ObjectManager::MetadataNodeKeys::CallSuper), True(isolate));
@@ -1705,8 +1752,10 @@ void MetadataNode::ClassConstructorCallback(const v8::FunctionCallbackInfo<v8::V
17051752
}
17061753

17071754
if (typeMetadata != nullptr && typeMetadata->isESDerived) {
1708-
auto context = isolate->GetCurrentContext();
1709-
auto implementationObject = newTargetFunc->Get(context, V8StringConstants::GetPrototype(isolate)).ToLocalChecked().As<Object>();
1755+
Local<Object> implementationObject;
1756+
if (!TryGetConstructorPrototype(isolate, newTargetFunc, implementationObject)) {
1757+
throw NativeScriptException(string("Cannot resolve the prototype of the ES class constructor."));
1758+
}
17101759

17111760
SetInstanceMetadata(isolate, thiz, node);
17121761
thiz->SetInternalField(static_cast<int>(ObjectManager::MetadataNodeKeys::CallSuper), True(isolate));

‎test-app/runtime/src/main/cpp/MetadataNode.h‎

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -60,7 +60,7 @@ class MetadataNode {
6060
*/
6161
static bool TryConstructESDerivedInstance(v8::Isolate* isolate, const std::string& proxyClassName, int javaObjectID, v8::Local<v8::Object>& out);
6262

63-
static bool TryConsumePendingESAdopt(v8::Isolate* isolate, int& javaObjectID);
63+
static bool TryConsumePendingESAdopt(v8::Isolate* isolate, const std::string& fullClassName, int& javaObjectID);
6464

6565
static v8::Local<v8::Object> GetImplementationObject(v8::Isolate* isolate, const v8::Local<v8::Object>& object);
6666

@@ -340,9 +340,10 @@ class MetadataNode {
340340

341341
// Java object id being adopted by an in-flight ES construct
342342
// (CreateJSInstanceNative → CallAsConstructor → super()).
343-
// RegisterInstance consumes it so super() binds that id and does
344-
// not NewObject again.
343+
// RegisterInstance consumes it only when fullClassName matches, so
344+
// a nested `new OtherNative()` before super() cannot steal the id.
345345
int PendingESAdoptObjectId = -1;
346+
std::string PendingESAdoptClassName;
346347

347348
~MetadataNodeCache() {
348349
delete MetadataKey;

0 commit comments

Comments
 (0)