mirror of
https://github.com/wgh136/flutter_qjs.git
synced 2025-09-27 13:27:24 +00:00
reference leak
This commit is contained in:
15
README.md
15
README.md
@@ -38,8 +38,12 @@ try {
|
|||||||
Method `close` can destroy quickjs runtime that can be recreated again if you call `evaluate`. Parameter `port` should be close to stop `dispatch` loop when you do not need it.
|
Method `close` can destroy quickjs runtime that can be recreated again if you call `evaluate`. Parameter `port` should be close to stop `dispatch` loop when you do not need it.
|
||||||
|
|
||||||
```dart
|
```dart
|
||||||
engine.port.close(); // stop dispatch loop
|
try {
|
||||||
engine.close(); // close engine
|
engine.port.close(); // stop dispatch loop
|
||||||
|
engine.close(); // close engine
|
||||||
|
} on JSError catch(e) {
|
||||||
|
print(e); // catch reference leak exception
|
||||||
|
}
|
||||||
engine = null;
|
engine = null;
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -72,6 +76,13 @@ or use `invoke` method to pass list parameters:
|
|||||||
(func as JSInvokable).invoke([arg1, arg2], thisVal);
|
(func as JSInvokable).invoke([arg1, arg2], thisVal);
|
||||||
```
|
```
|
||||||
|
|
||||||
|
`JSInvokable` returned by evaluation may increase reference of JS object.
|
||||||
|
You should manually call `release` to release JS reference:
|
||||||
|
|
||||||
|
```dart
|
||||||
|
(func as JSInvokable).release();
|
||||||
|
```
|
||||||
|
|
||||||
### Use modules
|
### Use modules
|
||||||
|
|
||||||
ES6 module with `import` function is supported and can be managed in dart with `moduleHandler`:
|
ES6 module with `import` function is supported and can be managed in dart with `moduleHandler`:
|
||||||
|
@@ -8,7 +8,7 @@
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
import 'package:flutter/services.dart';
|
import 'package:flutter/services.dart';
|
||||||
import 'package:flutter_qjs/isolate.dart';
|
import 'package:flutter_qjs/flutter_qjs.dart';
|
||||||
|
|
||||||
import 'highlight.dart';
|
import 'highlight.dart';
|
||||||
|
|
||||||
|
@@ -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:async';
|
||||||
import 'dart:ffi';
|
import 'dart:ffi';
|
||||||
|
import 'dart:io';
|
||||||
import 'dart:isolate';
|
import 'dart:isolate';
|
||||||
|
import 'dart:typed_data';
|
||||||
import 'package:ffi/ffi.dart';
|
import 'package:ffi/ffi.dart';
|
||||||
import 'ffi.dart';
|
import 'src/ffi.dart';
|
||||||
import 'wrapper.dart';
|
export 'src/ffi.dart' show JSEvalFlag;
|
||||||
|
|
||||||
/// Handler function to manage js module.
|
part 'src/engine.dart';
|
||||||
typedef JsModuleHandler = String Function(String name);
|
part 'src/isolate.dart';
|
||||||
|
part 'src/wrapper.dart';
|
||||||
/// Handler to manage unhandled promise rejection.
|
part 'src/object.dart';
|
||||||
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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
199
lib/isolate.dart
199
lib/isolate.dart
@@ -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
162
lib/src/engine.dart
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
@@ -1,5 +1,5 @@
|
|||||||
/*
|
/*
|
||||||
* @Description:
|
* @Description: ffi
|
||||||
* @Author: ekibun
|
* @Author: ekibun
|
||||||
* @Date: 2020-09-19 10:29:04
|
* @Date: 2020-09-19 10:29:04
|
||||||
* @LastEditors: ekibun
|
* @LastEditors: ekibun
|
||||||
@@ -8,10 +8,10 @@
|
|||||||
import 'dart:ffi';
|
import 'dart:ffi';
|
||||||
import 'dart:io';
|
import 'dart:io';
|
||||||
import 'dart:isolate';
|
import 'dart:isolate';
|
||||||
|
|
||||||
import 'package:ffi/ffi.dart';
|
import 'package:ffi/ffi.dart';
|
||||||
|
|
||||||
abstract class JSRef {
|
abstract class JSRef {
|
||||||
|
bool leakable = false;
|
||||||
void release();
|
void release();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -90,13 +90,13 @@ final Pointer Function() jsUNDEFINED = _qjsLib
|
|||||||
.lookup<NativeFunction<Pointer Function()>>('jsUNDEFINED')
|
.lookup<NativeFunction<Pointer Function()>>('jsUNDEFINED')
|
||||||
.asFunction();
|
.asFunction();
|
||||||
|
|
||||||
typedef JSChannel = Pointer Function(Pointer ctx, int method, Pointer argv);
|
typedef _JSChannel = Pointer Function(Pointer ctx, int method, Pointer argv);
|
||||||
typedef JSChannelNative = Pointer Function(
|
typedef _JSChannelNative = Pointer Function(
|
||||||
Pointer ctx, IntPtr method, Pointer argv);
|
Pointer ctx, IntPtr method, Pointer argv);
|
||||||
|
|
||||||
/// JSRuntime *jsNewRuntime(JSChannel channel)
|
/// JSRuntime *jsNewRuntime(JSChannel channel)
|
||||||
final Pointer Function(
|
final Pointer Function(
|
||||||
Pointer<NativeFunction<JSChannelNative>>,
|
Pointer<NativeFunction<_JSChannelNative>>,
|
||||||
) _jsNewRuntime = _qjsLib
|
) _jsNewRuntime = _qjsLib
|
||||||
.lookup<
|
.lookup<
|
||||||
NativeFunction<
|
NativeFunction<
|
||||||
@@ -105,29 +105,37 @@ final Pointer Function(
|
|||||||
)>>('jsNewRuntime')
|
)>>('jsNewRuntime')
|
||||||
.asFunction();
|
.asFunction();
|
||||||
|
|
||||||
class RuntimeOpaque {
|
class _RuntimeOpaque {
|
||||||
JSChannel channel;
|
_JSChannel _channel;
|
||||||
List<JSRef> ref = [];
|
List<JSRef> _ref = [];
|
||||||
ReceivePort port;
|
ReceivePort _port;
|
||||||
int dartObjectClassId;
|
int _dartObjectClassId;
|
||||||
int jsExceptionClassId;
|
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 channelDispacher(Pointer ctx, int type, Pointer argv) {
|
||||||
Pointer rt = type == JSChannelType.FREE_OBJECT ? ctx : jsGetRuntime(ctx);
|
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(
|
Pointer jsNewRuntime(
|
||||||
JSChannel callback,
|
_JSChannel callback,
|
||||||
ReceivePort port,
|
ReceivePort port,
|
||||||
) {
|
) {
|
||||||
var rt = _jsNewRuntime(Pointer.fromFunction(channelDispacher));
|
final rt = _jsNewRuntime(Pointer.fromFunction(channelDispacher));
|
||||||
runtimeOpaques[rt] = RuntimeOpaque()
|
runtimeOpaques[rt] = _RuntimeOpaque()
|
||||||
..channel = callback
|
.._channel = callback
|
||||||
..port = port;
|
.._port = port;
|
||||||
return rt;
|
return rt;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -158,12 +166,28 @@ final void Function(
|
|||||||
void jsFreeRuntime(
|
void jsFreeRuntime(
|
||||||
Pointer rt,
|
Pointer rt,
|
||||||
) {
|
) {
|
||||||
while (0 < runtimeOpaques[rt]?.ref?.length ?? 0) {
|
final referenceleak = <String>[];
|
||||||
final ref = runtimeOpaques[rt]?.ref?.first;
|
while (true) {
|
||||||
|
final ref = runtimeOpaques[rt]
|
||||||
|
?._ref
|
||||||
|
?.firstWhere((ref) => ref.leakable, orElse: () => null);
|
||||||
|
if (ref == null) break;
|
||||||
ref.release();
|
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);
|
_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)
|
/// JSValue *jsNewCFunction(JSContext *ctx, JSValue *funcData)
|
||||||
@@ -191,11 +215,10 @@ final Pointer Function(
|
|||||||
.asFunction();
|
.asFunction();
|
||||||
|
|
||||||
Pointer jsNewContext(Pointer rt) {
|
Pointer jsNewContext(Pointer rt) {
|
||||||
var ctx = _jsNewContext(rt);
|
final ctx = _jsNewContext(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;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -246,9 +269,9 @@ Pointer jsEval(
|
|||||||
String filename,
|
String filename,
|
||||||
int evalFlags,
|
int evalFlags,
|
||||||
) {
|
) {
|
||||||
var utf8input = Utf8.toUtf8(input);
|
final utf8input = Utf8.toUtf8(input);
|
||||||
var utf8filename = Utf8.toUtf8(filename);
|
final utf8filename = Utf8.toUtf8(filename);
|
||||||
var val = _jsEval(
|
final val = _jsEval(
|
||||||
ctx,
|
ctx,
|
||||||
utf8input,
|
utf8input,
|
||||||
Utf8.strlen(utf8input),
|
Utf8.strlen(utf8input),
|
||||||
@@ -257,7 +280,7 @@ Pointer jsEval(
|
|||||||
);
|
);
|
||||||
free(utf8input);
|
free(utf8input);
|
||||||
free(utf8filename);
|
free(utf8filename);
|
||||||
runtimeOpaques[jsGetRuntime(ctx)].port.sendPort.send('eval');
|
runtimeOpaques[jsGetRuntime(ctx)]._port.sendPort.send(#eval);
|
||||||
return val;
|
return val;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -350,8 +373,10 @@ Pointer jsNewString(
|
|||||||
Pointer ctx,
|
Pointer ctx,
|
||||||
String str,
|
String str,
|
||||||
) {
|
) {
|
||||||
var utf8str = Utf8.toUtf8(str);
|
final utf8str = Utf8.toUtf8(str);
|
||||||
return _jsNewString(ctx, utf8str);
|
final jsStr = _jsNewString(ctx, utf8str);
|
||||||
|
free(utf8str);
|
||||||
|
return jsStr;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// JSValue *jsNewArrayBufferCopy(JSContext *ctx, const uint8_t *buf, size_t len)
|
/// JSValue *jsNewArrayBufferCopy(JSContext *ctx, const uint8_t *buf, size_t len)
|
||||||
@@ -532,9 +557,9 @@ String jsToCString(
|
|||||||
Pointer ctx,
|
Pointer ctx,
|
||||||
Pointer val,
|
Pointer val,
|
||||||
) {
|
) {
|
||||||
var ptr = _jsToCString(ctx, val);
|
final ptr = _jsToCString(ctx, val);
|
||||||
if (ptr.address == 0) throw Exception('JSValue cannot convert to string');
|
if (ptr.address == 0) throw Exception('JSValue cannot convert to string');
|
||||||
var str = Utf8.fromUtf8(ptr);
|
final str = Utf8.fromUtf8(ptr);
|
||||||
jsFreeCString(ctx, ptr);
|
jsFreeCString(ctx, ptr);
|
||||||
return str;
|
return str;
|
||||||
}
|
}
|
||||||
@@ -556,8 +581,8 @@ int jsNewClass(
|
|||||||
Pointer ctx,
|
Pointer ctx,
|
||||||
String name,
|
String name,
|
||||||
) {
|
) {
|
||||||
var utf8name = Utf8.toUtf8(name);
|
final utf8name = Utf8.toUtf8(name);
|
||||||
var val = _jsNewClass(
|
final val = _jsNewClass(
|
||||||
ctx,
|
ctx,
|
||||||
utf8name,
|
utf8name,
|
||||||
);
|
);
|
||||||
@@ -842,7 +867,7 @@ Pointer jsCall(
|
|||||||
}
|
}
|
||||||
jsFreeValue(ctx, func1);
|
jsFreeValue(ctx, func1);
|
||||||
free(jsArgs);
|
free(jsArgs);
|
||||||
runtimeOpaques[jsGetRuntime(ctx)].port.sendPort.send('call');
|
runtimeOpaques[jsGetRuntime(ctx)]._port.sendPort.send(#call);
|
||||||
return jsRet;
|
return jsRet;
|
||||||
}
|
}
|
||||||
|
|
301
lib/src/isolate.dart
Normal file
301
lib/src/isolate.dart
Normal 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
342
lib/src/object.dart
Normal 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
235
lib/src/wrapper.dart
Normal 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;
|
||||||
|
}
|
615
lib/wrapper.dart
615
lib/wrapper.dart
@@ -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;
|
|
||||||
}
|
|
@@ -1,5 +1,5 @@
|
|||||||
/*
|
/*
|
||||||
* @Description:
|
* @Description: unit test
|
||||||
* @Author: ekibun
|
* @Author: ekibun
|
||||||
* @Date: 2020-09-06 13:02:46
|
* @Date: 2020-09-06 13:02:46
|
||||||
* @LastEditors: ekibun
|
* @LastEditors: ekibun
|
||||||
@@ -10,9 +10,6 @@ import 'dart:convert';
|
|||||||
import 'dart:io';
|
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/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}) {
|
||||||
@@ -21,69 +18,72 @@ dynamic myFunction(String args, {thisVal}) {
|
|||||||
|
|
||||||
Future testEvaluate(qjs) async {
|
Future testEvaluate(qjs) async {
|
||||||
final testWrap = await qjs.evaluate(
|
final testWrap = await qjs.evaluate(
|
||||||
"(a) => a",
|
'(a) => a',
|
||||||
name: "<testWrap>",
|
name: '<testWrap>',
|
||||||
);
|
);
|
||||||
final wrapNull = await testWrap(null);
|
final wrapNull = await testWrap(null);
|
||||||
expect(wrapNull, null, reason: "wrap null");
|
expect(wrapNull, null, reason: 'wrap null');
|
||||||
final primities = [0, 1, 0.1, true, false, "str"];
|
final primities = [0, 1, 0.1, true, false, 'str'];
|
||||||
final wrapPrimities = await testWrap(primities);
|
final wrapPrimities = await testWrap(primities);
|
||||||
for (var i = 0; i < primities.length; i++) {
|
for (int 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 jsError = JSError('test Error');
|
||||||
final wrapJsError = await testWrap(jsError);
|
final wrapJsError = await testWrap(jsError);
|
||||||
expect(jsError.message, (wrapJsError as JSError).message,
|
expect(jsError.message, (wrapJsError as JSError).message,
|
||||||
reason: "wrap JSError");
|
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',
|
||||||
name: "<testEqual>",
|
name: '<testEqual>',
|
||||||
);
|
);
|
||||||
expect(await testEqual(wrapFunction, testWrap), true,
|
expect(await testEqual(wrapFunction, testWrap), true,
|
||||||
reason: "wrap function");
|
reason: 'wrap function');
|
||||||
|
wrapFunction.release();
|
||||||
|
testEqual.release();
|
||||||
|
|
||||||
expect(wrapNull, null, reason: "wrap null");
|
expect(wrapNull, null, reason: 'wrap null');
|
||||||
final a = {};
|
final a = {};
|
||||||
a["a"] = a;
|
a['a'] = a;
|
||||||
final wrapA = await testWrap(a);
|
final wrapA = await testWrap(a);
|
||||||
expect(wrapA['a'], wrapA, reason: "recursive object");
|
expect(wrapA['a'], wrapA, reason: 'recursive object');
|
||||||
final testThis = await qjs.evaluate(
|
final testThis = await qjs.evaluate(
|
||||||
"(function (func, arg) { return func.call(this, arg) })",
|
'(function (func, arg) { return func.call(this, arg) })',
|
||||||
name: "<testThis>",
|
name: '<testThis>',
|
||||||
);
|
);
|
||||||
final funcRet = await testThis(myFunction, 'arg', thisVal: {'name': 'this'});
|
final funcRet = await testThis(myFunction, 'arg', thisVal: {'name': 'this'});
|
||||||
expect(funcRet[0]['name'], 'this', reason: "js function this");
|
testThis.release();
|
||||||
expect(funcRet[1], 'arg', reason: "js function argument");
|
expect(funcRet[0]['name'], 'this', reason: 'js function this');
|
||||||
|
expect(funcRet[1], 'arg', reason: 'js function argument');
|
||||||
final promises = await testWrap(await qjs.evaluate(
|
final promises = await testWrap(await qjs.evaluate(
|
||||||
"[Promise.reject('test Promise.reject'), Promise.resolve('test Promise.resolve')]",
|
'[Promise.reject("reject"), Promise.resolve("resolve"), new Promise(() => {})]',
|
||||||
name: "<promises>",
|
name: '<promises>',
|
||||||
));
|
));
|
||||||
for (final promise in promises)
|
for (final promise in promises)
|
||||||
expect(promise, isInstanceOf<Future>(), reason: "promise object");
|
expect(promise, isInstanceOf<Future>(), reason: 'promise object');
|
||||||
try {
|
try {
|
||||||
await promises[0];
|
await promises[0];
|
||||||
throw 'Future not reject';
|
throw 'Future not reject';
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
expect(e, 'test Promise.reject', reason: "promise object reject");
|
expect(e, 'reject', reason: 'promise object reject');
|
||||||
}
|
}
|
||||||
expect(await promises[1], 'test Promise.resolve',
|
expect(await promises[1], 'resolve', reason: 'promise object resolve');
|
||||||
reason: "promise object resolve");
|
testWrap.release();
|
||||||
}
|
}
|
||||||
|
|
||||||
void main() async {
|
void main() async {
|
||||||
test('make', () async {
|
test('make', () async {
|
||||||
final utf8Encoding = Encoding.getByName('utf-8');
|
final utf8Encoding = Encoding.getByName('utf-8');
|
||||||
var cmakePath = "cmake";
|
var cmakePath = 'cmake';
|
||||||
if (Platform.isWindows) {
|
if (Platform.isWindows) {
|
||||||
var vsDir = Directory("C:/Program Files (x86)/Microsoft Visual Studio/");
|
var vsDir = Directory('C:/Program Files (x86)/Microsoft Visual Studio/');
|
||||||
vsDir = (vsDir.listSync().firstWhere((e) => e is Directory) as Directory)
|
vsDir = (vsDir.listSync().firstWhere((e) => e is Directory) as Directory)
|
||||||
.listSync()
|
.listSync()
|
||||||
.last as Directory;
|
.last as Directory;
|
||||||
cmakePath = vsDir.path +
|
cmakePath = vsDir.path +
|
||||||
"/Common7/IDE/CommonExtensions/Microsoft/CMake/CMake/bin/cmake.exe";
|
'/Common7/IDE/CommonExtensions/Microsoft/CMake/CMake/bin/cmake.exe';
|
||||||
}
|
}
|
||||||
final buildDir = "./build";
|
final buildDir = './build';
|
||||||
var result = Process.runSync(
|
var result = Process.runSync(
|
||||||
cmakePath,
|
cmakePath,
|
||||||
['-S', './', '-B', buildDir],
|
['-S', './', '-B', buildDir],
|
||||||
@@ -109,7 +109,7 @@ void main() async {
|
|||||||
test('module', () async {
|
test('module', () async {
|
||||||
final qjs = FlutterQjs(
|
final qjs = FlutterQjs(
|
||||||
moduleHandler: (name) {
|
moduleHandler: (name) {
|
||||||
return "export default 'test module'";
|
return 'export default "test module"';
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
qjs.dispatch();
|
qjs.dispatch();
|
||||||
@@ -120,14 +120,11 @@ void main() async {
|
|||||||
};
|
};
|
||||||
''', name: 'evalModule', evalFlags: JSEvalFlag.MODULE);
|
''', name: 'evalModule', evalFlags: JSEvalFlag.MODULE);
|
||||||
var result = await qjs.evaluate('import("evalModule")');
|
var result = await qjs.evaluate('import("evalModule")');
|
||||||
expect(result['default']['data'], 'test module', reason: "eval module");
|
expect(result['default']['data'], 'test module', reason: 'eval module');
|
||||||
qjs.close();
|
qjs.close();
|
||||||
});
|
});
|
||||||
test('data conversion', () async {
|
test('data conversion', () async {
|
||||||
final qjs = FlutterQjs(
|
final qjs = FlutterQjs(
|
||||||
moduleHandler: (name) {
|
|
||||||
return "export default '${new DateTime.now()}'";
|
|
||||||
},
|
|
||||||
hostPromiseRejectionHandler: (_) {},
|
hostPromiseRejectionHandler: (_) {},
|
||||||
);
|
);
|
||||||
qjs.dispatch();
|
qjs.dispatch();
|
||||||
@@ -136,33 +133,43 @@ void main() async {
|
|||||||
});
|
});
|
||||||
test('isolate conversion', () async {
|
test('isolate conversion', () async {
|
||||||
final qjs = IsolateQjs(
|
final qjs = IsolateQjs(
|
||||||
moduleHandler: (name) async {
|
|
||||||
return "export default '${new DateTime.now()}'";
|
|
||||||
},
|
|
||||||
hostPromiseRejectionHandler: (_) {},
|
hostPromiseRejectionHandler: (_) {},
|
||||||
);
|
);
|
||||||
await testEvaluate(qjs);
|
await testEvaluate(qjs);
|
||||||
qjs.close();
|
await qjs.close();
|
||||||
});
|
});
|
||||||
test('isolate bind function', () async {
|
test('isolate bind function', () async {
|
||||||
final qjs = IsolateQjs();
|
final qjs = IsolateQjs();
|
||||||
var localVar;
|
var localVar;
|
||||||
final testFunc = await qjs.evaluate("(func)=>func('ret')", name: "<eval>");
|
final testFunc = await qjs.evaluate('(func)=>func("ret")', name: '<eval>');
|
||||||
final testFuncRet = await testFunc(await qjs.bind((args) {
|
final testFuncRet = await testFunc(await qjs.bind((args) {
|
||||||
localVar = 'test';
|
localVar = 'test';
|
||||||
return args;
|
return args;
|
||||||
}));
|
}));
|
||||||
expect(localVar, 'test', reason: "bind function");
|
testFunc.release();
|
||||||
expect(testFuncRet, 'ret', reason: "bind function args return");
|
expect(localVar, 'test', reason: 'bind function');
|
||||||
|
expect(testFuncRet, 'ret', reason: 'bind function args return');
|
||||||
|
await qjs.close();
|
||||||
|
});
|
||||||
|
test('reference leak', () async {
|
||||||
|
final qjs = FlutterQjs();
|
||||||
|
await qjs.evaluate('()=>{}', name: '<eval>');
|
||||||
|
try {
|
||||||
qjs.close();
|
qjs.close();
|
||||||
|
throw 'Error not throw';
|
||||||
|
} on JSError catch (e) {
|
||||||
|
expect(e.message, startsWith('reference leak:'),
|
||||||
|
reason: 'throw reference leak');
|
||||||
|
}
|
||||||
});
|
});
|
||||||
test('stack overflow', () async {
|
test('stack overflow', () async {
|
||||||
final qjs = FlutterQjs();
|
final qjs = FlutterQjs();
|
||||||
try {
|
try {
|
||||||
qjs.evaluate("a=()=>a();a();", name: "<eval>");
|
qjs.evaluate('a=()=>a();a();', name: '<eval>');
|
||||||
|
throw 'Error not throw';
|
||||||
} on JSError catch (e) {
|
} on JSError catch (e) {
|
||||||
expect(e.message, 'InternalError: stack overflow',
|
expect(e.message, 'InternalError: stack overflow',
|
||||||
reason: "throw stack overflow");
|
reason: 'throw stack overflow');
|
||||||
}
|
}
|
||||||
qjs.close();
|
qjs.close();
|
||||||
});
|
});
|
||||||
@@ -175,13 +182,13 @@ void main() async {
|
|||||||
);
|
);
|
||||||
qjs.dispatch();
|
qjs.dispatch();
|
||||||
qjs.evaluate(
|
qjs.evaluate(
|
||||||
"(() => { Promise.resolve().then(() => { throw 'unhandle' }) })()",
|
'(() => { Promise.resolve().then(() => { throw "unhandle" }) })()',
|
||||||
name: "<eval>");
|
name: '<eval>');
|
||||||
Future.delayed(Duration(seconds: 10)).then((value) {
|
Future.delayed(Duration(seconds: 10)).then((value) {
|
||||||
if (!completer.isCompleted) completer.completeError("not host reject");
|
if (!completer.isCompleted) completer.completeError('not host reject');
|
||||||
});
|
});
|
||||||
expect(await completer.future, "unhandle",
|
expect(await completer.future, 'unhandle',
|
||||||
reason: "host promise rejection");
|
reason: 'host promise rejection');
|
||||||
qjs.close();
|
qjs.close();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
Reference in New Issue
Block a user