From 6b0bab2faf6a4c11a47a626053df54b2dd54dec6 Mon Sep 17 00:00:00 2001 From: ekibun Date: Sun, 24 Jan 2021 16:43:19 +0800 Subject: [PATCH] setToGlobalObject --- README.md | 64 ++++++++++++++------------ cxx/ffi.cpp | 37 +++++++++------ cxx/ffi.h | 13 +++++- example/lib/main.dart | 2 +- lib/ffi.dart | 47 +++++++++++++++---- lib/flutter_qjs.dart | 94 ++++++++++++++++++++++---------------- lib/isolate.dart | 27 ++++++++--- lib/wrapper.dart | 52 +++++++++++---------- test/flutter_qjs_test.dart | 34 +++++++------- 9 files changed, 228 insertions(+), 142 deletions(-) diff --git a/README.md b/README.md index a03d9af..c45eef5 100644 --- a/README.md +++ b/README.md @@ -45,44 +45,36 @@ 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 | -| JSFunction(...args)
IsolateJSFunction(...args) | function(....args) | -| Future | Promise | -| Object | DartObject | +| dart | js | +| --------- | ------------------ | +| Bool | boolean | +| Int | number | +| Double | number | +| String | string | +| Uint8List | ArrayBuffer | +| List | Array | +| Map | Object | +| Function | function(....args) | +| Future | Promise | +| Object | DartObject | -**notice:** `function` can only be sent from js to dart. `DartObject` can only be used in `moduleHandler`. +**notice:** Dart function parameter `thisVal` is used to store `this` in js. -### Invoke dart function +### Set into global object -A global JavaScript function `channel` is presented to invoke dart function. - -In constructor, pass handler function to manage JavaScript call. For example, you can use `Dio` to implement http in JavaScript: +Method `setToGlobalObject` is presented to set dart object into global object. +For example, you can pass a function implement http in JavaScript with `Dio`: ```dart -final engine = FlutterQjs( - methodHandler: (String method, List arg) { - switch (method) { - case "http": - return Dio().get(arg[0]).then((response) => response.data); - default: - throw Exception("No such method"); - } - }, -); +engine.setToGlobalObject("http", (String url) { + return Dio().get(url).then((response) => response.data); +}); ``` -then, in java script you can use channel function to invoke `methodHandler`, make sure the second parameter is a list: +then, in java script you can use `http` function to invoke dart function: ```javascript -channel("http", ["http://example.com/"]); +http("http://example.com/"); ``` ### Use modules @@ -111,7 +103,9 @@ To use async function in module handler, try [Run on isolate thread](#Run-on-iso ### Run on isolate thread -Create a `IsolateQjs` object, 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**. Async function such as `rootBundle.loadString` can be used now to get modules: +Create a `IsolateQjs` object, pass handlers to resolving modules. Async function such as `rootBundle.loadString` can be used now to get modules: + +The `methodHandler` is used in isolate, so **the handler function must be a top-level function or a static method**. ```dart dynamic methodHandler(String method, List arg) { @@ -132,6 +126,16 @@ final engine = IsolateQjs( // not need engine.dispatch(); ``` +Method `setToGlobalObject` is still here to set dart object into global object. Use `await` to make sure it is finished. +**Make sure the object that can pass through isolate**, For example, a top level function: + +```dart +dynamic http(String url) { + return Dio().get(url).then((response) => response.data); +} +await engine.setToGlobalObject("http", http); +``` + Same as run on main thread, use `evaluate` to run js script. In this way, `Promise` return by `evaluate` will be automatically tracked and return the resolved data: ```dart diff --git a/cxx/ffi.cpp b/cxx/ffi.cpp index 573bf33..baefdb5 100644 --- a/cxx/ffi.cpp +++ b/cxx/ffi.cpp @@ -39,7 +39,7 @@ extern "C" { JSRuntime *rt = JS_GetRuntime(ctx); JSChannel *channel = (JSChannel *)JS_GetRuntimeOpaque(rt); - const char *str = (char *)channel(ctx, module_name, nullptr); + const char *str = (char *)channel(ctx, JSChannelType_MODULE, (void *)module_name); if (str == 0) return NULL; JSValue func_val = JS_Eval(ctx, str, strlen(str), module_name, JS_EVAL_TYPE_MODULE | JS_EVAL_FLAG_COMPILE_ONLY); @@ -51,16 +51,16 @@ extern "C" return m; } - JSValue js_channel(JSContext *ctx, JSValueConst this_val, int32_t argc, JSValueConst *argv) + JSValue js_channel(JSContext *ctx, JSValueConst this_val, int argc, JSValueConst *argv, int magic, JSValue *func_data) { JSRuntime *rt = JS_GetRuntime(ctx); JSChannel *channel = (JSChannel *)JS_GetRuntimeOpaque(rt); - const char *str = JS_ToCString(ctx, argv[0]); - JS_DupValue(ctx, *(argv + 1)); - JSValue ret = *(JSValue *)channel(ctx, str, argv + 1); - JS_FreeValue(ctx, *(argv + 1)); - JS_FreeCString(ctx, str); - return ret; + void *data[4]; + data[0] = &this_val; + data[1] = &argc; + data[2] = argv; + data[3] = func_data; + return *(JSValue *)channel(ctx, JSChannelType_METHON, data); } void js_promise_rejection_tracker(JSContext *ctx, JSValueConst promise, @@ -71,7 +71,7 @@ extern "C" return; JSRuntime *rt = JS_GetRuntime(ctx); JSChannel *channel = (JSChannel *)JS_GetRuntimeOpaque(rt); - channel(ctx, (char *)ctx, &reason); + channel(ctx, JSChannelType_PROMISE_TRACK, &reason); } DLLEXPORT JSRuntime *jsNewRuntime(JSChannel channel) @@ -97,7 +97,9 @@ extern "C" JSClassID classid = JS_GetClassID(obj); void *opaque = JS_GetOpaque(obj, classid); JSChannel *channel = (JSChannel *)JS_GetRuntimeOpaque(rt); - channel((JSContext *)rt, nullptr, opaque); + if (channel == nullptr) + return; + channel((JSContext *)rt, JSChannelType_FREE_OBJECT, opaque); }}; int e = JS_NewClass(rt, QJSClassId, &def); if (e < 0) @@ -134,14 +136,19 @@ extern "C" JS_FreeRuntime(rt); } + DLLEXPORT JSValue *jsNewCFunction(JSContext *ctx, JSValue *funcData) + { + return new JSValue(JS_NewCFunctionData(ctx, js_channel, 0, 0, 1, funcData)); + } + + DLLEXPORT JSValue *jsGetGlobalObject(JSContext *ctx) + { + return new JSValue(JS_GetGlobalObject(ctx)); + } + DLLEXPORT JSContext *jsNewContext(JSRuntime *rt) { JSContext *ctx = JS_NewContext(rt); - JSAtom atom = JS_NewAtom(ctx, "channel"); - JSValue globalObject = JS_GetGlobalObject(ctx); - JS_SetProperty(ctx, globalObject, atom, JS_NewCFunction(ctx, js_channel, "channel", 2)); - JS_FreeValue(ctx, globalObject); - JS_FreeAtom(ctx, atom); return ctx; } diff --git a/cxx/ffi.h b/cxx/ffi.h index 47981e8..a1ca636 100644 --- a/cxx/ffi.h +++ b/cxx/ffi.h @@ -8,7 +8,14 @@ extern "C" { - typedef void *JSChannel(JSContext *ctx, const char *method, void *argv); + enum JSChannelType { + JSChannelType_METHON = 0, + JSChannelType_MODULE = 1, + JSChannelType_PROMISE_TRACK = 2, + JSChannelType_FREE_OBJECT = 3, + }; + + typedef void *JSChannel(JSContext *ctx, size_t type, void *argv); DLLEXPORT JSValue *jsThrowInternalError(JSContext *ctx, char *message); @@ -30,6 +37,10 @@ extern "C" DLLEXPORT void jsFreeRuntime(JSRuntime *rt); + DLLEXPORT JSValue *jsNewCFunction(JSContext *ctx, JSValue *funcData); + + DLLEXPORT JSValue *jsGetGlobalObject(JSContext *ctx); + DLLEXPORT JSContext *jsNewContext(JSRuntime *rt); DLLEXPORT void jsFreeContext(JSContext *ctx); diff --git a/example/lib/main.dart b/example/lib/main.dart index 269c9af..b4d51a3 100644 --- a/example/lib/main.dart +++ b/example/lib/main.dart @@ -76,13 +76,13 @@ class _TestPageState extends State { _ensureEngine() { if (engine != null) return; engine = IsolateQjs( - methodHandler: methodHandler, moduleHandler: (String module) async { if (module == "test") return "export default '${new DateTime.now()}'"; return await rootBundle.loadString( "js/" + module.replaceFirst(new RegExp(r".js$"), "") + ".js"); }, ); + engine.setToGlobalObject("channel", methodHandler); } @override diff --git a/lib/ffi.dart b/lib/ffi.dart index 18ac833..15d8773 100644 --- a/lib/ffi.dart +++ b/lib/ffi.dart @@ -21,6 +21,13 @@ class JSEvalFlag { static const MODULE = 1 << 0; } +class JSChannelType { + static const METHON = 0; + static const MODULE = 1; + static const PROMISE_TRACK = 2; + static const FREE_OBJECT = 3; +} + class JSProp { static const CONFIGURABLE = (1 << 0); static const WRITABLE = (1 << 1); @@ -91,11 +98,13 @@ final Pointer Function() jsUNDEFINED = qjsLib .lookup>("jsUNDEFINED") .asFunction(); +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< - NativeFunction< - Pointer Function(Pointer ctx, Pointer method, Pointer argv)>>, + Pointer>, ) _jsNewRuntime = qjsLib .lookup< NativeFunction< @@ -104,8 +113,6 @@ final Pointer Function( )>>("jsNewRuntime") .asFunction(); -typedef JSChannel = Pointer Function(Pointer ctx, Pointer method, Pointer argv); - class RuntimeOpaque { JSChannel channel; List ref = []; @@ -116,9 +123,9 @@ class RuntimeOpaque { final Map runtimeOpaques = Map(); -Pointer channelDispacher(Pointer ctx, Pointer method, Pointer argv) { - Pointer rt = method.address == 0 ? ctx : jsGetRuntime(ctx); - return runtimeOpaques[rt]?.channel(ctx, method, argv); +Pointer channelDispacher(Pointer ctx, int type, Pointer argv) { + Pointer rt = type == JSChannelType.FREE_OBJECT ? ctx : jsGetRuntime(ctx); + return runtimeOpaques[rt]?.channel(ctx, type, argv); } Pointer jsNewRuntime( @@ -166,6 +173,30 @@ void jsFreeRuntime( _jsFreeRuntime(rt); } +/// JSValue *jsNewCFunction(JSContext *ctx, JSValue *funcData) +final Pointer Function( + Pointer ctx, + Pointer funcData, +) jsNewCFunction = qjsLib + .lookup< + NativeFunction< + Pointer Function( + Pointer, + Pointer, + )>>("jsNewCFunction") + .asFunction(); + +/// JSValue *jsGetGlobalObject(JSContext *ctx) +final Pointer Function( + Pointer ctx, +) jsGetGlobalObject = qjsLib + .lookup< + NativeFunction< + Pointer Function( + Pointer, + )>>("jsGetGlobalObject") + .asFunction(); + /// JSContext *jsNewContext(JSRuntime *rt) final Pointer Function( Pointer rt, diff --git a/lib/flutter_qjs.dart b/lib/flutter_qjs.dart index e8e27d9..2eb86f3 100644 --- a/lib/flutter_qjs.dart +++ b/lib/flutter_qjs.dart @@ -13,9 +13,6 @@ import 'package:ffi/ffi.dart'; import 'package:flutter_qjs/ffi.dart'; import 'package:flutter_qjs/wrapper.dart'; -/// Handler function to manage js call. -typedef JsMethodHandler = dynamic Function(String method, List args); - /// Handler function to manage js module. typedef JsModuleHandler = String Function(String name); @@ -32,9 +29,6 @@ class FlutterQjs { /// Message Port for event loop. Close it to stop dispatching event loop. ReceivePort port = ReceivePort(); - /// Handler function to manage js call with `channel(method, [...args])` function. - JsMethodHandler methodHandler; - /// Handler function to manage js module. JsModuleHandler moduleHandler; @@ -44,55 +38,75 @@ class FlutterQjs { /// Quickjs engine for flutter. /// /// Pass handlers to implement js-dart interaction and resolving modules. - FlutterQjs( - {this.methodHandler, - this.moduleHandler, - this.stackSize, - this.hostPromiseRejectionHandler}); + FlutterQjs({ + this.moduleHandler, + this.stackSize, + this.hostPromiseRejectionHandler, + }); + + setToGlobalObject(dynamic key, dynamic val) { + _ensureEngine(); + final globalObject = jsGetGlobalObject(_ctx); + definePropertyValue(_ctx, globalObject, key, val); + jsFreeValue(_ctx, globalObject); + } _ensureEngine() { if (_rt != null) return; - _rt = jsNewRuntime((ctx, method, argv) { + _rt = jsNewRuntime((ctx, type, ptr) { try { - if (method.address == 0) { - Pointer rt = ctx; - DartObject obj = DartObject.fromAddress(rt, argv.address); - obj?.release(); - runtimeOpaques[rt]?.ref?.remove(obj); - return Pointer.fromAddress(0); - } - if (argv.address != 0) { - if (method.address == ctx.address) { - final errStr = parseJSException(ctx, perr: argv); + switch (type) { + case JSChannelType.METHON: + final pdata = ptr.cast(); + final argc = pdata.elementAt(1).value.cast().value; + List args = []; + for (var i = 0; i < argc; i++) { + args.add(jsToDart( + ctx, + Pointer.fromAddress( + pdata.elementAt(2).value.address + sizeOfJSValue * i, + ))); + } + final thisVal = jsToDart(ctx, pdata.elementAt(0).value); + Function func = jsToDart(ctx, pdata.elementAt(3).value); + final passThis = + RegExp("{.*thisVal.*}").hasMatch(func.runtimeType.toString()); + return dartToJs( + ctx, + Function.apply(func, args, passThis ? {#thisVal: thisVal} : null), + ); + case JSChannelType.MODULE: + if (moduleHandler == null) throw Exception("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, perr: ptr); if (hostPromiseRejectionHandler != null) { hostPromiseRejectionHandler(errStr); } else { print("unhandled promise rejection: $errStr"); } return Pointer.fromAddress(0); - } - if (methodHandler == null) throw Exception("No MethodHandler"); - return dartToJs( - ctx, - methodHandler( - Utf8.fromUtf8(method.cast()), - jsToDart(ctx, argv), - )); + 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); } - if (moduleHandler == null) throw Exception("No ModuleHandler"); - var ret = - Utf8.toUtf8(moduleHandler(Utf8.fromUtf8(method.cast()))); - Future.microtask(() { - free(ret); - }); - return ret; + throw Exception("call channel with wrong type"); } catch (e, stack) { final errStr = e.toString() + "\n" + stack.toString(); - if (method.address == 0) { + if (type == JSChannelType.FREE_OBJECT) { print("DartObject release error: " + errStr); return Pointer.fromAddress(0); } - if (method.address == ctx.address) { + if (type == JSChannelType.MODULE) { print("host Promise Rejection Handler error: " + errStr); return Pointer.fromAddress(0); } @@ -100,7 +114,7 @@ class FlutterQjs { ctx, errStr, ); - if (argv.address == 0) { + if (type == JSChannelType.MODULE) { jsFreeValue(ctx, err); return Pointer.fromAddress(0); } diff --git a/lib/isolate.dart b/lib/isolate.dart index 53ffa29..978b187 100644 --- a/lib/isolate.dart +++ b/lib/isolate.dart @@ -153,7 +153,6 @@ void _runJsIsolate(Map spawnMessage) async { 'reason': reason, }); }, - methodHandler: spawnMessage['handler'], moduleHandler: (name) { var ptr = allocate>(); ptr.value = Pointer.fromAddress(0); @@ -192,6 +191,9 @@ void _runJsIsolate(Map spawnMessage) async { msg['val'], ).invoke(_decodeData(msg['args'], null)); break; + case 'setToGlobalObject': + qjs.setToGlobalObject(msg['key'], msg['val']); + break; case 'close': qjs.port.close(); qjs.close(); @@ -220,10 +222,6 @@ class IsolateQjs { /// Max stack size for quickjs. final int stackSize; - /// Handler to manage js call with `channel(method, [...args])` function. - /// The function must be a top-level function or a static method. - JsMethodHandler methodHandler; - /// Asynchronously handler to manage js module. JsAsyncModuleHandler moduleHandler; @@ -235,7 +233,6 @@ class IsolateQjs { /// 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.methodHandler, this.moduleHandler, this.stackSize, this.hostPromiseRejectionHandler, @@ -248,7 +245,6 @@ class IsolateQjs { _runJsIsolate, { 'port': port.sendPort, - 'handler': methodHandler, 'stackSize': stackSize, }, errorsAreFatal: true, @@ -305,6 +301,23 @@ class IsolateQjs { _sendPort = null; } + setToGlobalObject(dynamic key, dynamic val) async { + _ensureEngine(); + var evaluatePort = ReceivePort(); + var sendPort = await _sendPort; + sendPort.send({ + 'type': 'setToGlobalObject', + 'key': key, + 'val': val, + 'port': evaluatePort.sendPort, + }); + var result = await evaluatePort.first; + if (result['error'] == null) { + return; + } else + throw result['error']; + } + /// Evaluate js script. Future evaluate(String command, {String name, int evalFlags}) async { _ensureEngine(); diff --git a/lib/wrapper.dart b/lib/wrapper.dart index 5a58295..0cceac2 100644 --- a/lib/wrapper.dart +++ b/lib/wrapper.dart @@ -146,6 +146,26 @@ String parseJSException(Pointer ctx, {Pointer perr}) { 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 dartToJs(Pointer ctx, dynamic val, {Map cache}) { if (val == null) return jsUNDEFINED(); if (val is Future) { @@ -188,17 +208,7 @@ Pointer dartToJs(Pointer ctx, dynamic val, {Map cache}) { Pointer ret = jsNewArray(ctx); cache[val] = ret; for (int i = 0; i < val.length; ++i) { - var jsAtomVal = jsNewInt64(ctx, i); - var jsAtom = jsValueToAtom(ctx, jsAtomVal); - jsDefinePropertyValue( - ctx, - ret, - jsAtom, - dartToJs(ctx, val[i], cache: cache), - JSProp.C_W_E, - ); - jsFreeAtom(ctx, jsAtom); - jsFreeValue(ctx, jsAtomVal); + definePropertyValue(ctx, ret, i, val[i], cache: cache); } return ret; } @@ -206,28 +216,24 @@ Pointer dartToJs(Pointer ctx, dynamic val, {Map cache}) { Pointer ret = jsNewObject(ctx); cache[val] = ret; for (MapEntry entry in val.entries) { - var jsAtomVal = dartToJs(ctx, entry.key, cache: cache); - var jsAtom = jsValueToAtom(ctx, jsAtomVal); - jsDefinePropertyValue( - ctx, - ret, - jsAtom, - dartToJs(ctx, entry.value, cache: cache), - JSProp.C_W_E, - ); - jsFreeAtom(ctx, jsAtom); - jsFreeValue(ctx, jsAtomVal); + definePropertyValue(ctx, ret, entry.key, entry.value, cache: cache); } return ret; } int dartObjectClassId = runtimeOpaques[jsGetRuntime(ctx)]?.dartObjectClassId ?? 0; if (dartObjectClassId == 0) return jsUNDEFINED(); - return jsNewObjectClass( + var dartObject = jsNewObjectClass( ctx, dartObjectClassId, identityHashCode(DartObject(ctx, val)), ); + if (val is Function) { + final ret = jsNewCFunction(ctx, dartObject); + jsFreeValue(ctx, dartObject); + return ret; + } + return dartObject; } dynamic jsToDart(Pointer ctx, Pointer val, {Map cache}) { diff --git a/test/flutter_qjs_test.dart b/test/flutter_qjs_test.dart index 0ddc8ad..bd62b18 100644 --- a/test/flutter_qjs_test.dart +++ b/test/flutter_qjs_test.dart @@ -14,31 +14,32 @@ import 'package:flutter_qjs/flutter_qjs.dart'; import 'package:flutter_qjs/isolate.dart'; import 'package:flutter_test/flutter_test.dart'; -dynamic myMethodHandler(method, args) { - return args; +dynamic myMethodHandler(List args, {String thisVal}) { + return [thisVal, ...args]; } Future testEvaluate(qjs) async { - var value = await qjs.evaluate(""" + final value = await qjs.evaluate(""" const a = {}; a.a = a; - import('test').then((module) => channel('channel', [ + import('test').then((module) => channel.call('this', [ (...args)=>`hello \${args}!`, a, Promise.reject('test Promise.reject'), Promise.resolve('test Promise.resolve'), 0.1, true, false, 1, "world", module ])); """, name: ""); - expect(await value[0]('world'), 'hello world!', reason: "js function call"); - expect(value[1]['a'], value[1], reason: "recursive object"); - expect(value[2], isInstanceOf(), reason: "promise object"); + expect(value[0], 'this', reason: "js function this"); + expect(await value[1]('world'), 'hello world!', reason: "js function call"); + expect(value[2]['a'], value[2], reason: "recursive object"); + expect(value[3], isInstanceOf(), reason: "promise object"); try { - await value[2]; + await value[3]; throw 'Future not reject'; } catch (e) { expect(e, startsWith('test Promise.reject\n'), reason: "promise object reject"); } - expect(await value[3], 'test Promise.resolve', + expect(await value[4], 'test Promise.resolve', reason: "promise object resolve"); } @@ -96,12 +97,12 @@ void main() async { }); test('jsToDart', () async { final qjs = FlutterQjs( - methodHandler: myMethodHandler, moduleHandler: (name) { return "export default '${new DateTime.now()}'"; }, hostPromiseRejectionHandler: (_) {}, ); + qjs.setToGlobalObject("channel", myMethodHandler); qjs.dispatch(); await testEvaluate(qjs); qjs.close(); @@ -109,12 +110,12 @@ void main() async { test('isolate', () async { await runZonedGuarded(() async { final qjs = IsolateQjs( - methodHandler: myMethodHandler, moduleHandler: (name) async { return "export default '${new DateTime.now()}'"; }, hostPromiseRejectionHandler: (_) {}, ); + await qjs.setToGlobalObject("channel", myMethodHandler); await testEvaluate(qjs); qjs.close(); }, (e, stack) { @@ -122,13 +123,12 @@ void main() async { }); }); test('dart object', () async { - final qjs = FlutterQjs( - methodHandler: (method, args) { - return FlutterQjs(); - }, - ); + final qjs = FlutterQjs(); + qjs.setToGlobalObject("channel", () { + return FlutterQjs(); + }); qjs.dispatch(); - var value = await qjs.evaluate("channel('channel', [])", name: ""); + var value = await qjs.evaluate("channel()", name: ""); expect(value, isInstanceOf(), reason: "dart object"); qjs.close(); });