diff --git a/CHANGELOG.md b/CHANGELOG.md index e707ff2..1422f95 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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. diff --git a/README.md b/README.md index cc00f95..afa9418 100644 --- a/README.md +++ b/README.md @@ -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. -### Run on isolate thread +### 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. diff --git a/cxx/ffi.cpp b/cxx/ffi.cpp index b766e1d..af3bf6c 100644 --- a/cxx/ffi.cpp +++ b/cxx/ffi.cpp @@ -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 @@ -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); + } } \ No newline at end of file diff --git a/cxx/ffi.h b/cxx/ffi.h index 4fdeff8..7f2b299 100644 --- a/cxx/ffi.h +++ b/cxx/ffi.h @@ -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); } \ No newline at end of file diff --git a/example/lib/main.dart b/example/lib/main.dart index 0228080..7d89ef0 100644 --- a/example/lib/main.dart +++ b/example/lib/main.dart @@ -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 { onPressed: () async { _ensureEngine(); try { - resp = (await engine.evaluate( - _controller.text ?? '', "")) + resp = (await engine.evaluate(_controller.text ?? '', + name: "")) .toString(); } catch (e) { resp = e.toString(); diff --git a/example/pubspec.lock b/example/pubspec.lock index b017748..312ef0c 100644 --- a/example/pubspec.lock +++ b/example/pubspec.lock @@ -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" diff --git a/lib/ffi.dart b/lib/ffi.dart index 7b15966..89677fb 100644 --- a/lib/ffi.dart +++ b/lib/ffi.dart @@ -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(); diff --git a/lib/flutter_qjs.dart b/lib/flutter_qjs.dart index 4af1c82..ddc0ac8 100644 --- a/lib/flutter_qjs.dart +++ b/lib/flutter_qjs.dart @@ -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 evaluate(String command, String name) async { + Future evaluate(String command, {String name, int evalFlags}) async { _ensureEngine(); - var jsval = jsEval(_ctx, command, name, JSEvalType.GLOBAL); + var jsval = + jsEval(_ctx, command, name ?? "", evalFlags ?? JSEvalType.GLOBAL); if (jsIsException(jsval) != 0) { throw Exception(parseJSException(_ctx)); } diff --git a/lib/isolate.dart b/lib/isolate.dart index 38cc760..3b7459d 100644 --- a/lib/isolate.dart +++ b/lib/isolate.dart @@ -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(); + var ptr = allocate>(); + 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.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 Function(String name); typedef JsIsolateSpawn = void Function(SendPort sendPort); class IsolateQjs { - SendPort _sendPort; + Future _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 _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(); 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.fromAddress(msg['ptr']); + var ptr = 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 evaluate(String command, String name) async { - await _ensureEngine(); + Future 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']; } diff --git a/lib/wrapper.dart b/lib/wrapper.dart index 1a98f8d..923b1d1 100644 --- a/lib/wrapper.dart +++ b/lib/wrapper.dart @@ -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 cache}) { jsFreeValue(ctx, jsProp); jsFreeAtom(ctx, jsAtom); } + jsFree(ctx, ptab.value); free(ptab); return ret; } diff --git a/pubspec.lock b/pubspec.lock index b68689c..65c7e4f 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -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" diff --git a/pubspec.yaml b/pubspec.yaml index be064db..0537289 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -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: diff --git a/test/flutter_qjs_test.dart b/test/flutter_qjs_test.dart index ebeb76f..ef72181 100644 --- a/test/flutter_qjs_test.dart +++ b/test/flutter_qjs_test.dart @@ -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 ])); - """, ""); + """, name: ""); print(value); print(await value[0]('world')); qjs.close();