From 1589f87194b79b54897b2efa8f463c1eb97c3c6f Mon Sep 17 00:00:00 2001 From: ekibun Date: Sat, 23 Jan 2021 01:19:56 +0800 Subject: [PATCH] host promise rejection --- cxx/ffi.cpp | 24 +++++++++++++++-------- cxx/ffi.h | 6 ------ lib/flutter_qjs.dart | 39 ++++++++++++++++++++++++++++++++------ lib/isolate.dart | 34 ++++++++++++++++++++++++++++++--- lib/wrapper.dart | 11 +++++++---- test/flutter_qjs_test.dart | 20 +++++++++++++++++++ 6 files changed, 107 insertions(+), 27 deletions(-) diff --git a/cxx/ffi.cpp b/cxx/ffi.cpp index 6ed755c..573bf33 100644 --- a/cxx/ffi.cpp +++ b/cxx/ffi.cpp @@ -63,10 +63,22 @@ extern "C" return ret; } + void js_promise_rejection_tracker(JSContext *ctx, JSValueConst promise, + JSValueConst reason, + JS_BOOL is_handled, void *opaque) + { + if (is_handled) + return; + JSRuntime *rt = JS_GetRuntime(ctx); + JSChannel *channel = (JSChannel *)JS_GetRuntimeOpaque(rt); + channel(ctx, (char *)ctx, &reason); + } + DLLEXPORT JSRuntime *jsNewRuntime(JSChannel channel) { JSRuntime *rt = JS_NewRuntime(); JS_SetRuntimeOpaque(rt, (void *)channel); + JS_SetHostPromiseRejectionTracker(rt, js_promise_rejection_tracker, nullptr); JS_SetModuleLoaderFunc(rt, nullptr, js_module_loader, nullptr); return rt; } @@ -83,10 +95,9 @@ extern "C" // destructor [](JSRuntime *rt, JSValue obj) noexcept { JSClassID classid = JS_GetClassID(obj); - ObjectOpaque *opaque = (ObjectOpaque *)JS_GetOpaque(obj, classid); + void *opaque = JS_GetOpaque(obj, classid); JSChannel *channel = (JSChannel *)JS_GetRuntimeOpaque(rt); - channel((JSContext *)rt, nullptr, (void *)opaque->opaque); - delete opaque; + channel((JSContext *)rt, nullptr, opaque); }}; int e = JS_NewClass(rt, QJSClassId, &def); if (e < 0) @@ -100,9 +111,7 @@ extern "C" DLLEXPORT void *jsGetObjectOpaque(JSValue *obj, uint32_t classid) { - ObjectOpaque *opaque = (ObjectOpaque *)JS_GetOpaque(*obj, classid); - if(opaque == nullptr) return nullptr; - return opaque->opaque; + return JS_GetOpaque(*obj, classid); } DLLEXPORT JSValue *jsNewObjectClass(JSContext *ctx, uint32_t QJSClassId, void *opaque) @@ -110,8 +119,7 @@ extern "C" auto jsobj = new JSValue(JS_NewObjectClass(ctx, QJSClassId)); if (JS_IsException(*jsobj)) return jsobj; - ObjectOpaque *objOpaque = new ObjectOpaque{ctx, opaque}; - JS_SetOpaque(*jsobj, objOpaque); + JS_SetOpaque(*jsobj, opaque); return jsobj; } diff --git a/cxx/ffi.h b/cxx/ffi.h index e732b15..47981e8 100644 --- a/cxx/ffi.h +++ b/cxx/ffi.h @@ -8,12 +8,6 @@ extern "C" { - struct ObjectOpaque - { - JSContext *ctx; - void *opaque; - }; - typedef void *JSChannel(JSContext *ctx, const char *method, void *argv); DLLEXPORT JSValue *jsThrowInternalError(JSContext *ctx, char *message); diff --git a/lib/flutter_qjs.dart b/lib/flutter_qjs.dart index 9700903..e8e27d9 100644 --- a/lib/flutter_qjs.dart +++ b/lib/flutter_qjs.dart @@ -19,6 +19,9 @@ typedef JsMethodHandler = dynamic Function(String method, List args); /// Handler function to manage js module. typedef JsModuleHandler = String Function(String name); +/// Handler to manage unhandled promise rejection. +typedef JsHostPromiseRejectionHandler = void Function(String reason); + class FlutterQjs { Pointer _rt; Pointer _ctx; @@ -35,10 +38,17 @@ class FlutterQjs { /// Handler function to manage js module. JsModuleHandler moduleHandler; + /// Handler function to manage js module. + JsHostPromiseRejectionHandler hostPromiseRejectionHandler; + /// Quickjs engine for flutter. /// /// Pass handlers to implement js-dart interaction and resolving modules. - FlutterQjs({this.methodHandler, this.moduleHandler, this.stackSize}); + FlutterQjs( + {this.methodHandler, + this.moduleHandler, + this.stackSize, + this.hostPromiseRejectionHandler}); _ensureEngine() { if (_rt != null) return; @@ -51,14 +61,22 @@ class FlutterQjs { runtimeOpaques[rt]?.ref?.remove(obj); return Pointer.fromAddress(0); } - if (argv.address != 0 && method.address != 0) { + if (argv.address != 0) { + if (method.address == ctx.address) { + final errStr = parseJSException(ctx, perr: argv); + if (hostPromiseRejectionHandler != null) { + hostPromiseRejectionHandler(errStr); + } else { + print("unhandled promise rejection: $errStr"); + } + return Pointer.fromAddress(0); + } if (methodHandler == null) throw Exception("No MethodHandler"); - var argvs = jsToDart(ctx, argv); return dartToJs( ctx, methodHandler( Utf8.fromUtf8(method.cast()), - argvs, + jsToDart(ctx, argv), )); } if (moduleHandler == null) throw Exception("No ModuleHandler"); @@ -69,11 +87,20 @@ class FlutterQjs { }); return ret; } catch (e, stack) { + final errStr = e.toString() + "\n" + stack.toString(); + if (method.address == 0) { + print("DartObject release error: " + errStr); + return Pointer.fromAddress(0); + } + if (method.address == ctx.address) { + print("host Promise Rejection Handler error: " + errStr); + return Pointer.fromAddress(0); + } var err = jsThrowInternalError( ctx, - e.toString() + "\n" + stack.toString(), + errStr, ); - if (method.address == 0) { + if (argv.address == 0) { jsFreeValue(ctx, err); return Pointer.fromAddress(0); } diff --git a/lib/isolate.dart b/lib/isolate.dart index 91550e2..53ffa29 100644 --- a/lib/isolate.dart +++ b/lib/isolate.dart @@ -143,12 +143,17 @@ dynamic _decodeData(dynamic data, SendPort port, void _runJsIsolate(Map spawnMessage) async { SendPort sendPort = spawnMessage['port']; - JsMethodHandler methodHandler = spawnMessage['handler']; ReceivePort port = ReceivePort(); sendPort.send(port.sendPort); var qjs = FlutterQjs( stackSize: spawnMessage['stackSize'], - methodHandler: methodHandler, + hostPromiseRejectionHandler: (reason) { + sendPort.send({ + 'type': 'hostPromiseRejection', + 'reason': reason, + }); + }, + methodHandler: spawnMessage['handler'], moduleHandler: (name) { var ptr = allocate>(); ptr.value = Pointer.fromAddress(0); @@ -222,11 +227,19 @@ class IsolateQjs { /// 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.methodHandler, this.moduleHandler, this.stackSize}); + IsolateQjs({ + this.methodHandler, + this.moduleHandler, + this.stackSize, + this.hostPromiseRejectionHandler, + }); _ensureEngine() { if (_sendPort != null) return; @@ -247,6 +260,21 @@ class IsolateQjs { return; } switch (msg['type']) { + case 'hostPromiseRejection': + try { + final errStr = msg['reason']; + if (hostPromiseRejectionHandler != null) { + hostPromiseRejectionHandler(errStr); + } else { + print("unhandled promise rejection: $errStr"); + } + } catch (e, stack) { + print("host Promise Rejection Handler error: " + + e.toString() + + "\n" + + stack.toString()); + } + break; case 'module': var ptr = Pointer.fromAddress(msg['ptr']); try { diff --git a/lib/wrapper.dart b/lib/wrapper.dart index 68a8436..5a58295 100644 --- a/lib/wrapper.dart +++ b/lib/wrapper.dart @@ -78,10 +78,12 @@ class JSPromise extends JSRefValue { return true; } if (status["__rejected"] == true) { + final err = jsGetPropertyStr(ctx, val, "__error"); completer.completeError(parseJSException( ctx, - e: jsGetPropertyStr(ctx, val, "__error"), + perr: err, )); + jsFreeValue(ctx, err); return true; } return false; @@ -129,8 +131,9 @@ Pointer jsGetPropertyStr(Pointer ctx, Pointer val, String prop) { return jsProp; } -String parseJSException(Pointer ctx, {Pointer e}) { - e = e ?? jsGetException(ctx); +String parseJSException(Pointer ctx, {Pointer perr}) { + final e = perr ?? jsGetException(ctx); + var err = jsToCString(ctx, e); if (jsValueGetTag(e) == JSTag.OBJECT) { Pointer stack = jsGetPropertyStr(ctx, e, "stack"); @@ -139,7 +142,7 @@ String parseJSException(Pointer ctx, {Pointer e}) { } jsFreeValue(ctx, stack); } - jsFreeValue(ctx, e); + if (perr == null) jsFreeValue(ctx, e); return err; } diff --git a/test/flutter_qjs_test.dart b/test/flutter_qjs_test.dart index 50b8cfc..0ddc8ad 100644 --- a/test/flutter_qjs_test.dart +++ b/test/flutter_qjs_test.dart @@ -100,6 +100,7 @@ void main() async { moduleHandler: (name) { return "export default '${new DateTime.now()}'"; }, + hostPromiseRejectionHandler: (_) {}, ); qjs.dispatch(); await testEvaluate(qjs); @@ -112,6 +113,7 @@ void main() async { moduleHandler: (name) async { return "export default '${new DateTime.now()}'"; }, + hostPromiseRejectionHandler: (_) {}, ); await testEvaluate(qjs); qjs.close(); @@ -141,4 +143,22 @@ void main() async { } qjs.close(); }); + test('host promise rejection', () async { + final completer = Completer(); + final qjs = FlutterQjs( + hostPromiseRejectionHandler: (reason) { + completer.complete(reason); + }, + ); + qjs.dispatch(); + qjs.evaluate( + "(() => { Promise.resolve().then(() => { throw 'unhandle' }) })()", + name: ""); + Future.delayed(Duration(seconds: 10)).then((value) { + if (!completer.isCompleted) completer.completeError("not host reject"); + }); + expect(await completer.future, "unhandle", + reason: "host promise rejection"); + qjs.close(); + }); }