2
0
Эх сурвалжийг харах

Add task support to DefaultObjectConverter (#1787)

Tom Bruyneel 1 жил өмнө
parent
commit
3fbbff729c

+ 54 - 6
Jint.Tests/Runtime/AsyncTests.cs

@@ -1,3 +1,5 @@
+using Jint.Tests.Runtime.TestClasses;
+
 namespace Jint.Tests.Runtime;
 
 public class AsyncTests
@@ -28,6 +30,26 @@ public class AsyncTests
         }
     }
 
+    [Fact]
+    public void ShouldReturnedTaskConvertedToPromiseInJS()
+    {
+        Engine engine = new();
+        engine.SetValue("asyncTestClass", new AsyncTestClass());
+        var result = engine.Evaluate("asyncTestClass.ReturnDelayedTaskAsync().then(x=>x)");
+        result = result.UnwrapIfPromise();
+        Assert.Equal(AsyncTestClass.TestString, result);
+    }
+
+    [Fact]
+    public void ShouldReturnedCompletedTaskConvertedToPromiseInJS()
+    {
+        Engine engine = new();
+        engine.SetValue("asyncTestClass", new AsyncTestClass());
+        var result = engine.Evaluate("asyncTestClass.ReturnCompletedTask().then(x=>x)");
+        result = result.UnwrapIfPromise();
+        Assert.Equal(AsyncTestClass.TestString, result);
+    }
+
     [Fact]
     public void ShouldTaskCatchWhenCancelled()
     {
@@ -45,6 +67,19 @@ public class AsyncTests
         }
     }
 
+    [Fact]
+    public void ShouldReturnedTaskCatchWhenCancelled()
+    {
+        Engine engine = new();
+        CancellationTokenSource cancel = new();
+        cancel.Cancel();
+        engine.SetValue("token", cancel.Token);
+        engine.SetValue("asyncTestClass", new AsyncTestClass());
+        engine.SetValue("assert", new Action<bool>(Assert.True));
+        var result = engine.Evaluate("asyncTestClass.ReturnCancelledTask(token).then(_ => assert(false)).catch(_ => assert(true))");
+        result = result.UnwrapIfPromise();
+    }
+
     [Fact]
     public void ShouldTaskCatchWhenThrowError()
     {
@@ -60,23 +95,36 @@ public class AsyncTests
         }
     }
 
+    [Fact]
+    public void ShouldReturnedTaskCatchWhenThrowError()
+    {
+        Engine engine = new();
+        engine.SetValue("asyncTestClass", new AsyncTestClass());
+        engine.SetValue("assert", new Action<bool>(Assert.True));
+        var result = engine.Evaluate("asyncTestClass.ThrowAfterDelayAsync().then(_ => assert(false)).catch(_ => assert(true))");
+        result = result.UnwrapIfPromise();
+    }
+
     [Fact]
     public void ShouldTaskAwaitCurrentStack()
     {
         //https://github.com/sebastienros/jint/issues/514#issuecomment-1507127509
         Engine engine = new();
-        string log = "";
+        AsyncTestClass asyncTestClass = new();
+
         engine.SetValue("myAsyncMethod", new Func<Task>(async () =>
         {
             await Task.Delay(1000);
-            log += "1";
+            asyncTestClass.StringToAppend += "1";
         }));
-        engine.SetValue("myAsyncMethod2", new Action(() =>
+        engine.SetValue("mySyncMethod2", new Action(() =>
         {
-            log += "2";
+            asyncTestClass.StringToAppend += "2";
         }));
-        engine.Execute("async function hello() {await myAsyncMethod();myAsyncMethod2();} hello();");
-        Assert.Equal("12", log);
+        engine.SetValue("asyncTestClass", asyncTestClass);
+
+        engine.Execute("async function hello() {await myAsyncMethod();mySyncMethod2();await asyncTestClass.AddToStringDelayedAsync(\"3\")} hello();");
+        Assert.Equal("123", asyncTestClass.StringToAppend);
     }
 
 #if NETFRAMEWORK == false

+ 40 - 0
Jint.Tests/Runtime/TestClasses/AsyncTestClass.cs

@@ -0,0 +1,40 @@
+namespace Jint.Tests.Runtime.TestClasses
+{
+    internal class AsyncTestClass
+    {
+        public static readonly string TestString = "Hello World";
+
+        public string StringToAppend { get; set; } = string.Empty;
+
+        public async Task AddToStringDelayedAsync(string appendWith)
+        {
+            await Task.Delay(1000).ConfigureAwait(false);
+
+            StringToAppend += appendWith;
+        }
+
+        public async Task<string> ReturnDelayedTaskAsync()
+        {
+            await Task.Delay(1000).ConfigureAwait(false);
+
+            return TestString;
+        }
+
+        public Task<string> ReturnCompletedTask()
+        {
+            return Task.FromResult(TestString);
+        }
+
+        public Task<string> ReturnCancelledTask(CancellationToken token)
+        {
+            return Task.FromCanceled<string>(token);
+        }
+
+        public async Task<string> ThrowAfterDelayAsync()
+        {
+            await Task.Delay(100).ConfigureAwait(false);
+
+            throw new Exception("Task threw exception");
+        }
+    }
+}

+ 71 - 0
Jint/Native/JsValue.cs

@@ -119,6 +119,77 @@ namespace Jint.Native
             return true;
         }
 
+        internal static JsValue ConvertAwaitableToPromise(Engine engine, object obj)
+        {
+            if (obj is Task task)
+            {
+                return ConvertTaskToPromise(engine, task);
+            }
+
+#if NETSTANDARD2_1_OR_GREATER || NETCOREAPP
+            if (obj is ValueTask valueTask)
+            {
+                return ConvertTaskToPromise(engine, valueTask.AsTask());
+            }
+
+            // ValueTask<T>
+            var asTask = obj.GetType().GetMethod(nameof(ValueTask<object>.AsTask));
+            if (asTask is not null)
+            {
+                return ConvertTaskToPromise(engine, (Task) asTask.Invoke(obj, parameters: null)!);
+            }
+#endif
+
+            return FromObject(engine, JsValue.Undefined);
+        }
+
+        internal static JsValue ConvertTaskToPromise(Engine engine, Task task)
+        {
+            var (promise, resolve, reject) = engine.RegisterPromise();
+            task = task.ContinueWith(continuationAction =>
+            {
+                if (continuationAction.IsFaulted)
+                {
+                    reject(FromObject(engine, continuationAction.Exception));
+                }
+                else if (continuationAction.IsCanceled)
+                {
+                    reject(FromObject(engine, new ExecutionCanceledException()));
+                }
+                else
+                {
+                    // Special case: Marshal `async Task` as undefined, as this is `Task<VoidTaskResult>` at runtime
+                    // See https://github.com/sebastienros/jint/pull/1567#issuecomment-1681987702
+                    if (Task.CompletedTask.Equals(continuationAction))
+                    {
+                        resolve(FromObject(engine, JsValue.Undefined));
+                        return;
+                    }
+
+                    var result = continuationAction.GetType().GetProperty(nameof(Task<object>.Result));
+                    if (result is not null)
+                    {
+                        resolve(FromObject(engine, result.GetValue(continuationAction)));
+                    }
+                    else
+                    {
+                        resolve(FromObject(engine, JsValue.Undefined));
+                    }
+                }
+            });
+
+            engine.AddToEventLoop(() =>
+            {
+                if (!task.IsCompleted)
+                {
+                    // Task.Wait has the potential of inlining the task's execution on the current thread; avoid this.
+                    ((IAsyncResult) task).AsyncWaitHandle.WaitOne();
+                }
+            });
+
+            return promise;
+        }
+
         [DebuggerBrowsable(DebuggerBrowsableState.Never)]
         public Types Type
         {

+ 3 - 3
Jint/Native/Object/ObjectInstance.cs

@@ -1059,7 +1059,7 @@ namespace Jint.Native.Object
                         converted = result;
                         break;
                     }
-                    
+
                     if (this is JsTypedArray typedArrayInstance)
                     {
                         converted = typedArrayInstance._arrayElementType switch
@@ -1703,14 +1703,14 @@ namespace Jint.Native.Object
                     var i = 0;
                     if (_obj._properties is not null)
                     {
-                        foreach(var key in _obj._properties)
+                        foreach (var key in _obj._properties)
                         {
                             keys[i++] = new KeyValuePair<JsValue, JsValue>(key.Key.Name, UnwrapJsValue(key.Value, _obj));
                         }
                     }
                     if (_obj._symbols is not null)
                     {
-                        foreach(var key in _obj._symbols)
+                        foreach (var key in _obj._symbols)
                         {
                             keys[i++] = new KeyValuePair<JsValue, JsValue>(key.Key, UnwrapJsValue(key.Value, _obj));
                         }

+ 53 - 40
Jint/Runtime/Interop/DefaultObjectConverter.cs

@@ -69,67 +69,80 @@ namespace Jint
                 if (value is Delegate d)
                 {
                     result = new DelegateWrapper(engine, d);
+                    return result is not null;
                 }
-                else
+
+                if (value is Task task)
+                {
+                    result = JsValue.ConvertAwaitableToPromise(engine, task);
+                    return result is not null;
+                }
+
+#if NETSTANDARD2_1_OR_GREATER || NETCOREAPP
+                if (value is ValueTask valueTask)
+                {
+                    result = JsValue.ConvertAwaitableToPromise(engine, valueTask);
+                    return result is not null;
+                }
+#endif
+
+                var t = value.GetType();
+
+                if (!engine.Options.Interop.AllowSystemReflection
+                    && t.Namespace?.StartsWith("System.Reflection", StringComparison.Ordinal) == true)
+                {
+                    const string Message = "Cannot access System.Reflection namespace, check Engine's interop options";
+                    ExceptionHelper.ThrowInvalidOperationException(Message);
+                }
+
+                if (t.IsEnum)
                 {
-                    var t = value.GetType();
+                    var ut = Enum.GetUnderlyingType(t);
 
-                    if (!engine.Options.Interop.AllowSystemReflection
-                        && t.Namespace?.StartsWith("System.Reflection", StringComparison.Ordinal) == true)
+                    if (ut == typeof(ulong))
                     {
-                        const string Message = "Cannot access System.Reflection namespace, check Engine's interop options";
-                        ExceptionHelper.ThrowInvalidOperationException(Message);
+                        result = JsNumber.Create(Convert.ToDouble(value, CultureInfo.InvariantCulture));
                     }
-
-                    if (t.IsEnum)
+                    else
                     {
-                        var ut = Enum.GetUnderlyingType(t);
-
-                        if (ut == typeof(ulong))
+                        if (ut == typeof(uint) || ut == typeof(long))
                         {
-                            result = JsNumber.Create(Convert.ToDouble(value, CultureInfo.InvariantCulture));
+                            result = JsNumber.Create(Convert.ToInt64(value, CultureInfo.InvariantCulture));
                         }
                         else
                         {
-                            if (ut == typeof(uint) || ut == typeof(long))
-                            {
-                                result = JsNumber.Create(Convert.ToInt64(value, CultureInfo.InvariantCulture));
-                            }
-                            else
-                            {
-                                result = JsNumber.Create(Convert.ToInt32(value, CultureInfo.InvariantCulture));
-                            }
+                            result = JsNumber.Create(Convert.ToInt32(value, CultureInfo.InvariantCulture));
                         }
                     }
+                }
+                else
+                {
+                    // check global cache, have we already wrapped the value?
+                    if (engine._objectWrapperCache?.TryGetValue(value, out var cached) == true)
+                    {
+                        result = cached;
+                    }
                     else
                     {
-                        // check global cache, have we already wrapped the value?
-                        if (engine._objectWrapperCache?.TryGetValue(value, out var cached) == true)
+                        var wrapped = engine.Options.Interop.WrapObjectHandler.Invoke(engine, value, type);
+
+                        if (ReferenceEquals(wrapped?.GetPrototypeOf(), engine.Realm.Intrinsics.Object.PrototypeObject)
+                            && engine._typeReferences?.TryGetValue(t, out var typeReference) == true)
                         {
-                            result = cached;
+                            wrapped.SetPrototypeOf(typeReference);
                         }
-                        else
-                        {
-                            var wrapped = engine.Options.Interop.WrapObjectHandler.Invoke(engine, value, type);
-
-                            if (ReferenceEquals(wrapped?.GetPrototypeOf(), engine.Realm.Intrinsics.Object.PrototypeObject)
-                                && engine._typeReferences?.TryGetValue(t, out var typeReference) == true)
-                            {
-                                wrapped.SetPrototypeOf(typeReference);
-                            }
 
-                            result = wrapped;
+                        result = wrapped;
 
-                            if (engine.Options.Interop.TrackObjectWrapperIdentity && wrapped is not null)
-                            {
-                                engine._objectWrapperCache ??= new ConditionalWeakTable<object, ObjectInstance>();
-                                engine._objectWrapperCache.Add(value, wrapped);
-                            }
+                        if (engine.Options.Interop.TrackObjectWrapperIdentity && wrapped is not null)
+                        {
+                            engine._objectWrapperCache ??= new ConditionalWeakTable<object, ObjectInstance>();
+                            engine._objectWrapperCache.Add(value, wrapped);
                         }
                     }
-
-                    // if no known type could be guessed, use the default of wrapping using using ObjectWrapper.
                 }
+
+                // if no known type could be guessed, use the default of wrapping using using ObjectWrapper.                
             }
 
             return result is not null;

+ 1 - 72
Jint/Runtime/Interop/DelegateWrapper.cs

@@ -138,7 +138,7 @@ namespace Jint.Runtime.Interop
                 {
                     return FromObject(Engine, result);
                 }
-                return ConvertAwaitableToPromise(result!);
+                return ConvertAwaitableToPromise(Engine, result!);
             }
             catch (TargetInvocationException exception)
             {
@@ -176,76 +176,5 @@ namespace Jint.Runtime.Interop
 #endif
         }
 
-        private JsValue ConvertAwaitableToPromise(object obj)
-        {
-            if (obj is Task task)
-            {
-                return ConvertTaskToPromise(task);
-            }
-
-#if NETSTANDARD2_1_OR_GREATER || NETCOREAPP
-            if (obj is ValueTask valueTask)
-            {
-                return ConvertTaskToPromise(valueTask.AsTask());
-            }
-
-            // ValueTask<T>
-            var asTask = obj.GetType().GetMethod(nameof(ValueTask<object>.AsTask));
-            if (asTask is not null)
-            {
-                return ConvertTaskToPromise((Task) asTask.Invoke(obj, parameters: null)!);
-            }
-#endif
-
-            return FromObject(Engine, JsValue.Undefined);
-        }
-
-        private JsValue ConvertTaskToPromise(Task task)
-        {
-            var (promise, resolve, reject) = Engine.RegisterPromise();
-            task = task.ContinueWith(continuationAction =>
-            {
-                if (continuationAction.IsFaulted)
-                {
-                    reject(FromObject(Engine, continuationAction.Exception));
-                }
-                else if (continuationAction.IsCanceled)
-                {
-                    reject(FromObject(Engine, new ExecutionCanceledException()));
-                }
-                else
-                {
-                    // Special case: Marshal `async Task` as undefined, as this is `Task<VoidTaskResult>` at runtime
-                    // See https://github.com/sebastienros/jint/pull/1567#issuecomment-1681987702
-                    if (Task.CompletedTask.Equals(continuationAction))
-                    {
-                        resolve(FromObject(Engine, JsValue.Undefined));
-                        return;
-                    }
-
-                    var result = continuationAction.GetType().GetProperty(nameof(Task<object>.Result));
-                    if (result is not null)
-                    {
-                        resolve(FromObject(Engine, result.GetValue(continuationAction)));
-                    }
-                    else
-                    {
-                        resolve(FromObject(Engine, JsValue.Undefined));
-                    }
-                }
-            });
-
-            Engine.AddToEventLoop(() =>
-            {
-                if (!task.IsCompleted)
-                {
-                    // Task.Wait has the potential of inlining the task's execution on the current thread; avoid this.
-                    ((IAsyncResult) task).AsyncWaitHandle.WaitOne();
-                }
-            });
-
-            return promise;
-        }
-
     }
 }