This commit is contained in:
ekibun
2021-01-26 01:20:25 +08:00
parent b790073045
commit 896d563ba8
9 changed files with 166 additions and 89 deletions

View File

@@ -9,6 +9,7 @@
## 0.3.2 ## 0.3.2
* fix Promise reject cannot get Exception string. * fix Promise reject cannot get Exception string.
* wrap JSError.
## 0.3.1 ## 0.3.1

View File

@@ -56,6 +56,7 @@ Data conversion between dart and js are implemented as follow:
| Map | Object | | Map | Object |
| Function<br>JSInvokable | function(....args) | | Function<br>JSInvokable | function(....args) |
| Future | Promise | | Future | Promise |
| JSError | Error |
| Object | DartObject | | Object | DartObject |
**notice:** `JSInvokable` does not extend `Function`, but can be used same as `Function`. **notice:** `JSInvokable` does not extend `Function`, but can be used same as `Function`.

View File

@@ -13,9 +13,9 @@
extern "C" 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() DLLEXPORT JSValue *jsEXCEPTION()
@@ -293,6 +293,16 @@ extern "C"
return JS_IsArray(ctx, *val); 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, DLLEXPORT JSValue *jsGetProperty(JSContext *ctx, JSValueConst *this_obj,
JSAtom prop) JSAtom prop)
{ {

View File

@@ -17,7 +17,7 @@ extern "C"
typedef void *JSChannel(JSContext *ctx, size_t type, void *argv); 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(); DLLEXPORT JSValue *jsEXCEPTION();
@@ -95,6 +95,10 @@ extern "C"
DLLEXPORT int32_t jsIsArray(JSContext *ctx, JSValueConst *val); 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, DLLEXPORT JSValue *jsGetProperty(JSContext *ctx, JSValueConst *this_obj,
JSAtom prop); JSAtom prop);

View File

@@ -67,26 +67,19 @@ final DynamicLibrary _qjsLib = Platform.environment['FLUTTER_TEST'] == 'true'
? DynamicLibrary.open('libqjs.so') ? DynamicLibrary.open('libqjs.so')
: DynamicLibrary.process()); : DynamicLibrary.process());
/// JSValue *jsThrowInternalError(JSContext *ctx, char *message) /// DLLEXPORT JSValue *jsThrow(JSContext *ctx, JSValue *obj)
final Pointer Function( final Pointer Function(
Pointer ctx, Pointer ctx,
Pointer<Utf8> message, Pointer obj,
) _jsThrowInternalError = _qjsLib ) jsThrow = _qjsLib
.lookup< .lookup<
NativeFunction< NativeFunction<
Pointer Function( Pointer Function(
Pointer, Pointer,
Pointer<Utf8>, Pointer,
)>>('jsThrowInternalError') )>>('jsThrow')
.asFunction(); .asFunction();
Pointer jsThrowInternalError(Pointer ctx, String message) {
var utf8message = Utf8.toUtf8(message);
var val = _jsThrowInternalError(ctx, utf8message);
free(utf8message);
return val;
}
/// JSValue *jsEXCEPTION() /// JSValue *jsEXCEPTION()
final Pointer Function() jsEXCEPTION = _qjsLib final Pointer Function() jsEXCEPTION = _qjsLib
.lookup<NativeFunction<Pointer Function()>>('jsEXCEPTION') .lookup<NativeFunction<Pointer Function()>>('jsEXCEPTION')
@@ -117,6 +110,7 @@ class RuntimeOpaque {
List<JSRef> ref = []; List<JSRef> ref = [];
ReceivePort port; ReceivePort port;
int dartObjectClassId; int dartObjectClassId;
int jsExceptionClassId;
} }
final Map<Pointer, RuntimeOpaque> runtimeOpaques = Map(); final Map<Pointer, RuntimeOpaque> runtimeOpaques = Map();
@@ -164,10 +158,11 @@ final void Function(
void jsFreeRuntime( void jsFreeRuntime(
Pointer rt, Pointer rt,
) { ) {
runtimeOpaques[rt]?.ref?.forEach((val) { while (0 < runtimeOpaques[rt]?.ref?.length ?? 0) {
val.release(); final ref = runtimeOpaques[rt]?.ref?.first;
}); ref.release();
runtimeOpaques.remove(rt); runtimeOpaques[rt]?.ref?.remove(ref);
}
_jsFreeRuntime(rt); _jsFreeRuntime(rt);
} }
@@ -200,6 +195,7 @@ Pointer jsNewContext(Pointer rt) {
final runtimeOpaque = runtimeOpaques[rt]; final runtimeOpaque = runtimeOpaques[rt];
if (runtimeOpaque == null) throw Exception('Runtime has been released!'); if (runtimeOpaque == null) throw Exception('Runtime has been released!');
runtimeOpaque.dartObjectClassId = jsNewClass(ctx, 'DartObject'); runtimeOpaque.dartObjectClassId = jsNewClass(ctx, 'DartObject');
runtimeOpaque.jsExceptionClassId = jsNewClass(ctx, 'JSException');
return ctx; return ctx;
} }
@@ -651,6 +647,30 @@ final int Function(
)>>('jsIsArray') )>>('jsIsArray')
.asFunction(); .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, /// JSValue *jsGetProperty(JSContext *ctx, JSValueConst *this_obj,
/// JSAtom prop) /// JSAtom prop)
final Pointer Function( final Pointer Function(

View File

@@ -50,25 +50,24 @@ class FlutterQjs {
case JSChannelType.METHON: case JSChannelType.METHON:
final pdata = ptr.cast<Pointer>(); final pdata = ptr.cast<Pointer>();
final argc = pdata.elementAt(1).value.cast<Int32>().value; final argc = pdata.elementAt(1).value.cast<Int32>().value;
List pargs = <Pointer>[]; List pargs = [];
for (var i = 0; i < argc; i++) { for (var i = 0; i < argc; i++) {
pargs.add(Pointer.fromAddress( pargs.add(jsToDart(
ctx,
Pointer.fromAddress(
pdata.elementAt(2).value.address + sizeOfJSValue * i, pdata.elementAt(2).value.address + sizeOfJSValue * i,
),
)); ));
} }
final pThis = pdata.elementAt(0).value;
JSInvokable func = jsToDart(ctx, pdata.elementAt(3).value); JSInvokable func = jsToDart(ctx, pdata.elementAt(3).value);
if (func is NativeJSInvokable) {
return dartToJs(ctx, func.invokeNative(ctx, pThis, pargs));
}
return dartToJs( return dartToJs(
ctx, ctx,
func.invoke( func.invoke(
pargs.map((e) => jsToDart(ctx, e)).toList(), pargs,
jsToDart(ctx, pThis), jsToDart(ctx, pdata.elementAt(0).value),
)); ));
case JSChannelType.MODULE: case JSChannelType.MODULE:
if (moduleHandler == null) throw Exception('No ModuleHandler'); if (moduleHandler == null) throw JSError('No ModuleHandler');
var ret = Utf8.toUtf8(moduleHandler( var ret = Utf8.toUtf8(moduleHandler(
Utf8.fromUtf8(ptr.cast<Utf8>()), Utf8.fromUtf8(ptr.cast<Utf8>()),
)); ));
@@ -79,7 +78,7 @@ class FlutterQjs {
case JSChannelType.PROMISE_TRACK: case JSChannelType.PROMISE_TRACK:
final errStr = parseJSException(ctx, ptr); final errStr = parseJSException(ctx, ptr);
if (hostPromiseRejectionHandler != null) { if (hostPromiseRejectionHandler != null) {
hostPromiseRejectionHandler(errStr); hostPromiseRejectionHandler(errStr.toString());
} else { } else {
print('unhandled promise rejection: $errStr'); print('unhandled promise rejection: $errStr');
} }
@@ -91,21 +90,19 @@ class FlutterQjs {
runtimeOpaques[rt]?.ref?.remove(obj); runtimeOpaques[rt]?.ref?.remove(obj);
return Pointer.fromAddress(0); return Pointer.fromAddress(0);
} }
throw Exception('call channel with wrong type'); throw JSError('call channel with wrong type');
} catch (e, stack) { } catch (e) {
final errStr = e.toString() + '\n' + stack.toString();
if (type == JSChannelType.FREE_OBJECT) { if (type == JSChannelType.FREE_OBJECT) {
print('DartObject release error: ' + errStr); print('DartObject release error: $e');
return Pointer.fromAddress(0); return Pointer.fromAddress(0);
} }
if (type == JSChannelType.MODULE) { if (type == JSChannelType.MODULE) {
print('host Promise Rejection Handler error: ' + errStr); print('host Promise Rejection Handler error: $e');
return Pointer.fromAddress(0); return Pointer.fromAddress(0);
} }
var err = jsThrowInternalError( var throwObj = dartToJs(ctx, e);
ctx, var err = jsThrow(ctx, throwObj);
errStr, jsFreeValue(ctx, throwObj);
);
if (type == JSChannelType.MODULE) { if (type == JSChannelType.MODULE) {
jsFreeValue(ctx, err); jsFreeValue(ctx, err);
return Pointer.fromAddress(0); return Pointer.fromAddress(0);

View File

@@ -35,7 +35,7 @@ void _runJsIsolate(Map spawnMessage) async {
'ptr': ptr.address, 'ptr': ptr.address,
}); });
while (ptr.value.address == 0) sleep(Duration.zero); 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); var ret = Utf8.fromUtf8(ptr.value);
sendPort.send({ sendPort.send({
'type': 'release', 'type': 'release',
@@ -76,10 +76,10 @@ void _runJsIsolate(Map spawnMessage) async {
msgPort.send({ msgPort.send({
'data': encodeData(data), 'data': encodeData(data),
}); });
} catch (e, stack) { } catch (e) {
if (msgPort != null) if (msgPort != null)
msgPort.send({ msgPort.send({
'error': e.toString() + '\n' + stack.toString(), 'error': encodeData(e),
}); });
} }
}); });
@@ -137,11 +137,8 @@ class IsolateQjs {
} else { } else {
print('unhandled promise rejection: $errStr'); print('unhandled promise rejection: $errStr');
} }
} catch (e, stack) { } catch (e) {
print('host Promise Rejection Handler error: ' + print('host Promise Rejection Handler error: $e');
e.toString() +
'\n' +
stack.toString());
} }
break; break;
case 'module': case 'module':
@@ -197,6 +194,6 @@ class IsolateQjs {
if (result.containsKey('data')) { if (result.containsKey('data')) {
return decodeData(result['data'], sendPort); return decodeData(result['data'], sendPort);
} else } else
throw result['error']; throw decodeData(result['error'], sendPort);
} }
} }

View File

@@ -12,6 +12,37 @@ import 'dart:typed_data';
import 'package:ffi/ffi.dart'; import 'package:ffi/ffi.dart';
import '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 { abstract class JSInvokable {
dynamic invoke(List args, [dynamic thisVal]); dynamic invoke(List args, [dynamic thisVal]);
@@ -32,19 +63,19 @@ abstract class JSInvokable {
} }
} }
class NativeJSInvokable extends JSInvokable { // class NativeJSInvokable extends JSInvokable {
dynamic Function(Pointer ctx, Pointer thisVal, List<Pointer> args) _func; // dynamic Function(Pointer ctx, Pointer thisVal, List<Pointer> args) _func;
NativeJSInvokable(this._func); // NativeJSInvokable(this._func);
@override // @override
dynamic invoke(List args, [dynamic thisVal]) { // dynamic invoke(List args, [dynamic thisVal]) {
throw UnimplementedError('use invokeNative instead.'); // throw UnimplementedError('use invokeNative instead.');
} // }
invokeNative(Pointer ctx, Pointer thisVal, List<Pointer> args) { // invokeNative(Pointer ctx, Pointer thisVal, List<Pointer> args) {
_func(ctx, thisVal, args); // _func(ctx, thisVal, args);
} // }
} // }
class _DartFunction extends JSInvokable { class _DartFunction extends JSInvokable {
Function _func; Function _func;
@@ -181,7 +212,7 @@ class IsolateJSFunction extends JSInvokable {
if (result.containsKey('data')) if (result.containsKey('data'))
return decodeData(result['data'], port); return decodeData(result['data'], port);
else else
throw result['error']; throw decodeData(result['error'], port);
} }
} }
@@ -205,10 +236,10 @@ class IsolateFunction extends JSInvokable implements DartReleasable {
msgPort.send({ msgPort.send({
'data': encodeData(data), 'data': encodeData(data),
}); });
} catch (e, stack) { } catch (e) {
if (msgPort != null) if (msgPort != null)
msgPort.send({ 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')) if (result.containsKey('data'))
return decodeData(result['data'], _port); return decodeData(result['data'], _port);
else else
throw result['error']; throw decodeData(result['error'], _port);
} }
@override @override
@@ -242,6 +273,7 @@ class IsolateFunction extends JSInvokable implements DartReleasable {
dynamic encodeData(dynamic data, {Map<dynamic, dynamic> cache}) { dynamic encodeData(dynamic data, {Map<dynamic, dynamic> cache}) {
if (cache == null) cache = Map(); if (cache == null) cache = Map();
if (data is JSError) return data.encode();
if (cache.containsKey(data)) return cache[data]; if (cache.containsKey(data)) return cache[data];
if (data is List) { if (data is List) {
var ret = []; var ret = [];
@@ -285,11 +317,10 @@ dynamic encodeData(dynamic data, {Map<dynamic, dynamic> cache}) {
futurePort.close(); futurePort.close();
(port as SendPort).send({'data': encodeData(value)}); (port as SendPort).send({'data': encodeData(value)});
}); });
}, onError: (e, stack) { }, onError: (e) {
futurePort.first.then((port) { futurePort.first.then((port) {
futurePort.close(); futurePort.close();
(port as SendPort) (port as SendPort).send({'error': encodeData(e)});
.send({'error': e.toString() + '\n' + stack.toString()});
}); });
}); });
return { return {
@@ -311,6 +342,8 @@ dynamic decodeData(dynamic data, SendPort port, {Map<dynamic, dynamic> cache}) {
return ret; return ret;
} }
if (data is Map) { if (data is Map) {
final jsException = JSError.decode(data);
if (jsException != null) return jsException;
if (data.containsKey('__js_obj_val')) { if (data.containsKey('__js_obj_val')) {
int ctx = data['__js_obj_ctx']; int ctx = data['__js_obj_ctx'];
int val = data['__js_obj_val']; int val = data['__js_obj_val'];
@@ -340,9 +373,9 @@ dynamic decodeData(dynamic data, SendPort port, {Map<dynamic, dynamic> cache}) {
futurePort.first.then((value) { futurePort.first.then((value) {
futurePort.close(); futurePort.close();
if (value['error'] != null) { if (value['error'] != null) {
futureCompleter.completeError(value['error']); futureCompleter.completeError(decodeData(value['error'], port));
} else { } else {
futureCompleter.complete(value['data']); futureCompleter.complete(decodeData(value['data'], port));
} }
}); });
return futureCompleter.future; return futureCompleter.future;
@@ -358,16 +391,13 @@ dynamic decodeData(dynamic data, SendPort port, {Map<dynamic, dynamic> cache}) {
return data; return data;
} }
String parseJSException(Pointer ctx, [Pointer perr]) { dynamic parseJSException(Pointer ctx, [Pointer perr]) {
final e = perr ?? jsGetException(ctx); final e = perr ?? jsGetException(ctx);
var err;
var err = jsToCString(ctx, e); try {
if (jsValueGetTag(e) == JSTag.OBJECT) { err = jsToDart(ctx, e);
Pointer stack = jsGetPropertyValue(ctx, e, 'stack'); } catch (exception) {
if (jsToBool(ctx, stack) != 0) { err = exception;
err += '\n' + jsToCString(ctx, stack);
}
jsFreeValue(ctx, stack);
} }
if (perr == null) jsFreeValue(ctx, e); if (perr == null) jsFreeValue(ctx, e);
return err; return err;
@@ -409,6 +439,13 @@ Pointer jsGetPropertyValue(
Pointer dartToJs(Pointer ctx, dynamic val, {Map<dynamic, dynamic> cache}) { Pointer dartToJs(Pointer ctx, dynamic val, {Map<dynamic, dynamic> cache}) {
if (val == null) return jsUNDEFINED(); 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 JSObject) return jsDupValue(ctx, val._val);
if (val is Future) { if (val is Future) {
var resolvingFunc = allocate<Uint8>(count: sizeOfJSValue * 2); var resolvingFunc = allocate<Uint8>(count: sizeOfJSValue * 2);
@@ -422,8 +459,8 @@ Pointer dartToJs(Pointer ctx, dynamic val, {Map<dynamic, dynamic> cache}) {
free(resolvingFunc); free(resolvingFunc);
val.then((value) { val.then((value) {
res(value); res(value);
}, onError: (e, stack) { }, onError: (e) {
rej(e.toString() + '\n' + stack.toString()); rej(e);
}); });
return ret; return ret;
} }
@@ -511,6 +548,15 @@ dynamic jsToDart(Pointer ctx, Pointer val, {Map<int, dynamic> cache}) {
} }
if (jsIsFunction(ctx, val) != 0) { if (jsIsFunction(ctx, val) != 0) {
return JSFunction(ctx, val); 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) { } else if (jsIsPromise(ctx, val) != 0) {
Pointer jsPromiseThen = jsGetPropertyValue(ctx, val, 'then'); Pointer jsPromiseThen = jsGetPropertyValue(ctx, val, 'then');
JSFunction promiseThen = jsToDart(ctx, jsPromiseThen, cache: cache); JSFunction promiseThen = jsToDart(ctx, jsPromiseThen, cache: cache);
@@ -521,11 +567,9 @@ dynamic jsToDart(Pointer ctx, Pointer val, {Map<int, dynamic> cache}) {
(v) { (v) {
if (!completer.isCompleted) completer.complete(v); if (!completer.isCompleted) completer.complete(v);
}, },
NativeJSInvokable((ctx, thisVal, args) { (e) {
if (!completer.isCompleted) if (!completer.isCompleted) completer.completeError(e);
completer },
.completeError(parseJSException(ctx, args[0]));
}),
], JSObject.fromAddress(ctx, val)); ], JSObject.fromAddress(ctx, val));
bool isException = jsIsException(jsRet) != 0; bool isException = jsIsException(jsRet) != 0;
jsFreeValue(ctx, jsRet); jsFreeValue(ctx, jsRet);

View File

@@ -12,6 +12,7 @@ import 'dart:io';
import 'package:flutter_qjs/flutter_qjs.dart'; import 'package:flutter_qjs/flutter_qjs.dart';
import 'package:flutter_qjs/isolate.dart'; import 'package:flutter_qjs/isolate.dart';
import 'package:flutter_qjs/ffi.dart'; import 'package:flutter_qjs/ffi.dart';
import 'package:flutter_qjs/wrapper.dart';
import 'package:flutter_test/flutter_test.dart'; import 'package:flutter_test/flutter_test.dart';
dynamic myFunction(String args, {thisVal}) { dynamic myFunction(String args, {thisVal}) {
@@ -30,6 +31,10 @@ Future testEvaluate(qjs) async {
for (var i = 0; i < primities.length; i++) { for (var i = 0; i < primities.length; i++) {
expect(wrapPrimities[i], primities[i], reason: "wrap primities"); 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 wrapFunction = await testWrap(testWrap);
final testEqual = await qjs.evaluate( final testEqual = await qjs.evaluate(
"(a, b) => a === b", "(a, b) => a === b",
@@ -60,8 +65,7 @@ Future testEvaluate(qjs) async {
await promises[0]; await promises[0];
throw 'Future not reject'; throw 'Future not reject';
} catch (e) { } catch (e) {
expect(e, startsWith('test Promise.reject\n'), expect(e, 'test Promise.reject', reason: "promise object reject");
reason: "promise object reject");
} }
expect(await promises[1], 'test Promise.resolve', expect(await promises[1], 'test Promise.resolve',
reason: "promise object resolve"); reason: "promise object resolve");
@@ -156,9 +160,8 @@ void main() async {
final qjs = FlutterQjs(); final qjs = FlutterQjs();
try { try {
qjs.evaluate("a=()=>a();a();", name: "<eval>"); qjs.evaluate("a=()=>a();a();", name: "<eval>");
} catch (e) { } on JSError catch (e) {
expect( expect(e.message, 'InternalError: stack overflow',
e.toString(), startsWith('InternalError: stack overflow'),
reason: "throw stack overflow"); reason: "throw stack overflow");
} }
qjs.close(); qjs.close();