From 16db24ad71352aab439680c22eb9b46d54f47997 Mon Sep 17 00:00:00 2001 From: Wenxi Zeng Date: Mon, 29 Apr 2024 10:16:33 -0500 Subject: [PATCH] exception handling, overloading, global scope (#7) * add try catch to jni * bug fix for exception handler * make get operator return nullable value * add ability to do things in global scope to get away from memory management * bug fix and more unit tests * add comments * add support for overloading methods * optimize thread to avoid deadlock * add global memory table and unit tests for it * handle null property in json object --------- Co-authored-by: Wenxi Zeng --- .../analytics/substrata/kotlin/EngineTests.kt | 143 ++ .../analytics/substrata/kotlin/TypesTests.kt | 16 + substrata-kotlin/src/main/cpp/java_helper.cpp | 35 +- substrata-kotlin/src/main/cpp/java_helper.h | 74 +- substrata-kotlin/src/main/cpp/quickjs_jni.cpp | 1181 +++++++++-------- .../analytics/substrata/kotlin/Conversions.kt | 4 + .../analytics/substrata/kotlin/JSContext.kt | 28 +- .../analytics/substrata/kotlin/JSRegistry.kt | 27 +- .../substrata/kotlin/MemoryManager.kt | 21 +- .../analytics/substrata/kotlin/SafeEngine.kt | 80 +- .../analytics/substrata/kotlin/Types.kt | 22 +- 11 files changed, 1026 insertions(+), 605 deletions(-) diff --git a/substrata-kotlin/src/androidTest/java/com/segment/analytics/substrata/kotlin/EngineTests.kt b/substrata-kotlin/src/androidTest/java/com/segment/analytics/substrata/kotlin/EngineTests.kt index 8a6f20a..4576f62 100644 --- a/substrata-kotlin/src/androidTest/java/com/segment/analytics/substrata/kotlin/EngineTests.kt +++ b/substrata-kotlin/src/androidTest/java/com/segment/analytics/substrata/kotlin/EngineTests.kt @@ -2,7 +2,10 @@ package com.segment.analytics.substrata.kotlin import androidx.test.ext.junit.runners.AndroidJUnit4 import junit.framework.Assert.* +import kotlinx.serialization.json.Json import kotlinx.serialization.json.JsonObject +import kotlinx.serialization.json.jsonObject +import kotlinx.serialization.json.jsonPrimitive import org.junit.After import org.junit.Test import org.junit.runner.RunWith @@ -60,6 +63,12 @@ class EngineTests { assertEquals("123", bridge.getString("string")) assertEquals(123, bridge.getInt("int")) assertEquals(false, bridge.getBoolean("bool")) + + val ret = evaluate(""" + let v = DataBridge["int"] + v + """.trimIndent()) + assertEquals(123, ret) } assertNull(exception) } @@ -456,6 +465,140 @@ class EngineTests { assertNull(exception) } + @Test + fun testCallWithJsonElement() { + val message = "This came from a LivePlugin" + val script = """ + class MyTest { + track(event) { + event.context.livePluginMessage = "$message"; + const mcvid = DataBridge["mcvid"] + if (mcvid) { + event.context.mcvid = mcvid; + } + return event + } + } + let myTest = new MyTest() + myTest + """.trimIndent() + val json = """ + {"properties":{"version":1,"build":1,"from_background":false},"event":"Application Opened","type":"track","messageId":"2132f014-a8fe-41b6-b714-0226db39e0d3","anonymousId":"a7bffc58-991e-4a2d-98a7-2a04abb3ea93","integrations":{},"context":{"library":{"name":"analytics-kotlin","version":"1.15.0"},"instanceId":"49f19161-6d56-4024-b23d-7f32d6ab9982","app":{"name":"analytics-kotlin-live","version":1,"namespace":"com.segment.analytics.liveplugins.app","build":1},"device":{"id":"87bc73d4e4ca1608da083975d36421aef0411dff765c9766b9bfaf266b7c1586","manufacturer":"Google","model":"sdk_gphone64_arm64","name":"emu64a","type":"android"},"os":{"name":"Android","version":14},"screen":{"density":2.75,"height":2154,"width":1080},"network":{},"locale":"en-US","userAgent":"Dalvik/2.1.0 (Linux; U; Android 14; sdk_gphone64_arm64 Build/UE1A.230829.036.A1)","timezone":"America/Chicago"},"userId":"","_metadata":{"bundled":[],"unbundled":[],"bundledIds":[]},"timestamp":"2024-04-25T16:40:55.994Z"} + """.trimIndent() + val content = Json.parseToJsonElement(json) + + scope.sync { + val ret = evaluate(script) + assert(ret is JSObject) + val res: Any = call(ret as JSObject, "track", JsonElementConverter.write(content, context)) + assert(res is JSObject) + val jsonObject = JsonElementConverter.read(res) + assertNotNull(jsonObject) + assertEquals(message, jsonObject.jsonObject["context"]?.jsonObject?.get("livePluginMessage")?.jsonPrimitive?.content) + } + assertNull(exception) + } + + @Test + fun testOverloads() { + class MyTest { + fun track() = 0 + + fun track(str: String) = str + + fun track(i: Int, str: String) = "$i and $str" + } + + scope.sync { + export("MyTest", MyTest::class) + val ret = evaluate("let myTest = new MyTest(); myTest") + assert(ret is JSObject) + val jsObject = ret as JSObject + assertEquals(0, call(jsObject, "track")) + assertEquals("testtest", call(jsObject, "track", "testtest")) + assertEquals("0 and testtest", call(jsObject, "track", 0, "testtest")) + } + assertNull(exception) + } + + @Test + fun testNestedScopes() { + scope.sync { + val l1 = scope.await { + val l2 = scope.await { + scope.sync { + Thread.sleep(500L) + } + 1 + } + + (l2 ?: 0) + 1 + } + + assertEquals(2, l1) + } + assertNull(exception) + } + + @Test + fun testNestedScopesInCallback() { + class MyTest { + var engine: JSScope? = null + + fun track() { + engine?.sync { + println("callback") + } + } + } + val myTest = MyTest() + myTest.engine = scope + + scope.sync { + export(myTest, "MyTest", "myTest") + call("myTest", "track") + } + assertNull(exception) + } + + @Test + fun testGlobalScopeDoesPersist() { + var ret: JSObject? = null + scope.sync { + ret = scope.await(global = true) { + val jsObject = context.newObject() + jsObject["a"] = 1 + jsObject + } + } + assertNotNull(ret) + assert(ret is JSObject) + + scope.sync { + val a = (ret as JSObject)["a"] + assertEquals(1, a) + } + + assertNull(exception) + } + + @Test + fun testException() { + class MyJSClass { + fun test(): Int { + throw Exception("something wrong") + } + } + scope.sync { + export( "MyJSClass", MyJSClass::class) + evaluate(""" + let o = MyJSClass() + o.test() + """) + } + assertNotNull(exception) + } + @Test fun testAwait() { val ret = scope.await { diff --git a/substrata-kotlin/src/androidTest/java/com/segment/analytics/substrata/kotlin/TypesTests.kt b/substrata-kotlin/src/androidTest/java/com/segment/analytics/substrata/kotlin/TypesTests.kt index 6382d90..52189e8 100644 --- a/substrata-kotlin/src/androidTest/java/com/segment/analytics/substrata/kotlin/TypesTests.kt +++ b/substrata-kotlin/src/androidTest/java/com/segment/analytics/substrata/kotlin/TypesTests.kt @@ -1,6 +1,7 @@ package com.segment.analytics.substrata.kotlin import androidx.test.ext.junit.runners.AndroidJUnit4 +import kotlinx.serialization.json.Json import kotlinx.serialization.json.add import kotlinx.serialization.json.boolean import kotlinx.serialization.json.buildJsonArray @@ -190,4 +191,19 @@ class TypesTests { assertEquals("testtesttest", nestedArr[2].jsonPrimitive.content) assertEquals(3.3, nestedArr[3].jsonPrimitive.double, 0.01) } + + @Test + fun testJsonElementConverter() { + val json = """ + {"properties":{"version":1,"test": null, "build":1,"from_background":false},"event":"Application Opened","type":"track","messageId":"2132f014-a8fe-41b6-b714-0226db39e0d3","anonymousId":"a7bffc58-991e-4a2d-98a7-2a04abb3ea93","integrations":{},"context":{"library":{"name":"analytics-kotlin","version":"1.15.0"},"instanceId":"49f19161-6d56-4024-b23d-7f32d6ab9982","app":{"name":"analytics-kotlin-live","version":1,"namespace":"com.segment.analytics.liveplugins.app","build":1},"device":{"id":"87bc73d4e4ca1608da083975d36421aef0411dff765c9766b9bfaf266b7c1586","manufacturer":"Google","model":"sdk_gphone64_arm64","name":"emu64a","type":"android"},"os":{"name":"Android","version":14},"screen":{"density":2.75,"height":2154,"width":1080},"network":{},"locale":"en-US","userAgent":"Dalvik/2.1.0 (Linux; U; Android 14; sdk_gphone64_arm64 Build/UE1A.230829.036.A1)","timezone":"America/Chicago","livePluginMessage":"This came from a LivePlugin"},"userId":"","_metadata":{"bundled":[],"unbundled":[],"bundledIds":[]},"timestamp":"2024-04-25T16:40:55.994Z"} + """.trimIndent() + val content = Json.parseToJsonElement(json) + + context.memScope { + val jsObject = JsonElementConverter.write(content, this) + assert(jsObject is JSObject) + val jsonObject = JsonElementConverter.read(jsObject) + assertNotNull(jsonObject) + } + } } \ No newline at end of file diff --git a/substrata-kotlin/src/main/cpp/java_helper.cpp b/substrata-kotlin/src/main/cpp/java_helper.cpp index 3cd4ca9..be42ce2 100644 --- a/substrata-kotlin/src/main/cpp/java_helper.cpp +++ b/substrata-kotlin/src/main/cpp/java_helper.cpp @@ -1,20 +1,29 @@ #include +#include +#include #include "java_helper.h" -#define MAX_MSG_SIZE 1024 -jint throw_exception(JNIEnv *env, const char *exception_name, const char *message, ...) { - char formatted_message[MAX_MSG_SIZE]; - va_list va_args; - va_start(va_args, message); - vsnprintf(formatted_message, MAX_MSG_SIZE, message, va_args); - va_end(va_args); +void swallow_cpp_exception_and_throw_java(JNIEnv * env) { + try { + throw; + } catch(const ThrownJavaException&) { + //already reported to Java, ignore + } catch(const std::bad_alloc& rhs) { + //translate OOM C++ exception to a Java exception + NewJavaException(env, "java/lang/OutOfMemoryError", rhs.what()); + } catch(const std::ios_base::failure& rhs) { //sample translation + //translate IO C++ exception to a Java exception + NewJavaException(env, "java/io/IOException", rhs.what()); - jclass exception_class = env->FindClass(exception_name); - if (exception_class == NULL) { - return -1; - } + //TRANSLATE ANY OTHER C++ EXCEPTIONS TO JAVA EXCEPTIONS HERE - return env->ThrowNew(exception_class, formatted_message); -} + } catch(const std::exception& e) { + //translate unknown C++ exception to a Java exception + NewJavaException(env, "java/lang/Error", e.what()); + } catch(...) { + //translate unknown C++ exception to a Java exception + NewJavaException(env, "java/lang/Error", "Unknown exception type"); + } +} \ No newline at end of file diff --git a/substrata-kotlin/src/main/cpp/java_helper.h b/substrata-kotlin/src/main/cpp/java_helper.h index 6244018..4e3ae0b 100644 --- a/substrata-kotlin/src/main/cpp/java_helper.h +++ b/substrata-kotlin/src/main/cpp/java_helper.h @@ -1,48 +1,50 @@ // -// Created by Wenxi Zeng on 3/1/24. +// See the original post here: https://stackoverflow.com/a/12014833/8296631 // #ifndef SUBSTRATA_KOTLIN_JAVA_HELPER_H #define SUBSTRATA_KOTLIN_JAVA_HELPER_H #include +#include +#include -#define CLASS_NAME_ILLEGAL_STATE_EXCEPTION "java/lang/IllegalStateException" - -#define THROW_EXCEPTION(ENV, EXCEPTION_NAME, ...) \ - do { \ - throw_exception((ENV), (EXCEPTION_NAME), __VA_ARGS__); \ - return; \ - } while (0) -#define THROW_EXCEPTION_RET(ENV, EXCEPTION_NAME, ...) \ - do { \ - throw_exception((ENV), (EXCEPTION_NAME), __VA_ARGS__); \ - return 0; \ - } while (0) #define MSG_OOM "Out of memory" -#define MSG_NULL_JS_RUNTIME "Null JSRuntime" -#define MSG_NULL_JS_CONTEXT "Null JSContext" -#define MSG_NULL_JS_VALUE "Null JSValue" -#define THROW_ILLEGAL_STATE_EXCEPTION(ENV, ...) \ - THROW_EXCEPTION(ENV, CLASS_NAME_ILLEGAL_STATE_EXCEPTION, __VA_ARGS__) -#define THROW_ILLEGAL_STATE_EXCEPTION_RET(ENV, ...) \ - THROW_EXCEPTION_RET(ENV, CLASS_NAME_ILLEGAL_STATE_EXCEPTION, __VA_ARGS__) -#define CHECK_NULL(ENV, POINTER, MESSAGE) \ - do { \ - if ((POINTER) == NULL) { \ - THROW_ILLEGAL_STATE_EXCEPTION((ENV), (MESSAGE)); \ - } \ - } while (0) -#define CHECK_NULL_RET(ENV, POINTER, MESSAGE) \ - do { \ - if ((POINTER) == NULL) { \ - THROW_ILLEGAL_STATE_EXCEPTION_RET((ENV), (MESSAGE)); \ - } \ - } while (0) - -#define MAX_MSG_SIZE 1024 - -jint throw_exception(JNIEnv *env, const char *exception_name, const char *message, ...); +#define CLASS_NAME_ILLEGAL_STATE_EXCEPTION "java/lang/IllegalStateException" +struct ThrownJavaException : std::exception { + ThrownJavaException(const std::string& message) : m_message(message) {} + ThrownJavaException() : m_message("") {} + + // Override the what() method to provide a description of the exception + const char* what() const noexcept override { + return m_message.c_str(); + } + +private: + std::string m_message; +}; + +//used to throw a new Java exception. use full paths like: +//"java/lang/NoSuchFieldException" +//"java/lang/NullPointerException" +//"java/security/InvalidParameterException" +struct NewJavaException : public ThrownJavaException{ + NewJavaException(JNIEnv * env, const char* type, const char* message="") + :ThrownJavaException(type+std::string(" ")+message) + { + jclass newExcCls = env->FindClass(type); + if (newExcCls != NULL) + env->ThrowNew(newExcCls, message); + //if it is null, a NoClassDefFoundError was already thrown + } +}; + +inline void assert_no_exception(JNIEnv * env) { + if (env->ExceptionCheck()==JNI_TRUE) + throw ThrownJavaException("assert_no_exception"); +} + +void swallow_cpp_exception_and_throw_java(JNIEnv * env); #endif //SUBSTRATA_KOTLIN_JAVA_HELPER_H diff --git a/substrata-kotlin/src/main/cpp/quickjs_jni.cpp b/substrata-kotlin/src/main/cpp/quickjs_jni.cpp index 0d7ffb7..9fe8b80 100644 --- a/substrata-kotlin/src/main/cpp/quickjs_jni.cpp +++ b/substrata-kotlin/src/main/cpp/quickjs_jni.cpp @@ -48,51 +48,60 @@ Java_com_segment_analytics_substrata_kotlin_QuickJS_00024Companion_freeValue(JNI jlong context, jlong value) { - JSContext *ctx = (JSContext *) context; - CHECK_NULL(env, ctx, MSG_NULL_JS_CONTEXT); - JSValue *val = (JSValue *) value; - CHECK_NULL(env, val, MSG_NULL_JS_VALUE); - JSRuntime* rt = JS_GetRuntime(ctx); - if (JS_IsLiveObject(rt, *val)) { - if (js_get_refcount(*val) > 0) { - JS_FreeValue(ctx, *val); - js_free_rt(rt, val); + try { + JSContext *ctx = (JSContext *) context; + JSValue *val = (JSValue *) value; + JSRuntime *rt = JS_GetRuntime(ctx); + if (JS_IsLiveObject(rt, *val)) { + if (js_get_refcount(*val) > 0) { + JS_FreeValue(ctx, *val); + js_free_rt(rt, val); + } } } + catch (...) { + swallow_cpp_exception_and_throw_java(env); + } } extern "C" JNIEXPORT void JNICALL Java_com_segment_analytics_substrata_kotlin_QuickJS_00024Companion_freeRuntime(JNIEnv *env, jobject thiz, jlong runtime) { - - JSRuntime *rt = (JSRuntime *) runtime; -#ifdef LEAK_TRIGGER - leak_state = 0; -#endif - JS_FreeRuntime(rt); -#ifdef LEAK_TRIGGER - if (leak_state != 0) { - THROW_ILLEGAL_STATE_EXCEPTION(env, "Memory Leak"); + try { + JSRuntime *rt = (JSRuntime *) runtime; + JS_FreeRuntime(rt); + } + catch (...) { + swallow_cpp_exception_and_throw_java(env); } -#endif } extern "C" JNIEXPORT jboolean JNICALL Java_com_segment_analytics_substrata_kotlin_QuickJS_00024Companion_isBool(JNIEnv *env, jobject thiz, jlong value) { - JSValue *val = (JSValue *) value; - CHECK_NULL_RET(env, val, MSG_NULL_JS_VALUE); - return (jboolean) JS_IsBool(*val); + try { + JSValue *val = (JSValue *) value; + return (jboolean) JS_IsBool(*val); + } + catch (...) { + swallow_cpp_exception_and_throw_java(env); + return false; + } } extern "C" JNIEXPORT jboolean JNICALL Java_com_segment_analytics_substrata_kotlin_QuickJS_00024Companion_getBool(JNIEnv *env, jobject thiz, jlong value) { - JSValue *val = (JSValue *) value; - CHECK_NULL_RET(env, val, MSG_NULL_JS_VALUE); - return (jboolean) (JS_VALUE_GET_BOOL(*val)); + try { + JSValue *val = (JSValue *) value; + return (jboolean) (JS_VALUE_GET_BOOL(*val)); + } + catch (...) { + swallow_cpp_exception_and_throw_java(env); + return false; + } } extern "C" JNIEXPORT jlong JNICALL @@ -100,47 +109,64 @@ Java_com_segment_analytics_substrata_kotlin_QuickJS_00024Companion_newBool(JNIEn jobject thiz, jlong context, jboolean value) { - JSContext *ctx = (JSContext *) context; - CHECK_NULL_RET(env, ctx, MSG_NULL_JS_CONTEXT); + try { + JSContext *ctx = (JSContext *) context; - void *result = NULL; - JSValue val = JS_NewBool(ctx, value); + void *result = NULL; + JSValue val = JS_NewBool(ctx, value); - COPY_JS_VALUE(ctx, val, result); - CHECK_NULL_RET(env, result, MSG_OOM); - - return (jlong) result; + COPY_JS_VALUE(ctx, val, result); + return (jlong) result; + } + catch (...) { + swallow_cpp_exception_and_throw_java(env); + return 0; + } } extern "C" JNIEXPORT jboolean JNICALL Java_com_segment_analytics_substrata_kotlin_QuickJS_00024Companion_isNumber(JNIEnv *env, jobject thiz, jlong value) { - JSValue *val = (JSValue *) value; - CHECK_NULL_RET(env, val, MSG_NULL_JS_VALUE); - return (jboolean) JS_IsNumber(*val); + try { + JSValue *val = (JSValue *) value; + return (jboolean) JS_IsNumber(*val); + } + catch (...) { + swallow_cpp_exception_and_throw_java(env); + return false; + } } extern "C" JNIEXPORT jint JNICALL Java_com_segment_analytics_substrata_kotlin_QuickJS_00024Companion_getInt(JNIEnv *env, jobject thiz, jlong value) { - JSValue *val = (JSValue *) value; - CHECK_NULL_RET(env, val, MSG_NULL_JS_VALUE); - return (jint) (JS_VALUE_GET_INT(*val)); + try { + JSValue *val = (JSValue *) value; + return (jint) (JS_VALUE_GET_INT(*val)); + } + catch (...) { + swallow_cpp_exception_and_throw_java(env); + return 0; + } } extern "C" JNIEXPORT jlong JNICALL Java_com_segment_analytics_substrata_kotlin_QuickJS_00024Companion_newInt(JNIEnv *env, jobject thiz, jlong context, jint value) { - JSContext *ctx = (JSContext *) context; - CHECK_NULL_RET(env, ctx, MSG_NULL_JS_CONTEXT); + try { + JSContext *ctx = (JSContext *) context; - void *result = NULL; - JSValue val = JS_NewInt32(ctx, value); - COPY_JS_VALUE(ctx, val, result); - CHECK_NULL_RET(env, result, MSG_OOM); + void *result = NULL; + JSValue val = JS_NewInt32(ctx, value); + COPY_JS_VALUE(ctx, val, result); - return (jlong) result; + return (jlong) result; + } + catch (...) { + swallow_cpp_exception_and_throw_java(env); + return 0; + } } extern "C" JNIEXPORT jdouble JNICALL @@ -148,13 +174,17 @@ Java_com_segment_analytics_substrata_kotlin_QuickJS_00024Companion_getFloat64(JN jobject thiz, jlong context, jlong value) { - JSContext *ctx = (JSContext *) context; - CHECK_NULL_RET(env, ctx, MSG_NULL_JS_CONTEXT); - JSValue *val = (JSValue *) value; - CHECK_NULL_RET(env, val, MSG_NULL_JS_VALUE); - double d; - JS_ToFloat64(ctx, &d, *val); - return d; + try { + JSContext *ctx = (JSContext *) context; + JSValue *val = (JSValue *) value; + double d; + JS_ToFloat64(ctx, &d, *val); + return d; + } + catch (...) { + swallow_cpp_exception_and_throw_java(env); + return 0; + } } extern "C" JNIEXPORT jlong JNICALL @@ -162,24 +192,33 @@ Java_com_segment_analytics_substrata_kotlin_QuickJS_00024Companion_newFloat64(JN jobject thiz, jlong context, jdouble d) { - JSContext *ctx = (JSContext *) context; - CHECK_NULL_RET(env, ctx, MSG_NULL_JS_CONTEXT); + try { + JSContext *ctx = (JSContext *) context; - void *result = NULL; - JSValue val = JS_NewFloat64(ctx, d); - COPY_JS_VALUE(ctx, val, result); - CHECK_NULL_RET(env, result, MSG_OOM); + void *result = NULL; + JSValue val = JS_NewFloat64(ctx, d); + COPY_JS_VALUE(ctx, val, result); - return (jlong) result; + return (jlong) result; + } + catch (...) { + swallow_cpp_exception_and_throw_java(env); + return 0; + } } extern "C" JNIEXPORT jboolean JNICALL Java_com_segment_analytics_substrata_kotlin_QuickJS_00024Companion_isString(JNIEnv *env, jobject thiz, jlong value) { - JSValue *val = (JSValue *) value; - CHECK_NULL_RET(env, val, MSG_NULL_JS_VALUE); - return (jboolean) JS_IsString(*val); + try { + JSValue *val = (JSValue *) value; + return (jboolean) JS_IsString(*val); + } + catch (...) { + swallow_cpp_exception_and_throw_java(env); + return false; + } } extern "C" JNIEXPORT jstring JNICALL @@ -187,21 +226,21 @@ Java_com_segment_analytics_substrata_kotlin_QuickJS_00024Companion_getString(JNI jobject thiz, jlong context, jlong value) { - JSContext *ctx = (JSContext *) context; - CHECK_NULL_RET(env, ctx, MSG_NULL_JS_CONTEXT); - JSValue *val = (JSValue *) value; - CHECK_NULL_RET(env, val, MSG_NULL_JS_VALUE); - - const char *str = JS_ToCString(ctx, *val); - CHECK_NULL_RET(env, str, MSG_OOM); - - jstring j_str = env->NewStringUTF(str); + try { + JSContext *ctx = (JSContext *) context; + JSValue *val = (JSValue *) value; - JS_FreeCString(ctx, str); - - CHECK_NULL_RET(env, j_str, MSG_OOM); + const char *str = JS_ToCString(ctx, *val); + jstring j_str = env->NewStringUTF(str); + JS_FreeCString(ctx, str); - return j_str; + assert_no_exception(env); + return j_str; + } + catch (...) { + swallow_cpp_exception_and_throw_java(env); + return (jstring) ""; + } } extern "C" JNIEXPORT jlong JNICALL @@ -210,22 +249,24 @@ Java_com_segment_analytics_substrata_kotlin_QuickJS_00024Companion_newString(JNI jlong context, jstring value) { - JSContext *ctx = (JSContext *) context; - CHECK_NULL_RET(env, ctx, MSG_NULL_JS_CONTEXT); - CHECK_NULL_RET(env, value, "Null value"); + try { + JSContext *ctx = (JSContext *) context; - const char *value_utf = env->GetStringUTFChars(value, NULL); - CHECK_NULL_RET(env, value_utf, MSG_OOM); + const char *value_utf = env->GetStringUTFChars(value, NULL); - void *result = NULL; - JSValue val = JS_NewString(ctx, value_utf); - COPY_JS_VALUE(ctx, val, result); - - env->ReleaseStringUTFChars(value, value_utf); + void *result = NULL; + JSValue val = JS_NewString(ctx, value_utf); + COPY_JS_VALUE(ctx, val, result); - CHECK_NULL_RET(env, result, MSG_OOM); + env->ReleaseStringUTFChars(value, value_utf); - return (jlong) result; + assert_no_exception(env); + return (jlong) result; + } + catch (...) { + swallow_cpp_exception_and_throw_java(env); + return 0; + } } extern "C" JNIEXPORT jboolean JNICALL @@ -234,11 +275,15 @@ Java_com_segment_analytics_substrata_kotlin_QuickJS_00024Companion_isArray(JNIEn jlong context, jlong value) { - JSContext *ctx = (JSContext *) context; - CHECK_NULL_RET(env, ctx, MSG_NULL_JS_CONTEXT); - JSValue *val = (JSValue *) value; - CHECK_NULL_RET(env, val, MSG_NULL_JS_VALUE); - return (jboolean) JS_IsArray(ctx, *val); + try { + JSContext *ctx = (JSContext *) context; + JSValue *val = (JSValue *) value; + return (jboolean) JS_IsArray(ctx, *val); + } + catch (...) { + swallow_cpp_exception_and_throw_java(env); + return false; + } } extern "C" JNIEXPORT jlong JNICALL @@ -246,15 +291,19 @@ Java_com_segment_analytics_substrata_kotlin_QuickJS_00024Companion_newArray(JNIE jobject thiz, jlong context) { - JSContext *ctx = (JSContext *) context; - CHECK_NULL_RET(env, ctx, MSG_NULL_JS_CONTEXT); + try { + JSContext *ctx = (JSContext *) context; - void *result = NULL; - JSValue val = JS_NewArray(ctx); - COPY_JS_VALUE(ctx, val, result); - CHECK_NULL_RET(env, result, MSG_OOM); + void *result = NULL; + JSValue val = JS_NewArray(ctx); + COPY_JS_VALUE(ctx, val, result); - return (jlong) result; + return (jlong) result; + } + catch (...) { + swallow_cpp_exception_and_throw_java(env); + return 0; + } } extern "C" JNIEXPORT jlong JNICALL @@ -264,19 +313,20 @@ Java_com_segment_analytics_substrata_kotlin_QuickJS_00024Companion_getProperty__ jlong value, jint index) { - JSContext *ctx = (JSContext *) context; - CHECK_NULL_RET(env, ctx, MSG_NULL_JS_CONTEXT); - JSValue *val = (JSValue *) value; - CHECK_NULL_RET(env, val, MSG_NULL_JS_VALUE); - - void *result = NULL; + try { + JSContext *ctx = (JSContext *) context; + JSValue *val = (JSValue *) value; - JSValue prop = JS_GetPropertyUint32(ctx, *val, (uint32_t) index); + void *result = NULL; + JSValue prop = JS_GetPropertyUint32(ctx, *val, (uint32_t) index); + COPY_JS_VALUE(ctx, prop, result); - COPY_JS_VALUE(ctx, prop, result); - CHECK_NULL_RET(env, result, MSG_OOM); - - return (jlong) result; + return (jlong) result; + } + catch (...) { + swallow_cpp_exception_and_throw_java(env); + return 0; + } } extern "C" JNIEXPORT void JNICALL @@ -287,74 +337,79 @@ Java_com_segment_analytics_substrata_kotlin_QuickJS_00024Companion_setProperty__ jint index, jlong property) { - JSContext *ctx = (JSContext *) context; - CHECK_NULL(env, ctx, MSG_NULL_JS_CONTEXT); - JSValue *val = (JSValue *) value; - CHECK_NULL(env, val, MSG_NULL_JS_VALUE); - JSValue *prop = (JSValue *) property; - CHECK_NULL(env, prop, "Null property"); + try { + JSContext *ctx = (JSContext *) context; + JSValue *val = (JSValue *) value; + JSValue *prop = (JSValue *) property; - // JS_SetPropertyUint32 requires a reference count of the property JSValue - // Meanwhile, it calls JS_FreeValue on the property JSValue if it fails - JS_DupValue(ctx, *prop); - JS_SetPropertyUint32(ctx, *val, (uint32_t) index, *prop); + // JS_SetPropertyUint32 requires a reference count of the property JSValue + // Meanwhile, it calls JS_FreeValue on the property JSValue if it fails + JS_DupValue(ctx, *prop); + JS_SetPropertyUint32(ctx, *val, (uint32_t) index, *prop); + } + catch (...) { + swallow_cpp_exception_and_throw_java(env); + } } extern "C" JNIEXPORT void JNICALL Java_com_segment_analytics_substrata_kotlin_QuickJS_00024Companion_setProperty__JJLjava_lang_String_2J( JNIEnv *env, jobject thiz, jlong context, jlong value, jstring name, jlong property) { + try { + JSContext *ctx = (JSContext *) context; + JSValue *val = (JSValue *) value; + JSValue *prop = (JSValue *) property; - JSContext *ctx = (JSContext *) context; - CHECK_NULL(env, ctx, MSG_NULL_JS_CONTEXT); - JSValue *val = (JSValue *) value; - CHECK_NULL(env, val, MSG_NULL_JS_VALUE); - CHECK_NULL(env, name, "Null name"); - JSValue *prop = (JSValue *) property; - CHECK_NULL(env, prop, "Null property"); + const char *name_utf = env->GetStringUTFChars(name, NULL); - const char *name_utf = env->GetStringUTFChars(name, NULL); - CHECK_NULL(env, name_utf, MSG_OOM); + // JS_SetPropertyStr requires a reference count of the property JSValue + // Meanwhile, it calls JS_FreeValue on the property JSValue if it fails + JS_DupValue(ctx, *prop); + JS_SetPropertyStr(ctx, *val, name_utf, *prop); + env->ReleaseStringUTFChars(name, name_utf); - // JS_SetPropertyStr requires a reference count of the property JSValue - // Meanwhile, it calls JS_FreeValue on the property JSValue if it fails - JS_DupValue(ctx, *prop); - JS_SetPropertyStr(ctx, *val, name_utf, *prop); - env->ReleaseStringUTFChars(name, name_utf); + assert_no_exception(env); + } + catch (...) { + swallow_cpp_exception_and_throw_java(env); + } } extern "C" JNIEXPORT jlong JNICALL Java_com_segment_analytics_substrata_kotlin_QuickJS_00024Companion_getProperty__JJLjava_lang_String_2( JNIEnv *env, jobject thiz, jlong context, jlong value, jstring name) { + try { + JSContext *ctx = (JSContext *) context; + JSValue *val = (JSValue *) value; - JSContext *ctx = (JSContext *) context; - CHECK_NULL_RET(env, ctx, MSG_NULL_JS_CONTEXT); - JSValue *val = (JSValue *) value; - CHECK_NULL_RET(env, val, MSG_NULL_JS_VALUE); - CHECK_NULL_RET(env, name, "Null name"); - - const char *name_utf = env->GetStringUTFChars(name, NULL); - CHECK_NULL_RET(env, name_utf, MSG_OOM); - - void *result = NULL; - - JSValue prop = JS_GetPropertyStr(ctx, *val, name_utf); - - - COPY_JS_VALUE(ctx, prop, result); - env->ReleaseStringUTFChars(name, name_utf); + const char *name_utf = env->GetStringUTFChars(name, NULL); + void *result = NULL; - CHECK_NULL_RET(env, result, MSG_OOM); + JSValue prop = JS_GetPropertyStr(ctx, *val, name_utf); + COPY_JS_VALUE(ctx, prop, result); + env->ReleaseStringUTFChars(name, name_utf); - return (jlong) result; + assert_no_exception(env); + return (jlong) result; + } + catch (...) { + swallow_cpp_exception_and_throw_java(env); + return 0; + } } extern "C" JNIEXPORT jboolean JNICALL Java_com_segment_analytics_substrata_kotlin_QuickJS_00024Companion_isObject(JNIEnv *env, jobject thiz, jlong value) { - JSValue *val = (JSValue *) value; - CHECK_NULL_RET(env, val, MSG_NULL_JS_VALUE); - return (jboolean) JS_IsObject(*val); + try { + JSValue *val = (JSValue *) value; + return (jboolean) JS_IsObject(*val); + } + catch (...) { + swallow_cpp_exception_and_throw_java(env); + return false; + } } extern "C" JNIEXPORT jlong JNICALL @@ -362,15 +417,19 @@ Java_com_segment_analytics_substrata_kotlin_QuickJS_00024Companion_newObject(JNI jobject thiz, jlong context) { - JSContext *ctx = (JSContext *) context; - CHECK_NULL_RET(env, ctx, MSG_NULL_JS_CONTEXT); + try { + JSContext *ctx = (JSContext *) context; - void *result = NULL; - JSValue val = JS_NewObject(ctx); - COPY_JS_VALUE(ctx, val, result); - CHECK_NULL_RET(env, result, MSG_OOM); + void *result = NULL; + JSValue val = JS_NewObject(ctx); + COPY_JS_VALUE(ctx, val, result); - return (jlong) result; + return (jlong) result; + } + catch (...) { + swallow_cpp_exception_and_throw_java(env); + return 0; + } } extern "C" JNIEXPORT jlong JNICALL @@ -378,15 +437,19 @@ Java_com_segment_analytics_substrata_kotlin_QuickJS_00024Companion_getNull(JNIEn jobject thiz, jlong context) { - JSContext *ctx = (JSContext *) context; - CHECK_NULL_RET(env, ctx, MSG_NULL_JS_CONTEXT); + try { + JSContext *ctx = (JSContext *) context; - void *result = NULL; - JSValue val = JS_NULL; - COPY_JS_VALUE(ctx, val, result); - CHECK_NULL_RET(env, result, MSG_OOM); + void *result = NULL; + JSValue val = JS_NULL; + COPY_JS_VALUE(ctx, val, result); - return (jlong) result; + return (jlong) result; + } + catch (...) { + swallow_cpp_exception_and_throw_java(env); + return 0; + } } extern "C" JNIEXPORT jlong JNICALL @@ -394,24 +457,33 @@ Java_com_segment_analytics_substrata_kotlin_QuickJS_00024Companion_getUndefined( jobject thiz, jlong context) { - JSContext *ctx = (JSContext *) context; - CHECK_NULL_RET(env, ctx, MSG_NULL_JS_CONTEXT); + try { + JSContext *ctx = (JSContext *) context; - void *result = 0; - JSValue val = JS_UNDEFINED; - COPY_JS_VALUE(ctx, val, result); - CHECK_NULL_RET(env, result, MSG_OOM); + void *result = 0; + JSValue val = JS_UNDEFINED; + COPY_JS_VALUE(ctx, val, result); - return (jlong) result; + return (jlong) result; + } + catch (...) { + swallow_cpp_exception_and_throw_java(env); + return 0; + } } extern "C" JNIEXPORT jint JNICALL Java_com_segment_analytics_substrata_kotlin_QuickJS_00024Companion_getType(JNIEnv *env, jobject thiz, jlong value) { - JSValue *val = (JSValue *) value; - CHECK_NULL_RET(env, val, MSG_NULL_JS_VALUE); - return JS_VALUE_GET_NORM_TAG(*val); + try { + JSValue *val = (JSValue *) value; + return JS_VALUE_GET_NORM_TAG(*val); + } + catch (...) { + swallow_cpp_exception_and_throw_java(env); + return 0; + } } extern "C" @@ -420,29 +492,33 @@ Java_com_segment_analytics_substrata_kotlin_QuickJS_00024Companion_getOwnPropert jobject thiz, jlong context, jlong value) { - JSContext *ctx = (JSContext *) context; - JSPropertyEnum *names = NULL; - uint32_t count = 0; - JSValue *val = (JSValue *) value; - CHECK_NULL_RET(env, val, MSG_NULL_JS_VALUE); - - JS_GetOwnPropertyNames(ctx, &names, &count, *val, JS_GPN_ENUM_ONLY | JS_GPN_STRING_MASK); - - CHECK_NULL_RET(env, names, MSG_NULL_JS_VALUE); - - jclass stringClass = env->FindClass( "java/lang/String" ); - jobjectArray stringArray = env->NewObjectArray( count, stringClass, 0 ); + try { + JSContext *ctx = (JSContext *) context; + JSPropertyEnum *names = NULL; + uint32_t count = 0; + JSValue *val = (JSValue *) value; + + JS_GetOwnPropertyNames(ctx, &names, &count, *val, JS_GPN_ENUM_ONLY | JS_GPN_STRING_MASK); + + jclass stringClass = env->FindClass("java/lang/String"); + jobjectArray stringArray = env->NewObjectArray(count, stringClass, 0); + + JSPropertyEnum *iterator = names; + for (uint32_t i = 0; i < count; ++i) { + const char *str = JS_AtomToCString(ctx, iterator->atom); + JS_FreeCString(ctx, str); + jstring j_str = env->NewStringUTF(str); + env->SetObjectArrayElement(stringArray, i, j_str); + iterator++; + } - JSPropertyEnum* iterator = names; - for ( uint32_t i = 0; i < count; ++i ) { - const char* str = JS_AtomToCString(ctx, iterator->atom); - JS_FreeCString(ctx, str); - jstring j_str = env->NewStringUTF(str); - env->SetObjectArrayElement( stringArray, i, j_str ); - iterator++; + assert_no_exception(env); + return stringArray; + } + catch (...) { + swallow_cpp_exception_and_throw_java(env); + return nullptr; } - - return stringArray; } extern "C" JNIEXPORT jlong JNICALL @@ -451,71 +527,81 @@ Java_com_segment_analytics_substrata_kotlin_QuickJS_00024Companion_call(JNIEnv * jlong function, jlong obj, jlongArray args) { - JSContext *ctx = (JSContext *) context; - CHECK_NULL_RET(env, ctx, MSG_NULL_JS_CONTEXT); - JSValue *func_obj = (JSValue *) function; - CHECK_NULL_RET(env, func_obj, "Null function"); - JSValue *this_obj = (JSValue *) obj; - CHECK_NULL_RET(env, args, "Null arguments"); - jlong *elements = env->GetLongArrayElements(args, NULL); - CHECK_NULL_RET(env, elements, MSG_OOM); - - int argc = env->GetArrayLength(args); - JSValueConst argv[argc]; - for (int i = 0; i < argc; i++) { - argv[i] = *((JSValue *) elements[i]); - } - - void *result = NULL; - - JSValue ret = JS_Call(ctx, *func_obj, this_obj != NULL ? *this_obj : JS_UNDEFINED, argc, argv); + try { + JSContext *ctx = (JSContext *) context; + JSValue *func_obj = (JSValue *) function; + JSValue *this_obj = (JSValue *) obj; + jlong *elements = env->GetLongArrayElements(args, NULL); - COPY_JS_VALUE(ctx, ret, result); - - env->ReleaseLongArrayElements(args, elements, JNI_ABORT); + int argc = env->GetArrayLength(args); + JSValueConst argv[argc]; + for (int i = 0; i < argc; i++) { + argv[i] = *((JSValue *) elements[i]); + } - CHECK_NULL_RET(env, result, MSG_OOM); + void *result = NULL; + JSValue ret = JS_Call(ctx, *func_obj, this_obj != NULL ? *this_obj : JS_UNDEFINED, argc, + argv); + COPY_JS_VALUE(ctx, ret, result); + env->ReleaseLongArrayElements(args, elements, JNI_ABORT); - return (jlong) result; + assert_no_exception(env); + return (jlong) result; + } + catch (...) { + swallow_cpp_exception_and_throw_java(env); + return 0; + } } extern "C" JNIEXPORT jlong JNICALL Java_com_segment_analytics_substrata_kotlin_QuickJS_00024Companion_newRuntime(JNIEnv *env, jobject thiz) { - JSRuntime *rt = JS_NewRuntime(); - CHECK_NULL_RET(env, rt, MSG_OOM); - return (jlong) rt; + try { + JSRuntime *rt = JS_NewRuntime(); + return (jlong) rt; + } + catch (...) { + swallow_cpp_exception_and_throw_java(env); + return 0; + } } extern "C" JNIEXPORT jlong JNICALL Java_com_segment_analytics_substrata_kotlin_QuickJS_00024Companion_newContext(JNIEnv *env, jobject thiz, jlong runtime) { - JSRuntime *rt = (JSRuntime *) runtime; - JSContext *ctx = JS_NewContext(rt); - CHECK_NULL_RET(env, ctx, MSG_OOM); + try { + JSRuntime *rt = (JSRuntime *) runtime; + JSContext *ctx = JS_NewContext(rt); - if (java_callback_init(ctx)) THROW_ILLEGAL_STATE_EXCEPTION_RET(env, MSG_OOM); + if (java_callback_init(ctx)) throw NewJavaException(env, CLASS_NAME_ILLEGAL_STATE_EXCEPTION, MSG_OOM); - return (jlong) ctx; + return (jlong) ctx; + } + catch (...) { + swallow_cpp_exception_and_throw_java(env); + return 0; + } } extern "C" JNIEXPORT jlong JNICALL Java_com_segment_analytics_substrata_kotlin_QuickJS_00024Companion_getGlobalObject(JNIEnv *env, jobject thiz, jlong context) { + try { + JSContext *ctx = (JSContext *) context; - JSContext *ctx = (JSContext *) context; - CHECK_NULL_RET(env, ctx, MSG_NULL_JS_CONTEXT); - - void *result = NULL; - - JSValue val = JS_GetGlobalObject(ctx); - COPY_JS_VALUE(ctx, val, result); - - CHECK_NULL_RET(env, result, MSG_OOM); + void *result = NULL; + JSValue val = JS_GetGlobalObject(ctx); + COPY_JS_VALUE(ctx, val, result); - return (jlong) result; + return (jlong) result; + } + catch (...) { + swallow_cpp_exception_and_throw_java(env); + return 0; + } } extern "C" JNIEXPORT jlong JNICALL @@ -525,44 +611,51 @@ Java_com_segment_analytics_substrata_kotlin_QuickJS_00024Companion_evaluate(JNIE jstring source_code, jstring file_name, jint flags) { - JSContext *ctx = (JSContext *) context; - CHECK_NULL_RET(env, ctx, MSG_NULL_JS_CONTEXT); - CHECK_NULL_RET(env, source_code, "Null source code"); - CHECK_NULL_RET(env, file_name, "Null file name"); - - const char *source_code_utf = NULL; - jsize source_code_length = 0; - const char *file_name_utf = NULL; - void *result = NULL; - - source_code_utf = env->GetStringUTFChars(source_code, NULL); - source_code_length = env->GetStringUTFLength(source_code); - file_name_utf = env->GetStringUTFChars(file_name, NULL); + try { + JSContext *ctx = (JSContext *) context; + + const char *source_code_utf = NULL; + jsize source_code_length = 0; + const char *file_name_utf = NULL; + void *result = NULL; + + source_code_utf = env->GetStringUTFChars(source_code, NULL); + source_code_length = env->GetStringUTFLength(source_code); + file_name_utf = env->GetStringUTFChars(file_name, NULL); + + if (source_code_utf != NULL && file_name_utf != NULL) { + JSValue val = JS_Eval(ctx, source_code_utf, (size_t) source_code_length, file_name_utf, + flags); + COPY_JS_VALUE(ctx, val, result); + } - if (source_code_utf != NULL && file_name_utf != NULL) { - JSValue val = JS_Eval(ctx, source_code_utf, (size_t) source_code_length, file_name_utf, flags); - COPY_JS_VALUE(ctx, val, result); - } + if (source_code_utf != NULL) { + env->ReleaseStringUTFChars(source_code, source_code_utf); + } + if (file_name_utf != NULL) { + env->ReleaseStringUTFChars(file_name, file_name_utf); + } - if (source_code_utf != NULL) { - env->ReleaseStringUTFChars(source_code, source_code_utf); + assert_no_exception(env); + return (jlong) result; } - if (file_name_utf != NULL) { - env->ReleaseStringUTFChars(file_name, file_name_utf); + catch (...) { + swallow_cpp_exception_and_throw_java(env); + return 0; } - - CHECK_NULL_RET(env, result, MSG_OOM); - - return (jlong) result; } extern "C" JNIEXPORT void JNICALL Java_com_segment_analytics_substrata_kotlin_QuickJS_00024Companion_freeContext(JNIEnv *env, jobject thiz, jlong context) { - JSContext *ctx = (JSContext *) context; - CHECK_NULL(env, ctx, MSG_NULL_JS_CONTEXT); - JS_FreeContext(ctx); + try { + JSContext *ctx = (JSContext *) context; + JS_FreeContext(ctx); + } + catch (...) { + swallow_cpp_exception_and_throw_java(env); + } } extern "C" JNIEXPORT jboolean JNICALL @@ -570,11 +663,15 @@ Java_com_segment_analytics_substrata_kotlin_QuickJS_00024Companion_isFunction(JN jobject thiz, jlong context, jlong value) { - JSContext *ctx = (JSContext *) context; - CHECK_NULL_RET(env, ctx, MSG_NULL_JS_CONTEXT); - JSValue *val = (JSValue *) value; - CHECK_NULL_RET(env, val, MSG_NULL_JS_VALUE); - return (jboolean) JS_IsFunction(ctx, *val); + try { + JSContext *ctx = (JSContext *) context; + JSValue *val = (JSValue *) value; + return (jboolean) JS_IsFunction(ctx, *val); + } + catch (...) { + swallow_cpp_exception_and_throw_java(env); + return false; + } } extern "C" JNIEXPORT jboolean JNICALL @@ -583,86 +680,96 @@ Java_com_segment_analytics_substrata_kotlin_QuickJS_00024Companion_hasProperty(J jlong context, jlong value, jstring name) { - JSContext *ctx = (JSContext *) context; - CHECK_NULL_RET(env, ctx, MSG_NULL_JS_CONTEXT); - JSValue *val = (JSValue *) value; - CHECK_NULL_RET(env, val, MSG_NULL_JS_VALUE); - - const char *name_utf = env->GetStringUTFChars(name, NULL); - CHECK_NULL_RET(env, name_utf, MSG_OOM); - JSAtom atom = JS_NewAtom(ctx, name_utf); - int result = 0; - if (atom != JS_ATOM_NULL) { - result = JS_HasProperty(ctx, *val, atom); - JS_FreeAtom(ctx, atom); - return result; + try { + JSContext *ctx = (JSContext *) context; + JSValue *val = (JSValue *) value; + + const char *name_utf = env->GetStringUTFChars(name, NULL); + JSAtom atom = JS_NewAtom(ctx, name_utf); + int result = 0; + if (atom != JS_ATOM_NULL) { + result = JS_HasProperty(ctx, *val, atom); + JS_FreeAtom(ctx, atom); + return result; + } + env->ReleaseStringUTFChars(name, name_utf); + + assert_no_exception(env); + return (jboolean) result; + } + catch (...) { + swallow_cpp_exception_and_throw_java(env); + return false; } - env->ReleaseStringUTFChars(name, name_utf); - return (jboolean)result; } static jlong JS_ToPointer(JNIEnv* env, JSContext *ctx, JSValue val) { void *result = NULL; - COPY_JS_VALUE(ctx, val, result); - CHECK_NULL_RET(env, result, MSG_OOM); return (jlong)result; } static JSValue invoke(JSContext *ctx, JSValueConst this_val, int argc, JSValueConst *argv, int magic, JSValue *func_data) { - JavaCallbackData *data = (JavaCallbackData*)JS_GetOpaque(*func_data, java_callback_class_id); - JNIEnv *env; - bool attached = false; - switch(data->vm->GetEnv((void**)&env, JNI_VERSION_1_6)) { - case JNI_OK: - break; - case JNI_EDETACHED: - data->vm->AttachCurrentThread(&env, NULL); - attached = true; - break; - } - - jclass clazz = env->FindClass( "com/segment/analytics/substrata/kotlin/JSRegistry" ); - jmethodID method = env->GetMethodID(clazz, "jsCallback", "(Ljava/lang/Object;I[J)J"); - jclass contextClazz = env->FindClass( "com/segment/analytics/substrata/kotlin/JSContext" ); - - jlongArray params = env->NewLongArray(argc); - if (argc > 0) { - jlong paramsC[argc]; - for (int i = 0; i < argc; i++) { - paramsC[i] = JS_ToPointer(env, ctx, argv[i]); + try { + JavaCallbackData *data = (JavaCallbackData *) JS_GetOpaque(*func_data, + java_callback_class_id); + + bool attached = false; + switch (data->vm->GetEnv((void **) &env, JNI_VERSION_1_6)) { + case JNI_OK: + break; + case JNI_EDETACHED: + data->vm->AttachCurrentThread(&env, NULL); + attached = true; + break; } - env->SetLongArrayRegion(params, 0, argc, paramsC); - } - jfieldID field = env->GetFieldID(contextClazz, "registry", - "Lcom/segment/analytics/substrata/kotlin/JSRegistry;"); - jobject registry = env->GetObjectField(data->js_context, field); - - JSAtom atom = JS_NewAtom(ctx, "__instanceAtom"); - jlong ret; - if (JS_HasProperty(ctx, this_val, atom)) { - JSValue instanceClassId = JS_GetProperty(ctx, this_val, atom); - int classId = 0; - JS_ToInt32(ctx, &classId, instanceClassId); - JavaInstanceData *instanceData = (JavaInstanceData*)JS_GetOpaque(this_val, classId); - ret = env->CallLongMethod(registry, method, instanceData->instance, (jint) magic, params); - } - else { - jobject nullObject = NULL; - ret = env->CallLongMethod(registry, method, nullObject, (jint) magic, params); - } - JSValue *retVal = (JSValue *) ret; + jclass clazz = env->FindClass("com/segment/analytics/substrata/kotlin/JSRegistry"); + jmethodID method = env->GetMethodID(clazz, "jsCallback", "(Ljava/lang/Object;I[J)J"); + jclass contextClazz = env->FindClass("com/segment/analytics/substrata/kotlin/JSContext"); + + jlongArray params = env->NewLongArray(argc); + if (argc > 0) { + jlong paramsC[argc]; + for (int i = 0; i < argc; i++) { + paramsC[i] = JS_ToPointer(env, ctx, argv[i]); + } + env->SetLongArrayRegion(params, 0, argc, paramsC); + } - JS_FreeAtom(ctx, atom); - env->DeleteLocalRef(params); - if (attached) { - data->vm->DetachCurrentThread(); - } + jfieldID field = env->GetFieldID(contextClazz, "registry", + "Lcom/segment/analytics/substrata/kotlin/JSRegistry;"); + jobject registry = env->GetObjectField(data->js_context, field); + + JSAtom atom = JS_NewAtom(ctx, "__instanceAtom"); + jlong ret; + if (JS_HasProperty(ctx, this_val, atom)) { + JSValue instanceClassId = JS_GetProperty(ctx, this_val, atom); + int classId = 0; + JS_ToInt32(ctx, &classId, instanceClassId); + JavaInstanceData *instanceData = (JavaInstanceData *) JS_GetOpaque(this_val, classId); + ret = env->CallLongMethod(registry, method, instanceData->instance, (jint) magic, + params); + } else { + jobject nullObject = NULL; + ret = env->CallLongMethod(registry, method, nullObject, (jint) magic, params); + } + JSValue *retVal = (JSValue *) ret; - return *retVal; + JS_FreeAtom(ctx, atom); + env->DeleteLocalRef(params); + if (attached) { + data->vm->DetachCurrentThread(); + } + assert_no_exception(env); + return *retVal; + } + catch (...) { + swallow_cpp_exception_and_throw_java(env); + return JS_UNDEFINED; + } } extern "C" JNIEXPORT jlong JNICALL @@ -673,35 +780,40 @@ Java_com_segment_analytics_substrata_kotlin_QuickJS_00024Companion_newFunction(J jlong value, jstring name, jint id) { - JSContext *ctx = (JSContext *) context; - CHECK_NULL_RET(env, ctx, MSG_NULL_JS_CONTEXT); - JSValue *val = (JSValue *) value; - CHECK_NULL_RET(env, val, MSG_NULL_JS_VALUE); - - // create JavaCallbackData that carries JSContext instance for later use in callback - JSRuntime *rt = JS_GetRuntime(ctx); - JavaCallbackData *data = NULL; - data = (JavaCallbackData*) js_malloc_rt(rt, sizeof(JavaCallbackData)); - JSValue callback = JS_NewObjectClass(ctx, java_callback_class_id); - env->GetJavaVM(&data->vm); - data->js_context = env->NewGlobalRef(js_context); - JS_SetOpaque(callback, data); - - const char *name_utf = env->GetStringUTFChars(name, NULL); - JSValue newFunction = JS_NewCFunctionData(ctx, invoke, 1, id, 2, &callback); - - // JS_SetPropertyStr requires a reference count of the property JSValue - // Meanwhile, it calls JS_FreeValue on the property JSValue if it fails - JS_DupValue(ctx, newFunction); - JS_SetPropertyStr(ctx, *val, name_utf, newFunction); - - env->ReleaseStringUTFChars(name, name_utf); - JS_FreeValue(ctx, callback); - - void *result = NULL; - COPY_JS_VALUE(ctx, newFunction, result); - CHECK_NULL_RET(env, result, MSG_OOM); - return (jlong) result; + try { + JSContext *ctx = (JSContext *) context; + JSValue *val = (JSValue *) value; + + // create JavaCallbackData that carries JSContext instance for later use in callback + JSRuntime *rt = JS_GetRuntime(ctx); + JavaCallbackData *data = NULL; + data = (JavaCallbackData *) js_malloc_rt(rt, sizeof(JavaCallbackData)); + JSValue callback = JS_NewObjectClass(ctx, java_callback_class_id); + env->GetJavaVM(&data->vm); + data->js_context = env->NewGlobalRef(js_context); + JS_SetOpaque(callback, data); + + const char *name_utf = env->GetStringUTFChars(name, NULL); + JSValue newFunction = JS_NewCFunctionData(ctx, invoke, 1, id, 2, &callback); + + // JS_SetPropertyStr requires a reference count of the property JSValue + // Meanwhile, it calls JS_FreeValue on the property JSValue if it fails + JS_DupValue(ctx, newFunction); + JS_SetPropertyStr(ctx, *val, name_utf, newFunction); + + env->ReleaseStringUTFChars(name, name_utf); + JS_FreeValue(ctx, callback); + + void *result = NULL; + COPY_JS_VALUE(ctx, newFunction, result); + + assert_no_exception(env); + return (jlong) result; + } + catch (...) { + swallow_cpp_exception_and_throw_java(env); + return 0; + } } extern "C" JNIEXPORT jobject JNICALL @@ -709,97 +821,111 @@ Java_com_segment_analytics_substrata_kotlin_QuickJS_00024Companion_getException( jobject thiz, jlong context) { - JSContext *ctx = (JSContext *) context; - CHECK_NULL_RET(env, ctx, MSG_NULL_JS_CONTEXT); - - jclass js_exception_class = env->FindClass( "com/segment/analytics/substrata/kotlin/JSException"); - CHECK_NULL_RET(env, js_exception_class, "Can't find JSException"); - - jmethodID constructor_id = env->GetMethodID(js_exception_class, "", "(ZLjava/lang/String;Ljava/lang/String;)V"); - CHECK_NULL_RET(env, constructor_id, "Can't find JSException constructor"); + try { + JSContext *ctx = (JSContext *) context; + + jclass js_exception_class = env->FindClass( + "com/segment/analytics/substrata/kotlin/JSException"); + jmethodID constructor_id = env->GetMethodID(js_exception_class, "", + "(ZLjava/lang/String;Ljava/lang/String;)V"); + + const char *exception_str = NULL; + const char *stack_str = NULL; + + JSValue exception = JS_GetException(ctx); + exception_str = JS_ToCString(ctx, exception); + jboolean is_error = (jboolean) JS_IsError(ctx, exception); + if (is_error) { + JSValue stack = JS_GetPropertyStr(ctx, exception, "stack"); + if (!JS_IsUndefined(stack)) { + stack_str = JS_ToCString(ctx, stack); + } + JS_FreeValue(ctx, stack); + } + JS_FreeValue(ctx, exception); - const char *exception_str = NULL; - const char *stack_str = NULL; + jstring exception_j_str = (exception_str != NULL) ? env->NewStringUTF(exception_str) : NULL; + jstring stack_j_str = (stack_str != NULL) ? env->NewStringUTF(stack_str) : NULL; - JSValue exception = JS_GetException(ctx); - exception_str = JS_ToCString(ctx, exception); - jboolean is_error = (jboolean) JS_IsError(ctx, exception); - if (is_error) { - JSValue stack = JS_GetPropertyStr(ctx, exception, "stack"); - if (!JS_IsUndefined(stack)) { - stack_str = JS_ToCString(ctx, stack); + if (exception_str != NULL) { + JS_FreeCString(ctx, exception_str); + } + if (stack_str != NULL) { + JS_FreeCString(ctx, stack_str); } - JS_FreeValue(ctx, stack); - } - JS_FreeValue(ctx, exception); - jstring exception_j_str = (exception_str != NULL) ? env->NewStringUTF(exception_str) : NULL; - jstring stack_j_str = (stack_str != NULL) ? env->NewStringUTF(stack_str) : NULL; + jobject result = env->NewObject(js_exception_class, constructor_id, is_error, + exception_j_str, stack_j_str); - if (exception_str != NULL) { - JS_FreeCString(ctx, exception_str); + assert_no_exception(env); + return result; } - if (stack_str != NULL) { - JS_FreeCString(ctx, stack_str); + catch (...) { + swallow_cpp_exception_and_throw_java(env); + return nullptr; } - - jobject result = env->NewObject(js_exception_class, constructor_id, is_error, exception_j_str, stack_j_str); - CHECK_NULL_RET(env, result, "Can't create instance of JSException"); - - return result; } static JSValue construct(JSContext *ctx, JSValueConst this_val, int argc, JSValueConst *argv, int magic, JSValue *func_data) { - JavaConstructData *data = (JavaConstructData*)JS_GetOpaque(*func_data, magic); - JNIEnv *env; - bool attached = false; - switch(data->vm->GetEnv((void**)&env, JNI_VERSION_1_6)) { - case JNI_OK: - break; - case JNI_EDETACHED: - data->vm->AttachCurrentThread(&env, NULL); - attached = true; - break; - } - - JSValue result = js_create_from_ctor(ctx, this_val, magic); - void *resultPtr = NULL; - COPY_JS_VALUE(ctx, result, resultPtr); - - jclass clazz = env->FindClass( "com/segment/analytics/substrata/kotlin/JSRegistry" ); - jmethodID method = env->GetMethodID(clazz, "registerInstance", "(JI[J)Ljava/lang/Object;"); - jclass contextClazz = env->FindClass( "com/segment/analytics/substrata/kotlin/JSContext" ); - - jlongArray params = env->NewLongArray(argc); - if (argc > 0) { - jlong paramsC[argc]; - for (int i = 0; i < argc; i++) { - paramsC[i] = JS_ToPointer(env, ctx, argv[i]); - } - env->SetLongArrayRegion(params, 0, argc, paramsC); - } - jfieldID field = env->GetFieldID(contextClazz, "registry", - "Lcom/segment/analytics/substrata/kotlin/JSRegistry;"); - jobject registry = env->GetObjectField(data->js_context, field); - jobject instance = env->CallObjectMethod(registry, method, (jlong)resultPtr, (jint)data->class_id, params); + try { + JavaConstructData *data = (JavaConstructData *) JS_GetOpaque(*func_data, magic); + bool attached = false; + switch (data->vm->GetEnv((void **) &env, JNI_VERSION_1_6)) { + case JNI_OK: + break; + case JNI_EDETACHED: + data->vm->AttachCurrentThread(&env, NULL); + attached = true; + break; + } - JSRuntime *rt = JS_GetRuntime(ctx); - JavaInstanceData* instanceData = (JavaInstanceData*) js_malloc_rt(rt, sizeof(JavaInstanceData)); - instanceData->instance = env->NewGlobalRef(instance);; - JS_SetPropertyStr(ctx, JS_DupValue(ctx, result), "__instanceAtom", JS_NewInt32(ctx, magic)); - JS_SetOpaque(result, instanceData); + JSValue result = js_create_from_ctor(ctx, this_val, magic); + void *resultPtr = NULL; + COPY_JS_VALUE(ctx, result, resultPtr); + + jclass clazz = env->FindClass("com/segment/analytics/substrata/kotlin/JSRegistry"); + jmethodID method = env->GetMethodID(clazz, "registerInstance", "(JI[J)Ljava/lang/Object;"); + jclass contextClazz = env->FindClass("com/segment/analytics/substrata/kotlin/JSContext"); + + jlongArray params = env->NewLongArray(argc); + if (argc > 0) { + jlong paramsC[argc]; + for (int i = 0; i < argc; i++) { + paramsC[i] = JS_ToPointer(env, ctx, argv[i]); + } + env->SetLongArrayRegion(params, 0, argc, paramsC); + } - env->DeleteLocalRef(params); - if (attached) { - data->vm->DetachCurrentThread(); - } + jfieldID field = env->GetFieldID(contextClazz, "registry", + "Lcom/segment/analytics/substrata/kotlin/JSRegistry;"); + jobject registry = env->GetObjectField(data->js_context, field); + jobject instance = env->CallObjectMethod(registry, method, (jlong) resultPtr, + (jint) data->class_id, params); + + JSRuntime *rt = JS_GetRuntime(ctx); + JavaInstanceData *instanceData = (JavaInstanceData *) js_malloc_rt(rt, + sizeof(JavaInstanceData)); + instanceData->instance = env->NewGlobalRef(instance);; + JS_SetPropertyStr(ctx, JS_DupValue(ctx, result), "__instanceAtom", JS_NewInt32(ctx, magic)); + JS_SetOpaque(result, instanceData); + + env->DeleteLocalRef(params); + if (attached) { + data->vm->DetachCurrentThread(); + } // DO NOT release JavaConstructData, it is shared for all instance creation of the same class!! // env->DeleteGlobalRef(data->js_context); // js_free_rt(rt, data); - return result; + assert_no_exception(env); + return result; + } + catch (...) { + swallow_cpp_exception_and_throw_java(env); + return JS_UNDEFINED; + } } extern "C" @@ -811,60 +937,65 @@ Java_com_segment_analytics_substrata_kotlin_QuickJS_00024Companion_newClass(JNIE jlong value_ref, jstring name, jint id) { - JSContext *ctx = (JSContext *) context_ref; - CHECK_NULL(env, ctx, MSG_NULL_JS_CONTEXT); - JSValue *val = (JSValue *) value_ref; - CHECK_NULL(env, val, MSG_NULL_JS_VALUE); - JSRuntime *rt = JS_GetRuntime(ctx); - const char *class_name = env->GetStringUTFChars(name, NULL); - - // Create class - JSClassDef class_def = { - .class_name = class_name, - .finalizer = NULL, - }; - JSClassID class_id = 0; - JS_NewClassID(&class_id); - JS_NewClass(rt, class_id, &class_def); - - // create data for constructor callback - JavaConstructData *data = NULL; - data = (JavaConstructData*) js_malloc_rt(rt, sizeof(JavaConstructData)); - JSValue callback = JS_NewObjectClass(ctx, class_id); - env->GetJavaVM(&data->vm); - data->js_context = env->NewGlobalRef(context); - data->class_id = id; - JS_SetOpaque(callback, data); - - // create constructor and prototype - JSValue constructor = JS_NewCFunctionData2(ctx, construct, 1, JS_CFUNC_constructor, class_id, 2, &callback); - JSValue prototype = JS_NewObject(ctx); - JS_DefinePropertyValueStr(ctx, *val, class_name, - JS_DupValue(ctx, constructor), - JS_PROP_WRITABLE); - JS_SetConstructor(ctx, constructor, prototype); - JS_SetClassProto(ctx, class_id, prototype); - - void *ctorPtr = NULL; - void *prototypePtr = NULL; - COPY_JS_VALUE(ctx, constructor, ctorPtr); - COPY_JS_VALUE(ctx, prototype, prototypePtr); - - // set instance methods, static methods, static properties on prototype - jclass clazz = env->FindClass( "com/segment/analytics/substrata/kotlin/JSRegistry" ); - jmethodID registerCtor = env->GetMethodID(clazz, "registerConstructor","(JI)V"); - jmethodID registerProto = env->GetMethodID(clazz, "registerPrototype","(JI)V"); - jclass contextClazz = env->FindClass( "com/segment/analytics/substrata/kotlin/JSContext" ); - jfieldID field = env->GetFieldID(contextClazz, "registry", - "Lcom/segment/analytics/substrata/kotlin/JSRegistry;"); - jobject registry = env->GetObjectField(context, field); - env->CallVoidMethod(registry, registerCtor, (jlong)ctorPtr, id); - env->CallVoidMethod(registry, registerProto, (jlong)prototypePtr, id); - - - // clean up - env->ReleaseStringUTFChars(name, class_name); - JS_FreeValue(ctx, callback); + try { + JSContext *ctx = (JSContext *) context_ref; + JSValue *val = (JSValue *) value_ref; + JSRuntime *rt = JS_GetRuntime(ctx); + const char *class_name = env->GetStringUTFChars(name, NULL); + + // Create class + JSClassDef class_def = { + .class_name = class_name, + .finalizer = NULL, + }; + JSClassID class_id = 0; + JS_NewClassID(&class_id); + JS_NewClass(rt, class_id, &class_def); + + // create data for constructor callback + JavaConstructData *data = NULL; + data = (JavaConstructData *) js_malloc_rt(rt, sizeof(JavaConstructData)); + JSValue callback = JS_NewObjectClass(ctx, class_id); + env->GetJavaVM(&data->vm); + data->js_context = env->NewGlobalRef(context); + data->class_id = id; + JS_SetOpaque(callback, data); + + // create constructor and prototype + JSValue constructor = JS_NewCFunctionData2(ctx, construct, 1, JS_CFUNC_constructor, + class_id, 2, &callback); + JSValue prototype = JS_NewObject(ctx); + JS_DefinePropertyValueStr(ctx, *val, class_name, + JS_DupValue(ctx, constructor), + JS_PROP_WRITABLE); + JS_SetConstructor(ctx, constructor, prototype); + JS_SetClassProto(ctx, class_id, prototype); + + void *ctorPtr = NULL; + void *prototypePtr = NULL; + COPY_JS_VALUE(ctx, constructor, ctorPtr); + COPY_JS_VALUE(ctx, prototype, prototypePtr); + + // set instance methods, static methods, static properties on prototype + jclass clazz = env->FindClass("com/segment/analytics/substrata/kotlin/JSRegistry"); + jmethodID registerCtor = env->GetMethodID(clazz, "registerConstructor", "(JI)V"); + jmethodID registerProto = env->GetMethodID(clazz, "registerPrototype", "(JI)V"); + jclass contextClazz = env->FindClass("com/segment/analytics/substrata/kotlin/JSContext"); + jfieldID field = env->GetFieldID(contextClazz, "registry", + "Lcom/segment/analytics/substrata/kotlin/JSRegistry;"); + jobject registry = env->GetObjectField(context, field); + env->CallVoidMethod(registry, registerCtor, (jlong) ctorPtr, id); + env->CallVoidMethod(registry, registerProto, (jlong) prototypePtr, id); + + // clean up + env->ReleaseStringUTFChars(name, class_name); + JS_FreeValue(ctx, callback); + + assert_no_exception(env); + } + catch (...) { + swallow_cpp_exception_and_throw_java(env); + } } extern "C" JNIEXPORT void JNICALL @@ -876,30 +1007,38 @@ Java_com_segment_analytics_substrata_kotlin_QuickJS_00024Companion_newProperty(J jstring name, jint getter_id, jint setter_id) { - JSContext *ctx = (JSContext *) context; - CHECK_NULL(env, ctx, MSG_NULL_JS_CONTEXT); - JSValue *val = (JSValue *) value; - CHECK_NULL(env, val, MSG_NULL_JS_VALUE); - - // create JavaCallbackData that carries JSContext instance for later use in callback - JSRuntime *rt = JS_GetRuntime(ctx); - JavaCallbackData *data = NULL; - data = (JavaCallbackData*) js_malloc_rt(rt, sizeof(JavaCallbackData)); - JSValue callback = JS_NewObjectClass(ctx, java_callback_class_id); - env->GetJavaVM(&data->vm); - data->js_context = env->NewGlobalRef(js_context); - JS_SetOpaque(callback, data); - - const char *name_utf = env->GetStringUTFChars(name, NULL); - JSAtom propAtom = JS_NewAtom(ctx, name_utf); - JSValue getter = JS_NewCFunctionData(ctx, invoke, 1, getter_id, 2, &callback); - JSValue setter = JS_NewCFunctionData(ctx, invoke, 1, setter_id, 2, &callback); - - JS_DefinePropertyGetSet(ctx, *val, propAtom, JS_DupValue(ctx, getter), JS_DupValue(ctx, setter), JS_PROP_HAS_WRITABLE | JS_PROP_HAS_ENUMERABLE | JS_PROP_HAS_GET | JS_PROP_HAS_SET); - - env->ReleaseStringUTFChars(name, name_utf); - JS_FreeValue(ctx, callback); - JS_FreeAtom(ctx, propAtom); - JS_FreeValue(ctx, getter); - JS_FreeValue(ctx, setter); + try { + JSContext *ctx = (JSContext *) context; + JSValue *val = (JSValue *) value; + + // create JavaCallbackData that carries JSContext instance for later use in callback + JSRuntime *rt = JS_GetRuntime(ctx); + JavaCallbackData *data = NULL; + data = (JavaCallbackData *) js_malloc_rt(rt, sizeof(JavaCallbackData)); + JSValue callback = JS_NewObjectClass(ctx, java_callback_class_id); + env->GetJavaVM(&data->vm); + data->js_context = env->NewGlobalRef(js_context); + JS_SetOpaque(callback, data); + + const char *name_utf = env->GetStringUTFChars(name, NULL); + JSAtom propAtom = JS_NewAtom(ctx, name_utf); + JSValue getter = JS_NewCFunctionData(ctx, invoke, 1, getter_id, 2, &callback); + JSValue setter = JS_NewCFunctionData(ctx, invoke, 1, setter_id, 2, &callback); + + JS_DefinePropertyGetSet(ctx, *val, propAtom, JS_DupValue(ctx, getter), + JS_DupValue(ctx, setter), + JS_PROP_HAS_WRITABLE | JS_PROP_HAS_ENUMERABLE | JS_PROP_HAS_GET | + JS_PROP_HAS_SET); + + env->ReleaseStringUTFChars(name, name_utf); + JS_FreeValue(ctx, callback); + JS_FreeAtom(ctx, propAtom); + JS_FreeValue(ctx, getter); + JS_FreeValue(ctx, setter); + + assert_no_exception(env); + } + catch (...) { + swallow_cpp_exception_and_throw_java(env); + } } \ No newline at end of file diff --git a/substrata-kotlin/src/main/java/com/segment/analytics/substrata/kotlin/Conversions.kt b/substrata-kotlin/src/main/java/com/segment/analytics/substrata/kotlin/Conversions.kt index 977e167..88f2092 100644 --- a/substrata-kotlin/src/main/java/com/segment/analytics/substrata/kotlin/Conversions.kt +++ b/substrata-kotlin/src/main/java/com/segment/analytics/substrata/kotlin/Conversions.kt @@ -70,6 +70,8 @@ fun Any?.toJSValue(context: JSContext): JSConvertible = when(this) { interface JSConverter { /** + * NOTE: converter has to be used in a JSSCope + * * convert an object to target type. * the object can be any of the following types: * * int @@ -81,6 +83,8 @@ interface JSConverter { fun read(obj: Any?) : T /** + * NOTE: converter has to be used in a JSSCope + * * convert content to a JS compatible object. the object could be: * * int * * boolean diff --git a/substrata-kotlin/src/main/java/com/segment/analytics/substrata/kotlin/JSContext.kt b/substrata-kotlin/src/main/java/com/segment/analytics/substrata/kotlin/JSContext.kt index 20e6971..1d01c22 100644 --- a/substrata-kotlin/src/main/java/com/segment/analytics/substrata/kotlin/JSContext.kt +++ b/substrata-kotlin/src/main/java/com/segment/analytics/substrata/kotlin/JSContext.kt @@ -1,5 +1,6 @@ package com.segment.analytics.substrata.kotlin +import java.util.concurrent.ConcurrentHashMap import kotlin.reflect.KClass class JSContext( @@ -9,6 +10,8 @@ class JSContext( private val memoryManager = MemoryManager(this) + internal val globalReferences = ConcurrentHashMap>() + val JSNull = getNull() val JSUndefined = getUndefined() @@ -107,11 +110,11 @@ class JSContext( } } - fun getProperties(jsValue: JSConvertible): MutableMap { + fun getProperties(jsValue: JSConvertible): MutableMap { val names = QuickJS.getOwnPropertyNames(this.contextRef, jsValue.ref) - val result = mutableMapOf() + val result = mutableMapOf() for (name in names) { - val value: Any = getProperty(jsValue, name) + val value: Any? = getProperty(jsValue, name) result[name] = value } return result @@ -181,6 +184,8 @@ class JSContext( } override fun release() { + // first clear all the reference from the global table, so memory manager can release them + globalReferences.clear() memoryManager.release() QuickJS.freeContext(contextRef) } @@ -297,7 +302,16 @@ class JSContext( internal fun registerFunction(valueRef: Long, functionName: String, body: Function): JSFunction { val functionId = registry.nextFunctionId - registry.functions[functionId] = body + registry.functions[functionId] = mutableListOf(body) + val ret = QuickJS.newFunction(this, contextRef, valueRef, functionName, functionId) + return get(ret) + } + + fun registerFunction(jsValue: JSConvertible, functionName: String, overloads: MutableList>) = registerFunction(jsValue.ref, functionName, overloads) + + fun registerFunction(valueRef: Long, functionName: String, overloads: MutableList>): JSFunction { + val functionId = registry.nextFunctionId + registry.functions[functionId] = overloads val ret = QuickJS.newFunction(this, contextRef, valueRef, functionName, functionId) return get(ret) } @@ -319,12 +333,14 @@ class JSContext( val getter: JSInstanceFunctionBody = { instance, params -> property.getter(instance) } - registry.functions[getterId] = getter + registry.functions[getterId] = mutableListOf(getter) + val setterId = registry.nextFunctionId val setter: JSInstanceFunctionBody = { instance, params -> property.setter(instance, params[0]) } - registry.functions[setterId] = setter + registry.functions[setterId] = mutableListOf(setter) + QuickJS.newProperty(this, contextRef, valueRef, propertyName, getterId, setterId) } diff --git a/substrata-kotlin/src/main/java/com/segment/analytics/substrata/kotlin/JSRegistry.kt b/substrata-kotlin/src/main/java/com/segment/analytics/substrata/kotlin/JSRegistry.kt index 1a13489..2579619 100644 --- a/substrata-kotlin/src/main/java/com/segment/analytics/substrata/kotlin/JSRegistry.kt +++ b/substrata-kotlin/src/main/java/com/segment/analytics/substrata/kotlin/JSRegistry.kt @@ -2,7 +2,6 @@ package com.segment.analytics.substrata.kotlin import java.util.concurrent.ConcurrentHashMap import java.util.concurrent.atomic.AtomicInteger -import kotlin.jvm.functions.FunctionN class JSRegistry (val context: JSContext) { private var _nextFunctionId = AtomicInteger(0) @@ -22,7 +21,10 @@ class JSRegistry (val context: JSContext) { return _nextPropertyId.getAndIncrement() } - var functions = ConcurrentHashMap>() + /** + * bind the overload functions to the same id, so we don't have to handle it in jni + * */ + var functions = ConcurrentHashMap>>() private set var classes = ConcurrentHashMap() private set @@ -30,27 +32,32 @@ class JSRegistry (val context: JSContext) { private set fun jsCallback(instance: Any?, functionId: Int, args: LongArray): Long { - functions[functionId]?.let { f -> - val params = args.map { return@map context.get(it) } + val params = args.map { return@map context.get(it) } + var exception: Exception? = null + functions[functionId]?.forEach { f -> try { if (f is Function1<*, *>) { val f1 = f as Function1, Any?> f1(params).let { if (it !is Unit) return it.toJSValue(context).ref + else return context.JSUndefined.ref } - } - else if (f is Function2<*, *, *>) { + } else if (f is Function2<*, *, *>) { val f2 = f as Function2, Any?> f2(instance, params).let { if (it !is Unit) return it.toJSValue(context).ref + else return context.JSUndefined.ref } } } - catch (e: Exception) { - return context.JSUndefined.ref + catch (e : JSCallbackInvalidParametersException) { + exception = e } } + // if a matching function is found and executed with no errors, the value is already returned. + // if this line is reached, it means no matching function found + exception?.let { throw it } return context.JSUndefined.ref } @@ -77,8 +84,8 @@ class JSRegistry (val context: JSContext) { fun registerPrototype(jsProtoRef: Long, classId: Int) { classes[classId]?.let { clazz -> val jsProto: JSObject = context.get(jsProtoRef) - for ((function, body) in clazz.getMethods(clazz.createPrototype())) { - jsProto.register(function, body) + for ((function, overloads) in clazz.getMethods(clazz.createPrototype())) { + jsProto.register(function, overloads) } } } diff --git a/substrata-kotlin/src/main/java/com/segment/analytics/substrata/kotlin/MemoryManager.kt b/substrata-kotlin/src/main/java/com/segment/analytics/substrata/kotlin/MemoryManager.kt index b4ad467..a49096d 100644 --- a/substrata-kotlin/src/main/java/com/segment/analytics/substrata/kotlin/MemoryManager.kt +++ b/substrata-kotlin/src/main/java/com/segment/analytics/substrata/kotlin/MemoryManager.kt @@ -1,11 +1,16 @@ package com.segment.analytics.substrata.kotlin -inline fun JSContext.memScope(body: JSContext.() -> T): T { +inline fun JSContext.memScope(global: Boolean = false, body: JSContext.() -> T): T { val scope = MemoryManager(this) try { return body() } finally { - scope.release() + if (global) { + scope.persist() + } + else { + scope.release() + } } } @@ -33,6 +38,8 @@ class MemoryManager( try { for ((ref, list) in references) { + if (context.globalReferences.containsKey(ref)) continue + context.release(ref) for (v in list) { if (v is JSValue) { @@ -50,6 +57,16 @@ class MemoryManager( released = true } + fun persist() { + if (released) return + releasing = true + + context.globalReferences.putAll(references) + + releasing = false + released = true + } + override fun onCreated(reference: JSConvertible) { if (!references.containsKey(reference.ref)) { references.put(reference.ref, mutableSetOf()) diff --git a/substrata-kotlin/src/main/java/com/segment/analytics/substrata/kotlin/SafeEngine.kt b/substrata-kotlin/src/main/java/com/segment/analytics/substrata/kotlin/SafeEngine.kt index b8dea20..c92c81e 100644 --- a/substrata-kotlin/src/main/java/com/segment/analytics/substrata/kotlin/SafeEngine.kt +++ b/substrata-kotlin/src/main/java/com/segment/analytics/substrata/kotlin/SafeEngine.kt @@ -2,14 +2,23 @@ package com.segment.analytics.substrata.kotlin import java.util.concurrent.Callable import java.util.concurrent.Executors +import java.util.concurrent.Future +import java.util.concurrent.FutureTask import java.util.concurrent.TimeUnit class JSScope( val timeoutInSeconds: Long = 120L, var exceptionHandler: JSExceptionHandler? = null ): Releasable { + + companion object { + const val SUBSTRATA_THREAD = "SegmentSubstrataThread" + } + @PublishedApi - internal val executor = Executors.newSingleThreadExecutor() + internal val executor = Executors.newSingleThreadExecutor { + Thread(it, SUBSTRATA_THREAD) + } @PublishedApi internal lateinit var engine : JSEngine @@ -20,29 +29,59 @@ class JSScope( }.get() } - inline fun launch(crossinline closure: JSEngine.() -> Unit) = engine.context.memScope { + /** + * run a task in background. + * + * if `global` set to true, the MemoryManager won't automatically release the JSValues created + * within the task, making the values persist across tasks. values persisted in the global scope + * are released when the engine is released. do not abuse global scope to avoid OOM exception + * + * @param global whether to run the task in the global scope. + * @param closure content of the task + */ + fun launch(global: Boolean = false, closure: JSEngine.() -> Unit) { try { - executor.submit { - engine.closure() - } + optimize(global, closure) } catch (ex: Exception) { exceptionHandler?.invoke(ex) } } - inline fun sync(crossinline closure: JSEngine.() -> Unit) = engine.context.memScope { + /** + * run a task in a blocking way. + * + * if `global` set to true, the MemoryManager won't automatically release the JSValues created + * within the task, making the values persist across tasks. values persisted in the global scope + * are released when the engine is released. do not abuse global scope to avoid OOM exception + * + * @param global whether to run the task in the global scope. + * @param closure content of the task + */ + fun sync(global: Boolean = false, closure: JSEngine.() -> Unit) { try { - executor.submit { - engine.closure() - }.get(timeoutInSeconds, TimeUnit.SECONDS) + optimize(global, closure).get(timeoutInSeconds, TimeUnit.SECONDS) } catch (ex: Exception) { exceptionHandler?.invoke(ex) } } - inline fun await(crossinline closure: JSEngine.() -> T): T? = engine.context.memScope { + /** + * run a task in a blocking way and return the value of the task. + * + * if `global` set to true, the MemoryManager won't automatically release the JSValues created + * within the task, making the values persist across tasks. values persisted in the global scope + * are released when the engine is released. do not abuse global scope to avoid OOM exception. + * + * NOTE: do not return JSValue from the closure/task, since the returned JSValue might be already + * released when exiting the scope. convert JSValue to the type you want and return the converted + * value. + * + * @param global whether to run the task in the global scope. + * @param closure content of the task + */ + fun await(global: Boolean = false, closure: JSEngine.() -> T): T? { return try { - executor.submit(Callable { engine.closure() }).get(timeoutInSeconds, TimeUnit.SECONDS) + optimize(global, closure).get(timeoutInSeconds, TimeUnit.SECONDS) } catch (ex: Exception) { exceptionHandler?.invoke(ex) null @@ -55,6 +94,25 @@ class JSScope( }.get(timeoutInSeconds, TimeUnit.SECONDS) executor.shutdown() } + + private fun optimize(global: Boolean = false, closure: JSEngine.() -> T): Future { + val callable = Callable { + val ret = engine.context.memScope(global) { + engine.closure() + } + ret + } + + // if we are already in the current thread, no need to submit to thread pool task to avoid deadlock + return if (Thread.currentThread().name == SUBSTRATA_THREAD) { + val task = FutureTask(callable) + task.run() + task + } + else { + executor.submit(callable) + } + } } typealias JSExceptionHandler = (Exception) -> Unit diff --git a/substrata-kotlin/src/main/java/com/segment/analytics/substrata/kotlin/Types.kt b/substrata-kotlin/src/main/java/com/segment/analytics/substrata/kotlin/Types.kt index 04dcbee..2981ea0 100644 --- a/substrata-kotlin/src/main/java/com/segment/analytics/substrata/kotlin/Types.kt +++ b/substrata-kotlin/src/main/java/com/segment/analytics/substrata/kotlin/Types.kt @@ -194,7 +194,7 @@ class JSObject( override fun getJSConvertible(key: String): JSConvertible = context.getProperty(this, key) - override fun get(key: String): Any = context.getProperty(this, key) + override fun get(key: String): Any? = context.getProperty(this, key) override fun contains(key: String) = context.hasProperty(this, key) @@ -212,6 +212,10 @@ class JSObject( return context.registerFunction(this, function, body) } + internal fun register(function: String, overloads: MutableList>): JSFunction { + return context.registerFunction(this, function, overloads) + } + fun register(propertyName: String, property: JSProperty) { if (contains(propertyName)) return @@ -281,8 +285,8 @@ open class JSClass( open fun getProperties(obj: Any) = getProperties(clazz, obj) - private fun getMethods(clazz: KClass<*>?, obj: Any?): Map> { - val methods = mutableMapOf>() + private fun getMethods(clazz: KClass<*>?, obj: Any?): Map>> { + val methods = mutableMapOf>>() clazz?.let { for (method in it.memberFunctions) { @@ -290,18 +294,22 @@ open class JSClass( val body: JSInstanceFunctionBody = { instance, params -> val methodParams = method.valueParameters if (methodParams.size != params.size) { - throw Exception("Arguments does not match to Java method ${method.name}") + throw JSCallbackInvalidParametersException("Arguments does not match to Java method ${method.name}") } for (i in methodParams.indices) { if (methodParams[i].type.classifier != params[i]!!::class) { - throw Exception("Wrong argument passed to Java method ${method.name}. Expecting ${methodParams[i]::class.simpleName}, but was ${params[i]!!::class.simpleName}") + throw JSCallbackInvalidParametersException("Wrong argument passed to Java method ${method.name}. Expecting ${methodParams[i]::class.simpleName}, but was ${params[i]!!::class.simpleName}") } } method.call(instance ?: obj, *params.toTypedArray()) } - methods[method.name] = body + + if (methods[method.name] == null) { + methods[method.name] = mutableListOf() + } + methods[method.name]!!.add(body) } } @@ -387,6 +395,8 @@ data class JSProperty( val setter: (instance: Any?, params: Any?) -> Unit ) +class JSCallbackInvalidParametersException(message: String): Exception(message) + interface KeyValueObject { operator fun set(key: String, value: Int)