diff --git a/README.md b/README.md index bc0cd1a..8127946 100644 --- a/README.md +++ b/README.md @@ -77,10 +77,10 @@ or use `invoke` method to pass list parameters: ``` `JSInvokable` returned by evaluation may increase reference of JS object. -You should manually call `release` to release JS reference: +You should manually call `free` to release JS reference: ```dart -(func as JSInvokable).release(); +(func as JSInvokable).free(); ``` ### Use modules diff --git a/lib/src/engine.dart b/lib/src/engine.dart index 19dcbdc..8aea757 100644 --- a/lib/src/engine.dart +++ b/lib/src/engine.dart @@ -80,7 +80,7 @@ class FlutterQjs { case JSChannelType.FREE_OBJECT: Pointer rt = ctx; _DartObject obj = _DartObject.fromAddress(rt, ptr.address); - obj?.release(); + obj?.free(); return Pointer.fromAddress(0); } throw JSError('call channel with wrong type'); diff --git a/lib/src/ffi.dart b/lib/src/ffi.dart index 1ef3a05..1eec922 100644 --- a/lib/src/ffi.dart +++ b/lib/src/ffi.dart @@ -11,10 +11,21 @@ import 'dart:isolate'; import 'package:ffi/ffi.dart'; abstract class JSRef { - bool leakable = false; - void release(); + int _refCount = 0; + void dup() { + _refCount++; + } + + void free() { + _refCount--; + if (_refCount < 0) destroy(); + } + + void destroy(); } +abstract class JSRefLeakable {} + class JSEvalFlag { static const GLOBAL = 0 << 0; static const MODULE = 1 << 0; @@ -170,22 +181,20 @@ void jsFreeRuntime( while (true) { final ref = runtimeOpaques[rt] ?._ref - ?.firstWhere((ref) => ref.leakable, orElse: () => null); + ?.firstWhere((ref) => ref is JSRefLeakable, orElse: () => null); if (ref == null) break; - ref.release(); + ref.destroy(); 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); + " ${identityHashCode(ref)}\t${ref._refCount + 1}\t${ref.runtimeType.toString()}\t${ref.toString().replaceAll('\n', '\\n')}"); + ref.destroy(); } _jsFreeRuntime(rt); if (referenceleak.length > 0) { - throw ('reference leak:\n ADDR\t TYPE \t PROP\n' + + throw ('reference leak:\n ADDR\tREF\tTYPE\tPROP\n' + referenceleak.join('\n')); } } diff --git a/lib/src/isolate.dart b/lib/src/isolate.dart index 5428f1a..42ee310 100644 --- a/lib/src/isolate.dart +++ b/lib/src/isolate.dart @@ -7,22 +7,24 @@ */ part of '../flutter_qjs.dart'; -typedef dynamic _Decode(Map obj, SendPort port); +typedef dynamic _Decode(Map obj); List<_Decode> _decoders = [ JSError._decode, - _IsolateJSFunction._decode, - _IsolateFunction._decode, - _JSFunction._decode, + IsolateFunction._decode, ]; abstract class _IsolateEncodable { Map _encode(); } +final List _sendAllowType = [Null, String, int, double, bool, SendPort]; + dynamic _encodeData(dynamic data, {Map cache}) { + if (data is Function) return data; + if (_sendAllowType.contains(data.runtimeType)) return data; if (cache == null) cache = Map(); - if (data is _IsolateEncodable) return data._encode(); if (cache.containsKey(data)) return cache[data]; + if (data is _IsolateEncodable) return data._encode(); if (data is List) { final ret = []; cache[data] = ret; @@ -57,24 +59,23 @@ dynamic _encodeData(dynamic data, {Map cache}) { #jsFuturePort: futurePort.sendPort, }; } - return data; + throw JSError('unsupport type: ${data.runtimeType}\n${data.toString()}'); } -dynamic _decodeData(dynamic data, SendPort port, - {Map cache}) { +dynamic _decodeData(dynamic data, {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)); + ret.add(_decodeData(data[i], cache: cache)); } return ret; } if (data is Map) { for (final decoder in _decoders) { - final decodeObj = decoder(data, port); + final decodeObj = decoder(data); if (decodeObj != null) return decodeObj; } if (data.containsKey(#jsFuturePort)) { @@ -86,9 +87,9 @@ dynamic _decodeData(dynamic data, SendPort port, futurePort.first.then((value) { futurePort.close(); if (value is Map && value.containsKey(#error)) { - futureCompleter.completeError(_decodeData(value[#error], port)); + futureCompleter.completeError(_decodeData(value[#error])); } else { - futureCompleter.complete(_decodeData(value, port)); + futureCompleter.complete(_decodeData(value)); } }); return futureCompleter.future; @@ -96,8 +97,8 @@ dynamic _decodeData(dynamic data, SendPort port, final ret = {}; cache[data] = ret; for (final entry in data.entries) { - ret[_decodeData(entry.key, port, cache: cache)] = - _decodeData(entry.value, port, cache: cache); + ret[_decodeData(entry.key, cache: cache)] = + _decodeData(entry.value, cache: cache); } return ret; } @@ -147,21 +148,6 @@ void _runJsIsolate(Map spawnMessage) async { 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(); @@ -225,7 +211,7 @@ class IsolateQjs { switch (msg[#type]) { case #hostPromiseRejection: try { - final err = _decodeData(msg[#reason], port.sendPort); + final err = _decodeData(msg[#reason]); if (hostPromiseRejectionHandler != null) { hostPromiseRejectionHandler(err); } else { @@ -255,12 +241,6 @@ class IsolateQjs { _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; @@ -273,8 +253,8 @@ class IsolateQjs { final result = await closePort.first; closePort.close(); if (result is Map && result.containsKey(#error)) - throw _decodeData(result[#error], sendPort); - return _decodeData(result, sendPort); + throw _decodeData(result[#error]); + return _decodeData(result); }); _sendPort = null; return ret; @@ -295,7 +275,7 @@ class IsolateQjs { final result = await evaluatePort.first; evaluatePort.close(); if (result is Map && result.containsKey(#error)) - throw _decodeData(result[#error], sendPort); - return _decodeData(result, sendPort); + throw _decodeData(result[#error]); + return _decodeData(result); } } diff --git a/lib/src/object.dart b/lib/src/object.dart index fbcb44e..7fd93e9 100644 --- a/lib/src/object.dart +++ b/lib/src/object.dart @@ -8,7 +8,7 @@ part of '../flutter_qjs.dart'; /// js invokable -abstract class JSInvokable extends JSReleasable { +abstract class JSInvokable extends JSRef { dynamic invoke(List args, [dynamic thisVal]); static dynamic _wrap(dynamic func) { @@ -18,26 +18,39 @@ abstract class JSInvokable extends JSReleasable { ? _DartFunction(func) : func; } - - @override - noSuchMethod(Invocation invocation) { - return invoke( - invocation.positionalArguments, - invocation.namedArguments[#thisVal], - ); - } } class _DartFunction extends JSInvokable { final Function _func; _DartFunction(this._func); + void _freeRecursive(dynamic obj, [Set cache]) { + if (obj == null) return; + if (cache == null) cache = Set(); + if (cache.contains(obj)) return; + if (obj is List) { + cache.add(obj); + obj.forEach((e) => _freeRecursive(e, cache)); + } + if (obj is Map) { + cache.add(obj); + obj.values.forEach((e) => _freeRecursive(e, cache)); + } + if (obj is JSRef) { + obj.free(); + } + } + @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); + final ret = + Function.apply(_func, args, passThis ? {#thisVal: thisVal} : null); + _freeRecursive(args); + _freeRecursive(thisVal); + return ret; } @override @@ -46,21 +59,18 @@ class _DartFunction extends JSInvokable { } @override - release() {} + destroy() {} } /// implement this to capture js object release. -abstract class JSReleasable { - void release(); -} - -class _DartObject extends JSRef { - @override - bool leakable = true; +class _DartObject extends JSRef implements JSRefLeakable { Object _obj; Pointer _ctx; _DartObject(this._ctx, this._obj) { + if (_obj is JSRef) { + (_obj as JSRef).dup(); + } runtimeOpaques[jsGetRuntime(_ctx)]?.addRef(this); } @@ -70,17 +80,17 @@ class _DartObject extends JSRef { @override String toString() { - if (_ctx == null) return "DartObject()"; + if (_ctx == null) return "DartObject(released)"; return _obj.toString(); } @override - void release() { + void destroy() { if (_ctx == null) return; runtimeOpaques[jsGetRuntime(_ctx)]?.removeRef(this); _ctx = null; - if (_obj is JSReleasable) { - (_obj as JSReleasable).release(); + if (_obj is JSRef) { + (_obj as JSRef).free(); } _obj = null; } @@ -105,7 +115,7 @@ class JSError extends _IsolateEncodable { return stack == null ? message.toString() : "$message\n$stack"; } - static JSError _decode(Map obj, SendPort port) { + static JSError _decode(Map obj) { if (obj.containsKey(#jsError)) return JSError(obj[#jsError], obj[#jsErrorStack]); return null; @@ -133,16 +143,8 @@ class _JSObject extends JSRef { 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() { + void destroy() { if (_val == null) return; Pointer rt = jsGetRuntime(_ctx); runtimeOpaques[rt]?.removeRef(this); @@ -162,10 +164,6 @@ class _JSObject extends JSRef { 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); @@ -196,11 +194,124 @@ class _JSFunction extends _JSObject implements JSInvokable, _IsolateEncodable { 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]), + @override + Map _encode() { + final func = IsolateFunction._new(this); + final ret = func._encode(); + return ret; + } +} + +abstract class _IsolatePortHandler { + int _isolateId; + dynamic _handle(dynamic); +} + +class _IsolatePort { + static ReceivePort _invokeHandler; + static Set<_IsolatePortHandler> _handlers = Set(); + + static get _port { + if (_invokeHandler == null) { + _invokeHandler = ReceivePort(); + _invokeHandler.listen((msg) async { + final msgPort = msg[#port]; + try { + final handler = _handlers.firstWhere( + (v) => identityHashCode(v) == msg[#handler], + orElse: () => null, + ); + if (handler == null) throw JSError('handler released'); + final ret = _encodeData(await handler._handle(msg[#msg])); + if (msgPort != null) msgPort.send(ret); + } catch (e) { + final err = _encodeData(e); + if (msgPort != null) + msgPort.send({ + #error: err, + }); + } + }); + } + return _invokeHandler.sendPort; + } + + static _send(SendPort isolate, _IsolatePortHandler handler, msg) async { + if (isolate == null) return handler._handle(msg); + final evaluatePort = ReceivePort(); + isolate.send({ + #handler: handler._isolateId, + #msg: msg, + #port: evaluatePort.sendPort, + }); + final result = await evaluatePort.first; + if (result is Map && result.containsKey(#error)) + throw _decodeData(result[#error]); + return _decodeData(result); + } + + static _add(_IsolatePortHandler sendport) => _handlers.add(sendport); + static _remove(_IsolatePortHandler sendport) => _handlers.remove(sendport); +} + +/// Dart function wrapper for isolate +class IsolateFunction extends JSInvokable + implements _IsolateEncodable, _IsolatePortHandler { + @override + int _isolateId; + SendPort _port; + JSInvokable _invokable; + IsolateFunction._fromId(this._isolateId, this._port); + + IsolateFunction._new(this._invokable) { + _IsolatePort._add(this); + } + + static IsolateFunction func(Function func) { + return IsolateFunction._new(_DartFunction(func)); + } + + _destroy() { + _IsolatePort._remove(this); + _invokable?.free(); + } + + @override + _handle(msg) async { + switch (msg) { + case #dup: + _refCount++; + return null; + case #free: + _refCount--; + print("${identityHashCode(this)} ref $_refCount"); + if (_refCount < 0) _destroy(); + return null; + case #destroy: + _destroy(); + return null; + } + List args = _decodeData(msg[#args]); + Map thisVal = _decodeData(msg[#thisVal]); + return _invokable.invoke(args, thisVal); + } + + @override + Future invoke(List positionalArguments, [thisVal]) async { + List dArgs = _encodeData(positionalArguments); + Map dThisVal = _encodeData(thisVal); + return _IsolatePort._send(_port, this, { + #type: #invokeIsolate, + #args: dArgs, + #thisVal: dThisVal, + }); + } + + static IsolateFunction _decode(Map obj) { + if (obj.containsKey(#jsFunctionPort)) + return IsolateFunction._fromId( + obj[#jsFunctionId], + obj[#jsFunctionPort], ); return null; } @@ -208,135 +319,25 @@ class _JSFunction extends _JSObject implements JSInvokable, _IsolateEncodable { @override Map _encode() { return { - #jsFunction: _val.address, - #jsFunctionCtx: _ctx.address, + #jsFunctionId: _isolateId ?? identityHashCode(this), + #jsFunctionPort: _port ?? _IsolatePort._port, }; } + int _refCount = 0; + @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; + dup() { + _IsolatePort._send(_port, this, #dup); + } + + @override + free() { + _IsolatePort._send(_port, this, #free); + } + + @override + void destroy() { + _IsolatePort._send(_port, this, #destroy); } } diff --git a/lib/src/wrapper.dart b/lib/src/wrapper.dart index 955ea1b..083e63a 100644 --- a/lib/src/wrapper.dart +++ b/lib/src/wrapper.dart @@ -68,18 +68,22 @@ Pointer _dartToJs(Pointer ctx, dynamic val, {Map cache}) { 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; + _JSFunction res = _jsToDart(ctx, resolvingFunc); + _JSFunction rej = _jsToDart(ctx, resolvingFunc2); jsFreeValue(ctx, resolvingFunc, free: false); jsFreeValue(ctx, resolvingFunc2, free: false); free(resolvingFunc); + _DartObject refRes = _DartObject(ctx, res); + _DartObject refRej = _DartObject(ctx, rej); + res.free(); + rej.free(); val.then((value) { res.invoke([value]); }, onError: (e) { rej.invoke([e]); }).whenComplete(() { - res.release(); - rej.release(); + refRes.free(); + refRej.free(); }); return ret; } @@ -189,8 +193,8 @@ dynamic _jsToDart(Pointer ctx, Pointer val, {Map cache}) { if (!completer.isCompleted) completer.completeError(e); }, ], jsPromise); - jsPromise.release(); - promiseThen.release(); + jsPromise.free(); + promiseThen.free(); bool isException = jsIsException(jsRet) != 0; jsFreeValue(ctx, jsRet); if (isException) throw _parseJSException(ctx); diff --git a/test/flutter_qjs_test.dart b/test/flutter_qjs_test.dart index 107a482..fc574fc 100644 --- a/test/flutter_qjs_test.dart +++ b/test/flutter_qjs_test.dart @@ -8,6 +8,7 @@ import 'dart:async'; import 'dart:convert'; import 'dart:io'; +import 'dart:isolate'; import 'package:flutter_qjs/flutter_qjs.dart'; import 'package:flutter_test/flutter_test.dart'; @@ -17,48 +18,42 @@ dynamic myFunction(String args, {thisVal}) { } Future testEvaluate(qjs) async { - final testWrap = await qjs.evaluate( + JSInvokable wrapFunction = await qjs.evaluate( '(a) => a', name: '', ); - final wrapNull = await testWrap(null); + dynamic testWrap = await wrapFunction.invoke([wrapFunction]); + final wrapNull = await testWrap.invoke([null]); expect(wrapNull, null, reason: 'wrap null'); final primities = [0, 1, 0.1, true, false, 'str']; - final wrapPrimities = await testWrap(primities); + final wrapPrimities = await testWrap.invoke([primities]); for (int i = 0; i < primities.length; i++) { expect(wrapPrimities[i], primities[i], reason: 'wrap primities'); } final jsError = JSError('test Error'); - final wrapJsError = await testWrap(jsError); + final wrapJsError = await testWrap.invoke([jsError]); expect(jsError.message, (wrapJsError as JSError).message, reason: 'wrap JSError'); - final wrapFunction = await testWrap(testWrap); - final testEqual = await qjs.evaluate( - '(a, b) => a === b', - name: '', - ); - expect(await testEqual(wrapFunction, testWrap), true, - reason: 'wrap function'); - wrapFunction.release(); - testEqual.release(); expect(wrapNull, null, reason: 'wrap null'); final a = {}; a['a'] = a; - final wrapA = await testWrap(a); + final wrapA = await testWrap.invoke([a]); expect(wrapA['a'], wrapA, reason: 'recursive object'); - final testThis = await qjs.evaluate( + JSInvokable testThis = await qjs.evaluate( '(function (func, arg) { return func.call(this, arg) })', name: '', ); - final funcRet = await testThis(myFunction, 'arg', thisVal: {'name': 'this'}); - testThis.release(); + final funcRet = await testThis.invoke([myFunction, 'arg'], {'name': 'this'}); + testThis.free(); 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("reject"), Promise.resolve("resolve"), new Promise(() => {})]', - name: '', - )); + List promises = await testWrap.invoke([ + await qjs.evaluate( + '[Promise.reject("reject"), Promise.resolve("resolve"), new Promise(() => {})]', + name: '', + ) + ]); for (final promise in promises) expect(promise, isInstanceOf(), reason: 'promise object'); try { @@ -68,10 +63,16 @@ Future testEvaluate(qjs) async { expect(e, 'reject', reason: 'promise object reject'); } expect(await promises[1], 'resolve', reason: 'promise object resolve'); - testWrap.release(); + testWrap.free(); + wrapFunction.free(); } void main() async { + test('send', () async { + final rec = ReceivePort(); + rec.close(); + rec.sendPort.send("3232"); + }); test('make', () async { final utf8Encoding = Encoding.getByName('utf-8'); var cmakePath = 'cmake'; @@ -140,15 +141,23 @@ void main() async { }); 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; - })); - testFunc.release(); - expect(localVar, 'test', reason: 'bind function'); + final localVars = []; + JSInvokable testFunc = + await qjs.evaluate('(func)=>func(()=>"ret")', name: ''); + final func = IsolateFunction.func((args) { + localVars.add(args..dup()); + return args.invoke([]); + }); + final testFuncRet = await testFunc.invoke([func..dup()]); + final testFuncRet2 = await testFunc.invoke([func..dup()]); + func.free(); + testFunc.free(); + for (IsolateFunction vars in localVars) { + expect(await vars.invoke([]), 'ret', reason: 'bind function'); + vars.free(); + } expect(testFuncRet, 'ret', reason: 'bind function args return'); + expect(testFuncRet2, testFuncRet, reason: 'bind function args return2'); await qjs.close(); }); test('reference leak', () async {