This commit is contained in:
ekibun
2021-01-27 16:52:08 +08:00
parent 01ea420bd7
commit 0de94f12e2
12 changed files with 302 additions and 149 deletions

View File

@@ -6,6 +6,12 @@
* @LastEditTime: 2020-12-02 11:36:40
-->
## 0.3.3
* remove `JSInvokable.call`.
* fix crash when throw error.
* add reference count and leak detection.
## 0.3.2
* fix Promise reject cannot get Exception string.

157
README-CN.md Normal file
View File

@@ -0,0 +1,157 @@
<!--
* @Description:
* @Author: ekibun
* @Date: 2020-08-08 08:16:50
* @LastEditors: ekibun
* @LastEditTime: 2020-10-03 00:44:41
-->
# flutter_qjs
![Pub](https://img.shields.io/pub/v/flutter_qjs.svg)
![Test](https://github.com/ekibun/flutter_qjs/workflows/Test/badge.svg)
[English](README.md) | [中文](README-CN.md)
一个为flutter开发的 `quickjs` 引擎。插件基于 `dart:ffi`支持除Web以外的所有平台
## 基本使用
首先,创建 `FlutterQjs` 对象。调用 `dispatch` 建立事件循环:
```dart
final engine = FlutterQjs(
stackSize: 1024 * 1024, // change stack size here.
);
engine.dispatch();
```
使用 `evaluate` 方法运行js脚本方法同步执行使用 `await` 来获得 `Promise` 结果:
```dart
try {
print(engine.evaluate(code ?? ''));
} catch (e) {
print(e.toString());
}
```
使用 `close` 方法销毁 quickjs 实例,其在再次调用 `evaluate` 时将会重建。当不再需要 `FlutterQjs` 对象时,关闭 `port` 参数来结束事件循环。**在 v0.3.3 后增加了引用检查,可能会抛出异常**。
```dart
try {
engine.port.close(); // stop dispatch loop
engine.close(); // close engine
} on JSError catch(e) {
print(e); // catch reference leak exception
}
engine = null;
```
dart 与 js 间数据以如下规则转换:
| dart | js |
| ---------------------------- | ---------- |
| Bool | boolean |
| Int | number |
| Double | number |
| String | string |
| Uint8List | ArrayBuffer|
| List | Array |
| Map | Object |
| Function(arg1, arg2, ..., {thisVal})<br>JSInvokable.invoke(\[arg1, arg2, ...\], thisVal) | function.call(thisVal, arg1, arg2, ...) |
| Future | Promise |
| JSError | Error |
| Object | DartObject |
## 使用模块
插件支持 ES6 模块方法 `import`。使用 `moduleHandler` 来处理模块请求:
```dart
final engine = FlutterQjs(
moduleHandler: (String module) {
if(module == "hello")
return "export default (name) => `hello \${name}!`;";
throw Exception("Module Not found");
},
);
```
在JavaScript中`import` 方法用以获取模块:
```javascript
import("hello").then(({default: greet}) => greet("world"));
```
**注:** 模块将只被编译一次. 调用 `FlutterQjs.close``evaluate` 来重置模块缓存。
若要使用异步方法来处理模块请求,请参见 [在 isolate 中运行](#在-isolate-中运行)。
## 在 isolate 中运行
创建 `IsolateQjs` 对象,设置 `moduleHandler` 来处理模块请求。 现在可以使用异步函数来获得模块字符串,如 `rootBundle.loadString`
```dart
final engine = IsolateQjs(
moduleHandler: (String module) async {
return await rootBundle.loadString(
"js/" + module.replaceFirst(new RegExp(r".js$"), "") + ".js");
},
);
// not need engine.dispatch();
```
与在主线程运行一样,使用 `evaluate` 方法运行js脚本。在isolate中所有结果都将异步返回使用 `await` 来获取结果:
```dart
try {
print(await engine.evaluate(code ?? ''));
} catch (e) {
print(e.toString());
}
```
使用 `close` 方法销毁 isolate 线程,其在再次调用 `evaluate` 时将会重建。
## 调用 Dart 函数
Js脚本返回函数将被转换为 `JSInvokable`**它不能像 `Function` 一样调用,请使用 `invoke` 方法来调用**
```dart
(func as JSInvokable).invoke([arg1, arg2], thisVal);
```
**注:** 返回 `JSInvokable` 可能造成引用泄漏,需要手动调用 `free` 来释放引用:
```dart
(obj as JSRef).free();
// or JSRef.freeRecursive(obj);
```
传递给 `JSInvokable` 的参数将自动释放. 使用 `dup` 来保持引用:
```dart
(obj as JSRef).dup();
// or JSRef.dupRecursive(obj);
```
自 v0.3.0 起dart 函数可以作为参数传递给 `JSInvokable`,且 `channel` 函数不再默认内置。可以使用如下方法将 dart 函数赋值给全局,例如,使用 `Dio` 来为 qjs 提供 http 支持:
```dart
final setToGlobalObject = await engine.evaluate("(key, val) => { this[key] = val; }");
await setToGlobalObject.invoke(["http", (String url) {
return Dio().get(url).then((response) => response.data);
}]);
setToGlobalObject.free();
```
在 isolate 模式下,只有顶层和静态函数能作为参数传给 `JSInvokable`,函数将在 isolate 线程中调用。 使用 `IsolateFunction` 来传递局部函数(将在主线程中调用):
```dart
await setToGlobalObject.invoke([
"http",
IsolateFunction((String url) {
return Dio().get(url).then((response) => response.data);
}),
]);
```

115
README.md
View File

@@ -10,13 +10,15 @@
![Pub](https://img.shields.io/pub/v/flutter_qjs.svg)
![Test](https://github.com/ekibun/flutter_qjs/workflows/Test/badge.svg)
[English](README.md) | [中文](README-CN.md)
This plugin is a simple js engine for flutter using the `quickjs` project with `dart:ffi`. Plugin currently supports all the platforms except web!
## Getting Started
### Basic usage
Firstly, create a `FlutterQjs` object, then call `dispatch` to dispatch event loop:
Firstly, create a `FlutterQjs` object, then call `dispatch` to establish event loop:
```dart
final engine = FlutterQjs(
@@ -25,7 +27,7 @@ final engine = FlutterQjs(
engine.dispatch();
```
Use `evaluate` method to run js script, now you can use it synchronously, or use await to resolve `Promise`:
Use `evaluate` method to run js script, it runs synchronously, you can use await to resolve `Promise`:
```dart
try {
@@ -35,7 +37,7 @@ 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. **Reference leak exception will be thrown since v0.3.3**
```dart
try {
@@ -49,41 +51,21 @@ engine = null;
Data conversion between dart and js are implemented as follow:
| dart | js |
| ----------------------- | ------------------ |
| Bool | boolean |
| Int | number |
| Double | number |
| String | string |
| Uint8List | ArrayBuffer |
| List | Array |
| Map | Object |
| Function<br>JSInvokable | function(....args) |
| Future | Promise |
| JSError | Error |
| Object | DartObject |
| dart | js |
| ---------------------------- | ---------- |
| Bool | boolean |
| Int | number |
| Double | number |
| String | string |
| Uint8List | ArrayBuffer|
| List | Array |
| Map | Object |
| Function(arg1, arg2, ..., {thisVal})<br>JSInvokable.invoke(\[arg1, arg2, ...\], thisVal) | function.call(thisVal, arg1, arg2, ...) |
| Future | Promise |
| JSError | Error |
| Object | DartObject |
**notice:** `JSInvokable` does not extend `Function`, but can be used same as `Function`.
Dart function uses named argument `thisVal` to manage js function `this`:
```dart
func(arg1, arg2, {thisVal});
```
or use `invoke` method to pass list parameters:
```dart
(func as JSInvokable).invoke([arg1, arg2], thisVal);
```
`JSInvokable` returned by evaluation may increase reference of JS object.
You should manually call `free` to release JS reference:
```dart
(func as JSInvokable).free();
```
### Use modules
## Use Modules
ES6 module with `import` function is supported and can be managed in dart with `moduleHandler`:
@@ -105,9 +87,8 @@ import("hello").then(({default: greet}) => greet("world"));
**notice:** Module handler should be called only once for each module name. To reset the module cache, call `FlutterQjs.close` then `evaluate` again.
To use async function in module handler, try [Run on isolate thread](#Run-on-isolate-thread)
### Run on isolate thread
To use async function in module handler, try [run on isolate thread](#Run-on-Isolate-Thread)
## Run on Isolate Thread
Create a `IsolateQjs` object, pass handlers to resolving modules. Async function such as `rootBundle.loadString` can be used now to get modules:
@@ -121,7 +102,7 @@ final engine = IsolateQjs(
// not need engine.dispatch();
```
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 isolate, everything returns asynchronously, use `await` to get the result:
```dart
try {
@@ -131,25 +112,49 @@ try {
}
```
Method `close` can destroy quickjs runtime that can be recreated again if you call `evaluate`.
Method `close` can destroy isolate thread that will 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:
## Use Dart Function (Breaking change in v0.3.0)
Js script returning function will be converted to `JSInvokable`. **It does not extend `Function`, use `invoke` method to invoke it**:
```dart
await jsFunc(await engine.bind(({thisVal}) {
// DO SOMETHING
}));
(func as JSInvokable).invoke([arg1, arg2], thisVal);
```
[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 included by default.
Use js function to set dart object globally:
**notice:** evaluation returning `JSInvokable` may cause reference leak.
You should manually call `free` to release JS reference.
```dart
final setToGlobalObject = await engine.evaluate("(key, val) => this[key] = val;");
await setToGlobalObject("channel", methodHandler);
(obj as JSRef).free();
// or JSRef.freeRecursive(obj);
```
Arguments passed into `JSInvokable` will be freed automatically. Use `dup` to keep the reference.
```dart
(obj as JSRef).dup();
// or JSRef.dupRecursive(obj);
```
Since v0.3.0, you can pass a function to `JSInvokable` arguments, and `channel` function is no longer included by default. You can use js function to set dart object globally.
For example, use `Dio` to implement http in qjs:
```dart
final setToGlobalObject = await engine.evaluate("(key, val) => { this[key] = val; }");
await setToGlobalObject.invoke(["http", (String url) {
return Dio().get(url).then((response) => response.data);
}]);
setToGlobalObject.free();
```
In isolate, top level function passed in `JSInvokable` will be invoked in isolate thread. Use `IsolateFunction` to pass a instant function:
```dart
await setToGlobalObject.invoke([
"http",
IsolateFunction((String url) {
return Dio().get(url).then((response) => response.data);
}),
]);
```

View File

@@ -15,7 +15,7 @@ extern "C"
DLLEXPORT JSValue *jsThrow(JSContext *ctx, JSValue *obj)
{
return new JSValue(JS_Throw(ctx, *obj));
return new JSValue(JS_Throw(ctx, JS_DupValue(ctx, *obj)));
}
DLLEXPORT JSValue *jsEXCEPTION()

View File

@@ -75,7 +75,7 @@ packages:
path: ".."
relative: true
source: path
version: "0.3.2"
version: "0.3.3"
flutter_test:
dependency: "direct dev"
description: flutter

View File

@@ -5,7 +5,7 @@ import 'dart:isolate';
import 'dart:typed_data';
import 'package:ffi/ffi.dart';
import 'src/ffi.dart';
export 'src/ffi.dart' show JSEvalFlag;
export 'src/ffi.dart' show JSEvalFlag, JSRef;
part 'src/engine.dart';
part 'src/isolate.dart';

View File

@@ -22,6 +22,35 @@ abstract class JSRef {
}
void destroy();
static void freeRecursive(dynamic obj) {
_callRecursive(obj, (ref) => ref.free());
}
static void dupRecursive(dynamic obj) {
_callRecursive(obj, (ref) => ref.dup());
}
static void _callRecursive(
dynamic obj,
void Function(JSRef) cb, [
Set cache,
]) {
if (obj == null) return;
if (cache == null) cache = Set();
if (cache.contains(obj)) return;
if (obj is List) {
cache.add(obj);
obj.forEach((e) => _callRecursive(e, cb, cache));
}
if (obj is Map) {
cache.add(obj);
obj.values.forEach((e) => _callRecursive(e, cb, cache));
}
if (obj is JSRef) {
cb(obj);
}
}
}
abstract class JSRefLeakable {}
@@ -188,8 +217,10 @@ void jsFreeRuntime(
}
while (0 < runtimeOpaques[rt]?._ref?.length ?? 0) {
final ref = runtimeOpaques[rt]?._ref?.first;
final objStrs = ref.toString().split('\n');
final objStr = objStrs.length > 0 ? objStrs[0] + " ..." : objStrs[0];
referenceleak.add(
" ${identityHashCode(ref)}\t${ref._refCount + 1}\t${ref.runtimeType.toString()}\t${ref.toString().replaceAll('\n', '\\n')}");
" ${identityHashCode(ref)}\t${ref._refCount + 1}\t${ref.runtimeType.toString()}\t$objStr");
ref.destroy();
}
_jsFreeRuntime(rt);

View File

@@ -17,11 +17,7 @@ abstract class _IsolateEncodable {
Map _encode();
}
final List _sendAllowType = [Null, String, int, double, bool, SendPort];
dynamic _encodeData(dynamic data, {Map<dynamic, dynamic> cache}) {
if (data is Function) return data;
if (_sendAllowType.contains(data.runtimeType)) return data;
if (cache == null) cache = Map();
if (cache.containsKey(data)) return cache[data];
if (data is _IsolateEncodable) return data._encode();
@@ -59,7 +55,7 @@ dynamic _encodeData(dynamic data, {Map<dynamic, dynamic> cache}) {
#jsFuturePort: futurePort.sendPort,
};
}
throw JSError('unsupport type: ${data.runtimeType}\n${data.toString()}');
return data;
}
dynamic _decodeData(dynamic data, {Map<dynamic, dynamic> cache}) {

View File

@@ -24,23 +24,6 @@ class _DartFunction extends JSInvokable {
final Function _func;
_DartFunction(this._func);
void _freeRecursive(dynamic obj, [Set cache]) {
if (obj == null) return;
if (cache == null) cache = Set();
if (cache.contains(obj)) return;
if (obj is List) {
cache.add(obj);
obj.forEach((e) => _freeRecursive(e, cache));
}
if (obj is Map) {
cache.add(obj);
obj.values.forEach((e) => _freeRecursive(e, cache));
}
if (obj is JSRef) {
obj.free();
}
}
@override
invoke(List args, [thisVal]) {
/// wrap this into function
@@ -48,8 +31,8 @@ class _DartFunction extends JSInvokable {
RegExp('{.*thisVal.*}').hasMatch(_func.runtimeType.toString());
final ret =
Function.apply(_func, args, passThis ? {#thisVal: thisVal} : null);
_freeRecursive(args);
_freeRecursive(thisVal);
JSRef.freeRecursive(args);
JSRef.freeRecursive(thisVal);
return ret;
}
@@ -179,7 +162,7 @@ class _JSFunction extends _JSObject implements JSInvokable, _IsolateEncodable {
}
Pointer _invoke(List<dynamic> arguments, [dynamic thisVal]) {
if (_val == null) return null;
if (_val == null) throw JSError("InternalError: JSValue released");
List<Pointer> args = arguments
.map(
(e) => _dartToJs(_ctx, e),
@@ -196,22 +179,26 @@ class _JSFunction extends _JSObject implements JSInvokable, _IsolateEncodable {
@override
Map _encode() {
final func = IsolateFunction._new(this);
final ret = func._encode();
return ret;
return IsolateFunction._new(this)._encode();
}
}
abstract class _IsolatePortHandler {
/// Dart function wrapper for isolate
class IsolateFunction extends JSInvokable implements _IsolateEncodable {
int _isolateId;
dynamic _handle(dynamic);
}
SendPort _port;
JSInvokable _invokable;
IsolateFunction._fromId(this._isolateId, this._port);
IsolateFunction._new(this._invokable) {
_handlers.add(this);
}
IsolateFunction(Function func) : this._new(_DartFunction(func));
class _IsolatePort {
static ReceivePort _invokeHandler;
static Set<_IsolatePortHandler> _handlers = Set();
static Set<IsolateFunction> _handlers = Set();
static get _port {
static get _handlePort {
if (_invokeHandler == null) {
_invokeHandler = ReceivePort();
_invokeHandler.listen((msg) async {
@@ -236,11 +223,11 @@ class _IsolatePort {
return _invokeHandler.sendPort;
}
static _send(SendPort isolate, _IsolatePortHandler handler, msg) async {
if (isolate == null) return handler._handle(msg);
_send(msg) async {
if (_port == null) return _handle(msg);
final evaluatePort = ReceivePort();
isolate.send({
#handler: handler._isolateId,
_port.send({
#handler: _isolateId,
#msg: msg,
#port: evaluatePort.sendPort,
});
@@ -250,33 +237,11 @@ class _IsolatePort {
return _decodeData(result);
}
static _add(_IsolatePortHandler sendport) => _handlers.add(sendport);
static _remove(_IsolatePortHandler sendport) => _handlers.remove(sendport);
}
/// Dart function wrapper for isolate
class IsolateFunction extends JSInvokable
implements _IsolateEncodable, _IsolatePortHandler {
@override
int _isolateId;
SendPort _port;
JSInvokable _invokable;
IsolateFunction._fromId(this._isolateId, this._port);
IsolateFunction._new(this._invokable) {
_IsolatePort._add(this);
}
static IsolateFunction func(Function func) {
return IsolateFunction._new(_DartFunction(func));
}
_destroy() {
_IsolatePort._remove(this);
_handlers.remove(this);
_invokable?.free();
}
@override
_handle(msg) async {
switch (msg) {
case #dup:
@@ -284,7 +249,6 @@ class IsolateFunction extends JSInvokable
return null;
case #free:
_refCount--;
print("${identityHashCode(this)} ref $_refCount");
if (_refCount < 0) _destroy();
return null;
case #destroy:
@@ -300,8 +264,7 @@ class IsolateFunction extends JSInvokable
Future invoke(List positionalArguments, [thisVal]) async {
List dArgs = _encodeData(positionalArguments);
Map dThisVal = _encodeData(thisVal);
return _IsolatePort._send(_port, this, {
#type: #invokeIsolate,
return _send({
#args: dArgs,
#thisVal: dThisVal,
});
@@ -320,7 +283,7 @@ class IsolateFunction extends JSInvokable
Map _encode() {
return {
#jsFunctionId: _isolateId ?? identityHashCode(this),
#jsFunctionPort: _port ?? _IsolatePort._port,
#jsFunctionPort: _port ?? IsolateFunction._handlePort,
};
}
@@ -328,16 +291,16 @@ class IsolateFunction extends JSInvokable
@override
dup() {
_IsolatePort._send(_port, this, #dup);
_send(#dup);
}
@override
free() {
_IsolatePort._send(_port, this, #free);
_send(#free);
}
@override
void destroy() {
_IsolatePort._send(_port, this, #destroy);
_send(#destroy);
}
}

View File

@@ -55,6 +55,8 @@ Pointer _jsGetPropertyValue(
Pointer _dartToJs(Pointer ctx, dynamic val, {Map<dynamic, dynamic> cache}) {
if (val == null) return jsUNDEFINED();
if (val is Error) return _dartToJs(ctx, JSError(val, val.stackTrace));
if (val is Exception) return _dartToJs(ctx, JSError(val));
if (val is JSError) {
final ret = jsNewError(ctx);
_definePropertyValue(ctx, ret, "name", "");
@@ -187,9 +189,11 @@ dynamic _jsToDart(Pointer ctx, Pointer val, {Map<int, dynamic> cache}) {
final jsPromise = _JSObject(ctx, val);
final jsRet = promiseThen._invoke([
(v) {
JSRef.dupRecursive(v);
if (!completer.isCompleted) completer.complete(v);
},
(e) {
JSRef.dupRecursive(e);
if (!completer.isCompleted) completer.completeError(e);
},
], jsPromise);

View File

@@ -1,6 +1,6 @@
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!
version: 0.3.2
version: 0.3.3
homepage: https://github.com/ekibun/flutter_qjs
environment:

View File

@@ -8,7 +8,6 @@
import 'dart:async';
import 'dart:convert';
import 'dart:io';
import 'dart:isolate';
import 'package:flutter_qjs/flutter_qjs.dart';
import 'package:flutter_test/flutter_test.dart';
@@ -19,7 +18,7 @@ dynamic myFunction(String args, {thisVal}) {
Future testEvaluate(qjs) async {
JSInvokable wrapFunction = await qjs.evaluate(
'(a) => a',
'async (a) => a',
name: '<testWrap>',
);
dynamic testWrap = await wrapFunction.invoke([wrapFunction]);
@@ -68,11 +67,6 @@ Future testEvaluate(qjs) async {
}
void main() async {
test('send', () async {
final rec = ReceivePort();
rec.close();
rec.sendPort.send("3232");
});
test('make', () async {
final utf8Encoding = Encoding.getByName('utf-8');
var cmakePath = 'cmake';
@@ -139,25 +133,22 @@ void main() async {
await testEvaluate(qjs);
await qjs.close();
});
test('isolate bind function', () async {
test('isolate bind this', () async {
final qjs = IsolateQjs();
final localVars = [];
JSInvokable testFunc =
await qjs.evaluate('(func)=>func(()=>"ret")', name: '<eval>');
final func = IsolateFunction.func((args) {
localVars.add(args..dup());
JSInvokable localVar;
JSInvokable setToGlobal = await qjs
.evaluate('(name, func)=>{ this[name] = func }', name: '<eval>');
final func = IsolateFunction((args) {
localVar = args..dup();
return args.invoke([]);
});
final testFuncRet = await testFunc.invoke([func..dup()]);
final testFuncRet2 = await testFunc.invoke([func..dup()]);
await setToGlobal.invoke(["test", func..dup()]);
func.free();
testFunc.free();
for (IsolateFunction vars in localVars) {
expect(await vars.invoke([]), 'ret', reason: 'bind function');
vars.free();
}
setToGlobal.free();
final testFuncRet = await qjs.evaluate('test(()=>"ret")', name: '<eval>');
expect(await localVar.invoke([]), 'ret', reason: 'bind function');
localVar.free();
expect(testFuncRet, 'ret', reason: 'bind function args return');
expect(testFuncRet2, testFuncRet, reason: 'bind function args return2');
await qjs.close();
});
test('reference leak', () async {