diff --git a/packages/javascript_darwin/CHANGELOG.md b/packages/javascript_darwin/CHANGELOG.md index 276016d..d43c323 100644 --- a/packages/javascript_darwin/CHANGELOG.md +++ b/packages/javascript_darwin/CHANGELOG.md @@ -1,3 +1,10 @@ +# Unreleased + +- Remove per-runtime callback registrations and cancel timers on disposal. +- Guard asynchronous replies and Promise polling after context disposal. +- Release temporary JavaScriptCore strings and pointer slots in runtime paths. +- Handle null JSON string references without calling JSStringRelease on null. + # 2.0.0 - Update dependency of `javascript_platform_interface` to version `2.0.0` diff --git a/packages/javascript_darwin/lib/src/third_party/flutter_js/lib/extensions/handle_promises.dart b/packages/javascript_darwin/lib/src/third_party/flutter_js/lib/extensions/handle_promises.dart index 1d0927d..36b090f 100644 --- a/packages/javascript_darwin/lib/src/third_party/flutter_js/lib/extensions/handle_promises.dart +++ b/packages/javascript_darwin/lib/src/third_party/flutter_js/lib/extensions/handle_promises.dart @@ -34,7 +34,7 @@ extension HandlePromises on JavascriptRuntime { function FLUTTER_NATIVEJS_IS_REJECTED_PROMISE(idx) { return FLUTTER_NATIVEJS_PENDING_PROMISES[idx].isRejected(); } - + /** * This function allow you to modify a JS Promise by adding some status properties. * Based on: http://stackoverflow.com/questions/21485545/is-there-a-way-to-tell-if-an-es6-promise-is-fulfilled-rejected-resolved @@ -56,12 +56,12 @@ extension HandlePromises on JavascriptRuntime { isFulfilled = true; isPending = false; value = v; - return v; - }, + return v; + }, function(e) { isRejected = true; isPending = false; - value = e; + value = e; } ); @@ -107,6 +107,7 @@ extension HandlePromises on JavascriptRuntime { var completed = false; Function? fnEvaluatePromise; fnEvaluatePromise = () async { + if (isDisposed || completed) return; this.executePendingJob(); if (!completed) { await Future.delayed( @@ -140,6 +141,13 @@ extension HandlePromises on JavascriptRuntime { callFunction(evalRegisterPromise, value.rawResult).stringResult; int idxPromise = int.parse(promiseQuerableIdx); Timer.periodic(Duration(milliseconds: 20), (timer) { + if (isDisposed) { + timer.cancel(); + if (!completer.isCompleted) { + completer.completeError(StateError('JavaScript runtime is disposed')); + } + return; + } // call to _JS_ExecutePendingJob this.executePendingJob(); //eval(REGISTER_PROMISE_FUNCTION); diff --git a/packages/javascript_darwin/lib/src/third_party/flutter_js/lib/javascript_runtime.dart b/packages/javascript_darwin/lib/src/third_party/flutter_js/lib/javascript_runtime.dart index 8a35906..2eb63a7 100644 --- a/packages/javascript_darwin/lib/src/third_party/flutter_js/lib/javascript_runtime.dart +++ b/packages/javascript_darwin/lib/src/third_party/flutter_js/lib/javascript_runtime.dart @@ -79,6 +79,19 @@ abstract class JavascriptRuntime { Map dartContext = {}; + final Set _timers = {}; + + @protected + void releaseDartResources() { + channelFunctionsRegistered.remove(getEngineInstanceId()); + for (final timer in _timers) { + timer.cancel(); + } + _timers.clear(); + localContext.clear(); + dartContext.clear(); + } + void dispose(); static final Map> @@ -133,7 +146,7 @@ abstract class JavascriptRuntime { // console.log(typeof(sendMessage)); // console.log('BLA'); sendMessage('SetTimeout', JSON.stringify({ timeoutIndex, timeout})); - + } catch (e) { console.error('ERROR HERE',e.message); } @@ -146,13 +159,16 @@ abstract class JavascriptRuntime { int duration = args['timeout'] ?? 0; String idx = args['timeoutIndex']; - Timer(Duration(milliseconds: duration), () { + late final Timer timer; + timer = Timer(Duration(milliseconds: duration), () { + _timers.remove(timer); if (isDisposed) return; evaluate(""" __NATIVE_FLUTTER_JS__setTimeoutCallbacks[$idx].call(); delete __NATIVE_FLUTTER_JS__setTimeoutCallbacks[$idx]; """); }); + _timers.add(timer); } on Exception catch (e) { print('Exception no setTimeout: $e'); } on Error catch (e) { diff --git a/packages/javascript_darwin/lib/src/third_party/flutter_js/lib/javascriptcore/jscore/js_object.dart b/packages/javascript_darwin/lib/src/third_party/flutter_js/lib/javascriptcore/jscore/js_object.dart index 15b30c1..2c293fb 100644 --- a/packages/javascript_darwin/lib/src/third_party/flutter_js/lib/javascriptcore/jscore/js_object.dart +++ b/packages/javascript_darwin/lib/src/third_party/flutter_js/lib/javascriptcore/jscore/js_object.dart @@ -548,7 +548,7 @@ class JSObject { context.pointer, arguments.count, arguments.pointer, - (exception ?? JSValuePointer(nullptr)).pointer); + exception?.pointer ?? nullptr); /// Creates a JavaScript Date object, as if by invoking the built-in Date constructor. /// [arguments] A JSValue array of arguments to pass to the Date Constructor. Pass NULL if argumentCount is 0. @@ -561,7 +561,7 @@ class JSObject { context.pointer, arguments.count, arguments.pointer, - (exception ?? JSValuePointer(nullptr)).pointer); + exception?.pointer ?? nullptr); /// Creates a JavaScript Error object, as if by invoking the built-in Error constructor. /// [arguments] (JSValueRef[]) A JSValue array of arguments to pass to the Error Constructor. Pass NULL if argumentCount is 0. @@ -574,7 +574,7 @@ class JSObject { context.pointer, arguments.count, arguments.pointer, - (exception ?? JSValuePointer(nullptr)).pointer); + exception?.pointer ?? nullptr); /// Creates a JavaScript RegExp object, as if by invoking the built-in RegExp constructor. /// [arguments] (JSValueRef[]) A JSValue array of arguments to pass to the RegExp Constructor. Pass NULL if argumentCount is 0. @@ -587,7 +587,7 @@ class JSObject { context.pointer, arguments.count, arguments.pointer, - (exception ?? JSValuePointer(nullptr)).pointer); + exception?.pointer ?? nullptr); /// Creates a JavaScript promise object by invoking the provided executor. /// [resolve] (JSObjectRef*) A pointer to a JSObjectRef in which to store the resolve function for the new promise. Pass NULL if you do not care to store the resolve callback. @@ -602,7 +602,7 @@ class JSObject { context.pointer, resolve.pointer, reject.pointer, - (exception ?? JSValuePointer(nullptr)).pointer); + exception?.pointer ?? nullptr); /// Creates a function with a given script as its body. /// Use this method when you want to execute a script repeatedly, to avoid the cost of re-parsing the script before each execution. @@ -628,7 +628,7 @@ class JSObject { JSString.fromString(body).pointer, JSString.fromString(sourceURL).pointer, startingLineNumber, - (exception ?? JSValuePointer(nullptr)).pointer); + exception?.pointer ?? nullptr); /// Creates a JavaScript Typed Array object with the given number of elements. /// [arrayType] A value [JSTypedArrayType] identifying the type of array to create. If arrayType is kJSTypedArrayTypeNone or kJSTypedArrayTypeArrayBuffer then NULL will be returned. @@ -643,7 +643,7 @@ class JSObject { context.pointer, JSValue.jSTypedArrayTypeToCEnum(arrayType), length, - (exception ?? JSValuePointer(nullptr)).pointer); + exception?.pointer ?? nullptr); /// Creates a JavaScript Typed Array object from an existing pointer. /// If an exception is thrown during this function the bytesDeallocator will always be called. @@ -668,7 +668,7 @@ class JSObject { bytes.length, bytesDeallocator ?? nullptr, deallocatorContext, - (exception ?? JSValuePointer(nullptr)).pointer); + exception?.pointer ?? nullptr); /// Creates a JavaScript Typed Array object from an existing JavaScript Array Buffer object. /// [arrayType] A value [JSTypedArrayType] identifying the type of array to create. If arrayType is kJSTypedArrayTypeNone or kJSTypedArrayTypeArrayBuffer then NULL will be returned. @@ -683,7 +683,7 @@ class JSObject { context.pointer, JSValue.jSTypedArrayTypeToCEnum(arrayType), buffer.pointer, - (exception ?? JSValuePointer(nullptr)).pointer); + exception?.pointer ?? nullptr); /// Creates a JavaScript Typed Array object from an existing JavaScript Array Buffer object with the given offset and length. /// [arrayType] A value [JSTypedArrayType] identifying the type of array to create. If arrayType is kJSTypedArrayTypeNone or kJSTypedArrayTypeArrayBuffer then NULL will be returned. @@ -705,7 +705,7 @@ class JSObject { buffer.pointer, byteOffset, length, - (exception ?? JSValuePointer(nullptr)).pointer); + exception?.pointer ?? nullptr); /// Creates a JavaScript Array Buffer object from an existing pointer. /// If an exception is thrown during this function the bytesDeallocator will always be called. @@ -726,7 +726,7 @@ class JSObject { bytes.length, bytesDeallocator ?? nullptr, deallocatorContext, - (exception ?? JSValuePointer(nullptr)).pointer); + exception?.pointer ?? nullptr); /// Gets an object's prototype. JSValue get prototype { @@ -755,13 +755,13 @@ class JSObject { String propertyName, { JSValuePointer? exception, }) { - return JSValue( - context, - JSObjectRef.jSObjectGetProperty( - context.pointer, - pointer, - JSString.fromString(propertyName).pointer, - (exception ?? JSValuePointer(nullptr)).pointer)); + final name = JSString.fromString(propertyName); + try { + return JSValue(context, JSObjectRef.jSObjectGetProperty( + context.pointer, pointer, name.pointer, exception?.pointer ?? nullptr)); + } finally { + name.release(); + } } /// Sets a property on an object. @@ -781,7 +781,7 @@ class JSObject { JSString.fromString(propertyName).pointer, value.pointer, jSPropertyAttributesToCEnum(attributes), - (exception ?? JSValuePointer(nullptr)).pointer); + exception?.pointer ?? nullptr); } /// Deletes a property from an object. @@ -796,7 +796,7 @@ class JSObject { context.pointer, pointer, JSString.fromString(propertyName).pointer, - (exception ?? JSValuePointer(nullptr)).pointer) == + exception?.pointer ?? nullptr) == 1; } @@ -812,7 +812,7 @@ class JSObject { context.pointer, pointer, JSString.fromString(propertyKey).pointer, - (exception ?? JSValuePointer(nullptr)).pointer) == + exception?.pointer ?? nullptr) == 1; } @@ -830,7 +830,7 @@ class JSObject { context.pointer, pointer, JSString.fromString(propertyKey).pointer, - (exception ?? JSValuePointer(nullptr)).pointer)); + exception?.pointer ?? nullptr)); } /// Sets a property on an object using a JSValueRef as the property key. @@ -851,7 +851,7 @@ class JSObject { JSString.fromString(propertyKey).pointer, value.pointer, jSPropertyAttributesToCEnum(attributes), - (exception ?? JSValuePointer(nullptr)).pointer); + exception?.pointer ?? nullptr); } /// Gets a property from an object by numeric index. @@ -865,7 +865,7 @@ class JSObject { return JSValue( context, JSObjectRef.jSObjectGetPropertyAtIndex(context.pointer, pointer, - propertyIndex, (exception ?? JSValuePointer(nullptr)).pointer)); + propertyIndex, exception?.pointer ?? nullptr)); } /// Sets a property on an object by numeric index. @@ -883,7 +883,7 @@ class JSObject { pointer, propertyIndex, value.pointer, - (exception ?? JSValuePointer(nullptr)).pointer); + exception?.pointer ?? nullptr); } /// Gets an object's private data. @@ -921,7 +921,7 @@ class JSObject { thisObject.pointer, arguments.count, arguments.pointer, - (exception ?? JSValuePointer(nullptr)).pointer)); + exception?.pointer ?? nullptr)); } /// Tests whether an object can be called as a constructor. @@ -944,7 +944,7 @@ class JSObject { pointer, arguments.count, arguments.pointer, - (exception ?? JSValuePointer(nullptr)).pointer)); + exception?.pointer ?? nullptr)); } /// Gets the names of an object's enumerable properties. @@ -970,9 +970,9 @@ class JSObject { }) { return Bytes( JSTypedArray.jSObjectGetTypedArrayBytesPtr(context.pointer, pointer, - (exception ?? JSValuePointer(nullptr)).pointer), + exception?.pointer ?? nullptr), JSTypedArray.jSObjectGetTypedArrayLength(context.pointer, pointer, - (exception ?? JSValuePointer(nullptr)).pointer)); + exception?.pointer ?? nullptr)); } /// Returns the byte length of a JavaScript Typed Array object. @@ -981,7 +981,7 @@ class JSObject { JSValuePointer? exception, }) { return JSTypedArray.jSObjectGetTypedArrayByteLength(context.pointer, - pointer, (exception ?? JSValuePointer(nullptr)).pointer); + pointer, exception?.pointer ?? nullptr); } /// Returns the byte offset of a JavaScript Typed Array object. @@ -990,7 +990,7 @@ class JSObject { JSValuePointer? exception, }) { return JSTypedArray.jSObjectGetTypedArrayByteOffset(context.pointer, - pointer, (exception ?? JSValuePointer(nullptr)).pointer); + pointer, exception?.pointer ?? nullptr); } /// Returns the JavaScript Array Buffer object that is used as the backing of a JavaScript Typed Array object. @@ -1001,7 +1001,7 @@ class JSObject { return JSObject( context, JSTypedArray.jSObjectGetTypedArrayBuffer(context.pointer, pointer, - (exception ?? JSValuePointer(nullptr)).pointer)); + exception?.pointer ?? nullptr)); } /// Returns a pointer to the data buffer that serves as the backing store for a JavaScript Typed Array object. @@ -1012,9 +1012,9 @@ class JSObject { }) { return Bytes( JSTypedArray.jSObjectGetArrayBufferBytesPtr(context.pointer, pointer, - (exception ?? JSValuePointer(nullptr)).pointer), + exception?.pointer ?? nullptr), JSTypedArray.jSObjectGetArrayBufferByteLength(context.pointer, pointer, - (exception ?? JSValuePointer(nullptr)).pointer)); + exception?.pointer ?? nullptr)); } /// JSObject to JSValue diff --git a/packages/javascript_darwin/lib/src/third_party/flutter_js/lib/javascriptcore/jscore/js_value.dart b/packages/javascript_darwin/lib/src/third_party/flutter_js/lib/javascriptcore/jscore/js_value.dart index 28c1fc4..30e05a1 100644 --- a/packages/javascript_darwin/lib/src/third_party/flutter_js/lib/javascriptcore/jscore/js_value.dart +++ b/packages/javascript_darwin/lib/src/third_party/flutter_js/lib/javascriptcore/jscore/js_value.dart @@ -274,7 +274,7 @@ class JSValue { JSValuePointer? exception, }) { int typeCode = JSValueRef.jSValueGetTypedArrayType(context.pointer, pointer, - (exception ?? JSValuePointer(nullptr)).pointer); + exception?.pointer ?? nullptr); return cEnumToJSTypedArrayType(typeCode); } @@ -284,7 +284,7 @@ class JSValue { JSValuePointer? exception, }) { return JSValueRef.jSValueIsEqual(context.pointer, pointer, other.pointer, - (exception ?? JSValuePointer(nullptr)).pointer) == + exception?.pointer ?? nullptr) == 1; } @@ -297,7 +297,7 @@ class JSValue { context.pointer, pointer, constructor.pointer, - (exception ?? JSValuePointer(nullptr)).pointer) == + exception?.pointer ?? nullptr) == 1; } @@ -309,7 +309,7 @@ class JSValue { JSValuePointer? exception, }) { return JSString(JSValueRef.jSValueCreateJSONString(context.pointer, pointer, - indent, (exception ?? JSValuePointer(nullptr)).pointer)); + indent, exception?.pointer ?? nullptr)); } /// Converts a JavaScript value to boolean and returns the resulting boolean. @@ -323,7 +323,7 @@ class JSValue { JSValuePointer? exception, }) { return JSValueRef.jSValueToNumber(context.pointer, pointer, - (exception ?? JSValuePointer(nullptr)).pointer); + exception?.pointer ?? nullptr); } /// Converts a JavaScript value to number and returns the resulting string. @@ -340,7 +340,7 @@ class JSValue { JSValuePointer? exception, }) { return JSString(JSValueRef.jSValueToStringCopy(context.pointer, pointer, - (exception ?? JSValuePointer(nullptr)).pointer)); + exception?.pointer ?? nullptr)); } /// Converts a JavaScript value to object and returns the resulting object. @@ -351,7 +351,7 @@ class JSValue { return JSObject( context, JSValueRef.jSValueToObject(context.pointer, pointer, - (exception ?? JSValuePointer(nullptr)).pointer)); + exception?.pointer ?? nullptr)); } /// Protects a JavaScript value from garbage collection. diff --git a/packages/javascript_darwin/lib/src/third_party/flutter_js/lib/javascriptcore/jscore_runtime.dart b/packages/javascript_darwin/lib/src/third_party/flutter_js/lib/javascriptcore/jscore_runtime.dart index d3d8ac4..b025d4e 100644 --- a/packages/javascript_darwin/lib/src/third_party/flutter_js/lib/javascriptcore/jscore_runtime.dart +++ b/packages/javascript_darwin/lib/src/third_party/flutter_js/lib/javascriptcore/jscore_runtime.dart @@ -36,18 +36,18 @@ class JavascriptCoreRuntime extends JavascriptRuntime { _engineInstanceIdByContext[_globalContext.address] = getEngineInstanceId(); Pointer funcNameCString = 'sendMessage'.toNativeUtf8(); - var functionObject = jSObjectMakeFunctionWithCallback( - _globalContext, - jSStringCreateWithUTF8CString(funcNameCString), - Pointer.fromFunction(sendMessageBridgeFunction)); + final functionName = jSStringCreateWithUTF8CString(funcNameCString); + var functionObject = jSObjectMakeFunctionWithCallback(_globalContext, + functionName, Pointer.fromFunction(sendMessageBridgeFunction)); jSObjectSetProperty( _globalContext, _globalObject, - jSStringCreateWithUTF8CString(funcNameCString), + functionName, functionObject, jsObject.JSPropertyAttributes.kJSPropertyAttributeNone, nullptr); calloc.free(funcNameCString); + jSStringRelease(functionName); init(); } @@ -59,19 +59,19 @@ class JavascriptCoreRuntime extends JavascriptRuntime { @override JsEvalResult evaluate(String js, {String? sourceUrl}) { + _checkAlive(); Pointer scriptCString = js.toNativeUtf8(); Pointer? sourceUrlCString = sourceUrl?.toNativeUtf8(); JSValuePointer exception = JSValuePointer(); + final script = jSStringCreateWithUTF8CString(scriptCString); + final url = sourceUrlCString == null + ? nullptr + : jSStringCreateWithUTF8CString(sourceUrlCString); var jsValueRef = jSEvaluateScript( - _globalContext, - jSStringCreateWithUTF8CString(scriptCString), - nullptr, - sourceUrlCString != null - ? jSStringCreateWithUTF8CString(sourceUrlCString) - : nullptr, - 1, - exception.pointer); + _globalContext, script, nullptr, url, 1, exception.pointer); + jSStringRelease(script); + if (url != nullptr) jSStringRelease(url); calloc.free(scriptCString); if (sourceUrlCString != null) { calloc.free(sourceUrlCString as Pointer); @@ -80,13 +80,14 @@ class JavascriptCoreRuntime extends JavascriptRuntime { String result; JSValue exceptionValue = exception.getValue(context); + malloc.free(exception.pointer); bool isPromise = false; if (exceptionValue.isObject) { result = 'ERROR: ${exceptionValue.toObject().getProperty("message").string} \n at ${exceptionValue.toObject().getProperty("stack").string}'; } else { result = _getJsValue(jsValueRef); - JSValue resultValue = JSValuePointer(jsValueRef).getValue(context); + JSValue resultValue = JSValue(context, jsValueRef); isPromise = resultValue.isObject && resultValue.toObject().getProperty('then').isObject && @@ -103,15 +104,24 @@ class JavascriptCoreRuntime extends JavascriptRuntime { bool _isDisposed = false; + void _checkAlive() { + if (_isDisposed) throw StateError('JavaScript runtime is disposed'); + } + @override bool get isDisposed => _isDisposed; @override void dispose() { + if (_isDisposed) return; _isDisposed = true; + releaseDartResources(); + _sendMessageDartFuncByContext.remove(_globalContext.address); + _engineInstanceIdByContext.remove(_globalContext.address); + // JSContext allocates this output slot; it does not own the JSValueRef. + malloc.free(context.exception.pointer); jSGlobalContextRelease(_globalContext); jSContextGroupRelease(_contextGroup); - _sendMessageDartFuncByContext.remove(_globalContext.address); } @override @@ -120,6 +130,7 @@ class JavascriptCoreRuntime extends JavascriptRuntime { /// Works only for iOS & MacOS. @override void setInspectable(bool inspectable) { + _checkAlive(); if (Platform.isIOS || Platform.isMacOS) { try { context.setInspectable(inspectable); @@ -131,6 +142,7 @@ class JavascriptCoreRuntime extends JavascriptRuntime { @override bool setupBridge(String channelName, Function(dynamic args) fn) { + _checkAlive(); final channelFunctionCallbacks = JavascriptRuntime.channelFunctionsRegistered[getEngineInstanceId()]!; @@ -164,9 +176,11 @@ class JavascriptCoreRuntime extends JavascriptRuntime { } var resultJsString = jSValueToStringCopy(_globalContext, jsValueRef, nullptr); + if (resultJsString == nullptr) return 'null'; var resultCString = jSStringGetCharactersPtr(resultJsString); int resultCStringLength = jSStringGetLength(resultJsString); if (resultCString == nullptr) { + jSStringRelease(resultJsString); return 'null'; } String result = String.fromCharCodes(Uint16List.view( @@ -188,6 +202,7 @@ class JavascriptCoreRuntime extends JavascriptRuntime { int argumentCount, Pointer arguments, Pointer exception) { + if (_isDisposed || argumentCount < 2) return nullptr; final channelFunctions = JavascriptRuntime.channelFunctionsRegistered[getEngineInstanceId()]!; @@ -195,13 +210,18 @@ class JavascriptCoreRuntime extends JavascriptRuntime { String message = _getJsValue(arguments[1]); if (channelFunctions.containsKey(channelName)) { - final result = channelFunctions[channelName]!.call(jsonDecode(message)); try { + final result = channelFunctions[channelName]!.call(jsonDecode(message)); if (result is Future) { return _constructPromiseFor(result); } final encoded = json.encode(result); - return JSValue.makeFromJSONString(context, encoded).pointer; + final string = JSString.fromString(encoded); + try { + return jSValueMakeFromJSONString(context.pointer, string.pointer); + } finally { + string.release(); + } } catch (err) { print( 'Could not encode return value of message on channel $channelName to json... returning null'); @@ -220,37 +240,45 @@ class JavascriptCoreRuntime extends JavascriptRuntime { ' __JSC_promise_result$id.reject = reject;});') .toNativeUtf8(); - var jsValueRef = jSEvaluateScript( - _globalContext, - jSStringCreateWithUTF8CString(scriptCString), - nullptr, - nullptr, - 1, - nullptr); + final script = jSStringCreateWithUTF8CString(scriptCString); + var jsValueRef = + jSEvaluateScript(_globalContext, script, nullptr, nullptr, 1, nullptr); calloc.free(scriptCString); + jSStringRelease(script); future.then((value) { + if (_isDisposed) return; final encoded = json.encode(value); evaluate( '__JSC_promise_result$id.resolve($encoded); __JSC_promise_result$id = null;'); }).catchError((error) { + if (_isDisposed) return; evaluate( - '__JSC_promise_result$id.reject("$error"); __JSC_promise_result$id = null;'); + '__JSC_promise_result$id.reject(${jsonEncode(error.toString())}); __JSC_promise_result$id = null;'); }); return jsValueRef; } @override JsEvalResult callFunction(Pointer? fn, Pointer? obj) { - JSValue fnValue = JSValuePointer(fn).getValue(context); + _checkAlive(); + JSValue fnValue = JSValue(context, fn ?? nullptr); JSObject functionObj = fnValue.toObject(); JSValuePointer exception = JSValuePointer(); - JSValue result = functionObj.callAsFunction( - functionObj, - JSValuePointer(obj), - exception: exception, - ); - JSValue exceptionValue = exception.getValue(context); + final arguments = JSValuePointer(obj); + late final JSValue result; + late final JSValue exceptionValue; + try { + result = functionObj.callAsFunction( + functionObj, + arguments, + exception: exception, + ); + exceptionValue = exception.getValue(context); + } finally { + malloc.free(arguments.pointer); + malloc.free(exception.pointer); + } bool isPromise = false; if (exceptionValue.isObject) { @@ -273,6 +301,7 @@ class JavascriptCoreRuntime extends JavascriptRuntime { @override T? convertValue(JsEvalResult jsValue) { + _checkAlive(); if (jSValueIsNull(_globalContext, jsValue.rawResult) == 1) { return null; } else if (jSValueIsString(_globalContext, jsValue.rawResult) == 1) { @@ -299,8 +328,7 @@ class JavascriptCoreRuntime extends JavascriptRuntime { } } else if (jSValueIsObject(_globalContext, jsValue.rawResult) == 1 || jSValueIsArray(_globalContext, jsValue.rawResult) == 1) { - JSValue objValue = JSValuePointer(jsValue.rawResult).getValue(context); - String serialized = objValue.createJSONString().string!; + String serialized = jsonStringify(jsValue); return jsonDecode(serialized); } else { return null; @@ -309,8 +337,15 @@ class JavascriptCoreRuntime extends JavascriptRuntime { @override String jsonStringify(JsEvalResult jsValue) { - JSValue objValue = JSValuePointer(jsValue.rawResult).getValue(context); - return objValue.createJSONString().string!; + _checkAlive(); + final string = + jSValueCreateJSONString(context.pointer, jsValue.rawResult, 0, nullptr); + if (string == nullptr) return 'null'; + try { + return JSString(string).string!; + } finally { + jSStringRelease(string); + } } @override diff --git a/packages/javascript_darwin/test/runtime_lifecycle_test.dart b/packages/javascript_darwin/test/runtime_lifecycle_test.dart new file mode 100644 index 0000000..640e2a3 --- /dev/null +++ b/packages/javascript_darwin/test/runtime_lifecycle_test.dart @@ -0,0 +1,86 @@ +import 'dart:async'; +import 'dart:io'; + +import 'package:flutter_test/flutter_test.dart'; +import 'package:javascript_darwin/src/third_party/flutter_js/lib/javascript_runtime.dart'; +import 'package:javascript_darwin/src/third_party/flutter_js/lib/extensions/handle_promises.dart'; +import 'package:javascript_darwin/src/third_party/flutter_js/lib/javascriptcore/jscore_runtime.dart'; + +void main() { + test('disposing one runtime does not unregister another runtime', () { + final first = JavascriptCoreRuntime(); + final second = JavascriptCoreRuntime(); + addTearDown(first.dispose); + addTearDown(second.dispose); + second.onMessage('echo', (value) => value); + first.dispose(); + expect(second.evaluate('sendMessage("echo", "42")').stringResult, '42'); + expect(JavascriptRuntime.channelFunctionsRegistered, + contains(second.getEngineInstanceId())); + }, skip: !Platform.isMacOS); + + test('non-JSON JavaScript values do not release a null string', () { + final runtime = JavascriptCoreRuntime(); + addTearDown(runtime.dispose); + for (final script in [ + 'undefined', + '(function() {})', + 'Symbol("x")', + 'var cycle = {}; cycle.self = cycle; cycle', + ]) { + expect(runtime.jsonStringify(runtime.evaluate(script)), 'null'); + } + }, skip: !Platform.isMacOS); + + test('promise polling stops after disposal', () async { + final runtime = JavascriptCoreRuntime()..enableHandlePromises(); + final pending = + runtime.handlePromise(runtime.evaluate('new Promise(() => {})')); + final assertion = expectLater(pending, throwsStateError); + runtime.dispose(); + await assertion; + }, skip: !Platform.isMacOS); + + test('repeated native contexts unregister all channels', () { + final before = JavascriptRuntime.channelFunctionsRegistered.length; + for (var i = 0; i < 100; i++) { + final runtime = JavascriptCoreRuntime(); + expect(runtime.evaluate('1 + 2').stringResult, '3'); + expect(runtime.evaluate('throw new Error("test")').isError, isTrue); + expect(runtime.evaluate('Promise.resolve(42)').isPromise, isTrue); + runtime.evaluate('setTimeout(function() {}, 60000)'); + runtime.dispose(); + runtime.dispose(); + expect(JavascriptRuntime.channelFunctionsRegistered.length, before); + expect(() => runtime.evaluate('1'), throwsStateError); + } + }, skip: !Platform.isMacOS); + + test('late successful and failed channel replies do not enter dead JSC', + () async { + for (final fail in [false, true]) { + final runtime = JavascriptCoreRuntime(); + final reply = Completer(); + runtime.onMessage('late', (_) => reply.future); + expect(runtime.evaluate('sendMessage("late", "null")').isPromise, isTrue); + runtime.dispose(); + if (fail) { + reply.completeError(StateError('late failure')); + } else { + reply.complete('late result'); + } + await Future.delayed(Duration.zero); + } + }, skip: !Platform.isMacOS); + + test('live async replies and JSON round trips still work', () async { + final runtime = JavascriptCoreRuntime(); + addTearDown(runtime.dispose); + runtime.onMessage('echo', (value) async => value); + runtime.evaluate( + 'var answer; sendMessage("echo", "[1,2]").then(x => answer = x)'); + await Future.delayed(Duration.zero); + expect(runtime.jsonStringify(runtime.evaluate('answer')), '[1,2]'); + expect(runtime.convertValue(runtime.evaluate('({a: 1})')), {'a': 1}); + }, skip: !Platform.isMacOS); +}