host promise rejection

This commit is contained in:
ekibun
2021-01-23 01:19:56 +08:00
parent c7da8abb67
commit 1589f87194
6 changed files with 107 additions and 27 deletions

View File

@@ -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;
}

View File

@@ -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);

View File

@@ -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<Utf8>()),
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);
}

View File

@@ -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<Pointer<Utf8>>();
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<Pointer>.fromAddress(msg['ptr']);
try {

View File

@@ -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;
}

View File

@@ -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: "<eval>");
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();
});
}