Skip to content

Commit

Permalink
exception handling, overloading, global scope (#7)
Browse files Browse the repository at this point in the history
* 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 <wzeng@twilio.com>
  • Loading branch information
wenxi-zeng and Wenxi Zeng authored Apr 29, 2024
1 parent 67d1932 commit 16db24a
Show file tree
Hide file tree
Showing 11 changed files with 1,026 additions and 605 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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)
}
Expand Down Expand Up @@ -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 {
Expand Down
Original file line number Diff line number Diff line change
@@ -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
Expand Down Expand Up @@ -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)
}
}
}
35 changes: 22 additions & 13 deletions substrata-kotlin/src/main/cpp/java_helper.cpp
Original file line number Diff line number Diff line change
@@ -1,20 +1,29 @@
#include <stdio.h>
#include <string>
#include <iostream>

#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");
}
}
74 changes: 38 additions & 36 deletions substrata-kotlin/src/main/cpp/java_helper.h
Original file line number Diff line number Diff line change
@@ -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 <jni.h>
#include <string>
#include <stdexcept>

#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
Loading

0 comments on commit 16db24a

Please sign in to comment.