mirror of
https://github.com/wgh136/flutter_qjs.git
synced 2025-09-27 05:27:23 +00:00
v0.3.0
This commit is contained in:
@@ -6,6 +6,11 @@
|
|||||||
* @LastEditTime: 2020-12-02 11:36:40
|
* @LastEditTime: 2020-12-02 11:36:40
|
||||||
-->
|
-->
|
||||||
|
|
||||||
|
## 0.3.0
|
||||||
|
|
||||||
|
* breakdown change to remove `channel`.
|
||||||
|
* convert dart function to js.
|
||||||
|
|
||||||
## 0.2.7
|
## 0.2.7
|
||||||
|
|
||||||
* fix error in ios build.
|
* fix error in ios build.
|
||||||
|
49
README.md
49
README.md
@@ -60,23 +60,6 @@ Data conversion between dart and js are implemented as follow:
|
|||||||
|
|
||||||
**notice:** Dart function parameter `thisVal` is used to store `this` in js.
|
**notice:** Dart function parameter `thisVal` is used to store `this` in js.
|
||||||
|
|
||||||
### Set into global object
|
|
||||||
|
|
||||||
Method `setToGlobalObject` is presented to set dart object into global object.
|
|
||||||
For example, you can pass a function implement http in JavaScript with `Dio`:
|
|
||||||
|
|
||||||
```dart
|
|
||||||
engine.setToGlobalObject("http", (String url) {
|
|
||||||
return Dio().get(url).then((response) => response.data);
|
|
||||||
});
|
|
||||||
```
|
|
||||||
|
|
||||||
then, in java script you can use `http` function to invoke dart function:
|
|
||||||
|
|
||||||
```javascript
|
|
||||||
http("http://example.com/");
|
|
||||||
```
|
|
||||||
|
|
||||||
### 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`:
|
||||||
@@ -105,8 +88,6 @@ To use async function in module handler, try [Run on isolate thread](#Run-on-iso
|
|||||||
|
|
||||||
Create a `IsolateQjs` object, pass handlers to resolving modules. Async function such as `rootBundle.loadString` can be used now to get modules:
|
Create a `IsolateQjs` object, pass handlers to resolving modules. Async function such as `rootBundle.loadString` can be used now to get modules:
|
||||||
|
|
||||||
The `methodHandler` is used in isolate, so **the handler function must be a top-level function or a static method**.
|
|
||||||
|
|
||||||
```dart
|
```dart
|
||||||
dynamic methodHandler(String method, List arg) {
|
dynamic methodHandler(String method, List arg) {
|
||||||
switch (method) {
|
switch (method) {
|
||||||
@@ -126,21 +107,11 @@ final engine = IsolateQjs(
|
|||||||
// not need engine.dispatch();
|
// not need engine.dispatch();
|
||||||
```
|
```
|
||||||
|
|
||||||
Method `setToGlobalObject` is still here to set dart object into global object. Use `await` to make sure it is finished.
|
|
||||||
**Make sure the object that can pass through isolate**, For example, a top level function:
|
|
||||||
|
|
||||||
```dart
|
|
||||||
dynamic http(String url) {
|
|
||||||
return Dio().get(url).then((response) => response.data);
|
|
||||||
}
|
|
||||||
await engine.setToGlobalObject("http", http);
|
|
||||||
```
|
|
||||||
|
|
||||||
Same as run on main thread, use `evaluate` to run js script. In this way, `Promise` return by `evaluate` will be automatically tracked and return the resolved data:
|
Same as run on main thread, use `evaluate` to run js script. In this way, `Promise` return by `evaluate` will be automatically tracked and return the resolved data:
|
||||||
|
|
||||||
```dart
|
```dart
|
||||||
try {
|
try {
|
||||||
print(await engine.evaluate(code ?? '', "<eval>"));
|
print(await engine.evaluate(code ?? ''));
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
print(e.toString());
|
print(e.toString());
|
||||||
}
|
}
|
||||||
@@ -148,4 +119,22 @@ try {
|
|||||||
|
|
||||||
Method `close` can destroy quickjs runtime that can be recreated again if you call `evaluate`.
|
Method `close` can destroy quickjs runtime that can be recreated again if you call `evaluate`.
|
||||||
|
|
||||||
|
**notice:** Make sure arguments passed to `IsolateJSFunction` are avaliable for isolate, such as primities and top level function. Method `bind` can help to pass instance function to isolate:
|
||||||
|
|
||||||
|
```dart
|
||||||
|
await setToGlobalObject("func", await engine.bind(() {
|
||||||
|
// DO SOMETHING
|
||||||
|
}))
|
||||||
|
```
|
||||||
|
|
||||||
[This example](example/lib/main.dart) contains a complete demonstration on how to use this plugin.
|
[This example](example/lib/main.dart) contains a complete demonstration on how to use this plugin.
|
||||||
|
|
||||||
|
## Breaking change in v0.3.0
|
||||||
|
|
||||||
|
`channel` function is no longer utilized by default.
|
||||||
|
Use js function to set to global:
|
||||||
|
|
||||||
|
```dart
|
||||||
|
final setToGlobalObject = await engine.evaluate("(key, val) => this[key] = val;");
|
||||||
|
setToGlobalObject("channel", methodHandler);
|
||||||
|
```
|
@@ -141,11 +141,6 @@ extern "C"
|
|||||||
return new JSValue(JS_NewCFunctionData(ctx, js_channel, 0, 0, 1, funcData));
|
return new JSValue(JS_NewCFunctionData(ctx, js_channel, 0, 0, 1, funcData));
|
||||||
}
|
}
|
||||||
|
|
||||||
DLLEXPORT JSValue *jsGetGlobalObject(JSContext *ctx)
|
|
||||||
{
|
|
||||||
return new JSValue(JS_GetGlobalObject(ctx));
|
|
||||||
}
|
|
||||||
|
|
||||||
DLLEXPORT JSContext *jsNewContext(JSRuntime *rt)
|
DLLEXPORT JSContext *jsNewContext(JSRuntime *rt)
|
||||||
{
|
{
|
||||||
JSContext *ctx = JS_NewContext(rt);
|
JSContext *ctx = JS_NewContext(rt);
|
||||||
|
@@ -73,7 +73,7 @@ class _TestPageState extends State<TestPage> {
|
|||||||
CodeInputController _controller = CodeInputController(
|
CodeInputController _controller = CodeInputController(
|
||||||
text: 'import("hello").then(({default: greet}) => greet("world"));');
|
text: 'import("hello").then(({default: greet}) => greet("world"));');
|
||||||
|
|
||||||
_ensureEngine() {
|
_ensureEngine() async {
|
||||||
if (engine != null) return;
|
if (engine != null) return;
|
||||||
engine = IsolateQjs(
|
engine = IsolateQjs(
|
||||||
moduleHandler: (String module) async {
|
moduleHandler: (String module) async {
|
||||||
@@ -82,7 +82,9 @@ class _TestPageState extends State<TestPage> {
|
|||||||
"js/" + module.replaceFirst(new RegExp(r".js$"), "") + ".js");
|
"js/" + module.replaceFirst(new RegExp(r".js$"), "") + ".js");
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
engine.setToGlobalObject("channel", methodHandler);
|
final setToGlobalObject =
|
||||||
|
await engine.evaluate("(key, val) => this[key] = val;");
|
||||||
|
setToGlobalObject("channel", methodHandler);
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@@ -103,7 +105,7 @@ class _TestPageState extends State<TestPage> {
|
|||||||
FlatButton(
|
FlatButton(
|
||||||
child: Text("evaluate"),
|
child: Text("evaluate"),
|
||||||
onPressed: () async {
|
onPressed: () async {
|
||||||
_ensureEngine();
|
await _ensureEngine();
|
||||||
try {
|
try {
|
||||||
resp = (await engine.evaluate(_controller.text ?? '',
|
resp = (await engine.evaluate(_controller.text ?? '',
|
||||||
name: "<eval>"))
|
name: "<eval>"))
|
||||||
|
@@ -82,7 +82,7 @@ packages:
|
|||||||
path: ".."
|
path: ".."
|
||||||
relative: true
|
relative: true
|
||||||
source: path
|
source: path
|
||||||
version: "0.2.7"
|
version: "0.3.0"
|
||||||
flutter_test:
|
flutter_test:
|
||||||
dependency: "direct dev"
|
dependency: "direct dev"
|
||||||
description: flutter
|
description: flutter
|
||||||
|
@@ -1,27 +0,0 @@
|
|||||||
// This is a basic Flutter widget test.
|
|
||||||
//
|
|
||||||
// To perform an interaction with a widget in your test, use the WidgetTester
|
|
||||||
// utility that Flutter provides. For example, you can send tap and scroll
|
|
||||||
// gestures. You can also use WidgetTester to find child widgets in the widget
|
|
||||||
// tree, read text, and verify that the values of widget properties are correct.
|
|
||||||
|
|
||||||
import 'package:flutter/material.dart';
|
|
||||||
import 'package:flutter_test/flutter_test.dart';
|
|
||||||
|
|
||||||
import 'package:flutter_qjs_example/main.dart';
|
|
||||||
|
|
||||||
void main() {
|
|
||||||
testWidgets('Verify Platform version', (WidgetTester tester) async {
|
|
||||||
// Build our app and trigger a frame.
|
|
||||||
await tester.pumpWidget(MyApp());
|
|
||||||
|
|
||||||
// Verify that platform version is retrieved.
|
|
||||||
expect(
|
|
||||||
find.byWidgetPredicate(
|
|
||||||
(Widget widget) =>
|
|
||||||
widget is Text && widget.data.startsWith('Running on:'),
|
|
||||||
),
|
|
||||||
findsOneWidget,
|
|
||||||
);
|
|
||||||
});
|
|
||||||
}
|
|
11
lib/ffi.dart
11
lib/ffi.dart
@@ -186,17 +186,6 @@ final Pointer Function(
|
|||||||
)>>("jsNewCFunction")
|
)>>("jsNewCFunction")
|
||||||
.asFunction();
|
.asFunction();
|
||||||
|
|
||||||
/// JSValue *jsGetGlobalObject(JSContext *ctx)
|
|
||||||
final Pointer Function(
|
|
||||||
Pointer ctx,
|
|
||||||
) jsGetGlobalObject = qjsLib
|
|
||||||
.lookup<
|
|
||||||
NativeFunction<
|
|
||||||
Pointer Function(
|
|
||||||
Pointer,
|
|
||||||
)>>("jsGetGlobalObject")
|
|
||||||
.asFunction();
|
|
||||||
|
|
||||||
/// JSContext *jsNewContext(JSRuntime *rt)
|
/// JSContext *jsNewContext(JSRuntime *rt)
|
||||||
final Pointer Function(
|
final Pointer Function(
|
||||||
Pointer rt,
|
Pointer rt,
|
||||||
|
@@ -13,6 +13,8 @@ import 'package:ffi/ffi.dart';
|
|||||||
import 'package:flutter_qjs/ffi.dart';
|
import 'package:flutter_qjs/ffi.dart';
|
||||||
import 'package:flutter_qjs/wrapper.dart';
|
import 'package:flutter_qjs/wrapper.dart';
|
||||||
|
|
||||||
|
import 'isolate.dart';
|
||||||
|
|
||||||
/// Handler function to manage js module.
|
/// Handler function to manage js module.
|
||||||
typedef JsModuleHandler = String Function(String name);
|
typedef JsModuleHandler = String Function(String name);
|
||||||
|
|
||||||
@@ -44,11 +46,10 @@ class FlutterQjs {
|
|||||||
this.hostPromiseRejectionHandler,
|
this.hostPromiseRejectionHandler,
|
||||||
});
|
});
|
||||||
|
|
||||||
setToGlobalObject(dynamic key, dynamic val) {
|
static applyFunction(Function func, List args, dynamic thisVal) {
|
||||||
_ensureEngine();
|
final passThis =
|
||||||
final globalObject = jsGetGlobalObject(_ctx);
|
RegExp("{.*thisVal.*}").hasMatch(func.runtimeType.toString());
|
||||||
definePropertyValue(_ctx, globalObject, key, val);
|
return Function.apply(func, args, passThis ? {#thisVal: thisVal} : null);
|
||||||
jsFreeValue(_ctx, globalObject);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
_ensureEngine() {
|
_ensureEngine() {
|
||||||
@@ -68,13 +69,11 @@ class FlutterQjs {
|
|||||||
)));
|
)));
|
||||||
}
|
}
|
||||||
final thisVal = jsToDart(ctx, pdata.elementAt(0).value);
|
final thisVal = jsToDart(ctx, pdata.elementAt(0).value);
|
||||||
Function func = jsToDart(ctx, pdata.elementAt(3).value);
|
final func = jsToDart(ctx, pdata.elementAt(3).value);
|
||||||
final passThis =
|
final ret = func is QjsInvokable
|
||||||
RegExp("{.*thisVal.*}").hasMatch(func.runtimeType.toString());
|
? func.invoke(args, thisVal)
|
||||||
return dartToJs(
|
: applyFunction(func, args, thisVal);
|
||||||
ctx,
|
return dartToJs(ctx, ret);
|
||||||
Function.apply(func, args, passThis ? {#thisVal: thisVal} : null),
|
|
||||||
);
|
|
||||||
case JSChannelType.MODULE:
|
case JSChannelType.MODULE:
|
||||||
if (moduleHandler == null) throw Exception("No ModuleHandler");
|
if (moduleHandler == null) throw Exception("No ModuleHandler");
|
||||||
var ret = Utf8.toUtf8(moduleHandler(
|
var ret = Utf8.toUtf8(moduleHandler(
|
||||||
|
122
lib/isolate.dart
122
lib/isolate.dart
@@ -14,13 +14,13 @@ import 'package:ffi/ffi.dart';
|
|||||||
import 'package:flutter_qjs/flutter_qjs.dart';
|
import 'package:flutter_qjs/flutter_qjs.dart';
|
||||||
import 'package:flutter_qjs/wrapper.dart';
|
import 'package:flutter_qjs/wrapper.dart';
|
||||||
|
|
||||||
class IsolateJSFunction {
|
class IsolateJSFunction implements QjsInvokable {
|
||||||
int val;
|
int val;
|
||||||
int ctx;
|
int ctx;
|
||||||
SendPort port;
|
SendPort port;
|
||||||
IsolateJSFunction(this.ctx, this.val, this.port);
|
IsolateJSFunction(this.ctx, this.val, this.port);
|
||||||
|
|
||||||
Future<dynamic> invoke(List<dynamic> arguments) async {
|
Future invoke(List arguments, [thisVal]) async {
|
||||||
if (0 == val ?? 0) return;
|
if (0 == val ?? 0) return;
|
||||||
var evaluatePort = ReceivePort();
|
var evaluatePort = ReceivePort();
|
||||||
port.send({
|
port.send({
|
||||||
@@ -28,9 +28,11 @@ class IsolateJSFunction {
|
|||||||
'ctx': ctx,
|
'ctx': ctx,
|
||||||
'val': val,
|
'val': val,
|
||||||
'args': _encodeData(arguments),
|
'args': _encodeData(arguments),
|
||||||
|
'this': _encodeData(thisVal),
|
||||||
'port': evaluatePort.sendPort,
|
'port': evaluatePort.sendPort,
|
||||||
});
|
});
|
||||||
var result = await evaluatePort.first;
|
var result = await evaluatePort.first;
|
||||||
|
evaluatePort.close();
|
||||||
if (result['data'] != null)
|
if (result['data'] != null)
|
||||||
return _decodeData(result['data'], port);
|
return _decodeData(result['data'], port);
|
||||||
else
|
else
|
||||||
@@ -39,7 +41,63 @@ class IsolateJSFunction {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
noSuchMethod(Invocation invocation) {
|
noSuchMethod(Invocation invocation) {
|
||||||
return invoke(invocation.positionalArguments);
|
return invoke(
|
||||||
|
invocation.positionalArguments,
|
||||||
|
invocation.namedArguments[#thisVal],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class IsolateFunction implements QjsInvokable {
|
||||||
|
SendPort _port;
|
||||||
|
SendPort func;
|
||||||
|
IsolateFunction(this.func, this._port);
|
||||||
|
|
||||||
|
static IsolateFunction bind(Function func, SendPort port) {
|
||||||
|
final funcPort = ReceivePort();
|
||||||
|
funcPort.listen((msg) async {
|
||||||
|
var data;
|
||||||
|
SendPort msgPort = msg['port'];
|
||||||
|
try {
|
||||||
|
List args = _decodeData(msg['args'], port);
|
||||||
|
Map thisVal = _decodeData(msg['this'], port);
|
||||||
|
data = await FlutterQjs.applyFunction(func, args, thisVal);
|
||||||
|
if (msgPort != null)
|
||||||
|
msgPort.send({
|
||||||
|
'data': _encodeData(data),
|
||||||
|
});
|
||||||
|
} catch (e, stack) {
|
||||||
|
if (msgPort != null)
|
||||||
|
msgPort.send({
|
||||||
|
'error': e.toString() + "\n" + stack.toString(),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return IsolateFunction(funcPort.sendPort, port);
|
||||||
|
}
|
||||||
|
|
||||||
|
Future invoke(List positionalArguments, [thisVal]) async {
|
||||||
|
if (func == null) return;
|
||||||
|
var evaluatePort = ReceivePort();
|
||||||
|
func.send({
|
||||||
|
'args': _encodeData(positionalArguments),
|
||||||
|
'this': _encodeData(thisVal),
|
||||||
|
'port': evaluatePort.sendPort,
|
||||||
|
});
|
||||||
|
var result = await evaluatePort.first;
|
||||||
|
evaluatePort.close();
|
||||||
|
if (result['data'] != null)
|
||||||
|
return _decodeData(result['data'], _port);
|
||||||
|
else
|
||||||
|
throw result['error'];
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
noSuchMethod(Invocation invocation) {
|
||||||
|
return invoke(
|
||||||
|
invocation.positionalArguments,
|
||||||
|
invocation.namedArguments[#thisVal],
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -75,16 +133,23 @@ dynamic _encodeData(dynamic data, {Map<dynamic, dynamic> cache}) {
|
|||||||
'__js_function_val': data.val,
|
'__js_function_val': data.val,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
if (data is IsolateFunction) {
|
||||||
|
return {
|
||||||
|
'__js_function_port': data.func,
|
||||||
|
};
|
||||||
|
}
|
||||||
if (data is Future) {
|
if (data is Future) {
|
||||||
var futurePort = ReceivePort();
|
var futurePort = ReceivePort();
|
||||||
data.then((value) {
|
data.then((value) {
|
||||||
futurePort.first.then((port) => {
|
futurePort.first.then((port) {
|
||||||
(port as SendPort).send({'data': _encodeData(value)})
|
futurePort.close();
|
||||||
|
(port as SendPort).send({'data': _encodeData(value)});
|
||||||
});
|
});
|
||||||
}, onError: (e, stack) {
|
}, onError: (e, stack) {
|
||||||
futurePort.first.then((port) => {
|
futurePort.first.then((port) {
|
||||||
|
futurePort.close();
|
||||||
(port as SendPort)
|
(port as SendPort)
|
||||||
.send({'error': e.toString() + "\n" + stack.toString()})
|
.send({'error': e.toString() + "\n" + stack.toString()});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
return {
|
return {
|
||||||
@@ -116,12 +181,17 @@ dynamic _decodeData(dynamic data, SendPort port,
|
|||||||
return JSFunction.fromAddress(ctx, val);
|
return JSFunction.fromAddress(ctx, val);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if (data.containsKey('__js_function_port')) {
|
||||||
|
return IsolateFunction(data['__js_function_port'], port);
|
||||||
|
}
|
||||||
if (data.containsKey('__js_future_port')) {
|
if (data.containsKey('__js_future_port')) {
|
||||||
SendPort port = data['__js_future_port'];
|
SendPort port = data['__js_future_port'];
|
||||||
var futurePort = ReceivePort();
|
var futurePort = ReceivePort();
|
||||||
port.send(futurePort.sendPort);
|
port.send(futurePort.sendPort);
|
||||||
var futureCompleter = Completer();
|
var futureCompleter = Completer();
|
||||||
|
futureCompleter.future.catchError((e) {});
|
||||||
futurePort.first.then((value) {
|
futurePort.first.then((value) {
|
||||||
|
futurePort.close();
|
||||||
if (value['error'] != null) {
|
if (value['error'] != null) {
|
||||||
futureCompleter.completeError(value['error']);
|
futureCompleter.completeError(value['error']);
|
||||||
} else {
|
} else {
|
||||||
@@ -172,8 +242,7 @@ void _runJsIsolate(Map spawnMessage) async {
|
|||||||
return ret;
|
return ret;
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
qjs.dispatch();
|
port.listen((msg) async {
|
||||||
await for (var msg in port) {
|
|
||||||
var data;
|
var data;
|
||||||
SendPort msgPort = msg['port'];
|
SendPort msgPort = msg['port'];
|
||||||
try {
|
try {
|
||||||
@@ -189,10 +258,10 @@ void _runJsIsolate(Map spawnMessage) async {
|
|||||||
data = JSFunction.fromAddress(
|
data = JSFunction.fromAddress(
|
||||||
msg['ctx'],
|
msg['ctx'],
|
||||||
msg['val'],
|
msg['val'],
|
||||||
).invoke(_decodeData(msg['args'], null));
|
).invoke(
|
||||||
break;
|
_decodeData(msg['args'], null),
|
||||||
case 'setToGlobalObject':
|
_decodeData(msg['this'], null),
|
||||||
qjs.setToGlobalObject(msg['key'], msg['val']);
|
);
|
||||||
break;
|
break;
|
||||||
case 'close':
|
case 'close':
|
||||||
qjs.port.close();
|
qjs.port.close();
|
||||||
@@ -210,7 +279,8 @@ void _runJsIsolate(Map spawnMessage) async {
|
|||||||
'error': e.toString() + "\n" + stack.toString(),
|
'error': e.toString() + "\n" + stack.toString(),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
});
|
||||||
|
await qjs.dispatch();
|
||||||
}
|
}
|
||||||
|
|
||||||
typedef JsAsyncModuleHandler = Future<String> Function(String name);
|
typedef JsAsyncModuleHandler = Future<String> Function(String name);
|
||||||
@@ -290,6 +360,12 @@ class IsolateQjs {
|
|||||||
_sendPort = completer.future;
|
_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.
|
/// Free Runtime and close isolate thread that can be recreate when evaluate again.
|
||||||
close() {
|
close() {
|
||||||
if (_sendPort == null) return;
|
if (_sendPort == null) return;
|
||||||
@@ -301,23 +377,6 @@ class IsolateQjs {
|
|||||||
_sendPort = null;
|
_sendPort = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
setToGlobalObject(dynamic key, dynamic val) async {
|
|
||||||
_ensureEngine();
|
|
||||||
var evaluatePort = ReceivePort();
|
|
||||||
var sendPort = await _sendPort;
|
|
||||||
sendPort.send({
|
|
||||||
'type': 'setToGlobalObject',
|
|
||||||
'key': key,
|
|
||||||
'val': val,
|
|
||||||
'port': evaluatePort.sendPort,
|
|
||||||
});
|
|
||||||
var result = await evaluatePort.first;
|
|
||||||
if (result['error'] == null) {
|
|
||||||
return;
|
|
||||||
} else
|
|
||||||
throw result['error'];
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Evaluate js script.
|
/// Evaluate js script.
|
||||||
Future<dynamic> evaluate(String command, {String name, int evalFlags}) async {
|
Future<dynamic> evaluate(String command, {String name, int evalFlags}) async {
|
||||||
_ensureEngine();
|
_ensureEngine();
|
||||||
@@ -331,6 +390,7 @@ class IsolateQjs {
|
|||||||
'port': evaluatePort.sendPort,
|
'port': evaluatePort.sendPort,
|
||||||
});
|
});
|
||||||
var result = await evaluatePort.first;
|
var result = await evaluatePort.first;
|
||||||
|
evaluatePort.close();
|
||||||
if (result['error'] == null) {
|
if (result['error'] == null) {
|
||||||
return _decodeData(result['data'], sendPort);
|
return _decodeData(result['data'], sendPort);
|
||||||
} else
|
} else
|
||||||
|
@@ -12,6 +12,7 @@ import 'dart:typed_data';
|
|||||||
import 'package:ffi/ffi.dart';
|
import 'package:ffi/ffi.dart';
|
||||||
|
|
||||||
import 'ffi.dart';
|
import 'ffi.dart';
|
||||||
|
import 'isolate.dart';
|
||||||
|
|
||||||
class JSRefValue implements JSRef {
|
class JSRefValue implements JSRef {
|
||||||
Pointer val;
|
Pointer val;
|
||||||
@@ -37,6 +38,14 @@ class JSRefValue implements JSRef {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
abstract class QjsReleasable {
|
||||||
|
void release();
|
||||||
|
}
|
||||||
|
|
||||||
|
abstract class QjsInvokable {
|
||||||
|
dynamic invoke(List positionalArguments, [dynamic thisVal]);
|
||||||
|
}
|
||||||
|
|
||||||
class DartObject implements JSRef {
|
class DartObject implements JSRef {
|
||||||
Object obj;
|
Object obj;
|
||||||
Pointer ctx;
|
Pointer ctx;
|
||||||
@@ -53,6 +62,7 @@ class DartObject implements JSRef {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
void release() {
|
void release() {
|
||||||
|
if (obj is QjsReleasable) (obj as QjsReleasable).release();
|
||||||
obj = null;
|
obj = null;
|
||||||
ctx = null;
|
ctx = null;
|
||||||
}
|
}
|
||||||
@@ -90,19 +100,19 @@ class JSPromise extends JSRefValue {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class JSFunction extends JSRefValue {
|
class JSFunction extends JSRefValue implements QjsInvokable {
|
||||||
JSFunction(Pointer ctx, Pointer val) : super(ctx, val);
|
JSFunction(Pointer ctx, Pointer val) : super(ctx, val);
|
||||||
|
|
||||||
JSFunction.fromAddress(int ctx, int val) : super.fromAddress(ctx, val);
|
JSFunction.fromAddress(int ctx, int val) : super.fromAddress(ctx, val);
|
||||||
|
|
||||||
invoke(List<dynamic> arguments) {
|
invoke(List<dynamic> arguments, [dynamic thisVal]) {
|
||||||
if (val == null) return;
|
if (val == null) return;
|
||||||
List<Pointer> args = arguments
|
List<Pointer> args = arguments
|
||||||
.map(
|
.map(
|
||||||
(e) => dartToJs(ctx, e),
|
(e) => dartToJs(ctx, e),
|
||||||
)
|
)
|
||||||
.toList();
|
.toList();
|
||||||
Pointer jsRet = jsCall(ctx, val, null, args);
|
Pointer jsRet = jsCall(ctx, val, dartToJs(ctx, thisVal), args);
|
||||||
for (Pointer jsArg in args) {
|
for (Pointer jsArg in args) {
|
||||||
jsFreeValue(ctx, jsArg);
|
jsFreeValue(ctx, jsArg);
|
||||||
}
|
}
|
||||||
@@ -118,7 +128,10 @@ class JSFunction extends JSRefValue {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
noSuchMethod(Invocation invocation) {
|
noSuchMethod(Invocation invocation) {
|
||||||
return invoke(invocation.positionalArguments);
|
return invoke(
|
||||||
|
invocation.positionalArguments,
|
||||||
|
invocation.namedArguments[#thisVal],
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -228,7 +241,7 @@ Pointer dartToJs(Pointer ctx, dynamic val, {Map<dynamic, dynamic> cache}) {
|
|||||||
dartObjectClassId,
|
dartObjectClassId,
|
||||||
identityHashCode(DartObject(ctx, val)),
|
identityHashCode(DartObject(ctx, val)),
|
||||||
);
|
);
|
||||||
if (val is Function) {
|
if (val is Function || val is IsolateFunction) {
|
||||||
final ret = jsNewCFunction(ctx, dartObject);
|
final ret = jsNewCFunction(ctx, dartObject);
|
||||||
jsFreeValue(ctx, dartObject);
|
jsFreeValue(ctx, dartObject);
|
||||||
return ret;
|
return ret;
|
||||||
@@ -343,6 +356,7 @@ Pointer jsNewContextWithPromsieWrapper(Pointer rt) {
|
|||||||
jsFreeValue(ctx, jsPromiseWrapper);
|
jsFreeValue(ctx, jsPromiseWrapper);
|
||||||
runtimeOpaque.promiseToFuture = (promise) {
|
runtimeOpaque.promiseToFuture = (promise) {
|
||||||
var completer = Completer();
|
var completer = Completer();
|
||||||
|
completer.future.catchError((e) {});
|
||||||
var wrapper = promiseWrapper.val;
|
var wrapper = promiseWrapper.val;
|
||||||
if (wrapper == null)
|
if (wrapper == null)
|
||||||
completer.completeError(Exception("Runtime has been released!"));
|
completer.completeError(Exception("Runtime has been released!"));
|
||||||
|
@@ -1,6 +1,6 @@
|
|||||||
name: flutter_qjs
|
name: flutter_qjs
|
||||||
description: This plugin is a simple js engine for flutter using the `quickjs` project. Plugin currently supports all the platforms except web!
|
description: This plugin is a simple js engine for flutter using the `quickjs` project. Plugin currently supports all the platforms except web!
|
||||||
version: 0.2.7
|
version: 0.3.0
|
||||||
homepage: https://github.com/ekibun/flutter_qjs
|
homepage: https://github.com/ekibun/flutter_qjs
|
||||||
|
|
||||||
environment:
|
environment:
|
||||||
|
@@ -14,32 +14,42 @@ import 'package:flutter_qjs/flutter_qjs.dart';
|
|||||||
import 'package:flutter_qjs/isolate.dart';
|
import 'package:flutter_qjs/isolate.dart';
|
||||||
import 'package:flutter_test/flutter_test.dart';
|
import 'package:flutter_test/flutter_test.dart';
|
||||||
|
|
||||||
dynamic myMethodHandler(List args, {String thisVal}) {
|
dynamic myFunction(String args, {String thisVal}) {
|
||||||
return [thisVal, ...args];
|
return [thisVal, args];
|
||||||
}
|
}
|
||||||
|
|
||||||
Future testEvaluate(qjs) async {
|
Future testEvaluate(qjs) async {
|
||||||
final value = await qjs.evaluate("""
|
final testWrap = await qjs.evaluate("(a) => a", name: "<testWrap>");
|
||||||
const a = {};
|
final primities = [0, 1, 0.1, true, false, "str"];
|
||||||
a.a = a;
|
final wrapPrimities = await testWrap(primities);
|
||||||
import('test').then((module) => channel.call('this', [
|
for (var i = 0; i < primities.length; i++) {
|
||||||
(...args)=>`hello \${args}!`, a,
|
expect(wrapPrimities[i], primities[i], reason: "wrap primities");
|
||||||
Promise.reject('test Promise.reject'), Promise.resolve('test Promise.resolve'),
|
}
|
||||||
0.1, true, false, 1, "world", module
|
final a = {};
|
||||||
]));
|
a["a"] = a;
|
||||||
""", name: "<eval>");
|
final wrapA = await testWrap(a);
|
||||||
expect(value[0], 'this', reason: "js function this");
|
expect(wrapA['a'], wrapA, reason: "recursive object");
|
||||||
expect(await value[1]('world'), 'hello world!', reason: "js function call");
|
final testThis = await qjs.evaluate(
|
||||||
expect(value[2]['a'], value[2], reason: "recursive object");
|
"(func) => func.call('this', 'arg')",
|
||||||
expect(value[3], isInstanceOf<Future>(), reason: "promise object");
|
name: "<testThis>",
|
||||||
|
);
|
||||||
|
final funcRet = await testThis(myFunction);
|
||||||
|
expect(funcRet[0], 'this', reason: "js function this");
|
||||||
|
expect(funcRet[1], 'arg', reason: "js function argument");
|
||||||
|
final promises = await testWrap(await qjs.evaluate(
|
||||||
|
"[Promise.reject('test Promise.reject'), Promise.resolve('test Promise.resolve')]",
|
||||||
|
name: "<promises>",
|
||||||
|
));
|
||||||
|
for (final promise in promises)
|
||||||
|
expect(promise, isInstanceOf<Future>(), reason: "promise object");
|
||||||
try {
|
try {
|
||||||
await value[3];
|
await promises[0];
|
||||||
throw 'Future not reject';
|
throw 'Future not reject';
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
expect(e, startsWith('test Promise.reject\n'),
|
expect(e, startsWith('test Promise.reject\n'),
|
||||||
reason: "promise object reject");
|
reason: "promise object reject");
|
||||||
}
|
}
|
||||||
expect(await value[4], 'test Promise.resolve',
|
expect(await promises[1], 'test Promise.resolve',
|
||||||
reason: "promise object resolve");
|
reason: "promise object resolve");
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -95,41 +105,37 @@ void main() async {
|
|||||||
expect(result['default']['data'], 'test module', reason: "eval module");
|
expect(result['default']['data'], 'test module', reason: "eval module");
|
||||||
qjs.close();
|
qjs.close();
|
||||||
});
|
});
|
||||||
test('jsToDart', () async {
|
test('data conversion', () async {
|
||||||
final qjs = FlutterQjs(
|
final qjs = FlutterQjs(
|
||||||
moduleHandler: (name) {
|
moduleHandler: (name) {
|
||||||
return "export default '${new DateTime.now()}'";
|
return "export default '${new DateTime.now()}'";
|
||||||
},
|
},
|
||||||
hostPromiseRejectionHandler: (_) {},
|
hostPromiseRejectionHandler: (_) {},
|
||||||
);
|
);
|
||||||
qjs.setToGlobalObject("channel", myMethodHandler);
|
|
||||||
qjs.dispatch();
|
qjs.dispatch();
|
||||||
await testEvaluate(qjs);
|
await testEvaluate(qjs);
|
||||||
qjs.close();
|
qjs.close();
|
||||||
});
|
});
|
||||||
test('isolate', () async {
|
test('isolate conversion', () async {
|
||||||
await runZonedGuarded(() async {
|
|
||||||
final qjs = IsolateQjs(
|
final qjs = IsolateQjs(
|
||||||
moduleHandler: (name) async {
|
moduleHandler: (name) async {
|
||||||
return "export default '${new DateTime.now()}'";
|
return "export default '${new DateTime.now()}'";
|
||||||
},
|
},
|
||||||
hostPromiseRejectionHandler: (_) {},
|
hostPromiseRejectionHandler: (_) {},
|
||||||
);
|
);
|
||||||
await qjs.setToGlobalObject("channel", myMethodHandler);
|
|
||||||
await testEvaluate(qjs);
|
await testEvaluate(qjs);
|
||||||
qjs.close();
|
qjs.close();
|
||||||
}, (e, stack) {
|
|
||||||
if (!e.toString().startsWith("test Promise.reject")) throw e;
|
|
||||||
});
|
});
|
||||||
});
|
test('isolate bind function', () async {
|
||||||
test('dart object', () async {
|
final qjs = IsolateQjs();
|
||||||
final qjs = FlutterQjs();
|
var localVar;
|
||||||
qjs.setToGlobalObject("channel", () {
|
final testFunc = await qjs.evaluate("(func)=>func('ret')", name: "<eval>");
|
||||||
return FlutterQjs();
|
final testFuncRet = await testFunc(await qjs.bind((args) {
|
||||||
});
|
localVar = 'test';
|
||||||
qjs.dispatch();
|
return args;
|
||||||
var value = await qjs.evaluate("channel()", name: "<eval>");
|
}));
|
||||||
expect(value, isInstanceOf<FlutterQjs>(), reason: "dart object");
|
expect(localVar, 'test', reason: "bind function");
|
||||||
|
expect(testFuncRet, 'ret', reason: "bind function args return");
|
||||||
qjs.close();
|
qjs.close();
|
||||||
});
|
});
|
||||||
test('stack overflow', () async {
|
test('stack overflow', () async {
|
||||||
|
Reference in New Issue
Block a user