From 29c2b04a3618be64fe11fe5057bcd52d72162eaf Mon Sep 17 00:00:00 2001 From: ekibun Date: Tue, 22 Sep 2020 00:32:17 +0800 Subject: [PATCH] use native promise constructor --- CHANGELOG.md | 6 ++- README.md | 52 +++++++++---------- cxx/ffi.cpp | 7 ++- example/ios/Flutter/Generated.xcconfig | 14 +++++ .../ios/Flutter/flutter_export_environment.sh | 15 ++++++ .../ios/Runner/GeneratedPluginRegistrant.h | 17 ++++++ .../ios/Runner/GeneratedPluginRegistrant.m | 12 +++++ example/lib/main.dart | 8 ++- lib/ffi.dart | 16 +++++- lib/flutter_qjs.dart | 3 +- lib/wrapper.dart | 49 ++++++----------- 11 files changed, 128 insertions(+), 71 deletions(-) create mode 100644 example/ios/Flutter/Generated.xcconfig create mode 100644 example/ios/Flutter/flutter_export_environment.sh create mode 100644 example/ios/Runner/GeneratedPluginRegistrant.h create mode 100644 example/ios/Runner/GeneratedPluginRegistrant.m diff --git a/CHANGELOG.md b/CHANGELOG.md index 6bcf151..3a6fd0d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,8 +3,12 @@ * @Author: ekibun * @Date: 2020-08-08 08:16:50 * @LastEditors: ekibun - * @LastEditTime: 2020-08-28 10:46:26 + * @LastEditTime: 2020-09-21 23:19:02 --> +## 0.1.0 + +* refactor with ffi. + ## 0.0.6 * remove handler when destroy. diff --git a/README.md b/README.md index 318e698..85f6953 100644 --- a/README.md +++ b/README.md @@ -3,7 +3,7 @@ * @Author: ekibun * @Date: 2020-08-08 08:16:50 * @LastEditors: ekibun - * @LastEditTime: 2020-08-27 21:11:55 + * @LastEditTime: 2020-09-22 00:03:48 --> # flutter_qjs @@ -11,13 +11,13 @@ A quickjs engine for flutter. ## Feature -This plugin is a simple js engine for flutter using the `quickjs` project. Plugin currently supports Windows, Linux, and Android. +This plugin is a simple js engine for flutter using the `quickjs` project with `dart:ffi`. Plugin currently supports Windows, Linux, and Android. -Each `FlutterJs` object creates a new thread that runs a simple js loop. +Event loop of `FlutterQjs` should be implemented by calling `FlutterQjs.dispatch()`. ES6 module with `import` function is supported and can manage in dart with `setModuleHandler`. -A global async function `dart` is presented to invoke dart function, and `Promise` is supported so that you can use `await` or `then` to get external result from `dart`. Data convertion between dart and js are implemented as follow: +A global function `convert` is presented to invoke dart function. Data convertion between dart and js are implemented as follow: | dart | js | | --- | --- | @@ -25,54 +25,50 @@ A global async function `dart` is presented to invoke dart function, and `Promis | Int | number | | Double | number | | String | string | -| Uint8List/Int32List/Int64List | ArrayBuffer | -| Float64List | number[] | +| Uint8List | ArrayBuffer | | List | Array | | Map | Object | -| Closure(List) => Future | function(....args) | +| JSFunction | function(....args) | +| Future | Promise | -**notice:** -1. All the `Uint8List/Int32List/Int64List` sent from dart will be converted to `ArrayBuffer` without marked the size of elements, and the `ArrayBuffer` will be converted to `Uint8List`. - -2. `function` can only sent from js to dart and all the arguments will be packed in a dart `List` object. +**notice:** `function` can only sent from js to dart. ## Getting Started -1. Create a `FlutterJs` object. Make sure call `destroy` to terminate thread and release memory when you don't need it. +1. Create a `FlutterQjs` object. Call `dispatch` to dispatch event loop. ```dart -FlutterJs engine = FlutterJs(); -// do something ... -await engine.destroy(); -engine = null; +final engine = FlutterQjs(); +await engine.dispatch(); ``` 2. Call `setMethodHandler` to implements `dart` interaction. For example, you can use `Dio` to implements http in js: ```dart -await engine.setMethodHandler((String method, List arg) async { +await 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); default: - return JsMethodHandlerNotImplement(); + throw Exception("No such method"); } }); ``` -and in javascript, call `dart` function to get data: +and in javascript, call `convert` function to get data, make sure the second memeber is a list: ```javascript -dart("http", "http://example.com/"); +convert("http", ["http://example.com/"]); ``` -3. Call `setModuleHandler` to resolve js module. For example, you can use assets files as module: +3. Call `setModuleHandler` to resolve js module. + +**important:** I cannot find a way to convert the sync ffi callback into an async function. So the assets files got by async function `rootBundle.loadString` cannot be used in this version. I will be appreciated that you can provied me a solution to make `ModuleHandler` async. ```dart -await engine.setModuleHandler((String module) async { - return await rootBundle.loadString( - "js/" + module.replaceFirst(new RegExp(r".js$"), "") + ".js"); +await engine.setModuleHandler((String module) { + if(module == "hello") return "export default (name) => `hello \${name}!`;"; + throw Exception("Module Not found"); }); ``` @@ -82,7 +78,7 @@ and in javascript, call `import` function to get module: import("hello").then(({default: greet}) => greet("world")); ``` -4. Use `evaluate` to run js script, and try-catch is needed to capture js exception. +4. Use `evaluate` to run js script: ```dart try { @@ -92,4 +88,6 @@ try { } ``` +5. Method `recreate` can free quickjs runtime that can be recreated again when calling `evaluate`, it can be used to reset the module cache. Call `close` to stop `dispatch` when you do not need it. + [This example](example/lib/main.dart) contains a complete demonstration on how to use this plugin. diff --git a/cxx/ffi.cpp b/cxx/ffi.cpp index 7c2f766..2f08bd8 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 18:34:04 + * @LastEditTime: 2020-09-22 00:17:02 */ #include "quickjs/quickjs.h" #include @@ -298,4 +298,9 @@ extern "C" JSContext *ctx; return JS_ExecutePendingJob(rt, &ctx); } + + DLLEXPORT JSValue *jsNewPromiseCapability(JSContext *ctx, JSValue *resolving_funcs) + { + return new JSValue{JS_NewPromiseCapability(ctx, resolving_funcs)}; + } } \ No newline at end of file diff --git a/example/ios/Flutter/Generated.xcconfig b/example/ios/Flutter/Generated.xcconfig new file mode 100644 index 0000000..387c366 --- /dev/null +++ b/example/ios/Flutter/Generated.xcconfig @@ -0,0 +1,14 @@ +// This is a generated file; do not edit or check into version control. +FLUTTER_ROOT=D:\flutter +FLUTTER_APPLICATION_PATH=D:\project\vscode\neko\plugins\flutter_qjs\example +FLUTTER_TARGET=lib\main.dart +FLUTTER_BUILD_DIR=build +SYMROOT=${SOURCE_ROOT}/../build\ios +OTHER_LDFLAGS=$(inherited) -framework Flutter +FLUTTER_FRAMEWORK_DIR=D:\flutter\bin\cache\artifacts\engine\ios +FLUTTER_BUILD_NAME=1.0.0 +FLUTTER_BUILD_NUMBER=1 +DART_OBFUSCATION=false +TRACK_WIDGET_CREATION=false +TREE_SHAKE_ICONS=false +PACKAGE_CONFIG=.packages diff --git a/example/ios/Flutter/flutter_export_environment.sh b/example/ios/Flutter/flutter_export_environment.sh new file mode 100644 index 0000000..e553f63 --- /dev/null +++ b/example/ios/Flutter/flutter_export_environment.sh @@ -0,0 +1,15 @@ +#!/bin/sh +# This is a generated file; do not edit or check into version control. +export "FLUTTER_ROOT=D:\flutter" +export "FLUTTER_APPLICATION_PATH=D:\project\vscode\neko\plugins\flutter_qjs\example" +export "FLUTTER_TARGET=lib\main.dart" +export "FLUTTER_BUILD_DIR=build" +export "SYMROOT=${SOURCE_ROOT}/../build\ios" +export "OTHER_LDFLAGS=$(inherited) -framework Flutter" +export "FLUTTER_FRAMEWORK_DIR=D:\flutter\bin\cache\artifacts\engine\ios" +export "FLUTTER_BUILD_NAME=1.0.0" +export "FLUTTER_BUILD_NUMBER=1" +export "DART_OBFUSCATION=false" +export "TRACK_WIDGET_CREATION=false" +export "TREE_SHAKE_ICONS=false" +export "PACKAGE_CONFIG=.packages" diff --git a/example/ios/Runner/GeneratedPluginRegistrant.h b/example/ios/Runner/GeneratedPluginRegistrant.h new file mode 100644 index 0000000..ed9a5c6 --- /dev/null +++ b/example/ios/Runner/GeneratedPluginRegistrant.h @@ -0,0 +1,17 @@ +// +// Generated file. Do not edit. +// + +#ifndef GeneratedPluginRegistrant_h +#define GeneratedPluginRegistrant_h + +#import + +NS_ASSUME_NONNULL_BEGIN + +@interface GeneratedPluginRegistrant : NSObject ++ (void)registerWithRegistry:(NSObject*)registry; +@end + +NS_ASSUME_NONNULL_END +#endif /* GeneratedPluginRegistrant_h */ diff --git a/example/ios/Runner/GeneratedPluginRegistrant.m b/example/ios/Runner/GeneratedPluginRegistrant.m new file mode 100644 index 0000000..60dfa42 --- /dev/null +++ b/example/ios/Runner/GeneratedPluginRegistrant.m @@ -0,0 +1,12 @@ +// +// Generated file. Do not edit. +// + +#import "GeneratedPluginRegistrant.h" + +@implementation GeneratedPluginRegistrant + ++ (void)registerWithRegistry:(NSObject*)registry { +} + +@end diff --git a/example/lib/main.dart b/example/lib/main.dart index 0ab27cd..12741fb 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 13:50:40 + * @LastEditTime: 2020-09-21 23:54:55 */ import 'package:flutter/material.dart'; import 'dart:typed_data'; @@ -75,10 +75,8 @@ class _TestPageState extends State { } }); engine.setModuleHandler((String module) { - if (module == "test") return "export default '${new DateTime.now()}'"; - return ""; - // return await rootBundle.loadString( - // "js/" + module.replaceFirst(new RegExp(r".js$"), "") + ".js"); + if (module == "hello") return "export default '${new DateTime.now()}'"; + return "Module Not found"; }); engine.dispatch(); } diff --git a/lib/ffi.dart b/lib/ffi.dart index b277e55..9284db9 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 18:36:56 + * @LastEditTime: 2020-09-22 00:23:36 */ import 'dart:ffi'; import 'dart:io'; @@ -104,7 +104,6 @@ class RuntimeOpaque { JSChannel channel; List ref = List(); ReceivePort port; - Pointer Function(Future) futureToPromise; Future Function(Pointer) promsieToFuture; } @@ -723,3 +722,16 @@ final int Function( Pointer, )>>("jsExecutePendingJob") .asFunction(); + +/// JSValue *jsNewPromiseCapability(JSContext *ctx, JSValue *resolving_funcs) +final Pointer Function( + Pointer ctx, + Pointer resolvingFuncs, +) jsNewPromiseCapability = qjsLib + .lookup< + NativeFunction< + Pointer Function( + Pointer, + Pointer, + )>>("jsNewPromiseCapability") + .asFunction(); \ No newline at end of file diff --git a/lib/flutter_qjs.dart b/lib/flutter_qjs.dart index cecfe4c..f866ab1 100644 --- a/lib/flutter_qjs.dart +++ b/lib/flutter_qjs.dart @@ -3,11 +3,10 @@ * @Author: ekibun * @Date: 2020-08-08 08:29:09 * @LastEditors: ekibun - * @LastEditTime: 2020-09-21 13:46:50 + * @LastEditTime: 2020-09-21 23:59:40 */ import 'dart:async'; import 'dart:ffi'; -import 'dart:io'; import 'dart:isolate'; import 'package:ffi/ffi.dart'; diff --git a/lib/wrapper.dart b/lib/wrapper.dart index c3d0d61..424d8c7 100644 --- a/lib/wrapper.dart +++ b/lib/wrapper.dart @@ -3,13 +3,14 @@ * @Author: ekibun * @Date: 2020-09-19 22:07:47 * @LastEditors: ekibun - * @LastEditTime: 2020-09-21 13:56:53 + * @LastEditTime: 2020-09-22 00:27:54 */ import 'dart:async'; import 'dart:ffi'; import 'dart:typed_data'; import 'package:ffi/ffi.dart'; +import 'package:flutter/cupertino.dart'; import 'ffi.dart'; @@ -114,7 +115,20 @@ String parseJSException(Pointer ctx, {Pointer e}) { Pointer dartToJs(Pointer ctx, dynamic val, {Map cache}) { if (val is Future) { - return runtimeOpaques[jsGetRuntime(ctx)]?.futureToPromise(val); + var resolvingFunc = allocate(count: sizeOfJSValue * 2); + var resolvingFunc2 = Pointer.fromAddress(resolvingFunc.address + sizeOfJSValue); + var ret = jsNewPromiseCapability(ctx, resolvingFunc); + var res = jsToDart(ctx, resolvingFunc); + var rej = jsToDart(ctx, resolvingFunc2); + jsFreeValue(ctx, resolvingFunc); + jsFreeValue(ctx, resolvingFunc2); + free(resolvingFunc); + val.then((value) { + res(value); + }, onError: (e, stack) { + rej(e.toString() + "\n" + stack.toString()); + }); + return ret; } if (cache == null) cache = Map(); if (val is bool) return jsNewBool(ctx, val ? 1 : 0); @@ -251,37 +265,6 @@ dynamic jsToDart(Pointer ctx, Pointer val, {Map cache}) { Pointer jsNewContextWithPromsieWrapper(Pointer rt) { var ctx = jsNewContext(rt); - var jsPromiseCtor = jsEval( - ctx, - """ - () => { - const __resolver = {}; - const __ret = new Promise((res, rej) => { - __resolver.__res = res; - __resolver.__rej = rej; - }); - __ret.__res = __resolver.__res; - __ret.__rej = __resolver.__rej; - return __ret; - } - """, - "", - JSEvalType.GLOBAL); - var promiseCtor = JSRefValue(ctx, jsPromiseCtor); - jsFreeValue(ctx, jsPromiseCtor); - deleteJSValue(jsPromiseCtor); - runtimeOpaques[rt].futureToPromise = (future) { - var ctor = promiseCtor.val; - if (ctor == null) throw Exception("Runtime has been released!"); - var jsPromise = jsCall(ctx, ctor, null, List()); - var promise = jsToDart(ctx, jsPromise); - future.then((value) { - promise['__res'](value); - }).catchError((err) { - promise['__rej'](err); - }); - return jsPromise; - }; var jsPromiseWrapper = jsEval( ctx, """