From 4449266b2ea59d855bcf8c9184569e645d033554 Mon Sep 17 00:00:00 2001 From: Seonghyun Kim Date: Thu, 2 Nov 2023 17:13:40 +0900 Subject: [PATCH] Implement Context.throwException on Java API Signed-off-by: Seonghyun Kim --- .../samsung/lwe/escargot/EscargotTest.java | 63 ++++++++++++++++++- .../escargot/src/main/cpp/EscargotJNI.cpp | 53 ++++++++++++++-- .../escargot/src/main/cpp/EscargotJNI.h | 1 + .../escargot/src/main/cpp/JNIContext.cpp | 14 +++++ .../escargot/src/main/cpp/JNIEvaluator.cpp | 13 +--- .../com/samsung/lwe/escargot/Context.java | 1 + .../lwe/escargot/JavaScriptErrorObject.java | 2 +- .../internal/JavaScriptRuntimeException.java | 16 +++++ src/api/EscargotPublic.cpp | 5 ++ src/api/EscargotPublic.h | 3 +- src/runtime/Context.cpp | 5 ++ src/runtime/Context.h | 1 + 12 files changed, 156 insertions(+), 21 deletions(-) create mode 100644 build/android/escargot/src/main/java/com/samsung/lwe/escargot/internal/JavaScriptRuntimeException.java diff --git a/build/android/escargot/src/androidTest/java/com/samsung/lwe/escargot/EscargotTest.java b/build/android/escargot/src/androidTest/java/com/samsung/lwe/escargot/EscargotTest.java index f4fdef571..d25b39b32 100644 --- a/build/android/escargot/src/androidTest/java/com/samsung/lwe/escargot/EscargotTest.java +++ b/build/android/escargot/src/androidTest/java/com/samsung/lwe/escargot/EscargotTest.java @@ -1060,7 +1060,7 @@ public Optional callback(Context context, JavaScriptValue recei assertFalse(context.exceptionWasThrown()); Evaluator.evalScript(context, "new asdf()", "test.js", false); - assertFalse(context.lastThrownException().isPresent()); + assertTrue(context.lastThrownException().isPresent()); ret = Evaluator.evalScript(context, "asdf(1, 2, 3, 4)", "test.js", false); assertEquals(ret.get().asInt32(), 4); @@ -1799,6 +1799,67 @@ public void errorObjectTest() assertTrue(e.toString(context).get().toJavaString().equals(kinds[i].name() + ": test" + i)); } + // failed to throw exception + assertFalse(context.throwException(JavaScriptValue.create(123))); + assertFalse(context.exceptionWasThrown()); + + Bridge.register(context, "Native", "throwsException", new Bridge.Adapter() { + @Override + public Optional callback(Context context, Optional data) { + context.throwException(JavaScriptErrorObject.create(context, JavaScriptErrorObject.ErrorKind.None, "asdf")); + assertTrue(false); + return null; + } + }); + + // will fail + Optional ret = Evaluator.evalScript(context, "Native.throwsException()", ""); + assertFalse(ret.isPresent()); + assertTrue(context.exceptionWasThrown()); + assertTrue(context.lastThrownException().get().isErrorObject()); + + JavaScriptFunctionObject fn = + context.getGlobalObject().get(context, JavaScriptString.create("Native")) + .get().asScriptObject().get(context, JavaScriptString.create("throwsException")).get().asScriptFunctionObject(); + // will fail + ret = fn.call(context, context.getGlobalObject(), new JavaScriptValue[]{}); + assertFalse(ret.isPresent()); + assertTrue(context.exceptionWasThrown()); + assertTrue(context.lastThrownException().get().isErrorObject()); + + Bridge.register(context, "Native", "throwsException", new Bridge.Adapter() { + @Override + public Optional callback(Context context, Optional data) { + JavaScriptErrorObject err = JavaScriptErrorObject.create(context, JavaScriptErrorObject.ErrorKind.None, "asdf"); + err.setExtraData(Optional.of(new RuntimeException("asdf"))); + context.throwException(err); + assertTrue(false); + return null; + } + }); + Evaluator.evalScript(context, "Native.throwsException()", ""); + ret = Evaluator.evalScript(context, "Native.throwsException()", ""); + assertFalse(ret.isPresent()); + assertTrue(context.exceptionWasThrown()); + JavaScriptErrorObject err = context.lastThrownException().get().asScriptErrorObject(); + assertTrue(err.extraData().get() instanceof RuntimeException); + + ArrayList thenCalled = new ArrayList<>(); + JavaScriptPromiseObject promise = JavaScriptPromiseObject.create(context); + promise.then(context, JavaScriptValue.createUndefined(), JavaScriptJavaCallbackFunctionObject.create(context, "", 0, false, new JavaScriptJavaCallbackFunctionObject.Callback() { + @Override + public Optional callback(Context context, JavaScriptValue receiverValue, JavaScriptValue[] arguments) { + thenCalled.add(new Object()); + assertTrue(arguments[0].isErrorObject()); + assertTrue(arguments[0].toString(context).get().toJavaString().equals("Error: asdf")); + return Optional.empty(); + } + })); + promise.reject(context, JavaScriptErrorObject.create(context, JavaScriptErrorObject.ErrorKind.None, "asdf")); + assertTrue(thenCalled.size() == 0); + vmInstance.executeEveryPendingJobIfExists(); + assertTrue(thenCalled.size() == 1); + context = null; vmInstance = null; finalizeEngine(); diff --git a/build/android/escargot/src/main/cpp/EscargotJNI.cpp b/build/android/escargot/src/main/cpp/EscargotJNI.cpp index 91f2531ea..46a8525f5 100644 --- a/build/android/escargot/src/main/cpp/EscargotJNI.cpp +++ b/build/android/escargot/src/main/cpp/EscargotJNI.cpp @@ -190,18 +190,59 @@ void throwJavaRuntimeException(ExecutionStateRef* state) state->throwException(ErrorObjectRef::create(state, ErrorObjectRef::None, StringRef::createFromASCII("Java runtime exception"))); } -jobject storeExceptionOnContextAndReturnsIt(JNIEnv* env, jobject contextObject, ContextRef* context, Evaluator::EvaluatorResult& evaluatorResult) +bool hasJavaScriptRuntimeExceptionOnEnv(JNIEnv* env) +{ + if (env->ExceptionCheck()) { + jthrowable exception = env->ExceptionOccurred(); + env->ExceptionClear(); + bool ret = env->IsInstanceOf(exception, env->FindClass("com/samsung/lwe/escargot/internal/JavaScriptRuntimeException")); + env->Throw(exception); + return ret; + } + return false; +} + +OptionalRef extractExceptionFromEnv(JNIEnv* env) { if (env->ExceptionCheck()) { + jthrowable exception = env->ExceptionOccurred(); + env->ExceptionClear(); + bool ret = env->IsInstanceOf(exception, env->FindClass("com/samsung/lwe/escargot/internal/JavaScriptRuntimeException")); + if (ret) { + jclass clz = env->FindClass("com/samsung/lwe/escargot/internal/JavaScriptRuntimeException"); + jobject jv = env->CallObjectMethod(exception, env->GetMethodID(clz, "exception", "()Lcom/samsung/lwe/escargot/JavaScriptValue;")); + ValueRef* v = unwrapValueRefFromValue(env, env->GetObjectClass(jv), jv); + return v; + } else { + env->Throw(exception); + } + } + + return nullptr; +} + +bool hasJavaExcpetion(JNIEnv* env) +{ + if (env->ExceptionCheck() && !hasJavaScriptRuntimeExceptionOnEnv(env)) { + return true; + } + return false; +} + +jobject storeExceptionOnContextAndReturnsIt(JNIEnv* env, jobject contextObject, ContextRef* context, Evaluator::EvaluatorResult& evaluatorResult) +{ + if (hasJavaExcpetion(env)) { return nullptr; } + auto exceptionOnEnv = extractExceptionFromEnv(env); jclass optionalClazz = env->FindClass("java/util/Optional"); // store exception to context auto fieldId = env->GetFieldID(env->GetObjectClass(contextObject), "m_lastThrownException", "Ljava/util/Optional;"); + ValueRef* exception = exceptionOnEnv ? exceptionOnEnv.get() : evaluatorResult.error.value(); auto fieldValue = env->CallStaticObjectMethod(optionalClazz, env->GetStaticMethodID(optionalClazz, "of", "(Ljava/lang/Object;)Ljava/util/Optional;"), - createJavaObjectFromValue(env, evaluatorResult.error.value())); + createJavaObjectFromValue(env, exception)); env->SetObjectField(contextObject, fieldId, fieldValue); return env->CallStaticObjectMethod(optionalClazz, env->GetStaticMethodID(optionalClazz, "empty", @@ -210,7 +251,7 @@ jobject storeExceptionOnContextAndReturnsIt(JNIEnv* env, jobject contextObject, jobject createOptionalValueFromEvaluatorJavaScriptValueResult(JNIEnv* env, jobject contextObject, ContextRef* context, Evaluator::EvaluatorResult& evaluatorResult) { - if (env->ExceptionCheck()) { + if (hasJavaExcpetion(env)) { return nullptr; } if (evaluatorResult.isSuccessful()) { @@ -226,7 +267,7 @@ jobject createOptionalValueFromEvaluatorJavaScriptValueResult(JNIEnv* env, jobje jobject createOptionalValueFromEvaluatorBooleanResult(JNIEnv* env, jobject contextObject, ContextRef* context, Evaluator::EvaluatorResult& evaluatorResult) { - if (env->ExceptionCheck()) { + if (hasJavaExcpetion(env)) { return nullptr; } if (evaluatorResult.isSuccessful()) { @@ -245,7 +286,7 @@ jobject createOptionalValueFromEvaluatorBooleanResult(JNIEnv* env, jobject conte jobject createOptionalValueFromEvaluatorIntegerResult(JNIEnv* env, jobject contextObject, ContextRef* context, Evaluator::EvaluatorResult& evaluatorResult) { - if (env->ExceptionCheck()) { + if (hasJavaExcpetion(env)) { return nullptr; } if (evaluatorResult.isSuccessful()) { @@ -264,7 +305,7 @@ jobject createOptionalValueFromEvaluatorIntegerResult(JNIEnv* env, jobject conte jobject createOptionalValueFromEvaluatorDoubleResult(JNIEnv* env, jobject contextObject, ContextRef* context, Evaluator::EvaluatorResult& evaluatorResult) { - if (env->ExceptionCheck()) { + if (hasJavaExcpetion(env)) { return nullptr; } if (evaluatorResult.isSuccessful()) { diff --git a/build/android/escargot/src/main/cpp/EscargotJNI.h b/build/android/escargot/src/main/cpp/EscargotJNI.h index e73eb1a3c..a9fca7351 100644 --- a/build/android/escargot/src/main/cpp/EscargotJNI.h +++ b/build/android/escargot/src/main/cpp/EscargotJNI.h @@ -94,6 +94,7 @@ StringRef* createJSStringFromJava(JNIEnv* env, jstring str); std::string createStringFromJava(JNIEnv* env, jstring str); jstring createJavaStringFromJS(JNIEnv* env, StringRef* string); void throwJavaRuntimeException(ExecutionStateRef* state); +OptionalRef extractExceptionFromEnv(JNIEnv* env); jobject storeExceptionOnContextAndReturnsIt(JNIEnv* env, jobject contextObject, ContextRef* context, Evaluator::EvaluatorResult& evaluatorResult); jobject createOptionalValueFromEvaluatorJavaScriptValueResult(JNIEnv* env, jobject contextObject, ContextRef* context, Evaluator::EvaluatorResult& evaluatorResult); jobject createOptionalValueFromEvaluatorBooleanResult(JNIEnv* env, jobject contextObject, ContextRef* context, Evaluator::EvaluatorResult& evaluatorResult); diff --git a/build/android/escargot/src/main/cpp/JNIContext.cpp b/build/android/escargot/src/main/cpp/JNIContext.cpp index f34e12f72..7a0e9054b 100644 --- a/build/android/escargot/src/main/cpp/JNIContext.cpp +++ b/build/android/escargot/src/main/cpp/JNIContext.cpp @@ -38,3 +38,17 @@ Java_com_samsung_lwe_escargot_Context_getGlobalObject(JNIEnv* env, jobject thiz) auto contextRef = getPersistentPointerFromJava(env, env->GetObjectClass(thiz), thiz); return createJavaObjectFromValue(env, contextRef->get()->globalObject()); } + +extern "C" +JNIEXPORT jboolean JNICALL +Java_com_samsung_lwe_escargot_Context_throwException(JNIEnv* env, jobject thiz, jobject exception) +{ + THROW_NPE_RETURN_NULL(exception, "JavaScriptValue"); + auto contextRef = getPersistentPointerFromJava(env, env->GetObjectClass(thiz), thiz); + if (contextRef->get()->canThrowException()) { + jclass clz = env->FindClass("com/samsung/lwe/escargot/internal/JavaScriptRuntimeException"); + jobject obj = env->NewObject(clz, env->GetMethodID(clz, "", "(Lcom/samsung/lwe/escargot/JavaScriptValue;)V"), exception); + env->Throw(static_cast(obj)); + } + return false; +} \ No newline at end of file diff --git a/build/android/escargot/src/main/cpp/JNIEvaluator.cpp b/build/android/escargot/src/main/cpp/JNIEvaluator.cpp index e835b5d9a..57461937f 100644 --- a/build/android/escargot/src/main/cpp/JNIEvaluator.cpp +++ b/build/android/escargot/src/main/cpp/JNIEvaluator.cpp @@ -113,16 +113,5 @@ Java_com_samsung_lwe_escargot_Evaluator_evalScript(JNIEnv* env, jclass clazz, jo Evaluator::EvaluatorResult result = evalScript(ptr->get(), createJSStringFromJava(env, source), createJSStringFromJava(env, sourceFileName), shouldPrintScriptResult, shouldExecutePendingJobsAtEnd, false); - if (env->ExceptionCheck()) { - return nullptr; - } - jclass optionalClazz = env->FindClass("java/util/Optional"); - if (result.isSuccessful()) { - return env->CallStaticObjectMethod(optionalClazz, - env->GetStaticMethodID(optionalClazz, "of", - "(Ljava/lang/Object;)Ljava/util/Optional;"), - createJavaObjectFromValue(env, result.result)); - } - return env->CallStaticObjectMethod(optionalClazz, env->GetStaticMethodID(optionalClazz, "empty", - "()Ljava/util/Optional;")); + return createOptionalValueFromEvaluatorJavaScriptValueResult(env, context, ptr->get(), result); } \ No newline at end of file diff --git a/build/android/escargot/src/main/java/com/samsung/lwe/escargot/Context.java b/build/android/escargot/src/main/java/com/samsung/lwe/escargot/Context.java index d8466d509..2a5a1c84f 100644 --- a/build/android/escargot/src/main/java/com/samsung/lwe/escargot/Context.java +++ b/build/android/escargot/src/main/java/com/samsung/lwe/escargot/Context.java @@ -19,6 +19,7 @@ public Optional lastThrownException() return lastThrownException; } public native JavaScriptGlobalObject getGlobalObject(); + public native boolean throwException(JavaScriptValue exception); protected Optional m_lastThrownException = Optional.empty(); } diff --git a/build/android/escargot/src/main/java/com/samsung/lwe/escargot/JavaScriptErrorObject.java b/build/android/escargot/src/main/java/com/samsung/lwe/escargot/JavaScriptErrorObject.java index 8b63b77b1..63ba485cd 100644 --- a/build/android/escargot/src/main/java/com/samsung/lwe/escargot/JavaScriptErrorObject.java +++ b/build/android/escargot/src/main/java/com/samsung/lwe/escargot/JavaScriptErrorObject.java @@ -6,7 +6,7 @@ protected JavaScriptErrorObject(long nativePointer) super(nativePointer); } - enum ErrorKind { + public enum ErrorKind { None, ReferenceError, TypeError, diff --git a/build/android/escargot/src/main/java/com/samsung/lwe/escargot/internal/JavaScriptRuntimeException.java b/build/android/escargot/src/main/java/com/samsung/lwe/escargot/internal/JavaScriptRuntimeException.java new file mode 100644 index 000000000..3788f26c4 --- /dev/null +++ b/build/android/escargot/src/main/java/com/samsung/lwe/escargot/internal/JavaScriptRuntimeException.java @@ -0,0 +1,16 @@ +package com.samsung.lwe.escargot.internal; + +import com.samsung.lwe.escargot.JavaScriptValue; + +public class JavaScriptRuntimeException extends RuntimeException { + public JavaScriptRuntimeException(JavaScriptValue exception) + { + this.m_exception = exception; + } + + public JavaScriptValue exception() { + return m_exception; + } + + private JavaScriptValue m_exception; +} diff --git a/src/api/EscargotPublic.cpp b/src/api/EscargotPublic.cpp index a6929ff8b..94b92528a 100644 --- a/src/api/EscargotPublic.cpp +++ b/src/api/EscargotPublic.cpp @@ -2917,6 +2917,11 @@ VMInstanceRef* ContextRef::vmInstance() return toRef(ctx->vmInstance()); } +bool ContextRef::canThrowException() +{ + return toImpl(this)->canThrowException(); +} + void ContextRef::throwException(ValueRef* exceptionValue) { ExecutionState s(toImpl(this)); diff --git a/src/api/EscargotPublic.h b/src/api/EscargotPublic.h index 36a72f96f..91fa72c2e 100644 --- a/src/api/EscargotPublic.h +++ b/src/api/EscargotPublic.h @@ -875,7 +875,8 @@ class ESCARGOT_EXPORT ContextRef { // this setter try to update `globalThis` value on GlobalObject void setGlobalObjectProxy(ObjectRef* newGlobalObjectProxy); - void throwException(ValueRef* exceptionValue); // if you use this function without Evaluator, your program will crash :( + bool canThrowException(); + void throwException(ValueRef* exceptionValue); bool initDebugger(DebuggerOperationsRef::DebuggerClient* debuggerClient); // available options(separator is ';') diff --git a/src/runtime/Context.cpp b/src/runtime/Context.cpp index 729e7c9d0..1faccd142 100644 --- a/src/runtime/Context.cpp +++ b/src/runtime/Context.cpp @@ -113,6 +113,11 @@ Context::Context(VMInstance* instance) #endif } +bool Context::canThrowException() +{ + return vmInstance()->currentSandBox(); +} + void Context::throwException(ExecutionState& state, const Value& exception) { if (LIKELY(vmInstance()->currentSandBox() != nullptr)) { diff --git a/src/runtime/Context.h b/src/runtime/Context.h index 18d33636d..d4511a58a 100644 --- a/src/runtime/Context.h +++ b/src/runtime/Context.h @@ -255,6 +255,7 @@ class Context : public gc { return m_toStringRecursionPreventer; } + bool canThrowException(); void throwException(ExecutionState& state, const Value& exception); // this is not compatible with ECMAScript