diff --git a/CHANGELOG.md b/CHANGELOG.md index a2a2e57..b1698fb 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,7 @@ ## 0.3.2 * fix Promise reject cannot get Exception string. +* wrap JSError. ## 0.3.1 diff --git a/README.md b/README.md index 2a85e43..ed6a242 100644 --- a/README.md +++ b/README.md @@ -56,6 +56,7 @@ Data conversion between dart and js are implemented as follow: | Map | Object | | Function
JSInvokable | function(....args) | | Future | Promise | +| JSError | Error | | Object | DartObject | **notice:** `JSInvokable` does not extend `Function`, but can be used same as `Function`. diff --git a/cxx/ffi.cpp b/cxx/ffi.cpp index 1402678..3e799da 100644 --- a/cxx/ffi.cpp +++ b/cxx/ffi.cpp @@ -13,9 +13,9 @@ extern "C" { - DLLEXPORT JSValue *jsThrowInternalError(JSContext *ctx, char *message) + DLLEXPORT JSValue *jsThrow(JSContext *ctx, JSValue *obj) { - return new JSValue(JS_ThrowInternalError(ctx, "%s", message)); + return new JSValue(JS_Throw(ctx, *obj)); } DLLEXPORT JSValue *jsEXCEPTION() @@ -293,6 +293,16 @@ extern "C" return JS_IsArray(ctx, *val); } + DLLEXPORT int32_t jsIsError(JSContext *ctx, JSValueConst *val) + { + return JS_IsError(ctx, *val); + } + + DLLEXPORT JSValue *jsNewError(JSContext *ctx) + { + return new JSValue(JS_NewError(ctx)); + } + DLLEXPORT JSValue *jsGetProperty(JSContext *ctx, JSValueConst *this_obj, JSAtom prop) { diff --git a/cxx/ffi.h b/cxx/ffi.h index a1ca636..66fddb3 100644 --- a/cxx/ffi.h +++ b/cxx/ffi.h @@ -17,7 +17,7 @@ extern "C" typedef void *JSChannel(JSContext *ctx, size_t type, void *argv); - DLLEXPORT JSValue *jsThrowInternalError(JSContext *ctx, char *message); + DLLEXPORT JSValue *jsThrow(JSContext *ctx, JSValue *obj); DLLEXPORT JSValue *jsEXCEPTION(); @@ -95,6 +95,10 @@ extern "C" DLLEXPORT int32_t jsIsArray(JSContext *ctx, JSValueConst *val); + DLLEXPORT int32_t jsIsError(JSContext *ctx, JSValueConst *val); + + DLLEXPORT JSValue *jsNewError(JSContext *ctx); + DLLEXPORT JSValue *jsGetProperty(JSContext *ctx, JSValueConst *this_obj, JSAtom prop); diff --git a/lib/ffi.dart b/lib/ffi.dart index 4a07bd5..44e34c0 100644 --- a/lib/ffi.dart +++ b/lib/ffi.dart @@ -67,26 +67,19 @@ final DynamicLibrary _qjsLib = Platform.environment['FLUTTER_TEST'] == 'true' ? DynamicLibrary.open('libqjs.so') : DynamicLibrary.process()); -/// JSValue *jsThrowInternalError(JSContext *ctx, char *message) +/// DLLEXPORT JSValue *jsThrow(JSContext *ctx, JSValue *obj) final Pointer Function( Pointer ctx, - Pointer message, -) _jsThrowInternalError = _qjsLib + Pointer obj, +) jsThrow = _qjsLib .lookup< NativeFunction< Pointer Function( Pointer, - Pointer, - )>>('jsThrowInternalError') + Pointer, + )>>('jsThrow') .asFunction(); -Pointer jsThrowInternalError(Pointer ctx, String message) { - var utf8message = Utf8.toUtf8(message); - var val = _jsThrowInternalError(ctx, utf8message); - free(utf8message); - return val; -} - /// JSValue *jsEXCEPTION() final Pointer Function() jsEXCEPTION = _qjsLib .lookup>('jsEXCEPTION') @@ -117,6 +110,7 @@ class RuntimeOpaque { List ref = []; ReceivePort port; int dartObjectClassId; + int jsExceptionClassId; } final Map runtimeOpaques = Map(); @@ -164,10 +158,11 @@ final void Function( void jsFreeRuntime( Pointer rt, ) { - runtimeOpaques[rt]?.ref?.forEach((val) { - val.release(); - }); - runtimeOpaques.remove(rt); + while (0 < runtimeOpaques[rt]?.ref?.length ?? 0) { + final ref = runtimeOpaques[rt]?.ref?.first; + ref.release(); + runtimeOpaques[rt]?.ref?.remove(ref); + } _jsFreeRuntime(rt); } @@ -200,6 +195,7 @@ Pointer jsNewContext(Pointer rt) { final runtimeOpaque = runtimeOpaques[rt]; if (runtimeOpaque == null) throw Exception('Runtime has been released!'); runtimeOpaque.dartObjectClassId = jsNewClass(ctx, 'DartObject'); + runtimeOpaque.jsExceptionClassId = jsNewClass(ctx, 'JSException'); return ctx; } @@ -651,6 +647,30 @@ final int Function( )>>('jsIsArray') .asFunction(); +/// DLLEXPORT int32_t jsIsError(JSContext *ctx, JSValueConst *val); +final int Function( + Pointer ctx, + Pointer val, +) jsIsError = _qjsLib + .lookup< + NativeFunction< + Int32 Function( + Pointer, + Pointer, + )>>('jsIsError') + .asFunction(); + +/// DLLEXPORT JSValue *jsNewError(JSContext *ctx); +final Pointer Function( + Pointer ctx, +) jsNewError = _qjsLib + .lookup< + NativeFunction< + Pointer Function( + Pointer, + )>>('jsNewError') + .asFunction(); + /// JSValue *jsGetProperty(JSContext *ctx, JSValueConst *this_obj, /// JSAtom prop) final Pointer Function( diff --git a/lib/flutter_qjs.dart b/lib/flutter_qjs.dart index 07f3db4..cb2513b 100644 --- a/lib/flutter_qjs.dart +++ b/lib/flutter_qjs.dart @@ -50,25 +50,24 @@ class FlutterQjs { case JSChannelType.METHON: final pdata = ptr.cast(); final argc = pdata.elementAt(1).value.cast().value; - List pargs = []; + List pargs = []; for (var i = 0; i < argc; i++) { - pargs.add(Pointer.fromAddress( - pdata.elementAt(2).value.address + sizeOfJSValue * i, + pargs.add(jsToDart( + ctx, + Pointer.fromAddress( + pdata.elementAt(2).value.address + sizeOfJSValue * i, + ), )); } - final pThis = pdata.elementAt(0).value; JSInvokable func = jsToDart(ctx, pdata.elementAt(3).value); - if (func is NativeJSInvokable) { - return dartToJs(ctx, func.invokeNative(ctx, pThis, pargs)); - } return dartToJs( ctx, func.invoke( - pargs.map((e) => jsToDart(ctx, e)).toList(), - jsToDart(ctx, pThis), + pargs, + jsToDart(ctx, pdata.elementAt(0).value), )); case JSChannelType.MODULE: - if (moduleHandler == null) throw Exception('No ModuleHandler'); + if (moduleHandler == null) throw JSError('No ModuleHandler'); var ret = Utf8.toUtf8(moduleHandler( Utf8.fromUtf8(ptr.cast()), )); @@ -79,7 +78,7 @@ class FlutterQjs { case JSChannelType.PROMISE_TRACK: final errStr = parseJSException(ctx, ptr); if (hostPromiseRejectionHandler != null) { - hostPromiseRejectionHandler(errStr); + hostPromiseRejectionHandler(errStr.toString()); } else { print('unhandled promise rejection: $errStr'); } @@ -91,21 +90,19 @@ class FlutterQjs { runtimeOpaques[rt]?.ref?.remove(obj); return Pointer.fromAddress(0); } - throw Exception('call channel with wrong type'); - } catch (e, stack) { - final errStr = e.toString() + '\n' + stack.toString(); + throw JSError('call channel with wrong type'); + } catch (e) { if (type == JSChannelType.FREE_OBJECT) { - print('DartObject release error: ' + errStr); + print('DartObject release error: $e'); return Pointer.fromAddress(0); } if (type == JSChannelType.MODULE) { - print('host Promise Rejection Handler error: ' + errStr); + print('host Promise Rejection Handler error: $e'); return Pointer.fromAddress(0); } - var err = jsThrowInternalError( - ctx, - errStr, - ); + var throwObj = dartToJs(ctx, e); + var err = jsThrow(ctx, throwObj); + jsFreeValue(ctx, throwObj); if (type == JSChannelType.MODULE) { jsFreeValue(ctx, err); return Pointer.fromAddress(0); diff --git a/lib/isolate.dart b/lib/isolate.dart index 15dfc62..5f481dc 100644 --- a/lib/isolate.dart +++ b/lib/isolate.dart @@ -35,7 +35,7 @@ void _runJsIsolate(Map spawnMessage) async { 'ptr': ptr.address, }); while (ptr.value.address == 0) sleep(Duration.zero); - if (ptr.value.address == -1) throw Exception('Module Not found'); + if (ptr.value.address == -1) throw JSError('Module Not found'); var ret = Utf8.fromUtf8(ptr.value); sendPort.send({ 'type': 'release', @@ -76,10 +76,10 @@ void _runJsIsolate(Map spawnMessage) async { msgPort.send({ 'data': encodeData(data), }); - } catch (e, stack) { + } catch (e) { if (msgPort != null) msgPort.send({ - 'error': e.toString() + '\n' + stack.toString(), + 'error': encodeData(e), }); } }); @@ -137,11 +137,8 @@ class IsolateQjs { } else { print('unhandled promise rejection: $errStr'); } - } catch (e, stack) { - print('host Promise Rejection Handler error: ' + - e.toString() + - '\n' + - stack.toString()); + } catch (e) { + print('host Promise Rejection Handler error: $e'); } break; case 'module': @@ -197,6 +194,6 @@ class IsolateQjs { if (result.containsKey('data')) { return decodeData(result['data'], sendPort); } else - throw result['error']; + throw decodeData(result['error'], sendPort); } } diff --git a/lib/wrapper.dart b/lib/wrapper.dart index fb4f52f..b172962 100644 --- a/lib/wrapper.dart +++ b/lib/wrapper.dart @@ -12,6 +12,37 @@ import 'dart:typed_data'; import 'package:ffi/ffi.dart'; import 'ffi.dart'; +class JSError { + String message; + String stack; + JSError(message, [stack]) { + if (message is JSError) { + this.message = message.message; + this.stack = message.stack; + } else { + this.message = message.toString(); + this.stack = (stack ?? StackTrace.current).toString(); + } + } + @override + String toString() { + return stack == null ? message.toString() : "$message\n$stack"; + } + + static JSError decode(Map obj) { + if (obj.containsKey('__js_error')) + return JSError(obj['__js_error'], obj['__js_error_stack']); + return null; + } + + Map encode() { + return { + '__js_error': message, + '__js_error_stack': stack, + }; + } +} + abstract class JSInvokable { dynamic invoke(List args, [dynamic thisVal]); @@ -32,19 +63,19 @@ abstract class JSInvokable { } } -class NativeJSInvokable extends JSInvokable { - dynamic Function(Pointer ctx, Pointer thisVal, List args) _func; - NativeJSInvokable(this._func); +// class NativeJSInvokable extends JSInvokable { +// dynamic Function(Pointer ctx, Pointer thisVal, List args) _func; +// NativeJSInvokable(this._func); - @override - dynamic invoke(List args, [dynamic thisVal]) { - throw UnimplementedError('use invokeNative instead.'); - } +// @override +// dynamic invoke(List args, [dynamic thisVal]) { +// throw UnimplementedError('use invokeNative instead.'); +// } - invokeNative(Pointer ctx, Pointer thisVal, List args) { - _func(ctx, thisVal, args); - } -} +// invokeNative(Pointer ctx, Pointer thisVal, List args) { +// _func(ctx, thisVal, args); +// } +// } class _DartFunction extends JSInvokable { Function _func; @@ -181,7 +212,7 @@ class IsolateJSFunction extends JSInvokable { if (result.containsKey('data')) return decodeData(result['data'], port); else - throw result['error']; + throw decodeData(result['error'], port); } } @@ -205,10 +236,10 @@ class IsolateFunction extends JSInvokable implements DartReleasable { msgPort.send({ 'data': encodeData(data), }); - } catch (e, stack) { + } catch (e) { if (msgPort != null) msgPort.send({ - 'error': e.toString() + '\n' + stack.toString(), + 'error': encodeData(e), }); } }); @@ -229,7 +260,7 @@ class IsolateFunction extends JSInvokable implements DartReleasable { if (result.containsKey('data')) return decodeData(result['data'], _port); else - throw result['error']; + throw decodeData(result['error'], _port); } @override @@ -242,6 +273,7 @@ class IsolateFunction extends JSInvokable implements DartReleasable { dynamic encodeData(dynamic data, {Map cache}) { if (cache == null) cache = Map(); + if (data is JSError) return data.encode(); if (cache.containsKey(data)) return cache[data]; if (data is List) { var ret = []; @@ -285,11 +317,10 @@ dynamic encodeData(dynamic data, {Map cache}) { futurePort.close(); (port as SendPort).send({'data': encodeData(value)}); }); - }, onError: (e, stack) { + }, onError: (e) { futurePort.first.then((port) { futurePort.close(); - (port as SendPort) - .send({'error': e.toString() + '\n' + stack.toString()}); + (port as SendPort).send({'error': encodeData(e)}); }); }); return { @@ -311,6 +342,8 @@ dynamic decodeData(dynamic data, SendPort port, {Map cache}) { return ret; } if (data is Map) { + final jsException = JSError.decode(data); + if (jsException != null) return jsException; if (data.containsKey('__js_obj_val')) { int ctx = data['__js_obj_ctx']; int val = data['__js_obj_val']; @@ -340,9 +373,9 @@ dynamic decodeData(dynamic data, SendPort port, {Map cache}) { futurePort.first.then((value) { futurePort.close(); if (value['error'] != null) { - futureCompleter.completeError(value['error']); + futureCompleter.completeError(decodeData(value['error'], port)); } else { - futureCompleter.complete(value['data']); + futureCompleter.complete(decodeData(value['data'], port)); } }); return futureCompleter.future; @@ -358,16 +391,13 @@ dynamic decodeData(dynamic data, SendPort port, {Map cache}) { return data; } -String parseJSException(Pointer ctx, [Pointer perr]) { +dynamic parseJSException(Pointer ctx, [Pointer perr]) { final e = perr ?? jsGetException(ctx); - - var err = jsToCString(ctx, e); - if (jsValueGetTag(e) == JSTag.OBJECT) { - Pointer stack = jsGetPropertyValue(ctx, e, 'stack'); - if (jsToBool(ctx, stack) != 0) { - err += '\n' + jsToCString(ctx, stack); - } - jsFreeValue(ctx, stack); + var err; + try { + err = jsToDart(ctx, e); + } catch (exception) { + err = exception; } if (perr == null) jsFreeValue(ctx, e); return err; @@ -409,6 +439,13 @@ Pointer jsGetPropertyValue( Pointer dartToJs(Pointer ctx, dynamic val, {Map cache}) { if (val == null) return jsUNDEFINED(); + if (val is JSError) { + final ret = jsNewError(ctx); + definePropertyValue(ctx, ret, "name", ""); + definePropertyValue(ctx, ret, "message", val.message); + definePropertyValue(ctx, ret, "stack", val.stack); + return ret; + } if (val is JSObject) return jsDupValue(ctx, val._val); if (val is Future) { var resolvingFunc = allocate(count: sizeOfJSValue * 2); @@ -422,8 +459,8 @@ Pointer dartToJs(Pointer ctx, dynamic val, {Map cache}) { free(resolvingFunc); val.then((value) { res(value); - }, onError: (e, stack) { - rej(e.toString() + '\n' + stack.toString()); + }, onError: (e) { + rej(e); }); return ret; } @@ -511,6 +548,15 @@ dynamic jsToDart(Pointer ctx, Pointer val, {Map cache}) { } if (jsIsFunction(ctx, val) != 0) { return JSFunction(ctx, val); + } else if (jsIsError(ctx, val) != 0) { + final err = jsToCString(ctx, val); + final pstack = jsGetPropertyValue(ctx, val, 'stack'); + var stack; + if (jsToBool(ctx, pstack) != 0) { + stack = jsToCString(ctx, pstack); + } + jsFreeValue(ctx, pstack); + return JSError(err, stack); } else if (jsIsPromise(ctx, val) != 0) { Pointer jsPromiseThen = jsGetPropertyValue(ctx, val, 'then'); JSFunction promiseThen = jsToDart(ctx, jsPromiseThen, cache: cache); @@ -521,11 +567,9 @@ dynamic jsToDart(Pointer ctx, Pointer val, {Map cache}) { (v) { if (!completer.isCompleted) completer.complete(v); }, - NativeJSInvokable((ctx, thisVal, args) { - if (!completer.isCompleted) - completer - .completeError(parseJSException(ctx, args[0])); - }), + (e) { + if (!completer.isCompleted) completer.completeError(e); + }, ], JSObject.fromAddress(ctx, val)); bool isException = jsIsException(jsRet) != 0; jsFreeValue(ctx, jsRet); diff --git a/test/flutter_qjs_test.dart b/test/flutter_qjs_test.dart index 7438833..15580a3 100644 --- a/test/flutter_qjs_test.dart +++ b/test/flutter_qjs_test.dart @@ -12,6 +12,7 @@ import 'dart:io'; import 'package:flutter_qjs/flutter_qjs.dart'; import 'package:flutter_qjs/isolate.dart'; import 'package:flutter_qjs/ffi.dart'; +import 'package:flutter_qjs/wrapper.dart'; import 'package:flutter_test/flutter_test.dart'; dynamic myFunction(String args, {thisVal}) { @@ -30,6 +31,10 @@ Future testEvaluate(qjs) async { for (var i = 0; i < primities.length; i++) { expect(wrapPrimities[i], primities[i], reason: "wrap primities"); } + final jsError = JSError("test Error"); + final wrapJsError = await testWrap(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", @@ -60,8 +65,7 @@ Future testEvaluate(qjs) async { await promises[0]; throw 'Future not reject'; } catch (e) { - expect(e, startsWith('test Promise.reject\n'), - reason: "promise object reject"); + expect(e, 'test Promise.reject', reason: "promise object reject"); } expect(await promises[1], 'test Promise.resolve', reason: "promise object resolve"); @@ -156,9 +160,8 @@ void main() async { final qjs = FlutterQjs(); try { qjs.evaluate("a=()=>a();a();", name: ""); - } catch (e) { - expect( - e.toString(), startsWith('InternalError: stack overflow'), + } on JSError catch (e) { + expect(e.message, 'InternalError: stack overflow', reason: "throw stack overflow"); } qjs.close();