Skip to content

Commit 16db24a

Browse files
authored
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 <wzeng@twilio.com>
1 parent 67d1932 commit 16db24a

File tree

11 files changed

+1026
-605
lines changed

11 files changed

+1026
-605
lines changed

substrata-kotlin/src/androidTest/java/com/segment/analytics/substrata/kotlin/EngineTests.kt

Lines changed: 143 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,10 @@ package com.segment.analytics.substrata.kotlin
22

33
import androidx.test.ext.junit.runners.AndroidJUnit4
44
import junit.framework.Assert.*
5+
import kotlinx.serialization.json.Json
56
import kotlinx.serialization.json.JsonObject
7+
import kotlinx.serialization.json.jsonObject
8+
import kotlinx.serialization.json.jsonPrimitive
69
import org.junit.After
710
import org.junit.Test
811
import org.junit.runner.RunWith
@@ -60,6 +63,12 @@ class EngineTests {
6063
assertEquals("123", bridge.getString("string"))
6164
assertEquals(123, bridge.getInt("int"))
6265
assertEquals(false, bridge.getBoolean("bool"))
66+
67+
val ret = evaluate("""
68+
let v = DataBridge["int"]
69+
v
70+
""".trimIndent())
71+
assertEquals(123, ret)
6372
}
6473
assertNull(exception)
6574
}
@@ -456,6 +465,140 @@ class EngineTests {
456465
assertNull(exception)
457466
}
458467

468+
@Test
469+
fun testCallWithJsonElement() {
470+
val message = "This came from a LivePlugin"
471+
val script = """
472+
class MyTest {
473+
track(event) {
474+
event.context.livePluginMessage = "$message";
475+
const mcvid = DataBridge["mcvid"]
476+
if (mcvid) {
477+
event.context.mcvid = mcvid;
478+
}
479+
return event
480+
}
481+
}
482+
let myTest = new MyTest()
483+
myTest
484+
""".trimIndent()
485+
val json = """
486+
{"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"}
487+
""".trimIndent()
488+
val content = Json.parseToJsonElement(json)
489+
490+
scope.sync {
491+
val ret = evaluate(script)
492+
assert(ret is JSObject)
493+
val res: Any = call(ret as JSObject, "track", JsonElementConverter.write(content, context))
494+
assert(res is JSObject)
495+
val jsonObject = JsonElementConverter.read(res)
496+
assertNotNull(jsonObject)
497+
assertEquals(message, jsonObject.jsonObject["context"]?.jsonObject?.get("livePluginMessage")?.jsonPrimitive?.content)
498+
}
499+
assertNull(exception)
500+
}
501+
502+
@Test
503+
fun testOverloads() {
504+
class MyTest {
505+
fun track() = 0
506+
507+
fun track(str: String) = str
508+
509+
fun track(i: Int, str: String) = "$i and $str"
510+
}
511+
512+
scope.sync {
513+
export("MyTest", MyTest::class)
514+
val ret = evaluate("let myTest = new MyTest(); myTest")
515+
assert(ret is JSObject)
516+
val jsObject = ret as JSObject
517+
assertEquals(0, call(jsObject, "track"))
518+
assertEquals("testtest", call(jsObject, "track", "testtest"))
519+
assertEquals("0 and testtest", call(jsObject, "track", 0, "testtest"))
520+
}
521+
assertNull(exception)
522+
}
523+
524+
@Test
525+
fun testNestedScopes() {
526+
scope.sync {
527+
val l1 = scope.await {
528+
val l2 = scope.await {
529+
scope.sync {
530+
Thread.sleep(500L)
531+
}
532+
1
533+
}
534+
535+
(l2 ?: 0) + 1
536+
}
537+
538+
assertEquals(2, l1)
539+
}
540+
assertNull(exception)
541+
}
542+
543+
@Test
544+
fun testNestedScopesInCallback() {
545+
class MyTest {
546+
var engine: JSScope? = null
547+
548+
fun track() {
549+
engine?.sync {
550+
println("callback")
551+
}
552+
}
553+
}
554+
val myTest = MyTest()
555+
myTest.engine = scope
556+
557+
scope.sync {
558+
export(myTest, "MyTest", "myTest")
559+
call("myTest", "track")
560+
}
561+
assertNull(exception)
562+
}
563+
564+
@Test
565+
fun testGlobalScopeDoesPersist() {
566+
var ret: JSObject? = null
567+
scope.sync {
568+
ret = scope.await(global = true) {
569+
val jsObject = context.newObject()
570+
jsObject["a"] = 1
571+
jsObject
572+
}
573+
}
574+
assertNotNull(ret)
575+
assert(ret is JSObject)
576+
577+
scope.sync {
578+
val a = (ret as JSObject)["a"]
579+
assertEquals(1, a)
580+
}
581+
582+
assertNull(exception)
583+
}
584+
585+
@Test
586+
fun testException() {
587+
class MyJSClass {
588+
fun test(): Int {
589+
throw Exception("something wrong")
590+
}
591+
}
592+
scope.sync {
593+
export( "MyJSClass", MyJSClass::class)
594+
evaluate("""
595+
let o = MyJSClass()
596+
o.test()
597+
""")
598+
}
599+
assertNotNull(exception)
600+
}
601+
459602
@Test
460603
fun testAwait() {
461604
val ret = scope.await {

substrata-kotlin/src/androidTest/java/com/segment/analytics/substrata/kotlin/TypesTests.kt

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
package com.segment.analytics.substrata.kotlin
22

33
import androidx.test.ext.junit.runners.AndroidJUnit4
4+
import kotlinx.serialization.json.Json
45
import kotlinx.serialization.json.add
56
import kotlinx.serialization.json.boolean
67
import kotlinx.serialization.json.buildJsonArray
@@ -190,4 +191,19 @@ class TypesTests {
190191
assertEquals("testtesttest", nestedArr[2].jsonPrimitive.content)
191192
assertEquals(3.3, nestedArr[3].jsonPrimitive.double, 0.01)
192193
}
194+
195+
@Test
196+
fun testJsonElementConverter() {
197+
val json = """
198+
{"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"}
199+
""".trimIndent()
200+
val content = Json.parseToJsonElement(json)
201+
202+
context.memScope {
203+
val jsObject = JsonElementConverter.write(content, this)
204+
assert(jsObject is JSObject)
205+
val jsonObject = JsonElementConverter.read(jsObject)
206+
assertNotNull(jsonObject)
207+
}
208+
}
193209
}
Lines changed: 22 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,29 @@
11
#include <stdio.h>
2+
#include <string>
3+
#include <iostream>
24

35
#include "java_helper.h"
46

5-
#define MAX_MSG_SIZE 1024
67

7-
jint throw_exception(JNIEnv *env, const char *exception_name, const char *message, ...) {
8-
char formatted_message[MAX_MSG_SIZE];
9-
va_list va_args;
10-
va_start(va_args, message);
11-
vsnprintf(formatted_message, MAX_MSG_SIZE, message, va_args);
12-
va_end(va_args);
8+
void swallow_cpp_exception_and_throw_java(JNIEnv * env) {
9+
try {
10+
throw;
11+
} catch(const ThrownJavaException&) {
12+
//already reported to Java, ignore
13+
} catch(const std::bad_alloc& rhs) {
14+
//translate OOM C++ exception to a Java exception
15+
NewJavaException(env, "java/lang/OutOfMemoryError", rhs.what());
16+
} catch(const std::ios_base::failure& rhs) { //sample translation
17+
//translate IO C++ exception to a Java exception
18+
NewJavaException(env, "java/io/IOException", rhs.what());
1319

14-
jclass exception_class = env->FindClass(exception_name);
15-
if (exception_class == NULL) {
16-
return -1;
17-
}
20+
//TRANSLATE ANY OTHER C++ EXCEPTIONS TO JAVA EXCEPTIONS HERE
1821

19-
return env->ThrowNew(exception_class, formatted_message);
20-
}
22+
} catch(const std::exception& e) {
23+
//translate unknown C++ exception to a Java exception
24+
NewJavaException(env, "java/lang/Error", e.what());
25+
} catch(...) {
26+
//translate unknown C++ exception to a Java exception
27+
NewJavaException(env, "java/lang/Error", "Unknown exception type");
28+
}
29+
}
Lines changed: 38 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -1,48 +1,50 @@
11
//
2-
// Created by Wenxi Zeng on 3/1/24.
2+
// See the original post here: https://stackoverflow.com/a/12014833/8296631
33
//
44

55
#ifndef SUBSTRATA_KOTLIN_JAVA_HELPER_H
66
#define SUBSTRATA_KOTLIN_JAVA_HELPER_H
77

88
#include <jni.h>
9+
#include <string>
10+
#include <stdexcept>
911

10-
#define CLASS_NAME_ILLEGAL_STATE_EXCEPTION "java/lang/IllegalStateException"
11-
12-
#define THROW_EXCEPTION(ENV, EXCEPTION_NAME, ...) \
13-
do { \
14-
throw_exception((ENV), (EXCEPTION_NAME), __VA_ARGS__); \
15-
return; \
16-
} while (0)
17-
#define THROW_EXCEPTION_RET(ENV, EXCEPTION_NAME, ...) \
18-
do { \
19-
throw_exception((ENV), (EXCEPTION_NAME), __VA_ARGS__); \
20-
return 0; \
21-
} while (0)
2212
#define MSG_OOM "Out of memory"
23-
#define MSG_NULL_JS_RUNTIME "Null JSRuntime"
24-
#define MSG_NULL_JS_CONTEXT "Null JSContext"
25-
#define MSG_NULL_JS_VALUE "Null JSValue"
26-
#define THROW_ILLEGAL_STATE_EXCEPTION(ENV, ...) \
27-
THROW_EXCEPTION(ENV, CLASS_NAME_ILLEGAL_STATE_EXCEPTION, __VA_ARGS__)
28-
#define THROW_ILLEGAL_STATE_EXCEPTION_RET(ENV, ...) \
29-
THROW_EXCEPTION_RET(ENV, CLASS_NAME_ILLEGAL_STATE_EXCEPTION, __VA_ARGS__)
30-
#define CHECK_NULL(ENV, POINTER, MESSAGE) \
31-
do { \
32-
if ((POINTER) == NULL) { \
33-
THROW_ILLEGAL_STATE_EXCEPTION((ENV), (MESSAGE)); \
34-
} \
35-
} while (0)
36-
#define CHECK_NULL_RET(ENV, POINTER, MESSAGE) \
37-
do { \
38-
if ((POINTER) == NULL) { \
39-
THROW_ILLEGAL_STATE_EXCEPTION_RET((ENV), (MESSAGE)); \
40-
} \
41-
} while (0)
42-
43-
#define MAX_MSG_SIZE 1024
44-
45-
jint throw_exception(JNIEnv *env, const char *exception_name, const char *message, ...);
13+
#define CLASS_NAME_ILLEGAL_STATE_EXCEPTION "java/lang/IllegalStateException"
4614

15+
struct ThrownJavaException : std::exception {
16+
ThrownJavaException(const std::string& message) : m_message(message) {}
17+
ThrownJavaException() : m_message("") {}
18+
19+
// Override the what() method to provide a description of the exception
20+
const char* what() const noexcept override {
21+
return m_message.c_str();
22+
}
23+
24+
private:
25+
std::string m_message;
26+
};
27+
28+
//used to throw a new Java exception. use full paths like:
29+
//"java/lang/NoSuchFieldException"
30+
//"java/lang/NullPointerException"
31+
//"java/security/InvalidParameterException"
32+
struct NewJavaException : public ThrownJavaException{
33+
NewJavaException(JNIEnv * env, const char* type, const char* message="")
34+
:ThrownJavaException(type+std::string(" ")+message)
35+
{
36+
jclass newExcCls = env->FindClass(type);
37+
if (newExcCls != NULL)
38+
env->ThrowNew(newExcCls, message);
39+
//if it is null, a NoClassDefFoundError was already thrown
40+
}
41+
};
42+
43+
inline void assert_no_exception(JNIEnv * env) {
44+
if (env->ExceptionCheck()==JNI_TRUE)
45+
throw ThrownJavaException("assert_no_exception");
46+
}
47+
48+
void swallow_cpp_exception_and_throw_java(JNIEnv * env);
4749

4850
#endif //SUBSTRATA_KOTLIN_JAVA_HELPER_H

0 commit comments

Comments
 (0)