diff --git a/android/build.gradle b/android/build.gradle index 1043ba8..f644c10 100644 --- a/android/build.gradle +++ b/android/build.gradle @@ -42,7 +42,7 @@ android { } externalNativeBuild { cmake { - path "src/main/jni/CMakeLists.txt" + path "src/main/cxx/CMakeLists.txt" version "3.10.2" } } diff --git a/android/src/main/jni/CMakeLists.txt b/android/src/main/cxx/CMakeLists.txt similarity index 92% rename from android/src/main/jni/CMakeLists.txt rename to android/src/main/cxx/CMakeLists.txt index d79bbfb..c02b04f 100644 --- a/android/src/main/jni/CMakeLists.txt +++ b/android/src/main/cxx/CMakeLists.txt @@ -4,13 +4,15 @@ # Sets the minimum version of CMake required to build the native library. cmake_minimum_required(VERSION 3.4.1) -set(JNI_LIB_NAME libjsengine) +set(JNI_LIB_NAME qjs) # Creates and names a library, sets it as either STATIC # or SHARED, and provides the relative paths to its source code. # You can define multiple libraries, and CMake builds them for you. # Gradle automatically packages shared libraries with your APK. +set(CXX_LIB_DIR ${CMAKE_CURRENT_SOURCE_DIR}/../../../../cxx) + add_library( # Sets the name of the library. ${JNI_LIB_NAME} @@ -18,10 +20,10 @@ add_library( # Sets the name of the library. SHARED # Provides a relative path to your source file(s). - native-lib.cpp ) + ${CXX_LIB_DIR}/ffi.cpp ) # quickjs -set(QUICK_JS_LIB_DIR ../../../../cxx/quickjs) +set(QUICK_JS_LIB_DIR ${CXX_LIB_DIR}/quickjs) file (STRINGS "${QUICK_JS_LIB_DIR}/VERSION" QUICKJS_VERSION) set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -DDUMP_LEAKS -DCONFIG_VERSION=\\\"${QUICKJS_VERSION}\\\"") target_sources(${JNI_LIB_NAME} PUBLIC diff --git a/android/src/main/jni/java_js_wrapper.hpp b/android/src/main/jni/java_js_wrapper.hpp deleted file mode 100644 index 0becc97..0000000 --- a/android/src/main/jni/java_js_wrapper.hpp +++ /dev/null @@ -1,214 +0,0 @@ -/* - * @Description: - * @Author: ekibun - * @Date: 2020-08-16 11:08:23 - * @LastEditors: ekibun - * @LastEditTime: 2020-08-25 18:06:08 - */ -#include -#include -#include "jni_helper.hpp" -#include "../../../../cxx/js_engine.hpp" - -namespace qjs -{ - - JSValue javaToJs(JSContext *ctx, JNIEnv *env, jobject val, std::unordered_map cache = std::unordered_map()) - { - if (val == nullptr) - return JS_UNDEFINED; - if (cache.find(val) != cache.end()) - return cache[val]; - jclass objclass = env->GetObjectClass(val); - jclass classclass = env->GetObjectClass((jobject)objclass); - jmethodID mid = env->GetMethodID(classclass, "getName", "()Ljava/lang/String;"); - jobject clsObj = env->CallObjectMethod(objclass, mid); - std::string className(env->GetStringUTFChars((jstring)clsObj, 0)); - if (className.compare("[B") == 0) - { - jsize len = env->GetArrayLength((jbyteArray)val); - return JS_NewArrayBufferCopy(ctx, (uint8_t *)env->GetByteArrayElements((jbyteArray)val, 0), len); - } - else if (className.compare("[I") == 0) - { - jsize len = env->GetArrayLength((jintArray)val); - return JS_NewArrayBufferCopy(ctx, (uint8_t *)env->GetIntArrayElements((jintArray)val, 0), len * 4); - } - else if (className.compare("[J") == 0) - { - jsize len = env->GetArrayLength((jlongArray)val); - return JS_NewArrayBufferCopy(ctx, (uint8_t *)env->GetLongArrayElements((jlongArray)val, 0), len * 8); - } - else if (className.compare("[D") == 0) - { - jsize size = env->GetArrayLength((jdoubleArray)val); - JSValue array = JS_NewArray(ctx); - auto buf = env->GetDoubleArrayElements((jdoubleArray)val, 0); - for (uint32_t i = 0; i < size; i++) - { - auto atom = JS_NewAtomUInt32(ctx, i); - JS_DefinePropertyValue( - ctx, array, atom, - JS_NewFloat64(ctx, buf[i]), - JS_PROP_C_W_E); - JS_FreeAtom(ctx, atom); - } - - return array; - } - else if (className.compare("java.lang.Boolean") == 0) - { - jmethodID getVal = env->GetMethodID(objclass, "booleanValue", "()Z"); - return JS_NewBool(ctx, env->CallBooleanMethod(val, getVal)); - } - else if (className.compare("java.lang.Integer") == 0) - { - jmethodID getVal = env->GetMethodID(objclass, "intValue", "()I"); - return JS_NewInt32(ctx, env->CallIntMethod(val, getVal)); - } - else if (className.compare("java.lang.Long") == 0) - { - jmethodID getVal = env->GetMethodID(objclass, "longValue", "()J"); - return JS_NewInt64(ctx, env->CallLongMethod(val, getVal)); - } - else if (className.compare("java.lang.Double") == 0) - { - jmethodID getVal = env->GetMethodID(objclass, "doubleValue", "()D"); - return JS_NewFloat64(ctx, env->CallDoubleMethod(val, getVal)); - } - else if (className.compare("java.lang.String") == 0) - { - return JS_NewString(ctx, env->GetStringUTFChars((jstring)val, 0)); - } - else if (className.compare("java.util.ArrayList") == 0) - { - jobjectArray list = jniToArray(env, val); - jsize size = env->GetArrayLength(list); - JSValue array = JS_NewArray(ctx); - cache[val] = array; - for (uint32_t i = 0; i < size; i++) - { - auto atom = JS_NewAtomUInt32(ctx, i); - JS_DefinePropertyValue( - ctx, array, atom, - javaToJs(ctx, env, env->GetObjectArrayElement(list, i), cache), - JS_PROP_C_W_E); - JS_FreeAtom(ctx, atom); - } - - return array; - } - else if (className.compare("java.util.HashMap") == 0) - { - // 获取HashMap类entrySet()方法ID - jmethodID entrySetMID = env->GetMethodID(objclass, "entrySet", "()Ljava/util/Set;"); - // 调用entrySet()方法获取Set对象 - jobject setObj = env->CallObjectMethod(val, entrySetMID); - // 获取Set类中iterator()方法ID - jclass setClass = env->FindClass("java/util/Set"); - jmethodID iteratorMID = env->GetMethodID(setClass, "iterator", "()Ljava/util/Iterator;"); - // 调用iterator()方法获取Iterator对象 - jobject iteratorObj = env->CallObjectMethod(setObj, iteratorMID); - // 获取Iterator类中hasNext()方法ID - // 用于while循环判断HashMap中是否还有数据 - jclass iteratorClass = env->FindClass("java/util/Iterator"); - jmethodID hasNextMID = env->GetMethodID(iteratorClass, "hasNext", "()Z"); - // 获取Iterator类中next()方法ID - // 用于读取HashMap中的每一条数据 - jmethodID nextMID = env->GetMethodID(iteratorClass, "next", "()Ljava/lang/Object;"); - // 获取Map.Entry类中getKey()和getValue()的方法ID - // 用于读取“K-V”键值对,注意:内部类使用$符号表示 - jclass entryClass = env->FindClass("java/util/Map$Entry"); - jmethodID getKeyMID = env->GetMethodID(entryClass, "getKey", "()Ljava/lang/Object;"); - jmethodID getValueMID = env->GetMethodID(entryClass, "getValue", "()Ljava/lang/Object;"); - // JSObject - JSValue obj = JS_NewObject(ctx); - cache[val] = obj; - // 循环检测HashMap中是否还有数据 - while (env->CallBooleanMethod(iteratorObj, hasNextMID)) - { - // 读取一条数据 - jobject entryObj = env->CallObjectMethod(iteratorObj, nextMID); - auto atomvalue = javaToJs(ctx, env, env->CallObjectMethod(entryObj, getKeyMID), cache); - auto atom = JS_ValueToAtom(ctx, atomvalue); - JS_DefinePropertyValue( - ctx, obj, atom, - javaToJs(ctx, env, env->CallObjectMethod(entryObj, getValueMID), cache), - JS_PROP_C_W_E); - JS_FreeAtom(ctx, atom); - JS_FreeValue(ctx, atomvalue); - } - return obj; - } - return JS_UNDEFINED; - } - - jobject jsToJava(JNIEnv *env, qjs::Value val, std::unordered_map cache = std::unordered_map()) - { - int tag = JS_VALUE_GET_TAG(val.v); - if (JS_TAG_IS_FLOAT64(tag)) - return jniWrapPrimity(env, (double)val); - switch (tag) - { - case JS_TAG_BOOL: - return jniWrapPrimity(env, (bool)val); - case JS_TAG_INT: - return jniWrapPrimity(env, (int64_t)val); - case JS_TAG_STRING: - return env->NewStringUTF(((std::string)val).c_str()); - case JS_TAG_OBJECT: - { // ArrayBuffer - size_t size; - uint8_t *buf = JS_GetArrayBuffer(val.ctx, &size, val.v); - if (buf) - { - jbyteArray arr = env->NewByteArray(size); - env->SetByteArrayRegion(arr, 0, size, (int8_t *)buf); - return arr; - } - } - if (cache.find(val) != cache.end()) - return cache[val]; - if (JS_IsFunction(val.ctx, val.v)) - { - std::map retMap; - retMap[env->NewStringUTF("__js_function__")] = jniWrapPrimity(env, (int64_t) new JSValue{js_add_ref(val)}); - return jniWrapMap(env, retMap); - } - else if (JS_IsArray(val.ctx, val.v) > 0) - { - uint32_t arrlen = (uint32_t)val["length"]; - jclass class_arraylist = env->FindClass("java/util/ArrayList"); - jmethodID arraylist_init = env->GetMethodID(class_arraylist, "", "()V"); - jobject list = env->NewObject(class_arraylist, arraylist_init); - jmethodID arraylist_add = env->GetMethodID(class_arraylist, "add", "(Ljava/lang/Object;)Z"); - for (uint32_t i = 0; i < arrlen; i++) - { - env->CallBooleanMethod(list, arraylist_add, jsToJava(env, val[i], cache)); - } - cache[val] = list; - return list; - } - else - { - qjs::JSPropertyEnum *ptab; - uint32_t plen; - if (JS_GetOwnPropertyNames(val.ctx, &ptab, &plen, val.v, -1)) - return nullptr; - std::map retMap; - for (uint32_t i = 0; i < plen; i++) - { - retMap[jsToJava(env, {val.ctx, JS_AtomToValue(val.ctx, ptab[i].atom)}, cache)] = - jsToJava(env, {val.ctx, JS_GetProperty(val.ctx, val.v, ptab[i].atom)}, cache); - JS_FreeAtom(val.ctx, ptab[i].atom); - } - js_free(val.ctx, ptab); - jobject ret = jniWrapMap(env, retMap); - cache[val] = ret; - return ret; - } - default: - return nullptr; - } - } -} // namespace qjs diff --git a/android/src/main/jni/jni_helper.hpp b/android/src/main/jni/jni_helper.hpp deleted file mode 100644 index 4bb2444..0000000 --- a/android/src/main/jni/jni_helper.hpp +++ /dev/null @@ -1,62 +0,0 @@ -/* - * @Description: - * @Author: ekibun - * @Date: 2020-08-16 13:20:03 - * @LastEditors: ekibun - * @LastEditTime: 2020-08-16 17:56:52 - */ -#include -#include - -jclass jniInstanceOf(JNIEnv *env, jobject obj, const char *className) -{ - jclass jclazz = env->FindClass(className); - return env->IsInstanceOf(obj, jclazz) ? jclazz : nullptr; -} - -jobjectArray jniToArray(JNIEnv *env, jobject obj) -{ - jmethodID mToArray = env->GetMethodID(env->GetObjectClass(obj), "toArray", "()[Ljava/lang/Object;"); - return (jobjectArray)env->CallObjectMethod(obj, mToArray); -} - -jobject jniWrapMap(JNIEnv *env, std::map val) -{ - jclass class_hashmap = env->FindClass("java/util/HashMap"); - jmethodID hashmap_init = env->GetMethodID(class_hashmap, "", "()V"); - jobject map = env->NewObject(class_hashmap, hashmap_init); - jmethodID hashMap_put = env->GetMethodID(class_hashmap, "put", "(Ljava/lang/Object;Ljava/lang/Object;)Ljava/lang/Object;"); - for (auto it = val.begin(); it != val.end(); ++it) - { - env->CallObjectMethod(map, hashMap_put, it->first, it->second); - } - return map; -} - -template -jobject jniWrapPrimity(JNIEnv *env, T obj); - -template <> -jobject jniWrapPrimity(JNIEnv *env, jlong obj) -{ - jclass jclazz = env->FindClass("java/lang/Long"); - jmethodID jmethod = env->GetMethodID(jclazz, "", "(J)V"); - return env->NewObject(jclazz, jmethod, obj); -} - -template <> -jobject jniWrapPrimity(JNIEnv *env, jboolean obj) -{ - // TODO see https://github.com/flutter/flutter/issues/45066 - std::map retMap; - retMap[env->NewStringUTF("__js_boolean__")] = jniWrapPrimity(env, (int64_t)obj); - return jniWrapMap(env, retMap); -} - -template <> -jobject jniWrapPrimity(JNIEnv *env, jdouble obj) -{ - jclass jclazz = env->FindClass("java/lang/Double"); - jmethodID jmethod = env->GetMethodID(jclazz, "", "(D)V"); - return env->NewObject(jclazz, jmethod, obj); -} \ No newline at end of file diff --git a/android/src/main/jni/native-lib.cpp b/android/src/main/jni/native-lib.cpp deleted file mode 100644 index 42c3be6..0000000 --- a/android/src/main/jni/native-lib.cpp +++ /dev/null @@ -1,184 +0,0 @@ -/* - * @Description: - * @Author: ekibun - * @Date: 2020-08-09 18:16:11 - * @LastEditors: ekibun - * @LastEditTime: 2020-08-25 16:00:46 - */ -#include "java_js_wrapper.hpp" -#include "android/log.h" - -JNIEnv *getEnv(JavaVM *gJvm) -{ - JNIEnv *env; - int status = gJvm->GetEnv((void **)&env, JNI_VERSION_1_6); - if (status < 0) - { - status = gJvm->AttachCurrentThread(&env, NULL); - if (status < 0) - { - return nullptr; - } - } - return env; -} - -void jniResultResolve(JNIEnv *env, jobject result, jobject data) -{ - jclass jclazz = env->GetObjectClass(result); - jmethodID jmethod = env->GetMethodID(jclazz, "success", "(Ljava/lang/Object;)V"); - env->CallVoidMethod(result, jmethod, data); - env->DeleteLocalRef(data); - env->DeleteGlobalRef(result); -} - -void jniResultReject(JNIEnv *env, jobject result, std::string reason) -{ - jclass jclazz = env->GetObjectClass(result); - jmethodID jmethod = env->GetMethodID(jclazz, "error", "(Ljava/lang/String;)V"); - jstring jreason = env->NewStringUTF(reason.c_str()); - env->CallVoidMethod(result, jmethod, jreason); - env->DeleteLocalRef(jreason); - env->DeleteGlobalRef(result); -} - -void jniChannelInvoke(JNIEnv *env, jobject channel, std::promise *promise, std::string method, qjs::Value args, qjs::Engine *engine) -{ - jclass jclazz = env->GetObjectClass(channel); - jmethodID jmethod = env->GetMethodID(jclazz, "invokeMethod", "(Ljava/lang/String;Ljava/lang/Object;J)V"); - jstring jstrmethod = env->NewStringUTF(method.c_str()); - std::map retMap; - retMap[env->NewStringUTF("engine")] = jniWrapPrimity(env, (int64_t) engine); - retMap[env->NewStringUTF("args")] = qjs::jsToJava(env, args); - jobject jsargs = jniWrapMap(env, retMap); - env->CallVoidMethod(channel, jmethod, jstrmethod, jsargs, (jlong)promise); - env->DeleteLocalRef(jstrmethod); - env->DeleteLocalRef(jsargs); -} - -extern "C" JNIEXPORT jlong JNICALL -Java_soko_ekibun_flutter_1qjs_JniBridge_createEngine( - JNIEnv *env, - jobject thiz, - jobject channel) -{ - JavaVM *jvm = nullptr; - env->GetJavaVM(&jvm); - jobject gchannel = env->NewGlobalRef(channel); - qjs::Engine *engine = new qjs::Engine([jvm, gchannel](std::string name, qjs::Value args, qjs::Engine *engine) { - auto promise = new std::promise(); - JNIEnv *env = getEnv(jvm); - jniChannelInvoke(env, gchannel, promise, name, args, engine); - jvm->DetachCurrentThread(); - return promise; - }); - return (jlong)engine; -} - -extern "C" JNIEXPORT void JNICALL -Java_soko_ekibun_flutter_1qjs_JniBridge_evaluate( - JNIEnv *env, - jobject thiz, - jlong engine, - jstring script, - jstring name, - jobject result) -{ - JavaVM *jvm = nullptr; - env->GetJavaVM(&jvm); - jobject gresult = env->NewGlobalRef(result); - ((qjs::Engine *)engine)->commit(qjs::EngineTask{ - [script = std::string(env->GetStringUTFChars(script, 0)), - name = std::string(env->GetStringUTFChars(name, 0))](qjs::Context &ctx) { - return ctx.eval(script, name.c_str(), JS_EVAL_TYPE_GLOBAL); - }, - [jvm, gresult](qjs::Value resolve) { - JNIEnv *env = getEnv(jvm); - jniResultResolve(env, gresult, qjs::jsToJava(env, resolve)); - jvm->DetachCurrentThread(); - }, - [jvm, gresult](qjs::Value reject) { - JNIEnv *env = getEnv(jvm); - jniResultReject(env, gresult, qjs::getStackTrack(reject)); - jvm->DetachCurrentThread(); - }}); -} - -extern "C" JNIEXPORT void JNICALL -Java_soko_ekibun_flutter_1qjs_JniBridge_close( - JNIEnv *env, - jobject thiz, - jlong engine) -{ - delete ((qjs::Engine *)engine); -} - -extern "C" JNIEXPORT void JNICALL -Java_soko_ekibun_flutter_1qjs_JniBridge_call( - JNIEnv *env, - jobject thiz, - jlong engine, - jlong function, - jobject args, - jobject result) -{ - JavaVM *jvm = nullptr; - env->GetJavaVM(&jvm); - jobject gresult = env->NewGlobalRef(result); - jobject gargs = env->NewGlobalRef(args); - ((qjs::Engine *)engine)->commit(qjs::EngineTask{ - [jvm, function = (qjs::JSValue *)function, gargs](qjs::Context &ctx) { - JNIEnv *env = getEnv(jvm); - jobjectArray array = jniToArray(env, gargs); - jsize argscount = env->GetArrayLength(array); - qjs::JSValue *callargs = new qjs::JSValue[argscount]; - for (jsize i = 0; i < argscount; i++) - { - callargs[i] = qjs::javaToJs(ctx.ctx, env, env->GetObjectArrayElement(array, i)); - } - jvm->DetachCurrentThread(); - qjs::JSValue ret = qjs::call_handler(ctx.ctx, *function, (int)argscount, callargs); - delete[] callargs; - if (qjs::JS_IsException(ret)) - throw qjs::exception{}; - return qjs::Value{ctx.ctx, ret}; - }, - [jvm, gresult](qjs::Value resolve) { - JNIEnv *env = getEnv(jvm); - jniResultResolve(env, gresult, qjs::jsToJava(env, resolve)); - jvm->DetachCurrentThread(); - }, - [jvm, gresult](qjs::Value reject) { - JNIEnv *env = getEnv(jvm); - jniResultReject(env, gresult, qjs::getStackTrack(reject)); - jvm->DetachCurrentThread(); - }}); -} - -extern "C" JNIEXPORT void JNICALL -Java_soko_ekibun_flutter_1qjs_JniBridge_resolve( - JNIEnv *env, - jobject clazz, - jlong promise, jobject data) -{ - JavaVM *jvm = nullptr; - env->GetJavaVM(&jvm); - jobject gdata = env->NewGlobalRef(data); - ((std::promise *)promise)->set_value((qjs::JSFutureReturn)[jvm, gdata](qjs::JSContext * ctx) { - JNIEnv *env = getEnv(jvm); - qjs::JSValue *ret = new qjs::JSValue{qjs::javaToJs(ctx, env, gdata)}; - return qjs::JSOSFutureArgv{1, ret}; - }); -} - -extern "C" JNIEXPORT void JNICALL -Java_soko_ekibun_flutter_1qjs_JniBridge_reject( - JNIEnv *env, - jobject clazz, - jlong promise, jstring data) -{ - ((std::promise *)promise)->set_value((qjs::JSFutureReturn)[data = std::string(env->GetStringUTFChars(data, 0))](qjs::JSContext * ctx) { - qjs::JSValue *ret = new qjs::JSValue{JS_NewString(ctx, data.c_str())}; - return qjs::JSOSFutureArgv{-1, ret}; - }); -} \ No newline at end of file diff --git a/android/src/main/kotlin/soko/ekibun/flutter_qjs/FlutterQjsPlugin.kt b/android/src/main/kotlin/soko/ekibun/flutter_qjs/FlutterQjsPlugin.kt index 0eaa2cf..5ba9d67 100644 --- a/android/src/main/kotlin/soko/ekibun/flutter_qjs/FlutterQjsPlugin.kt +++ b/android/src/main/kotlin/soko/ekibun/flutter_qjs/FlutterQjsPlugin.kt @@ -2,55 +2,16 @@ package soko.ekibun.flutter_qjs import android.os.Handler import io.flutter.embedding.engine.plugins.FlutterPlugin -import io.flutter.plugin.common.MethodCall -import io.flutter.plugin.common.MethodChannel -import io.flutter.plugin.common.MethodChannel.MethodCallHandler -import io.flutter.plugin.common.MethodChannel.Result -import io.flutter.plugin.common.PluginRegistry.Registrar /** FlutterQjsPlugin */ -class FlutterQjsPlugin: FlutterPlugin, MethodCallHandler { +class FlutterQjsPlugin: FlutterPlugin { /// The MethodChannel that will the communication between Flutter and native Android /// /// This local reference serves to register the plugin with the Flutter Engine and unregister it /// when the Flutter Engine is detached from the Activity - private lateinit var channelwrapper : MethodChannelWrapper - private lateinit var channel : MethodChannel - private lateinit var applicationContext: android.content.Context - private val handler by lazy { Handler() } - override fun onAttachedToEngine(flutterPluginBinding: FlutterPlugin.FlutterPluginBinding) { - applicationContext = flutterPluginBinding.applicationContext - channel = MethodChannel(flutterPluginBinding.binaryMessenger, "soko.ekibun.flutter_qjs") - channel.setMethodCallHandler(this) - channelwrapper = MethodChannelWrapper(handler, channel) - } - - override fun onMethodCall(call: MethodCall, result: Result) { - if (call.method == "createEngine") { - val engine: Long = JniBridge.instance.createEngine(channelwrapper) - result.success(engine) - } else if (call.method == "evaluate") { - val engine: Long = call.argument("engine")!! - val script: String = call.argument("script")!! - val name: String = call.argument("name")!! - JniBridge.instance.evaluate(engine, script, name, ResultWrapper(handler, result)) - } else if (call.method == "call") { - val engine: Long = call.argument("engine")!! - val function: Long = call.argument("function")!! - val args: List = call.argument>("arguments")!! - JniBridge.instance.call(engine, function, args, ResultWrapper(handler, result)) - } else if (call.method == "close") { - val engine: Long = call.arguments() - JniBridge.instance.close(engine) - result.success(null) - } else { - result.notImplemented() - result.success(null) - } } override fun onDetachedFromEngine(binding: FlutterPlugin.FlutterPluginBinding) { - channel.setMethodCallHandler(null) } } diff --git a/android/src/main/kotlin/soko/ekibun/flutter_qjs/JniBridge.kt b/android/src/main/kotlin/soko/ekibun/flutter_qjs/JniBridge.kt deleted file mode 100644 index f3b8356..0000000 --- a/android/src/main/kotlin/soko/ekibun/flutter_qjs/JniBridge.kt +++ /dev/null @@ -1,24 +0,0 @@ -package soko.ekibun.flutter_qjs - -class JniBridge { - companion object { - // Used to load the 'native-lib' library on application startup. - init { - System.loadLibrary("libjsengine") - } - - val instance by lazy { JniBridge() } - } - - external fun createEngine(channel: MethodChannelWrapper): Long - - external fun evaluate(engine: Long, script: String, name: String, result: ResultWrapper) - - external fun close(engine: Long) - - external fun call(engine: Long, function: Long, args: List, result: ResultWrapper) - - external fun reject(promise: Long, reason: String) - - external fun resolve(promise: Long, data: Any?) -} \ No newline at end of file diff --git a/android/src/main/kotlin/soko/ekibun/flutter_qjs/MethodChannelWrapper.kt b/android/src/main/kotlin/soko/ekibun/flutter_qjs/MethodChannelWrapper.kt deleted file mode 100644 index 01f79b1..0000000 --- a/android/src/main/kotlin/soko/ekibun/flutter_qjs/MethodChannelWrapper.kt +++ /dev/null @@ -1,29 +0,0 @@ -package soko.ekibun.flutter_qjs - -import androidx.annotation.Keep - -import android.os.Handler -import io.flutter.plugin.common.MethodChannel - -@Keep -class MethodChannelWrapper(private val handler: Handler, private val channel: MethodChannel) { - fun invokeMethod(method: String, arguments: Any?, promise: Long) { - handler.post { - channel.invokeMethod(method, arguments, object : MethodChannel.Result { - override fun notImplemented() { - JniBridge.instance.reject(promise, "notImplemented") - } - - override fun error(error_code: String?, error_message: String?, error_data: Any?) { - JniBridge.instance.reject(promise, error_message ?: "undefined") - } - - override fun success(data: Any?) { - JniBridge.instance.resolve(promise, data) - } - - }) - } - - } -} \ No newline at end of file diff --git a/android/src/main/kotlin/soko/ekibun/flutter_qjs/ResultWrapper.kt b/android/src/main/kotlin/soko/ekibun/flutter_qjs/ResultWrapper.kt deleted file mode 100644 index 10ef0b0..0000000 --- a/android/src/main/kotlin/soko/ekibun/flutter_qjs/ResultWrapper.kt +++ /dev/null @@ -1,20 +0,0 @@ -package soko.ekibun.flutter_qjs - -import androidx.annotation.Keep - -import android.os.Handler -import io.flutter.plugin.common.MethodChannel.Result - -@Keep -class ResultWrapper(private val handler: Handler, private val result: Result) { - - @Keep - fun success(dat: Any?) { - handler.post { result.success(dat) } - } - - @Keep - fun error(error_message: String) { - handler.post { result.error("FlutterJSException", error_message, null) } - } -} \ No newline at end of file diff --git a/cxx/ffi.cpp b/cxx/ffi.cpp index 45453f5..5e246e3 100644 --- a/cxx/ffi.cpp +++ b/cxx/ffi.cpp @@ -3,7 +3,7 @@ * @Author: ekibun * @Date: 2020-09-06 18:32:45 * @LastEditors: ekibun - * @LastEditTime: 2020-09-21 01:41:39 + * @LastEditTime: 2020-09-21 14:09:25 */ #include "quickjs/quickjs.h" #include @@ -19,6 +19,11 @@ extern "C" { typedef void *JSChannel(JSContext *ctx, const char *method, void *argv); + DLLEXPORT JSValue *jsThrowInternalError(JSContext *ctx, char *message) + { + return new JSValue{JS_ThrowInternalError(ctx, "%s", message)}; + } + DLLEXPORT JSValue *jsEXCEPTION() { return new JSValue{JS_EXCEPTION}; @@ -42,10 +47,7 @@ extern "C" JSChannel *channel = (JSChannel *)JS_GetRuntimeOpaque(rt); const char *str = (char *)channel(ctx, (char *)0, (void *)module_name); if (str == 0) - { - JS_ThrowReferenceError(ctx, "Module Not Found"); return NULL; - } JSValue func_val = JS_Eval(ctx, str, strlen(str), module_name, JS_EVAL_TYPE_MODULE | JS_EVAL_FLAG_COMPILE_ONLY); if (JS_IsException(func_val)) return NULL; @@ -70,7 +72,7 @@ extern "C" DLLEXPORT JSRuntime *jsNewRuntime(JSChannel channel) { JSRuntime *rt = JS_NewRuntime(); - JS_SetRuntimeOpaque(rt, channel); + JS_SetRuntimeOpaque(rt, (void *)channel); JS_SetModuleLoaderFunc(rt, nullptr, js_module_loader, nullptr); return rt; } @@ -266,7 +268,7 @@ extern "C" DLLEXPORT uint32_t sizeOfJSValue() { - return sizeof JSValue; + return sizeof (JSValue); } DLLEXPORT void setJSValueList(JSValue *list, uint32_t i, JSValue *val) diff --git a/cxx/js_dart_promise.hpp b/cxx/js_dart_promise.hpp deleted file mode 100644 index e911936..0000000 --- a/cxx/js_dart_promise.hpp +++ /dev/null @@ -1,266 +0,0 @@ -/* - * @Description: - * @Author: ekibun - * @Date: 2020-08-07 13:55:52 - * @LastEditors: ekibun - * @LastEditTime: 2020-09-16 00:29:57 - */ -#pragma once -#include "quickjspp.hpp" -#include -#include - -namespace std -{ - template <> - struct hash - { - size_t operator()(const qjs::Value &key) const - { - return (size_t)JS_VALUE_GET_PTR(key.v); - } - }; -} // namespace std - -namespace qjs -{ -#include "quickjs/list.h" - - static JSClassID js_dart_promise_class_id; - - typedef struct - { - int count; - JSValue *argv; - } JSOSFutureArgv; - - using JSFutureReturn = std::function; - using DartChannel = std::function *(std::string, Value)>; - - typedef struct - { - struct list_head link; - std::promise *promise; - std::shared_future future; - JSValue resolve; - JSValue reject; - } JSOSFuture; - - typedef struct - { - struct list_head link; - JSValue ref; - } JSOSRef; - - typedef struct JSThreadState - { - struct list_head os_future; - struct list_head os_ref; - DartChannel channel; - } JSThreadState; - - JSModuleDef *js_module_loader( - JSContext *ctx, - const char *module_name, void *opaque) - { - JSRuntime *rt = JS_GetRuntime(ctx); - JSThreadState *ts = (JSThreadState *)JS_GetRuntimeOpaque(rt); - auto promise = ts->channel("__dart_load_module__", Value{ctx, JS_NewString(ctx, module_name)}); - JSOSFutureArgv argv = promise->get_future().get()(ctx); - if (argv.count > 0) - { - const char *str = JS_ToCString(ctx, argv.argv[0]); - JSValue func_val = JS_Eval(ctx, str, strlen(str), module_name, JS_EVAL_TYPE_MODULE | JS_EVAL_FLAG_COMPILE_ONLY); - JS_FreeCString(ctx, str); - JS_FreeValue(ctx, argv.argv[0]); - if (JS_IsException(func_val)) - return NULL; - /* the module is already referenced, so we must free it */ - JSModuleDef *m = (JSModuleDef *)JS_VALUE_GET_PTR(func_val); - JS_FreeValue(ctx, func_val); - return m; - } - else - { - JS_Throw(ctx, argv.argv[0]); - return NULL; - } - } - - JSValue js_add_ref(Value val) - { - JSRuntime *rt = JS_GetRuntime(val.ctx); - JSThreadState *ts = (JSThreadState *)JS_GetRuntimeOpaque(rt); - JSOSRef *th; - th = (JSOSRef *)js_mallocz(val.ctx, sizeof(*th)); - th->ref = JS_DupValue(val.ctx, val.v); - list_add_tail(&th->link, &ts->os_ref); - return th->ref; - } - - static JSValue js_add_future(Value resolve, Value reject, std::promise *promise) - { - JSRuntime *rt = JS_GetRuntime(resolve.ctx); - JSThreadState *ts = (JSThreadState *)JS_GetRuntimeOpaque(rt); - JSValueConst jsResolve, jsReject; - JSOSFuture *th; - JSValue obj; - - jsResolve = resolve.v; - if (!JS_IsFunction(resolve.ctx, jsResolve)) - return JS_ThrowTypeError(resolve.ctx, "resolve not a function"); - jsReject = reject.v; - if (!JS_IsFunction(reject.ctx, jsReject)) - return JS_ThrowTypeError(reject.ctx, "reject not a function"); - obj = JS_NewObjectClass(resolve.ctx, js_dart_promise_class_id); - if (JS_IsException(obj)) - return obj; - th = (JSOSFuture *)js_mallocz(resolve.ctx, sizeof(*th)); - if (!th) - { - JS_FreeValue(resolve.ctx, obj); - return JS_EXCEPTION; - } - th->promise = promise; - th->future = promise->get_future(); - th->resolve = JS_DupValue(resolve.ctx, jsResolve); - th->reject = JS_DupValue(reject.ctx, jsReject); - list_add_tail(&th->link, &ts->os_future); - JS_SetOpaque(obj, th); - return obj; - } - - JSValue js_dart_future(Value resolve, Value reject, std::string name, Value args) - { - JSRuntime *rt = JS_GetRuntime(resolve.ctx); - JSThreadState *ts = (JSThreadState *)JS_GetRuntimeOpaque(rt); - return js_add_future(resolve, reject, ts->channel(name, args)); - } - - static void unlink_ref(JSRuntime *rt, JSOSRef *th) - { - if (th->link.prev) - { - list_del(&th->link); - th->link.prev = th->link.next = NULL; - } - } - - static void free_ref(JSRuntime *rt, JSOSRef *th) - { - JS_FreeValueRT(rt, th->ref); - js_free_rt(rt, th); - } - - static void unlink_future(JSRuntime *rt, JSOSFuture *th) - { - if (th->link.prev) - { - list_del(&th->link); - th->link.prev = th->link.next = NULL; - } - } - - static void free_future(JSRuntime *rt, JSOSFuture *th) - { - JS_FreeValueRT(rt, th->resolve); - JS_FreeValueRT(rt, th->reject); - js_free_rt(rt, th); - } - - void js_init_handlers(JSRuntime *rt, DartChannel channel) - { - JSThreadState *ts = (JSThreadState *)malloc(sizeof(*ts)); - if (!ts) - { - fprintf(stderr, "Could not allocate memory for the worker"); - exit(1); - } - memset(ts, 0, sizeof(*ts)); - init_list_head(&ts->os_future); - init_list_head(&ts->os_ref); - ts->channel = channel; - - JS_SetRuntimeOpaque(rt, ts); - } - - void js_free_handlers(JSRuntime *rt) - { - JSThreadState *ts = (JSThreadState *)JS_GetRuntimeOpaque(rt); - struct list_head *el, *el1; - - list_for_each_safe(el, el1, &ts->os_future) - { - JSOSFuture *th = list_entry(el, JSOSFuture, link); - th->future.get(); - delete th->promise; - unlink_future(rt, th); - free_future(rt, th); - } - list_for_each_safe(el, el1, &ts->os_ref) - { - JSOSRef *th = list_entry(el, JSOSRef, link); - unlink_ref(rt, th); - free_ref(rt, th); - } - ts->channel = nullptr; - free(ts); - JS_SetRuntimeOpaque(rt, NULL); /* fail safe */ - } - - static JSValue call_handler(JSContext *ctx, JSValueConst func, int count, JSValue *argv) - { - JSValue ret, func1; - /* 'func' might be destroyed when calling itself (if it frees the - handler), so must take extra care */ - func1 = JS_DupValue(ctx, func); - ret = JS_Call(ctx, func1, JS_UNDEFINED, count, argv); - JS_FreeValue(ctx, func1); - for (int i = 0; i < count; ++i) - JS_FreeValue(ctx, argv[i]); - return ret; - } - - static int js_dart_poll(JSContext *ctx) - { - JSRuntime *rt = JS_GetRuntime(ctx); - JSThreadState *ts = (JSThreadState *)JS_GetRuntimeOpaque(rt); - struct list_head *el; - - /* XXX: handle signals if useful */ - - if (list_empty(&ts->os_future)) - return -1; /* no more events */ - - /* XXX: only timers and basic console input are supported */ - if (!list_empty(&ts->os_future)) - { - list_for_each(el, &ts->os_future) - { - JSOSFuture *th = list_entry(el, JSOSFuture, link); - auto status = th->future.wait_for(std::chrono::seconds(0)); - if (status == std::future_status::ready) - { - JSOSFutureArgv argv = th->future.get()(ctx); - JSValue resolve, reject; - /* the timer expired */ - resolve = th->resolve; - th->resolve = JS_UNDEFINED; - reject = th->reject; - th->reject = JS_UNDEFINED; - unlink_future(rt, th); - free_future(rt, th); - JSValue ret = call_handler(ctx, argv.count < 0 ? reject : resolve, abs(argv.count), argv.argv); - if (qjs::JS_IsException(ret)) - throw qjs::exception{}; - JS_FreeValue(ctx, ret); - JS_FreeValue(ctx, resolve); - JS_FreeValue(ctx, reject); - delete argv.argv; - return 0; - } - } - } - return 0; - } -} // namespace qjs \ No newline at end of file diff --git a/cxx/js_engine.hpp b/cxx/js_engine.hpp deleted file mode 100644 index 3535e96..0000000 --- a/cxx/js_engine.hpp +++ /dev/null @@ -1,196 +0,0 @@ -/* - * @Description: - * @Author: ekibun - * @Date: 2020-08-08 10:30:59 - * @LastEditors: ekibun - * @LastEditTime: 2020-08-27 18:55:57 - */ -#pragma once - -#include -#include -#include -#include -#include -#include - -#include "js_dart_promise.hpp" - -namespace qjs -{ - struct EngineTask - { - std::function invoke; - std::function resolve; - std::function reject; - }; - - struct EngineTaskResolver - { - Value result; - std::function resolve; - std::function reject; - }; - - std::string getStackTrack(Value exc) - { - std::string err = (std::string)exc; - if ((bool)exc["stack"]) - err += "\n" + (std::string)exc["stack"]; - return err; - } - - class Engine - { - // 引擎线程 - std::thread thread; - // 任务队列 - std::queue tasks; - // 同步 - std::mutex m_lock; - // 是否关闭提交 - std::atomic stoped; - - void handleException(qjs::Value exc) - { - printf("%s", getStackTrack(exc).c_str()); - std::cout << std::endl; - } - - public: - inline Engine(std::function *(std::string, Value, Engine *)> channel) : stoped{false} - { - thread = std::thread([this, channel = [this, channel](std::string method, Value args){ - return channel(method, args, this); - }] { - // 创建运行环境 - Runtime rt; - js_init_handlers(rt.rt, channel); - Context ctx(rt); - auto &module = ctx.addModule("__DartImpl"); - module.function<&js_dart_future>("__invoke"); - ctx.eval( - R"xxx( - import * as __DartImpl from "__DartImpl"; - globalThis.dart = (method, ...args) => new Promise((res, rej) => - __DartImpl.__invoke(res, rej, method, args)); - )xxx", - "", JS_EVAL_TYPE_MODULE); - JS_SetModuleLoaderFunc(rt.rt, nullptr, js_module_loader, nullptr); - std::vector unresolvedTask; - Value promiseWrapper = ctx.eval( - R"xxx( - (value) => { - const __ret = Promise.resolve(value) - .then(v => { - __ret.__value = v; - __ret.__resolved = true; - }).catch(e => { - __ret.__error = e; - __ret.__rejected = true; - }); - return __ret; - } - )xxx", - "", JS_EVAL_TYPE_GLOBAL); - - // 循环 - while (!this->stoped) - { - // 获取待执行的task - EngineTask task; - { // 获取一个待执行的 task - std::unique_lock lock{this->m_lock}; // unique_lock 相比 lock_guard 的好处是:可以随时 unlock() 和 lock() - if (!this->tasks.empty()) - { - task = this->tasks.front(); // 取一个 task - this->tasks.pop(); - } - } - // 执行task - if (task.resolve) - try - { - Value val = task.invoke(ctx); - Value ret = Value{ctx.ctx, JS_Call(ctx.ctx, promiseWrapper.v, ctx.global().v, 1, &(val.v))}; - unresolvedTask.emplace_back(EngineTaskResolver{ret, std::move(task.resolve), std::move(task.reject)}); - } - catch (exception) - { - task.reject(ctx.getException()); - } - // 执行microtask - JSContext *pctx; - for (;;) - { - int err = JS_ExecutePendingJob(rt.rt, &pctx); - if (err <= 0) - { - if (err < 0) - handleException(ctx.getException()); - break; - } - } - // TODO 检查promise状态 - for (auto it = unresolvedTask.begin(); it != unresolvedTask.end();) - { - bool finished = false; - if (it->result["__resolved"]) - { - it->resolve(it->result["__value"]); - finished = true; - }; - if (it->result["__rejected"]) - { - it->reject(it->result["__error"]); - finished = true; - }; - if (finished) - it = unresolvedTask.erase(it); - else - ++it; - } - // 检查dart交互 - bool idle = true; - try - { - idle = js_dart_poll(ctx.ctx); - } - catch (exception) - { - handleException(ctx.getException()); - } - // 空闲时reject所有task - if (idle && !JS_IsJobPending(rt.rt) && !unresolvedTask.empty()) - { - for (EngineTaskResolver &_task : unresolvedTask) - { - _task.reject(ctx.newValue("Promise cannot resolve")); - } - unresolvedTask.clear(); - } - } - js_free_handlers(rt.rt); - }); - } - inline ~Engine() - { - stoped.store(true); - if (thread.joinable()) - thread.join(); // 等待任务结束, 前提:线程一定会执行完 - } - - public: - // 提交一个任务 - void commit(EngineTask task) - { - if (stoped.load()) // stop == true ?? - throw std::runtime_error("commit on stopped engine."); - { // 添加任务到队列 - std::lock_guard lock{m_lock}; //对当前块的语句加锁 lock_guard 是 mutex 的 stack 封装类,构造的时候 lock(),析构的时候 unlock() - tasks.emplace(task); - } - } - }; - -} // namespace qjs diff --git a/cxx/quickjs b/cxx/quickjs index b994795..56cbb57 160000 --- a/cxx/quickjs +++ b/cxx/quickjs @@ -1 +1 @@ -Subproject commit b9947950913b9dd3edb79f72f8fcc4b61a02a0d4 +Subproject commit 56cbb57a2e94a02a1bc2b2e75b15612c89fe324d diff --git a/cxx/quickjspp.hpp b/cxx/quickjspp.hpp deleted file mode 100644 index 928c0f2..0000000 --- a/cxx/quickjspp.hpp +++ /dev/null @@ -1,1294 +0,0 @@ -#pragma once - -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include - -namespace qjs { - -#include "quickjs/quickjs.h" - -/** Exception type. - * Indicates that exception has occured in JS context. - */ -class exception {}; - -/** Javascript conversion traits. - * Describes how to convert type R to/from JSValue. Second template argument can be used for SFINAE/enable_if type filters. - */ -template -struct js_traits -{ - /** Create an object of C++ type R given JSValue v and JSContext. - * This function is intentionally not implemented. User should implement this function for their own type. - * @param v This value is passed as JSValueConst so it should be freed by the caller. - * @throws exception in case of conversion error - */ - static R unwrap(JSContext * ctx, JSValueConst v); - /** Create JSValue from an object of type R and JSContext. - * This function is intentionally not implemented. User should implement this function for their own type. - * @return Returns JSValue which should be freed by the caller or JS_EXCEPTION in case of error. - */ - static JSValue wrap(JSContext * ctx, R value); -}; - -/** Conversion traits for JSValue (identity). - */ -template <> -struct js_traits -{ - static JSValue unwrap(JSContext * ctx, JSValueConst v) noexcept - { - return JS_DupValue(ctx, v); - } - - static JSValue wrap(JSContext * ctx, JSValue v) noexcept - { - return v; - } -}; - -/** Conversion traits for integers. - */ -template -struct js_traits && sizeof(Int) <= sizeof(int64_t)>> -{ - - /// @throws exception - static Int unwrap(JSContext * ctx, JSValueConst v) - { - if constexpr (sizeof(Int) > sizeof(int32_t)) - { - int64_t r; - if(JS_ToInt64(ctx, &r, v)) - throw exception{}; - return static_cast(r); - } - else - { - int32_t r; - if(JS_ToInt32(ctx, &r, v)) - throw exception{}; - return static_cast(r); - } - } - - static JSValue wrap(JSContext * ctx, Int i) noexcept - { - if constexpr (std::is_same_v || sizeof(Int) > sizeof(int32_t)) - return JS_NewInt64(ctx, static_cast(i)); - else - return JS_NewInt32(ctx, static_cast(i)); - } -}; - -/** Conversion traits for boolean. - */ -template <> -struct js_traits -{ - static bool unwrap(JSContext * ctx, JSValueConst v) noexcept - { - return JS_ToBool(ctx, v); - } - - static JSValue wrap(JSContext * ctx, bool i) noexcept - { - return JS_NewBool(ctx, i); - } -}; - -/** Conversion trait for void. - */ -template <> -struct js_traits -{ - /// @throws exception if jsvalue is neither undefined nor null - static void unwrap(JSContext * ctx, JSValueConst value) - { - if(JS_IsException(value)) - throw exception{}; - } -}; - -/** Conversion traits for float64/double. - */ -template <> -struct js_traits -{ - /// @throws exception - static double unwrap(JSContext * ctx, JSValueConst v) - { - double r; - if(JS_ToFloat64(ctx, &r, v)) - throw exception{}; - return r; - } - - static JSValue wrap(JSContext * ctx, double i) noexcept - { - return JS_NewFloat64(ctx, i); - } -}; - -namespace detail { -/** Fake std::string_view which frees the string on destruction. -*/ -class js_string : public std::string_view -{ - using Base = std::string_view; - JSContext * ctx = nullptr; - - friend struct js_traits; - - js_string(JSContext * ctx, const char * ptr, std::size_t len) : Base(ptr, len), ctx(ctx) - {} - -public: - - template - js_string(Args&& ... args) : Base(std::forward(args)...), ctx(nullptr) - {} - - js_string(const js_string& other) = delete; - - operator const char * () const { - return this->data(); - } - - ~js_string() - { - if(ctx) - JS_FreeCString(ctx, this->data()); - } -}; -} // namespace detail - -/** Conversion traits from std::string_view and to detail::js_string. */ -template <> -struct js_traits -{ - static detail::js_string unwrap(JSContext * ctx, JSValueConst v) - { - size_t plen; - const char * ptr = JS_ToCStringLen(ctx, &plen, v); - if(!ptr) - throw exception{}; - return detail::js_string{ctx, ptr, plen}; - } - - static JSValue wrap(JSContext * ctx, std::string_view str) noexcept - { - return JS_NewStringLen(ctx, str.data(), str.size()); - } -}; - -/** Conversion traits for std::string */ -template <> // slower -struct js_traits -{ - static std::string unwrap(JSContext * ctx, JSValueConst v) - { - auto str_view = js_traits::unwrap(ctx, v); - return std::string{str_view.data(), str_view.size()}; - } - - static JSValue wrap(JSContext * ctx, const std::string& str) noexcept - { - return JS_NewStringLen(ctx, str.data(), str.size()); - } -}; - -/** Conversion from const char * */ -template <> -struct js_traits -{ - static JSValue wrap(JSContext * ctx, const char * str) noexcept - { - return JS_NewString(ctx, str); - } - static detail::js_string unwrap(JSContext * ctx, JSValueConst v) - { - return js_traits::unwrap(ctx, v); - } -}; - - -namespace detail { - -/** Helper function to convert and then free JSValue. */ -template -T unwrap_free(JSContext * ctx, JSValue val) -{ - if constexpr(std::is_same_v) - { - JS_FreeValue(ctx, val); - return js_traits::unwrap(ctx, val); - } else - { - try - { - T result = js_traits>::unwrap(ctx, val); - JS_FreeValue(ctx, val); - return result; - } - catch(...) - { - JS_FreeValue(ctx, val); - throw; - } - } -} - -template -Tuple unwrap_args_impl(JSContext * ctx, JSValueConst * argv, std::index_sequence) -{ - return Tuple{js_traits>>::unwrap(ctx, argv[I])...}; -} - -/** Helper function to convert an array of JSValues to a tuple. - * @tparam Args C++ types of the argv array - */ -template -std::tuple...> unwrap_args(JSContext * ctx, JSValueConst * argv) -{ - return unwrap_args_impl...>>(ctx, argv, std::make_index_sequence()); -} - -/** Helper function to call f with an array of JSValues. - * @tparam R return type of f - * @tparam Args argument types of f - * @tparam Callable type of f (inferred) - * @param ctx JSContext - * @param f callable object - * @param argv array of JSValue's - * @return converted return value of f or JS_NULL if f returns void - */ -template -JSValue wrap_call(JSContext * ctx, Callable&& f, JSValueConst * argv) noexcept -{ - try - { - if constexpr(std::is_same_v) - { - std::apply(std::forward(f), unwrap_args(ctx, argv)); - return JS_NULL; - } else - { - return js_traits>::wrap(ctx, - std::apply(std::forward(f), - unwrap_args(ctx, argv))); - } - } - catch(exception) - { - return JS_EXCEPTION; - } -} - -/** Same as wrap_call, but pass this_value as first argument. - * @tparam FirstArg type of this_value - */ -template -JSValue wrap_this_call(JSContext * ctx, Callable&& f, JSValueConst this_value, JSValueConst * argv) noexcept -{ - try - { - if constexpr(std::is_same_v) - { - std::apply(std::forward(f), std::tuple_cat(unwrap_args(ctx, &this_value), - unwrap_args(ctx, argv))); - return JS_NULL; - } else - { - return js_traits>::wrap(ctx, - std::apply(std::forward(f), - std::tuple_cat( - unwrap_args(ctx, &this_value), - unwrap_args(ctx, argv)))); - } - } - catch(exception) - { - return JS_EXCEPTION; - } -} - -template -void wrap_args_impl(JSContext * ctx, JSValue * argv, Tuple tuple, std::index_sequence) -{ - ((argv[I] = js_traits>>::wrap(ctx, std::get(tuple))), ...); -} - -/** Converts C++ args to JSValue array. - * @tparam Args argument types - * @param argv array of size at least sizeof...(Args) - */ -template -void wrap_args(JSContext * ctx, JSValue * argv, Args&& ... args) -{ - wrap_args_impl(ctx, argv, std::make_tuple(std::forward(args)...), - std::make_index_sequence()); -} -} // namespace detail - -/** A wrapper type for free and class member functions. - * Pointer to function F is a template argument. - * @tparam F either a pointer to free function or a pointer to class member function - * @tparam PassThis if true and F is a pointer to free function, passes Javascript "this" value as first argument: - */ -template -struct fwrapper -{ - /// "name" property of the JS function object (not defined if nullptr) - const char * name = nullptr; -}; - -/** Conversion to JSValue for free function in fwrapper. */ -template -struct js_traits> -{ - static JSValue wrap(JSContext * ctx, fwrapper fw) noexcept - { - return JS_NewCFunction(ctx, [](JSContext * ctx, JSValueConst this_value, int argc, - JSValueConst * argv) noexcept -> JSValue { - if constexpr(PassThis) - return detail::wrap_this_call(ctx, F, this_value, argv); - else - return detail::wrap_call(ctx, F, argv); - }, fw.name, sizeof...(Args)); - - } -}; - -/** Conversion to JSValue for class member function in fwrapper. */ -template -struct js_traits> -{ - static JSValue wrap(JSContext * ctx, fwrapper fw) noexcept - { - return JS_NewCFunction(ctx, [](JSContext * ctx, JSValueConst this_value, int argc, - JSValueConst * argv) noexcept -> JSValue { - return detail::wrap_this_call, Args...>(ctx, F, this_value, argv); - }, fw.name, sizeof...(Args)); - - } -}; - -/** Conversion to JSValue for const class member function in fwrapper. */ -template -struct js_traits> -{ - static JSValue wrap(JSContext * ctx, fwrapper fw) noexcept - { - return JS_NewCFunction(ctx, [](JSContext * ctx, JSValueConst this_value, int argc, - JSValueConst * argv) noexcept -> JSValue { - return detail::wrap_this_call, Args...>(ctx, F, this_value, argv); - }, fw.name, sizeof...(Args)); - - } -}; - -/** A wrapper type for constructor of type T with arguments Args. - * Compilation fails if no such constructor is defined. - * @tparam Args constructor arguments - */ -template -struct ctor_wrapper -{ - static_assert(std::is_constructible::value, "no such constructor!"); - /// "name" property of JS constructor object - const char * name = nullptr; -}; - -/** Conversion to JSValue for ctor_wrapper. */ -template -struct js_traits> -{ - static JSValue wrap(JSContext * ctx, ctor_wrapper cw) noexcept - { - return JS_NewCFunction2(ctx, [](JSContext * ctx, JSValueConst this_value, int argc, - JSValueConst * argv) noexcept -> JSValue { - - if(js_traits>::QJSClassId == 0) // not registered - { -#if defined(__cpp_rtti) - // automatically register class on first use (no prototype) - js_traits>::register_class(ctx, typeid(T).name()); -#else - JS_ThrowTypeError(ctx, "quickjspp ctor_wrapper::wrap: Class is not registered"); - return JS_EXCEPTION; -#endif - } - - auto proto = JS_GetPropertyStr(ctx, this_value, "prototype"); - if (JS_IsException(proto)) - return proto; - auto jsobj = JS_NewObjectProtoClass(ctx, proto, js_traits>::QJSClassId); - JS_FreeValue(ctx, proto); - if (JS_IsException(jsobj)) - return jsobj; - - std::shared_ptr ptr = std::apply(std::make_shared, detail::unwrap_args(ctx, argv)); - JS_SetOpaque(jsobj, new std::shared_ptr(std::move(ptr))); - return jsobj; - - // return detail::wrap_call, Args...>(ctx, std::make_shared, argv); - }, cw.name, sizeof...(Args), JS_CFUNC_constructor, 0); - } -}; - - -/** Conversions for std::shared_ptr. - * T should be registered to a context before conversions. - * @tparam T class type - */ -template -struct js_traits> -{ - /// Registered class id in QuickJS. - inline static JSClassID QJSClassId = 0; - - /** Register class in QuickJS context. - * - * @param ctx context - * @param name class name - * @param proto class prototype or JS_NULL - * @throws exception - */ - static void register_class(JSContext * ctx, const char * name, JSValue proto = JS_NULL) - { - if(QJSClassId == 0) - { - JS_NewClassID(&QJSClassId); - } - auto rt = JS_GetRuntime(ctx); - if(!JS_IsRegisteredClass(rt, QJSClassId)) - { - JSClassDef def{ - name, - // destructor - [](JSRuntime * rt, JSValue obj) noexcept { - auto pptr = reinterpret_cast *>(JS_GetOpaque(obj, QJSClassId)); - delete pptr; - } - }; - int e = JS_NewClass(rt, QJSClassId, &def); - if(e < 0) - { - JS_ThrowInternalError(ctx, "Cant register class %s", name); - throw exception{}; - } - } - JS_SetClassProto(ctx, QJSClassId, proto); - } - - /** Create a JSValue from std::shared_ptr. - * Creates an object with class if #QJSClassId and sets its opaque pointer to a new copy of #ptr. - */ - static JSValue wrap(JSContext * ctx, std::shared_ptr ptr) - { - if(QJSClassId == 0) // not registered - { -#if defined(__cpp_rtti) - // automatically register class on first use (no prototype) - register_class(ctx, typeid(T).name()); -#else - JS_ThrowTypeError(ctx, "quickjspp std::shared_ptr::wrap: Class is not registered"); - return JS_EXCEPTION; -#endif - } - auto jsobj = JS_NewObjectClass(ctx, QJSClassId); - if(JS_IsException(jsobj)) - return jsobj; - - auto pptr = new std::shared_ptr(std::move(ptr)); - JS_SetOpaque(jsobj, pptr); - return jsobj; - } - - /// @throws exception if #v doesn't have the correct class id - static const std::shared_ptr& unwrap(JSContext * ctx, JSValueConst v) - { - auto ptr = reinterpret_cast *>(JS_GetOpaque2(ctx, v, QJSClassId)); - if(!ptr) - throw exception{}; - return *ptr; - } -}; - -/** Conversions for non-owning pointers to class T. - * @tparam T class type - */ -template -struct js_traits>> -{ - static JSValue wrap(JSContext * ctx, T * ptr) - { - if(js_traits>::QJSClassId == 0) // not registered - { -#if defined(__cpp_rtti) - js_traits>::register_class(ctx, typeid(T).name()); -#else - JS_ThrowTypeError(ctx, "quickjspp js_traits::wrap: Class is not registered"); - return JS_EXCEPTION; -#endif - } - auto jsobj = JS_NewObjectClass(ctx, js_traits>::QJSClassId); - if(JS_IsException(jsobj)) - return jsobj; - - // shared_ptr with empty deleter since we don't own T* - auto pptr = new std::shared_ptr(ptr, [](T *){}); - JS_SetOpaque(jsobj, pptr); - return jsobj; - } - - static T * unwrap(JSContext * ctx, JSValueConst v) - { - auto ptr = reinterpret_cast *>(JS_GetOpaque2(ctx, v, - js_traits>::QJSClassId)); - if(!ptr) - throw exception{}; - return ptr->get(); - } -}; - -namespace detail { -/** A faster std::function-like object with type erasure. - * Used to convert any callable objects (including lambdas) to JSValue. - */ -struct function -{ - JSValue - (* invoker)(function * self, JSContext * ctx, JSValueConst this_value, int argc, JSValueConst * argv) = nullptr; - - void (* destroyer)(function * self) = nullptr; - - alignas(std::max_align_t) char functor[1]; - - template - static function * create(JSRuntime * rt, Functor&& f) - { - auto fptr = reinterpret_cast(js_malloc_rt(rt, sizeof(function) + sizeof(Functor))); - if(!fptr) - throw std::bad_alloc{}; - new(fptr) function; - auto functorptr = reinterpret_cast(fptr->functor); - new(functorptr) Functor(std::forward(f)); - fptr->destroyer = nullptr; - if constexpr(!std::is_trivially_destructible_v) - { - fptr->destroyer = [](function * fptr) { - auto functorptr = reinterpret_cast(fptr->functor); - functorptr->~Functor(); - }; - } - return fptr; - } -}; - -static_assert(std::is_trivially_destructible_v); -} - -template <> -struct js_traits -{ - inline static JSClassID QJSClassId = 0; - - // TODO: replace ctx with rt - static void register_class(JSContext * ctx, const char * name) - { - if(QJSClassId == 0) - { - JS_NewClassID(&QJSClassId); - } - auto rt = JS_GetRuntime(ctx); - if(JS_IsRegisteredClass(rt, QJSClassId)) - return; - JSClassDef def{ - name, - // destructor - [](JSRuntime * rt, JSValue obj) noexcept { - auto fptr = reinterpret_cast(JS_GetOpaque(obj, QJSClassId)); - assert(fptr); - if(fptr->destroyer) - fptr->destroyer(fptr); - js_free_rt(rt, fptr); - }, - nullptr, // mark - // call - [](JSContext * ctx, JSValueConst func_obj, JSValueConst this_val, int argc, - JSValueConst * argv, int flags) -> JSValue { - auto ptr = reinterpret_cast(JS_GetOpaque2(ctx, func_obj, QJSClassId)); - if(!ptr) - return JS_EXCEPTION; - return ptr->invoker(ptr, ctx, this_val, argc, argv); - } - }; - int e = JS_NewClass(rt, QJSClassId, &def); - if(e < 0) - throw std::runtime_error{"Cannot register C++ function class"}; - } -}; - - -/** Traits for accessing object properties. - * @tparam Key property key type (uint32 and strings are supported) - */ -template -struct js_property_traits -{ - static void set_property(JSContext * ctx, JSValue this_obj, Key key, JSValue value); - static JSValue get_property(JSContext * ctx, JSValue this_obj, Key key); -}; - -template <> -struct js_property_traits -{ - static void set_property(JSContext * ctx, JSValue this_obj, const char * name, JSValue value) - { - int err = JS_SetPropertyStr(ctx, this_obj, name, value); - if(err < 0) - throw exception{}; - } - - static JSValue get_property(JSContext * ctx, JSValue this_obj, const char * name) noexcept - { - return JS_GetPropertyStr(ctx, this_obj, name); - } -}; - -template <> -struct js_property_traits -{ - static void set_property(JSContext * ctx, JSValue this_obj, uint32_t idx, JSValue value) - { - int err = JS_SetPropertyUint32(ctx, this_obj, idx, value); - if(err < 0) - throw exception{}; - } - - static JSValue get_property(JSContext * ctx, JSValue this_obj, uint32_t idx) noexcept - { - return JS_GetPropertyUint32(ctx, this_obj, idx); - } -}; - -class Value; - -namespace detail { -template -struct property_proxy -{ - JSContext * ctx; - JSValue this_obj; - Key key; - - /** Conversion helper function */ - template - T as() const - { - return unwrap_free(ctx, js_property_traits::get_property(ctx, this_obj, key)); - } - - /** Explicit conversion operator (to any type) */ - template - explicit operator T() const { return as(); } - - /** Implicit converion to qjs::Value */ - operator Value() const; // defined later due to Value being incomplete type - - template - property_proxy& operator =(Value value) - { - js_property_traits::set_property(ctx, this_obj, key, - js_traits::wrap(ctx, std::move(value))); - return *this; - } -}; - - -// class member variable getter/setter -template -struct get_set {}; - -template -struct get_set -{ - using is_const = std::is_const; - - static const R& get(const std::shared_ptr& ptr) - { - return *ptr.*M; - } - - static R& set(const std::shared_ptr& ptr, R value) - { - return *ptr.*M = std::move(value); - } - -}; - -} // namespace detail - -/** JSValue with RAAI semantics. - * A wrapper over (JSValue v, JSContext * ctx). - * Calls JS_FreeValue(ctx, v) on destruction. Can be copied and moved. - * A JSValue can be released by either JSValue x = std::move(value); or JSValue x = value.release(), then the Value becomes invalid and FreeValue won't be called - * Can be converted to C++ type, for example: auto string = value.as(); qjs::exception would be thrown on error - * Properties can be accessed (read/write): value["property1"] = 1; value[2] = "2"; - */ -class Value -{ -public: - JSValue v; - JSContext * ctx = nullptr; - -public: - /** Use context.newValue(val) instead */ - template - Value(JSContext * ctx, T&& val) : ctx(ctx) - { - v = js_traits>::wrap(ctx, std::forward(val)); - if(JS_IsException(v)) - throw exception{}; - } - - Value(const Value& rhs) - { - ctx = rhs.ctx; - v = JS_DupValue(ctx, rhs.v); - } - - Value(Value&& rhs) - { - std::swap(ctx, rhs.ctx); - v = rhs.v; - } - - Value& operator=(Value rhs) - { - std::swap(ctx, rhs.ctx); - std::swap(v, rhs.v); - return *this; - } - - bool operator==(JSValueConst other) const - { - return JS_VALUE_GET_TAG(v) == JS_VALUE_GET_TAG(other) && JS_VALUE_GET_PTR(v) == JS_VALUE_GET_PTR(other); - } - - bool operator!=(JSValueConst other) const { return !((*this) == other); } - - - /** Returns true if 2 values are the same (equality for arithmetic types or point to the same object) */ - bool operator==(const Value& rhs) const - { - return ctx == rhs.ctx && (*this == rhs.v); - } - - bool operator!=(const Value& rhs) const { return !((*this) == rhs); } - - - ~Value() - { - if(ctx) JS_FreeValue(ctx, v); - } - - bool isError() const { return JS_IsError(ctx, v); } - - /** Conversion helper function: value.as() - * @tparam T type to convert to - * @return type returned by js_traits>::unwrap that should be implicitly convertible to T - * */ - template - auto as() const { return js_traits>::unwrap(ctx, v); } - - /** Explicit conversion: static_cast(value) or (T)value */ - template - explicit operator T() const { return as(); } - - JSValue release() // dont call freevalue - { - ctx = nullptr; - return v; - } - - /** Implicit conversion to JSValue (rvalue only). Example: JSValue v = std::move(value); */ - operator JSValue() && { return release(); } - - - /** Access JS properties. Returns proxy type which is implicitly convertible to qjs::Value */ - template - detail::property_proxy operator [](Key key) - { - return {ctx, v, std::move(key)}; - } - - - // add("f", []() {...}); - template - Value& add(const char * name, Function&& f) - { - (*this)[name] = js_traits(f)})>::wrap(ctx, - std::forward(f)); - return *this; - } - - // add<&f>("f"); - // add<&T::f>("f"); - template - std::enable_if_t, Value&> - add(const char * name) - { - (*this)[name] = fwrapper{name}; - return *this; - } - - // add<&T::member>("member"); - template - std::enable_if_t, Value&> - add(const char * name) - { - auto prop = JS_NewAtom(ctx, name); - using fgetter = fwrapper::get, true>; - int ret; - if constexpr (detail::get_set::is_const::value) - { - ret = JS_DefinePropertyGetSet(ctx, v, prop, - js_traits::wrap(ctx, fgetter{name}), - JS_UNDEFINED, - JS_PROP_CONFIGURABLE | JS_PROP_ENUMERABLE - ); - } else - { - using fsetter = fwrapper::set, true>; - ret = JS_DefinePropertyGetSet(ctx, v, prop, - js_traits::wrap(ctx, fgetter{name}), - js_traits::wrap(ctx, fsetter{name}), - JS_PROP_CONFIGURABLE | JS_PROP_WRITABLE | JS_PROP_ENUMERABLE - ); - } - JS_FreeAtom(ctx, prop); - if(ret < 0) - throw exception{}; - return *this; - } - - std::string toJSON(const Value& replacer = Value{nullptr, JS_UNDEFINED}, const Value& space = Value{nullptr, JS_UNDEFINED}) - { - assert(ctx); - assert(!replacer.ctx || ctx == replacer.ctx); - assert(!space.ctx || ctx == space.ctx); - JSValue json = JS_JSONStringify(ctx, v, replacer.v, space.v); - return (std::string)Value{ctx, json}; - } - -}; - -/** Thin wrapper over JSRuntime * rt - * Calls JS_FreeRuntime on destruction. noncopyable. - */ -class Runtime -{ -public: - JSRuntime * rt; - - Runtime() - { - rt = JS_NewRuntime(); - if(!rt) - throw std::runtime_error{"qjs: Cannot create runtime"}; - } - - // noncopyable - Runtime(const Runtime&) = delete; - - ~Runtime() - { - JS_FreeRuntime(rt); - } -}; - -/** Wrapper over JSContext * ctx - * Calls JS_SetContextOpaque(ctx, this); on construction and JS_FreeContext on destruction - */ -class Context -{ -public: - JSContext * ctx; - - /** Module wrapper - * Workaround for lack of opaque pointer for module load function by keeping a list of modules in qjs::Context. - */ - class Module - { - friend class Context; - - JSModuleDef * m; - JSContext * ctx; - - using nvp = std::pair; - std::vector exports; - public: - Module(JSContext * ctx, const char * name) : ctx(ctx) - { - m = JS_NewCModule(ctx, name, [](JSContext * ctx, JSModuleDef * m) noexcept { - auto& context = Context::get(ctx); - auto it = std::find_if(context.modules.begin(), context.modules.end(), - [m](const Module& module) { return module.m == m; }); - if(it == context.modules.end()) - return -1; - for(const auto& e : it->exports) - { - if(JS_SetModuleExport(ctx, m, e.first, JS_DupValue(ctx, e.second.v)) != 0) - return -1; - } - return 0; - }); - if(!m) - throw exception{}; - } - - Module& add(const char *_name, JSValue value) - { - exports.push_back({_name, {ctx, value}}); - JS_AddModuleExport(ctx, m, _name); - return *this; - } - - Module& add(const char * _name, Value value) - { - assert(value.ctx == ctx); - exports.push_back({_name, std::move(value)}); - JS_AddModuleExport(ctx, m, _name); - return *this; - } - - template - Module& add(const char * name, T value) - { - return add(name, js_traits::wrap(ctx, std::move(value))); - } - - Module(const Module&) = delete; - - Module(Module&&) = default; - //Module& operator=(Module&&) = default; - - - // function wrappers - - /** Add free function F. - * Example: - * module.function(&::sin)>("sin"); - */ - template - Module& function(const char * name) - { - return add(name, qjs::fwrapper{name}); - } - - /** Add function object f. - * Slower than template version. - * Example: module.function("sin", [](double x) { return ::sin(x); }); - */ - template - Module& function(const char * name, F&& f) - { - return add(name, js_traits(f)})>::wrap(std::forward(f))); - } - - // class register wrapper - private: - /** Helper class to register class members and constructors. - * See fun, constructor. - * Actual registration occurs at object destruction. - */ - template - class class_registrar - { - const char * name; - qjs::Value prototype; - qjs::Context::Module& module; - qjs::Context& context; - public: - explicit class_registrar(const char * name, qjs::Context::Module& module, qjs::Context& context) : - name(name), - prototype(context.newObject()), - module(module), - context(context) - { - } - - class_registrar(const class_registrar&) = delete; - - /** Add functional object f - */ - template - class_registrar& fun(const char * name, F&& f) - { - prototype.add(name, std::forward(f)); - return *this; - } - - /** Add class member function or class member variable F - * Example: - * struct T { int var; int func(); } - * auto& module = context.addModule("module"); - * module.class_("T").fun<&T::var>("var").fun<&T::func>("func"); - */ - template - class_registrar& fun(const char * name) - { - prototype.add(name); - return *this; - } - - /** Add class constructor - * @tparam Args contructor arguments - * @param name constructor name (if not specified class name will be used) - */ - template - class_registrar& constructor(const char * name = nullptr) - { - if(!name) - name = this->name; - Value ctor = context.newValue(qjs::ctor_wrapper{name}); - JS_SetConstructor(context.ctx, ctor.v, prototype.v); - module.add(name, std::move(ctor)); - return *this; - } - - /* TODO: needs casting to base class - template - class_registrar& base() - { - assert(js_traits>::QJSClassId && "base class is not registered"); - auto base_proto = JS_GetClassProto(context.ctx, js_traits>::QJSClassId); - int err = JS_SetPrototype(context.ctx, prototype.v, base_proto); - JS_FreeValue(context.ctx, base_proto); - if(err < 0) - throw exception{}; - return *this; - } - */ - - ~class_registrar() - { - context.registerClass(name, std::move(prototype)); - } - }; - - public: - /** Add class to module. - * See \ref class_registrar. - */ - template - class_registrar class_(const char * name) - { - return class_registrar{name, *this, qjs::Context::get(ctx)}; - } - - }; - - std::vector modules; -private: - void init() - { - JS_SetContextOpaque(ctx, this); - js_traits::register_class(ctx, "C++ function"); - } - -public: - Context(Runtime& rt) : Context(rt.rt) - {} - - Context(JSRuntime * rt) - { - ctx = JS_NewContext(rt); - if(!ctx) - throw std::runtime_error{"qjs: Cannot create context"}; - init(); - } - - Context(JSContext * ctx) : ctx{ctx} - { - init(); - } - - // noncopyable - Context(const Context&) = delete; - - ~Context() - { - modules.clear(); - JS_FreeContext(ctx); - } - - /** Create module and return a reference to it */ - Module& addModule(const char * name) - { - modules.emplace_back(ctx, name); - return modules.back(); - } - - /** returns globalThis */ - Value global() { return Value{ctx, JS_GetGlobalObject(ctx)}; } - - /** returns new Object() */ - Value newObject() { return Value{ctx, JS_NewObject(ctx)}; } - - /** returns JS value converted from c++ object val */ - template - Value newValue(T&& val) { return Value{ctx, std::forward(val)}; } - - /** returns current exception associated with context, and resets it. Should be called when qjs::exception is caught */ - Value getException() { return Value{ctx, JS_GetException(ctx)}; } - - /** Register class T for conversions to/from std::shared_ptr to work. - * Wherever possible module.class_("T")... should be used instead. - * @tparam T class type - * @param name class name in JS engine - * @param proto JS class prototype or JS_UNDEFINED - */ - template - void registerClass(const char * name, JSValue proto = JS_NULL) - { - js_traits>::register_class(ctx, name, proto); - } - - Value eval(std::string_view buffer, const char * filename = "", unsigned eval_flags = 0) - { - assert(buffer.data()[buffer.size()] == '\0' && "eval buffer is not null-terminated"); // JS_Eval requirement - JSValue v = JS_Eval(ctx, buffer.data(), buffer.size(), filename, eval_flags); - return Value{ctx, v}; - } - - Value fromJSON(std::string_view buffer, const char * filename = "") - { - assert(buffer.data()[buffer.size()] == '\0' && "fromJSON buffer is not null-terminated"); // JS_ParseJSON requirement - JSValue v = JS_ParseJSON(ctx, buffer.data(), buffer.size(), filename); - return Value{ctx, v}; - } - - /** Get qjs::Context from JSContext opaque pointer */ - static Context& get(JSContext * ctx) - { - void * ptr = JS_GetContextOpaque(ctx); - assert(ptr); - return *reinterpret_cast(ptr); - } -}; - -/** Conversion traits for Value. - */ -template <> -struct js_traits -{ - static Value unwrap(JSContext * ctx, JSValueConst v) - { - return Value{ctx, JS_DupValue(ctx, v)}; - } - - static JSValue wrap(JSContext * ctx, Value v) noexcept - { - assert(ctx == v.ctx); - return v.release(); - } -}; - -/** Convert to/from std::function. Actually accepts/returns callable object that is compatible with function. - * @tparam R return type - * @tparam Args argument types - */ -template -struct js_traits> -{ - static auto unwrap(JSContext * ctx, JSValueConst fun_obj) - { - return [jsfun_obj = Value{ctx, JS_DupValue(ctx, fun_obj)}](Args&& ... args) -> R { - const int argc = sizeof...(Args); - JSValue argv[argc]; - detail::wrap_args(jsfun_obj.ctx, argv, std::forward(args)...); - JSValue result = JS_Call(jsfun_obj.ctx, jsfun_obj.v, JS_UNDEFINED, argc, const_cast(argv)); - for(int i = 0; i < argc; i++) JS_FreeValue(jsfun_obj.ctx, argv[i]); - return detail::unwrap_free(jsfun_obj.ctx, result); - }; - } - - /** Convert from function object functor to JSValue. - * Uses detail::function for type-erasure. - */ - template - static JSValue wrap(JSContext * ctx, Functor&& functor) - { - using detail::function; - assert(js_traits::QJSClassId); - auto obj = JS_NewObjectClass(ctx, js_traits::QJSClassId); - if(JS_IsException(obj)) - return JS_EXCEPTION; - auto fptr = function::create(JS_GetRuntime(ctx), std::forward(functor)); - fptr->invoker = [](function * self, JSContext * ctx, JSValueConst this_value, int argc, JSValueConst * argv) { - assert(self); - auto f = reinterpret_cast(&self->functor); - return detail::wrap_call(ctx, *f, argv); - }; - JS_SetOpaque(obj, fptr); - return obj; - } -}; - -/** Convert from std::vector to Array and vice-versa. If Array holds objects that are non-convertible to T throws qjs::exception */ -template -struct js_traits> -{ - static JSValue wrap(JSContext * ctx, const std::vector& arr) noexcept - { - try - { - auto jsarray = Value{ctx, JS_NewArray(ctx)}; - for(uint32_t i = 0; i < (uint32_t) arr.size(); i++) - jsarray[i] = arr[i]; - return jsarray.release(); - } - catch(exception) - { - return JS_EXCEPTION; - } - } - - static std::vector unwrap(JSContext * ctx, JSValueConst jsarr) - { - int e = JS_IsArray(ctx, jsarr); - if(e == 0) - JS_ThrowTypeError(ctx, "js_traits>::unwrap expects array"); - if(e <= 0) - throw exception{}; - Value jsarray{ctx, JS_DupValue(ctx, jsarr)}; - std::vector arr; - auto len = static_cast(jsarray["length"]); - arr.reserve((uint32_t) len); - for(uint32_t i = 0; i < (uint32_t) len; i++) - arr.push_back(static_cast(jsarray[i])); - return arr; - } -}; - -namespace detail { -template -property_proxy::operator Value() const -{ - return as(); -} -} - -} // namespace qjs diff --git a/example/lib/main.dart b/example/lib/main.dart index 1dda450..0ab27cd 100644 --- a/example/lib/main.dart +++ b/example/lib/main.dart @@ -3,7 +3,7 @@ * @Author: ekibun * @Date: 2020-08-08 08:16:51 * @LastEditors: ekibun - * @LastEditTime: 2020-09-21 01:20:17 + * @LastEditTime: 2020-09-21 13:50:40 */ import 'package:flutter/material.dart'; import 'dart:typed_data'; @@ -53,13 +53,12 @@ class _TestPageState extends State { _createEngine() async { if (engine != null) return; engine = FlutterQjs(); - engine.setMethodHandler((String method, List arg) async { + engine.setMethodHandler((String method, List arg) { switch (method) { case "http": - Response response = await Dio().get(arg[0]); - return response.data; + return Dio().get(arg[0]).then((response) => response.data); case "test": - return await arg[0]([ + return arg[0]([ true, 1, 0.5, @@ -72,7 +71,7 @@ class _TestPageState extends State { Float32List(2) ]); default: - throw Exception("NotImplement"); + throw Exception("No such method"); } }); engine.setModuleHandler((String module) { diff --git a/lib/ffi.dart b/lib/ffi.dart index c373987..40db9f4 100644 --- a/lib/ffi.dart +++ b/lib/ffi.dart @@ -3,7 +3,7 @@ * @Author: ekibun * @Date: 2020-09-19 10:29:04 * @LastEditors: ekibun - * @LastEditTime: 2020-09-21 01:30:41 + * @LastEditTime: 2020-09-21 14:25:47 */ import 'dart:ffi'; import 'dart:io'; @@ -53,7 +53,31 @@ final DynamicLibrary qjsLib = Platform.environment['FLUTTER_TEST'] == 'true' ? (Platform.isWindows ? DynamicLibrary.open("test/build/Debug/flutter_qjs.dll") : DynamicLibrary.process()) - : (Platform.isWindows ? DynamicLibrary.open("flutter_qjs_plugin.dll") : DynamicLibrary.process()); + : (Platform.isWindows + ? DynamicLibrary.open("flutter_qjs_plugin.dll") + : Platform.isAndroid + ? DynamicLibrary.open("libqjs.so") + : DynamicLibrary.process()); + +/// JSValue *jsThrowInternalError(JSContext *ctx, char *message) +final Pointer Function( + Pointer ctx, + Pointer message, +) _jsThrowInternalError = qjsLib + .lookup< + NativeFunction< + Pointer Function( + Pointer, + Pointer, + )>>("jsThrowInternalError") + .asFunction(); + +Pointer jsThrowInternalError(Pointer ctx, String message) { + var utf8message = Utf8.toUtf8(message); + var val = _jsThrowInternalError(ctx, utf8message); + free(utf8message); + return val; +} /// JSValue *jsEXCEPTION() final Pointer Function() jsEXCEPTION = @@ -95,7 +119,9 @@ Pointer jsNewRuntime( ReceivePort port, ) { var rt = _jsNewRuntime(Pointer.fromFunction(channelDispacher)); - runtimeOpaques[rt] = RuntimeOpaque()..channel = callback..port = port; + runtimeOpaques[rt] = RuntimeOpaque() + ..channel = callback + ..port = port; return rt; } @@ -696,4 +722,4 @@ final int Function( Int32 Function( Pointer, )>>("jsExecutePendingJob") - .asFunction(); \ No newline at end of file + .asFunction(); diff --git a/lib/flutter_qjs.dart b/lib/flutter_qjs.dart index afd76fd..cecfe4c 100644 --- a/lib/flutter_qjs.dart +++ b/lib/flutter_qjs.dart @@ -3,10 +3,11 @@ * @Author: ekibun * @Date: 2020-08-08 08:29:09 * @LastEditors: ekibun - * @LastEditTime: 2020-09-21 01:36:30 + * @LastEditTime: 2020-09-21 13:46:50 */ import 'dart:async'; import 'dart:ffi'; +import 'dart:io'; import 'dart:isolate'; import 'package:ffi/ffi.dart'; @@ -29,17 +30,27 @@ class FlutterQjs { _ensureEngine() { if (_rt != null) return; _rt = jsNewRuntime((ctx, method, argv) { - if (method.address != 0) { - var argvs = jsToDart(ctx, argv); - if (methodHandler == null) throw Exception("No MethodHandler"); - return dartToJs(ctx, methodHandler(Utf8.fromUtf8(method.cast()), argvs)); + try { + if (method.address != 0) { + if (methodHandler == null) throw Exception("No MethodHandler"); + var argvs = jsToDart(ctx, argv); + return dartToJs(ctx, methodHandler(Utf8.fromUtf8(method.cast()), argvs)); + } + if (moduleHandler == null) throw Exception("No ModuleHandler"); + var ret = Utf8.toUtf8(moduleHandler(Utf8.fromUtf8(argv.cast()))); + Future.microtask(() { + free(ret); + }); + return ret; + } catch (e, stack) { + var err = jsThrowInternalError(ctx, e.toString() + "\n" + stack.toString()); + if (method.address == 0) { + jsFreeValue(ctx, err); + deleteJSValue(err); + return Pointer.fromAddress(0); + } + return err; } - if (moduleHandler == null) throw Exception("No ModuleHandler"); - var ret = Utf8.toUtf8(moduleHandler(Utf8.fromUtf8(argv.cast()))); - Future.microtask(() { - free(ret); - }); - return ret; }, port); _ctx = jsNewContextWithPromsieWrapper(_rt); } diff --git a/lib/wrapper.dart b/lib/wrapper.dart index ff8eff8..c3d0d61 100644 --- a/lib/wrapper.dart +++ b/lib/wrapper.dart @@ -3,7 +3,7 @@ * @Author: ekibun * @Date: 2020-09-19 22:07:47 * @LastEditors: ekibun - * @LastEditTime: 2020-09-21 01:23:06 + * @LastEditTime: 2020-09-21 13:56:53 */ import 'dart:async'; import 'dart:ffi'; @@ -53,7 +53,10 @@ class JSPromise extends JSRefValue { return true; } if (status["__rejected"] == true) { - completer.completeError(status["__error"] ?? "undefined"); + completer.completeError(parseJSException( + ctx, + e: jsGetPropertyStr(ctx, val, "__error"), + )); return true; } return false; @@ -93,8 +96,8 @@ Pointer jsGetPropertyStr(Pointer ctx, Pointer val, String prop) { return jsProp; } -String parseJSException(Pointer ctx) { - Pointer e = jsGetException(ctx); +String parseJSException(Pointer ctx, {Pointer e}) { + e = e ?? jsGetException(ctx); var err = jsToCString(ctx, e); if (jsValueGetTag(e) == JSTag.OBJECT) { Pointer stack = jsGetPropertyStr(ctx, e, "stack"); @@ -192,7 +195,7 @@ dynamic jsToDart(Pointer ctx, Pointer val, {Map cache}) { int size = psize.value; free(psize); if (buf.address != 0) { - return buf.asTypedList(size); + return Uint8List.fromList(buf.asTypedList(size)); } int valptr = jsValueGetPtr(val).address; if (cache.containsKey(valptr)) {