reference leak

This commit is contained in:
ekibun
2021-01-26 21:48:43 +08:00
parent 896d563ba8
commit 3fdeca56a2
11 changed files with 1182 additions and 1059 deletions

View File

@@ -1,159 +1,13 @@
/*
* @Description: quickjs engine
* @Author: ekibun
* @Date: 2020-08-08 08:29:09
* @LastEditors: ekibun
* @LastEditTime: 2020-10-06 23:47:13
*/
import 'dart:async';
import 'dart:ffi';
import 'dart:io';
import 'dart:isolate';
import 'dart:typed_data';
import 'package:ffi/ffi.dart';
import 'ffi.dart';
import 'wrapper.dart';
import 'src/ffi.dart';
export 'src/ffi.dart' show JSEvalFlag;
/// Handler function to manage js module.
typedef JsModuleHandler = String Function(String name);
/// Handler to manage unhandled promise rejection.
typedef JsHostPromiseRejectionHandler = void Function(String reason);
/// Quickjs engine for flutter.
class FlutterQjs {
Pointer _rt;
Pointer _ctx;
/// Max stack size for quickjs.
final int stackSize;
/// Message Port for event loop. Close it to stop dispatching event loop.
ReceivePort port = ReceivePort();
/// Handler function to manage js module.
JsModuleHandler moduleHandler;
/// Handler function to manage js module.
JsHostPromiseRejectionHandler hostPromiseRejectionHandler;
FlutterQjs({
this.moduleHandler,
this.stackSize,
this.hostPromiseRejectionHandler,
});
_ensureEngine() {
if (_rt != null) return;
_rt = jsNewRuntime((ctx, type, ptr) {
try {
switch (type) {
case JSChannelType.METHON:
final pdata = ptr.cast<Pointer>();
final argc = pdata.elementAt(1).value.cast<Int32>().value;
List pargs = [];
for (var i = 0; i < argc; i++) {
pargs.add(jsToDart(
ctx,
Pointer.fromAddress(
pdata.elementAt(2).value.address + sizeOfJSValue * i,
),
));
}
JSInvokable func = jsToDart(ctx, pdata.elementAt(3).value);
return dartToJs(
ctx,
func.invoke(
pargs,
jsToDart(ctx, pdata.elementAt(0).value),
));
case JSChannelType.MODULE:
if (moduleHandler == null) throw JSError('No ModuleHandler');
var ret = Utf8.toUtf8(moduleHandler(
Utf8.fromUtf8(ptr.cast<Utf8>()),
));
Future.microtask(() {
free(ret);
});
return ret;
case JSChannelType.PROMISE_TRACK:
final errStr = parseJSException(ctx, ptr);
if (hostPromiseRejectionHandler != null) {
hostPromiseRejectionHandler(errStr.toString());
} else {
print('unhandled promise rejection: $errStr');
}
return Pointer.fromAddress(0);
case JSChannelType.FREE_OBJECT:
Pointer rt = ctx;
DartObject obj = DartObject.fromAddress(rt, ptr.address);
obj?.release();
runtimeOpaques[rt]?.ref?.remove(obj);
return Pointer.fromAddress(0);
}
throw JSError('call channel with wrong type');
} catch (e) {
if (type == JSChannelType.FREE_OBJECT) {
print('DartObject release error: $e');
return Pointer.fromAddress(0);
}
if (type == JSChannelType.MODULE) {
print('host Promise Rejection Handler error: $e');
return Pointer.fromAddress(0);
}
var throwObj = dartToJs(ctx, e);
var err = jsThrow(ctx, throwObj);
jsFreeValue(ctx, throwObj);
if (type == JSChannelType.MODULE) {
jsFreeValue(ctx, err);
return Pointer.fromAddress(0);
}
return err;
}
}, port);
if (this.stackSize != null && this.stackSize > 0)
jsSetMaxStackSize(_rt, this.stackSize);
_ctx = jsNewContext(_rt);
}
/// Free Runtime and Context which can be recreate when evaluate again.
close() {
if (_rt != null) {
jsFreeContext(_ctx);
jsFreeRuntime(_rt);
}
_rt = null;
_ctx = null;
}
/// Dispatch JavaScript Event loop.
Future<void> dispatch() async {
await for (var _ in port) {
if (_rt == null) continue;
while (true) {
int err = jsExecutePendingJob(_rt);
if (err <= 0) {
if (err < 0) print(parseJSException(_ctx));
break;
}
}
}
}
/// Evaluate js script.
dynamic evaluate(String command, {String name, int evalFlags}) {
_ensureEngine();
var jsval = jsEval(
_ctx,
command,
name ?? '<eval>',
evalFlags ?? JSEvalFlag.GLOBAL,
);
if (jsIsException(jsval) != 0) {
jsFreeValue(_ctx, jsval);
throw parseJSException(_ctx);
}
var result = jsToDart(_ctx, jsval);
jsFreeValue(_ctx, jsval);
return result;
}
}
part 'src/engine.dart';
part 'src/isolate.dart';
part 'src/wrapper.dart';
part 'src/object.dart';

View File

@@ -1,199 +0,0 @@
/*
* @Description:
* @Author: ekibun
* @Date: 2020-10-02 13:49:03
* @LastEditors: ekibun
* @LastEditTime: 2020-10-03 22:21:31
*/
import 'dart:async';
import 'dart:ffi';
import 'dart:io';
import 'dart:isolate';
import 'package:ffi/ffi.dart';
import 'flutter_qjs.dart';
import 'wrapper.dart';
void _runJsIsolate(Map spawnMessage) async {
SendPort sendPort = spawnMessage['port'];
ReceivePort port = ReceivePort();
sendPort.send(port.sendPort);
var qjs = FlutterQjs(
stackSize: spawnMessage['stackSize'],
hostPromiseRejectionHandler: (reason) {
sendPort.send({
'type': 'hostPromiseRejection',
'reason': reason,
});
},
moduleHandler: (name) {
var ptr = allocate<Pointer<Utf8>>();
ptr.value = Pointer.fromAddress(0);
sendPort.send({
'type': 'module',
'name': name,
'ptr': ptr.address,
});
while (ptr.value.address == 0) sleep(Duration.zero);
if (ptr.value.address == -1) throw JSError('Module Not found');
var ret = Utf8.fromUtf8(ptr.value);
sendPort.send({
'type': 'release',
'ptr': ptr.value.address,
});
free(ptr);
return ret;
},
);
port.listen((msg) async {
var data;
SendPort msgPort = msg['port'];
try {
switch (msg['type']) {
case 'evaluate':
data = await qjs.evaluate(
msg['command'],
name: msg['name'],
evalFlags: msg['flag'],
);
break;
case 'call':
data = await JSFunction.fromAddress(
Pointer.fromAddress(msg['ctx']),
Pointer.fromAddress(msg['val']),
).invoke(
decodeData(msg['args'], null),
decodeData(msg['this'], null),
);
break;
case 'close':
qjs.port.close();
qjs.close();
port.close();
break;
}
if (msgPort != null)
msgPort.send({
'data': encodeData(data),
});
} catch (e) {
if (msgPort != null)
msgPort.send({
'error': encodeData(e),
});
}
});
await qjs.dispatch();
}
typedef JsAsyncModuleHandler = Future<String> Function(String name);
typedef JsIsolateSpawn = void Function(SendPort sendPort);
class IsolateQjs {
Future<SendPort> _sendPort;
/// Max stack size for quickjs.
final int stackSize;
/// 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.moduleHandler,
this.stackSize,
this.hostPromiseRejectionHandler,
});
_ensureEngine() {
if (_sendPort != null) return;
ReceivePort port = ReceivePort();
Isolate.spawn(
_runJsIsolate,
{
'port': port.sendPort,
'stackSize': stackSize,
},
errorsAreFatal: true,
);
var completer = Completer<SendPort>();
port.listen((msg) async {
if (msg is SendPort && !completer.isCompleted) {
completer.complete(msg);
return;
}
switch (msg['type']) {
case 'hostPromiseRejection':
try {
final errStr = msg['reason'];
if (hostPromiseRejectionHandler != null) {
hostPromiseRejectionHandler(errStr);
} else {
print('unhandled promise rejection: $errStr');
}
} catch (e) {
print('host Promise Rejection Handler error: $e');
}
break;
case 'module':
var ptr = Pointer<Pointer>.fromAddress(msg['ptr']);
try {
ptr.value = Utf8.toUtf8(await moduleHandler(msg['name']));
} catch (e) {
ptr.value = Pointer.fromAddress(-1);
}
break;
case 'release':
free(Pointer.fromAddress(msg['ptr']));
break;
}
}, onDone: () {
close();
if (!completer.isCompleted) completer.completeError('isolate close');
});
_sendPort = completer.future;
}
/// Create isolate function
Future<IsolateFunction> bind(Function func) async {
_ensureEngine();
return IsolateFunction.bind(func, await _sendPort);
}
/// Free Runtime and close isolate thread that can be recreate when evaluate again.
close() {
if (_sendPort == null) return;
_sendPort.then((sendPort) {
sendPort.send({
'type': 'close',
});
});
_sendPort = null;
}
/// Evaluate js script.
Future<dynamic> evaluate(String command, {String name, int evalFlags}) async {
_ensureEngine();
var evaluatePort = ReceivePort();
var sendPort = await _sendPort;
sendPort.send({
'type': 'evaluate',
'command': command,
'name': name,
'flag': evalFlags,
'port': evaluatePort.sendPort,
});
Map result = await evaluatePort.first;
evaluatePort.close();
if (result.containsKey('data')) {
return decodeData(result['data'], sendPort);
} else
throw decodeData(result['error'], sendPort);
}
}

162
lib/src/engine.dart Normal file
View File

@@ -0,0 +1,162 @@
/*
* @Description: quickjs engine
* @Author: ekibun
* @Date: 2020-08-08 08:29:09
* @LastEditors: ekibun
* @LastEditTime: 2020-10-06 23:47:13
*/
part of '../flutter_qjs.dart';
/// Handler function to manage js module.
typedef _JsModuleHandler = String Function(String name);
/// Handler to manage unhandled promise rejection.
typedef _JsHostPromiseRejectionHandler = void Function(dynamic reason);
/// Quickjs engine for flutter.
class FlutterQjs {
Pointer _rt;
Pointer _ctx;
/// Max stack size for quickjs.
final int stackSize;
/// Message Port for event loop. Close it to stop dispatching event loop.
ReceivePort port = ReceivePort();
/// Handler function to manage js module.
_JsModuleHandler moduleHandler;
/// Handler function to manage js module.
_JsHostPromiseRejectionHandler hostPromiseRejectionHandler;
FlutterQjs({
this.moduleHandler,
this.stackSize,
this.hostPromiseRejectionHandler,
});
_ensureEngine() {
if (_rt != null) return;
_rt = jsNewRuntime((ctx, type, ptr) {
try {
switch (type) {
case JSChannelType.METHON:
final pdata = ptr.cast<Pointer>();
final argc = pdata.elementAt(1).value.cast<Int32>().value;
List pargs = [];
for (int i = 0; i < argc; ++i) {
pargs.add(_jsToDart(
ctx,
Pointer.fromAddress(
pdata.elementAt(2).value.address + sizeOfJSValue * i,
),
));
}
JSInvokable func = _jsToDart(ctx, pdata.elementAt(3).value);
return _dartToJs(
ctx,
func.invoke(
pargs,
_jsToDart(ctx, pdata.elementAt(0).value),
));
case JSChannelType.MODULE:
if (moduleHandler == null) throw JSError('No ModuleHandler');
final ret = Utf8.toUtf8(moduleHandler(
Utf8.fromUtf8(ptr.cast<Utf8>()),
));
Future.microtask(() {
free(ret);
});
return ret;
case JSChannelType.PROMISE_TRACK:
final err = _parseJSException(ctx, ptr);
if (hostPromiseRejectionHandler != null) {
hostPromiseRejectionHandler(err);
} else {
print('unhandled promise rejection: $err');
}
return Pointer.fromAddress(0);
case JSChannelType.FREE_OBJECT:
Pointer rt = ctx;
_DartObject obj = _DartObject.fromAddress(rt, ptr.address);
obj?.release();
return Pointer.fromAddress(0);
}
throw JSError('call channel with wrong type');
} catch (e) {
if (type == JSChannelType.FREE_OBJECT) {
print('DartObject release error: $e');
return Pointer.fromAddress(0);
}
if (type == JSChannelType.MODULE) {
print('host Promise Rejection Handler error: $e');
return Pointer.fromAddress(0);
}
final throwObj = _dartToJs(ctx, e);
final err = jsThrow(ctx, throwObj);
jsFreeValue(ctx, throwObj);
if (type == JSChannelType.MODULE) {
jsFreeValue(ctx, err);
return Pointer.fromAddress(0);
}
return err;
}
}, port);
if (this.stackSize != null && this.stackSize > 0)
jsSetMaxStackSize(_rt, this.stackSize);
_ctx = jsNewContext(_rt);
}
/// Free Runtime and Context which can be recreate when evaluate again.
close() {
if (_rt == null) return;
final rt = _rt;
final ctx = _ctx;
_executePendingJob();
_rt = null;
_ctx = null;
jsFreeContext(ctx);
try {
jsFreeRuntime(rt);
} on String catch (e) {
throw JSError(e);
}
}
void _executePendingJob() {
if (_rt == null) return;
while (true) {
int err = jsExecutePendingJob(_rt);
if (err <= 0) {
if (err < 0) print(_parseJSException(_ctx));
break;
}
}
}
/// Dispatch JavaScript Event loop.
Future<void> dispatch() async {
await for (final _ in port) {
_executePendingJob();
}
}
/// Evaluate js script.
dynamic evaluate(String command, {String name, int evalFlags}) {
_ensureEngine();
final jsval = jsEval(
_ctx,
command,
name ?? '<eval>',
evalFlags ?? JSEvalFlag.GLOBAL,
);
if (jsIsException(jsval) != 0) {
jsFreeValue(_ctx, jsval);
throw _parseJSException(_ctx);
}
final result = _jsToDart(_ctx, jsval);
jsFreeValue(_ctx, jsval);
return result;
}
}

View File

@@ -1,5 +1,5 @@
/*
* @Description:
* @Description: ffi
* @Author: ekibun
* @Date: 2020-09-19 10:29:04
* @LastEditors: ekibun
@@ -8,10 +8,10 @@
import 'dart:ffi';
import 'dart:io';
import 'dart:isolate';
import 'package:ffi/ffi.dart';
abstract class JSRef {
bool leakable = false;
void release();
}
@@ -90,13 +90,13 @@ final Pointer Function() jsUNDEFINED = _qjsLib
.lookup<NativeFunction<Pointer Function()>>('jsUNDEFINED')
.asFunction();
typedef JSChannel = Pointer Function(Pointer ctx, int method, Pointer argv);
typedef JSChannelNative = Pointer Function(
typedef _JSChannel = Pointer Function(Pointer ctx, int method, Pointer argv);
typedef _JSChannelNative = Pointer Function(
Pointer ctx, IntPtr method, Pointer argv);
/// JSRuntime *jsNewRuntime(JSChannel channel)
final Pointer Function(
Pointer<NativeFunction<JSChannelNative>>,
Pointer<NativeFunction<_JSChannelNative>>,
) _jsNewRuntime = _qjsLib
.lookup<
NativeFunction<
@@ -105,29 +105,37 @@ final Pointer Function(
)>>('jsNewRuntime')
.asFunction();
class RuntimeOpaque {
JSChannel channel;
List<JSRef> ref = [];
ReceivePort port;
int dartObjectClassId;
int jsExceptionClassId;
class _RuntimeOpaque {
_JSChannel _channel;
List<JSRef> _ref = [];
ReceivePort _port;
int _dartObjectClassId;
get dartObjectClassId => _dartObjectClassId;
void addRef(JSRef ref) => _ref.add(ref);
bool removeRef(JSRef ref) => _ref.remove(ref);
JSRef getRef(bool Function(JSRef ref) test) {
return _ref.firstWhere(test, orElse: () => null);
}
}
final Map<Pointer, RuntimeOpaque> runtimeOpaques = Map();
final Map<Pointer, _RuntimeOpaque> runtimeOpaques = Map();
Pointer channelDispacher(Pointer ctx, int type, Pointer argv) {
Pointer rt = type == JSChannelType.FREE_OBJECT ? ctx : jsGetRuntime(ctx);
return runtimeOpaques[rt]?.channel(ctx, type, argv);
return runtimeOpaques[rt]?._channel(ctx, type, argv);
}
Pointer jsNewRuntime(
JSChannel callback,
_JSChannel callback,
ReceivePort port,
) {
var rt = _jsNewRuntime(Pointer.fromFunction(channelDispacher));
runtimeOpaques[rt] = RuntimeOpaque()
..channel = callback
..port = port;
final rt = _jsNewRuntime(Pointer.fromFunction(channelDispacher));
runtimeOpaques[rt] = _RuntimeOpaque()
.._channel = callback
.._port = port;
return rt;
}
@@ -158,12 +166,28 @@ final void Function(
void jsFreeRuntime(
Pointer rt,
) {
while (0 < runtimeOpaques[rt]?.ref?.length ?? 0) {
final ref = runtimeOpaques[rt]?.ref?.first;
final referenceleak = <String>[];
while (true) {
final ref = runtimeOpaques[rt]
?._ref
?.firstWhere((ref) => ref.leakable, orElse: () => null);
if (ref == null) break;
ref.release();
runtimeOpaques[rt]?.ref?.remove(ref);
runtimeOpaques[rt]?._ref?.remove(ref);
}
while (0 < runtimeOpaques[rt]?._ref?.length ?? 0) {
final ref = runtimeOpaques[rt]?._ref?.first;
assert(!ref.leakable);
referenceleak.add(
" ${identityHashCode(ref)}\t${ref.runtimeType.toString()}\t${ref.toString().replaceAll('\n', '\\n')}");
ref.release();
runtimeOpaques[rt]?._ref?.remove(ref);
}
_jsFreeRuntime(rt);
if (referenceleak.length > 0) {
throw ('reference leak:\n ADDR\t TYPE \t PROP\n' +
referenceleak.join('\n'));
}
}
/// JSValue *jsNewCFunction(JSContext *ctx, JSValue *funcData)
@@ -191,11 +215,10 @@ final Pointer Function(
.asFunction();
Pointer jsNewContext(Pointer rt) {
var ctx = _jsNewContext(rt);
final ctx = _jsNewContext(rt);
final runtimeOpaque = runtimeOpaques[rt];
if (runtimeOpaque == null) throw Exception('Runtime has been released!');
runtimeOpaque.dartObjectClassId = jsNewClass(ctx, 'DartObject');
runtimeOpaque.jsExceptionClassId = jsNewClass(ctx, 'JSException');
runtimeOpaque._dartObjectClassId = jsNewClass(ctx, 'DartObject');
return ctx;
}
@@ -246,9 +269,9 @@ Pointer jsEval(
String filename,
int evalFlags,
) {
var utf8input = Utf8.toUtf8(input);
var utf8filename = Utf8.toUtf8(filename);
var val = _jsEval(
final utf8input = Utf8.toUtf8(input);
final utf8filename = Utf8.toUtf8(filename);
final val = _jsEval(
ctx,
utf8input,
Utf8.strlen(utf8input),
@@ -257,7 +280,7 @@ Pointer jsEval(
);
free(utf8input);
free(utf8filename);
runtimeOpaques[jsGetRuntime(ctx)].port.sendPort.send('eval');
runtimeOpaques[jsGetRuntime(ctx)]._port.sendPort.send(#eval);
return val;
}
@@ -350,8 +373,10 @@ Pointer jsNewString(
Pointer ctx,
String str,
) {
var utf8str = Utf8.toUtf8(str);
return _jsNewString(ctx, utf8str);
final utf8str = Utf8.toUtf8(str);
final jsStr = _jsNewString(ctx, utf8str);
free(utf8str);
return jsStr;
}
/// JSValue *jsNewArrayBufferCopy(JSContext *ctx, const uint8_t *buf, size_t len)
@@ -532,9 +557,9 @@ String jsToCString(
Pointer ctx,
Pointer val,
) {
var ptr = _jsToCString(ctx, val);
final ptr = _jsToCString(ctx, val);
if (ptr.address == 0) throw Exception('JSValue cannot convert to string');
var str = Utf8.fromUtf8(ptr);
final str = Utf8.fromUtf8(ptr);
jsFreeCString(ctx, ptr);
return str;
}
@@ -556,8 +581,8 @@ int jsNewClass(
Pointer ctx,
String name,
) {
var utf8name = Utf8.toUtf8(name);
var val = _jsNewClass(
final utf8name = Utf8.toUtf8(name);
final val = _jsNewClass(
ctx,
utf8name,
);
@@ -842,7 +867,7 @@ Pointer jsCall(
}
jsFreeValue(ctx, func1);
free(jsArgs);
runtimeOpaques[jsGetRuntime(ctx)].port.sendPort.send('call');
runtimeOpaques[jsGetRuntime(ctx)]._port.sendPort.send(#call);
return jsRet;
}

301
lib/src/isolate.dart Normal file
View File

@@ -0,0 +1,301 @@
/*
* @Description: isolate
* @Author: ekibun
* @Date: 2020-10-02 13:49:03
* @LastEditors: ekibun
* @LastEditTime: 2020-10-03 22:21:31
*/
part of '../flutter_qjs.dart';
typedef dynamic _Decode(Map obj, SendPort port);
List<_Decode> _decoders = [
JSError._decode,
_IsolateJSFunction._decode,
_IsolateFunction._decode,
_JSFunction._decode,
];
abstract class _IsolateEncodable {
Map _encode();
}
dynamic _encodeData(dynamic data, {Map<dynamic, dynamic> cache}) {
if (cache == null) cache = Map();
if (data is _IsolateEncodable) return data._encode();
if (cache.containsKey(data)) return cache[data];
if (data is List) {
final ret = [];
cache[data] = ret;
for (int i = 0; i < data.length; ++i) {
ret.add(_encodeData(data[i], cache: cache));
}
return ret;
}
if (data is Map) {
final ret = {};
cache[data] = ret;
for (final entry in data.entries) {
ret[_encodeData(entry.key, cache: cache)] =
_encodeData(entry.value, cache: cache);
}
return ret;
}
if (data is Future) {
final futurePort = ReceivePort();
data.then((value) {
futurePort.first.then((port) {
futurePort.close();
(port as SendPort).send(_encodeData(value));
});
}, onError: (e) {
futurePort.first.then((port) {
futurePort.close();
(port as SendPort).send({#error: _encodeData(e)});
});
});
return {
#jsFuturePort: futurePort.sendPort,
};
}
return data;
}
dynamic _decodeData(dynamic data, SendPort port,
{Map<dynamic, dynamic> cache}) {
if (cache == null) cache = Map();
if (cache.containsKey(data)) return cache[data];
if (data is List) {
final ret = [];
cache[data] = ret;
for (int i = 0; i < data.length; ++i) {
ret.add(_decodeData(data[i], port, cache: cache));
}
return ret;
}
if (data is Map) {
for (final decoder in _decoders) {
final decodeObj = decoder(data, port);
if (decodeObj != null) return decodeObj;
}
if (data.containsKey(#jsFuturePort)) {
SendPort port = data[#jsFuturePort];
final futurePort = ReceivePort();
port.send(futurePort.sendPort);
final futureCompleter = Completer();
futureCompleter.future.catchError((e) {});
futurePort.first.then((value) {
futurePort.close();
if (value is Map && value.containsKey(#error)) {
futureCompleter.completeError(_decodeData(value[#error], port));
} else {
futureCompleter.complete(_decodeData(value, port));
}
});
return futureCompleter.future;
}
final ret = {};
cache[data] = ret;
for (final entry in data.entries) {
ret[_decodeData(entry.key, port, cache: cache)] =
_decodeData(entry.value, port, cache: cache);
}
return ret;
}
return data;
}
void _runJsIsolate(Map spawnMessage) async {
SendPort sendPort = spawnMessage[#port];
ReceivePort port = ReceivePort();
sendPort.send(port.sendPort);
final qjs = FlutterQjs(
stackSize: spawnMessage[#stackSize],
hostPromiseRejectionHandler: (reason) {
sendPort.send({
#type: #hostPromiseRejection,
#reason: _encodeData(reason),
});
},
moduleHandler: (name) {
final ptr = allocate<Pointer<Utf8>>();
ptr.value = Pointer.fromAddress(0);
sendPort.send({
#type: #module,
#name: name,
#ptr: ptr.address,
});
while (ptr.value.address == 0) sleep(Duration.zero);
if (ptr.value.address == -1) throw JSError('Module Not found');
final ret = Utf8.fromUtf8(ptr.value);
sendPort.send({
#type: #release,
#ptr: ptr.value.address,
});
free(ptr);
return ret;
},
);
port.listen((msg) async {
var data;
SendPort msgPort = msg[#port];
try {
switch (msg[#type]) {
case #evaluate:
data = await qjs.evaluate(
msg[#command],
name: msg[#name],
evalFlags: msg[#flag],
);
break;
case #call:
data = await _JSFunction.fromAddress(
Pointer.fromAddress(msg[#ctx]),
Pointer.fromAddress(msg[#val]),
).invoke(
_decodeData(msg[#args], null),
_decodeData(msg[#thisVal], null),
);
break;
case #closeFunction:
_JSFunction.fromAddress(
Pointer.fromAddress(msg[#ctx]),
Pointer.fromAddress(msg[#val]),
).release();
break;
case #close:
data = false;
qjs.port.close();
qjs.close();
port.close();
data = true;
break;
}
if (msgPort != null) msgPort.send(_encodeData(data));
} catch (e) {
if (msgPort != null)
msgPort.send({
#error: _encodeData(e),
});
}
});
await qjs.dispatch();
}
typedef _JsAsyncModuleHandler = Future<String> Function(String name);
class IsolateQjs {
Future<SendPort> _sendPort;
/// Max stack size for quickjs.
final int stackSize;
/// 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.moduleHandler,
this.stackSize,
this.hostPromiseRejectionHandler,
});
_ensureEngine() {
if (_sendPort != null) return;
ReceivePort port = ReceivePort();
Isolate.spawn(
_runJsIsolate,
{
#port: port.sendPort,
#stackSize: stackSize,
},
errorsAreFatal: true,
);
final completer = Completer<SendPort>();
port.listen((msg) async {
if (msg is SendPort && !completer.isCompleted) {
completer.complete(msg);
return;
}
switch (msg[#type]) {
case #hostPromiseRejection:
try {
final err = _decodeData(msg[#reason], port.sendPort);
if (hostPromiseRejectionHandler != null) {
hostPromiseRejectionHandler(err);
} else {
print('unhandled promise rejection: $err');
}
} catch (e) {
print('host Promise Rejection Handler error: $e');
}
break;
case #module:
final ptr = Pointer<Pointer>.fromAddress(msg[#ptr]);
try {
ptr.value = Utf8.toUtf8(await moduleHandler(msg[#name]));
} catch (e) {
ptr.value = Pointer.fromAddress(-1);
}
break;
case #release:
free(Pointer.fromAddress(msg[#ptr]));
break;
}
}, onDone: () {
close();
if (!completer.isCompleted)
completer.completeError(JSError('isolate close'));
});
_sendPort = completer.future;
}
/// Create isolate function
Future<_IsolateFunction> bind(Function func) async {
_ensureEngine();
return _IsolateFunction._bind(func, await _sendPort);
}
/// Free Runtime and close isolate thread that can be recreate when evaluate again.
close() {
if (_sendPort == null) return;
final ret = _sendPort.then((sendPort) async {
final closePort = ReceivePort();
sendPort.send({
#type: #close,
#port: closePort.sendPort,
});
final result = await closePort.first;
closePort.close();
if (result is Map && result.containsKey(#error))
throw _decodeData(result[#error], sendPort);
return _decodeData(result, sendPort);
});
_sendPort = null;
return ret;
}
/// Evaluate js script.
Future<dynamic> evaluate(String command, {String name, int evalFlags}) async {
_ensureEngine();
final evaluatePort = ReceivePort();
final sendPort = await _sendPort;
sendPort.send({
#type: #evaluate,
#command: command,
#name: name,
#flag: evalFlags,
#port: evaluatePort.sendPort,
});
final result = await evaluatePort.first;
evaluatePort.close();
if (result is Map && result.containsKey(#error))
throw _decodeData(result[#error], sendPort);
return _decodeData(result, sendPort);
}
}

342
lib/src/object.dart Normal file
View File

@@ -0,0 +1,342 @@
/*
* @Description: wrap object
* @Author: ekibun
* @Date: 2020-10-02 13:49:03
* @LastEditors: ekibun
* @LastEditTime: 2020-10-03 22:21:31
*/
part of '../flutter_qjs.dart';
/// js invokable
abstract class JSInvokable extends JSReleasable {
dynamic invoke(List args, [dynamic thisVal]);
static dynamic _wrap(dynamic func) {
return func is JSInvokable
? func
: func is Function
? _DartFunction(func)
: func;
}
@override
noSuchMethod(Invocation invocation) {
return invoke(
invocation.positionalArguments,
invocation.namedArguments[#thisVal],
);
}
}
class _DartFunction extends JSInvokable {
final Function _func;
_DartFunction(this._func);
@override
invoke(List args, [thisVal]) {
/// wrap this into function
final passThis =
RegExp('{.*thisVal.*}').hasMatch(_func.runtimeType.toString());
return Function.apply(_func, args, passThis ? {#thisVal: thisVal} : null);
}
@override
String toString() {
return _func.toString();
}
@override
release() {}
}
/// implement this to capture js object release.
abstract class JSReleasable {
void release();
}
class _DartObject extends JSRef {
@override
bool leakable = true;
Object _obj;
Pointer _ctx;
_DartObject(this._ctx, this._obj) {
runtimeOpaques[jsGetRuntime(_ctx)]?.addRef(this);
}
static _DartObject fromAddress(Pointer rt, int val) {
return runtimeOpaques[rt]?.getRef((e) => identityHashCode(e) == val);
}
@override
String toString() {
if (_ctx == null) return "DartObject(<released>)";
return _obj.toString();
}
@override
void release() {
if (_ctx == null) return;
runtimeOpaques[jsGetRuntime(_ctx)]?.removeRef(this);
_ctx = null;
if (_obj is JSReleasable) {
(_obj as JSReleasable).release();
}
_obj = null;
}
}
/// JS Error wrapper
class JSError extends _IsolateEncodable {
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, SendPort port) {
if (obj.containsKey(#jsError))
return JSError(obj[#jsError], obj[#jsErrorStack]);
return null;
}
@override
Map _encode() {
return {
#jsError: message,
#jsErrorStack: stack,
};
}
}
/// JS Object reference
/// call [release] to release js object.
class _JSObject extends JSRef {
Pointer _val;
Pointer _ctx;
/// Create
_JSObject(this._ctx, Pointer _val) {
Pointer rt = jsGetRuntime(_ctx);
this._val = jsDupValue(_ctx, _val);
runtimeOpaques[rt]?.addRef(this);
}
static _JSObject fromAddress(Pointer ctx, Pointer val) {
Pointer rt = jsGetRuntime(ctx);
return runtimeOpaques[rt]?.getRef((e) =>
e is _JSObject &&
e._val.address == val.address &&
e._ctx.address == ctx.address);
}
@override
void release() {
if (_val == null) return;
Pointer rt = jsGetRuntime(_ctx);
runtimeOpaques[rt]?.removeRef(this);
jsFreeValue(_ctx, _val);
_val = null;
_ctx = null;
}
@override
String toString() {
if (_val == null) return "JSObject(released)";
return jsToCString(_ctx, _val);
}
}
/// JS function wrapper
class _JSFunction extends _JSObject implements JSInvokable, _IsolateEncodable {
_JSFunction(Pointer ctx, Pointer val) : super(ctx, val);
static _JSFunction fromAddress(Pointer ctx, Pointer val) {
return _JSObject.fromAddress(ctx, val);
}
@override
invoke(List<dynamic> arguments, [dynamic thisVal]) {
Pointer jsRet = _invoke(arguments, thisVal);
if (jsRet == null) return;
bool isException = jsIsException(jsRet) != 0;
if (isException) {
jsFreeValue(_ctx, jsRet);
throw _parseJSException(_ctx);
}
final ret = _jsToDart(_ctx, jsRet);
jsFreeValue(_ctx, jsRet);
return ret;
}
Pointer _invoke(List<dynamic> arguments, [dynamic thisVal]) {
if (_val == null) return null;
List<Pointer> args = arguments
.map(
(e) => _dartToJs(_ctx, e),
)
.toList();
Pointer jsThis = _dartToJs(_ctx, thisVal);
Pointer jsRet = jsCall(_ctx, _val, jsThis, args);
jsFreeValue(_ctx, jsThis);
for (Pointer jsArg in args) {
jsFreeValue(_ctx, jsArg);
}
return jsRet;
}
static _JSFunction _decode(Map obj, SendPort port) {
if (obj.containsKey(#jsFunction) && port == null)
return _JSFunction.fromAddress(
Pointer.fromAddress(obj[#jsFunctionCtx]),
Pointer.fromAddress(obj[#jsFunction]),
);
return null;
}
@override
Map _encode() {
return {
#jsFunction: _val.address,
#jsFunctionCtx: _ctx.address,
};
}
@override
noSuchMethod(Invocation invocation) {
return invoke(
invocation.positionalArguments,
invocation.namedArguments[#thisVal],
);
}
}
/// JS function wrapper for isolate
class _IsolateJSFunction extends JSInvokable implements _IsolateEncodable {
int _val;
int _ctx;
SendPort _port;
_IsolateJSFunction(this._ctx, this._val, this._port);
@override
Future invoke(List arguments, [thisVal]) async {
if (0 == _val ?? 0) return;
final evaluatePort = ReceivePort();
_port.send({
#type: #call,
#ctx: _ctx,
#val: _val,
#args: _encodeData(arguments),
#thisVal: _encodeData(thisVal),
#port: evaluatePort.sendPort,
});
final result = await evaluatePort.first;
evaluatePort.close();
if (result is Map && result.containsKey(#error))
throw _decodeData(result[#error], _port);
return _decodeData(result, _port);
}
static _IsolateJSFunction _decode(Map obj, SendPort port) {
if (obj.containsKey(#jsFunction) && port != null)
return _IsolateJSFunction(obj[#jsFunctionCtx], obj[#jsFunction], port);
return null;
}
@override
Map _encode() {
return {
#jsFunction: _val,
#jsFunctionCtx: _ctx,
};
}
@override
void release() {
if (_port == null) return;
_port.send({
#type: #closeFunction,
#ctx: _ctx,
#val: _val,
});
_port = null;
_val = null;
_ctx = null;
}
}
/// Dart function wrapper for isolate
class _IsolateFunction extends JSInvokable
implements JSReleasable, _IsolateEncodable {
SendPort _port;
SendPort _func;
_IsolateFunction(this._func, this._port);
static _IsolateFunction _bind(Function func, SendPort port) {
final JSInvokable invokable = JSInvokable._wrap(func);
final funcPort = ReceivePort();
funcPort.listen((msg) async {
if (msg == #close) return funcPort.close();
SendPort msgPort = msg[#port];
try {
List args = _decodeData(msg[#args], port);
Map thisVal = _decodeData(msg[#thisVal], port);
final data = await invokable.invoke(args, thisVal);
if (msgPort != null) msgPort.send(_encodeData(data));
} catch (e) {
if (msgPort != null)
msgPort.send({
#error: _encodeData(e),
});
}
});
return _IsolateFunction(funcPort.sendPort, port);
}
@override
Future invoke(List positionalArguments, [thisVal]) async {
if (_func == null) return;
final evaluatePort = ReceivePort();
_func.send({
#args: _encodeData(positionalArguments),
#thisVal: _encodeData(thisVal),
#port: evaluatePort.sendPort,
});
final result = await evaluatePort.first;
evaluatePort.close();
if (result is Map && result.containsKey(#error))
throw _decodeData(result[#error], _port);
return _decodeData(result, _port);
}
static _IsolateFunction _decode(Map obj, SendPort port) {
if (obj.containsKey(#jsFunctionPort))
return _IsolateFunction(obj[#jsFunctionPort], port);
return null;
}
@override
Map _encode() {
return {
#jsFunctionPort: _func,
};
}
@override
void release() {
if (_func == null) return;
_func.send(#close);
_func = null;
}
}

235
lib/src/wrapper.dart Normal file
View File

@@ -0,0 +1,235 @@
/*
* @Description: wrapper
* @Author: ekibun
* @Date: 2020-09-19 22:07:47
* @LastEditors: ekibun
* @LastEditTime: 2020-12-02 11:14:03
*/
part of '../flutter_qjs.dart';
dynamic _parseJSException(Pointer ctx, [Pointer perr]) {
final e = perr ?? jsGetException(ctx);
var err;
try {
err = _jsToDart(ctx, e);
} catch (exception) {
err = exception;
}
if (perr == null) jsFreeValue(ctx, e);
return err;
}
void _definePropertyValue(
Pointer ctx,
Pointer obj,
dynamic key,
dynamic val, {
Map<dynamic, dynamic> cache,
}) {
final jsAtomVal = _dartToJs(ctx, key, cache: cache);
final jsAtom = jsValueToAtom(ctx, jsAtomVal);
jsDefinePropertyValue(
ctx,
obj,
jsAtom,
_dartToJs(ctx, val, cache: cache),
JSProp.C_W_E,
);
jsFreeAtom(ctx, jsAtom);
jsFreeValue(ctx, jsAtomVal);
}
Pointer _jsGetPropertyValue(
Pointer ctx,
Pointer obj,
dynamic key, {
Map<dynamic, dynamic> cache,
}) {
final jsAtomVal = _dartToJs(ctx, key, cache: cache);
final jsAtom = jsValueToAtom(ctx, jsAtomVal);
final jsProp = jsGetProperty(ctx, obj, jsAtom);
jsFreeAtom(ctx, jsAtom);
jsFreeValue(ctx, jsAtomVal);
return jsProp;
}
Pointer _dartToJs(Pointer ctx, dynamic val, {Map<dynamic, dynamic> 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) {
final resolvingFunc = allocate<Uint8>(count: sizeOfJSValue * 2);
final resolvingFunc2 =
Pointer.fromAddress(resolvingFunc.address + sizeOfJSValue);
final ret = jsNewPromiseCapability(ctx, resolvingFunc);
_JSFunction res = _jsToDart(ctx, resolvingFunc)..leakable = true;
_JSFunction rej = _jsToDart(ctx, resolvingFunc2)..leakable = true;
jsFreeValue(ctx, resolvingFunc, free: false);
jsFreeValue(ctx, resolvingFunc2, free: false);
free(resolvingFunc);
val.then((value) {
res.invoke([value]);
}, onError: (e) {
rej.invoke([e]);
}).whenComplete(() {
res.release();
rej.release();
});
return ret;
}
if (cache == null) cache = Map();
if (val is bool) return jsNewBool(ctx, val ? 1 : 0);
if (val is int) return jsNewInt64(ctx, val);
if (val is double) return jsNewFloat64(ctx, val);
if (val is String) return jsNewString(ctx, val);
if (val is Uint8List) {
final ptr = allocate<Uint8>(count: val.length);
final byteList = ptr.asTypedList(val.length);
byteList.setAll(0, val);
final ret = jsNewArrayBufferCopy(ctx, ptr, val.length);
free(ptr);
return ret;
}
if (cache.containsKey(val)) {
return jsDupValue(ctx, cache[val]);
}
if (val is List) {
Pointer ret = jsNewArray(ctx);
cache[val] = ret;
for (int i = 0; i < val.length; ++i) {
_definePropertyValue(ctx, ret, i, val[i], cache: cache);
}
return ret;
}
if (val is Map) {
Pointer ret = jsNewObject(ctx);
cache[val] = ret;
for (MapEntry<dynamic, dynamic> entry in val.entries) {
_definePropertyValue(ctx, ret, entry.key, entry.value, cache: cache);
}
return ret;
}
// wrap Function to JSInvokable
final valWrap = JSInvokable._wrap(val);
int dartObjectClassId =
runtimeOpaques[jsGetRuntime(ctx)]?.dartObjectClassId ?? 0;
if (dartObjectClassId == 0) return jsUNDEFINED();
final dartObject = jsNewObjectClass(
ctx,
dartObjectClassId,
identityHashCode(_DartObject(ctx, valWrap)),
);
if (valWrap is JSInvokable) {
final ret = jsNewCFunction(ctx, dartObject);
jsFreeValue(ctx, dartObject);
return ret;
}
return dartObject;
}
dynamic _jsToDart(Pointer ctx, Pointer val, {Map<int, dynamic> cache}) {
if (cache == null) cache = Map();
int tag = jsValueGetTag(val);
if (jsTagIsFloat64(tag) != 0) {
return jsToFloat64(ctx, val);
}
switch (tag) {
case JSTag.BOOL:
return jsToBool(ctx, val) != 0;
case JSTag.INT:
return jsToInt64(ctx, val);
case JSTag.STRING:
return jsToCString(ctx, val);
case JSTag.OBJECT:
final rt = jsGetRuntime(ctx);
final dartObjectClassId = runtimeOpaques[rt].dartObjectClassId;
if (dartObjectClassId != 0) {
final dartObject = _DartObject.fromAddress(
rt, jsGetObjectOpaque(val, dartObjectClassId));
if (dartObject != null) return dartObject._obj;
}
Pointer<IntPtr> psize = allocate<IntPtr>();
Pointer<Uint8> buf = jsGetArrayBuffer(ctx, psize, val);
int size = psize.value;
free(psize);
if (buf.address != 0) {
return Uint8List.fromList(buf.asTypedList(size));
}
int valptr = jsValueGetPtr(val).address;
if (cache.containsKey(valptr)) {
return cache[valptr];
}
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');
final stack =
jsToBool(ctx, pstack) != 0 ? jsToCString(ctx, pstack) : null;
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);
jsFreeValue(ctx, jsPromiseThen);
final completer = Completer();
completer.future.catchError((e) {});
final jsPromise = _JSObject(ctx, val);
final jsRet = promiseThen._invoke([
(v) {
if (!completer.isCompleted) completer.complete(v);
},
(e) {
if (!completer.isCompleted) completer.completeError(e);
},
], jsPromise);
jsPromise.release();
promiseThen.release();
bool isException = jsIsException(jsRet) != 0;
jsFreeValue(ctx, jsRet);
if (isException) throw _parseJSException(ctx);
return completer.future;
} else if (jsIsArray(ctx, val) != 0) {
Pointer jslength = _jsGetPropertyValue(ctx, val, 'length');
int length = jsToInt64(ctx, jslength);
List<dynamic> ret = [];
cache[valptr] = ret;
for (int i = 0; i < length; ++i) {
final jsProp = _jsGetPropertyValue(ctx, val, i);
ret.add(_jsToDart(ctx, jsProp, cache: cache));
jsFreeValue(ctx, jsProp);
}
return ret;
} else {
Pointer<Pointer> ptab = allocate<Pointer>();
Pointer<Uint32> plen = allocate<Uint32>();
if (jsGetOwnPropertyNames(ctx, ptab, plen, val, -1) != 0) return null;
int len = plen.value;
free(plen);
Map<dynamic, dynamic> ret = Map();
cache[valptr] = ret;
for (int i = 0; i < len; ++i) {
final jsAtom = jsPropertyEnumGetAtom(ptab.value, i);
final jsAtomValue = jsAtomToValue(ctx, jsAtom);
final jsProp = jsGetProperty(ctx, val, jsAtom);
ret[_jsToDart(ctx, jsAtomValue, cache: cache)] =
_jsToDart(ctx, jsProp, cache: cache);
jsFreeValue(ctx, jsAtomValue);
jsFreeValue(ctx, jsProp);
jsFreeAtom(ctx, jsAtom);
}
jsFree(ctx, ptab.value);
free(ptab);
return ret;
}
break;
default:
}
return null;
}

View File

@@ -1,615 +0,0 @@
/*
* @Description:
* @Author: ekibun
* @Date: 2020-09-19 22:07:47
* @LastEditors: ekibun
* @LastEditTime: 2020-12-02 11:14:03
*/
import 'dart:async';
import 'dart:ffi';
import 'dart:isolate';
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]);
static dynamic wrap(dynamic func) {
return func is JSInvokable
? func
: func is Function
? _DartFunction(func)
: func;
}
@override
noSuchMethod(Invocation invocation) {
return invoke(
invocation.positionalArguments,
invocation.namedArguments[#thisVal],
);
}
}
// class NativeJSInvokable extends JSInvokable {
// dynamic Function(Pointer ctx, Pointer thisVal, List<Pointer> args) _func;
// NativeJSInvokable(this._func);
// @override
// dynamic invoke(List args, [dynamic thisVal]) {
// throw UnimplementedError('use invokeNative instead.');
// }
// invokeNative(Pointer ctx, Pointer thisVal, List<Pointer> args) {
// _func(ctx, thisVal, args);
// }
// }
class _DartFunction extends JSInvokable {
Function _func;
_DartFunction(this._func);
@override
invoke(List args, [thisVal]) {
/// wrap this into function
final passThis =
RegExp('{.*thisVal.*}').hasMatch(_func.runtimeType.toString());
return Function.apply(_func, args, passThis ? {#thisVal: thisVal} : null);
}
}
abstract class DartReleasable {
void release();
}
class DartObject implements JSRef {
Object _obj;
Pointer _ctx;
DartObject(this._ctx, this._obj) {
runtimeOpaques[jsGetRuntime(_ctx)]?.ref?.add(this);
}
static DartObject fromAddress(Pointer rt, int val) {
return runtimeOpaques[rt]?.ref?.firstWhere(
(e) => identityHashCode(e) == val,
orElse: () => null,
);
}
@override
void release() {
if (_obj is DartReleasable) {
(_obj as DartReleasable).release();
}
_obj = null;
_ctx = null;
}
}
class JSObject implements JSRef {
Pointer _val;
Pointer _ctx;
/// Create
JSObject(this._ctx, Pointer _val) {
Pointer rt = jsGetRuntime(_ctx);
this._val = jsDupValue(_ctx, _val);
runtimeOpaques[rt]?.ref?.add(this);
}
JSObject.fromAddress(Pointer ctx, Pointer val) {
this._ctx = ctx;
this._val = val;
}
@override
void release() {
if (_val != null) {
jsFreeValue(_ctx, _val);
}
_val = null;
_ctx = null;
}
}
class JSFunction extends JSObject implements JSInvokable {
JSFunction(Pointer ctx, Pointer val) : super(ctx, val);
JSFunction.fromAddress(Pointer ctx, Pointer val)
: super.fromAddress(ctx, val);
@override
invoke(List<dynamic> arguments, [dynamic thisVal]) {
Pointer jsRet = _invoke(arguments, thisVal);
if (jsRet == null) return;
bool isException = jsIsException(jsRet) != 0;
if (isException) {
jsFreeValue(_ctx, jsRet);
throw parseJSException(_ctx);
}
var ret = jsToDart(_ctx, jsRet);
jsFreeValue(_ctx, jsRet);
return ret;
}
Pointer _invoke(List<dynamic> arguments, [dynamic thisVal]) {
if (_val == null) return null;
List<Pointer> args = arguments
.map(
(e) => dartToJs(_ctx, e),
)
.toList();
Pointer jsThis = dartToJs(_ctx, thisVal);
Pointer jsRet = jsCall(_ctx, _val, jsThis, args);
jsFreeValue(_ctx, jsThis);
for (Pointer jsArg in args) {
jsFreeValue(_ctx, jsArg);
}
return jsRet;
}
@override
noSuchMethod(Invocation invocation) {
return invoke(
invocation.positionalArguments,
invocation.namedArguments[#thisVal],
);
}
}
class IsolateJSFunction extends JSInvokable {
int _val;
int _ctx;
SendPort port;
IsolateJSFunction(this._ctx, this._val, this.port);
@override
Future invoke(List arguments, [thisVal]) async {
if (0 == _val ?? 0) return;
var evaluatePort = ReceivePort();
port.send({
'type': 'call',
'ctx': _ctx,
'val': _val,
'args': encodeData(arguments),
'this': encodeData(thisVal),
'port': evaluatePort.sendPort,
});
Map result = await evaluatePort.first;
evaluatePort.close();
if (result.containsKey('data'))
return decodeData(result['data'], port);
else
throw decodeData(result['error'], port);
}
}
class IsolateFunction extends JSInvokable implements DartReleasable {
SendPort _port;
SendPort func;
IsolateFunction(this.func, this._port);
static IsolateFunction bind(Function func, SendPort port) {
final JSInvokable invokable = JSInvokable.wrap(func);
final funcPort = ReceivePort();
funcPort.listen((msg) async {
if (msg == 'close') return funcPort.close();
var data;
SendPort msgPort = msg['port'];
try {
List args = decodeData(msg['args'], port);
Map thisVal = decodeData(msg['this'], port);
data = await invokable.invoke(args, thisVal);
if (msgPort != null)
msgPort.send({
'data': encodeData(data),
});
} catch (e) {
if (msgPort != null)
msgPort.send({
'error': encodeData(e),
});
}
});
return IsolateFunction(funcPort.sendPort, port);
}
@override
Future invoke(List positionalArguments, [thisVal]) async {
if (func == null) return;
var evaluatePort = ReceivePort();
func.send({
'args': encodeData(positionalArguments),
'this': encodeData(thisVal),
'port': evaluatePort.sendPort,
});
Map result = await evaluatePort.first;
evaluatePort.close();
if (result.containsKey('data'))
return decodeData(result['data'], _port);
else
throw decodeData(result['error'], _port);
}
@override
void release() {
if (func == null) return;
func.send('close');
func = null;
}
}
dynamic encodeData(dynamic data, {Map<dynamic, dynamic> 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 = [];
cache[data] = ret;
for (int i = 0; i < data.length; ++i) {
ret.add(encodeData(data[i], cache: cache));
}
return ret;
}
if (data is Map) {
var ret = {};
cache[data] = ret;
for (var entry in data.entries) {
ret[encodeData(entry.key, cache: cache)] =
encodeData(entry.value, cache: cache);
}
return ret;
}
if (data is JSObject) {
return {
'__js_function': data is JSFunction,
'__js_obj_ctx': data._ctx.address,
'__js_obj_val': data._val.address,
};
}
if (data is IsolateJSFunction) {
return {
'__js_obj_ctx': data._ctx,
'__js_obj_val': data._val,
};
}
if (data is IsolateFunction) {
return {
'__js_function_port': data.func,
};
}
if (data is Future) {
var futurePort = ReceivePort();
data.then((value) {
futurePort.first.then((port) {
futurePort.close();
(port as SendPort).send({'data': encodeData(value)});
});
}, onError: (e) {
futurePort.first.then((port) {
futurePort.close();
(port as SendPort).send({'error': encodeData(e)});
});
});
return {
'__js_future_port': futurePort.sendPort,
};
}
return data;
}
dynamic decodeData(dynamic data, SendPort port, {Map<dynamic, dynamic> cache}) {
if (cache == null) cache = Map();
if (cache.containsKey(data)) return cache[data];
if (data is List) {
var ret = [];
cache[data] = ret;
for (int i = 0; i < data.length; ++i) {
ret.add(decodeData(data[i], port, cache: 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'];
if (data['__js_function'] == false) {
return JSObject.fromAddress(
Pointer.fromAddress(ctx),
Pointer.fromAddress(val),
);
} else if (port != null) {
return IsolateJSFunction(ctx, val, port);
} else {
return JSFunction.fromAddress(
Pointer.fromAddress(ctx),
Pointer.fromAddress(val),
);
}
}
if (data.containsKey('__js_function_port')) {
return IsolateFunction(data['__js_function_port'], port);
}
if (data.containsKey('__js_future_port')) {
SendPort port = data['__js_future_port'];
var futurePort = ReceivePort();
port.send(futurePort.sendPort);
var futureCompleter = Completer();
futureCompleter.future.catchError((e) {});
futurePort.first.then((value) {
futurePort.close();
if (value['error'] != null) {
futureCompleter.completeError(decodeData(value['error'], port));
} else {
futureCompleter.complete(decodeData(value['data'], port));
}
});
return futureCompleter.future;
}
var ret = {};
cache[data] = ret;
for (var entry in data.entries) {
ret[decodeData(entry.key, port, cache: cache)] =
decodeData(entry.value, port, cache: cache);
}
return ret;
}
return data;
}
dynamic parseJSException(Pointer ctx, [Pointer perr]) {
final e = perr ?? jsGetException(ctx);
var err;
try {
err = jsToDart(ctx, e);
} catch (exception) {
err = exception;
}
if (perr == null) jsFreeValue(ctx, e);
return err;
}
void definePropertyValue(
Pointer ctx,
Pointer obj,
dynamic key,
dynamic val, {
Map<dynamic, dynamic> cache,
}) {
var jsAtomVal = dartToJs(ctx, key, cache: cache);
var jsAtom = jsValueToAtom(ctx, jsAtomVal);
jsDefinePropertyValue(
ctx,
obj,
jsAtom,
dartToJs(ctx, val, cache: cache),
JSProp.C_W_E,
);
jsFreeAtom(ctx, jsAtom);
jsFreeValue(ctx, jsAtomVal);
}
Pointer jsGetPropertyValue(
Pointer ctx,
Pointer obj,
dynamic key, {
Map<dynamic, dynamic> cache,
}) {
var jsAtomVal = dartToJs(ctx, key, cache: cache);
var jsAtom = jsValueToAtom(ctx, jsAtomVal);
var jsProp = jsGetProperty(ctx, obj, jsAtom);
jsFreeAtom(ctx, jsAtom);
jsFreeValue(ctx, jsAtomVal);
return jsProp;
}
Pointer dartToJs(Pointer ctx, dynamic val, {Map<dynamic, dynamic> 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<Uint8>(count: sizeOfJSValue * 2);
var resolvingFunc2 =
Pointer.fromAddress(resolvingFunc.address + sizeOfJSValue);
var ret = jsNewPromiseCapability(ctx, resolvingFunc);
var res = jsToDart(ctx, resolvingFunc);
var rej = jsToDart(ctx, resolvingFunc2);
jsFreeValue(ctx, resolvingFunc, free: false);
jsFreeValue(ctx, resolvingFunc2, free: false);
free(resolvingFunc);
val.then((value) {
res(value);
}, onError: (e) {
rej(e);
});
return ret;
}
if (cache == null) cache = Map();
if (val is bool) return jsNewBool(ctx, val ? 1 : 0);
if (val is int) return jsNewInt64(ctx, val);
if (val is double) return jsNewFloat64(ctx, val);
if (val is String) return jsNewString(ctx, val);
if (val is Uint8List) {
var ptr = allocate<Uint8>(count: val.length);
var byteList = ptr.asTypedList(val.length);
byteList.setAll(0, val);
var ret = jsNewArrayBufferCopy(ctx, ptr, val.length);
free(ptr);
return ret;
}
if (cache.containsKey(val)) {
return jsDupValue(ctx, cache[val]);
}
if (val is List) {
Pointer ret = jsNewArray(ctx);
cache[val] = ret;
for (int i = 0; i < val.length; ++i) {
definePropertyValue(ctx, ret, i, val[i], cache: cache);
}
return ret;
}
if (val is Map) {
Pointer ret = jsNewObject(ctx);
cache[val] = ret;
for (MapEntry<dynamic, dynamic> entry in val.entries) {
definePropertyValue(ctx, ret, entry.key, entry.value, cache: cache);
}
return ret;
}
// wrap Function to JSInvokable
final valWrap = JSInvokable.wrap(val);
int dartObjectClassId =
runtimeOpaques[jsGetRuntime(ctx)]?.dartObjectClassId ?? 0;
if (dartObjectClassId == 0) return jsUNDEFINED();
var dartObject = jsNewObjectClass(
ctx,
dartObjectClassId,
identityHashCode(DartObject(ctx, valWrap)),
);
if (valWrap is JSInvokable) {
final ret = jsNewCFunction(ctx, dartObject);
jsFreeValue(ctx, dartObject);
return ret;
}
return dartObject;
}
dynamic jsToDart(Pointer ctx, Pointer val, {Map<int, dynamic> cache}) {
if (cache == null) cache = Map();
int tag = jsValueGetTag(val);
if (jsTagIsFloat64(tag) != 0) {
return jsToFloat64(ctx, val);
}
switch (tag) {
case JSTag.BOOL:
return jsToBool(ctx, val) != 0;
case JSTag.INT:
return jsToInt64(ctx, val);
case JSTag.STRING:
return jsToCString(ctx, val);
case JSTag.OBJECT:
final rt = jsGetRuntime(ctx);
final dartObjectClassId = runtimeOpaques[rt].dartObjectClassId;
if (dartObjectClassId != 0) {
final dartObject = DartObject.fromAddress(
rt, jsGetObjectOpaque(val, dartObjectClassId));
if (dartObject != null) return dartObject._obj;
}
Pointer<IntPtr> psize = allocate<IntPtr>();
Pointer<Uint8> buf = jsGetArrayBuffer(ctx, psize, val);
int size = psize.value;
free(psize);
if (buf.address != 0) {
return Uint8List.fromList(buf.asTypedList(size));
}
int valptr = jsValueGetPtr(val).address;
if (cache.containsKey(valptr)) {
return cache[valptr];
}
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);
jsFreeValue(ctx, jsPromiseThen);
var completer = Completer();
completer.future.catchError((e) {});
final jsRet = promiseThen._invoke([
(v) {
if (!completer.isCompleted) completer.complete(v);
},
(e) {
if (!completer.isCompleted) completer.completeError(e);
},
], JSObject.fromAddress(ctx, val));
bool isException = jsIsException(jsRet) != 0;
jsFreeValue(ctx, jsRet);
if (isException) throw parseJSException(ctx);
return completer.future;
} else if (jsIsArray(ctx, val) != 0) {
Pointer jslength = jsGetPropertyValue(ctx, val, 'length');
int length = jsToInt64(ctx, jslength);
List<dynamic> ret = [];
cache[valptr] = ret;
for (int i = 0; i < length; ++i) {
var jsProp = jsGetPropertyValue(ctx, val, i);
ret.add(jsToDart(ctx, jsProp, cache: cache));
jsFreeValue(ctx, jsProp);
}
return ret;
} else {
Pointer<Pointer> ptab = allocate<Pointer>();
Pointer<Uint32> plen = allocate<Uint32>();
if (jsGetOwnPropertyNames(ctx, ptab, plen, val, -1) != 0) return null;
int len = plen.value;
free(plen);
Map<dynamic, dynamic> ret = Map();
cache[valptr] = ret;
for (int i = 0; i < len; ++i) {
var jsAtom = jsPropertyEnumGetAtom(ptab.value, i);
var jsAtomValue = jsAtomToValue(ctx, jsAtom);
var jsProp = jsGetProperty(ctx, val, jsAtom);
ret[jsToDart(ctx, jsAtomValue, cache: cache)] =
jsToDart(ctx, jsProp, cache: cache);
jsFreeValue(ctx, jsAtomValue);
jsFreeValue(ctx, jsProp);
jsFreeAtom(ctx, jsAtom);
}
jsFree(ctx, ptab.value);
free(ptab);
return ret;
}
break;
default:
}
return null;
}