From 7b35868c5cce161351cb1fd196c05bdaf32603ba Mon Sep 17 00:00:00 2001 From: ekibun Date: Sat, 15 Aug 2020 14:52:53 +0800 Subject: [PATCH] add function channel --- .vscode/launch.json | 13 ++- .vscode/settings.json | 3 +- LICENSE | 22 ++++- README.md | 2 +- cxx/js_dart_promise.hpp | 19 ++-- cxx/js_engine.hpp | 49 ++++----- cxx/quickjspp | 2 +- example/lib/test.dart | 4 +- example/windows/CMakeLists.txt | 2 +- lib/flutter_qjs.dart | 25 ++++- windows/dart_js_wrapper.hpp | 176 +++++++++++++++++++++++++++++++++ windows/flutter_qjs_plugin.cpp | 48 +++++++-- 12 files changed, 313 insertions(+), 52 deletions(-) create mode 100644 windows/dart_js_wrapper.hpp diff --git a/.vscode/launch.json b/.vscode/launch.json index c4d33c2..9a138fc 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -4,9 +4,20 @@ // 欲了解更多信息,请访问: https://go.microsoft.com/fwlink/?linkid=830387 "version": "0.2.0", "configurations": [ + { + "name": "(Windows) 启动", + "type": "cppvsdbg", + "request": "launch", + "program": "example/build/windows/runner/Debug/flutter_qjs_example.exe", + "args": [], + "stopAtEntry": false, + "cwd": "${workspaceFolder}", + "environment": [], + "externalConsole": false + }, { "name": "Flutter", - "program": "lib/main.dart", + "program": "example/lib/main.dart", "request": "launch", "type": "dart" } diff --git a/.vscode/settings.json b/.vscode/settings.json index f487e24..ecd48a1 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -98,7 +98,8 @@ "numeric": "cpp", "cfenv": "cpp", "cinttypes": "cpp", - "typeindex": "cpp" + "typeindex": "cpp", + "__functional_03": "cpp" }, "java.configuration.updateBuildConfiguration": "interactive" } \ No newline at end of file diff --git a/LICENSE b/LICENSE index ba75c69..4ae93d7 100644 --- a/LICENSE +++ b/LICENSE @@ -1 +1,21 @@ -TODO: Add your license here. +MIT License + +Copyright (c) 2019 https://github.com/czy0729 + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. \ No newline at end of file diff --git a/README.md b/README.md index fe10bcc..a06a428 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # flutter_qjs -A new flutter plugin project. +A quickjs engine for flutter. ## Getting Started diff --git a/cxx/js_dart_promise.hpp b/cxx/js_dart_promise.hpp index 497d8a5..64d2a0e 100644 --- a/cxx/js_dart_promise.hpp +++ b/cxx/js_dart_promise.hpp @@ -3,16 +3,17 @@ * @Author: ekibun * @Date: 2020-08-07 13:55:52 * @LastEditors: ekibun - * @LastEditTime: 2020-08-13 13:47:16 + * @LastEditTime: 2020-08-15 14:45:00 */ #pragma once #include "quickjspp/quickjspp.hpp" -#include "quickjspp/quickjs/list.h" #include #include namespace qjs { +#include "quickjspp/quickjs/list.h" + static JSClassID js_dart_promise_class_id; typedef struct @@ -22,11 +23,12 @@ namespace qjs } JSOSFutureArgv; using JSFutureReturn = std::function; - using DartChannel = std::function(std::string, std::string)>; + using DartChannel = std::function *(std::string, Value)>; typedef struct { struct list_head link; + std::promise *promise; std::shared_future future; JSValue resolve; JSValue reject; @@ -35,10 +37,10 @@ namespace qjs typedef struct JSThreadState { struct list_head os_future; /* list of JSOSFuture.link */ - std::function(std::string, std::string)> channel; + DartChannel channel; } JSThreadState; - static JSValue js_add_future(Value resolve, Value reject, std::shared_future future) + static JSValue js_add_future(Value resolve, Value reject, std::promise *promise) { JSRuntime *rt = JS_GetRuntime(resolve.ctx); JSThreadState *ts = (JSThreadState *)JS_GetRuntimeOpaque(rt); @@ -61,7 +63,8 @@ namespace qjs JS_FreeValue(resolve.ctx, obj); return JS_EXCEPTION; } - th->future = future; + 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); @@ -69,7 +72,7 @@ namespace qjs return obj; } - JSValue js_dart_future(Value resolve, Value reject, std::string name, std::string args) + 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); @@ -116,6 +119,7 @@ namespace qjs { JSOSFuture *th = list_entry(el, JSOSFuture, link); th->future.get(); + delete th->promise; unlink_future(rt, th); free_future(rt, th); } @@ -159,7 +163,6 @@ namespace qjs { JSOSFutureArgv argv = th->future.get()(ctx); JSValue resolve, reject; - int64_t delay; /* the timer expired */ resolve = th->resolve; th->resolve = JS_UNDEFINED; diff --git a/cxx/js_engine.hpp b/cxx/js_engine.hpp index afbc7fc..acf4c2a 100644 --- a/cxx/js_engine.hpp +++ b/cxx/js_engine.hpp @@ -3,7 +3,7 @@ * @Author: ekibun * @Date: 2020-08-08 10:30:59 * @LastEditors: ekibun - * @LastEditTime: 2020-08-13 13:47:28 + * @LastEditTime: 2020-08-15 13:13:43 */ #pragma once @@ -21,8 +21,7 @@ namespace qjs { struct EngineTask { - std::string command; - std::string name; + std::function invoke; std::function resolve; std::function reject; }; @@ -72,10 +71,26 @@ namespace qjs R"xxx( import * as __DartImpl from "__DartImpl"; globalThis.dart = (method, ...args) => new Promise((res, rej) => - __DartImpl.__invoke((v) => res(JSON.parse(v)), rej, method, JSON.stringify(args))); + __DartImpl.__invoke(res, rej, method, args)); )xxx", "", JS_EVAL_TYPE_MODULE); 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) { @@ -93,25 +108,11 @@ namespace qjs if (task.resolve) try { - ctx.global()["__evalstr"] = JS_NewString(ctx.ctx, task.command.c_str()); - Value ret = ctx.eval( - R"xxx( - (() => { - const __ret = Promise.resolve(eval(__evalstr)) - .then(v => { - __ret.__value = v; - __ret.__resolved = true; - }).catch(e => { - __ret.__error = e; - __ret.__rejected = true; - }); - return __ret; - })() - )xxx", - task.name.c_str()); + 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 e) + catch (exception) { task.reject(getStackTrack(ctx.getException())); } @@ -152,16 +153,16 @@ namespace qjs { idle = js_dart_poll(ctx.ctx); } - catch (exception e) + catch (exception) { handleException(ctx.getException()); } // 空闲时reject所有task if (idle && !JS_IsJobPending(rt.rt) && !unresolvedTask.empty()) { - for (EngineTaskResolver &task : unresolvedTask) + for (EngineTaskResolver &_task : unresolvedTask) { - task.reject("Promise cannot resolve"); + _task.reject("Promise cannot resolve"); } unresolvedTask.clear(); } diff --git a/cxx/quickjspp b/cxx/quickjspp index b98fbc1..0768b66 160000 --- a/cxx/quickjspp +++ b/cxx/quickjspp @@ -1 +1 @@ -Subproject commit b98fbc128ad55f52ebff96ce099890e107a8f0df +Subproject commit 0768b665d5a7606c85bb875e20aedf65ee7da35a diff --git a/example/lib/test.dart b/example/lib/test.dart index 0694caa..4c0b063 100644 --- a/example/lib/test.dart +++ b/example/lib/test.dart @@ -3,7 +3,7 @@ * @Author: ekibun * @Date: 2020-07-18 23:28:55 * @LastEditors: ekibun - * @LastEditTime: 2020-08-08 17:38:48 + * @LastEditTime: 2020-08-15 14:01:09 */ import 'package:dio/dio.dart'; import 'package:flutter/material.dart'; @@ -48,6 +48,8 @@ class _TestPageState extends State { case "http": Response response = await Dio().get(arg[0]); return response.data; + case "hello": + return await arg[0](["hello: "]); default: } }; diff --git a/example/windows/CMakeLists.txt b/example/windows/CMakeLists.txt index ea7bfdf..3f0cac3 100644 --- a/example/windows/CMakeLists.txt +++ b/example/windows/CMakeLists.txt @@ -32,7 +32,7 @@ add_definitions(-DUNICODE -D_UNICODE) # Compilation settings that should be applied to most targets. function(APPLY_STANDARD_SETTINGS TARGET) target_compile_features(${TARGET} PUBLIC cxx_std_17) - target_compile_options(${TARGET} PRIVATE /W4 /WX- /wd"4100") + target_compile_options(${TARGET} PRIVATE /W4 /WX /wd"4100") target_compile_options(${TARGET} PRIVATE /EHsc) target_compile_definitions(${TARGET} PRIVATE "_HAS_EXCEPTIONS=0") target_compile_definitions(${TARGET} PRIVATE "$<$:_DEBUG>") diff --git a/lib/flutter_qjs.dart b/lib/flutter_qjs.dart index 93b182a..5ff74ed 100644 --- a/lib/flutter_qjs.dart +++ b/lib/flutter_qjs.dart @@ -3,10 +3,9 @@ * @Author: ekibun * @Date: 2020-08-08 08:29:09 * @LastEditors: ekibun - * @LastEditTime: 2020-08-08 17:40:35 + * @LastEditTime: 2020-08-15 13:58:11 */ import 'dart:async'; -import 'dart:convert'; import 'package:flutter/services.dart'; @@ -19,12 +18,30 @@ class FlutterJs { final int engineId = await _channel.invokeMethod("initEngine"); _channel.setMethodCallHandler((call) async { if (methodHandler == null) return call.noSuchMethod(null); - List args = jsonDecode(call.arguments); - return jsonEncode(await methodHandler(call.method, args)); + return await methodHandler(call.method, _wrapFunctionArguments(call.arguments)); }); return engineId; } + static dynamic _wrapFunctionArguments(dynamic val) { + if (val is List) { + for (var i = 0; i < val.length; ++i) { + val[i] = _wrapFunctionArguments(val[i]); + } + } else if (val is Map) { + if (val["__js_function__"] != 0) { + var functionId = val["__js_function__"]; + return (List args) async { + var arguments = {"function": functionId, "arguments": args}; + return await _channel.invokeMethod("call", arguments); + }; + }else for(var key in val.keys) { + val[key] = _wrapFunctionArguments(val[key]); + } + } + return val; + } + static Future evaluate(String command, String name) async { var arguments = {"script": command, "name": command}; final String jsResult = await _channel.invokeMethod("evaluate", arguments); diff --git a/windows/dart_js_wrapper.hpp b/windows/dart_js_wrapper.hpp new file mode 100644 index 0000000..6e58034 --- /dev/null +++ b/windows/dart_js_wrapper.hpp @@ -0,0 +1,176 @@ +/* + * @Description: + * @Author: ekibun + * @Date: 2020-08-14 21:45:02 + * @LastEditors: ekibun + * @LastEditTime: 2020-08-15 14:08:04 + */ +#include "../cxx/js_engine.hpp" +#include +#include + +namespace std +{ + template <> + struct hash + { + std::size_t operator()(const qjs::Value &key) const + { + return std::hash()((std::string)key); + } + }; + + template <> + struct hash + { + std::size_t operator()(const flutter::EncodableValue &key) const + { + return key.index(); + } + }; +} // namespace std + +namespace qjs +{ + JSValue dartToJsAtom(JSContext *ctx, flutter::EncodableValue val) + { + if (std::holds_alternative(val)) + return JS_NewBool(ctx, std::get(val)); + if (std::holds_alternative(val)) + return JS_NewInt32(ctx, std::get(val)); + if (std::holds_alternative(val)) + return JS_NewInt64(ctx, std::get(val)); + if (std::holds_alternative(val)) + 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) + { + 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); + return JS_NewArrayBufferCopy(ctx, buf.data(), buf.size()); + } + if (std::holds_alternative>(val)) + { + auto buf = std::get>(val); + return JS_NewArrayBufferCopy(ctx, (uint8_t *)buf.data(), buf.size() * 4); + } + if (std::holds_alternative>(val)) + { + auto buf = std::get>(val); + return JS_NewArrayBufferCopy(ctx, (uint8_t *)buf.data(), buf.size() * 8); + } + if (std::holds_alternative>(val)) + { + auto buf = std::get>(val); + JSValue array = JS_NewArray(ctx); + cache[val] = array; + auto size = (uint32_t)buf.size(); + for (uint32_t i = 0; i < size; i++) + JS_DefinePropertyValue( + ctx, array, JS_NewAtomUInt32(ctx, i), JS_NewFloat64(ctx, buf[i]), + JS_PROP_C_W_E); + return array; + } + if (std::holds_alternative(val)) + { + auto list = std::get(val); + JSValue array = JS_NewArray(ctx); + cache[val] = array; + auto size = (uint32_t)list.size(); + for (uint32_t i = 0; i < size; i++) + JS_DefinePropertyValue( + ctx, array, JS_NewAtomUInt32(ctx, i), dartToJs(ctx, list[i], cache), + JS_PROP_C_W_E); + return array; + } + if (std::holds_alternative(val)) + { + auto map = std::get(val); + JSValue obj = JS_NewObject(ctx); + cache[val] = obj; + for (auto iter = map.begin(); iter != map.end(); ++iter) + JS_DefinePropertyValue( + ctx, obj, JS_ValueToAtom(ctx, dartToJs(ctx, iter->first, cache)), dartToJs(ctx, iter->second, cache), + JS_PROP_C_W_E); + return obj; + } + return JS_UNDEFINED; + } + + flutter::EncodableValue jsToDart(Value val, std::unordered_map cache) + { + if (cache.find(val) != cache.end()) + return cache[val]; + if (JS_IsBool(val.v)) + return (bool)val; + if (JS_IsNumber(val.v)) + return (double)val; + if (JS_IsString(val.v)) + return (std::string)val; + { // ArrayBuffer + size_t size; + uint8_t *buf = JS_GetArrayBuffer(val.ctx, &size, val.v); + if (buf) + return (std::vector(buf, buf + size)); + } + flutter::EncodableValue ret; + if (JS_IsUndefined(val.v) || JS_IsNull(val.v) || JS_IsUninitialized(val.v)) + goto exception; + if (JS_IsObject(val.v)) + { + 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) }; + ret = retMap; + } + else if (JS_IsArray(val.ctx, val.v) > 0) + { + flutter::EncodableList retList; + cache[val] = retList; + uint32_t arrlen = (uint32_t)val["length"]; + for (uint32_t i = 0; i < arrlen; i++) + { + retList.push_back(jsToDart(val[i], cache)); + } + ret = retList; + } + else + { + qjs::JSPropertyEnum *ptab; + uint32_t plen; + if (JS_GetOwnPropertyNames(val.ctx, &ptab, &plen, val.v, -1)) + goto exception; + flutter::EncodableMap retMap; + cache[val] = retMap; + for (uint32_t i = 0; i < plen; i++) + { + retMap[jsToDart({val.ctx, JS_AtomToValue(val.ctx, ptab[i].atom)}, cache)] = + jsToDart({val.ctx, JS_GetProperty(val.ctx, val.v, ptab[i].atom)}, cache); + JS_FreeAtom(val.ctx, ptab[i].atom); + } + js_free(val.ctx, ptab); + ret = retMap; + } + goto done; + } + exception: + ret = flutter::EncodableValue(); + done: + return ret; + } +} // namespace qjs diff --git a/windows/flutter_qjs_plugin.cpp b/windows/flutter_qjs_plugin.cpp index 6f43bbc..aa0ef74 100644 --- a/windows/flutter_qjs_plugin.cpp +++ b/windows/flutter_qjs_plugin.cpp @@ -8,7 +8,7 @@ #include #include -#include "../cxx/js_engine.hpp" +#include "dart_js_wrapper.hpp" namespace { @@ -30,17 +30,17 @@ namespace }; std::shared_ptr> channel; - std::future invokeChannelMethod(std::string name, std::string args) + std::promise *invokeChannelMethod(std::string name, qjs::Value args) { auto promise = new std::promise(); channel->InvokeMethod( name, - std::make_unique(args), + std::make_unique(qjs::jsToDart(args, std::unordered_map())), std::make_unique>( (flutter::ResultHandlerSuccess)[promise]( const flutter::EncodableValue *result) { - promise->set_value((qjs::JSFutureReturn)[rep = std::get(*result)](qjs::JSContext * ctx) { - qjs::JSValue *ret = new qjs::JSValue{JS_NewString(ctx, rep.c_str())}; + promise->set_value((qjs::JSFutureReturn)[result = result ? *result : flutter::EncodableValue()](qjs::JSContext * ctx) { + qjs::JSValue *ret = new qjs::JSValue{qjs::dartToJs(ctx, result, std::unordered_map())}; return qjs::JSOSFutureArgv{1, ret}; }); }, @@ -59,7 +59,7 @@ namespace return qjs::JSOSFutureArgv{-1, ret}; }); })); - return promise->get_future(); + return promise; } // static @@ -111,7 +111,7 @@ namespace if (method_call.method_name().compare("initEngine") == 0) { engine = new qjs::Engine((qjs::DartChannel)invokeChannelMethod); - flutter::EncodableValue response((long)engine); + flutter::EncodableValue response = (int64_t)engine; result->Success(&response); } else if (method_call.method_name().compare("evaluate") == 0) @@ -121,9 +121,39 @@ namespace std::string name = std::get(ValueOrNull(args, "name")); auto presult = result.release(); engine->commit(qjs::EngineTask{ - script, name, + [script, name](qjs::Context &ctx) { + return ctx.eval(script, name.c_str(), JS_EVAL_TYPE_GLOBAL); + }, [presult](std::string resolve) { - flutter::EncodableValue response(resolve); + flutter::EncodableValue response = resolve; + presult->Success(&response); + }, + [presult](std::string reject) { + presult->Error("FlutterJSException", reject); + }}); + } + else if (method_call.method_name().compare("call") == 0) + { + flutter::EncodableMap args = *std::get_if(method_call.arguments()); + qjs::JSValue *function = (qjs::JSValue *)std::get(ValueOrNull(args, "function")); + flutter::EncodableList arguments = std::get(ValueOrNull(args, "arguments")); + auto presult = result.release(); + engine->commit(qjs::EngineTask{ + [function, arguments](qjs::Context &ctx) { + size_t argscount = arguments.size(); + qjs::JSValue *callargs = new qjs::JSValue[argscount]; + for (size_t i = 0; i < argscount; i++) + { + callargs[i] = qjs::dartToJs(ctx.ctx, arguments[i], std::unordered_map()); + } + qjs::JSValue ret = JS_Call(ctx.ctx, *function, qjs::JSValue{qjs::JSValueUnion{0}, qjs::JS_TAG_UNDEFINED}, (int)argscount, callargs); + qjs::JS_FreeValue(ctx.ctx, *function); + if (qjs::JS_IsException(ret)) + throw qjs::exception{}; + return qjs::Value{ctx.ctx, ret}; + }, + [presult](std::string resolve) { + flutter::EncodableValue response = resolve; presult->Success(&response); }, [presult](std::string reject) {