diff --git a/README.md b/README.md index ed6a242..bc0cd1a 100644 --- a/README.md +++ b/README.md @@ -35,11 +35,15 @@ try { } ``` -Method `close` can destroy quickjs runtime that can be recreated again if you call `evaluate`. Parameter `port` should be close to stop `dispatch` loop when you do not need it. +Method `close` can destroy quickjs runtime that can be recreated again if you call `evaluate`. Parameter `port` should be close to stop `dispatch` loop when you do not need it. ```dart -engine.port.close(); // stop dispatch loop -engine.close(); // close engine +try { + engine.port.close(); // stop dispatch loop + engine.close(); // close engine +} on JSError catch(e) { + print(e); // catch reference leak exception +} engine = null; ``` @@ -72,6 +76,13 @@ or use `invoke` method to pass list parameters: (func as JSInvokable).invoke([arg1, arg2], thisVal); ``` +`JSInvokable` returned by evaluation may increase reference of JS object. +You should manually call `release` to release JS reference: + +```dart +(func as JSInvokable).release(); +``` + ### Use modules ES6 module with `import` function is supported and can be managed in dart with `moduleHandler`: diff --git a/example/lib/main.dart b/example/lib/main.dart index e270b7a..2caa884 100644 --- a/example/lib/main.dart +++ b/example/lib/main.dart @@ -8,7 +8,7 @@ import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; -import 'package:flutter_qjs/isolate.dart'; +import 'package:flutter_qjs/flutter_qjs.dart'; import 'highlight.dart'; diff --git a/lib/flutter_qjs.dart b/lib/flutter_qjs.dart index cb2513b..f99a917 100644 --- a/lib/flutter_qjs.dart +++ b/lib/flutter_qjs.dart @@ -1,159 +1,13 @@ -/* - * @Description: quickjs engine - * @Author: ekibun - * @Date: 2020-08-08 08:29:09 - * @LastEditors: ekibun - * @LastEditTime: 2020-10-06 23:47:13 - */ import 'dart:async'; import 'dart:ffi'; +import 'dart:io'; import 'dart:isolate'; - +import 'dart:typed_data'; import 'package:ffi/ffi.dart'; -import 'ffi.dart'; -import 'wrapper.dart'; +import 'src/ffi.dart'; +export 'src/ffi.dart' show JSEvalFlag; -/// Handler function to manage js module. -typedef JsModuleHandler = String Function(String name); - -/// Handler to manage unhandled promise rejection. -typedef JsHostPromiseRejectionHandler = void Function(String reason); - -/// Quickjs engine for flutter. -class FlutterQjs { - Pointer _rt; - Pointer _ctx; - - /// Max stack size for quickjs. - final int stackSize; - - /// Message Port for event loop. Close it to stop dispatching event loop. - ReceivePort port = ReceivePort(); - - /// Handler function to manage js module. - JsModuleHandler moduleHandler; - - /// Handler function to manage js module. - JsHostPromiseRejectionHandler hostPromiseRejectionHandler; - - FlutterQjs({ - this.moduleHandler, - this.stackSize, - this.hostPromiseRejectionHandler, - }); - - _ensureEngine() { - if (_rt != null) return; - _rt = jsNewRuntime((ctx, type, ptr) { - try { - switch (type) { - case JSChannelType.METHON: - final pdata = ptr.cast(); - final argc = pdata.elementAt(1).value.cast().value; - List pargs = []; - for (var i = 0; i < argc; i++) { - pargs.add(jsToDart( - ctx, - Pointer.fromAddress( - pdata.elementAt(2).value.address + sizeOfJSValue * i, - ), - )); - } - JSInvokable func = jsToDart(ctx, pdata.elementAt(3).value); - return dartToJs( - ctx, - func.invoke( - pargs, - jsToDart(ctx, pdata.elementAt(0).value), - )); - case JSChannelType.MODULE: - if (moduleHandler == null) throw JSError('No ModuleHandler'); - var ret = Utf8.toUtf8(moduleHandler( - Utf8.fromUtf8(ptr.cast()), - )); - Future.microtask(() { - free(ret); - }); - return ret; - case JSChannelType.PROMISE_TRACK: - final errStr = parseJSException(ctx, ptr); - if (hostPromiseRejectionHandler != null) { - hostPromiseRejectionHandler(errStr.toString()); - } else { - print('unhandled promise rejection: $errStr'); - } - return Pointer.fromAddress(0); - case JSChannelType.FREE_OBJECT: - Pointer rt = ctx; - DartObject obj = DartObject.fromAddress(rt, ptr.address); - obj?.release(); - runtimeOpaques[rt]?.ref?.remove(obj); - return Pointer.fromAddress(0); - } - throw JSError('call channel with wrong type'); - } catch (e) { - if (type == JSChannelType.FREE_OBJECT) { - print('DartObject release error: $e'); - return Pointer.fromAddress(0); - } - if (type == JSChannelType.MODULE) { - print('host Promise Rejection Handler error: $e'); - return Pointer.fromAddress(0); - } - var throwObj = dartToJs(ctx, e); - var err = jsThrow(ctx, throwObj); - jsFreeValue(ctx, throwObj); - if (type == JSChannelType.MODULE) { - jsFreeValue(ctx, err); - return Pointer.fromAddress(0); - } - return err; - } - }, port); - if (this.stackSize != null && this.stackSize > 0) - jsSetMaxStackSize(_rt, this.stackSize); - _ctx = jsNewContext(_rt); - } - - /// Free Runtime and Context which can be recreate when evaluate again. - close() { - if (_rt != null) { - jsFreeContext(_ctx); - jsFreeRuntime(_rt); - } - _rt = null; - _ctx = null; - } - - /// Dispatch JavaScript Event loop. - Future dispatch() async { - await for (var _ in port) { - if (_rt == null) continue; - while (true) { - int err = jsExecutePendingJob(_rt); - if (err <= 0) { - if (err < 0) print(parseJSException(_ctx)); - break; - } - } - } - } - - /// Evaluate js script. - dynamic evaluate(String command, {String name, int evalFlags}) { - _ensureEngine(); - var jsval = jsEval( - _ctx, - command, - name ?? '', - evalFlags ?? JSEvalFlag.GLOBAL, - ); - if (jsIsException(jsval) != 0) { - jsFreeValue(_ctx, jsval); - throw parseJSException(_ctx); - } - var result = jsToDart(_ctx, jsval); - jsFreeValue(_ctx, jsval); - return result; - } -} +part 'src/engine.dart'; +part 'src/isolate.dart'; +part 'src/wrapper.dart'; +part 'src/object.dart'; diff --git a/lib/isolate.dart b/lib/isolate.dart deleted file mode 100644 index 5f481dc..0000000 --- a/lib/isolate.dart +++ /dev/null @@ -1,199 +0,0 @@ -/* - * @Description: - * @Author: ekibun - * @Date: 2020-10-02 13:49:03 - * @LastEditors: ekibun - * @LastEditTime: 2020-10-03 22:21:31 - */ -import 'dart:async'; -import 'dart:ffi'; -import 'dart:io'; -import 'dart:isolate'; - -import 'package:ffi/ffi.dart'; -import 'flutter_qjs.dart'; -import 'wrapper.dart'; - -void _runJsIsolate(Map spawnMessage) async { - SendPort sendPort = spawnMessage['port']; - ReceivePort port = ReceivePort(); - sendPort.send(port.sendPort); - var qjs = FlutterQjs( - stackSize: spawnMessage['stackSize'], - hostPromiseRejectionHandler: (reason) { - sendPort.send({ - 'type': 'hostPromiseRejection', - 'reason': reason, - }); - }, - moduleHandler: (name) { - var ptr = allocate>(); - ptr.value = Pointer.fromAddress(0); - sendPort.send({ - 'type': 'module', - 'name': name, - 'ptr': ptr.address, - }); - while (ptr.value.address == 0) sleep(Duration.zero); - if (ptr.value.address == -1) throw JSError('Module Not found'); - var ret = Utf8.fromUtf8(ptr.value); - sendPort.send({ - 'type': 'release', - 'ptr': ptr.value.address, - }); - free(ptr); - return ret; - }, - ); - port.listen((msg) async { - var data; - SendPort msgPort = msg['port']; - try { - switch (msg['type']) { - case 'evaluate': - data = await qjs.evaluate( - msg['command'], - name: msg['name'], - evalFlags: msg['flag'], - ); - break; - case 'call': - data = await JSFunction.fromAddress( - Pointer.fromAddress(msg['ctx']), - Pointer.fromAddress(msg['val']), - ).invoke( - decodeData(msg['args'], null), - decodeData(msg['this'], null), - ); - break; - case 'close': - qjs.port.close(); - qjs.close(); - port.close(); - break; - } - if (msgPort != null) - msgPort.send({ - 'data': encodeData(data), - }); - } catch (e) { - if (msgPort != null) - msgPort.send({ - 'error': encodeData(e), - }); - } - }); - await qjs.dispatch(); -} - -typedef JsAsyncModuleHandler = Future Function(String name); -typedef JsIsolateSpawn = void Function(SendPort sendPort); - -class IsolateQjs { - Future _sendPort; - - /// Max stack size for quickjs. - final int stackSize; - - /// Asynchronously handler to manage js module. - JsAsyncModuleHandler moduleHandler; - - /// Handler function to manage js module. - JsHostPromiseRejectionHandler hostPromiseRejectionHandler; - - /// Quickjs engine runing on isolate thread. - /// - /// Pass handlers to implement js-dart interaction and resolving modules. The `methodHandler` is - /// used in isolate, so **the handler function must be a top-level function or a static method**. - IsolateQjs({ - this.moduleHandler, - this.stackSize, - this.hostPromiseRejectionHandler, - }); - - _ensureEngine() { - if (_sendPort != null) return; - ReceivePort port = ReceivePort(); - Isolate.spawn( - _runJsIsolate, - { - 'port': port.sendPort, - 'stackSize': stackSize, - }, - errorsAreFatal: true, - ); - var completer = Completer(); - port.listen((msg) async { - if (msg is SendPort && !completer.isCompleted) { - completer.complete(msg); - return; - } - switch (msg['type']) { - case 'hostPromiseRejection': - try { - final errStr = msg['reason']; - if (hostPromiseRejectionHandler != null) { - hostPromiseRejectionHandler(errStr); - } else { - print('unhandled promise rejection: $errStr'); - } - } catch (e) { - print('host Promise Rejection Handler error: $e'); - } - break; - case 'module': - var ptr = Pointer.fromAddress(msg['ptr']); - try { - ptr.value = Utf8.toUtf8(await moduleHandler(msg['name'])); - } catch (e) { - ptr.value = Pointer.fromAddress(-1); - } - break; - case 'release': - free(Pointer.fromAddress(msg['ptr'])); - break; - } - }, onDone: () { - close(); - if (!completer.isCompleted) completer.completeError('isolate close'); - }); - _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; - _sendPort.then((sendPort) { - sendPort.send({ - 'type': 'close', - }); - }); - _sendPort = null; - } - - /// Evaluate js script. - Future evaluate(String command, {String name, int evalFlags}) async { - _ensureEngine(); - var evaluatePort = ReceivePort(); - var sendPort = await _sendPort; - sendPort.send({ - 'type': 'evaluate', - 'command': command, - 'name': name, - 'flag': evalFlags, - 'port': evaluatePort.sendPort, - }); - Map result = await evaluatePort.first; - evaluatePort.close(); - if (result.containsKey('data')) { - return decodeData(result['data'], sendPort); - } else - throw decodeData(result['error'], sendPort); - } -} diff --git a/lib/src/engine.dart b/lib/src/engine.dart new file mode 100644 index 0000000..19dcbdc --- /dev/null +++ b/lib/src/engine.dart @@ -0,0 +1,162 @@ +/* + * @Description: quickjs engine + * @Author: ekibun + * @Date: 2020-08-08 08:29:09 + * @LastEditors: ekibun + * @LastEditTime: 2020-10-06 23:47:13 + */ +part of '../flutter_qjs.dart'; + +/// Handler function to manage js module. +typedef _JsModuleHandler = String Function(String name); + +/// Handler to manage unhandled promise rejection. +typedef _JsHostPromiseRejectionHandler = void Function(dynamic reason); + +/// Quickjs engine for flutter. +class FlutterQjs { + Pointer _rt; + Pointer _ctx; + + /// Max stack size for quickjs. + final int stackSize; + + /// Message Port for event loop. Close it to stop dispatching event loop. + ReceivePort port = ReceivePort(); + + /// Handler function to manage js module. + _JsModuleHandler moduleHandler; + + /// Handler function to manage js module. + _JsHostPromiseRejectionHandler hostPromiseRejectionHandler; + + FlutterQjs({ + this.moduleHandler, + this.stackSize, + this.hostPromiseRejectionHandler, + }); + + _ensureEngine() { + if (_rt != null) return; + _rt = jsNewRuntime((ctx, type, ptr) { + try { + switch (type) { + case JSChannelType.METHON: + final pdata = ptr.cast(); + final argc = pdata.elementAt(1).value.cast().value; + List pargs = []; + for (int i = 0; i < argc; ++i) { + pargs.add(_jsToDart( + ctx, + Pointer.fromAddress( + pdata.elementAt(2).value.address + sizeOfJSValue * i, + ), + )); + } + JSInvokable func = _jsToDart(ctx, pdata.elementAt(3).value); + return _dartToJs( + ctx, + func.invoke( + pargs, + _jsToDart(ctx, pdata.elementAt(0).value), + )); + case JSChannelType.MODULE: + if (moduleHandler == null) throw JSError('No ModuleHandler'); + final ret = Utf8.toUtf8(moduleHandler( + Utf8.fromUtf8(ptr.cast()), + )); + Future.microtask(() { + free(ret); + }); + return ret; + case JSChannelType.PROMISE_TRACK: + final err = _parseJSException(ctx, ptr); + if (hostPromiseRejectionHandler != null) { + hostPromiseRejectionHandler(err); + } else { + print('unhandled promise rejection: $err'); + } + return Pointer.fromAddress(0); + case JSChannelType.FREE_OBJECT: + Pointer rt = ctx; + _DartObject obj = _DartObject.fromAddress(rt, ptr.address); + obj?.release(); + return Pointer.fromAddress(0); + } + throw JSError('call channel with wrong type'); + } catch (e) { + if (type == JSChannelType.FREE_OBJECT) { + print('DartObject release error: $e'); + return Pointer.fromAddress(0); + } + if (type == JSChannelType.MODULE) { + print('host Promise Rejection Handler error: $e'); + return Pointer.fromAddress(0); + } + final throwObj = _dartToJs(ctx, e); + final err = jsThrow(ctx, throwObj); + jsFreeValue(ctx, throwObj); + if (type == JSChannelType.MODULE) { + jsFreeValue(ctx, err); + return Pointer.fromAddress(0); + } + return err; + } + }, port); + if (this.stackSize != null && this.stackSize > 0) + jsSetMaxStackSize(_rt, this.stackSize); + _ctx = jsNewContext(_rt); + } + + /// Free Runtime and Context which can be recreate when evaluate again. + close() { + if (_rt == null) return; + final rt = _rt; + final ctx = _ctx; + _executePendingJob(); + _rt = null; + _ctx = null; + jsFreeContext(ctx); + try { + jsFreeRuntime(rt); + } on String catch (e) { + throw JSError(e); + } + } + + void _executePendingJob() { + if (_rt == null) return; + while (true) { + int err = jsExecutePendingJob(_rt); + if (err <= 0) { + if (err < 0) print(_parseJSException(_ctx)); + break; + } + } + } + + /// Dispatch JavaScript Event loop. + Future dispatch() async { + await for (final _ in port) { + _executePendingJob(); + } + } + + /// Evaluate js script. + dynamic evaluate(String command, {String name, int evalFlags}) { + _ensureEngine(); + final jsval = jsEval( + _ctx, + command, + name ?? '', + evalFlags ?? JSEvalFlag.GLOBAL, + ); + if (jsIsException(jsval) != 0) { + jsFreeValue(_ctx, jsval); + throw _parseJSException(_ctx); + } + final result = _jsToDart(_ctx, jsval); + jsFreeValue(_ctx, jsval); + return result; + } +} diff --git a/lib/ffi.dart b/lib/src/ffi.dart similarity index 89% rename from lib/ffi.dart rename to lib/src/ffi.dart index 44e34c0..1ef3a05 100644 --- a/lib/ffi.dart +++ b/lib/src/ffi.dart @@ -1,5 +1,5 @@ /* - * @Description: + * @Description: ffi * @Author: ekibun * @Date: 2020-09-19 10:29:04 * @LastEditors: ekibun @@ -8,10 +8,10 @@ import 'dart:ffi'; import 'dart:io'; import 'dart:isolate'; - import 'package:ffi/ffi.dart'; abstract class JSRef { + bool leakable = false; void release(); } @@ -90,13 +90,13 @@ final Pointer Function() jsUNDEFINED = _qjsLib .lookup>('jsUNDEFINED') .asFunction(); -typedef JSChannel = Pointer Function(Pointer ctx, int method, Pointer argv); -typedef JSChannelNative = Pointer Function( +typedef _JSChannel = Pointer Function(Pointer ctx, int method, Pointer argv); +typedef _JSChannelNative = Pointer Function( Pointer ctx, IntPtr method, Pointer argv); /// JSRuntime *jsNewRuntime(JSChannel channel) final Pointer Function( - Pointer>, + Pointer>, ) _jsNewRuntime = _qjsLib .lookup< NativeFunction< @@ -105,29 +105,37 @@ final Pointer Function( )>>('jsNewRuntime') .asFunction(); -class RuntimeOpaque { - JSChannel channel; - List ref = []; - ReceivePort port; - int dartObjectClassId; - int jsExceptionClassId; +class _RuntimeOpaque { + _JSChannel _channel; + List _ref = []; + ReceivePort _port; + int _dartObjectClassId; + get dartObjectClassId => _dartObjectClassId; + + void addRef(JSRef ref) => _ref.add(ref); + + bool removeRef(JSRef ref) => _ref.remove(ref); + + JSRef getRef(bool Function(JSRef ref) test) { + return _ref.firstWhere(test, orElse: () => null); + } } -final Map runtimeOpaques = Map(); +final Map runtimeOpaques = Map(); Pointer channelDispacher(Pointer ctx, int type, Pointer argv) { Pointer rt = type == JSChannelType.FREE_OBJECT ? ctx : jsGetRuntime(ctx); - return runtimeOpaques[rt]?.channel(ctx, type, argv); + return runtimeOpaques[rt]?._channel(ctx, type, argv); } Pointer jsNewRuntime( - JSChannel callback, + _JSChannel callback, ReceivePort port, ) { - var rt = _jsNewRuntime(Pointer.fromFunction(channelDispacher)); - runtimeOpaques[rt] = RuntimeOpaque() - ..channel = callback - ..port = port; + final rt = _jsNewRuntime(Pointer.fromFunction(channelDispacher)); + runtimeOpaques[rt] = _RuntimeOpaque() + .._channel = callback + .._port = port; return rt; } @@ -158,12 +166,28 @@ final void Function( void jsFreeRuntime( Pointer rt, ) { - while (0 < runtimeOpaques[rt]?.ref?.length ?? 0) { - final ref = runtimeOpaques[rt]?.ref?.first; + final referenceleak = []; + while (true) { + final ref = runtimeOpaques[rt] + ?._ref + ?.firstWhere((ref) => ref.leakable, orElse: () => null); + if (ref == null) break; ref.release(); - runtimeOpaques[rt]?.ref?.remove(ref); + runtimeOpaques[rt]?._ref?.remove(ref); + } + while (0 < runtimeOpaques[rt]?._ref?.length ?? 0) { + final ref = runtimeOpaques[rt]?._ref?.first; + assert(!ref.leakable); + referenceleak.add( + " ${identityHashCode(ref)}\t${ref.runtimeType.toString()}\t${ref.toString().replaceAll('\n', '\\n')}"); + ref.release(); + runtimeOpaques[rt]?._ref?.remove(ref); } _jsFreeRuntime(rt); + if (referenceleak.length > 0) { + throw ('reference leak:\n ADDR\t TYPE \t PROP\n' + + referenceleak.join('\n')); + } } /// JSValue *jsNewCFunction(JSContext *ctx, JSValue *funcData) @@ -191,11 +215,10 @@ final Pointer Function( .asFunction(); Pointer jsNewContext(Pointer rt) { - var ctx = _jsNewContext(rt); + final ctx = _jsNewContext(rt); final runtimeOpaque = runtimeOpaques[rt]; if (runtimeOpaque == null) throw Exception('Runtime has been released!'); - runtimeOpaque.dartObjectClassId = jsNewClass(ctx, 'DartObject'); - runtimeOpaque.jsExceptionClassId = jsNewClass(ctx, 'JSException'); + runtimeOpaque._dartObjectClassId = jsNewClass(ctx, 'DartObject'); return ctx; } @@ -246,9 +269,9 @@ Pointer jsEval( String filename, int evalFlags, ) { - var utf8input = Utf8.toUtf8(input); - var utf8filename = Utf8.toUtf8(filename); - var val = _jsEval( + final utf8input = Utf8.toUtf8(input); + final utf8filename = Utf8.toUtf8(filename); + final val = _jsEval( ctx, utf8input, Utf8.strlen(utf8input), @@ -257,7 +280,7 @@ Pointer jsEval( ); free(utf8input); free(utf8filename); - runtimeOpaques[jsGetRuntime(ctx)].port.sendPort.send('eval'); + runtimeOpaques[jsGetRuntime(ctx)]._port.sendPort.send(#eval); return val; } @@ -350,8 +373,10 @@ Pointer jsNewString( Pointer ctx, String str, ) { - var utf8str = Utf8.toUtf8(str); - return _jsNewString(ctx, utf8str); + final utf8str = Utf8.toUtf8(str); + final jsStr = _jsNewString(ctx, utf8str); + free(utf8str); + return jsStr; } /// JSValue *jsNewArrayBufferCopy(JSContext *ctx, const uint8_t *buf, size_t len) @@ -532,9 +557,9 @@ String jsToCString( Pointer ctx, Pointer val, ) { - var ptr = _jsToCString(ctx, val); + final ptr = _jsToCString(ctx, val); if (ptr.address == 0) throw Exception('JSValue cannot convert to string'); - var str = Utf8.fromUtf8(ptr); + final str = Utf8.fromUtf8(ptr); jsFreeCString(ctx, ptr); return str; } @@ -556,8 +581,8 @@ int jsNewClass( Pointer ctx, String name, ) { - var utf8name = Utf8.toUtf8(name); - var val = _jsNewClass( + final utf8name = Utf8.toUtf8(name); + final val = _jsNewClass( ctx, utf8name, ); @@ -842,7 +867,7 @@ Pointer jsCall( } jsFreeValue(ctx, func1); free(jsArgs); - runtimeOpaques[jsGetRuntime(ctx)].port.sendPort.send('call'); + runtimeOpaques[jsGetRuntime(ctx)]._port.sendPort.send(#call); return jsRet; } diff --git a/lib/src/isolate.dart b/lib/src/isolate.dart new file mode 100644 index 0000000..5428f1a --- /dev/null +++ b/lib/src/isolate.dart @@ -0,0 +1,301 @@ +/* + * @Description: isolate + * @Author: ekibun + * @Date: 2020-10-02 13:49:03 + * @LastEditors: ekibun + * @LastEditTime: 2020-10-03 22:21:31 + */ +part of '../flutter_qjs.dart'; + +typedef dynamic _Decode(Map obj, SendPort port); +List<_Decode> _decoders = [ + JSError._decode, + _IsolateJSFunction._decode, + _IsolateFunction._decode, + _JSFunction._decode, +]; + +abstract class _IsolateEncodable { + Map _encode(); +} + +dynamic _encodeData(dynamic data, {Map cache}) { + if (cache == null) cache = Map(); + if (data is _IsolateEncodable) return data._encode(); + if (cache.containsKey(data)) return cache[data]; + if (data is List) { + final ret = []; + cache[data] = ret; + for (int i = 0; i < data.length; ++i) { + ret.add(_encodeData(data[i], cache: cache)); + } + return ret; + } + if (data is Map) { + final ret = {}; + cache[data] = ret; + for (final entry in data.entries) { + ret[_encodeData(entry.key, cache: cache)] = + _encodeData(entry.value, cache: cache); + } + return ret; + } + if (data is Future) { + final futurePort = ReceivePort(); + data.then((value) { + futurePort.first.then((port) { + futurePort.close(); + (port as SendPort).send(_encodeData(value)); + }); + }, onError: (e) { + futurePort.first.then((port) { + futurePort.close(); + (port as SendPort).send({#error: _encodeData(e)}); + }); + }); + return { + #jsFuturePort: futurePort.sendPort, + }; + } + return data; +} + +dynamic _decodeData(dynamic data, SendPort port, + {Map cache}) { + if (cache == null) cache = Map(); + if (cache.containsKey(data)) return cache[data]; + if (data is List) { + final ret = []; + cache[data] = ret; + for (int i = 0; i < data.length; ++i) { + ret.add(_decodeData(data[i], port, cache: cache)); + } + return ret; + } + if (data is Map) { + for (final decoder in _decoders) { + final decodeObj = decoder(data, port); + if (decodeObj != null) return decodeObj; + } + if (data.containsKey(#jsFuturePort)) { + SendPort port = data[#jsFuturePort]; + final futurePort = ReceivePort(); + port.send(futurePort.sendPort); + final futureCompleter = Completer(); + futureCompleter.future.catchError((e) {}); + futurePort.first.then((value) { + futurePort.close(); + if (value is Map && value.containsKey(#error)) { + futureCompleter.completeError(_decodeData(value[#error], port)); + } else { + futureCompleter.complete(_decodeData(value, port)); + } + }); + return futureCompleter.future; + } + final ret = {}; + cache[data] = ret; + for (final entry in data.entries) { + ret[_decodeData(entry.key, port, cache: cache)] = + _decodeData(entry.value, port, cache: cache); + } + return ret; + } + return data; +} + +void _runJsIsolate(Map spawnMessage) async { + SendPort sendPort = spawnMessage[#port]; + ReceivePort port = ReceivePort(); + sendPort.send(port.sendPort); + final qjs = FlutterQjs( + stackSize: spawnMessage[#stackSize], + hostPromiseRejectionHandler: (reason) { + sendPort.send({ + #type: #hostPromiseRejection, + #reason: _encodeData(reason), + }); + }, + moduleHandler: (name) { + final ptr = allocate>(); + ptr.value = Pointer.fromAddress(0); + sendPort.send({ + #type: #module, + #name: name, + #ptr: ptr.address, + }); + while (ptr.value.address == 0) sleep(Duration.zero); + if (ptr.value.address == -1) throw JSError('Module Not found'); + final ret = Utf8.fromUtf8(ptr.value); + sendPort.send({ + #type: #release, + #ptr: ptr.value.address, + }); + free(ptr); + return ret; + }, + ); + port.listen((msg) async { + var data; + SendPort msgPort = msg[#port]; + try { + switch (msg[#type]) { + case #evaluate: + data = await qjs.evaluate( + msg[#command], + name: msg[#name], + evalFlags: msg[#flag], + ); + break; + case #call: + data = await _JSFunction.fromAddress( + Pointer.fromAddress(msg[#ctx]), + Pointer.fromAddress(msg[#val]), + ).invoke( + _decodeData(msg[#args], null), + _decodeData(msg[#thisVal], null), + ); + break; + case #closeFunction: + _JSFunction.fromAddress( + Pointer.fromAddress(msg[#ctx]), + Pointer.fromAddress(msg[#val]), + ).release(); + break; + case #close: + data = false; + qjs.port.close(); + qjs.close(); + port.close(); + data = true; + break; + } + if (msgPort != null) msgPort.send(_encodeData(data)); + } catch (e) { + if (msgPort != null) + msgPort.send({ + #error: _encodeData(e), + }); + } + }); + await qjs.dispatch(); +} + +typedef _JsAsyncModuleHandler = Future Function(String name); + +class IsolateQjs { + Future _sendPort; + + /// Max stack size for quickjs. + final int stackSize; + + /// Asynchronously handler to manage js module. + _JsAsyncModuleHandler moduleHandler; + + /// Handler function to manage js module. + _JsHostPromiseRejectionHandler hostPromiseRejectionHandler; + + /// Quickjs engine runing on isolate thread. + /// + /// Pass handlers to implement js-dart interaction and resolving modules. The `methodHandler` is + /// used in isolate, so **the handler function must be a top-level function or a static method**. + IsolateQjs({ + this.moduleHandler, + this.stackSize, + this.hostPromiseRejectionHandler, + }); + + _ensureEngine() { + if (_sendPort != null) return; + ReceivePort port = ReceivePort(); + Isolate.spawn( + _runJsIsolate, + { + #port: port.sendPort, + #stackSize: stackSize, + }, + errorsAreFatal: true, + ); + final completer = Completer(); + port.listen((msg) async { + if (msg is SendPort && !completer.isCompleted) { + completer.complete(msg); + return; + } + switch (msg[#type]) { + case #hostPromiseRejection: + try { + final err = _decodeData(msg[#reason], port.sendPort); + if (hostPromiseRejectionHandler != null) { + hostPromiseRejectionHandler(err); + } else { + print('unhandled promise rejection: $err'); + } + } catch (e) { + print('host Promise Rejection Handler error: $e'); + } + break; + case #module: + final ptr = Pointer.fromAddress(msg[#ptr]); + try { + ptr.value = Utf8.toUtf8(await moduleHandler(msg[#name])); + } catch (e) { + ptr.value = Pointer.fromAddress(-1); + } + break; + case #release: + free(Pointer.fromAddress(msg[#ptr])); + break; + } + }, onDone: () { + close(); + if (!completer.isCompleted) + completer.completeError(JSError('isolate close')); + }); + _sendPort = completer.future; + } + + /// Create isolate function + Future<_IsolateFunction> 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; + final ret = _sendPort.then((sendPort) async { + final closePort = ReceivePort(); + sendPort.send({ + #type: #close, + #port: closePort.sendPort, + }); + final result = await closePort.first; + closePort.close(); + if (result is Map && result.containsKey(#error)) + throw _decodeData(result[#error], sendPort); + return _decodeData(result, sendPort); + }); + _sendPort = null; + return ret; + } + + /// Evaluate js script. + Future evaluate(String command, {String name, int evalFlags}) async { + _ensureEngine(); + final evaluatePort = ReceivePort(); + final sendPort = await _sendPort; + sendPort.send({ + #type: #evaluate, + #command: command, + #name: name, + #flag: evalFlags, + #port: evaluatePort.sendPort, + }); + final result = await evaluatePort.first; + evaluatePort.close(); + if (result is Map && result.containsKey(#error)) + throw _decodeData(result[#error], sendPort); + return _decodeData(result, sendPort); + } +} diff --git a/lib/src/object.dart b/lib/src/object.dart new file mode 100644 index 0000000..fbcb44e --- /dev/null +++ b/lib/src/object.dart @@ -0,0 +1,342 @@ +/* + * @Description: wrap object + * @Author: ekibun + * @Date: 2020-10-02 13:49:03 + * @LastEditors: ekibun + * @LastEditTime: 2020-10-03 22:21:31 + */ +part of '../flutter_qjs.dart'; + +/// js invokable +abstract class JSInvokable extends JSReleasable { + dynamic invoke(List args, [dynamic thisVal]); + + static dynamic _wrap(dynamic func) { + return func is JSInvokable + ? func + : func is Function + ? _DartFunction(func) + : func; + } + + @override + noSuchMethod(Invocation invocation) { + return invoke( + invocation.positionalArguments, + invocation.namedArguments[#thisVal], + ); + } +} + +class _DartFunction extends JSInvokable { + final Function _func; + _DartFunction(this._func); + + @override + invoke(List args, [thisVal]) { + /// wrap this into function + final passThis = + RegExp('{.*thisVal.*}').hasMatch(_func.runtimeType.toString()); + return Function.apply(_func, args, passThis ? {#thisVal: thisVal} : null); + } + + @override + String toString() { + return _func.toString(); + } + + @override + release() {} +} + +/// implement this to capture js object release. +abstract class JSReleasable { + void release(); +} + +class _DartObject extends JSRef { + @override + bool leakable = true; + + Object _obj; + Pointer _ctx; + _DartObject(this._ctx, this._obj) { + runtimeOpaques[jsGetRuntime(_ctx)]?.addRef(this); + } + + static _DartObject fromAddress(Pointer rt, int val) { + return runtimeOpaques[rt]?.getRef((e) => identityHashCode(e) == val); + } + + @override + String toString() { + if (_ctx == null) return "DartObject()"; + return _obj.toString(); + } + + @override + void release() { + if (_ctx == null) return; + runtimeOpaques[jsGetRuntime(_ctx)]?.removeRef(this); + _ctx = null; + if (_obj is JSReleasable) { + (_obj as JSReleasable).release(); + } + _obj = null; + } +} + +/// JS Error wrapper +class JSError extends _IsolateEncodable { + String message; + String stack; + JSError(message, [stack]) { + if (message is JSError) { + this.message = message.message; + this.stack = message.stack; + } else { + this.message = message.toString(); + this.stack = (stack ?? StackTrace.current).toString(); + } + } + + @override + String toString() { + return stack == null ? message.toString() : "$message\n$stack"; + } + + static JSError _decode(Map obj, SendPort port) { + if (obj.containsKey(#jsError)) + return JSError(obj[#jsError], obj[#jsErrorStack]); + return null; + } + + @override + Map _encode() { + return { + #jsError: message, + #jsErrorStack: stack, + }; + } +} + +/// JS Object reference +/// call [release] to release js object. +class _JSObject extends JSRef { + Pointer _val; + Pointer _ctx; + + /// Create + _JSObject(this._ctx, Pointer _val) { + Pointer rt = jsGetRuntime(_ctx); + this._val = jsDupValue(_ctx, _val); + runtimeOpaques[rt]?.addRef(this); + } + + static _JSObject fromAddress(Pointer ctx, Pointer val) { + Pointer rt = jsGetRuntime(ctx); + return runtimeOpaques[rt]?.getRef((e) => + e is _JSObject && + e._val.address == val.address && + e._ctx.address == ctx.address); + } + + @override + void release() { + if (_val == null) return; + Pointer rt = jsGetRuntime(_ctx); + runtimeOpaques[rt]?.removeRef(this); + jsFreeValue(_ctx, _val); + _val = null; + _ctx = null; + } + + @override + String toString() { + if (_val == null) return "JSObject(released)"; + return jsToCString(_ctx, _val); + } +} + +/// JS function wrapper +class _JSFunction extends _JSObject implements JSInvokable, _IsolateEncodable { + _JSFunction(Pointer ctx, Pointer val) : super(ctx, val); + + static _JSFunction fromAddress(Pointer ctx, Pointer val) { + return _JSObject.fromAddress(ctx, val); + } + + @override + invoke(List arguments, [dynamic thisVal]) { + Pointer jsRet = _invoke(arguments, thisVal); + if (jsRet == null) return; + bool isException = jsIsException(jsRet) != 0; + if (isException) { + jsFreeValue(_ctx, jsRet); + throw _parseJSException(_ctx); + } + final ret = _jsToDart(_ctx, jsRet); + jsFreeValue(_ctx, jsRet); + return ret; + } + + Pointer _invoke(List arguments, [dynamic thisVal]) { + if (_val == null) return null; + List args = arguments + .map( + (e) => _dartToJs(_ctx, e), + ) + .toList(); + Pointer jsThis = _dartToJs(_ctx, thisVal); + Pointer jsRet = jsCall(_ctx, _val, jsThis, args); + jsFreeValue(_ctx, jsThis); + for (Pointer jsArg in args) { + jsFreeValue(_ctx, jsArg); + } + return jsRet; + } + + static _JSFunction _decode(Map obj, SendPort port) { + if (obj.containsKey(#jsFunction) && port == null) + return _JSFunction.fromAddress( + Pointer.fromAddress(obj[#jsFunctionCtx]), + Pointer.fromAddress(obj[#jsFunction]), + ); + return null; + } + + @override + Map _encode() { + return { + #jsFunction: _val.address, + #jsFunctionCtx: _ctx.address, + }; + } + + @override + noSuchMethod(Invocation invocation) { + return invoke( + invocation.positionalArguments, + invocation.namedArguments[#thisVal], + ); + } +} + +/// JS function wrapper for isolate +class _IsolateJSFunction extends JSInvokable implements _IsolateEncodable { + int _val; + int _ctx; + SendPort _port; + _IsolateJSFunction(this._ctx, this._val, this._port); + + @override + Future invoke(List arguments, [thisVal]) async { + if (0 == _val ?? 0) return; + final evaluatePort = ReceivePort(); + _port.send({ + #type: #call, + #ctx: _ctx, + #val: _val, + #args: _encodeData(arguments), + #thisVal: _encodeData(thisVal), + #port: evaluatePort.sendPort, + }); + final result = await evaluatePort.first; + evaluatePort.close(); + if (result is Map && result.containsKey(#error)) + throw _decodeData(result[#error], _port); + return _decodeData(result, _port); + } + + static _IsolateJSFunction _decode(Map obj, SendPort port) { + if (obj.containsKey(#jsFunction) && port != null) + return _IsolateJSFunction(obj[#jsFunctionCtx], obj[#jsFunction], port); + return null; + } + + @override + Map _encode() { + return { + #jsFunction: _val, + #jsFunctionCtx: _ctx, + }; + } + + @override + void release() { + if (_port == null) return; + _port.send({ + #type: #closeFunction, + #ctx: _ctx, + #val: _val, + }); + _port = null; + _val = null; + _ctx = null; + } +} + +/// Dart function wrapper for isolate +class _IsolateFunction extends JSInvokable + implements JSReleasable, _IsolateEncodable { + SendPort _port; + SendPort _func; + _IsolateFunction(this._func, this._port); + + static _IsolateFunction _bind(Function func, SendPort port) { + final JSInvokable invokable = JSInvokable._wrap(func); + final funcPort = ReceivePort(); + funcPort.listen((msg) async { + if (msg == #close) return funcPort.close(); + SendPort msgPort = msg[#port]; + try { + List args = _decodeData(msg[#args], port); + Map thisVal = _decodeData(msg[#thisVal], port); + final data = await invokable.invoke(args, thisVal); + if (msgPort != null) msgPort.send(_encodeData(data)); + } catch (e) { + if (msgPort != null) + msgPort.send({ + #error: _encodeData(e), + }); + } + }); + return _IsolateFunction(funcPort.sendPort, port); + } + + @override + Future invoke(List positionalArguments, [thisVal]) async { + if (_func == null) return; + final evaluatePort = ReceivePort(); + _func.send({ + #args: _encodeData(positionalArguments), + #thisVal: _encodeData(thisVal), + #port: evaluatePort.sendPort, + }); + final result = await evaluatePort.first; + evaluatePort.close(); + if (result is Map && result.containsKey(#error)) + throw _decodeData(result[#error], _port); + return _decodeData(result, _port); + } + + static _IsolateFunction _decode(Map obj, SendPort port) { + if (obj.containsKey(#jsFunctionPort)) + return _IsolateFunction(obj[#jsFunctionPort], port); + return null; + } + + @override + Map _encode() { + return { + #jsFunctionPort: _func, + }; + } + + @override + void release() { + if (_func == null) return; + _func.send(#close); + _func = null; + } +} diff --git a/lib/src/wrapper.dart b/lib/src/wrapper.dart new file mode 100644 index 0000000..955ea1b --- /dev/null +++ b/lib/src/wrapper.dart @@ -0,0 +1,235 @@ +/* + * @Description: wrapper + * @Author: ekibun + * @Date: 2020-09-19 22:07:47 + * @LastEditors: ekibun + * @LastEditTime: 2020-12-02 11:14:03 + */ +part of '../flutter_qjs.dart'; + +dynamic _parseJSException(Pointer ctx, [Pointer perr]) { + final e = perr ?? jsGetException(ctx); + var err; + try { + err = _jsToDart(ctx, e); + } catch (exception) { + err = exception; + } + if (perr == null) jsFreeValue(ctx, e); + return err; +} + +void _definePropertyValue( + Pointer ctx, + Pointer obj, + dynamic key, + dynamic val, { + Map cache, +}) { + final jsAtomVal = _dartToJs(ctx, key, cache: cache); + final jsAtom = jsValueToAtom(ctx, jsAtomVal); + jsDefinePropertyValue( + ctx, + obj, + jsAtom, + _dartToJs(ctx, val, cache: cache), + JSProp.C_W_E, + ); + jsFreeAtom(ctx, jsAtom); + jsFreeValue(ctx, jsAtomVal); +} + +Pointer _jsGetPropertyValue( + Pointer ctx, + Pointer obj, + dynamic key, { + Map cache, +}) { + final jsAtomVal = _dartToJs(ctx, key, cache: cache); + final jsAtom = jsValueToAtom(ctx, jsAtomVal); + final jsProp = jsGetProperty(ctx, obj, jsAtom); + jsFreeAtom(ctx, jsAtom); + jsFreeValue(ctx, jsAtomVal); + return jsProp; +} + +Pointer _dartToJs(Pointer ctx, dynamic val, {Map cache}) { + if (val == null) return jsUNDEFINED(); + if (val is JSError) { + final ret = jsNewError(ctx); + _definePropertyValue(ctx, ret, "name", ""); + _definePropertyValue(ctx, ret, "message", val.message); + _definePropertyValue(ctx, ret, "stack", val.stack); + return ret; + } + if (val is _JSObject) return jsDupValue(ctx, val._val); + if (val is Future) { + final resolvingFunc = allocate(count: sizeOfJSValue * 2); + final resolvingFunc2 = + Pointer.fromAddress(resolvingFunc.address + sizeOfJSValue); + final ret = jsNewPromiseCapability(ctx, resolvingFunc); + _JSFunction res = _jsToDart(ctx, resolvingFunc)..leakable = true; + _JSFunction rej = _jsToDart(ctx, resolvingFunc2)..leakable = true; + jsFreeValue(ctx, resolvingFunc, free: false); + jsFreeValue(ctx, resolvingFunc2, free: false); + free(resolvingFunc); + val.then((value) { + res.invoke([value]); + }, onError: (e) { + rej.invoke([e]); + }).whenComplete(() { + res.release(); + rej.release(); + }); + return ret; + } + if (cache == null) cache = Map(); + if (val is bool) return jsNewBool(ctx, val ? 1 : 0); + if (val is int) return jsNewInt64(ctx, val); + if (val is double) return jsNewFloat64(ctx, val); + if (val is String) return jsNewString(ctx, val); + if (val is Uint8List) { + final ptr = allocate(count: val.length); + final byteList = ptr.asTypedList(val.length); + byteList.setAll(0, val); + final ret = jsNewArrayBufferCopy(ctx, ptr, val.length); + free(ptr); + return ret; + } + if (cache.containsKey(val)) { + return jsDupValue(ctx, cache[val]); + } + if (val is List) { + Pointer ret = jsNewArray(ctx); + cache[val] = ret; + for (int i = 0; i < val.length; ++i) { + _definePropertyValue(ctx, ret, i, val[i], cache: cache); + } + return ret; + } + if (val is Map) { + Pointer ret = jsNewObject(ctx); + cache[val] = ret; + for (MapEntry entry in val.entries) { + _definePropertyValue(ctx, ret, entry.key, entry.value, cache: cache); + } + return ret; + } + // wrap Function to JSInvokable + final valWrap = JSInvokable._wrap(val); + int dartObjectClassId = + runtimeOpaques[jsGetRuntime(ctx)]?.dartObjectClassId ?? 0; + if (dartObjectClassId == 0) return jsUNDEFINED(); + final dartObject = jsNewObjectClass( + ctx, + dartObjectClassId, + identityHashCode(_DartObject(ctx, valWrap)), + ); + if (valWrap is JSInvokable) { + final ret = jsNewCFunction(ctx, dartObject); + jsFreeValue(ctx, dartObject); + return ret; + } + return dartObject; +} + +dynamic _jsToDart(Pointer ctx, Pointer val, {Map cache}) { + if (cache == null) cache = Map(); + int tag = jsValueGetTag(val); + if (jsTagIsFloat64(tag) != 0) { + return jsToFloat64(ctx, val); + } + switch (tag) { + case JSTag.BOOL: + return jsToBool(ctx, val) != 0; + case JSTag.INT: + return jsToInt64(ctx, val); + case JSTag.STRING: + return jsToCString(ctx, val); + case JSTag.OBJECT: + final rt = jsGetRuntime(ctx); + final dartObjectClassId = runtimeOpaques[rt].dartObjectClassId; + if (dartObjectClassId != 0) { + final dartObject = _DartObject.fromAddress( + rt, jsGetObjectOpaque(val, dartObjectClassId)); + if (dartObject != null) return dartObject._obj; + } + Pointer psize = allocate(); + Pointer buf = jsGetArrayBuffer(ctx, psize, val); + int size = psize.value; + free(psize); + if (buf.address != 0) { + return Uint8List.fromList(buf.asTypedList(size)); + } + int valptr = jsValueGetPtr(val).address; + if (cache.containsKey(valptr)) { + return cache[valptr]; + } + if (jsIsFunction(ctx, val) != 0) { + return _JSFunction(ctx, val); + } else if (jsIsError(ctx, val) != 0) { + final err = jsToCString(ctx, val); + final pstack = _jsGetPropertyValue(ctx, val, 'stack'); + final stack = + jsToBool(ctx, pstack) != 0 ? jsToCString(ctx, pstack) : null; + jsFreeValue(ctx, pstack); + return JSError(err, stack); + } else if (jsIsPromise(ctx, val) != 0) { + Pointer jsPromiseThen = _jsGetPropertyValue(ctx, val, 'then'); + _JSFunction promiseThen = _jsToDart(ctx, jsPromiseThen, cache: cache); + jsFreeValue(ctx, jsPromiseThen); + final completer = Completer(); + completer.future.catchError((e) {}); + final jsPromise = _JSObject(ctx, val); + final jsRet = promiseThen._invoke([ + (v) { + if (!completer.isCompleted) completer.complete(v); + }, + (e) { + if (!completer.isCompleted) completer.completeError(e); + }, + ], jsPromise); + jsPromise.release(); + promiseThen.release(); + bool isException = jsIsException(jsRet) != 0; + jsFreeValue(ctx, jsRet); + if (isException) throw _parseJSException(ctx); + return completer.future; + } else if (jsIsArray(ctx, val) != 0) { + Pointer jslength = _jsGetPropertyValue(ctx, val, 'length'); + int length = jsToInt64(ctx, jslength); + List ret = []; + cache[valptr] = ret; + for (int i = 0; i < length; ++i) { + final jsProp = _jsGetPropertyValue(ctx, val, i); + ret.add(_jsToDart(ctx, jsProp, cache: cache)); + jsFreeValue(ctx, jsProp); + } + return ret; + } else { + Pointer ptab = allocate(); + Pointer plen = allocate(); + if (jsGetOwnPropertyNames(ctx, ptab, plen, val, -1) != 0) return null; + int len = plen.value; + free(plen); + Map ret = Map(); + cache[valptr] = ret; + for (int i = 0; i < len; ++i) { + final jsAtom = jsPropertyEnumGetAtom(ptab.value, i); + final jsAtomValue = jsAtomToValue(ctx, jsAtom); + final jsProp = jsGetProperty(ctx, val, jsAtom); + ret[_jsToDart(ctx, jsAtomValue, cache: cache)] = + _jsToDart(ctx, jsProp, cache: cache); + jsFreeValue(ctx, jsAtomValue); + jsFreeValue(ctx, jsProp); + jsFreeAtom(ctx, jsAtom); + } + jsFree(ctx, ptab.value); + free(ptab); + return ret; + } + break; + default: + } + return null; +} diff --git a/lib/wrapper.dart b/lib/wrapper.dart deleted file mode 100644 index b172962..0000000 --- a/lib/wrapper.dart +++ /dev/null @@ -1,615 +0,0 @@ -/* - * @Description: - * @Author: ekibun - * @Date: 2020-09-19 22:07:47 - * @LastEditors: ekibun - * @LastEditTime: 2020-12-02 11:14:03 - */ -import 'dart:async'; -import 'dart:ffi'; -import 'dart:isolate'; -import 'dart:typed_data'; -import 'package:ffi/ffi.dart'; -import 'ffi.dart'; - -class JSError { - String message; - String stack; - JSError(message, [stack]) { - if (message is JSError) { - this.message = message.message; - this.stack = message.stack; - } else { - this.message = message.toString(); - this.stack = (stack ?? StackTrace.current).toString(); - } - } - @override - String toString() { - return stack == null ? message.toString() : "$message\n$stack"; - } - - static JSError decode(Map obj) { - if (obj.containsKey('__js_error')) - return JSError(obj['__js_error'], obj['__js_error_stack']); - return null; - } - - Map encode() { - return { - '__js_error': message, - '__js_error_stack': stack, - }; - } -} - -abstract class JSInvokable { - dynamic invoke(List args, [dynamic thisVal]); - - static dynamic wrap(dynamic func) { - return func is JSInvokable - ? func - : func is Function - ? _DartFunction(func) - : func; - } - - @override - noSuchMethod(Invocation invocation) { - return invoke( - invocation.positionalArguments, - invocation.namedArguments[#thisVal], - ); - } -} - -// class NativeJSInvokable extends JSInvokable { -// dynamic Function(Pointer ctx, Pointer thisVal, List args) _func; -// NativeJSInvokable(this._func); - -// @override -// dynamic invoke(List args, [dynamic thisVal]) { -// throw UnimplementedError('use invokeNative instead.'); -// } - -// invokeNative(Pointer ctx, Pointer thisVal, List args) { -// _func(ctx, thisVal, args); -// } -// } - -class _DartFunction extends JSInvokable { - Function _func; - _DartFunction(this._func); - - @override - invoke(List args, [thisVal]) { - /// wrap this into function - final passThis = - RegExp('{.*thisVal.*}').hasMatch(_func.runtimeType.toString()); - return Function.apply(_func, args, passThis ? {#thisVal: thisVal} : null); - } -} - -abstract class DartReleasable { - void release(); -} - -class DartObject implements JSRef { - Object _obj; - Pointer _ctx; - DartObject(this._ctx, this._obj) { - runtimeOpaques[jsGetRuntime(_ctx)]?.ref?.add(this); - } - - static DartObject fromAddress(Pointer rt, int val) { - return runtimeOpaques[rt]?.ref?.firstWhere( - (e) => identityHashCode(e) == val, - orElse: () => null, - ); - } - - @override - void release() { - if (_obj is DartReleasable) { - (_obj as DartReleasable).release(); - } - _obj = null; - _ctx = null; - } -} - -class JSObject implements JSRef { - Pointer _val; - Pointer _ctx; - - /// Create - JSObject(this._ctx, Pointer _val) { - Pointer rt = jsGetRuntime(_ctx); - this._val = jsDupValue(_ctx, _val); - runtimeOpaques[rt]?.ref?.add(this); - } - - JSObject.fromAddress(Pointer ctx, Pointer val) { - this._ctx = ctx; - this._val = val; - } - - @override - void release() { - if (_val != null) { - jsFreeValue(_ctx, _val); - } - _val = null; - _ctx = null; - } -} - -class JSFunction extends JSObject implements JSInvokable { - JSFunction(Pointer ctx, Pointer val) : super(ctx, val); - - JSFunction.fromAddress(Pointer ctx, Pointer val) - : super.fromAddress(ctx, val); - - @override - invoke(List arguments, [dynamic thisVal]) { - Pointer jsRet = _invoke(arguments, thisVal); - if (jsRet == null) return; - bool isException = jsIsException(jsRet) != 0; - if (isException) { - jsFreeValue(_ctx, jsRet); - throw parseJSException(_ctx); - } - var ret = jsToDart(_ctx, jsRet); - jsFreeValue(_ctx, jsRet); - return ret; - } - - Pointer _invoke(List arguments, [dynamic thisVal]) { - if (_val == null) return null; - List args = arguments - .map( - (e) => dartToJs(_ctx, e), - ) - .toList(); - Pointer jsThis = dartToJs(_ctx, thisVal); - Pointer jsRet = jsCall(_ctx, _val, jsThis, args); - jsFreeValue(_ctx, jsThis); - for (Pointer jsArg in args) { - jsFreeValue(_ctx, jsArg); - } - return jsRet; - } - - @override - noSuchMethod(Invocation invocation) { - return invoke( - invocation.positionalArguments, - invocation.namedArguments[#thisVal], - ); - } -} - -class IsolateJSFunction extends JSInvokable { - int _val; - int _ctx; - SendPort port; - IsolateJSFunction(this._ctx, this._val, this.port); - - @override - Future invoke(List arguments, [thisVal]) async { - if (0 == _val ?? 0) return; - var evaluatePort = ReceivePort(); - port.send({ - 'type': 'call', - 'ctx': _ctx, - 'val': _val, - 'args': encodeData(arguments), - 'this': encodeData(thisVal), - 'port': evaluatePort.sendPort, - }); - Map result = await evaluatePort.first; - evaluatePort.close(); - if (result.containsKey('data')) - return decodeData(result['data'], port); - else - throw decodeData(result['error'], port); - } -} - -class IsolateFunction extends JSInvokable implements DartReleasable { - SendPort _port; - SendPort func; - IsolateFunction(this.func, this._port); - - static IsolateFunction bind(Function func, SendPort port) { - final JSInvokable invokable = JSInvokable.wrap(func); - final funcPort = ReceivePort(); - funcPort.listen((msg) async { - if (msg == 'close') return funcPort.close(); - var data; - SendPort msgPort = msg['port']; - try { - List args = decodeData(msg['args'], port); - Map thisVal = decodeData(msg['this'], port); - data = await invokable.invoke(args, thisVal); - if (msgPort != null) - msgPort.send({ - 'data': encodeData(data), - }); - } catch (e) { - if (msgPort != null) - msgPort.send({ - 'error': encodeData(e), - }); - } - }); - return IsolateFunction(funcPort.sendPort, port); - } - - @override - Future invoke(List positionalArguments, [thisVal]) async { - if (func == null) return; - var evaluatePort = ReceivePort(); - func.send({ - 'args': encodeData(positionalArguments), - 'this': encodeData(thisVal), - 'port': evaluatePort.sendPort, - }); - Map result = await evaluatePort.first; - evaluatePort.close(); - if (result.containsKey('data')) - return decodeData(result['data'], _port); - else - throw decodeData(result['error'], _port); - } - - @override - void release() { - if (func == null) return; - func.send('close'); - func = null; - } -} - -dynamic encodeData(dynamic data, {Map cache}) { - if (cache == null) cache = Map(); - if (data is JSError) return data.encode(); - if (cache.containsKey(data)) return cache[data]; - if (data is List) { - var ret = []; - cache[data] = ret; - for (int i = 0; i < data.length; ++i) { - ret.add(encodeData(data[i], cache: cache)); - } - return ret; - } - if (data is Map) { - var ret = {}; - cache[data] = ret; - for (var entry in data.entries) { - ret[encodeData(entry.key, cache: cache)] = - encodeData(entry.value, cache: cache); - } - return ret; - } - if (data is JSObject) { - return { - '__js_function': data is JSFunction, - '__js_obj_ctx': data._ctx.address, - '__js_obj_val': data._val.address, - }; - } - if (data is IsolateJSFunction) { - return { - '__js_obj_ctx': data._ctx, - '__js_obj_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) { - futurePort.close(); - (port as SendPort).send({'data': encodeData(value)}); - }); - }, onError: (e) { - futurePort.first.then((port) { - futurePort.close(); - (port as SendPort).send({'error': encodeData(e)}); - }); - }); - return { - '__js_future_port': futurePort.sendPort, - }; - } - return data; -} - -dynamic decodeData(dynamic data, SendPort port, {Map cache}) { - if (cache == null) cache = Map(); - if (cache.containsKey(data)) return cache[data]; - if (data is List) { - var ret = []; - cache[data] = ret; - for (int i = 0; i < data.length; ++i) { - ret.add(decodeData(data[i], port, cache: cache)); - } - return ret; - } - if (data is Map) { - final jsException = JSError.decode(data); - if (jsException != null) return jsException; - if (data.containsKey('__js_obj_val')) { - int ctx = data['__js_obj_ctx']; - int val = data['__js_obj_val']; - if (data['__js_function'] == false) { - return JSObject.fromAddress( - Pointer.fromAddress(ctx), - Pointer.fromAddress(val), - ); - } else if (port != null) { - return IsolateJSFunction(ctx, val, port); - } else { - return JSFunction.fromAddress( - Pointer.fromAddress(ctx), - Pointer.fromAddress(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(decodeData(value['error'], port)); - } else { - futureCompleter.complete(decodeData(value['data'], port)); - } - }); - return futureCompleter.future; - } - var ret = {}; - cache[data] = ret; - for (var entry in data.entries) { - ret[decodeData(entry.key, port, cache: cache)] = - decodeData(entry.value, port, cache: cache); - } - return ret; - } - return data; -} - -dynamic parseJSException(Pointer ctx, [Pointer perr]) { - final e = perr ?? jsGetException(ctx); - var err; - try { - err = jsToDart(ctx, e); - } catch (exception) { - err = exception; - } - if (perr == null) jsFreeValue(ctx, e); - return err; -} - -void definePropertyValue( - Pointer ctx, - Pointer obj, - dynamic key, - dynamic val, { - Map cache, -}) { - var jsAtomVal = dartToJs(ctx, key, cache: cache); - var jsAtom = jsValueToAtom(ctx, jsAtomVal); - jsDefinePropertyValue( - ctx, - obj, - jsAtom, - dartToJs(ctx, val, cache: cache), - JSProp.C_W_E, - ); - jsFreeAtom(ctx, jsAtom); - jsFreeValue(ctx, jsAtomVal); -} - -Pointer jsGetPropertyValue( - Pointer ctx, - Pointer obj, - dynamic key, { - Map cache, -}) { - var jsAtomVal = dartToJs(ctx, key, cache: cache); - var jsAtom = jsValueToAtom(ctx, jsAtomVal); - var jsProp = jsGetProperty(ctx, obj, jsAtom); - jsFreeAtom(ctx, jsAtom); - jsFreeValue(ctx, jsAtomVal); - return jsProp; -} - -Pointer dartToJs(Pointer ctx, dynamic val, {Map cache}) { - if (val == null) return jsUNDEFINED(); - if (val is JSError) { - final ret = jsNewError(ctx); - definePropertyValue(ctx, ret, "name", ""); - definePropertyValue(ctx, ret, "message", val.message); - definePropertyValue(ctx, ret, "stack", val.stack); - return ret; - } - if (val is JSObject) return jsDupValue(ctx, val._val); - if (val is Future) { - 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, free: false); - jsFreeValue(ctx, resolvingFunc2, free: false); - free(resolvingFunc); - val.then((value) { - res(value); - }, onError: (e) { - rej(e); - }); - return ret; - } - if (cache == null) cache = Map(); - if (val is bool) return jsNewBool(ctx, val ? 1 : 0); - if (val is int) return jsNewInt64(ctx, val); - if (val is double) return jsNewFloat64(ctx, val); - if (val is String) return jsNewString(ctx, val); - if (val is Uint8List) { - var ptr = allocate(count: val.length); - var byteList = ptr.asTypedList(val.length); - byteList.setAll(0, val); - var ret = jsNewArrayBufferCopy(ctx, ptr, val.length); - free(ptr); - return ret; - } - if (cache.containsKey(val)) { - return jsDupValue(ctx, cache[val]); - } - if (val is List) { - Pointer ret = jsNewArray(ctx); - cache[val] = ret; - for (int i = 0; i < val.length; ++i) { - definePropertyValue(ctx, ret, i, val[i], cache: cache); - } - return ret; - } - if (val is Map) { - Pointer ret = jsNewObject(ctx); - cache[val] = ret; - for (MapEntry entry in val.entries) { - definePropertyValue(ctx, ret, entry.key, entry.value, cache: cache); - } - return ret; - } - // wrap Function to JSInvokable - final valWrap = JSInvokable.wrap(val); - int dartObjectClassId = - runtimeOpaques[jsGetRuntime(ctx)]?.dartObjectClassId ?? 0; - if (dartObjectClassId == 0) return jsUNDEFINED(); - var dartObject = jsNewObjectClass( - ctx, - dartObjectClassId, - identityHashCode(DartObject(ctx, valWrap)), - ); - if (valWrap is JSInvokable) { - final ret = jsNewCFunction(ctx, dartObject); - jsFreeValue(ctx, dartObject); - return ret; - } - return dartObject; -} - -dynamic jsToDart(Pointer ctx, Pointer val, {Map cache}) { - if (cache == null) cache = Map(); - int tag = jsValueGetTag(val); - if (jsTagIsFloat64(tag) != 0) { - return jsToFloat64(ctx, val); - } - switch (tag) { - case JSTag.BOOL: - return jsToBool(ctx, val) != 0; - case JSTag.INT: - return jsToInt64(ctx, val); - case JSTag.STRING: - return jsToCString(ctx, val); - case JSTag.OBJECT: - final rt = jsGetRuntime(ctx); - final dartObjectClassId = runtimeOpaques[rt].dartObjectClassId; - if (dartObjectClassId != 0) { - final dartObject = DartObject.fromAddress( - rt, jsGetObjectOpaque(val, dartObjectClassId)); - if (dartObject != null) return dartObject._obj; - } - Pointer psize = allocate(); - Pointer buf = jsGetArrayBuffer(ctx, psize, val); - int size = psize.value; - free(psize); - if (buf.address != 0) { - return Uint8List.fromList(buf.asTypedList(size)); - } - int valptr = jsValueGetPtr(val).address; - if (cache.containsKey(valptr)) { - return cache[valptr]; - } - if (jsIsFunction(ctx, val) != 0) { - return JSFunction(ctx, val); - } else if (jsIsError(ctx, val) != 0) { - final err = jsToCString(ctx, val); - final pstack = jsGetPropertyValue(ctx, val, 'stack'); - var stack; - if (jsToBool(ctx, pstack) != 0) { - stack = jsToCString(ctx, pstack); - } - jsFreeValue(ctx, pstack); - return JSError(err, stack); - } else if (jsIsPromise(ctx, val) != 0) { - Pointer jsPromiseThen = jsGetPropertyValue(ctx, val, 'then'); - JSFunction promiseThen = jsToDart(ctx, jsPromiseThen, cache: cache); - jsFreeValue(ctx, jsPromiseThen); - var completer = Completer(); - completer.future.catchError((e) {}); - final jsRet = promiseThen._invoke([ - (v) { - if (!completer.isCompleted) completer.complete(v); - }, - (e) { - if (!completer.isCompleted) completer.completeError(e); - }, - ], JSObject.fromAddress(ctx, val)); - bool isException = jsIsException(jsRet) != 0; - jsFreeValue(ctx, jsRet); - if (isException) throw parseJSException(ctx); - return completer.future; - } else if (jsIsArray(ctx, val) != 0) { - Pointer jslength = jsGetPropertyValue(ctx, val, 'length'); - int length = jsToInt64(ctx, jslength); - List ret = []; - cache[valptr] = ret; - for (int i = 0; i < length; ++i) { - var jsProp = jsGetPropertyValue(ctx, val, i); - ret.add(jsToDart(ctx, jsProp, cache: cache)); - jsFreeValue(ctx, jsProp); - } - return ret; - } else { - Pointer ptab = allocate(); - Pointer plen = allocate(); - if (jsGetOwnPropertyNames(ctx, ptab, plen, val, -1) != 0) return null; - int len = plen.value; - free(plen); - Map ret = Map(); - cache[valptr] = ret; - for (int i = 0; i < len; ++i) { - var jsAtom = jsPropertyEnumGetAtom(ptab.value, i); - var jsAtomValue = jsAtomToValue(ctx, jsAtom); - var jsProp = jsGetProperty(ctx, val, jsAtom); - ret[jsToDart(ctx, jsAtomValue, cache: cache)] = - jsToDart(ctx, jsProp, cache: cache); - jsFreeValue(ctx, jsAtomValue); - jsFreeValue(ctx, jsProp); - jsFreeAtom(ctx, jsAtom); - } - jsFree(ctx, ptab.value); - free(ptab); - return ret; - } - break; - default: - } - return null; -} diff --git a/test/flutter_qjs_test.dart b/test/flutter_qjs_test.dart index 15580a3..107a482 100644 --- a/test/flutter_qjs_test.dart +++ b/test/flutter_qjs_test.dart @@ -1,5 +1,5 @@ /* - * @Description: + * @Description: unit test * @Author: ekibun * @Date: 2020-09-06 13:02:46 * @LastEditors: ekibun @@ -10,9 +10,6 @@ import 'dart:convert'; import 'dart:io'; import 'package:flutter_qjs/flutter_qjs.dart'; -import 'package:flutter_qjs/isolate.dart'; -import 'package:flutter_qjs/ffi.dart'; -import 'package:flutter_qjs/wrapper.dart'; import 'package:flutter_test/flutter_test.dart'; dynamic myFunction(String args, {thisVal}) { @@ -21,69 +18,72 @@ dynamic myFunction(String args, {thisVal}) { Future testEvaluate(qjs) async { final testWrap = await qjs.evaluate( - "(a) => a", - name: "", + '(a) => a', + name: '', ); final wrapNull = await testWrap(null); - expect(wrapNull, null, reason: "wrap null"); - final primities = [0, 1, 0.1, true, false, "str"]; + expect(wrapNull, null, reason: 'wrap null'); + 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"); + for (int i = 0; i < primities.length; i++) { + expect(wrapPrimities[i], primities[i], reason: 'wrap primities'); } - final jsError = JSError("test Error"); + final jsError = JSError('test Error'); final wrapJsError = await testWrap(jsError); expect(jsError.message, (wrapJsError as JSError).message, - reason: "wrap JSError"); + reason: 'wrap JSError'); final wrapFunction = await testWrap(testWrap); final testEqual = await qjs.evaluate( - "(a, b) => a === b", - name: "", + '(a, b) => a === b', + name: '', ); expect(await testEqual(wrapFunction, testWrap), true, - reason: "wrap function"); + reason: 'wrap function'); + wrapFunction.release(); + testEqual.release(); - expect(wrapNull, null, reason: "wrap null"); + expect(wrapNull, null, reason: 'wrap null'); final a = {}; - a["a"] = a; + a['a'] = a; final wrapA = await testWrap(a); - expect(wrapA['a'], wrapA, reason: "recursive object"); + expect(wrapA['a'], wrapA, reason: 'recursive object'); final testThis = await qjs.evaluate( - "(function (func, arg) { return func.call(this, arg) })", - name: "", + '(function (func, arg) { return func.call(this, arg) })', + name: '', ); final funcRet = await testThis(myFunction, 'arg', thisVal: {'name': 'this'}); - expect(funcRet[0]['name'], 'this', reason: "js function this"); - expect(funcRet[1], 'arg', reason: "js function argument"); + testThis.release(); + expect(funcRet[0]['name'], '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: "", + '[Promise.reject("reject"), Promise.resolve("resolve"), new Promise(() => {})]', + name: '', )); for (final promise in promises) - expect(promise, isInstanceOf(), reason: "promise object"); + expect(promise, isInstanceOf(), reason: 'promise object'); try { await promises[0]; throw 'Future not reject'; } catch (e) { - expect(e, 'test Promise.reject', reason: "promise object reject"); + expect(e, 'reject', reason: 'promise object reject'); } - expect(await promises[1], 'test Promise.resolve', - reason: "promise object resolve"); + expect(await promises[1], 'resolve', reason: 'promise object resolve'); + testWrap.release(); } void main() async { test('make', () async { final utf8Encoding = Encoding.getByName('utf-8'); - var cmakePath = "cmake"; + var cmakePath = 'cmake'; if (Platform.isWindows) { - var vsDir = Directory("C:/Program Files (x86)/Microsoft Visual Studio/"); + var vsDir = Directory('C:/Program Files (x86)/Microsoft Visual Studio/'); vsDir = (vsDir.listSync().firstWhere((e) => e is Directory) as Directory) .listSync() .last as Directory; cmakePath = vsDir.path + - "/Common7/IDE/CommonExtensions/Microsoft/CMake/CMake/bin/cmake.exe"; + '/Common7/IDE/CommonExtensions/Microsoft/CMake/CMake/bin/cmake.exe'; } - final buildDir = "./build"; + final buildDir = './build'; var result = Process.runSync( cmakePath, ['-S', './', '-B', buildDir], @@ -109,7 +109,7 @@ void main() async { test('module', () async { final qjs = FlutterQjs( moduleHandler: (name) { - return "export default 'test module'"; + return 'export default "test module"'; }, ); qjs.dispatch(); @@ -120,14 +120,11 @@ void main() async { }; ''', name: 'evalModule', evalFlags: JSEvalFlag.MODULE); var result = await qjs.evaluate('import("evalModule")'); - expect(result['default']['data'], 'test module', reason: "eval module"); + expect(result['default']['data'], 'test module', reason: 'eval module'); qjs.close(); }); test('data conversion', () async { final qjs = FlutterQjs( - moduleHandler: (name) { - return "export default '${new DateTime.now()}'"; - }, hostPromiseRejectionHandler: (_) {}, ); qjs.dispatch(); @@ -136,33 +133,43 @@ void main() async { }); test('isolate conversion', () async { final qjs = IsolateQjs( - moduleHandler: (name) async { - return "export default '${new DateTime.now()}'"; - }, hostPromiseRejectionHandler: (_) {}, ); await testEvaluate(qjs); - qjs.close(); + await qjs.close(); }); test('isolate bind function', () async { final qjs = IsolateQjs(); var localVar; - final testFunc = await qjs.evaluate("(func)=>func('ret')", name: ""); + 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(); + testFunc.release(); + expect(localVar, 'test', reason: 'bind function'); + expect(testFuncRet, 'ret', reason: 'bind function args return'); + await qjs.close(); + }); + test('reference leak', () async { + final qjs = FlutterQjs(); + await qjs.evaluate('()=>{}', name: ''); + try { + qjs.close(); + throw 'Error not throw'; + } on JSError catch (e) { + expect(e.message, startsWith('reference leak:'), + reason: 'throw reference leak'); + } }); test('stack overflow', () async { final qjs = FlutterQjs(); try { - qjs.evaluate("a=()=>a();a();", name: ""); + qjs.evaluate('a=()=>a();a();', name: ''); + throw 'Error not throw'; } on JSError catch (e) { expect(e.message, 'InternalError: stack overflow', - reason: "throw stack overflow"); + reason: 'throw stack overflow'); } qjs.close(); }); @@ -175,13 +182,13 @@ void main() async { ); qjs.dispatch(); qjs.evaluate( - "(() => { Promise.resolve().then(() => { throw 'unhandle' }) })()", - name: ""); + '(() => { Promise.resolve().then(() => { throw "unhandle" }) })()', + name: ''); Future.delayed(Duration(seconds: 10)).then((value) { - if (!completer.isCompleted) completer.completeError("not host reject"); + if (!completer.isCompleted) completer.completeError('not host reject'); }); - expect(await completer.future, "unhandle", - reason: "host promise rejection"); + expect(await completer.future, 'unhandle', + reason: 'host promise rejection'); qjs.close(); }); }