Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

exception handling, overloading, global scope #7

Merged
merged 10 commits into from
Apr 29, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Loading