diff --git a/CHANGELOG.md b/CHANGELOG.md index cf714c4..142246e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,11 @@ * @LastEditTime: 2020-12-02 11:36:40 --> +## 0.2.4 + +* wrap dart object to js. +* fix stack overflow when use jsCall nesting. + ## 0.2.3 * fix compiler error in windows release. diff --git a/README.md b/README.md index ca015a1..b238633 100644 --- a/README.md +++ b/README.md @@ -53,8 +53,9 @@ Data conversion between dart and js are implemented as follow: | Map | Object | | JSFunction(...args)
IsolateJSFunction(...args) | function(....args) | | Future | Promise | +| Object | DartObject | -**notice:** `function` can only be sent from js to dart. +**notice:** `function` can only be sent from js to dart. `DartObject` can only be used in `moduleHandler`. ### Invoke dart function diff --git a/cxx/ffi.cpp b/cxx/ffi.cpp index 1d1ff7e..c9843de 100644 --- a/cxx/ffi.cpp +++ b/cxx/ffi.cpp @@ -105,8 +105,11 @@ extern "C" DLLEXPORT JSValue *jsEval(JSContext *ctx, const char *input, size_t input_len, const char *filename, int32_t eval_flags) { - JS_ResetStackTop(JS_GetRuntime(ctx)); - return new JSValue(JS_Eval(ctx, input, input_len, filename, eval_flags)); + JSRuntime *rt = JS_GetRuntime(ctx); + uint8_t *stack_top = JS_SetStackTop(rt, 0); + JSValue *ret = new JSValue(JS_Eval(ctx, input, input_len, filename, eval_flags)); + JS_SetStackTop(rt, stack_top); + return ret; } DLLEXPORT int32_t jsValueGetTag(JSValue *val) @@ -283,8 +286,11 @@ extern "C" DLLEXPORT JSValue *jsCall(JSContext *ctx, JSValueConst *func_obj, JSValueConst *this_obj, int32_t argc, JSValueConst *argv) { - JS_ResetStackTop(JS_GetRuntime(ctx)); - return new JSValue(JS_Call(ctx, *func_obj, *this_obj, argc, argv)); + JSRuntime *rt = JS_GetRuntime(ctx); + uint8_t *stack_top = JS_SetStackTop(rt, 0); + JSValue *ret = new JSValue(JS_Call(ctx, *func_obj, *this_obj, argc, argv)); + JS_SetStackTop(rt, stack_top); + return ret; } DLLEXPORT int32_t jsIsException(JSValueConst *val) @@ -299,9 +305,11 @@ extern "C" DLLEXPORT int32_t jsExecutePendingJob(JSRuntime *rt) { - JS_ResetStackTop(rt); + uint8_t *stack_top = JS_SetStackTop(rt, 0); JSContext *ctx; - return JS_ExecutePendingJob(rt, &ctx); + int ret = JS_ExecutePendingJob(rt, &ctx); + JS_SetStackTop(rt, stack_top); + return ret; } DLLEXPORT JSValue *jsNewPromiseCapability(JSContext *ctx, JSValue *resolving_funcs) diff --git a/cxx/quickjs b/cxx/quickjs index 16e640b..9ac8741 160000 --- a/cxx/quickjs +++ b/cxx/quickjs @@ -1 +1 @@ -Subproject commit 16e640b3dcfe8a4fb1845b9e961b8e42bf56af81 +Subproject commit 9ac874168b360f760459d3af51d33946279cce9b diff --git a/example/pubspec.lock b/example/pubspec.lock index fd9c5e9..6752578 100644 --- a/example/pubspec.lock +++ b/example/pubspec.lock @@ -82,7 +82,7 @@ packages: path: ".." relative: true source: path - version: "0.2.3" + version: "0.2.4" flutter_test: dependency: "direct dev" description: flutter diff --git a/lib/ffi.dart b/lib/ffi.dart index 25c602f..bedb673 100644 --- a/lib/ffi.dart +++ b/lib/ffi.dart @@ -113,6 +113,7 @@ class RuntimeOpaque { List ref = []; ReceivePort port; Future Function(Pointer) promiseToFuture; + Pointer Function(Object) objectWrapper; } final Map runtimeOpaques = Map(); diff --git a/lib/wrapper.dart b/lib/wrapper.dart index d9b8ff8..ba8e054 100644 --- a/lib/wrapper.dart +++ b/lib/wrapper.dart @@ -37,6 +37,26 @@ class JSRefValue implements JSRef { } } +class DartObject implements JSRef { + Object obj; + Pointer ctx; + DartObject(this.ctx, this.obj) { + runtimeOpaques[jsGetRuntime(ctx)]?.ref?.add(this); + } + + static DartObject fromAddress(Pointer ctx, int val) { + return runtimeOpaques[jsGetRuntime(ctx)]?.ref?.firstWhere( + (e) => identityHashCode(e) == val, + orElse: null, + ); + } + + @override + void release() { + ctx = null; + } +} + class JSPromise extends JSRefValue { Completer completer; JSPromise(Pointer ctx, Pointer val, this.completer) : super(ctx, val); @@ -123,6 +143,7 @@ String parseJSException(Pointer ctx, {Pointer e}) { } Pointer dartToJs(Pointer ctx, dynamic val, {Map cache}) { + if (val == null) return jsUNDEFINED(); if (val is Future) { var resolvingFunc = allocate(count: sizeOfJSValue * 2); var resolvingFunc2 = @@ -195,7 +216,7 @@ Pointer dartToJs(Pointer ctx, dynamic val, {Map cache}) { } return ret; } - return jsUNDEFINED(); + return runtimeOpaques[jsGetRuntime(ctx)]?.objectWrapper(val) ?? jsUNDEFINED(); } dynamic jsToDart(Pointer ctx, Pointer val, {Map cache}) { @@ -262,6 +283,10 @@ dynamic jsToDart(Pointer ctx, Pointer val, {Map cache}) { } jsFree(ctx, ptab.value); free(ptab); + final objHash = ret["__dart_obj_hash__"]; + if (objHash is int) { + return DartObject.fromAddress(ctx, objHash)?.obj ?? ret; + } return ret; } break; @@ -272,6 +297,9 @@ dynamic jsToDart(Pointer ctx, Pointer val, {Map cache}) { Pointer jsNewContextWithPromsieWrapper(Pointer rt) { var ctx = jsNewContext(rt); + final runtimeOpaque = runtimeOpaques[rt]; + if (runtimeOpaque == null) throw Exception("Runtime has been released!"); + var jsPromiseWrapper = jsEval( ctx, """ @@ -290,17 +318,42 @@ Pointer jsNewContextWithPromsieWrapper(Pointer rt) { """, "", JSEvalFlag.GLOBAL); - var promiseWrapper = JSRefValue(ctx, jsPromiseWrapper); + var jsObjectWrapper = jsEval( + ctx, + """ + (objHash, type) => { + const ret = { + "__dart_obj_hash__": objHash, + }; + ret.__proto__.toString = ()=> "" + type; + return ret; + }; + """, + "", + JSEvalFlag.GLOBAL); + final promiseWrapper = JSRefValue(ctx, jsPromiseWrapper); jsFreeValue(ctx, jsPromiseWrapper); - runtimeOpaques[rt].promiseToFuture = (promise) { + final objectWrapper = JSRefValue(ctx, jsObjectWrapper); + jsFreeValue(ctx, jsObjectWrapper); + runtimeOpaque.promiseToFuture = (promise) { var completer = Completer(); var wrapper = promiseWrapper.val; if (wrapper == null) completer.completeError(Exception("Runtime has been released!")); var jsPromise = jsCall(ctx, wrapper, null, [promise]); - runtimeOpaques[rt].ref.add(JSPromise(ctx, jsPromise, completer)); + var wrapPromise = JSPromise(ctx, jsPromise, completer); jsFreeValue(ctx, jsPromise); - return completer.future; + return wrapPromise.completer.future; + }; + runtimeOpaque.objectWrapper = (obj) { + var jsObjHash = jsNewInt64(ctx, identityHashCode(DartObject(ctx, obj))); + var jsTypeString = jsNewString(ctx, obj.toString()); + var wrapper = objectWrapper.val; + if (wrapper == null) throw Exception("Runtime has been released!"); + var ret = jsCall(ctx, wrapper, null, [jsObjHash, jsTypeString]); + jsFreeValue(ctx, jsObjHash); + jsFreeValue(ctx, jsTypeString); + return ret; }; return ctx; diff --git a/pubspec.yaml b/pubspec.yaml index c5687ac..cc92630 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.2.3 +version: 0.2.4 homepage: https://github.com/ekibun/flutter_qjs environment: diff --git a/test/flutter_qjs_test.dart b/test/flutter_qjs_test.dart index 6ba709b..dbb6292 100644 --- a/test/flutter_qjs_test.dart +++ b/test/flutter_qjs_test.dart @@ -98,19 +98,15 @@ void main() async { qjs.close(); }); test('jsToDart', () async { - await runZonedGuarded(() async { - final qjs = FlutterQjs( - methodHandler: myMethodHandler, - moduleHandler: (name) { - return "export default '${new DateTime.now()}'"; - }, - ); - qjs.dispatch(); - await testEvaluate(qjs); - qjs.close(); - }, (e, stack) { - if (e is TestFailure) throw e; - }); + final qjs = FlutterQjs( + methodHandler: myMethodHandler, + moduleHandler: (name) { + return "export default '${new DateTime.now()}'"; + }, + ); + qjs.dispatch(); + await testEvaluate(qjs); + qjs.close(); }); test('isolate', () async { await runZonedGuarded(() async { @@ -123,9 +119,20 @@ void main() async { await testEvaluate(qjs); qjs.close(); }, (e, stack) { - if (e is TestFailure) throw e; + if (!e.toString().startsWith("test Promise.reject")) throw e; }); }); + test('dart object', () async { + final qjs = FlutterQjs( + methodHandler: (method, args) { + return FlutterQjs(); + }, + ); + qjs.dispatch(); + var value = await qjs.evaluate("channel('channel', [])", name: ""); + expect(value, isInstanceOf(), reason: "dart object"); + qjs.close(); + }); test('stack overflow', () async { final qjs = FlutterQjs(); try {