From 7c1c7ceee9903288e2076c3c0cb79f68acb7da84 Mon Sep 17 00:00:00 2001 From: ekibun Date: Sun, 16 Aug 2020 20:05:53 +0800 Subject: [PATCH] android type wrapper --- .idea/libraries/Dart_SDK.xml | 19 ++ .idea/modules.xml | 10 + .../example_lib_main_dart.xml | 6 + .idea/workspace.xml | 45 ++++ .vscode/c_cpp_properties.json | 37 ++++ .vscode/settings.json | 3 +- android/.idea/misc.xml | 2 +- android/src/main/jni/CMakeLists.txt | 2 +- android/src/main/jni/java_js_wrapper.hpp | 195 ++++++++++++++++++ android/src/main/jni/jni_helper.hpp | 62 ++++++ android/src/main/jni/native-lib.cpp | 131 ++++++++---- .../ekibun/flutter_qjs/FlutterQjsPlugin.kt | 25 ++- .../soko/ekibun/flutter_qjs/JniBridge.kt | 8 +- .../soko/ekibun/flutter_qjs/JsEngine.kt | 80 ------- .../flutter_qjs/MethodChannelWrapper.kt | 4 +- .../soko/ekibun/flutter_qjs/ResultWrapper.kt | 2 +- example/windows/CMakeLists.txt | 4 +- example/windows/flutter/.template_version | 2 +- example/windows/flutter/CMakeLists.txt | 5 +- example/windows/runner/flutter_window.cpp | 11 +- example/windows/runner/flutter_window.h | 2 +- example/windows/runner/win32_window.cpp | 9 +- example/windows/runner/win32_window.h | 4 +- .../windows/runner/window_configuration.cpp | 2 +- lib/flutter_qjs.dart | 29 +-- windows/dart_js_wrapper.hpp | 37 ++-- .../include/flutter_qjs/flutter_qjs_plugin.h | 6 +- 27 files changed, 552 insertions(+), 190 deletions(-) create mode 100644 .idea/libraries/Dart_SDK.xml create mode 100644 .idea/modules.xml create mode 100644 .idea/runConfigurations/example_lib_main_dart.xml create mode 100644 .idea/workspace.xml create mode 100644 .vscode/c_cpp_properties.json create mode 100644 android/src/main/jni/java_js_wrapper.hpp create mode 100644 android/src/main/jni/jni_helper.hpp delete mode 100644 android/src/main/kotlin/soko/ekibun/flutter_qjs/JsEngine.kt diff --git a/.idea/libraries/Dart_SDK.xml b/.idea/libraries/Dart_SDK.xml new file mode 100644 index 0000000..70af93c --- /dev/null +++ b/.idea/libraries/Dart_SDK.xml @@ -0,0 +1,19 @@ + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/.idea/modules.xml b/.idea/modules.xml new file mode 100644 index 0000000..4f16f99 --- /dev/null +++ b/.idea/modules.xml @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/.idea/runConfigurations/example_lib_main_dart.xml b/.idea/runConfigurations/example_lib_main_dart.xml new file mode 100644 index 0000000..5fd9159 --- /dev/null +++ b/.idea/runConfigurations/example_lib_main_dart.xml @@ -0,0 +1,6 @@ + + + + \ No newline at end of file diff --git a/.idea/workspace.xml b/.idea/workspace.xml new file mode 100644 index 0000000..e513d54 --- /dev/null +++ b/.idea/workspace.xml @@ -0,0 +1,45 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/.vscode/c_cpp_properties.json b/.vscode/c_cpp_properties.json new file mode 100644 index 0000000..c8a4e87 --- /dev/null +++ b/.vscode/c_cpp_properties.json @@ -0,0 +1,37 @@ +{ + "configurations": [ + { + "name": "Win32", + "includePath": [ + "${workspaceFolder}/example/windows/flutter/ephemeral/cpp_client_wrapper/include/**" + ], + "defines": [ + "_DEBUG", + "UNICODE", + "_UNICODE" + ], + "windowsSdkVersion": "10.0.18362.0", + "compilerPath": "C:/Program Files (x86)/Microsoft Visual Studio/2019/BuildTools/VC/Tools/MSVC/14.27.29110/bin/Hostx64/x64/cl.exe", + "cStandard": "c11", + "cppStandard": "c++17", + "intelliSenseMode": "msvc-x64" + }, + { + "name": "Linux", + "includePath": [ + "C:/Users/ekibun/AppData/Local/Android/Sdk/ndk/21.3.6528147/toolchains/llvm/prebuilt/windows-x86_64/sysroot/usr/include" + ], + "defines": [ + "_DEBUG", + "UNICODE", + "_UNICODE" + ], + "windowsSdkVersion": "10.0.18362.0", + "compilerPath": "C:/Users/ekibun/AppData/Local/Android/Sdk/cmake/3.10.2.4988404/bin/cmake.exe", + "cStandard": "c11", + "cppStandard": "c++17", + "intelliSenseMode": "gcc-x64" + } + ], + "version": 4 +} \ No newline at end of file diff --git a/.vscode/settings.json b/.vscode/settings.json index ecd48a1..f40bb0d 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -99,7 +99,8 @@ "cfenv": "cpp", "cinttypes": "cpp", "typeindex": "cpp", - "__functional_03": "cpp" + "__functional_03": "cpp", + "compare": "cpp" }, "java.configuration.updateBuildConfiguration": "interactive" } \ No newline at end of file diff --git a/android/.idea/misc.xml b/android/.idea/misc.xml index c532fb7..4bc4fc6 100644 --- a/android/.idea/misc.xml +++ b/android/.idea/misc.xml @@ -1,5 +1,5 @@ - + \ No newline at end of file diff --git a/android/src/main/jni/CMakeLists.txt b/android/src/main/jni/CMakeLists.txt index d5994bc..d51ef03 100644 --- a/android/src/main/jni/CMakeLists.txt +++ b/android/src/main/jni/CMakeLists.txt @@ -5,7 +5,7 @@ cmake_minimum_required(VERSION 3.4.1) set(JNI_LIB_NAME libjsengine) -set(QUICK_JS_LIB_DIR ../../../../cxx) +set(QUICK_JS_LIB_DIR ../../../../cxx/quickjspp) # Creates and names a library, sets it as either STATIC diff --git a/android/src/main/jni/java_js_wrapper.hpp b/android/src/main/jni/java_js_wrapper.hpp new file mode 100644 index 0000000..bb4c80e --- /dev/null +++ b/android/src/main/jni/java_js_wrapper.hpp @@ -0,0 +1,195 @@ +/* + * @Description: + * @Author: ekibun + * @Date: 2020-08-16 11:08:23 + * @LastEditors: ekibun + * @LastEditTime: 2020-08-16 19:51:11 + */ +#include +#include +#include "jni_helper.hpp" +#include +#include "../../../../cxx/js_engine.hpp" + +namespace std +{ + template <> + struct hash + { + std::size_t operator()(const qjs::Value &key) const + { + return std::hash()((std::string)key); + } + }; +} // namespace std + +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)); + __android_log_print(ANDROID_LOG_DEBUG, "class", "class: %s", className.c_str()); + 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("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++) + JS_DefinePropertyValue( + ctx, array, JS_NewAtomUInt32(ctx, i), + javaToJs(ctx, env, env->GetObjectArrayElement(list, i), cache), + JS_PROP_C_W_E); + 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); + JS_DefinePropertyValue( + ctx, obj, JS_ValueToAtom(ctx, javaToJs(ctx, env, env->CallObjectMethod(entryObj, getKeyMID), cache)), + javaToJs(ctx, env, env->CallObjectMethod(entryObj, getValueMID), cache), + JS_PROP_C_W_E); + } + return obj; + } + return JS_UNDEFINED; + } + + jobject jsToJava(JNIEnv *env, qjs::Value val, std::unordered_map cache = std::unordered_map()) + { + if (cache.find(val) != cache.end()) + return cache[val]; + if (JS_IsBool(val.v)) + return jniWrapPrimity(env, (bool)val); + { + int tag = JS_VALUE_GET_TAG(val.v); + if(tag == JS_TAG_INT) { + return jniWrapPrimity(env, (int64_t)val); + } else if (JS_TAG_IS_FLOAT64(tag)) { + return jniWrapPrimity(env, (double)val); + } + } + if (JS_IsString(val.v)) + return env->NewStringUTF(((std::string)val).c_str()); + { + 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 (JS_IsUndefined(val.v) || JS_IsNull(val.v) || JS_IsUninitialized(val.v)) + return nullptr; + if (JS_IsObject(val.v)) + { + if (JS_IsFunction(val.ctx, val.v)) + { + std::map retMap; + retMap[env->NewStringUTF("__js_function__")] = jniWrapPrimity(env, (int64_t) new JSValue{JS_DupValue(val.ctx, val.v)}); + 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; + } + } + return nullptr; + } +} // namespace qjs diff --git a/android/src/main/jni/jni_helper.hpp b/android/src/main/jni/jni_helper.hpp new file mode 100644 index 0000000..4bb2444 --- /dev/null +++ b/android/src/main/jni/jni_helper.hpp @@ -0,0 +1,62 @@ +/* + * @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 index b02b903..7cb91a9 100644 --- a/android/src/main/jni/native-lib.cpp +++ b/android/src/main/jni/native-lib.cpp @@ -3,13 +3,9 @@ * @Author: ekibun * @Date: 2020-08-09 18:16:11 * @LastEditors: ekibun - * @LastEditTime: 2020-08-12 23:37:28 + * @LastEditTime: 2020-08-16 19:00:06 */ -#include -#include -#include "../../../../cxx/js_engine.hpp" - -qjs::Engine *engine = nullptr; +#include "java_js_wrapper.hpp" JNIEnv *getEnv(JavaVM *gJvm) { @@ -26,48 +22,41 @@ JNIEnv *getEnv(JavaVM *gJvm) return env; } -void jniResultResolve(JavaVM *jvm, jobject result, std::string data) +void jniResultResolve(JNIEnv *env, jobject result, jobject data) { - JNIEnv *env = getEnv(jvm); jclass jclazz = env->GetObjectClass(result); - jmethodID jmethod = env->GetMethodID(jclazz, "success", "(Ljava/lang/String;)V"); - jstring jdata = env->NewStringUTF(data.c_str()); - env->CallVoidMethod(result, jmethod, jdata); - env->DeleteLocalRef(jdata); + jmethodID jmethod = env->GetMethodID(jclazz, "success", "(Ljava/lang/Object;)V"); + env->CallVoidMethod(result, jmethod, data); + env->DeleteLocalRef(data); env->DeleteGlobalRef(result); - jvm->DetachCurrentThread(); } -void jniResultReject(JavaVM *jvm, jobject result, std::string reason) +void jniResultReject(JNIEnv *env, jobject result, std::string reason) { - JNIEnv *env = getEnv(jvm); 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); - jvm->DetachCurrentThread(); } -void jniChannelInvoke(JavaVM *jvm, jobject channel, std::promise *promise, std::string method, std::string argv) +void jniChannelInvoke(JNIEnv *env, jobject channel, std::promise *promise, std::string method, qjs::Value args, qjs::Engine *engine) { - JNIEnv *env = nullptr; - jvm->GetEnv((void **)&env, JNI_VERSION_1_2); - jvm->AttachCurrentThread(&env, NULL); jclass jclazz = env->GetObjectClass(channel); - jmethodID jmethod = env->GetMethodID(jclazz, "invokeMethod", "(Ljava/lang/String;Ljava/lang/String;J)V"); + jmethodID jmethod = env->GetMethodID(jclazz, "invokeMethod", "(Ljava/lang/String;Ljava/lang/Object;J)V"); jstring jstrmethod = env->NewStringUTF(method.c_str()); - jstring jstrargv = env->NewStringUTF(argv.c_str()); - - env->CallVoidMethod(channel, jmethod, jstrmethod, jstrargv, (jlong)promise); + 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(jstrargv); - jvm->DetachCurrentThread(); + env->DeleteLocalRef(jsargs); } -extern "C" JNIEXPORT jint JNICALL -Java_soko_ekibun_flutter_1qjs_JniBridge_initEngine( +extern "C" JNIEXPORT jlong JNICALL +Java_soko_ekibun_flutter_1qjs_JniBridge_createEngine( JNIEnv *env, jobject thiz, jobject channel) @@ -75,10 +64,12 @@ Java_soko_ekibun_flutter_1qjs_JniBridge_initEngine( JavaVM *jvm = nullptr; env->GetJavaVM(&jvm); jobject gchannel = env->NewGlobalRef(channel); - engine = new qjs::Engine([jvm, gchannel](std::string name, std::string args) { + qjs::Engine *engine = new qjs::Engine([jvm, gchannel](std::string name, qjs::Value args, qjs::Engine *engine) { auto promise = new std::promise(); - jniChannelInvoke(jvm, gchannel, promise, name, args); - return promise->get_future(); + JNIEnv *env = getEnv(jvm); + jniChannelInvoke(env, gchannel, promise, name, args, engine); + jvm->DetachCurrentThread(); + return promise; }); return (jlong)engine; } @@ -87,6 +78,7 @@ extern "C" JNIEXPORT void JNICALL Java_soko_ekibun_flutter_1qjs_JniBridge_evaluate( JNIEnv *env, jobject thiz, + jlong engine, jstring script, jstring name, jobject result) @@ -94,33 +86,86 @@ Java_soko_ekibun_flutter_1qjs_JniBridge_evaluate( JavaVM *jvm = nullptr; env->GetJavaVM(&jvm); jobject gresult = env->NewGlobalRef(result); - engine->commit(qjs::EngineTask{ - env->GetStringUTFChars(script, 0), - env->GetStringUTFChars(name, 0), - [jvm, gresult](std::string resolve) { - jniResultResolve(jvm, gresult, resolve); + ((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](std::string reject) { - jniResultReject(jvm, gresult, reject); + [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 /* this */) + jobject thiz, + jlong engine) { - delete 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 = JS_Call(ctx.ctx, *function, ctx.global(), (int)argscount, callargs); + qjs::JS_FreeValue(ctx.ctx, *function); + 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, jstring data) + jlong promise, jobject 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())}; + 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}; }); } 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 938f100..096708f 100644 --- a/android/src/main/kotlin/soko/ekibun/flutter_qjs/FlutterQjsPlugin.kt +++ b/android/src/main/kotlin/soko/ekibun/flutter_qjs/FlutterQjsPlugin.kt @@ -16,7 +16,6 @@ class FlutterQjsPlugin: FlutterPlugin, MethodCallHandler { /// when the Flutter Engine is detached from the Activity private lateinit var channelwrapper : MethodChannelWrapper private lateinit var channel : MethodChannel - private var engine : JniBridge? = null private lateinit var applicationContext: android.content.Context private val handler by lazy { Handler() } @@ -28,20 +27,24 @@ class FlutterQjsPlugin: FlutterPlugin, MethodCallHandler { } override fun onMethodCall(call: MethodCall, result: Result) { - if (call.method == "initEngine") { - // engine = JsEngine(channel) - engine = JniBridge() - engine?.initEngine(channelwrapper) - result.success(1) + if (call.method == "createEngine") { + val engine: Long = JniBridge.instance.createEngine(channelwrapper) + println(engine) + 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")!! - engine?.evaluate(script, name, ResultWrapper(handler, result)) - // engine?.evaluate(script, result) + JniBridge.instance.evaluate(engine, script, name, ResultWrapper(handler, result)) + } else if (call.method == "call") { + println(call.arguments>()); + 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") { - // engine?.release() - engine?.close() - engine = null + val engine: Long = call.argument("engine")!! + JniBridge.instance.close(engine) result.success(null) } else { result.notImplemented() diff --git a/android/src/main/kotlin/soko/ekibun/flutter_qjs/JniBridge.kt b/android/src/main/kotlin/soko/ekibun/flutter_qjs/JniBridge.kt index bf7177c..f3b8356 100644 --- a/android/src/main/kotlin/soko/ekibun/flutter_qjs/JniBridge.kt +++ b/android/src/main/kotlin/soko/ekibun/flutter_qjs/JniBridge.kt @@ -10,11 +10,13 @@ class JniBridge { val instance by lazy { JniBridge() } } - external fun initEngine(channel: MethodChannelWrapper): Int + external fun createEngine(channel: MethodChannelWrapper): Long - external fun evaluate(script: String, name: String, result: ResultWrapper) + external fun evaluate(engine: Long, script: String, name: String, result: ResultWrapper) - external fun close() + external fun close(engine: Long) + + external fun call(engine: Long, function: Long, args: List, result: ResultWrapper) external fun reject(promise: Long, reason: String) diff --git a/android/src/main/kotlin/soko/ekibun/flutter_qjs/JsEngine.kt b/android/src/main/kotlin/soko/ekibun/flutter_qjs/JsEngine.kt deleted file mode 100644 index 8eb0194..0000000 --- a/android/src/main/kotlin/soko/ekibun/flutter_qjs/JsEngine.kt +++ /dev/null @@ -1,80 +0,0 @@ -package soko.ekibun.flutter_qjs - -// import android.util.Log -// import de.prosiebensat1digital.oasisjsbridge.* -import io.flutter.plugin.common.MethodChannel -// import kotlinx.coroutines.* - -class JsEngine(private val channel: MethodChannel) { -// private val jsBridge = JsBridge(JsBridgeConfig.bareConfig()) - -// fun processPromiseQueue() { -// JsBridge::class.java.getDeclaredMethod("processPromiseQueue").invoke(jsBridge) -// } - -// private fun dartInteract(resolve: (String) -> Unit, reject: (String) -> Unit, method: String, args: String) { -// println("dart: $method") -// jsBridge.launch(Dispatchers.Main) { -// channel.invokeMethod(method, args, object : MethodChannel.Result { -// override fun notImplemented() { -// println("dart error: notImplemented") -// jsBridge.launch(Dispatchers.Main) { -// reject("notImplemented") -// withContext(jsBridge.coroutineContext) { -// processPromiseQueue() -// } -// } -// } - -// override fun error(error_code: String?, error_message: String?, error_details: Any?) { -// println("dart error: ${error_message ?: "undefined"}") -// jsBridge.launch(Dispatchers.Main) { -// reject(error_message ?: "undefined") -// withContext(jsBridge.coroutineContext) { -// processPromiseQueue() -// } -// } -// } - -// override fun success(result: Any?) { -// println("dart success: $result") -// jsBridge.launch(Dispatchers.Main) { -// resolve(result.toString()) -// withContext(jsBridge.coroutineContext) { -// processPromiseQueue() -// } -// } -// } -// }) -// } -// } - -// init { -// // jsBridge.initEngine() -// jsBridge.launch(Dispatchers.Main) { -// JsValue.fromNativeFunction4(jsBridge, ::dartInteract).assignToGlobal("__DartImpl__invoke") -// jsBridge.evaluateAsync>(""" -// this.dart = (method, ...args) => new Promise((res, rej) => -// this.__DartImpl__invoke((v) => res(JSON.parse(v)), rej, method, JSON.stringify(args))) -// """).await().await() -// } -// } - -// fun eval(script: String, result: MethodChannel.Result) { -// jsBridge.launch(Dispatchers.Main) { -// try { -// var ret = jsBridge.evaluateAsync>(script).await().await() -// println(ret) -// result.success(ret) -// } catch (e: Throwable) { -// e.printStackTrace() -// result.error("FlutterJSException", Log.getStackTraceString(e), null) -// } -// } - -// } - -// fun release() { -// jsBridge.release() -// } -} \ 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 index c8e8811..01f79b1 100644 --- a/android/src/main/kotlin/soko/ekibun/flutter_qjs/MethodChannelWrapper.kt +++ b/android/src/main/kotlin/soko/ekibun/flutter_qjs/MethodChannelWrapper.kt @@ -7,7 +7,7 @@ import io.flutter.plugin.common.MethodChannel @Keep class MethodChannelWrapper(private val handler: Handler, private val channel: MethodChannel) { - fun invokeMethod(method: String, arguments: String, promise: Long) { + fun invokeMethod(method: String, arguments: Any?, promise: Long) { handler.post { channel.invokeMethod(method, arguments, object : MethodChannel.Result { override fun notImplemented() { @@ -19,7 +19,7 @@ class MethodChannelWrapper(private val handler: Handler, private val channel: Me } override fun success(data: Any?) { - JniBridge.instance.resolve(promise, data.toString()) + JniBridge.instance.resolve(promise, data) } }) diff --git a/android/src/main/kotlin/soko/ekibun/flutter_qjs/ResultWrapper.kt b/android/src/main/kotlin/soko/ekibun/flutter_qjs/ResultWrapper.kt index ae79454..10ef0b0 100644 --- a/android/src/main/kotlin/soko/ekibun/flutter_qjs/ResultWrapper.kt +++ b/android/src/main/kotlin/soko/ekibun/flutter_qjs/ResultWrapper.kt @@ -9,7 +9,7 @@ import io.flutter.plugin.common.MethodChannel.Result class ResultWrapper(private val handler: Handler, private val result: Result) { @Keep - fun success(dat: String) { + fun success(dat: Any?) { handler.post { result.success(dat) } } diff --git a/example/windows/CMakeLists.txt b/example/windows/CMakeLists.txt index 3f0cac3..abf9040 100644 --- a/example/windows/CMakeLists.txt +++ b/example/windows/CMakeLists.txt @@ -1,7 +1,7 @@ cmake_minimum_required(VERSION 3.15) -project(flutter_qjs_example LANGUAGES CXX) +project(example LANGUAGES CXX) -set(BINARY_NAME "flutter_qjs_example") +set(BINARY_NAME "example") cmake_policy(SET CMP0063 NEW) diff --git a/example/windows/flutter/.template_version b/example/windows/flutter/.template_version index b8626c4..7ed6ff8 100644 --- a/example/windows/flutter/.template_version +++ b/example/windows/flutter/.template_version @@ -1 +1 @@ -4 +5 diff --git a/example/windows/flutter/CMakeLists.txt b/example/windows/flutter/CMakeLists.txt index 3c1726f..ff47d32 100644 --- a/example/windows/flutter/CMakeLists.txt +++ b/example/windows/flutter/CMakeLists.txt @@ -43,6 +43,7 @@ list(APPEND CPP_WRAPPER_SOURCES_PLUGIN ) list(TRANSFORM CPP_WRAPPER_SOURCES_PLUGIN PREPEND "${WRAPPER_ROOT}/") list(APPEND CPP_WRAPPER_SOURCES_APP + "flutter_engine.cc" "flutter_view_controller.cc" ) list(TRANSFORM CPP_WRAPPER_SOURCES_APP PREPEND "${WRAPPER_ROOT}/") @@ -79,11 +80,13 @@ add_dependencies(flutter_wrapper_app flutter_assemble) # _phony_ is a non-existent file to force this command to run every time, # since currently there's no way to get a full input/output list from the # flutter tool. +set(PHONY_OUTPUT "${CMAKE_CURRENT_BINARY_DIR}/_phony_") +set_source_files_properties("${PHONY_OUTPUT}" PROPERTIES SYMBOLIC TRUE) add_custom_command( OUTPUT ${FLUTTER_LIBRARY} ${FLUTTER_LIBRARY_HEADERS} ${CPP_WRAPPER_SOURCES_CORE} ${CPP_WRAPPER_SOURCES_PLUGIN} ${CPP_WRAPPER_SOURCES_APP} - ${CMAKE_CURRENT_BINARY_DIR}/_phony_ + ${PHONY_OUTPUT} COMMAND ${CMAKE_COMMAND} -E env ${FLUTTER_TOOL_ENVIRONMENT} "${FLUTTER_ROOT}/packages/flutter_tools/bin/tool_backend.bat" diff --git a/example/windows/runner/flutter_window.cpp b/example/windows/runner/flutter_window.cpp index fe980cf..f3661dc 100644 --- a/example/windows/runner/flutter_window.cpp +++ b/example/windows/runner/flutter_window.cpp @@ -8,15 +8,22 @@ FlutterWindow::FlutterWindow(RunLoop* run_loop, FlutterWindow::~FlutterWindow() {} -void FlutterWindow::OnCreate() { - Win32Window::OnCreate(); +bool FlutterWindow::OnCreate() { + if (!Win32Window::OnCreate()) { + return false; + } // The size here is arbitrary since SetChildContent will resize it. flutter_controller_ = std::make_unique(100, 100, project_); + // Ensure that basic setup of the controller was successful. + if (!flutter_controller_->engine() || !flutter_controller_->view()) { + return false; + } RegisterPlugins(flutter_controller_.get()); run_loop_->RegisterFlutterInstance(flutter_controller_.get()); SetChildContent(flutter_controller_->view()->GetNativeWindow()); + return true; } void FlutterWindow::OnDestroy() { diff --git a/example/windows/runner/flutter_window.h b/example/windows/runner/flutter_window.h index 4f41e16..7f3162f 100644 --- a/example/windows/runner/flutter_window.h +++ b/example/windows/runner/flutter_window.h @@ -20,7 +20,7 @@ class FlutterWindow : public Win32Window { protected: // Win32Window: - void OnCreate() override; + bool OnCreate() override; void OnDestroy() override; private: diff --git a/example/windows/runner/win32_window.cpp b/example/windows/runner/win32_window.cpp index 677a9a6..e7607ef 100644 --- a/example/windows/runner/win32_window.cpp +++ b/example/windows/runner/win32_window.cpp @@ -122,9 +122,11 @@ bool Win32Window::CreateAndShow(const std::wstring& title, Scale(size.width, scale_factor), Scale(size.height, scale_factor), nullptr, nullptr, GetModuleHandle(nullptr), this); - OnCreate(); + if (!window) { + return false; + } - return window != nullptr; + return OnCreate(); } // static @@ -240,8 +242,9 @@ void Win32Window::SetQuitOnClose(bool quit_on_close) { quit_on_close_ = quit_on_close; } -void Win32Window::OnCreate() { +bool Win32Window::OnCreate() { // No-op; provided for subclasses. + return true; } void Win32Window::OnDestroy() { diff --git a/example/windows/runner/win32_window.h b/example/windows/runner/win32_window.h index 5cbb5d5..ea09829 100644 --- a/example/windows/runner/win32_window.h +++ b/example/windows/runner/win32_window.h @@ -62,8 +62,8 @@ class Win32Window { LPARAM const lparam) noexcept; // Called when CreateAndShow is called, allowing subclass window-related - // setup. - virtual void OnCreate(); + // setup. Subclasses should return false if setup fails. + virtual bool OnCreate(); // Called when Destroy is called. virtual void OnDestroy(); diff --git a/example/windows/runner/window_configuration.cpp b/example/windows/runner/window_configuration.cpp index 5d27ba9..c56ede1 100644 --- a/example/windows/runner/window_configuration.cpp +++ b/example/windows/runner/window_configuration.cpp @@ -1,6 +1,6 @@ #include "window_configuration.h" -const wchar_t* kFlutterWindowTitle = L"flutter_qjs_example"; +const wchar_t* kFlutterWindowTitle = L"example"; const unsigned int kFlutterWindowOriginX = 10; const unsigned int kFlutterWindowOriginY = 10; const unsigned int kFlutterWindowWidth = 1280; diff --git a/lib/flutter_qjs.dart b/lib/flutter_qjs.dart index 12ac8cb..1990c92 100644 --- a/lib/flutter_qjs.dart +++ b/lib/flutter_qjs.dart @@ -3,9 +3,10 @@ * @Author: ekibun * @Date: 2020-08-08 08:29:09 * @LastEditors: ekibun - * @LastEditTime: 2020-08-15 16:21:56 + * @LastEditTime: 2020-08-16 19:10:47 */ import 'dart:async'; +import 'dart:io'; import 'package:flutter/services.dart'; typedef JsMethodHandler = Future Function(String method, List args); @@ -24,24 +25,28 @@ class _FlutterJs { print(methodHandlers.entries); print(methodHandlers[engine]); if (methodHandlers[engine] == null) return call.noSuchMethod(null); - return await methodHandlers[engine](call.method, _wrapFunctionArguments(args)); + return await methodHandlers[engine](call.method, _wrapFunctionArguments(args, engine)); }); } - dynamic _wrapFunctionArguments(dynamic val) { - if (val is List) { + dynamic _wrapFunctionArguments(dynamic val, dynamic engine) { + if (val is List && !(val is List)) { for (var i = 0; i < val.length; ++i) { - val[i] = _wrapFunctionArguments(val[i]); + val[i] = _wrapFunctionArguments(val[i], engine); } } else if (val is Map) { - if (val["__js_function__"] != 0) { + // wrap boolean in Android see https://github.com/flutter/flutter/issues/45066 + if (Platform.isAndroid && val["__js_boolean__"] != null) { + return val["__js_boolean__"] != 0; + } + if (val["__js_function__"] != null) { var functionId = val["__js_function__"]; return (List args) async { - var arguments = {"function": functionId, "arguments": args}; - return await _channel.invokeMethod("call", arguments); + var arguments = {"engine": engine, "function": functionId, "arguments": args}; + return _wrapFunctionArguments(await _channel.invokeMethod("call", arguments), engine); }; } else for (var key in val.keys) { - val[key] = _wrapFunctionArguments(val[key]); + val[key] = _wrapFunctionArguments(val[key], engine); } } return val; @@ -59,7 +64,7 @@ class FlutterJs { dynamic _engine; ensureEngine() async { - if(_engine == null){ + if (_engine == null) { _engine = await _FlutterJs.instance._channel.invokeMethod("createEngine"); } } @@ -70,7 +75,7 @@ class FlutterJs { } destroy() async { - if (_engine != null){ + if (_engine != null) { await _FlutterJs.instance._channel.invokeMethod("close", {"engine": _engine}); _engine = null; } @@ -80,6 +85,6 @@ class FlutterJs { ensureEngine(); var arguments = {"engine": _engine, "script": command, "name": command}; return _FlutterJs.instance._wrapFunctionArguments( - await _FlutterJs.instance._channel.invokeMethod("evaluate", arguments)); + await _FlutterJs.instance._channel.invokeMethod("evaluate", arguments), _engine); } } diff --git a/windows/dart_js_wrapper.hpp b/windows/dart_js_wrapper.hpp index 143888b..dab1cf8 100644 --- a/windows/dart_js_wrapper.hpp +++ b/windows/dart_js_wrapper.hpp @@ -3,7 +3,7 @@ * @Author: ekibun * @Date: 2020-08-14 21:45:02 * @LastEditors: ekibun - * @LastEditTime: 2020-08-15 15:42:55 + * @LastEditTime: 2020-08-16 19:52:35 */ #include "../cxx/js_engine.hpp" #include @@ -32,8 +32,12 @@ namespace std namespace qjs { - JSValue dartToJsAtom(JSContext *ctx, flutter::EncodableValue val) + JSValue dartToJs(JSContext *ctx, flutter::EncodableValue val, std::unordered_map cache = std::unordered_map()) { + if (val.IsNull()) + return JS_UNDEFINED; + if (cache.find(val) != cache.end()) + return cache[val]; if (std::holds_alternative(val)) return JS_NewBool(ctx, std::get(val)); if (std::holds_alternative(val)) @@ -44,20 +48,6 @@ namespace qjs return JS_NewFloat64(ctx, std::get(val)); if (std::holds_alternative(val)) return JS_NewString(ctx, std::get(val).c_str()); - return JS_UNDEFINED; - } - - JSValue dartToJs(JSContext *ctx, flutter::EncodableValue val, std::unordered_map cache = std::unordered_map()) - { - if (val.IsNull()) - return JS_UNDEFINED; - if (cache.find(val) != cache.end()) - return cache[val]; - { - JSValue atomValue = dartToJsAtom(ctx, val); - if (!JS_IsUndefined(atomValue)) - return atomValue; - } if (std::holds_alternative>(val)) { auto buf = std::get>(val); @@ -117,8 +107,17 @@ namespace qjs return cache[val]; if (JS_IsBool(val.v)) return (bool)val; - if (JS_IsNumber(val.v)) - return (double)val; + { + int tag = JS_VALUE_GET_TAG(val.v); + if (tag == JS_TAG_INT) + { + return (int64_t)val; + } + else if (JS_TAG_IS_FLOAT64(tag)) + { + return (double)val; + } + } if (JS_IsString(val.v)) return (std::string)val; { // ArrayBuffer @@ -135,7 +134,7 @@ namespace qjs if (JS_IsFunction(val.ctx, val.v)) { flutter::EncodableMap retMap; - retMap[std::string("__js_function__")] = (int64_t) new JSValue { JS_DupValue(val.ctx, val.v) }; + retMap[std::string("__js_function__")] = (int64_t) new JSValue{JS_DupValue(val.ctx, val.v)}; ret = retMap; } else if (JS_IsArray(val.ctx, val.v) > 0) diff --git a/windows/include/flutter_qjs/flutter_qjs_plugin.h b/windows/include/flutter_qjs/flutter_qjs_plugin.h index cc4130d..b38f7d1 100644 --- a/windows/include/flutter_qjs/flutter_qjs_plugin.h +++ b/windows/include/flutter_qjs/flutter_qjs_plugin.h @@ -1,5 +1,5 @@ -#ifndef FLUTTER_PLUGIN_FLUTTER_JS_PLUGIN_H_ -#define FLUTTER_PLUGIN_FLUTTER_JS_PLUGIN_H_ +#ifndef FLUTTER_PLUGIN_FLUTTER_QJS_PLUGIN_H_ +#define FLUTTER_PLUGIN_FLUTTER_QJS_PLUGIN_H_ #include @@ -20,4 +20,4 @@ FLUTTER_PLUGIN_EXPORT void FlutterQjsPluginRegisterWithRegistrar( } // extern "C" #endif -#endif // FLUTTER_PLUGIN_FLUTTER_JS_PLUGIN_H_ +#endif // FLUTTER_PLUGIN_FLUTTER_QJS_PLUGIN_H_