| FazBrowse GitHub Viewer | Trending | | Home |
| Tools: [Download Repo ZIP] [Original HTTPS Page] |
1 parent 2044ebf commit 302691c
8 files changed
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
@@ -76,6 +76,7 @@ require('./tests/testPerformanceNow'); | |||
| 76 | 76 | require('./tests/testQueueMicrotask'); | |
| 77 | 77 | require('./tests/testErrorEvents'); | |
| 78 | 78 | require('./tests/testUnhandledRejections'); | |
| 79 | + require('./tests/testEscapeException'); | ||
| 79 | 80 | require("./tests/testConcurrentAccess"); | |
| 80 | 81 | ||
| 81 | 82 | require("./tests/testESModules.mjs"); | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
@@ -0,0 +1,100 @@ | |||
| 1 | + describe("interop.escapeException", function () { | ||
| 2 | + it("exists on the interop global", function () { | ||
| 3 | + expect(typeof interop).toBe("object"); | ||
| 4 | + expect(typeof interop.escapeException).toBe("function"); | ||
| 5 | + }); | ||
| 6 | + | ||
| 7 | + it("returns a throwable Error preserving the message", function () { | ||
| 8 | + var wrapped = interop.escapeException(new Error("boom")); | ||
| 9 | + expect(wrapped instanceof Error).toBe(true); | ||
| 10 | + expect(wrapped.message).toBe("boom"); | ||
| 11 | + | ||
| 12 | + var caught = null; | ||
| 13 | + try { | ||
| 14 | + throw wrapped; | ||
| 15 | + } catch (e) { | ||
| 16 | + caught = e; | ||
| 17 | + } | ||
| 18 | + expect(caught).toBe(wrapped); | ||
| 19 | + }); | ||
| 20 | + | ||
| 21 | + it("throws TypeError when called with no arguments", function () { | ||
| 22 | + expect(function () { | ||
| 23 | + interop.escapeException(); | ||
| 24 | + }).toThrowError(TypeError); | ||
| 25 | + }); | ||
| 26 | + | ||
| 27 | + it("is idempotent (double-wrap returns the same object)", function () { | ||
| 28 | + var once = interop.escapeException(new Error("once")); | ||
| 29 | + var twice = interop.escapeException(once); | ||
| 30 | + expect(twice).toBe(once); | ||
| 31 | + }); | ||
| 32 | + | ||
| 33 | + it("rethrows the ORIGINAL Java exception to a native caller", function () { | ||
| 34 | + var caught = null; | ||
| 35 | + try { | ||
| 36 | + com.tns.tests.EscapeExceptionTest.throwIOException(); | ||
| 37 | + } catch (e) { | ||
| 38 | + caught = e; | ||
| 39 | + } | ||
| 40 | + expect(caught).not.toBeNull(); | ||
| 41 | + expect(caught.nativeException).toBeDefined(); | ||
| 42 | + | ||
| 43 | + var runnable = new java.lang.Runnable({ | ||
| 44 | + run: function () { | ||
| 45 | + throw interop.escapeException(caught); | ||
| 46 | + } | ||
| 47 | + }); | ||
| 48 | + var ret = com.tns.tests.EscapeExceptionTest.invokeCatchingThrowable(runnable); | ||
| 49 | + | ||
| 50 | + // The native caller caught the original java.io.IOException - not a | ||
| 51 | + // com.tns.NativeScriptException wrapper - so a concrete | ||
| 52 | + // `catch (IOException e)` in native code would match. | ||
| 53 | + expect(ret).not.toBeNull(); | ||
| 54 | + expect(ret.getClass().getName()).toBe("java.io.IOException"); | ||
| 55 | + expect(ret.getMessage()).toBe("original-io-exception"); | ||
| 56 | + expect(ret.equals(caught.nativeException)).toBe(true); | ||
| 57 | + | ||
| 58 | + // JS is still alive after the escape round-trip. | ||
| 59 | + expect(1 + 1).toBe(2); | ||
| 60 | + }); | ||
| 61 | + | ||
| 62 | + it("an unbranded rethrow keeps today's wrapping semantics", function () { | ||
| 63 | + var caught = null; | ||
| 64 | + try { | ||
| 65 | + com.tns.tests.EscapeExceptionTest.throwIOException(); | ||
| 66 | + } catch (e) { | ||
| 67 | + caught = e; | ||
| 68 | + } | ||
| 69 | + expect(caught).not.toBeNull(); | ||
| 70 | + | ||
| 71 | + var runnable = new java.lang.Runnable({ | ||
| 72 | + run: function () { | ||
| 73 | + throw caught; | ||
| 74 | + } | ||
| 75 | + }); | ||
| 76 | + var ret = com.tns.tests.EscapeExceptionTest.invokeCatchingThrowable(runnable); | ||
| 77 | + | ||
| 78 | + // Without the brand the caller receives the NativeScriptException | ||
| 79 | + // wrapper, with the original exception preserved as its cause. | ||
| 80 | + expect(ret).not.toBeNull(); | ||
| 81 | + expect(ret.getClass().getName()).toBe("com.tns.NativeScriptException"); | ||
| 82 | + expect(ret.getCause().equals(caught.nativeException)).toBe(true); | ||
| 83 | + }); | ||
| 84 | + | ||
| 85 | + it("a branded plain JS error escapes with the default NativeScriptException shape", function () { | ||
| 86 | + var runnable = new java.lang.Runnable({ | ||
| 87 | + run: function () { | ||
| 88 | + throw interop.escapeException(new Error("plain-escape")); | ||
| 89 | + } | ||
| 90 | + }); | ||
| 91 | + var ret = com.tns.tests.EscapeExceptionTest.invokeCatchingThrowable(runnable); | ||
| 92 | + | ||
| 93 | + // No underlying Java throwable to unwrap, so the standard escape path | ||
| 94 | + // applies: the caller gets a com.tns.NativeScriptException carrying | ||
| 95 | + // the JS message. | ||
| 96 | + expect(ret).not.toBeNull(); | ||
| 97 | + expect(ret.getClass().getName()).toBe("com.tns.NativeScriptException"); | ||
| 98 | + expect(ret.getMessage()).toContain("plain-escape"); | ||
| 99 | + }); | ||
| 100 | + }); | ||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
@@ -0,0 +1,22 @@ | |||
| 1 | + package com.tns.tests; | ||
| 2 | + | ||
| 3 | + public class EscapeExceptionTest { | ||
| 4 | + public static void throwIOException() throws java.io.IOException { | ||
| 5 | + throw new java.io.IOException("original-io-exception"); | ||
| 6 | + } | ||
| 7 | + | ||
| 8 | + /* | ||
| 9 | + * Invokes a callback implemented in JS and returns whatever Throwable | ||
| 10 | + * escapes it (or null). Catching Throwable (rather than a concrete type) | ||
| 11 | + * lets the tests assert exactly which exception class crossed the | ||
| 12 | + * JS->Java boundary. | ||
| 13 | + */ | ||
| 14 | + public static Throwable invokeCatchingThrowable(Runnable runnable) { | ||
| 15 | + try { | ||
| 16 | + runnable.run(); | ||
| 17 | + return null; | ||
| 18 | + } catch (Throwable t) { | ||
| 19 | + return t; | ||
| 20 | + } | ||
| 21 | + } | ||
| 22 | + } | ||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
@@ -107,6 +107,7 @@ add_library( | |||
| 107 | 107 | src/main/cpp/Events.cpp | |
| 108 | 108 | src/main/cpp/FieldAccessor.cpp | |
| 109 | 109 | src/main/cpp/File.cpp | |
| 110 | + src/main/cpp/Interop.cpp | ||
| 110 | 111 | src/main/cpp/IsolateDisposer.cpp | |
| 111 | 112 | src/main/cpp/JEnv.cpp | |
| 112 | 113 | src/main/cpp/DesugaredInterfaceCompanionClassNameResolver.cpp | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
@@ -0,0 +1,216 @@ | |||
| 1 | + #include "Interop.h" | ||
| 2 | + | ||
| 3 | + #include <string> | ||
| 4 | + | ||
| 5 | + #include "ArgConverter.h" | ||
| 6 | + #include "JniLocalRef.h" | ||
| 7 | + #include "NativeScriptException.h" | ||
| 8 | + #include "ObjectManager.h" | ||
| 9 | + #include "Runtime.h" | ||
| 10 | + #include "V8StringConstants.h" | ||
| 11 | + | ||
| 12 | + using namespace std; | ||
| 13 | + using namespace tns; | ||
| 14 | + using namespace v8; | ||
| 15 | + | ||
| 16 | + /* | ||
| 17 | + * The brand is a per-isolate private symbol: invisible to JS property | ||
| 18 | + * enumeration, so app code can neither observe nor forge it. Its value is the | ||
| 19 | + * escape payload - `{nativeException}` when the branded error carries an | ||
| 20 | + * original Java throwable, `{name, message, stack}` synthesis info otherwise | ||
| 21 | + * (mirroring the iOS runtime's payload shape). | ||
| 22 | + */ | ||
| 23 | + static Local<Private> GetBrand(Isolate* isolate) { | ||
| 24 | + return Private::ForApi( | ||
| 25 | + isolate, | ||
| 26 | + ArgConverter::ConvertToV8String(isolate, "tns::escapedExceptionPayload")); | ||
| 27 | + } | ||
| 28 | + | ||
| 29 | + /* | ||
| 30 | + * Resolves the JS wrapper of a Java throwable carried by `value` - `value` | ||
| 31 | + * itself when it wraps a java.lang.Throwable, or its `nativeException` when | ||
| 32 | + * `value` is an Error carrying one. Empty handle otherwise. | ||
| 33 | + */ | ||
| 34 | + static Local<Value> GetWrappedJavaThrowable(Local<Context> context, Local<Value> value) { | ||
| 35 | + auto isolate = context->GetIsolate(); | ||
| 36 | + | ||
| 37 | + auto isWrappedThrowable = [&](Local<Value> v) -> bool { | ||
| 38 | + if (v.IsEmpty() || !v->IsObject()) { | ||
| 39 | + return false; | ||
| 40 | + } | ||
| 41 | + auto objectManager = Runtime::GetObjectManager(isolate); | ||
| 42 | + auto javaObj = objectManager->GetJavaObjectByJsObject(v.As<Object>()); | ||
| 43 | + if (javaObj.IsNull()) { | ||
| 44 | + return false; | ||
| 45 | + } | ||
| 46 | + JEnv env; | ||
| 47 | + JniLocalRef objClass(env.GetObjectClass(javaObj)); | ||
| 48 | + jclass throwableClass = env.FindClass("java/lang/Throwable"); | ||
| 49 | + return env.IsAssignableFrom(objClass, throwableClass) == JNI_TRUE; | ||
| 50 | + }; | ||
| 51 | + | ||
| 52 | + if (isWrappedThrowable(value)) { | ||
| 53 | + return value; | ||
| 54 | + } | ||
| 55 | + if (value->IsObject()) { | ||
| 56 | + Local<Value> nativeExc; | ||
| 57 | + if (value.As<Object>() | ||
| 58 | + ->Get(context, V8StringConstants::GetNativeException(isolate)) | ||
| 59 | + .ToLocal(&nativeExc) && | ||
| 60 | + isWrappedThrowable(nativeExc)) { | ||
| 61 | + return nativeExc; | ||
| 62 | + } | ||
| 63 | + } | ||
| 64 | + return Local<Value>(); | ||
| 65 | + } | ||
| 66 | + | ||
| 67 | + static void EscapeExceptionCallback(const FunctionCallbackInfo<Value>& info) { | ||
| 68 | + auto isolate = info.GetIsolate(); | ||
| 69 | + auto context = isolate->GetCurrentContext(); | ||
| 70 | + | ||
| 71 | + if (info.Length() < 1) { | ||
| 72 | + isolate->ThrowException(Exception::TypeError(ArgConverter::ConvertToV8String( | ||
| 73 | + isolate, | ||
| 74 | + "interop.escapeException requires 1 argument, but only 0 present."))); | ||
| 75 | + return; | ||
| 76 | + } | ||
| 77 | + | ||
| 78 | + Local<Value> x = info[0]; | ||
| 79 | + Local<Private> brand = GetBrand(isolate); | ||
| 80 | + | ||
| 81 | + // Idempotent: an already-branded value is returned unchanged. | ||
| 82 | + if (x->IsObject() && | ||
| 83 | + x.As<Object>()->HasPrivate(context, brand).FromMaybe(false)) { | ||
| 84 | + info.GetReturnValue().Set(x); | ||
| 85 | + return; | ||
| 86 | + } | ||
| 87 | + | ||
| 88 | + // Derive the message string, preferring x.message when x is an Error. | ||
| 89 | + string message; | ||
| 90 | + bool xIsObject = x->IsObject(); | ||
| 91 | + Local<String> strVal; | ||
| 92 | + if (xIsObject) { | ||
| 93 | + Local<Value> msgVal; | ||
| 94 | + if (x.As<Object>() | ||
| 95 | + ->Get(context, ArgConverter::ConvertToV8String(isolate, "message")) | ||
| 96 | + .ToLocal(&msgVal) && | ||
| 97 | + !msgVal->IsNullOrUndefined() && msgVal->ToString(context).ToLocal(&strVal)) { | ||
| 98 | + message = ArgConverter::ConvertToString(strVal); | ||
| 99 | + } else if (x->ToString(context).ToLocal(&strVal)) { | ||
| 100 | + message = ArgConverter::ConvertToString(strVal); | ||
| 101 | + } | ||
| 102 | + } else if (x->ToString(context).ToLocal(&strVal)) { | ||
| 103 | + message = ArgConverter::ConvertToString(strVal); | ||
| 104 | + } | ||
| 105 | + | ||
| 106 | + // The returned value is a real JS Error so `throw interop.escapeException(x)` | ||
| 107 | + // behaves like a normal throw in pure-JS paths. | ||
| 108 | + auto errObj = Exception::Error(ArgConverter::ConvertToV8String(isolate, message)) | ||
| 109 | + .As<Object>(); | ||
| 110 | + | ||
| 111 | + // Copy stack from x when it is an Error carrying one. | ||
| 112 | + string stack; | ||
| 113 | + if (xIsObject) { | ||
| 114 | + Local<Value> stackVal; | ||
| 115 | + if (x.As<Object>() | ||
| 116 | + ->Get(context, V8StringConstants::GetStack(isolate)) | ||
| 117 | + .ToLocal(&stackVal) && | ||
| 118 | + stackVal->IsString()) { | ||
| 119 | + stack = ArgConverter::ConvertToString(stackVal.As<String>()); | ||
| 120 | + errObj->Set(context, V8StringConstants::GetStack(isolate), stackVal) | ||
| 121 | + .FromMaybe(false); | ||
| 122 | + } | ||
| 123 | + } | ||
| 124 | + | ||
| 125 | + // Build the branded payload: the original Java throwable when x carries | ||
| 126 | + // one, otherwise synthesis info (name/message/stack). | ||
| 127 | + auto payload = Object::New(isolate); | ||
| 128 | + Local<Value> nativeExc = GetWrappedJavaThrowable(context, x); | ||
| 129 | + if (!nativeExc.IsEmpty()) { | ||
| 130 | + payload->Set(context, V8StringConstants::GetNativeException(isolate), nativeExc) | ||
| 131 | + .FromMaybe(false); | ||
| 132 | + } else { | ||
| 133 | + string name = "Error"; | ||
| 134 | + if (xIsObject) { | ||
| 135 | + Local<Value> nameVal; | ||
| 136 | + if (x.As<Object>() | ||
| 137 | + ->Get(context, ArgConverter::ConvertToV8String(isolate, "name")) | ||
| 138 | + .ToLocal(&nameVal) && | ||
| 139 | + nameVal->IsString()) { | ||
| 140 | + name = ArgConverter::ConvertToString(nameVal.As<String>()); | ||
| 141 | + } | ||
| 142 | + } | ||
| 143 | + payload->Set(context, ArgConverter::ConvertToV8String(isolate, "name"), | ||
| 144 | + ArgConverter::ConvertToV8String(isolate, name)) | ||
| 145 | + .FromMaybe(false); | ||
| 146 | + payload->Set(context, ArgConverter::ConvertToV8String(isolate, "message"), | ||
| 147 | + ArgConverter::ConvertToV8String(isolate, message)) | ||
| 148 | + .FromMaybe(false); | ||
| 149 | + payload->Set(context, V8StringConstants::GetStack(isolate), | ||
| 150 | + ArgConverter::ConvertToV8String(isolate, stack)) | ||
| 151 | + .FromMaybe(false); | ||
| 152 | + } | ||
| 153 | + | ||
| 154 | + errObj->SetPrivate(context, brand, payload).FromMaybe(false); | ||
| 155 | + | ||
| 156 | + info.GetReturnValue().Set(errObj); | ||
| 157 | + } | ||
| 158 | + | ||
| 159 | + void Interop::Init(Local<Context> context) { | ||
| 160 | + auto isolate = context->GetIsolate(); | ||
| 161 | + auto global = context->Global(); | ||
| 162 | + | ||
| 163 | + auto interop = Object::New(isolate); | ||
| 164 | + | ||
| 165 | + Local<Function> escapeException; | ||
| 166 | + if (!Function::New(context, EscapeExceptionCallback).ToLocal(&escapeException)) { | ||
| 167 | + throw NativeScriptException("Interop::Init: failed to create escapeException"); | ||
| 168 | + } | ||
| 169 | + | ||
| 170 | + if (!interop->Set(context, ArgConverter::ConvertToV8String(isolate, "escapeException"), | ||
| 171 | + escapeException) | ||
| 172 | + .FromMaybe(false) || | ||
| 173 | + !global->Set(context, ArgConverter::ConvertToV8String(isolate, "interop"), interop) | ||
| 174 | + .FromMaybe(false)) { | ||
| 175 | + throw NativeScriptException("Interop::Init: failed to install the interop global"); | ||
| 176 | + } | ||
| 177 | + } | ||
| 178 | + | ||
| 179 | + jthrowable Interop::ExtractEscapedJavaException(JEnv& env, | ||
| 180 | + const Local<Object>& errObj) { | ||
| 181 | + auto isolate = Isolate::GetCurrent(); | ||
| 182 | + auto context = isolate->GetCurrentContext(); | ||
| 183 | + if (context.IsEmpty()) { | ||
| 184 | + return nullptr; | ||
| 185 | + } | ||
| 186 | + | ||
| 187 | + Local<Private> brand = GetBrand(isolate); | ||
| 188 | + Local<Value> payload; | ||
| 189 | + if (!errObj->HasPrivate(context, brand).FromMaybe(false) || | ||
| 190 | + !errObj->GetPrivate(context, brand).ToLocal(&payload) || | ||
| 191 | + !payload->IsObject()) { | ||
| 192 | + return nullptr; | ||
| 193 | + } | ||
| 194 | + | ||
| 195 | + Local<Value> nativeExc; | ||
| 196 | + if (!payload.As<Object>() | ||
| 197 | + ->Get(context, V8StringConstants::GetNativeException(isolate)) | ||
| 198 | + .ToLocal(&nativeExc) || | ||
| 199 | + nativeExc.IsEmpty() || !nativeExc->IsObject()) { | ||
| 200 | + return nullptr; | ||
| 201 | + } | ||
| 202 | + | ||
| 203 | + auto objectManager = Runtime::GetObjectManager(isolate); | ||
| 204 | + auto javaObj = objectManager->GetJavaObjectByJsObject(nativeExc.As<Object>()); | ||
| 205 | + if (javaObj.IsNull()) { | ||
| 206 | + return nullptr; | ||
| 207 | + } | ||
| 208 | + | ||
| 209 | + JniLocalRef objClass(env.GetObjectClass(javaObj)); | ||
| 210 | + jclass throwableClass = env.FindClass("java/lang/Throwable"); | ||
| 211 | + if (env.IsAssignableFrom(objClass, throwableClass) != JNI_TRUE) { | ||
| 212 | + return nullptr; | ||
| 213 | + } | ||
| 214 | + | ||
| 215 | + return static_cast<jthrowable>(env.NewLocalRef(javaObj)); | ||
| 216 | + } | ||
| Back | FazBrowse Home | New Git URL |
0 commit comments