diff --git a/test-app/app/src/main/assets/app/mainpage.js b/test-app/app/src/main/assets/app/mainpage.js index 62aeb52eb..0d0f26200 100644 --- a/test-app/app/src/main/assets/app/mainpage.js +++ b/test-app/app/src/main/assets/app/mainpage.js @@ -64,6 +64,7 @@ require("./tests/testClassForNameDiscovery"); require("./tests/testInterfaceImplementation"); require("./tests/testRuntimeImplementedAPIs"); require("./tests/testsInstanceOfOperator"); +require("./tests/testProxyReceivers"); require("./tests/testReleaseNativeCounterpart"); require("./tests/testJSONObjects"); require("./tests/kotlin/companions/testCompanionObjectsSupport"); diff --git a/test-app/app/src/main/assets/app/tests/testProxyReceivers.js b/test-app/app/src/main/assets/app/tests/testProxyReceivers.js new file mode 100644 index 000000000..15b6a0687 --- /dev/null +++ b/test-app/app/src/main/assets/app/tests/testProxyReceivers.js @@ -0,0 +1,284 @@ +// Reactive frameworks (Vue's reactive(), MobX observables) hand Java wrappers +// around behind a JS Proxy, so the Proxy reaches the runtime both as `this` +// and as a method argument. +describe("Proxy receivers and arguments", function () { + + function passThrough() { + return { + get: function (target, key, receiver) { + return Reflect.get(target, key, receiver); + }, + set: function (target, key, value, receiver) { + return Reflect.set(target, key, value, receiver); + } + }; + } + + it("calls an instance method on the proxied Java object", function () { + var sb = new java.lang.StringBuilder(); + var proxy = new Proxy(sb, {}); + + proxy.append("abc"); + + expect(sb.toString()).toBe("abc"); + expect(proxy.length()).toBe(3); + }); + + it("calls an instance method through a Proxy with traps", function () { + var sb = new java.lang.StringBuilder(); + var proxy = new Proxy(sb, passThrough()); + + proxy.append("x").append("y"); + + expect(sb.toString()).toBe("xy"); + }); + + it("calls an instance method through nested Proxies", function () { + var sb = new java.lang.StringBuilder("a"); + var proxy = new Proxy(new Proxy(sb, {}), passThrough()); + + proxy.append("b"); + + expect(sb.toString()).toBe("ab"); + }); + + it("calls a static method through a Proxy of the class", function () { + var IntegerProxy = new Proxy(java.lang.Integer, {}); + var StringProxy = new Proxy(java.lang.String, passThrough()); + + expect(IntegerProxy.parseInt("5")).toBe(5); + expect(StringProxy.format("%d", [java.lang.Integer.valueOf(3)])).toBe("3"); + }); + + it("reads and writes a public Java field through a Proxy", function () { + var point = new android.graphics.Point(1, 2); + var proxy = new Proxy(point, passThrough()); + + expect(proxy.x).toBe(1); + expect(proxy.y).toBe(2); + + proxy.y = 7; + + expect(point.y).toBe(7); + expect(proxy.y).toBe(7); + }); + + it("converts a proxied Java object to the Java toString()", function () { + var sb = new java.lang.StringBuilder("hello"); + var proxy = new Proxy(sb, {}); + + expect(proxy.toString()).toBe("hello"); + expect(String(proxy)).toBe("hello"); + expect("" + proxy).toBe("hello"); + expect(`${proxy}`).toBe("hello"); + }); + + it("passes a proxied Java object as a method argument", function () { + var sb = new java.lang.StringBuilder("item"); + var list = new java.util.ArrayList(); + + list.add(new Proxy(sb, {})); + + expect(list.size()).toBe(1); + expect(list.get(0).equals(sb)).toBe(true); + expect(list.contains(new Proxy(new Proxy(sb, {}), {}))).toBe(true); + }); + + it("assigns a proxied Java object to a Java array element", function () { + var arr = Array.create(java.lang.Object, 1); + var sb = new java.lang.StringBuilder("el"); + + arr[0] = new Proxy(sb, {}); + + expect(arr[0].equals(sb)).toBe(true); + }); + + it("passes a proxied JS array of Java objects as a Java array", function () { + var a = new java.io.File("/a"); + var b = new java.io.File("/b"); + + var plain = java.util.Arrays.asList([a, b]); + var proxied = java.util.Arrays.asList(new Proxy([a, b], passThrough())); + + expect(plain.size()).toBe(2); + expect(proxied.size()).toBe(2); + expect(proxied.get(0).getPath()).toBe("/a"); + expect(proxied.get(1).equals(b)).toBe(true); + }); + + it("passes proxied elements of a JS array as Java objects", function () { + var a = new java.io.File("/a"); + var b = new java.io.File("/b"); + + var list = java.util.Arrays.asList(new Proxy([new Proxy(a, {}), b], {})); + + expect(list.size()).toBe(2); + expect(list.get(0).equals(a)).toBe(true); + }); + + function echoIntArray(arr) { + var echoed = new com.tns.tests.JniReferenceLeakTest(null).echoIntArray(arr); + var result = []; + for (var i = 0; i < echoed.length; i++) { + result.push(echoed[i]); + } + return result; + } + + it("reads a proxied JS array through its get trap when converting to a Java array", function () { + var proxy = new Proxy([], { + get: function (target, key, receiver) { + if (key === "length") { + return 3; + } + if (key === "0" || key === "1" || key === "2") { + return Number(key) * 10; + } + return Reflect.get(target, key, receiver); + } + }); + + expect(echoIntArray(proxy)).toEqual([0, 10, 20]); + }); + + it("passes the values a reactive-style get trap unwraps from refs", function () { + function ref(value) { + return { __v_isRef: true, value: value }; + } + var proxy = new Proxy([ref(1), 2, ref(3)], { + get: function (target, key, receiver) { + var v = Reflect.get(target, key, receiver); + return v !== null && typeof v === "object" && v.__v_isRef ? v.value : v; + } + }); + + expect(echoIntArray(proxy)).toEqual([1, 2, 3]); + }); + + it("throws the get trap's error when converting a proxied JS array", function () { + var proxy = new Proxy([1, 2], { + get: function (target, key, receiver) { + if (key === "1") { + throw new Error("boom"); + } + return Reflect.get(target, key, receiver); + } + }); + + expect(function () { echoIntArray(proxy); }).toThrowError(/boom/); + }); + + it("converts a proxied Java object or typed array from its target, bypassing traps", function () { + var lying = { + get: function (target, key, receiver) { + if (key === "length") { + return 0; + } + if (key === "0") { + return 99; + } + var v = Reflect.get(target, key, target); + return typeof v === "function" ? v.bind(target) : v; + } + }; + var source = new java.util.ArrayList(); + source.add("a"); + source.add("b"); + var bytes = new Uint8Array([7, 8, 9]); + + var copy = new java.util.ArrayList(new Proxy(source, lying)); + var holder = new java.util.ArrayList(); + holder.add(new Proxy(bytes, lying)); + var buffer = holder.toString(); + + expect(copy.size()).toBe(2); + expect(copy.get(1)).toBe("b"); + expect(buffer).toMatch(/ByteBuffer.*cap=3/); + }); + + it("throws when the receiver is a revoked Proxy", function () { + var sb = new java.lang.StringBuilder(); + var revocable = Proxy.revocable(sb, {}); + var append = revocable.proxy.append; + var xGetter = Object.getOwnPropertyDescriptor(android.graphics.Point.prototype, "x").get; + var pointRevocable = Proxy.revocable(new android.graphics.Point(1, 2), {}); + + revocable.revoke(); + pointRevocable.revoke(); + + expect(function () { append.call(revocable.proxy, "x"); }).toThrowError(TypeError, /revoked Proxy/); + expect(function () { xGetter.call(pointRevocable.proxy); }).toThrowError(TypeError, /revoked Proxy/); + expect(sb.length()).toBe(0); + }); + + it("throws when a revoked Proxy is passed as an argument", function () { + var revocable = Proxy.revocable(new java.lang.StringBuilder(), {}); + var list = new java.util.ArrayList(); + + revocable.revoke(); + + expect(function () { list.add(revocable.proxy); }).toThrow(); + expect(list.size()).toBe(0); + }); + + it("throws when the Proxy target is not a Java object", function () { + var sb = new java.lang.StringBuilder(); + var append = sb.append; + var toString = sb.toString; + + expect(function () { append.call(new Proxy({}, {}), "x"); }).toThrowError(TypeError, /not a Java object/); + expect(function () { toString.call(new Proxy([], {})); }).toThrowError(TypeError, /not a Java object/); + expect(sb.length()).toBe(0); + }); + + it("throws instead of crashing when the receiver is a plain object", function () { + var append = new java.lang.StringBuilder().append; + + expect(function () { append.call({}, "x"); }).toThrow(); + }); + + it("supports instanceof on a proxied Java object", function () { + var sb = new java.lang.StringBuilder(); + var proxy = new Proxy(sb, {}); + var revocable = Proxy.revocable(new java.lang.StringBuilder(), {}); + revocable.revoke(); + + // Class checks walk the Proxy's prototype chain (the target's); + // interface checks go through the runtime's Symbol.hasInstance. + expect(proxy instanceof java.lang.StringBuilder).toBe(true); + expect(proxy instanceof java.lang.CharSequence).toBe(true); + expect(proxy instanceof java.util.List).toBe(false); + expect(revocable.proxy instanceof java.lang.CharSequence).toBe(false); + expect(function () { return revocable.proxy instanceof java.lang.StringBuilder; }).toThrowError(TypeError); + }); + + it("calls overridden and super methods of an extended class instance through a Proxy", function () { + var MyObject = java.lang.Object.extend({ + toString: function () { + return "custom:" + (this.super.hashCode() === this.hashCode()); + } + }); + var instance = new MyObject(); + var proxy = new Proxy(instance, passThrough()); + + expect(proxy.toString()).toBe("custom:true"); + expect(String(proxy)).toBe("custom:true"); + expect(proxy.hashCode()).toBe(instance.hashCode()); + expect(proxy.super.hashCode()).toBe(instance.hashCode()); + expect(proxy.equals(instance)).toBe(true); + }); + + it("releases the native counterpart of the Proxy target", function () { + var target = new java.lang.Object(); + var proxy = new Proxy(target, passThrough()); + var revocable = Proxy.revocable(new java.lang.Object(), {}); + revocable.revoke(); + + global.__releaseNativeCounterpart(proxy); + + expect(function () { global.__releaseNativeCounterpart(target); }).toThrowError(/non native object/); + expect(function () { global.__releaseNativeCounterpart(proxy); }).toThrowError(/non native object/); + expect(function () { target.toString(); }).toThrowError(/no longer has available Java instance counterpart/); + expect(function () { global.__releaseNativeCounterpart(revocable.proxy); }).toThrowError(/non native object/); + }); +}); diff --git a/test-app/app/src/main/assets/app/tests/testReleaseNativeCounterpart.js b/test-app/app/src/main/assets/app/tests/testReleaseNativeCounterpart.js index 727a82cb8..28b28dc15 100644 --- a/test-app/app/src/main/assets/app/tests/testReleaseNativeCounterpart.js +++ b/test-app/app/src/main/assets/app/tests/testReleaseNativeCounterpart.js @@ -68,6 +68,34 @@ describe("Test native counterpart release", function () { expect(errorMessage).toBe("Argument is not an object!"); }); + it("Releasing an extended instance also releases its super object", function () { + var MyObject = java.lang.Object.extend({ + toString: function () { return "custom"; } + }); + var instance = new MyObject(); + var sup = instance.super; + + global.__releaseNativeCounterpart(instance); + + expect(function () { sup.hashCode(); }).toThrowError(/no longer has available Java instance counterpart/); + expect(function () { instance.hashCode(); }).toThrowError(/no longer has available Java instance counterpart/); + expect(function () { global.__releaseNativeCounterpart(sup); }).toThrowError("Trying to release a non native object!"); + }); + + it("Releasing through the super object releases the instance", function () { + var MyObject = java.lang.Object.extend({ + toString: function () { return "custom"; } + }); + var instance = new MyObject(); + var sup = instance.super; + + global.__releaseNativeCounterpart(sup); + + expect(function () { instance.hashCode(); }).toThrowError(/no longer has available Java instance counterpart/); + expect(function () { sup.hashCode(); }).toThrowError(/no longer has available Java instance counterpart/); + expect(function () { global.__releaseNativeCounterpart(instance); }).toThrowError("Trying to release a non native object!"); + }); + it("Calling the __releaseNativeCounterpart function with 0 arguments should throw exception", function(){ var errorMessage = ""; diff --git a/test-app/runtime/src/main/cpp/CallbackHandlers.cpp b/test-app/runtime/src/main/cpp/CallbackHandlers.cpp index 8d2b18135..00bd2576c 100644 --- a/test-app/runtime/src/main/cpp/CallbackHandlers.cpp +++ b/test-app/runtime/src/main/cpp/CallbackHandlers.cpp @@ -611,7 +611,7 @@ CallbackHandlers::GetImplementedInterfaces(JEnv &env, const Local &imple vector interfacesToImplement; auto isolate = v8::Isolate::GetCurrent(); - auto context = implementationObject->GetCreationContext(isolate).ToLocalChecked(); + auto context = GetCreationContextOrCurrent(isolate, implementationObject); Local interfacesName = String::NewFromUtf8Literal(isolate, "interfaces"); Local prop; if (implementationObject->Get(context, interfacesName).ToLocal(&prop) && !prop.IsEmpty() && prop->IsArray()) { @@ -657,7 +657,7 @@ CallbackHandlers::GetMethodOverrides(JEnv &env, const Local &implementat vector methodNames; auto isolate = v8::Isolate::GetCurrent(); - auto context = implementationObject->GetCreationContext(isolate).ToLocalChecked(); + auto context = GetCreationContextOrCurrent(isolate, implementationObject); auto propNames = implementationObject->GetOwnPropertyNames(context).ToLocalChecked(); for (int i = 0; i < propNames->Length(); i++) { auto name = propNames->Get(context, i).ToLocalChecked().As(); diff --git a/test-app/runtime/src/main/cpp/FieldAccessor.cpp b/test-app/runtime/src/main/cpp/FieldAccessor.cpp index a2b7bb274..4801568e6 100644 --- a/test-app/runtime/src/main/cpp/FieldAccessor.cpp +++ b/test-app/runtime/src/main/cpp/FieldAccessor.cpp @@ -333,6 +333,9 @@ void FieldAccessor::SetJavaField(Isolate* isolate, const Local& target, //TODO: validate valie is a string; result = ArgConverter::ConvertToJavaString(value); } else { + if (value->IsProxy() && !ObjectManager::UnwrapProxy(value)->IsObject()) { + throw NativeScriptException("Cannot assign a revoked Proxy to field '" + fieldMetadata.name + "'"); + } auto context = isolate->GetCurrentContext(); auto objectWithHiddenID = value->ToObject(context).ToLocalChecked(); result = objectManager->GetJavaObjectByJsObject(objectWithHiddenID); diff --git a/test-app/runtime/src/main/cpp/JsArgConverter.cpp b/test-app/runtime/src/main/cpp/JsArgConverter.cpp index e5d82f0bd..5884dc8a5 100644 --- a/test-app/runtime/src/main/cpp/JsArgConverter.cpp +++ b/test-app/runtime/src/main/cpp/JsArgConverter.cpp @@ -112,44 +112,69 @@ tns::BufferCastType JsArgConverter::GetCastType(const v8::Local &arg, int index) { +bool JsArgConverter::ConvertArg(const Local &jsArg, int index) { bool success = false; char buff[1024]; const auto &typeSignature = m_tokens.at(index); + // Classify by the target, which holds the Java link and internal slots; + // array elements are read through the Proxy itself. + auto arg = jsArg.IsEmpty() ? jsArg : ObjectManager::UnwrapProxy(jsArg); + if (arg.IsEmpty()) { SetConvertedObject(index, nullptr); success = false; + } else if (jsArg->IsProxy() && !arg->IsObject()) { + SetConvertedObject(index, nullptr); + snprintf(buff, sizeof(buff), "Cannot convert a revoked Proxy to %s at index %d", typeSignature.c_str(), index); } else if (arg->IsArray()) { success = typeSignature[0] == '['; + bool trapThrew = false; if (success) { - auto jsArr = Local::Cast(arg); - success = ConvertJavaScriptArray(jsArr, index); + Local jsArr; + if (jsArg->IsProxy()) { + TryCatch tc(m_isolate); + auto context = m_isolate->GetCurrentContext(); + if (!ObjectManager::ReadArrayThroughProxy(context, jsArg.As()).ToLocal(&jsArr)) { + trapThrew = true; + success = false; + string reason = tc.HasCaught() + ? ArgConverter::ConvertToString(tc.Exception()->ToString(context).FromMaybe(Local())) + : string("unknown error"); + snprintf(buff, sizeof(buff), "%s", reason.c_str()); + } + } else { + jsArr = arg.As(); + } + + if (success) { + success = ConvertJavaScriptArray(jsArr, index); + } } - if (!success) { - sprintf(buff, "Cannot convert array to %s at index %d", typeSignature.c_str(), index); + if (!success && !trapThrew) { + snprintf(buff, sizeof(buff), "Cannot convert array to %s at index %d", typeSignature.c_str(), index); } } else if (arg->IsNumber() || arg->IsNumberObject()) { success = ConvertJavaScriptNumber(arg, index); if (!success) { - sprintf(buff, "Cannot convert number to %s at index %d", typeSignature.c_str(), index); + snprintf(buff, sizeof(buff), "Cannot convert number to %s at index %d", typeSignature.c_str(), index); } } else if (arg->IsBoolean() || arg->IsBooleanObject()) { success = ConvertJavaScriptBoolean(arg, index); if (!success) { - sprintf(buff, "Cannot convert boolean to %s at index %d", typeSignature.c_str(), index); + snprintf(buff, sizeof(buff), "Cannot convert boolean to %s at index %d", typeSignature.c_str(), index); } } else if (arg->IsString() || arg->IsStringObject()) { success = ConvertJavaScriptString(arg, index); if (!success) { - sprintf(buff, "Cannot convert string to %s at index %d", typeSignature.c_str(), index); + snprintf(buff, sizeof(buff), "Cannot convert string to %s at index %d", typeSignature.c_str(), index); } } else if (arg->IsObject()) { auto context = m_isolate->GetCurrentContext(); @@ -356,7 +381,7 @@ bool JsArgConverter::ConvertArg(const Local &arg, int index) { if (success) { SetConvertedObject(index, obj.Move(), obj.IsGlobal()); } else { - sprintf(buff, "Cannot convert object to %s at index %d", typeSignature.c_str(), + snprintf(buff, sizeof(buff), "Cannot convert object to %s at index %d", typeSignature.c_str(), index); } break; diff --git a/test-app/runtime/src/main/cpp/JsArgConverter.h b/test-app/runtime/src/main/cpp/JsArgConverter.h index ba4678d88..14a098e3e 100644 --- a/test-app/runtime/src/main/cpp/JsArgConverter.h +++ b/test-app/runtime/src/main/cpp/JsArgConverter.h @@ -52,7 +52,7 @@ namespace tns { private: - bool ConvertArg(const v8::Local& arg, int index); + bool ConvertArg(const v8::Local& jsArg, int index); bool ConvertJavaScriptArray(const v8::Local& jsArr, int index); diff --git a/test-app/runtime/src/main/cpp/JsArgToArrayConverter.cpp b/test-app/runtime/src/main/cpp/JsArgToArrayConverter.cpp index 63216a06f..3e24d986a 100644 --- a/test-app/runtime/src/main/cpp/JsArgToArrayConverter.cpp +++ b/test-app/runtime/src/main/cpp/JsArgToArrayConverter.cpp @@ -58,7 +58,7 @@ JsArgToArrayConverter::JsArgToArrayConverter(const v8::FunctionCallbackInfo context, const Local &arg, int index) { +bool JsArgToArrayConverter::ConvertArg(Local context, const Local &jsArg, int index) { bool success = false; stringstream s; @@ -67,9 +67,15 @@ bool JsArgToArrayConverter::ConvertArg(Local context, const LocalIsProxy() && !arg->IsObject()) { + s << "Cannot marshal a revoked Proxy at index " << index << " to Java type."; + success = false; } else if (arg->IsInt32() && (returnType == Type::Int || returnType == Type::Null)) { jint value = arg->Int32Value(context).ToChecked(); auto javaObject = JType::NewInt(env, value); diff --git a/test-app/runtime/src/main/cpp/JsArgToArrayConverter.h b/test-app/runtime/src/main/cpp/JsArgToArrayConverter.h index 853989e79..b8479aa8b 100644 --- a/test-app/runtime/src/main/cpp/JsArgToArrayConverter.h +++ b/test-app/runtime/src/main/cpp/JsArgToArrayConverter.h @@ -38,7 +38,7 @@ class JsArgToArrayConverter { }; private: - bool ConvertArg(v8::Local context, const v8::Local& arg, int index); + bool ConvertArg(v8::Local context, const v8::Local& jsArg, int index); void SetConvertedObject(JEnv& env, int index, jobject obj, bool isGlobal = false); diff --git a/test-app/runtime/src/main/cpp/MetadataNode.cpp b/test-app/runtime/src/main/cpp/MetadataNode.cpp index 83e77fa34..a981cc4e2 100644 --- a/test-app/runtime/src/main/cpp/MetadataNode.cpp +++ b/test-app/runtime/src/main/cpp/MetadataNode.cpp @@ -35,10 +35,9 @@ void MetadataNode::Init(Isolate* isolate) { isolate, ArgConverter::ConvertToV8String(isolate, "tns::PackageKey")); } -// Deliberately not V8GetPrivateValue: that one resolves the creation context, -// which hard-crashes on values that have none (a revoked proxy), and throws -// when the lookup comes back Nothing. Callers here take arbitrary values, so a -// failed read must also leave no exception pending for whoever calls next. +// Deliberately not V8GetPrivateValue: that one throws when the lookup comes +// back Nothing. Callers here take arbitrary values, so a failed read must also +// leave no exception pending for whoever calls next. static void* TryReadPrivateExternal(Isolate* isolate, const Local& value, Persistent* key) { if (key == nullptr) { return nullptr; @@ -60,6 +59,43 @@ static void* TryReadPrivateExternal(Isolate* isolate, const Local& value return hidden.As()->Value(v8::kExternalPointerTypeTagDefault); } +static NativeScriptException ProxyTypeError(Isolate* isolate, const string& message) { + return NativeScriptException(isolate, Exception::TypeError(ArgConverter::ConvertToV8String(isolate, message)), message); +} + +// Reactive frameworks put Java wrappers behind a Proxy, which carries none of +// the wrapper's internal fields or private symbols. +static Local ResolveProxyReceiver(Isolate* isolate, const Local& receiver, const char* verb, const string& member, bool requireJavaObject) { + if (!receiver->IsProxy()) { + return receiver; + } + + auto target = ObjectManager::UnwrapProxy(receiver); + if (!target->IsObject()) { + throw ProxyTypeError(isolate, string("Cannot ") + verb + " '" + member + "' on a revoked Proxy"); + } + + auto object = target.As(); + if (requireJavaObject && !ObjectManager::IsJsRuntimeObject(object)) { + throw ProxyTypeError(isolate, string("Cannot ") + verb + " '" + member + "' on a Proxy whose target is not a Java object"); + } + + return object; +} + +static Local ResolveImplementationObject(Isolate* isolate, const Local& implementationObject) { + if (!implementationObject->IsProxy()) { + return implementationObject; + } + + auto target = ObjectManager::UnwrapProxy(implementationObject); + if (!target->IsObject()) { + throw ProxyTypeError(isolate, "Cannot use a revoked Proxy as an implementation object"); + } + + return target.As(); +} + bool MetadataNode::TryGetInstanceTypeName(Isolate* isolate, const Local& value, std::string& out) { auto cache = GetMetadataNodeCache(isolate); auto node = static_cast(TryReadPrivateExternal(isolate, value, cache->MetadataKey)); @@ -211,7 +247,11 @@ bool MetadataNode::IsNodeTypeInterface() { } string MetadataNode::GetTypeMetadataName(Isolate* isolate, Local& value) { - auto data = GetTypeMetadata(isolate, value.As()); + auto target = ObjectManager::UnwrapProxy(value); + auto data = target->IsFunction() ? GetTypeMetadata(isolate, target.As()) : nullptr; + if (data == nullptr) { + throw NativeScriptException(string("Expected a Java class or interface constructor")); + } return data->name; } @@ -336,9 +376,12 @@ void MetadataNode::SetClassAccessor(Local& ctorFunction) { void MetadataNode::ClassAccessorGetterCallback(const FunctionCallbackInfo& info) { try { - auto thiz = info.This(); auto isolate = info.GetIsolate(); - auto data = GetTypeMetadata(isolate, thiz.As()); + auto thiz = ResolveProxyReceiver(isolate, info.This(), "read", "class", false); + auto data = thiz->IsFunction() ? GetTypeMetadata(isolate, thiz.As()) : nullptr; + if (data == nullptr) { + throw NativeScriptException(string("'class' can only be read from a Java class constructor")); + } auto value = CallbackHandlers::FindClass(isolate, data->name); info.GetReturnValue().Set(value); @@ -360,7 +403,7 @@ void MetadataNode::NullObjectAccessorGetterCallback(const FunctionCallbackInfo hiddenVal; V8GetPrivateValue(isolate, thiz, V8StringConstants::GetNullNodeName(isolate), hiddenVal); if (hiddenVal.IsEmpty()) { @@ -405,9 +448,11 @@ void MetadataNode::NullValueOfCallback(const FunctionCallbackInfo& args) void MetadataNode::FieldAccessorGetterCallback(const FunctionCallbackInfo& info) { try { - auto thiz = info.This(); auto fieldCallbackData = reinterpret_cast(info.Data().As()->Value(v8::kExternalPointerTypeTagDefault)); auto &fieldCallbackMetadata = fieldCallbackData->metadata; + auto thiz = ResolveProxyReceiver(info.GetIsolate(), info.This(), + "read Java field", fieldCallbackMetadata.getName(), + !fieldCallbackMetadata.isStatic); auto objectManager = Runtime::GetRuntime(info.GetIsolate())->GetObjectManager(); @@ -437,9 +482,11 @@ void MetadataNode::FieldAccessorGetterCallback(const FunctionCallbackInfo void MetadataNode::FieldAccessorSetterCallback(const FunctionCallbackInfo& info) { auto value = info[0]; try { - auto thiz = info.This(); auto fieldCallbackData = reinterpret_cast(info.Data().As()->Value(v8::kExternalPointerTypeTagDefault)); auto &fieldCallbackMetadata = fieldCallbackData->metadata; + auto thiz = ResolveProxyReceiver(info.GetIsolate(), info.This(), + "write Java field", fieldCallbackMetadata.getName(), + !fieldCallbackMetadata.isStatic); auto objectManager = Runtime::GetRuntime(info.GetIsolate())->GetObjectManager(); @@ -532,8 +579,8 @@ void MetadataNode::PropertyAccessorSetterCallback(const FunctionCallbackInfo& info) { try { - auto thiz = info.This(); auto isolate = info.GetIsolate(); + auto thiz = ResolveProxyReceiver(isolate, info.This(), "read", "super", true); auto key = ArgConverter::ConvertToV8String(isolate, "supervalue"); Local hidenVal; V8GetPrivateValue(isolate, thiz, key, hidenVal); @@ -553,6 +600,25 @@ void MetadataNode::SuperAccessorGetterCallback(const FunctionCallbackInfo V8SetPrivateValue(isolate, thiz, key, superValue); objectManager->CloneLink(thiz, superValue); + // The alias is tracked on the object that owns the link, which for + // the TypeScript layout is the prototype rather than `thiz`. + auto owner = ObjectManager::IsJsRuntimeObject(thiz) ? ObjectManager::GetJSInstanceInfoOwner(thiz) : Local(); + if (owner.IsEmpty()) { + owner = thiz; + } + V8SetPrivateValue(isolate, superValue, ArgConverter::ConvertToV8String(isolate, "superowner"), owner); + auto aliasesKey = ArgConverter::ConvertToV8String(isolate, "superaliases"); + Local aliasesValue; + V8GetPrivateValue(isolate, owner, aliasesKey, aliasesValue); + Local aliases; + if (!aliasesValue.IsEmpty() && aliasesValue->IsArray()) { + aliases = aliasesValue.As(); + } else { + aliases = Array::New(isolate); + V8SetPrivateValue(isolate, owner, aliasesKey, aliases); + } + aliases->CreateDataProperty(context, aliases->Length(), superValue).Check(); + DEBUG_WRITE("superValue.GetPrototype=%d", superValue->GetPrototype().As()->GetIdentityHash()); auto node = GetInstanceMetadata(isolate, thiz); @@ -1148,6 +1214,9 @@ Persistent* MetadataNode::GetPersistentConstructorFunction(Isolate* is MetadataNode::TypeMetadata* MetadataNode::GetTypeMetadata(Isolate* isolate, const Local& value) { Local hiddenVal; V8GetPrivateValue(isolate, value, String::NewFromUtf8(isolate, "typemetadata").ToLocalChecked(), hiddenVal); + if (hiddenVal.IsEmpty() || !hiddenVal->IsExternal()) { + return nullptr; + } auto data = reinterpret_cast(hiddenVal.As()->Value(v8::kExternalPointerTypeTagDefault)); return data; @@ -1239,7 +1308,7 @@ void MetadataNode::InterfaceConstructorCallback(const v8::FunctionCallbackInfoIsObject()) { throw NativeScriptException(string("First argument must be implementation object")); } - implementationObject = info[0]->ToObject(context).ToLocalChecked(); + implementationObject = ResolveImplementationObject(isolate, info[0].As()); } else if (info.Length() == 2) { if (!info[0]->IsString()) { throw NativeScriptException(string("First argument must be string")); @@ -1249,7 +1318,7 @@ void MetadataNode::InterfaceConstructorCallback(const v8::FunctionCallbackInfoToString(context).ToLocalChecked(); - implementationObject = info[1]->ToObject(context).ToLocalChecked(); + implementationObject = ResolveImplementationObject(isolate, info[1].As()); } else { throw NativeScriptException(string("Invalid number of arguments")); } @@ -1360,16 +1429,17 @@ void MetadataNode::MethodCallback(const v8::FunctionCallbackInfo& inf } } - auto thiz = info.This(); + auto thiz = ResolveProxyReceiver(info.GetIsolate(), info.This(), + "call Java method", methodName, !first.isStatic); auto isSuper = false; - if (!first.isStatic) { + if (!first.isStatic && ObjectManager::IsJsRuntimeObject(thiz)) { auto superValue = thiz->GetInternalField(static_cast(ObjectManager::MetadataNodeKeys::CallSuper)).As(); isSuper = !superValue.IsEmpty() && superValue->IsTrue(); } if ((argLength == 0) && (methodName == V8StringConstants::VALUE_OF)) { - info.GetReturnValue().Set(thiz); + info.GetReturnValue().Set(info.This()); } else { bool isFromInterface = initialCallbackData->node->IsNodeTypeInterface(); CallbackHandlers::CallJavaMethod(thiz, *className, methodName, entry, isFromInterface, first.isStatic, isSuper, info); @@ -1452,7 +1522,7 @@ Local MetadataNode::GetImplementationObject(Isolate* isolate, const Loca return implementationObject; } - auto context = object->GetCreationContext(isolate).ToLocalChecked(); + auto context = GetCreationContextOrCurrent(isolate, object); if (object->HasOwnProperty(context, V8StringConstants::GetIsPrototypeImplementationObject(isolate)).ToChecked()) { auto v8Prototype = V8StringConstants::GetPrototype(isolate); auto maybeHasOwnProperty = object->HasOwnProperty(context, v8Prototype); @@ -1726,6 +1796,7 @@ void MetadataNode::ExtendMethodCallback(const v8::FunctionCallbackInfo(info.Data().As()->Value(v8::kExternalPointerTypeTagDefault)); @@ -1970,7 +2041,11 @@ bool MetadataNode::GetExtendLocation(v8::Isolate* isolate, string& extendLocatio } MetadataNode* MetadataNode::GetNodeFromHandle(const Local& value) { - auto node = GetInstanceMetadata(Isolate::GetCurrent(), value); + auto target = ObjectManager::UnwrapProxy(value); + if (!target->IsObject()) { + return nullptr; + } + auto node = GetInstanceMetadata(Isolate::GetCurrent(), target.As()); return node; } @@ -2337,37 +2412,48 @@ void MetadataNode::RegisterSymbolHasInstanceCallback(Isolate* isolate, MetadataE } void MetadataNode::SymbolHasInstanceCallback(const v8::FunctionCallbackInfo& info) { - auto length = info.Length(); - if (length != 1) { - throw NativeScriptException(string("Symbol.hasInstance must take exactly 1 argument")); - } + try { + auto length = info.Length(); + if (length != 1) { + throw NativeScriptException(string("Symbol.hasInstance must take exactly 1 argument")); + } - auto arg = info[0]; - if (arg->IsNullOrUndefined()) { - info.GetReturnValue().Set(false); - return; - } + auto arg = info[0]; + if (!arg->IsObject()) { + info.GetReturnValue().Set(false); + return; + } - auto clazz = reinterpret_cast(info.Data().As()->Value(v8::kExternalPointerTypeTagDefault)); + auto clazz = reinterpret_cast(info.Data().As()->Value(v8::kExternalPointerTypeTagDefault)); - auto isolate = info.GetIsolate(); - auto context = isolate->GetCurrentContext(); - auto runtime = Runtime::GetRuntime(isolate); + auto isolate = info.GetIsolate(); + auto runtime = Runtime::GetRuntime(isolate); - auto objectManager = runtime->GetObjectManager(); - auto obj = objectManager->GetJavaObjectByJsObject(arg->ToObject(context).ToLocalChecked()); + auto objectManager = runtime->GetObjectManager(); + auto obj = objectManager->GetJavaObjectByJsObject(arg.As()); - if (obj.IsNull()) { - // Couldn't find a corresponding java instance counterpart. This could happen - // if the "instanceof" operator is invoked on a pure javascript instance - info.GetReturnValue().Set(false); - return; - } + if (obj.IsNull()) { + // Couldn't find a corresponding java instance counterpart. This could happen + // if the "instanceof" operator is invoked on a pure javascript instance + info.GetReturnValue().Set(false); + return; + } - JEnv env; - auto isInstanceOf = env.IsInstanceOf(obj, clazz); + JEnv env; + auto isInstanceOf = env.IsInstanceOf(obj, clazz); - info.GetReturnValue().Set(isInstanceOf); + info.GetReturnValue().Set(isInstanceOf); + } catch (NativeScriptException& e) { + e.ReThrowToV8(); + } catch (std::exception e) { + stringstream ss; + ss << "Error: c++ exception: " << e.what() << endl; + NativeScriptException nsEx(ss.str()); + nsEx.ReThrowToV8(); + } catch (...) { + NativeScriptException nsEx(std::string("Error: c++ exception!")); + nsEx.ReThrowToV8(); + } } std::string MetadataNode::GetJniClassName(MetadataEntry& entry) { diff --git a/test-app/runtime/src/main/cpp/MethodCache.cpp b/test-app/runtime/src/main/cpp/MethodCache.cpp index a94196313..97531ce2e 100644 --- a/test-app/runtime/src/main/cpp/MethodCache.cpp +++ b/test-app/runtime/src/main/cpp/MethodCache.cpp @@ -12,6 +12,7 @@ #include "NumericCasts.h" #include "NativeScriptException.h" #include "Runtime.h" +#include "ObjectManager.h" #include using namespace v8; @@ -132,8 +133,10 @@ string MethodCache::EncodeSignature(const string& className, const string& metho return sig; } -string MethodCache::GetType(Isolate* isolate, const v8::Local& value) { +string MethodCache::GetType(Isolate* isolate, const v8::Local& jsValue) { string type; + // Overloads resolve against what a Proxy wraps. + auto value = ObjectManager::UnwrapProxy(jsValue); if (value->IsObject()) { auto context = isolate->GetCurrentContext(); diff --git a/test-app/runtime/src/main/cpp/NumericCasts.cpp b/test-app/runtime/src/main/cpp/NumericCasts.cpp index 48e82c523..ba1806292 100644 --- a/test-app/runtime/src/main/cpp/NumericCasts.cpp +++ b/test-app/runtime/src/main/cpp/NumericCasts.cpp @@ -37,7 +37,7 @@ CastType NumericCasts::GetCastType(Isolate* isolate, const Local& object Local NumericCasts::GetCastValue(const Local& object) { auto isolate = v8::Isolate::GetCurrent(); - auto context = object->GetCreationContext(isolate).ToLocalChecked(); + auto context = GetCreationContextOrCurrent(isolate, object); Local value; object->Get(context, V8StringConstants::GetValue(isolate)).ToLocal(&value); return value; @@ -364,7 +364,7 @@ void NumericCasts::MarkJsObject(Isolate* isolate, const Local& object, C auto key = ArgConverter::ConvertToV8String(isolate, s_castMarker); auto type = Integer::New(isolate, static_cast(castType)); V8SetPrivateValue(isolate, object, key, type); - auto context = object->GetCreationContext(isolate).ToLocalChecked(); + auto context = GetCreationContextOrCurrent(isolate, object); object->Set(context, V8StringConstants::GetValue(isolate), value); DEBUG_WRITE("MarkJsObject: Marking js object: %d with cast type: %d", object->GetIdentityHash(), castType); diff --git a/test-app/runtime/src/main/cpp/ObjectManager.cpp b/test-app/runtime/src/main/cpp/ObjectManager.cpp index 56c54fdcd..87a73caf0 100644 --- a/test-app/runtime/src/main/cpp/ObjectManager.cpp +++ b/test-app/runtime/src/main/cpp/ObjectManager.cpp @@ -131,39 +131,92 @@ JniLocalRef ObjectManager::GetJavaObjectByJsObject( ObjectManager::JSInstanceInfo* ObjectManager::GetJSInstanceInfo( const Local& object) { - JSInstanceInfo* jsInstanceInfo = nullptr; + if (object->IsProxy()) { + auto target = UnwrapProxy(object); + return target->IsObject() ? GetJSInstanceInfo(target.As()) + : nullptr; + } if (IsJsRuntimeObject(object)) { return GetJSInstanceInfoFromRuntimeObject(object); } return nullptr; } -ObjectManager::JSInstanceInfo* -ObjectManager::GetJSInstanceInfoFromRuntimeObject(const Local& object) { - HandleScope handleScope(m_isolate); +Local ObjectManager::UnwrapProxy(Local value) { + while (value->IsProxy()) { + value = value.As()->GetTarget(); + } + return value; +} +MaybeLocal ObjectManager::ReadArrayThroughProxy(Local context, + Local proxy) { + auto isolate = Isolate::GetCurrent(); + EscapableHandleScope scope(isolate); + + Local lengthValue; + if (!proxy->Get(context, ArgConverter::ConvertToV8String(isolate, "length")) + .ToLocal(&lengthValue)) { + return {}; + } + double length; + if (!lengthValue->NumberValue(context).To(&length)) { + return {}; + } + if (!(length > 0)) { + length = 0; + } + // Java arrays are indexed by jint. + if (length > INT32_MAX) { + isolate->ThrowException(Exception::RangeError(ArgConverter::ConvertToV8String( + isolate, "Array length exceeds the maximum Java array size"))); + return {}; + } + + auto count = static_cast(length); + auto snapshot = Array::New(isolate, static_cast(count)); + for (uint32_t i = 0; i < count; i++) { + Local element; + if (!proxy->Get(context, i).ToLocal(&element) || + snapshot->CreateDataProperty(context, i, element).IsNothing()) { + return {}; + } + } + return scope.Escape(snapshot); +} + +Local ObjectManager::GetJSInstanceInfoOwner(const Local& object) { const int jsInfoIdx = static_cast(MetadataNodeKeys::JsInfo); - auto jsInfo = object->GetInternalField(jsInfoIdx).As(); - if (jsInfo->IsUndefined()) { - // Typescript object layout has an object instance as child of the actual - // registered instance. checking for that - auto prototypeObject = object->GetPrototype().As(); - - if (!prototypeObject.IsEmpty() && prototypeObject->IsObject()) { - DEBUG_WRITE("GetJSInstanceInfo: need to check prototype :%d", - prototypeObject->GetIdentityHash()); - if (IsJsRuntimeObject(prototypeObject)) { - jsInfo = prototypeObject->GetInternalField(jsInfoIdx).As(); - } + if (object->GetInternalField(jsInfoIdx).As()->IsExternal()) { + return object; + } + + // Typescript object layout has an object instance as child of the actual + // registered instance. checking for that + auto prototypeObject = object->GetPrototype().As(); + if (!prototypeObject.IsEmpty() && prototypeObject->IsObject()) { + DEBUG_WRITE("GetJSInstanceInfo: need to check prototype :%d", + prototypeObject->GetIdentityHash()); + if (IsJsRuntimeObject(prototypeObject) && + prototypeObject->GetInternalField(jsInfoIdx).As()->IsExternal()) { + return prototypeObject; } } - if (!jsInfo.IsEmpty() && jsInfo->IsExternal()) { - auto external = jsInfo.As(); - return static_cast(external->Value(v8::kExternalPointerTypeTagDefault)); + return Local(); +} + +ObjectManager::JSInstanceInfo* +ObjectManager::GetJSInstanceInfoFromRuntimeObject(const Local& object) { + HandleScope handleScope(m_isolate); + + auto owner = GetJSInstanceInfoOwner(object); + if (owner.IsEmpty()) { + return nullptr; } - return nullptr; + auto external = owner->GetInternalField(static_cast(MetadataNodeKeys::JsInfo)).As(); + return static_cast(external->Value(v8::kExternalPointerTypeTagDefault)); } bool ObjectManager::IsJsRuntimeObject(const v8::Local& object) { @@ -301,17 +354,19 @@ void ObjectManager::Link(const Local& object, uint32_t javaObjectID, bool ObjectManager::CloneLink(const Local& src, const Local& dest) { - auto jsInfo = GetJSInstanceInfo(src); - - auto success = jsInfo != nullptr; + auto source = UnwrapProxy(src); + if (!source->IsObject() || !IsJsRuntimeObject(source.As())) { + return false; + } - if (success) { - auto jsInfoIdx = static_cast(MetadataNodeKeys::JsInfo); - auto jsInfo = src->GetInternalField(jsInfoIdx); - dest->SetInternalField(jsInfoIdx, jsInfo); + auto owner = GetJSInstanceInfoOwner(source.As()); + if (owner.IsEmpty()) { + return false; } - return success; + auto jsInfoIdx = static_cast(MetadataNodeKeys::JsInfo); + dest->SetInternalField(jsInfoIdx, owner->GetInternalField(jsInfoIdx)); + return true; } string ObjectManager::GetClassName(jobject javaObject) { @@ -643,12 +698,32 @@ void ObjectManager::ReleaseNativeCounterpart(v8::Local& object) { throw NativeScriptException("Argument is not an object!"); } - JSInstanceInfo* jsInstanceInfo = GetJSInstanceInfo(object); + auto source = UnwrapProxy(object); + if (!source->IsObject()) { + throw NativeScriptException("Trying to release a non native object!"); + } + auto instance = source.As(); + + // A `super` object shares its owner's JsInfo External; the owner is the one + // registered, so the release is resolved and recorded there. + Local superOwner; + V8GetPrivateValue(m_isolate, instance, ArgConverter::ConvertToV8String(m_isolate, "superowner"), superOwner); + if (!superOwner.IsEmpty() && superOwner->IsObject()) { + instance = superOwner.As(); + } - if (jsInstanceInfo == nullptr) { + Local owner; + if (IsJsRuntimeObject(instance)) { + owner = GetJSInstanceInfoOwner(instance); + } + if (owner.IsEmpty()) { throw NativeScriptException("Trying to release a non native object!"); } + auto jsInfoIdx = static_cast(MetadataNodeKeys::JsInfo); + auto jsInstanceInfo = static_cast( + owner->GetInternalField(jsInfoIdx).As()->Value(v8::kExternalPointerTypeTagDefault)); + JEnv env; env.CallVoidMethod(m_javaRuntimeObject, RELEASE_NATIVE_INSTANCE_METHOD_ID, jsInstanceInfo->JavaObjectID); @@ -662,8 +737,22 @@ void ObjectManager::ReleaseNativeCounterpart(v8::Local& object) { } delete jsInstanceInfo; - auto jsInfoIdx = static_cast(MetadataNodeKeys::JsInfo); - object->SetInternalField(jsInfoIdx, Undefined(m_isolate)); + owner->SetInternalField(jsInfoIdx, Undefined(m_isolate)); + + // Every alias of the freed External must be cleared with it. + Local aliasesValue; + V8GetPrivateValue(m_isolate, owner, ArgConverter::ConvertToV8String(m_isolate, "superaliases"), aliasesValue); + if (!aliasesValue.IsEmpty() && aliasesValue->IsArray()) { + auto aliases = aliasesValue.As(); + auto context = m_isolate->GetCurrentContext(); + for (uint32_t i = 0; i < aliases->Length(); i++) { + Local alias; + if (aliases->Get(context, i).ToLocal(&alias) && alias->IsObject() && + IsJsRuntimeObject(alias.As())) { + alias.As()->SetInternalField(jsInfoIdx, Undefined(m_isolate)); + } + } + } } ObjectManager::JavaScriptMarkingMode ObjectManager::GetMarkingMode() { diff --git a/test-app/runtime/src/main/cpp/ObjectManager.h b/test-app/runtime/src/main/cpp/ObjectManager.h index 23194133a..5fbe4afce 100644 --- a/test-app/runtime/src/main/cpp/ObjectManager.h +++ b/test-app/runtime/src/main/cpp/ObjectManager.h @@ -62,7 +62,35 @@ class ObjectManager { bool CloneLink(const v8::Local& src, const v8::Local& dest); - bool IsJsRuntimeObject(const v8::Local& object); + /* + * Whether GetInternalField is safe to call on this exact object. False for + * a Proxy even when its target qualifies; unwrap first where a Proxy should + * be accepted. + */ + static bool IsJsRuntimeObject(const v8::Local& object); + + /* + * The runtime object holding the JsInfo External for `object`: the object + * itself, or its prototype when the TypeScript layout keeps the registered + * instance one level up. Empty when neither holds one. Reads and clears of + * the link must go through this object, never through a Proxy or a child. + */ + static v8::Local GetJSInstanceInfoOwner(const v8::Local& object); + + /* + * The innermost target of a Proxy chain; any other value is returned as is. + * A revoked Proxy anywhere in the chain yields null. Internal fields and + * private symbols live on the target, never on the Proxy. + */ + static v8::Local UnwrapProxy(v8::Local value); + + /* + * Copies an array-like Proxy into a fresh Array by reading `length` and each + * index through the Proxy, so its traps decide what Java receives. Empty when + * a trap throws; the exception is left for the caller's TryCatch. + */ + static v8::MaybeLocal ReadArrayThroughProxy( + v8::Local context, v8::Local proxy); static std::string GetClassName(jobject javaObject); @@ -166,6 +194,7 @@ class ObjectManager { JSInstanceInfo* GetJSInstanceInfoFromRuntimeObject( const v8::Local& object); + void ReleaseJSInstance(v8::Persistent* po, JSInstanceInfo* jsInstanceInfo); diff --git a/test-app/runtime/src/main/cpp/URLImpl.cpp b/test-app/runtime/src/main/cpp/URLImpl.cpp index dd3348387..6820cd25d 100644 --- a/test-app/runtime/src/main/cpp/URLImpl.cpp +++ b/test-app/runtime/src/main/cpp/URLImpl.cpp @@ -10,6 +10,9 @@ using namespace ada; URLImpl::URLImpl(url_aggregator url) : url_(url) {} URLImpl *URLImpl::GetPointer(v8::Local object) { + if (object->InternalFieldCount() < 1) { + return nullptr; + } auto ptr = object->GetAlignedPointerFromInternalField(0, v8::kEmbedderDataTypeTagDefault); if (ptr == nullptr) { return nullptr; diff --git a/test-app/runtime/src/main/cpp/V8GlobalHelpers.cpp b/test-app/runtime/src/main/cpp/V8GlobalHelpers.cpp index adc523884..ba467dee0 100644 --- a/test-app/runtime/src/main/cpp/V8GlobalHelpers.cpp +++ b/test-app/runtime/src/main/cpp/V8GlobalHelpers.cpp @@ -64,10 +64,18 @@ std::string tns::GetNativeWrapperHint(Isolate* isolate, const Local& valu return "JavaObject"; } +Local tns::GetCreationContextOrCurrent(Isolate* isolate, const Local& obj) { + Local context; + if (obj->GetCreationContext(isolate).ToLocal(&context)) { + return context; + } + return isolate->GetCurrentContext(); +} + bool tns::V8GetPrivateValue(Isolate* isolate, const Local& obj, const Local& propName, Local& out) { auto privateKey = Private::ForApi(isolate, propName); - auto context = obj->GetCreationContext(isolate).ToLocalChecked(); + auto context = GetCreationContextOrCurrent(isolate, obj); auto hasPrivate = obj->HasPrivate(context, privateKey); if (hasPrivate.IsNothing()) { @@ -93,7 +101,7 @@ bool tns::V8GetPrivateValue(Isolate* isolate, const Local& obj, const Lo bool tns::V8SetPrivateValue(Isolate* isolate, const Local& obj, const Local& propName, const Local& value) { auto privateKey = Private::ForApi(isolate, propName); - auto context = obj->GetCreationContext(isolate).ToLocalChecked(); + auto context = GetCreationContextOrCurrent(isolate, obj); auto res = obj->SetPrivate(context, privateKey, value); if (res.IsNothing()) { diff --git a/test-app/runtime/src/main/cpp/V8GlobalHelpers.h b/test-app/runtime/src/main/cpp/V8GlobalHelpers.h index 4f5e165c8..6483574b8 100644 --- a/test-app/runtime/src/main/cpp/V8GlobalHelpers.h +++ b/test-app/runtime/src/main/cpp/V8GlobalHelpers.h @@ -12,6 +12,11 @@ namespace tns { // name etc.); empty when the value is a plain JS object. Never runs JS. std::string GetNativeWrapperHint(v8::Isolate* isolate, const v8::Local& value); +// A Proxy (live or revoked) has no creation context, and ToLocalChecked on the +// empty result aborts the process. The current context serves the same +// purpose for private symbols and plain property access. +v8::Local GetCreationContextOrCurrent(v8::Isolate* isolate, const v8::Local& obj); + bool V8GetPrivateValue(v8::Isolate* isolate, const v8::Local& obj, const v8::Local& propName, v8::Local& out); bool V8SetPrivateValue(v8::Isolate* isolate, const v8::Local& obj, const v8::Local& propName, const v8::Local& value); diff --git a/test-app/runtime/src/main/cpp/conversions/JSToJavaConverter.cpp b/test-app/runtime/src/main/cpp/conversions/JSToJavaConverter.cpp index ac9cefc5e..f2effda1d 100644 --- a/test-app/runtime/src/main/cpp/conversions/JSToJavaConverter.cpp +++ b/test-app/runtime/src/main/cpp/conversions/JSToJavaConverter.cpp @@ -111,8 +111,16 @@ JSToJavaConverter::JSToJavaConverter( } } -bool JSToJavaConverter::ConvertArg(const Local &arg, int index) { - +bool JSToJavaConverter::ConvertArg(const Local &jsArg, int index) { + // Classify by the target, which holds the Java link and internal slots; + // array elements are read through the Proxy itself. + auto arg = ObjectManager::UnwrapProxy(jsArg); + if (jsArg->IsProxy() && !arg->IsObject()) { + // The constructors do not inspect the result, so the failure must + // surface here rather than as a null argument. + string message = "Cannot pass a revoked Proxy to Java at index " + to_string(index); + throw NativeScriptException(m_isolate, Exception::TypeError(ArgConverter::ConvertToV8String(m_isolate, message)), message); + } if (arg->IsNumber() || arg->IsNumberObject()) { return tns::ConvertJavaScriptNumber( @@ -137,9 +145,18 @@ bool JSToJavaConverter::ConvertArg(const Local &arg, int index) { m_jniArgRefsState ); } else if (arg->IsArray() && m_tokens->at(index)[0] == '[') { + Local elements = arg; + if (jsArg->IsProxy()) { + TryCatch tc(m_isolate); + Local snapshot; + if (!ObjectManager::ReadArrayThroughProxy(m_isolate->GetCurrentContext(), jsArg.As()).ToLocal(&snapshot)) { + throw NativeScriptException(tc); + } + elements = snapshot; + } return tns::ConvertJavaScriptArray( m_isolate, - arg, + elements, index, m_tokens, m_jniArgRefsState diff --git a/test-app/runtime/src/main/cpp/conversions/JSToJavaConverter.h b/test-app/runtime/src/main/cpp/conversions/JSToJavaConverter.h index 6dd13314a..73f455f28 100644 --- a/test-app/runtime/src/main/cpp/conversions/JSToJavaConverter.h +++ b/test-app/runtime/src/main/cpp/conversions/JSToJavaConverter.h @@ -41,7 +41,7 @@ namespace tns { private: - bool ConvertArg(const v8::Local &arg, int index); + bool ConvertArg(const v8::Local &jsArg, int index); v8::Isolate *m_isolate; std::string m_methodSignature;