fix qjs memory leak

This commit is contained in:
ekibun
2020-10-03 23:36:39 +08:00
parent 8a72bac6a9
commit d294dd59bc
13 changed files with 125 additions and 84 deletions

View File

@@ -3,8 +3,12 @@
* @Author: ekibun
* @Date: 2020-08-08 08:16:50
* @LastEditors: ekibun
* @LastEditTime: 2020-10-03 00:28:18
* @LastEditTime: 2020-10-03 23:34:30
-->
## 0.1.2
* fix qjs memory leak.
## 0.1.1
* run on isolate.

View File

@@ -3,7 +3,7 @@
* @Author: ekibun
* @Date: 2020-08-08 08:16:50
* @LastEditors: ekibun
* @LastEditTime: 2020-10-03 00:36:36
* @LastEditTime: 2020-10-03 00:44:41
-->
# flutter_qjs
@@ -67,7 +67,7 @@ channel("http", ["http://example.com/"]);
~~I cannot find a way to convert the sync ffi callback into an async function. So the assets files received by async function `rootBundle.loadString` cannot be used in this version. I will appreciate it if you can provide me a solution to make `ModuleHandler` async.~~
To use async function in module handler, try [Run on isolate thread](#isolate)
To use async function in module handler, try [Run on isolate thread](#Run-on-isolate-thread)
```dart
await engine.setModuleHandler((String module) {
@@ -94,7 +94,7 @@ try {
5. Method `recreate` can destroy quickjs runtime that can be recreated again if you call `evaluate`, `recreat` can be used to reset the module cache. Call `close` to stop `dispatch` when you do not need it.
### <span id="isolate">Run on isolate thread</span>
### Run on isolate thread
1. Create a `IsolateQjs` object, pass a handler to implement js-dart interaction. The handler is used in isolate, so the function must be a top-level function or a static method.

View File

@@ -3,7 +3,7 @@
* @Author: ekibun
* @Date: 2020-09-06 18:32:45
* @LastEditors: ekibun
* @LastEditTime: 2020-09-24 22:32:15
* @LastEditTime: 2020-10-03 23:26:14
*/
#include "ffi.h"
#include <functional>
@@ -226,7 +226,7 @@ extern "C"
}
DLLEXPORT int32_t jsDefinePropertyValue(JSContext *ctx, JSValueConst *this_obj,
JSAtom prop, JSValue *val, int32_t flags)
JSAtom prop, JSValue *val, int32_t flags)
{
return JS_DefinePropertyValue(ctx, *this_obj, prop, *val, flags);
}
@@ -247,7 +247,7 @@ extern "C"
}
DLLEXPORT int32_t jsGetOwnPropertyNames(JSContext *ctx, JSPropertyEnum **ptab,
uint32_t *plen, JSValueConst *obj, int32_t flags)
uint32_t *plen, JSValueConst *obj, int32_t flags)
{
return JS_GetOwnPropertyNames(ctx, ptab, plen, *obj, flags);
}
@@ -259,7 +259,7 @@ extern "C"
DLLEXPORT uint32_t sizeOfJSValue()
{
return sizeof (JSValue);
return sizeof(JSValue);
}
DLLEXPORT void setJSValueList(JSValue *list, uint32_t i, JSValue *val)
@@ -293,4 +293,9 @@ extern "C"
{
return new JSValue(JS_NewPromiseCapability(ctx, resolving_funcs));
}
DLLEXPORT void jsFree(JSContext *ctx, void *ptab)
{
js_free(ctx, ptab);
}
}

View File

@@ -106,4 +106,6 @@ extern "C"
DLLEXPORT int32_t jsExecutePendingJob(JSRuntime *rt);
DLLEXPORT JSValue *jsNewPromiseCapability(JSContext *ctx, JSValue *resolving_funcs);
DLLEXPORT void jsFree(JSContext *ctx, void *ptab);
}

View File

@@ -3,7 +3,7 @@
* @Author: ekibun
* @Date: 2020-08-08 08:16:51
* @LastEditors: ekibun
* @LastEditTime: 2020-10-03 00:38:41
* @LastEditTime: 2020-10-03 21:37:22
*/
import 'package:flutter/material.dart';
import 'dart:typed_data';
@@ -103,8 +103,8 @@ class _TestPageState extends State<TestPage> {
onPressed: () async {
_ensureEngine();
try {
resp = (await engine.evaluate(
_controller.text ?? '', "<eval>"))
resp = (await engine.evaluate(_controller.text ?? '',
name: "<eval>"))
.toString();
} catch (e) {
resp = e.toString();

View File

@@ -7,42 +7,42 @@ packages:
name: async
url: "https://pub.flutter-io.cn"
source: hosted
version: "2.5.0-nullsafety"
version: "2.5.0-nullsafety.1"
boolean_selector:
dependency: transitive
description:
name: boolean_selector
url: "https://pub.flutter-io.cn"
source: hosted
version: "2.1.0-nullsafety"
version: "2.1.0-nullsafety.1"
characters:
dependency: transitive
description:
name: characters
url: "https://pub.flutter-io.cn"
source: hosted
version: "1.1.0-nullsafety.2"
version: "1.1.0-nullsafety.3"
charcode:
dependency: transitive
description:
name: charcode
url: "https://pub.flutter-io.cn"
source: hosted
version: "1.2.0-nullsafety"
version: "1.2.0-nullsafety.1"
clock:
dependency: transitive
description:
name: clock
url: "https://pub.flutter-io.cn"
source: hosted
version: "1.1.0-nullsafety"
version: "1.1.0-nullsafety.1"
collection:
dependency: transitive
description:
name: collection
url: "https://pub.flutter-io.cn"
source: hosted
version: "1.15.0-nullsafety.2"
version: "1.15.0-nullsafety.3"
dio:
dependency: "direct main"
description:
@@ -56,7 +56,7 @@ packages:
name: fake_async
url: "https://pub.flutter-io.cn"
source: hosted
version: "1.1.0-nullsafety"
version: "1.2.0-nullsafety.1"
ffi:
dependency: transitive
description:
@@ -82,7 +82,7 @@ packages:
path: ".."
relative: true
source: path
version: "0.1.1"
version: "0.1.2"
flutter_test:
dependency: "direct dev"
description: flutter
@@ -108,21 +108,21 @@ packages:
name: matcher
url: "https://pub.flutter-io.cn"
source: hosted
version: "0.12.10-nullsafety"
version: "0.12.10-nullsafety.1"
meta:
dependency: transitive
description:
name: meta
url: "https://pub.flutter-io.cn"
source: hosted
version: "1.3.0-nullsafety.2"
version: "1.3.0-nullsafety.3"
path:
dependency: transitive
description:
name: path
url: "https://pub.flutter-io.cn"
source: hosted
version: "1.8.0-nullsafety"
version: "1.8.0-nullsafety.1"
sky_engine:
dependency: transitive
description: flutter
@@ -134,56 +134,56 @@ packages:
name: source_span
url: "https://pub.flutter-io.cn"
source: hosted
version: "1.8.0-nullsafety"
version: "1.8.0-nullsafety.2"
stack_trace:
dependency: transitive
description:
name: stack_trace
url: "https://pub.flutter-io.cn"
source: hosted
version: "1.10.0-nullsafety"
version: "1.10.0-nullsafety.2"
stream_channel:
dependency: transitive
description:
name: stream_channel
url: "https://pub.flutter-io.cn"
source: hosted
version: "2.1.0-nullsafety"
version: "2.1.0-nullsafety.1"
string_scanner:
dependency: transitive
description:
name: string_scanner
url: "https://pub.flutter-io.cn"
source: hosted
version: "1.1.0-nullsafety"
version: "1.1.0-nullsafety.1"
term_glyph:
dependency: transitive
description:
name: term_glyph
url: "https://pub.flutter-io.cn"
source: hosted
version: "1.2.0-nullsafety"
version: "1.2.0-nullsafety.1"
test_api:
dependency: transitive
description:
name: test_api
url: "https://pub.flutter-io.cn"
source: hosted
version: "0.2.19-nullsafety"
version: "0.2.19-nullsafety.2"
typed_data:
dependency: transitive
description:
name: typed_data
url: "https://pub.flutter-io.cn"
source: hosted
version: "1.3.0-nullsafety.2"
version: "1.3.0-nullsafety.3"
vector_math:
dependency: transitive
description:
name: vector_math
url: "https://pub.flutter-io.cn"
source: hosted
version: "2.1.0-nullsafety.2"
version: "2.1.0-nullsafety.3"
sdks:
dart: ">=2.10.0-0.0.dev <2.10.0"
dart: ">=2.10.0-110 <=2.11.0-181.0.dev"
flutter: ">=1.20.0 <2.0.0"

View File

@@ -3,7 +3,7 @@
* @Author: ekibun
* @Date: 2020-09-19 10:29:04
* @LastEditors: ekibun
* @LastEditTime: 2020-09-27 01:12:16
* @LastEditTime: 2020-10-03 23:27:15
*/
import 'dart:ffi';
import 'dart:io';
@@ -744,3 +744,16 @@ final Pointer Function(
Pointer,
)>>("jsNewPromiseCapability")
.asFunction();
/// void jsFree(JSContext *ctx, void *ptab)
final void Function(
Pointer ctx,
Pointer ptab,
) jsFree = qjsLib
.lookup<
NativeFunction<
Void Function(
Pointer,
Pointer,
)>>("jsFree")
.asFunction();

View File

@@ -3,7 +3,7 @@
* @Author: ekibun
* @Date: 2020-08-08 08:29:09
* @LastEditors: ekibun
* @LastEditTime: 2020-10-03 00:18:49
* @LastEditTime: 2020-10-03 21:55:07
*/
import 'dart:async';
import 'dart:ffi';
@@ -116,9 +116,10 @@ class FlutterQjs {
}
/// Evaluate js script.
Future<dynamic> evaluate(String command, String name) async {
Future<dynamic> evaluate(String command, {String name, int evalFlags}) async {
_ensureEngine();
var jsval = jsEval(_ctx, command, name, JSEvalType.GLOBAL);
var jsval =
jsEval(_ctx, command, name ?? "<eval>", evalFlags ?? JSEvalType.GLOBAL);
if (jsIsException(jsval) != 0) {
throw Exception(parseJSException(_ctx));
}

View File

@@ -3,7 +3,7 @@
* @Author: ekibun
* @Date: 2020-10-02 13:49:03
* @LastEditors: ekibun
* @LastEditTime: 2020-10-03 00:18:40
* @LastEditTime: 2020-10-03 22:21:31
*/
import 'dart:async';
import 'dart:ffi';
@@ -123,18 +123,21 @@ void _runJsIsolate(Map spawnMessage) async {
sendPort.send(port.sendPort);
qjs.setMethodHandler(methodHandler);
qjs.setModuleHandler((name) {
var ptr = allocate<Int64>();
var ptr = allocate<Pointer<Utf8>>();
ptr.value = Pointer.fromAddress(0);
sendPort.send({
'type': 'module',
'name': name,
'ptr': ptr.address,
});
ptr.value = 0;
while (ptr.value == 0) sleep(Duration.zero);
print(ptr.value);
if (ptr.value == -1) throw Exception("Module Not found");
var strptr = Pointer<Utf8>.fromAddress(ptr.value);
var ret = Utf8.fromUtf8(strptr);
while (ptr.value.address == 0) sleep(Duration.zero);
if (ptr.value.address == -1) throw Exception("Module Not found");
var ret = Utf8.fromUtf8(ptr.value);
sendPort.send({
'type': 'release',
'ptr': ptr.value.address,
});
free(ptr);
return ret;
});
qjs.dispatch();
@@ -144,7 +147,11 @@ void _runJsIsolate(Map spawnMessage) async {
try {
switch (msg['type']) {
case 'evaluate':
data = await qjs.evaluate(msg['command'], msg['name']);
data = await qjs.evaluate(
msg['command'],
name: msg['name'],
evalFlags: msg['flag'],
);
break;
case 'call':
data = JSFunction.fromAddress(
@@ -174,7 +181,7 @@ typedef JsAsyncModuleHandler = Future<String> Function(String name);
typedef JsIsolateSpawn = void Function(SendPort sendPort);
class IsolateQjs {
SendPort _sendPort;
Future<SendPort> _sendPort;
JsMethodHandler _methodHandler;
JsAsyncModuleHandler _moduleHandler;
@@ -182,7 +189,7 @@ class IsolateQjs {
/// The function must be a top-level function or a static method
IsolateQjs(this._methodHandler);
Future<void> _ensureEngine() async {
_ensureEngine() {
if (_sendPort != null) return;
ReceivePort port = ReceivePort();
Isolate.spawn(
@@ -193,28 +200,30 @@ class IsolateQjs {
},
errorsAreFatal: true,
);
var completer = Completer();
var completer = Completer<SendPort>();
port.listen((msg) async {
if (msg is SendPort && !completer.isCompleted) {
_sendPort = msg;
completer.complete();
completer.complete(msg);
return;
}
switch (msg['type']) {
case 'module':
var ptr = Pointer<Int64>.fromAddress(msg['ptr']);
var ptr = Pointer<Pointer>.fromAddress(msg['ptr']);
try {
ptr.value = Utf8.toUtf8(await _moduleHandler(msg['name'])).address;
ptr.value = Utf8.toUtf8(await _moduleHandler(msg['name']));
} catch (e) {
ptr.value = -1;
ptr.value = Pointer.fromAddress(-1);
}
break;
case 'release':
free(Pointer.fromAddress(msg['ptr']));
break;
}
}, onDone: () {
close();
if (!completer.isCompleted) completer.completeError('isolate close');
});
await completer.future;
_sendPort = completer.future;
}
/// Set a handler to manage js module.
@@ -223,24 +232,29 @@ class IsolateQjs {
}
close() {
_sendPort.send({
'type': 'close',
if (_sendPort == null) return;
_sendPort.then((sendPort) {
sendPort.send({
'type': 'close',
});
});
_sendPort = null;
}
Future<dynamic> evaluate(String command, String name) async {
await _ensureEngine();
Future<dynamic> evaluate(String command, {String name, int evalFlags}) async {
_ensureEngine();
var evaluatePort = ReceivePort();
_sendPort.send({
var sendPort = await _sendPort;
sendPort.send({
'type': 'evaluate',
'command': command,
'name': name,
'flag': evalFlags,
'port': evaluatePort.sendPort,
});
var result = await evaluatePort.first;
if (result['data'] != null)
return _decodeData(result['data'], _sendPort);
return _decodeData(result['data'], sendPort);
else
throw result['error'];
}

View File

@@ -3,7 +3,7 @@
* @Author: ekibun
* @Date: 2020-09-19 22:07:47
* @LastEditors: ekibun
* @LastEditTime: 2020-10-02 16:37:16
* @LastEditTime: 2020-10-03 23:27:36
*/
import 'dart:async';
import 'dart:ffi';
@@ -84,11 +84,12 @@ class JSFunction extends JSRefValue {
jsFreeValue(ctx, jsArg);
}
bool isException = jsIsException(jsRet) != 0;
var ret = jsToDart(ctx, jsRet);
jsFreeValue(ctx, jsRet);
if (isException) {
jsFreeValue(ctx, jsRet);
throw Exception(parseJSException(ctx));
}
var ret = jsToDart(ctx, jsRet);
jsFreeValue(ctx, jsRet);
return ret;
}
@@ -257,6 +258,7 @@ dynamic jsToDart(Pointer ctx, Pointer val, {Map<int, dynamic> cache}) {
jsFreeValue(ctx, jsProp);
jsFreeAtom(ctx, jsAtom);
}
jsFree(ctx, ptab.value);
free(ptab);
return ret;
}

View File

@@ -7,49 +7,49 @@ packages:
name: async
url: "https://pub.flutter-io.cn"
source: hosted
version: "2.5.0-nullsafety"
version: "2.5.0-nullsafety.1"
boolean_selector:
dependency: transitive
description:
name: boolean_selector
url: "https://pub.flutter-io.cn"
source: hosted
version: "2.1.0-nullsafety"
version: "2.1.0-nullsafety.1"
characters:
dependency: transitive
description:
name: characters
url: "https://pub.flutter-io.cn"
source: hosted
version: "1.1.0-nullsafety.2"
version: "1.1.0-nullsafety.3"
charcode:
dependency: transitive
description:
name: charcode
url: "https://pub.flutter-io.cn"
source: hosted
version: "1.2.0-nullsafety"
version: "1.2.0-nullsafety.1"
clock:
dependency: transitive
description:
name: clock
url: "https://pub.flutter-io.cn"
source: hosted
version: "1.1.0-nullsafety"
version: "1.1.0-nullsafety.1"
collection:
dependency: transitive
description:
name: collection
url: "https://pub.flutter-io.cn"
source: hosted
version: "1.15.0-nullsafety.2"
version: "1.15.0-nullsafety.3"
fake_async:
dependency: transitive
description:
name: fake_async
url: "https://pub.flutter-io.cn"
source: hosted
version: "1.1.0-nullsafety"
version: "1.2.0-nullsafety.1"
ffi:
dependency: "direct main"
description:
@@ -73,21 +73,21 @@ packages:
name: matcher
url: "https://pub.flutter-io.cn"
source: hosted
version: "0.12.10-nullsafety"
version: "0.12.10-nullsafety.1"
meta:
dependency: transitive
description:
name: meta
url: "https://pub.flutter-io.cn"
source: hosted
version: "1.3.0-nullsafety.2"
version: "1.3.0-nullsafety.3"
path:
dependency: transitive
description:
name: path
url: "https://pub.flutter-io.cn"
source: hosted
version: "1.8.0-nullsafety"
version: "1.8.0-nullsafety.1"
sky_engine:
dependency: transitive
description: flutter
@@ -99,56 +99,56 @@ packages:
name: source_span
url: "https://pub.flutter-io.cn"
source: hosted
version: "1.8.0-nullsafety"
version: "1.8.0-nullsafety.2"
stack_trace:
dependency: transitive
description:
name: stack_trace
url: "https://pub.flutter-io.cn"
source: hosted
version: "1.10.0-nullsafety"
version: "1.10.0-nullsafety.2"
stream_channel:
dependency: transitive
description:
name: stream_channel
url: "https://pub.flutter-io.cn"
source: hosted
version: "2.1.0-nullsafety"
version: "2.1.0-nullsafety.1"
string_scanner:
dependency: transitive
description:
name: string_scanner
url: "https://pub.flutter-io.cn"
source: hosted
version: "1.1.0-nullsafety"
version: "1.1.0-nullsafety.1"
term_glyph:
dependency: transitive
description:
name: term_glyph
url: "https://pub.flutter-io.cn"
source: hosted
version: "1.2.0-nullsafety"
version: "1.2.0-nullsafety.1"
test_api:
dependency: transitive
description:
name: test_api
url: "https://pub.flutter-io.cn"
source: hosted
version: "0.2.19-nullsafety"
version: "0.2.19-nullsafety.2"
typed_data:
dependency: transitive
description:
name: typed_data
url: "https://pub.flutter-io.cn"
source: hosted
version: "1.3.0-nullsafety.2"
version: "1.3.0-nullsafety.3"
vector_math:
dependency: transitive
description:
name: vector_math
url: "https://pub.flutter-io.cn"
source: hosted
version: "2.1.0-nullsafety.2"
version: "2.1.0-nullsafety.3"
sdks:
dart: ">=2.10.0-0.0.dev <2.10.0"
dart: ">=2.10.0-110 <=2.11.0-181.0.dev"
flutter: ">=1.20.0 <2.0.0"

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.1.1
version: 0.1.2
homepage: https://github.com/ekibun/flutter_qjs
environment:

View File

@@ -3,7 +3,7 @@
* @Author: ekibun
* @Date: 2020-09-06 13:02:46
* @LastEditors: ekibun
* @LastEditTime: 2020-10-02 17:27:52
* @LastEditTime: 2020-10-03 21:36:06
*/
import 'dart:convert';
import 'dart:io';
@@ -67,7 +67,7 @@ void main() async {
(...args)=>`hello \${args}!`, a,
0.1, true, false, 1, "world", module
]));
""", "<eval>");
""", name: "<eval>");
print(value);
print(await value[0]('world'));
qjs.close();