This repository has been archived on 2023-11-26. You can view files and clone it, but cannot push or open issues or pull requests.
ScriptCraft/scriptcraft/src/main/c/com_thebrokenrail_scriptcra...

504 lines
18 KiB
C

#include <limits.h>
#include <assert.h>
#include <string.h>
#include <stdlib.h>
#include <cutils.h>
#include <quickjs.h>
#include "com_thebrokenrail_scriptcraft_core_quickjs_QuickJSNative.h"
#include "console.h"
static JavaVM *jvm;
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)->AttachCurrentThread(jvm, (void **) &env, NULL);
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);
(*jvm)->DetachCurrentThread(jvm);
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)->ExceptionClear(env);
} else {
JS_ThrowReferenceError(ctx, "could not normalize module name '%s'", name);
}
(*jvm)->DetachCurrentThread(jvm);
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)->AttachCurrentThread(jvm, (void **) &env, NULL);
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);
(*jvm)->DetachCurrentThread(jvm);
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)->ExceptionClear(env);
} else {
JS_ThrowReferenceError(ctx, "could not load module filename '%s'", filename);
}
(*jvm)->DetachCurrentThread(jvm);
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)->AttachCurrentThread(jvm, (void **) &env, NULL);
(*env)->DeleteGlobalRef(env, data->obj);
js_free_rt(rt, data);
(*jvm)->DetachCurrentThread(jvm);
}
}
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 double_clazz = (*env)->FindClass(env, "java/lang/Double");
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, double_clazz)) {
jmethodID double_methodID = (*env)->GetMethodID(env, double_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, "<init>", "(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, "<init>", "(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) {
JSValue global_obj = JS_GetGlobalObject(ctx);
JSValue scriptcraft_obj = JS_GetPropertyStr(ctx, global_obj, "__scriptcraft_bridges__");
const char *name = JS_ToCString(ctx, argv[0]);
JS_SetPropertyStr(ctx, scriptcraft_obj, name, JS_DupValue(ctx, argv[1]));
JS_FreeCString(ctx, name);
JS_FreeValue(ctx, scriptcraft_obj);
JS_FreeValue(ctx, global_obj);
return JS_UNDEFINED;
}
static JSValue js_use_bridge(JSContext *ctx, JSValueConst this_val, int argc, JSValueConst *argv) {
JNIEnv *env;
(*jvm)->AttachCurrentThread(jvm, (void **) &env, NULL);
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) {
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_out = JS_ThrowReferenceError(ctx, "unable to use bridge '%s': %s", js_bridge_name, str);
(*env)->ExceptionClear(env);
} else {
js_out = JS_NULL;
}
}
JS_FreeCString(ctx, js_bridge_name);
(*jvm)->DetachCurrentThread(jvm);
return js_out;
}
JNIEXPORT jobject JNICALL Java_com_thebrokenrail_scriptcraft_core_quickjs_QuickJSNative_bridge(JNIEnv *env, jobject this_val, jstring bridge_name, jobjectArray arr) {
JSContext *ctx = (JSContext *) get_pointer(env, this_val, "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 global_obj = JS_GetGlobalObject(ctx);
JSValue scriptcraft_obj = JS_GetPropertyStr(ctx, global_obj, "__scriptcraft_bridges__");
const char *native_string = (*env)->GetStringUTFChars(env, bridge_name, 0);
JSValue bridge = JS_GetPropertyStr(ctx, scriptcraft_obj, native_string);
(*env)->ReleaseStringUTFChars(env, bridge_name, native_string);
JSValue out;
if (JS_IsFunction(ctx, bridge)) {
out = JS_Call(ctx, bridge, global_obj, length, args);
if (JS_IsException(out)) {
js_std_dump_error(ctx);
out = JS_NULL;
}
} else {
out = JS_NULL;
throw_exception(env, "Invalid Bridge");
}
for (int i = 0; i < length; i++) {
JS_FreeValue(ctx, args[i]);
}
JS_FreeValue(ctx, bridge);
JS_FreeValue(ctx, scriptcraft_obj);
JS_FreeValue(ctx, global_obj);
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) {
jint rc = (*env)->GetJavaVM(env, &jvm);
if (rc != JNI_OK) {
throw_exception(env, "qjs: unable to cache JavaVM");
return;
}
JSRuntime *rt = JS_NewRuntime();
if (!rt) {
throw_exception(env, "qjs: cannot allocate JS runtime");
return;
}
JSContext *ctx = JS_NewContext(rt);
if (!ctx) {
throw_exception(env, "qjs: cannot allocate JS context");
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, "qjs: unable to allocate C module");
return;
}
JS_AddModuleExport(ctx, m, "useBridge");
JS_AddModuleExport(ctx, m, "addBridge");
JSValue global_obj = JS_GetGlobalObject(ctx);
JS_SetPropertyStr(ctx, global_obj, "__scriptcraft_bridges__", JS_NewObject(ctx));
JS_FreeValue(ctx, global_obj);
set_pointer(env, this_val, "rt", rt);
set_pointer(env, this_val, "ctx", ctx);
}
JNIEXPORT void JNICALL Java_com_thebrokenrail_scriptcraft_core_quickjs_QuickJSNative_free(JNIEnv *env, jobject this_val) {
JSContext *ctx = (JSContext *) get_pointer(env, this_val, "ctx");
JSRuntime *rt = (JSRuntime *) get_pointer(env, this_val, "rt");
JS_FreeContext(ctx);
JS_FreeRuntime(rt);
}
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 = (JSContext *) get_pointer(env, this_val, "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), "<init>", JS_EVAL_TYPE_MODULE);
free(init_js);
}
void print_data(char *data, int err) {
JNIEnv *env;
(*jvm)->AttachCurrentThread(jvm, (void **) &env, NULL);
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);
(*jvm)->DetachCurrentThread(jvm);
}