#include #include #include #include #include #include #include "com_thebrokenrail_scriptcraft_core_quickjs_QuickJSNative.h" #include "console.h" typedef struct { JSContext *ctx; JSRuntime *rt; JSValue bridges; } scriptcraft_ctx; static JavaVM *jvm; static const jint JNI_VERSION = JNI_VERSION_1_6; JNIEXPORT jint JNICALL JNI_OnLoad(JavaVM *vm, void *reserved) { jvm = vm; JNIEnv *env = NULL; if (jvm && (*jvm)->GetEnv(jvm, (void **) &env, JNI_VERSION) == JNI_OK) { return JNI_VERSION; } else { return -1; } } static void *get_pointer(JNIEnv *env, jobject obj, const char *name) { jclass clazz = (*env)->FindClass(env, "com/thebrokenrail/scriptcraft/core/quickjs/QuickJSNative"); jfieldID field = (*env)->GetFieldID(env, clazz, name, "J"); return (void *) (long) (*env)->GetLongField(env, obj, field); } static void set_pointer(JNIEnv *env, jobject obj, const char *name, void *value) { jclass clazz = (*env)->FindClass(env, "com/thebrokenrail/scriptcraft/core/quickjs/QuickJSNative"); jfieldID field = (*env)->GetFieldID(env, clazz, name, "J"); (*env)->SetLongField(env, obj, field, (jlong) (long) value); } static char *js_module_normalize_name(JSContext *ctx, const char *base_name, const char *name, void *opaque) { JNIEnv *env; (*jvm)->GetEnv(jvm, (void**) &env, JNI_VERSION); jclass clazz = (*env)->FindClass(env, "com/thebrokenrail/scriptcraft/core/quickjs/QuickJSModules"); jmethodID methodID = (*env)->GetStaticMethodID(env, clazz, "normalizeModule", "(Ljava/lang/String;Ljava/lang/String;)Ljava/lang/String;"); jstring java_name = (*env)->NewStringUTF(env, name); jstring java_base_name = (*env)->NewStringUTF(env, base_name); jstring java_string = (jstring) (*env)->CallStaticObjectMethod(env, clazz, methodID, java_base_name, java_name); (*env)->DeleteLocalRef(env, java_name); (*env)->DeleteLocalRef(env, java_base_name); if (java_string) { const char *native_string = (*env)->GetStringUTFChars(env, java_string, 0); char *new_string = js_strdup(ctx, native_string); (*env)->ReleaseStringUTFChars(env, java_string, native_string); (*env)->DeleteLocalRef(env, java_string); return new_string; } else { jthrowable err = (*env)->ExceptionOccurred(env); if (err) { jmethodID to_string = (*env)->GetMethodID(env, (*env)->FindClass(env, "java/lang/Object"), "toString", "()Ljava/lang/String;"); jstring java_string = (jstring) (*env)->CallObjectMethod(env, err, to_string); const char *str = (*env)->GetStringUTFChars(env, java_string, 0); JS_ThrowReferenceError(ctx, "could not normalize module name '%s': %s", name, str); (*env)->ReleaseStringUTFChars(env, java_string, str); (*env)->ExceptionClear(env); } else { JS_ThrowReferenceError(ctx, "could not normalize module name '%s'", name); } return NULL; } } static int js_module_set_import_meta(JSContext *ctx, JSValueConst func_val, JS_BOOL use_realpath, JS_BOOL is_main) { JSModuleDef *m; char buf[PATH_MAX + 16]; JSValue meta_obj; JSAtom module_name_atom; const char *module_name; assert(JS_VALUE_GET_TAG(func_val) == JS_TAG_MODULE); m = JS_VALUE_GET_PTR(func_val); module_name_atom = JS_GetModuleName(ctx, m); module_name = JS_AtomToCString(ctx, module_name_atom); JS_FreeAtom(ctx, module_name_atom); if (!module_name) { return -1; } if (!strchr(module_name, ':')) { strcpy(buf, "file://"); } pstrcpy(buf, sizeof(buf), module_name); JS_FreeCString(ctx, module_name); meta_obj = JS_GetImportMeta(ctx, m); if (JS_IsException(meta_obj)) { return -1; } JS_DefinePropertyValueStr(ctx, meta_obj, "url", JS_NewString(ctx, buf), JS_PROP_C_W_E); JS_DefinePropertyValueStr(ctx, meta_obj, "main", JS_NewBool(ctx, is_main), JS_PROP_C_W_E); JS_FreeValue(ctx, meta_obj); return 0; } static char *js_load_file(JSContext *ctx, const char *filename) { JNIEnv *env; (*jvm)->GetEnv(jvm, (void**) &env, JNI_VERSION); jclass clazz = (*env)->FindClass(env, "com/thebrokenrail/scriptcraft/core/quickjs/QuickJSModules"); jmethodID methodID = (*env)->GetStaticMethodID(env, clazz, "loadModule", "(Ljava/lang/String;)Ljava/lang/String;"); jstring java_filename = (*env)->NewStringUTF(env, filename); jstring java_string = (jstring) (*env)->CallStaticObjectMethod(env, clazz, methodID, java_filename); (*env)->DeleteLocalRef(env, java_filename); if (java_string) { const char *native_string = (*env)->GetStringUTFChars(env, java_string, 0); char *new_string = js_strdup(ctx, native_string); (*env)->ReleaseStringUTFChars(env, java_string, native_string); (*env)->DeleteLocalRef(env, java_string); return new_string; } else { jthrowable err = (*env)->ExceptionOccurred(env); if (err) { jmethodID to_string = (*env)->GetMethodID(env, (*env)->FindClass(env, "java/lang/Object"), "toString", "()Ljava/lang/String;"); jstring java_string = (jstring) (*env)->CallObjectMethod(env, err, to_string); const char *str = (*env)->GetStringUTFChars(env, java_string, 0); JS_ThrowReferenceError(ctx, "could not load module filename '%s': %s", filename, str); (*env)->ReleaseStringUTFChars(env, java_string, str); (*env)->ExceptionClear(env); } else { JS_ThrowReferenceError(ctx, "could not load module filename '%s'", filename); } return NULL; } } static JSModuleDef *js_module_loader(JSContext *ctx, const char *module_name, void *opaque) { JSModuleDef *m; char *buf; JSValue func_val; buf = js_load_file(ctx, module_name); if (!buf) { return NULL; } int is_json = has_suffix(module_name, ".json"); if (is_json) { asprintf(&buf, "export default JSON.parse(`%s`);", buf); } /* compile the module */ func_val = JS_Eval(ctx, buf, strlen(buf), module_name, JS_EVAL_TYPE_MODULE | JS_EVAL_FLAG_COMPILE_ONLY); js_free(ctx, buf); if (JS_IsException(func_val)) { return NULL; } /* XXX: could propagate the exception */ js_module_set_import_meta(ctx, func_val, 1, 0); /* the module is already referenced, so we must free it */ m = JS_VALUE_GET_PTR(func_val); JS_FreeValue(ctx, func_val); return m; } static void throw_exception(JNIEnv *env, const char *message) { jclass exception_clazz = (*env)->FindClass(env, "com/thebrokenrail/scriptcraft/core/quickjs/JSException"); (*env)->ThrowNew(env, exception_clazz, message); } static JSClassID JS_CLASS_JAVA_OBJECT_ID; typedef struct java_object_data { jobject obj; } java_object_data; static void java_object_finalizer(JSRuntime *rt, JSValue val) { java_object_data *data = JS_GetOpaque(val, JS_CLASS_JAVA_OBJECT_ID); if (data) { JNIEnv *env; (*jvm)->GetEnv(jvm, (void**) &env, JNI_VERSION); (*env)->DeleteGlobalRef(env, data->obj); js_free_rt(rt, data); } } static JSClassDef JS_CLASS_JAVA_OBJECT = { "JavaObject", .finalizer = java_object_finalizer }; static JSValue java_object_to_js_object(JNIEnv *env, JSContext *ctx, jobject obj) { JSValue out; jclass boolean_clazz = (*env)->FindClass(env, "java/lang/Boolean"); jclass number_clazz = (*env)->FindClass(env, "java/lang/Number"); jclass string_clazz = (*env)->FindClass(env, "java/lang/String"); jclass array_clazz = (*env)->FindClass(env, "[Ljava/lang/Object;"); if (!obj) { out = JS_NULL; } else if ((*env)->IsInstanceOf(env, obj, string_clazz)) { const char *native_string = (*env)->GetStringUTFChars(env, (jstring) obj, 0); out = JS_NewString(ctx, native_string); (*env)->ReleaseStringUTFChars(env, (jstring) obj, native_string); } else if ((*env)->IsInstanceOf(env, obj, boolean_clazz)) { jmethodID boolean_methodID = (*env)->GetMethodID(env, boolean_clazz, "booleanValue", "()Z"); jboolean val = (*env)->CallBooleanMethod(env, obj, boolean_methodID); out = JS_NewBool(ctx, val); } else if ((*env)->IsInstanceOf(env, obj, number_clazz)) { jmethodID double_methodID = (*env)->GetMethodID(env, number_clazz, "doubleValue", "()D"); jdouble val = (*env)->CallDoubleMethod(env, obj, double_methodID); out = JS_NewFloat64(ctx, val); } else if ((*env)->IsInstanceOf(env, obj, array_clazz)) { out = JS_NewArray(ctx); int length = (*env)->GetArrayLength(env, (jobjectArray) obj); for (uint32_t i = 0; i < length; i++) { JS_SetPropertyUint32(ctx, out, i, java_object_to_js_object(env, ctx, (*env)->GetObjectArrayElement(env, obj, i))); } } else { java_object_data *data = js_mallocz(ctx, sizeof (java_object_data)); data->obj = (*env)->NewGlobalRef(env, obj); out = JS_NewObjectClass(ctx, JS_CLASS_JAVA_OBJECT_ID); JS_SetOpaque(out, data); } return out; } static jobject js_object_to_java_object(JNIEnv *env, JSContext *ctx, JSValue input) { jobject obj = NULL; if (JS_IsBool(input)) { int val = JS_ToBool(ctx, input); jclass boolean_clazz = (*env)->FindClass(env, "java/lang/Boolean"); jmethodID methodID = (*env)->GetMethodID(env, boolean_clazz, "", "(Z)V"); obj = (*env)->NewObject(env, boolean_clazz, methodID, val); } else if (JS_IsNumber(input)) { double val; JS_ToFloat64(ctx, &val, input); jclass double_clazz = (*env)->FindClass(env, "java/lang/Double"); jmethodID methodID = (*env)->GetMethodID(env, double_clazz, "", "(D)V"); obj = (*env)->NewObject(env, double_clazz, methodID, val); } else if (JS_IsString(input)) { const char *val = JS_ToCString(ctx, input); obj = (*env)->NewStringUTF(env, val); JS_FreeCString(ctx, val); } else if (JS_IsArray(ctx, input)) { uint32_t length; JSValue js_length = JS_GetPropertyStr(ctx, input, "length"); JS_ToUint32(ctx, &length, js_length); JS_FreeValue(ctx, js_length); jclass obj_clazz = (*env)->FindClass(env, "java/lang/Object"); jobjectArray arr = (*env)->NewObjectArray(env, length, obj_clazz, NULL); for (int i = 0; i < length; i++) { JSValue element = JS_GetPropertyUint32(ctx, input, i); jobject obj = js_object_to_java_object(env, ctx, element); (*env)->SetObjectArrayElement(env, arr, i - 1, obj); (*env)->DeleteLocalRef(env, obj); JS_FreeValue(ctx, element); } obj = arr; } else { java_object_data *data = JS_GetOpaque(input, JS_CLASS_JAVA_OBJECT_ID); if (data) { obj = (*env)->NewLocalRef(env, data->obj); } } return obj; } static JSValue js_add_bridge(JSContext *ctx, JSValueConst this_val, int argc, JSValueConst *argv) { scriptcraft_ctx *data = JS_GetContextOpaque(ctx); JSValue scriptcraft_obj = data->bridges; const char *name = JS_ToCString(ctx, argv[0]); JS_SetPropertyStr(ctx, scriptcraft_obj, name, JS_DupValue(ctx, argv[1])); JS_FreeCString(ctx, name); return JS_UNDEFINED; } static JSValue js_use_bridge(JSContext *ctx, JSValueConst this_val, int argc, JSValueConst *argv) { JNIEnv *env; (*jvm)->GetEnv(jvm, (void**) &env, JNI_VERSION); jclass obj_clazz = (*env)->FindClass(env, "java/lang/Object"); jobjectArray arr = (*env)->NewObjectArray(env, argc - 1, obj_clazz, NULL); for (int i = 1; i < argc; i++) { jobject obj = js_object_to_java_object(env, ctx, argv[i]); (*env)->SetObjectArrayElement(env, arr, i - 1, obj); (*env)->DeleteLocalRef(env, obj); } jclass clazz = (*env)->FindClass(env, "com/thebrokenrail/scriptcraft/core/ScriptCraftCore"); jmethodID methodID = (*env)->GetStaticMethodID(env, clazz, "useBridgeNative", "(Ljava/lang/String;[Ljava/lang/Object;)Ljava/lang/Object;"); const char *js_bridge_name = JS_ToCString(ctx, argv[0]); jstring bridge_name = (*env)->NewStringUTF(env, js_bridge_name); jobject out = (*env)->CallStaticObjectMethod(env, clazz, methodID, bridge_name, arr); (*env)->DeleteLocalRef(env, bridge_name); (*env)->DeleteLocalRef(env, arr); JSValue js_out; if (out) { js_out = java_object_to_js_object(env, ctx, out); (*env)->DeleteLocalRef(env, out); } else { jthrowable err = (*env)->ExceptionOccurred(env); if (err) { jclass exception_clazz = (*env)->FindClass(env, "com/thebrokenrail/scriptcraft/core/quickjs/JSException"); jmethodID to_string = (*env)->GetMethodID(env, exception_clazz, "getStackTrace", "(Ljava/lang/Throwable;)Ljava/lang/String;"); jstring java_string = (jstring) (*env)->CallObjectMethod(env, err, to_string); const char *str = (*env)->GetStringUTFChars(env, java_string, 0); js_out = JS_ThrowReferenceError(ctx, "unable to use bridge '%s': %s", js_bridge_name, str); (*env)->ReleaseStringUTFChars(env, java_string, str); (*env)->ExceptionClear(env); } else { js_out = JS_NULL; } } JS_FreeCString(ctx, js_bridge_name); return js_out; } JNIEXPORT jobject JNICALL Java_com_thebrokenrail_scriptcraft_core_quickjs_QuickJSNative_bridge(JNIEnv *env, jobject this_val, jstring bridge_name, jobjectArray arr) { scriptcraft_ctx *data = (scriptcraft_ctx *) get_pointer(env, this_val, "data"); JSContext *ctx = data->ctx; int length = (*env)->GetArrayLength(env, arr); JSValue args[length]; for (int i = 0; i < length; i++) { args[i] = java_object_to_js_object(env, ctx, (*env)->GetObjectArrayElement(env, arr, i)); } JSValue scriptcraft_obj = data->bridges; const char *native_string = (*env)->GetStringUTFChars(env, bridge_name, 0); JSValue bridge = JS_GetPropertyStr(ctx, scriptcraft_obj, native_string); JSValue out; if (JS_IsFunction(ctx, bridge)) { JSValue global_obj = JS_GetGlobalObject(ctx); out = JS_Call(ctx, bridge, global_obj, length, args); JS_FreeValue(ctx, global_obj); if (JS_IsException(out)) { js_std_dump_error(ctx); out = JS_NULL; } } else { out = JS_NULL; char *err; asprintf(&err, "Invalid Bridge: %s", native_string); throw_exception(env, err); free(err); } (*env)->ReleaseStringUTFChars(env, bridge_name, native_string); for (int i = 0; i < length; i++) { JS_FreeValue(ctx, args[i]); } JS_FreeValue(ctx, bridge); jobject java_out = js_object_to_java_object(env, ctx, out); JS_FreeValue(ctx, out); return java_out; } static int scriptcraft_core_init(JSContext *ctx, JSModuleDef *m) { JS_SetModuleExport(ctx, m, "useBridge", JS_NewCFunction(ctx, js_use_bridge, "useBridge", 1)); JS_SetModuleExport(ctx, m, "addBridge", JS_NewCFunction(ctx, js_add_bridge, "addBridge", 2)); return 0; } JNIEXPORT void JNICALL Java_com_thebrokenrail_scriptcraft_core_quickjs_QuickJSNative_init(JNIEnv *env, jobject this_val) { JSRuntime *rt = JS_NewRuntime(); if (!rt) { throw_exception(env, "Cannot Allocate JS Runtime"); return; } JSContext *ctx = JS_NewContext(rt); if (!ctx) { throw_exception(env, "Cannot Allocate JS Context"); JS_FreeRuntime(rt); return; } JS_SetModuleLoaderFunc(rt, js_module_normalize_name, js_module_loader, NULL); js_console_init(ctx); JS_NewClassID(&JS_CLASS_JAVA_OBJECT_ID); JS_NewClass(JS_GetRuntime(ctx), JS_CLASS_JAVA_OBJECT_ID, &JS_CLASS_JAVA_OBJECT); JS_SetClassProto(ctx, JS_CLASS_JAVA_OBJECT_ID, JS_NewObject(ctx)); JSModuleDef *m = JS_NewCModule(ctx, "scriptcraft-core", scriptcraft_core_init); if (!m) { throw_exception(env, "Unable To Allocate C Module"); JS_FreeContext(ctx); JS_FreeRuntime(rt); return; } JS_AddModuleExport(ctx, m, "useBridge"); JS_AddModuleExport(ctx, m, "addBridge"); scriptcraft_ctx *data = malloc(sizeof (scriptcraft_ctx)); data->ctx = ctx; data->rt = rt; data->bridges = JS_NewObject(ctx); set_pointer(env, this_val, "data", data); JS_SetContextOpaque(ctx, data); } JNIEXPORT void JNICALL Java_com_thebrokenrail_scriptcraft_core_quickjs_QuickJSNative_free(JNIEnv *env, jobject this_val) { scriptcraft_ctx *data = (scriptcraft_ctx *) get_pointer(env, this_val, "data"); JS_FreeValue(data->ctx, data->bridges); JS_FreeContext(data->ctx); JS_FreeRuntime(data->rt); free(data); } static int eval_buf(JNIEnv *env, JSContext *ctx, const void *buf, int buf_len, const char *filename, int eval_flags) { JSValue val; int ret; if ((eval_flags & JS_EVAL_TYPE_MASK) == JS_EVAL_TYPE_MODULE) { /* for the modules, we compile then run to be able to set import.meta */ val = JS_Eval(ctx, buf, buf_len, filename, eval_flags | JS_EVAL_FLAG_COMPILE_ONLY); if (!JS_IsException(val)) { js_module_set_import_meta(ctx, val, TRUE, TRUE); val = JS_EvalFunction(ctx, val); } } else { val = JS_Eval(ctx, buf, buf_len, filename, eval_flags); } if (JS_IsException(val)) { js_std_dump_error(ctx); ret = -1; } else { ret = 0; } JS_FreeValue(ctx, val); return ret; } JNIEXPORT void JNICALL Java_com_thebrokenrail_scriptcraft_core_quickjs_QuickJSNative_run(JNIEnv *env, jobject this_val, jstring data) { JSContext *ctx = ((scriptcraft_ctx *) get_pointer(env, this_val, "data"))->ctx; JSValue module_name, json_str; char *init_js; const char *native_string = (*env)->GetStringUTFChars(env, data, 0); module_name = JS_NewString(ctx, native_string); (*env)->ReleaseStringUTFChars(env, data, native_string); json_str = JS_JSONStringify(ctx, module_name, JS_NULL, JS_NULL); JS_FreeValue(ctx, module_name); const char *json_native_str = JS_ToCString(ctx, json_str); JS_FreeValue(ctx, json_str); asprintf(&init_js, "import %s;", json_native_str); JS_FreeCString(ctx, json_native_str); eval_buf(env, ctx, init_js, strlen(init_js), "", JS_EVAL_TYPE_MODULE); free(init_js); } void print_data(char *data, int err) { JNIEnv *env; (*jvm)->GetEnv(jvm, (void**) &env, JNI_VERSION); jclass clazz = (*env)->FindClass(env, "com/thebrokenrail/scriptcraft/core/quickjs/QuickJSNative"); jmethodID print_methodID = (*env)->GetStaticMethodID(env, clazz, "print", "(Ljava/lang/String;Z)V"); jstring str = (*env)->NewStringUTF(env, data); (*env)->CallStaticVoidMethod(env, clazz, print_methodID, str, err); (*env)->DeleteLocalRef(env, str); }