From 0de94f12e218e72a61fe9751bac840c7a4e44d56 Mon Sep 17 00:00:00 2001 From: ekibun Date: Wed, 27 Jan 2021 16:52:08 +0800 Subject: [PATCH] v0.3.3 --- CHANGELOG.md | 6 ++ README-CN.md | 157 +++++++++++++++++++++++++++++++++++++ README.md | 115 ++++++++++++++------------- cxx/ffi.cpp | 2 +- example/pubspec.lock | 2 +- lib/flutter_qjs.dart | 2 +- lib/src/ffi.dart | 33 +++++++- lib/src/isolate.dart | 6 +- lib/src/object.dart | 89 ++++++--------------- lib/src/wrapper.dart | 4 + pubspec.yaml | 2 +- test/flutter_qjs_test.dart | 33 +++----- 12 files changed, 302 insertions(+), 149 deletions(-) create mode 100644 README-CN.md diff --git a/CHANGELOG.md b/CHANGELOG.md index b1698fb..bd07a4b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,12 @@ * @LastEditTime: 2020-12-02 11:36:40 --> +## 0.3.3 + +* remove `JSInvokable.call`. +* fix crash when throw error. +* add reference count and leak detection. + ## 0.3.2 * fix Promise reject cannot get Exception string. diff --git a/README-CN.md b/README-CN.md new file mode 100644 index 0000000..70b75b6 --- /dev/null +++ b/README-CN.md @@ -0,0 +1,157 @@ + +# flutter_qjs + +![Pub](https://img.shields.io/pub/v/flutter_qjs.svg) +![Test](https://github.com/ekibun/flutter_qjs/workflows/Test/badge.svg) + +[English](README.md) | [中文](README-CN.md) + +一个为flutter开发的 `quickjs` 引擎。插件基于 `dart:ffi`,支持除Web以外的所有平台! + +## 基本使用 + +首先,创建 `FlutterQjs` 对象。调用 `dispatch` 建立事件循环: + +```dart +final engine = FlutterQjs( + stackSize: 1024 * 1024, // change stack size here. +); +engine.dispatch(); +``` + +使用 `evaluate` 方法运行js脚本,方法同步执行,使用 `await` 来获得 `Promise` 结果: + +```dart +try { + print(engine.evaluate(code ?? '')); +} catch (e) { + print(e.toString()); +} +``` + +使用 `close` 方法销毁 quickjs 实例,其在再次调用 `evaluate` 时将会重建。当不再需要 `FlutterQjs` 对象时,关闭 `port` 参数来结束事件循环。**在 v0.3.3 后增加了引用检查,可能会抛出异常**。 + +```dart +try { + engine.port.close(); // stop dispatch loop + engine.close(); // close engine +} on JSError catch(e) { + print(e); // catch reference leak exception +} +engine = null; +``` + +dart 与 js 间数据以如下规则转换: + +| dart | js | +| ---------------------------- | ---------- | +| Bool | boolean | +| Int | number | +| Double | number | +| String | string | +| Uint8List | ArrayBuffer| +| List | Array | +| Map | Object | +| Function(arg1, arg2, ..., {thisVal})
JSInvokable.invoke(\[arg1, arg2, ...\], thisVal) | function.call(thisVal, arg1, arg2, ...) | +| Future | Promise | +| JSError | Error | +| Object | DartObject | + +## 使用模块 + +插件支持 ES6 模块方法 `import`。使用 `moduleHandler` 来处理模块请求: + +```dart +final engine = FlutterQjs( + moduleHandler: (String module) { + if(module == "hello") + return "export default (name) => `hello \${name}!`;"; + throw Exception("Module Not found"); + }, +); +``` + +在JavaScript中,`import` 方法用以获取模块: + +```javascript +import("hello").then(({default: greet}) => greet("world")); +``` + +**注:** 模块将只被编译一次. 调用 `FlutterQjs.close` 再 `evaluate` 来重置模块缓存。 + +若要使用异步方法来处理模块请求,请参见 [在 isolate 中运行](#在-isolate-中运行)。 + +## 在 isolate 中运行 + +创建 `IsolateQjs` 对象,设置 `moduleHandler` 来处理模块请求。 现在可以使用异步函数来获得模块字符串,如 `rootBundle.loadString`: + +```dart +final engine = IsolateQjs( + moduleHandler: (String module) async { + return await rootBundle.loadString( + "js/" + module.replaceFirst(new RegExp(r".js$"), "") + ".js"); + }, +); +// not need engine.dispatch(); +``` + +与在主线程运行一样,使用 `evaluate` 方法运行js脚本。在isolate中,所有结果都将异步返回,使用 `await` 来获取结果: + +```dart +try { + print(await engine.evaluate(code ?? '')); +} catch (e) { + print(e.toString()); +} +``` + +使用 `close` 方法销毁 isolate 线程,其在再次调用 `evaluate` 时将会重建。 + +## 调用 Dart 函数 + +Js脚本返回函数将被转换为 `JSInvokable`。 **它不能像 `Function` 一样调用,请使用 `invoke` 方法来调用**: + +```dart +(func as JSInvokable).invoke([arg1, arg2], thisVal); +``` + +**注:** 返回 `JSInvokable` 可能造成引用泄漏,需要手动调用 `free` 来释放引用: + +```dart +(obj as JSRef).free(); +// or JSRef.freeRecursive(obj); +``` + +传递给 `JSInvokable` 的参数将自动释放. 使用 `dup` 来保持引用: + +```dart +(obj as JSRef).dup(); +// or JSRef.dupRecursive(obj); +``` + +自 v0.3.0 起,dart 函数可以作为参数传递给 `JSInvokable`,且 `channel` 函数不再默认内置。可以使用如下方法将 dart 函数赋值给全局,例如,使用 `Dio` 来为 qjs 提供 http 支持: + +```dart +final setToGlobalObject = await engine.evaluate("(key, val) => { this[key] = val; }"); +await setToGlobalObject.invoke(["http", (String url) { + return Dio().get(url).then((response) => response.data); +}]); +setToGlobalObject.free(); +``` + +在 isolate 模式下,只有顶层和静态函数能作为参数传给 `JSInvokable`,函数将在 isolate 线程中调用。 使用 `IsolateFunction` 来传递局部函数(将在主线程中调用): + +```dart +await setToGlobalObject.invoke([ + "http", + IsolateFunction((String url) { + return Dio().get(url).then((response) => response.data); + }), +]); +``` \ No newline at end of file diff --git a/README.md b/README.md index 8127946..5a7cc67 100644 --- a/README.md +++ b/README.md @@ -10,13 +10,15 @@ ![Pub](https://img.shields.io/pub/v/flutter_qjs.svg) ![Test](https://github.com/ekibun/flutter_qjs/workflows/Test/badge.svg) +[English](README.md) | [中文](README-CN.md) + This plugin is a simple js engine for flutter using the `quickjs` project with `dart:ffi`. Plugin currently supports all the platforms except web! ## Getting Started ### Basic usage -Firstly, create a `FlutterQjs` object, then call `dispatch` to dispatch event loop: +Firstly, create a `FlutterQjs` object, then call `dispatch` to establish event loop: ```dart final engine = FlutterQjs( @@ -25,7 +27,7 @@ final engine = FlutterQjs( engine.dispatch(); ``` -Use `evaluate` method to run js script, now you can use it synchronously, or use await to resolve `Promise`: +Use `evaluate` method to run js script, it runs synchronously, you can use await to resolve `Promise`: ```dart try { @@ -35,7 +37,7 @@ 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. **Reference leak exception will be thrown since v0.3.3** ```dart try { @@ -49,41 +51,21 @@ engine = null; Data conversion between dart and js are implemented as follow: -| dart | js | -| ----------------------- | ------------------ | -| Bool | boolean | -| Int | number | -| Double | number | -| String | string | -| Uint8List | ArrayBuffer | -| List | Array | -| Map | Object | -| Function
JSInvokable | function(....args) | -| Future | Promise | -| JSError | Error | -| Object | DartObject | +| dart | js | +| ---------------------------- | ---------- | +| Bool | boolean | +| Int | number | +| Double | number | +| String | string | +| Uint8List | ArrayBuffer| +| List | Array | +| Map | Object | +| Function(arg1, arg2, ..., {thisVal})
JSInvokable.invoke(\[arg1, arg2, ...\], thisVal) | function.call(thisVal, arg1, arg2, ...) | +| Future | Promise | +| JSError | Error | +| Object | DartObject | -**notice:** `JSInvokable` does not extend `Function`, but can be used same as `Function`. -Dart function uses named argument `thisVal` to manage js function `this`: - -```dart -func(arg1, arg2, {thisVal}); -``` - -or use `invoke` method to pass list parameters: - -```dart -(func as JSInvokable).invoke([arg1, arg2], thisVal); -``` - -`JSInvokable` returned by evaluation may increase reference of JS object. -You should manually call `free` to release JS reference: - -```dart -(func as JSInvokable).free(); -``` - -### Use modules +## Use Modules ES6 module with `import` function is supported and can be managed in dart with `moduleHandler`: @@ -105,9 +87,8 @@ import("hello").then(({default: greet}) => greet("world")); **notice:** Module handler should be called only once for each module name. To reset the module cache, call `FlutterQjs.close` then `evaluate` again. -To use async function in module handler, try [Run on isolate thread](#Run-on-isolate-thread) - -### Run on isolate thread +To use async function in module handler, try [run on isolate thread](#Run-on-Isolate-Thread) +## Run on Isolate Thread Create a `IsolateQjs` object, pass handlers to resolving modules. Async function such as `rootBundle.loadString` can be used now to get modules: @@ -121,7 +102,7 @@ final engine = IsolateQjs( // not need engine.dispatch(); ``` -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: +Same as run on main thread, use `evaluate` to run js script. In isolate, everything returns asynchronously, use `await` to get the result: ```dart try { @@ -131,25 +112,49 @@ try { } ``` -Method `close` can destroy quickjs runtime that can be recreated again if you call `evaluate`. +Method `close` can destroy isolate thread that will 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: +## Use Dart Function (Breaking change in v0.3.0) + +Js script returning function will be converted to `JSInvokable`. **It does not extend `Function`, use `invoke` method to invoke it**: ```dart -await jsFunc(await engine.bind(({thisVal}) { - // DO SOMETHING -})); +(func as JSInvokable).invoke([arg1, arg2], thisVal); ``` -[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 included by default. -Use js function to set dart object globally: +**notice:** evaluation returning `JSInvokable` may cause reference leak. +You should manually call `free` to release JS reference. ```dart -final setToGlobalObject = await engine.evaluate("(key, val) => this[key] = val;"); -await setToGlobalObject("channel", methodHandler); +(obj as JSRef).free(); +// or JSRef.freeRecursive(obj); +``` + +Arguments passed into `JSInvokable` will be freed automatically. Use `dup` to keep the reference. + +```dart +(obj as JSRef).dup(); +// or JSRef.dupRecursive(obj); +``` + +Since v0.3.0, you can pass a function to `JSInvokable` arguments, and `channel` function is no longer included by default. You can use js function to set dart object globally. +For example, use `Dio` to implement http in qjs: + +```dart +final setToGlobalObject = await engine.evaluate("(key, val) => { this[key] = val; }"); +await setToGlobalObject.invoke(["http", (String url) { + return Dio().get(url).then((response) => response.data); +}]); +setToGlobalObject.free(); +``` + +In isolate, top level function passed in `JSInvokable` will be invoked in isolate thread. Use `IsolateFunction` to pass a instant function: + +```dart +await setToGlobalObject.invoke([ + "http", + IsolateFunction((String url) { + return Dio().get(url).then((response) => response.data); + }), +]); ``` \ No newline at end of file diff --git a/cxx/ffi.cpp b/cxx/ffi.cpp index 3e799da..12da8cf 100644 --- a/cxx/ffi.cpp +++ b/cxx/ffi.cpp @@ -15,7 +15,7 @@ extern "C" DLLEXPORT JSValue *jsThrow(JSContext *ctx, JSValue *obj) { - return new JSValue(JS_Throw(ctx, *obj)); + return new JSValue(JS_Throw(ctx, JS_DupValue(ctx, *obj))); } DLLEXPORT JSValue *jsEXCEPTION() diff --git a/example/pubspec.lock b/example/pubspec.lock index 0566f0c..591231c 100644 --- a/example/pubspec.lock +++ b/example/pubspec.lock @@ -75,7 +75,7 @@ packages: path: ".." relative: true source: path - version: "0.3.2" + version: "0.3.3" flutter_test: dependency: "direct dev" description: flutter diff --git a/lib/flutter_qjs.dart b/lib/flutter_qjs.dart index f99a917..be401cf 100644 --- a/lib/flutter_qjs.dart +++ b/lib/flutter_qjs.dart @@ -5,7 +5,7 @@ import 'dart:isolate'; import 'dart:typed_data'; import 'package:ffi/ffi.dart'; import 'src/ffi.dart'; -export 'src/ffi.dart' show JSEvalFlag; +export 'src/ffi.dart' show JSEvalFlag, JSRef; part 'src/engine.dart'; part 'src/isolate.dart'; diff --git a/lib/src/ffi.dart b/lib/src/ffi.dart index 1eec922..dade9a9 100644 --- a/lib/src/ffi.dart +++ b/lib/src/ffi.dart @@ -22,6 +22,35 @@ abstract class JSRef { } void destroy(); + + static void freeRecursive(dynamic obj) { + _callRecursive(obj, (ref) => ref.free()); + } + + static void dupRecursive(dynamic obj) { + _callRecursive(obj, (ref) => ref.dup()); + } + + static void _callRecursive( + dynamic obj, + void Function(JSRef) cb, [ + 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) => _callRecursive(e, cb, cache)); + } + if (obj is Map) { + cache.add(obj); + obj.values.forEach((e) => _callRecursive(e, cb, cache)); + } + if (obj is JSRef) { + cb(obj); + } + } } abstract class JSRefLeakable {} @@ -188,8 +217,10 @@ void jsFreeRuntime( } while (0 < runtimeOpaques[rt]?._ref?.length ?? 0) { final ref = runtimeOpaques[rt]?._ref?.first; + final objStrs = ref.toString().split('\n'); + final objStr = objStrs.length > 0 ? objStrs[0] + " ..." : objStrs[0]; referenceleak.add( - " ${identityHashCode(ref)}\t${ref._refCount + 1}\t${ref.runtimeType.toString()}\t${ref.toString().replaceAll('\n', '\\n')}"); + " ${identityHashCode(ref)}\t${ref._refCount + 1}\t${ref.runtimeType.toString()}\t$objStr"); ref.destroy(); } _jsFreeRuntime(rt); diff --git a/lib/src/isolate.dart b/lib/src/isolate.dart index 42ee310..2fc4e1e 100644 --- a/lib/src/isolate.dart +++ b/lib/src/isolate.dart @@ -17,11 +17,7 @@ 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 (cache.containsKey(data)) return cache[data]; if (data is _IsolateEncodable) return data._encode(); @@ -59,7 +55,7 @@ dynamic _encodeData(dynamic data, {Map cache}) { #jsFuturePort: futurePort.sendPort, }; } - throw JSError('unsupport type: ${data.runtimeType}\n${data.toString()}'); + return data; } dynamic _decodeData(dynamic data, {Map cache}) { diff --git a/lib/src/object.dart b/lib/src/object.dart index 7fd93e9..73d1530 100644 --- a/lib/src/object.dart +++ b/lib/src/object.dart @@ -24,23 +24,6 @@ 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 @@ -48,8 +31,8 @@ class _DartFunction extends JSInvokable { RegExp('{.*thisVal.*}').hasMatch(_func.runtimeType.toString()); final ret = Function.apply(_func, args, passThis ? {#thisVal: thisVal} : null); - _freeRecursive(args); - _freeRecursive(thisVal); + JSRef.freeRecursive(args); + JSRef.freeRecursive(thisVal); return ret; } @@ -179,7 +162,7 @@ class _JSFunction extends _JSObject implements JSInvokable, _IsolateEncodable { } Pointer _invoke(List arguments, [dynamic thisVal]) { - if (_val == null) return null; + if (_val == null) throw JSError("InternalError: JSValue released"); List args = arguments .map( (e) => _dartToJs(_ctx, e), @@ -196,22 +179,26 @@ class _JSFunction extends _JSObject implements JSInvokable, _IsolateEncodable { @override Map _encode() { - final func = IsolateFunction._new(this); - final ret = func._encode(); - return ret; + return IsolateFunction._new(this)._encode(); } } -abstract class _IsolatePortHandler { +/// Dart function wrapper for isolate +class IsolateFunction extends JSInvokable implements _IsolateEncodable { int _isolateId; - dynamic _handle(dynamic); -} + SendPort _port; + JSInvokable _invokable; + IsolateFunction._fromId(this._isolateId, this._port); + + IsolateFunction._new(this._invokable) { + _handlers.add(this); + } + IsolateFunction(Function func) : this._new(_DartFunction(func)); -class _IsolatePort { static ReceivePort _invokeHandler; - static Set<_IsolatePortHandler> _handlers = Set(); + static Set _handlers = Set(); - static get _port { + static get _handlePort { if (_invokeHandler == null) { _invokeHandler = ReceivePort(); _invokeHandler.listen((msg) async { @@ -236,11 +223,11 @@ class _IsolatePort { return _invokeHandler.sendPort; } - static _send(SendPort isolate, _IsolatePortHandler handler, msg) async { - if (isolate == null) return handler._handle(msg); + _send(msg) async { + if (_port == null) return _handle(msg); final evaluatePort = ReceivePort(); - isolate.send({ - #handler: handler._isolateId, + _port.send({ + #handler: _isolateId, #msg: msg, #port: evaluatePort.sendPort, }); @@ -250,33 +237,11 @@ class _IsolatePort { 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); + _handlers.remove(this); _invokable?.free(); } - @override _handle(msg) async { switch (msg) { case #dup: @@ -284,7 +249,6 @@ class IsolateFunction extends JSInvokable return null; case #free: _refCount--; - print("${identityHashCode(this)} ref $_refCount"); if (_refCount < 0) _destroy(); return null; case #destroy: @@ -300,8 +264,7 @@ class IsolateFunction extends JSInvokable Future invoke(List positionalArguments, [thisVal]) async { List dArgs = _encodeData(positionalArguments); Map dThisVal = _encodeData(thisVal); - return _IsolatePort._send(_port, this, { - #type: #invokeIsolate, + return _send({ #args: dArgs, #thisVal: dThisVal, }); @@ -320,7 +283,7 @@ class IsolateFunction extends JSInvokable Map _encode() { return { #jsFunctionId: _isolateId ?? identityHashCode(this), - #jsFunctionPort: _port ?? _IsolatePort._port, + #jsFunctionPort: _port ?? IsolateFunction._handlePort, }; } @@ -328,16 +291,16 @@ class IsolateFunction extends JSInvokable @override dup() { - _IsolatePort._send(_port, this, #dup); + _send(#dup); } @override free() { - _IsolatePort._send(_port, this, #free); + _send(#free); } @override void destroy() { - _IsolatePort._send(_port, this, #destroy); + _send(#destroy); } } diff --git a/lib/src/wrapper.dart b/lib/src/wrapper.dart index 083e63a..55e1e97 100644 --- a/lib/src/wrapper.dart +++ b/lib/src/wrapper.dart @@ -55,6 +55,8 @@ Pointer _jsGetPropertyValue( Pointer _dartToJs(Pointer ctx, dynamic val, {Map cache}) { if (val == null) return jsUNDEFINED(); + if (val is Error) return _dartToJs(ctx, JSError(val, val.stackTrace)); + if (val is Exception) return _dartToJs(ctx, JSError(val)); if (val is JSError) { final ret = jsNewError(ctx); _definePropertyValue(ctx, ret, "name", ""); @@ -187,9 +189,11 @@ dynamic _jsToDart(Pointer ctx, Pointer val, {Map cache}) { final jsPromise = _JSObject(ctx, val); final jsRet = promiseThen._invoke([ (v) { + JSRef.dupRecursive(v); if (!completer.isCompleted) completer.complete(v); }, (e) { + JSRef.dupRecursive(e); if (!completer.isCompleted) completer.completeError(e); }, ], jsPromise); diff --git a/pubspec.yaml b/pubspec.yaml index c882ae1..461f92d 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.3.2 +version: 0.3.3 homepage: https://github.com/ekibun/flutter_qjs environment: diff --git a/test/flutter_qjs_test.dart b/test/flutter_qjs_test.dart index fc574fc..18036f0 100644 --- a/test/flutter_qjs_test.dart +++ b/test/flutter_qjs_test.dart @@ -8,7 +8,6 @@ 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'; @@ -19,7 +18,7 @@ dynamic myFunction(String args, {thisVal}) { Future testEvaluate(qjs) async { JSInvokable wrapFunction = await qjs.evaluate( - '(a) => a', + 'async (a) => a', name: '', ); dynamic testWrap = await wrapFunction.invoke([wrapFunction]); @@ -68,11 +67,6 @@ Future testEvaluate(qjs) async { } 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'; @@ -139,25 +133,22 @@ void main() async { await testEvaluate(qjs); await qjs.close(); }); - test('isolate bind function', () async { + test('isolate bind this', () async { final qjs = IsolateQjs(); - final localVars = []; - JSInvokable testFunc = - await qjs.evaluate('(func)=>func(()=>"ret")', name: ''); - final func = IsolateFunction.func((args) { - localVars.add(args..dup()); + JSInvokable localVar; + JSInvokable setToGlobal = await qjs + .evaluate('(name, func)=>{ this[name] = func }', name: ''); + final func = IsolateFunction((args) { + localVar = args..dup(); return args.invoke([]); }); - final testFuncRet = await testFunc.invoke([func..dup()]); - final testFuncRet2 = await testFunc.invoke([func..dup()]); + await setToGlobal.invoke(["test", func..dup()]); func.free(); - testFunc.free(); - for (IsolateFunction vars in localVars) { - expect(await vars.invoke([]), 'ret', reason: 'bind function'); - vars.free(); - } + setToGlobal.free(); + final testFuncRet = await qjs.evaluate('test(()=>"ret")', name: ''); + expect(await localVar.invoke([]), 'ret', reason: 'bind function'); + localVar.free(); expect(testFuncRet, 'ret', reason: 'bind function args return'); - expect(testFuncRet2, testFuncRet, reason: 'bind function args return2'); await qjs.close(); }); test('reference leak', () async {