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