Skip to content

Commit

Permalink
Add task support to DefaultObjectConverter (#1787)
Browse files Browse the repository at this point in the history
  • Loading branch information
tom-b-iodigital authored Feb 21, 2024
1 parent c2bb947 commit 3fbbff7
Show file tree
Hide file tree
Showing 6 changed files with 222 additions and 121 deletions.
60 changes: 54 additions & 6 deletions Jint.Tests/Runtime/AsyncTests.cs
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
using Jint.Tests.Runtime.TestClasses;

namespace Jint.Tests.Runtime;

public class AsyncTests
Expand Down Expand Up @@ -28,6 +30,26 @@ static async Task<int> Callable()
}
}

[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()
{
Expand All @@ -45,6 +67,19 @@ static async Task Callable(CancellationToken token)
}
}

[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()
{
Expand All @@ -60,23 +95,36 @@ static async Task Callable()
}
}

[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
Expand Down
40 changes: 40 additions & 0 deletions Jint.Tests/Runtime/TestClasses/AsyncTestClass.cs
Original file line number Diff line number Diff line change
@@ -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 changes: 71 additions & 0 deletions Jint/Native/JsValue.cs
Original file line number Diff line number Diff line change
Expand Up @@ -119,6 +119,77 @@ internal virtual bool TryGetIterator(
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
{
Expand Down
6 changes: 3 additions & 3 deletions Jint/Native/Object/ObjectInstance.cs
Original file line number Diff line number Diff line change
Expand Up @@ -1059,7 +1059,7 @@ private object ToObject(ObjectTraverseStack stack)
converted = result;
break;
}

if (this is JsTypedArray typedArrayInstance)
{
converted = typedArrayInstance._arrayElementType switch
Expand Down Expand Up @@ -1703,14 +1703,14 @@ public KeyValuePair<JsValue, JsValue>[] Entries
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));
}
Expand Down
93 changes: 53 additions & 40 deletions Jint/Runtime/Interop/DefaultObjectConverter.cs
Original file line number Diff line number Diff line change
Expand Up @@ -69,67 +69,80 @@ public static bool TryConvert(Engine engine, object value, Type? type, [NotNullW
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;
Expand Down
Loading

0 comments on commit 3fbbff7

Please sign in to comment.