From ee110f55e14acd475505162074b0b893173c0d12 Mon Sep 17 00:00:00 2001 From: ekibun Date: Sun, 24 Jan 2021 21:12:13 +0800 Subject: [PATCH] v0.3.0 --- CHANGELOG.md | 5 ++ README.md | 49 +++++-------- cxx/ffi.cpp | 5 -- example/lib/main.dart | 8 ++- example/pubspec.lock | 2 +- example/test/widget_test.dart | 27 ------- lib/ffi.dart | 11 --- lib/flutter_qjs.dart | 23 +++--- lib/isolate.dart | 128 +++++++++++++++++++++++++--------- lib/wrapper.dart | 24 +++++-- pubspec.yaml | 2 +- test/flutter_qjs_test.dart | 88 ++++++++++++----------- 12 files changed, 202 insertions(+), 170 deletions(-) delete mode 100644 example/test/widget_test.dart diff --git a/CHANGELOG.md b/CHANGELOG.md index a5af7e0..9b386bd 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,11 @@ * @LastEditTime: 2020-12-02 11:36:40 --> +## 0.3.0 + +* breakdown change to remove `channel`. +* convert dart function to js. + ## 0.2.7 * fix error in ios build. diff --git a/README.md b/README.md index c45eef5..8b15947 100644 --- a/README.md +++ b/README.md @@ -60,23 +60,6 @@ Data conversion between dart and js are implemented as follow: **notice:** Dart function parameter `thisVal` is used to store `this` in js. -### Set into global object - -Method `setToGlobalObject` is presented to set dart object into global object. -For example, you can pass a function implement http in JavaScript with `Dio`: - -```dart -engine.setToGlobalObject("http", (String url) { - return Dio().get(url).then((response) => response.data); -}); -``` - -then, in java script you can use `http` function to invoke dart function: - -```javascript -http("http://example.com/"); -``` - ### Use modules ES6 module with `import` function is supported and can be managed in dart with `moduleHandler`: @@ -105,8 +88,6 @@ To use async function in module handler, try [Run on isolate thread](#Run-on-iso Create a `IsolateQjs` object, pass handlers to resolving modules. Async function such as `rootBundle.loadString` can be used now to get modules: -The `methodHandler` is used in isolate, so **the handler function must be a top-level function or a static method**. - ```dart dynamic methodHandler(String method, List arg) { switch (method) { @@ -126,21 +107,11 @@ final engine = IsolateQjs( // not need engine.dispatch(); ``` -Method `setToGlobalObject` is still here to set dart object into global object. Use `await` to make sure it is finished. -**Make sure the object that can pass through isolate**, For example, a top level function: - -```dart -dynamic http(String url) { - return Dio().get(url).then((response) => response.data); -} -await engine.setToGlobalObject("http", http); -``` - Same as run on main thread, use `evaluate` to run js script. In this way, `Promise` return by `evaluate` will be automatically tracked and return the resolved data: ```dart try { - print(await engine.evaluate(code ?? '', "")); + print(await engine.evaluate(code ?? '')); } catch (e) { print(e.toString()); } @@ -148,4 +119,22 @@ try { Method `close` can destroy quickjs runtime that can be recreated again if you call `evaluate`. +**notice:** Make sure arguments passed to `IsolateJSFunction` are avaliable for isolate, such as primities and top level function. Method `bind` can help to pass instance function to isolate: + +```dart +await setToGlobalObject("func", await engine.bind(() { + // DO SOMETHING +})) +``` + [This example](example/lib/main.dart) contains a complete demonstration on how to use this plugin. + +## Breaking change in v0.3.0 + +`channel` function is no longer utilized by default. +Use js function to set to global: + +```dart +final setToGlobalObject = await engine.evaluate("(key, val) => this[key] = val;"); +setToGlobalObject("channel", methodHandler); +``` \ No newline at end of file diff --git a/cxx/ffi.cpp b/cxx/ffi.cpp index baefdb5..1402678 100644 --- a/cxx/ffi.cpp +++ b/cxx/ffi.cpp @@ -141,11 +141,6 @@ extern "C" return new JSValue(JS_NewCFunctionData(ctx, js_channel, 0, 0, 1, funcData)); } - DLLEXPORT JSValue *jsGetGlobalObject(JSContext *ctx) - { - return new JSValue(JS_GetGlobalObject(ctx)); - } - DLLEXPORT JSContext *jsNewContext(JSRuntime *rt) { JSContext *ctx = JS_NewContext(rt); diff --git a/example/lib/main.dart b/example/lib/main.dart index b4d51a3..711f95e 100644 --- a/example/lib/main.dart +++ b/example/lib/main.dart @@ -73,7 +73,7 @@ class _TestPageState extends State { CodeInputController _controller = CodeInputController( text: 'import("hello").then(({default: greet}) => greet("world"));'); - _ensureEngine() { + _ensureEngine() async { if (engine != null) return; engine = IsolateQjs( moduleHandler: (String module) async { @@ -82,7 +82,9 @@ class _TestPageState extends State { "js/" + module.replaceFirst(new RegExp(r".js$"), "") + ".js"); }, ); - engine.setToGlobalObject("channel", methodHandler); + final setToGlobalObject = + await engine.evaluate("(key, val) => this[key] = val;"); + setToGlobalObject("channel", methodHandler); } @override @@ -103,7 +105,7 @@ class _TestPageState extends State { FlatButton( child: Text("evaluate"), onPressed: () async { - _ensureEngine(); + await _ensureEngine(); try { resp = (await engine.evaluate(_controller.text ?? '', name: "")) diff --git a/example/pubspec.lock b/example/pubspec.lock index 3437c61..7ae5e34 100644 --- a/example/pubspec.lock +++ b/example/pubspec.lock @@ -82,7 +82,7 @@ packages: path: ".." relative: true source: path - version: "0.2.7" + version: "0.3.0" flutter_test: dependency: "direct dev" description: flutter diff --git a/example/test/widget_test.dart b/example/test/widget_test.dart deleted file mode 100644 index 05bf9bc..0000000 --- a/example/test/widget_test.dart +++ /dev/null @@ -1,27 +0,0 @@ -// This is a basic Flutter widget test. -// -// To perform an interaction with a widget in your test, use the WidgetTester -// utility that Flutter provides. For example, you can send tap and scroll -// gestures. You can also use WidgetTester to find child widgets in the widget -// tree, read text, and verify that the values of widget properties are correct. - -import 'package:flutter/material.dart'; -import 'package:flutter_test/flutter_test.dart'; - -import 'package:flutter_qjs_example/main.dart'; - -void main() { - testWidgets('Verify Platform version', (WidgetTester tester) async { - // Build our app and trigger a frame. - await tester.pumpWidget(MyApp()); - - // Verify that platform version is retrieved. - expect( - find.byWidgetPredicate( - (Widget widget) => - widget is Text && widget.data.startsWith('Running on:'), - ), - findsOneWidget, - ); - }); -} diff --git a/lib/ffi.dart b/lib/ffi.dart index 15d8773..855afd9 100644 --- a/lib/ffi.dart +++ b/lib/ffi.dart @@ -186,17 +186,6 @@ final Pointer Function( )>>("jsNewCFunction") .asFunction(); -/// JSValue *jsGetGlobalObject(JSContext *ctx) -final Pointer Function( - Pointer ctx, -) jsGetGlobalObject = qjsLib - .lookup< - NativeFunction< - Pointer Function( - Pointer, - )>>("jsGetGlobalObject") - .asFunction(); - /// JSContext *jsNewContext(JSRuntime *rt) final Pointer Function( Pointer rt, diff --git a/lib/flutter_qjs.dart b/lib/flutter_qjs.dart index 2eb86f3..fee656a 100644 --- a/lib/flutter_qjs.dart +++ b/lib/flutter_qjs.dart @@ -13,6 +13,8 @@ import 'package:ffi/ffi.dart'; import 'package:flutter_qjs/ffi.dart'; import 'package:flutter_qjs/wrapper.dart'; +import 'isolate.dart'; + /// Handler function to manage js module. typedef JsModuleHandler = String Function(String name); @@ -44,11 +46,10 @@ class FlutterQjs { this.hostPromiseRejectionHandler, }); - setToGlobalObject(dynamic key, dynamic val) { - _ensureEngine(); - final globalObject = jsGetGlobalObject(_ctx); - definePropertyValue(_ctx, globalObject, key, val); - jsFreeValue(_ctx, globalObject); + static applyFunction(Function func, List args, dynamic thisVal) { + final passThis = + RegExp("{.*thisVal.*}").hasMatch(func.runtimeType.toString()); + return Function.apply(func, args, passThis ? {#thisVal: thisVal} : null); } _ensureEngine() { @@ -68,13 +69,11 @@ class FlutterQjs { ))); } final thisVal = jsToDart(ctx, pdata.elementAt(0).value); - Function func = jsToDart(ctx, pdata.elementAt(3).value); - final passThis = - RegExp("{.*thisVal.*}").hasMatch(func.runtimeType.toString()); - return dartToJs( - ctx, - Function.apply(func, args, passThis ? {#thisVal: thisVal} : null), - ); + final func = jsToDart(ctx, pdata.elementAt(3).value); + final ret = func is QjsInvokable + ? func.invoke(args, thisVal) + : applyFunction(func, args, thisVal); + return dartToJs(ctx, ret); case JSChannelType.MODULE: if (moduleHandler == null) throw Exception("No ModuleHandler"); var ret = Utf8.toUtf8(moduleHandler( diff --git a/lib/isolate.dart b/lib/isolate.dart index 978b187..47eea09 100644 --- a/lib/isolate.dart +++ b/lib/isolate.dart @@ -14,13 +14,13 @@ import 'package:ffi/ffi.dart'; import 'package:flutter_qjs/flutter_qjs.dart'; import 'package:flutter_qjs/wrapper.dart'; -class IsolateJSFunction { +class IsolateJSFunction implements QjsInvokable { int val; int ctx; SendPort port; IsolateJSFunction(this.ctx, this.val, this.port); - Future invoke(List arguments) async { + Future invoke(List arguments, [thisVal]) async { if (0 == val ?? 0) return; var evaluatePort = ReceivePort(); port.send({ @@ -28,9 +28,11 @@ class IsolateJSFunction { 'ctx': ctx, 'val': val, 'args': _encodeData(arguments), + 'this': _encodeData(thisVal), 'port': evaluatePort.sendPort, }); var result = await evaluatePort.first; + evaluatePort.close(); if (result['data'] != null) return _decodeData(result['data'], port); else @@ -39,7 +41,63 @@ class IsolateJSFunction { @override noSuchMethod(Invocation invocation) { - return invoke(invocation.positionalArguments); + return invoke( + invocation.positionalArguments, + invocation.namedArguments[#thisVal], + ); + } +} + +class IsolateFunction implements QjsInvokable { + SendPort _port; + SendPort func; + IsolateFunction(this.func, this._port); + + static IsolateFunction bind(Function func, SendPort port) { + final funcPort = ReceivePort(); + funcPort.listen((msg) async { + var data; + SendPort msgPort = msg['port']; + try { + List args = _decodeData(msg['args'], port); + Map thisVal = _decodeData(msg['this'], port); + data = await FlutterQjs.applyFunction(func, args, thisVal); + if (msgPort != null) + msgPort.send({ + 'data': _encodeData(data), + }); + } catch (e, stack) { + if (msgPort != null) + msgPort.send({ + 'error': e.toString() + "\n" + stack.toString(), + }); + } + }); + return IsolateFunction(funcPort.sendPort, port); + } + + Future invoke(List positionalArguments, [thisVal]) async { + if (func == null) return; + var evaluatePort = ReceivePort(); + func.send({ + 'args': _encodeData(positionalArguments), + 'this': _encodeData(thisVal), + 'port': evaluatePort.sendPort, + }); + var result = await evaluatePort.first; + evaluatePort.close(); + if (result['data'] != null) + return _decodeData(result['data'], _port); + else + throw result['error']; + } + + @override + noSuchMethod(Invocation invocation) { + return invoke( + invocation.positionalArguments, + invocation.namedArguments[#thisVal], + ); } } @@ -75,17 +133,24 @@ dynamic _encodeData(dynamic data, {Map cache}) { '__js_function_val': data.val, }; } + if (data is IsolateFunction) { + return { + '__js_function_port': data.func, + }; + } if (data is Future) { var futurePort = ReceivePort(); data.then((value) { - futurePort.first.then((port) => { - (port as SendPort).send({'data': _encodeData(value)}) - }); + futurePort.first.then((port) { + futurePort.close(); + (port as SendPort).send({'data': _encodeData(value)}); + }); }, onError: (e, stack) { - futurePort.first.then((port) => { - (port as SendPort) - .send({'error': e.toString() + "\n" + stack.toString()}) - }); + futurePort.first.then((port) { + futurePort.close(); + (port as SendPort) + .send({'error': e.toString() + "\n" + stack.toString()}); + }); }); return { '__js_future_port': futurePort.sendPort, @@ -116,12 +181,17 @@ dynamic _decodeData(dynamic data, SendPort port, return JSFunction.fromAddress(ctx, val); } } + if (data.containsKey('__js_function_port')) { + return IsolateFunction(data['__js_function_port'], port); + } if (data.containsKey('__js_future_port')) { SendPort port = data['__js_future_port']; var futurePort = ReceivePort(); port.send(futurePort.sendPort); var futureCompleter = Completer(); + futureCompleter.future.catchError((e) {}); futurePort.first.then((value) { + futurePort.close(); if (value['error'] != null) { futureCompleter.completeError(value['error']); } else { @@ -172,8 +242,7 @@ void _runJsIsolate(Map spawnMessage) async { return ret; }, ); - qjs.dispatch(); - await for (var msg in port) { + port.listen((msg) async { var data; SendPort msgPort = msg['port']; try { @@ -189,10 +258,10 @@ void _runJsIsolate(Map spawnMessage) async { data = JSFunction.fromAddress( msg['ctx'], msg['val'], - ).invoke(_decodeData(msg['args'], null)); - break; - case 'setToGlobalObject': - qjs.setToGlobalObject(msg['key'], msg['val']); + ).invoke( + _decodeData(msg['args'], null), + _decodeData(msg['this'], null), + ); break; case 'close': qjs.port.close(); @@ -210,7 +279,8 @@ void _runJsIsolate(Map spawnMessage) async { 'error': e.toString() + "\n" + stack.toString(), }); } - } + }); + await qjs.dispatch(); } typedef JsAsyncModuleHandler = Future Function(String name); @@ -290,6 +360,12 @@ class IsolateQjs { _sendPort = completer.future; } + /// Create isolate function + Future bind(Function func) async { + _ensureEngine(); + return IsolateFunction.bind(func, await _sendPort); + } + /// Free Runtime and close isolate thread that can be recreate when evaluate again. close() { if (_sendPort == null) return; @@ -301,23 +377,6 @@ class IsolateQjs { _sendPort = null; } - setToGlobalObject(dynamic key, dynamic val) async { - _ensureEngine(); - var evaluatePort = ReceivePort(); - var sendPort = await _sendPort; - sendPort.send({ - 'type': 'setToGlobalObject', - 'key': key, - 'val': val, - 'port': evaluatePort.sendPort, - }); - var result = await evaluatePort.first; - if (result['error'] == null) { - return; - } else - throw result['error']; - } - /// Evaluate js script. Future evaluate(String command, {String name, int evalFlags}) async { _ensureEngine(); @@ -331,6 +390,7 @@ class IsolateQjs { 'port': evaluatePort.sendPort, }); var result = await evaluatePort.first; + evaluatePort.close(); if (result['error'] == null) { return _decodeData(result['data'], sendPort); } else diff --git a/lib/wrapper.dart b/lib/wrapper.dart index 0cceac2..600d327 100644 --- a/lib/wrapper.dart +++ b/lib/wrapper.dart @@ -12,6 +12,7 @@ import 'dart:typed_data'; import 'package:ffi/ffi.dart'; import 'ffi.dart'; +import 'isolate.dart'; class JSRefValue implements JSRef { Pointer val; @@ -37,6 +38,14 @@ class JSRefValue implements JSRef { } } +abstract class QjsReleasable { + void release(); +} + +abstract class QjsInvokable { + dynamic invoke(List positionalArguments, [dynamic thisVal]); +} + class DartObject implements JSRef { Object obj; Pointer ctx; @@ -53,6 +62,7 @@ class DartObject implements JSRef { @override void release() { + if (obj is QjsReleasable) (obj as QjsReleasable).release(); obj = null; ctx = null; } @@ -90,19 +100,19 @@ class JSPromise extends JSRefValue { } } -class JSFunction extends JSRefValue { +class JSFunction extends JSRefValue implements QjsInvokable { JSFunction(Pointer ctx, Pointer val) : super(ctx, val); JSFunction.fromAddress(int ctx, int val) : super.fromAddress(ctx, val); - invoke(List arguments) { + invoke(List arguments, [dynamic thisVal]) { if (val == null) return; List args = arguments .map( (e) => dartToJs(ctx, e), ) .toList(); - Pointer jsRet = jsCall(ctx, val, null, args); + Pointer jsRet = jsCall(ctx, val, dartToJs(ctx, thisVal), args); for (Pointer jsArg in args) { jsFreeValue(ctx, jsArg); } @@ -118,7 +128,10 @@ class JSFunction extends JSRefValue { @override noSuchMethod(Invocation invocation) { - return invoke(invocation.positionalArguments); + return invoke( + invocation.positionalArguments, + invocation.namedArguments[#thisVal], + ); } } @@ -228,7 +241,7 @@ Pointer dartToJs(Pointer ctx, dynamic val, {Map cache}) { dartObjectClassId, identityHashCode(DartObject(ctx, val)), ); - if (val is Function) { + if (val is Function || val is IsolateFunction) { final ret = jsNewCFunction(ctx, dartObject); jsFreeValue(ctx, dartObject); return ret; @@ -343,6 +356,7 @@ Pointer jsNewContextWithPromsieWrapper(Pointer rt) { jsFreeValue(ctx, jsPromiseWrapper); runtimeOpaque.promiseToFuture = (promise) { var completer = Completer(); + completer.future.catchError((e) {}); var wrapper = promiseWrapper.val; if (wrapper == null) completer.completeError(Exception("Runtime has been released!")); diff --git a/pubspec.yaml b/pubspec.yaml index 5fedda0..7f636c7 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,6 +1,6 @@ name: flutter_qjs description: This plugin is a simple js engine for flutter using the `quickjs` project. Plugin currently supports all the platforms except web! -version: 0.2.7 +version: 0.3.0 homepage: https://github.com/ekibun/flutter_qjs environment: diff --git a/test/flutter_qjs_test.dart b/test/flutter_qjs_test.dart index bd62b18..9170172 100644 --- a/test/flutter_qjs_test.dart +++ b/test/flutter_qjs_test.dart @@ -14,32 +14,42 @@ import 'package:flutter_qjs/flutter_qjs.dart'; import 'package:flutter_qjs/isolate.dart'; import 'package:flutter_test/flutter_test.dart'; -dynamic myMethodHandler(List args, {String thisVal}) { - return [thisVal, ...args]; +dynamic myFunction(String args, {String thisVal}) { + return [thisVal, args]; } Future testEvaluate(qjs) async { - final value = await qjs.evaluate(""" - const a = {}; - a.a = a; - import('test').then((module) => channel.call('this', [ - (...args)=>`hello \${args}!`, a, - Promise.reject('test Promise.reject'), Promise.resolve('test Promise.resolve'), - 0.1, true, false, 1, "world", module - ])); - """, name: ""); - expect(value[0], 'this', reason: "js function this"); - expect(await value[1]('world'), 'hello world!', reason: "js function call"); - expect(value[2]['a'], value[2], reason: "recursive object"); - expect(value[3], isInstanceOf(), reason: "promise object"); + final testWrap = await qjs.evaluate("(a) => a", name: ""); + final primities = [0, 1, 0.1, true, false, "str"]; + final wrapPrimities = await testWrap(primities); + for (var i = 0; i < primities.length; i++) { + expect(wrapPrimities[i], primities[i], reason: "wrap primities"); + } + final a = {}; + a["a"] = a; + final wrapA = await testWrap(a); + expect(wrapA['a'], wrapA, reason: "recursive object"); + final testThis = await qjs.evaluate( + "(func) => func.call('this', 'arg')", + name: "", + ); + final funcRet = await testThis(myFunction); + expect(funcRet[0], 'this', reason: "js function this"); + expect(funcRet[1], 'arg', reason: "js function argument"); + final promises = await testWrap(await qjs.evaluate( + "[Promise.reject('test Promise.reject'), Promise.resolve('test Promise.resolve')]", + name: "", + )); + for (final promise in promises) + expect(promise, isInstanceOf(), reason: "promise object"); try { - await value[3]; + await promises[0]; throw 'Future not reject'; } catch (e) { expect(e, startsWith('test Promise.reject\n'), reason: "promise object reject"); } - expect(await value[4], 'test Promise.resolve', + expect(await promises[1], 'test Promise.resolve', reason: "promise object resolve"); } @@ -95,41 +105,37 @@ void main() async { expect(result['default']['data'], 'test module', reason: "eval module"); qjs.close(); }); - test('jsToDart', () async { + test('data conversion', () async { final qjs = FlutterQjs( moduleHandler: (name) { return "export default '${new DateTime.now()}'"; }, hostPromiseRejectionHandler: (_) {}, ); - qjs.setToGlobalObject("channel", myMethodHandler); qjs.dispatch(); await testEvaluate(qjs); qjs.close(); }); - test('isolate', () async { - await runZonedGuarded(() async { - final qjs = IsolateQjs( - moduleHandler: (name) async { - return "export default '${new DateTime.now()}'"; - }, - hostPromiseRejectionHandler: (_) {}, - ); - await qjs.setToGlobalObject("channel", myMethodHandler); - await testEvaluate(qjs); - qjs.close(); - }, (e, stack) { - if (!e.toString().startsWith("test Promise.reject")) throw e; - }); + test('isolate conversion', () async { + final qjs = IsolateQjs( + moduleHandler: (name) async { + return "export default '${new DateTime.now()}'"; + }, + hostPromiseRejectionHandler: (_) {}, + ); + await testEvaluate(qjs); + qjs.close(); }); - test('dart object', () async { - final qjs = FlutterQjs(); - qjs.setToGlobalObject("channel", () { - return FlutterQjs(); - }); - qjs.dispatch(); - var value = await qjs.evaluate("channel()", name: ""); - expect(value, isInstanceOf(), reason: "dart object"); + test('isolate bind function', () async { + final qjs = IsolateQjs(); + var localVar; + final testFunc = await qjs.evaluate("(func)=>func('ret')", name: ""); + final testFuncRet = await testFunc(await qjs.bind((args) { + localVar = 'test'; + return args; + })); + expect(localVar, 'test', reason: "bind function"); + expect(testFuncRet, 'ret', reason: "bind function args return"); qjs.close(); }); test('stack overflow', () async {