async #105

Open
xen2 opened this Issue Jun 19, 2012 · 13 comments

2 participants

@xen2

Just some thoughts about feasibility of async support.

There could be two roads:

  • Use generated state machine directly from IL (but well, I guess it probably won't work because of goto/labels)
  • It seems ILspy now support async, so maybe combined with a framework such as TameJS, it could be a viable option.

Might take a look into that over the next months.

@kg
Squared Interactive member
kg commented Jun 19, 2012

Personally prefer the state machine approach, since it's more straightforward and debugging would be easier (you can compare the execution of the state machine in JSIL with execution of the same state machine in .NET), but something like TameJS could definitely work.

@xen2

Well, good news!
I have been playing with it a bit and could get a proof of concept test code working!
I went for the first option (state machine from IL, with help of AsyncBridge, and implemented some Task/TaskCompletionSource/etc... methods).

I was using this kind of code as test case:

using System;
using System.Threading.Tasks;

public static class Program
{
    public static async Task Test2()
    {
        //await Task.Factory.StartNew(() => { });
        await TaskEx.Delay(1000);
    }

    public static async Task Test()
    {
        await Test2();
    }

    public static void Main(string[] args)
    {
        Test();
    }
}

I implemented TaskEx.Delay as:

    function Delay (dueTime) {
      var tcs = new (System.Threading.Tasks.TaskCompletionSource$b1.Of(System.Boolean))();
      setTimeout(function() { tcs.TrySetResult(true); }, dueTime);
      return tcs.Task;
    }

So basically it is quite easy to implement from javascript (that was important for me because most of the "real" async functions (I/O blocking) have a continuation callback. As a result, it should be easy to map I/O code such as socket send/receive, download, I/O, timer, etc... using the same async Task prototypes as from C#.

Still lot of work to do (had to comment some stuff out such as ExecutionContext, have to implement dummy functions for those) and need to validate it is really working as expected and that there will be no blocker for more complex situations.

Also, I had to patch a few things inside JSIL. I will run those changes by you.
Maybe the biggest issue was when using AsyncBridge, it overrides mscorlib, and sometimes TypeInfoProvider.Get was called with TypeReference instead of TypeDefinition, which doesn't include the actual referenced assembly. As a result it was calling the function from the wrong assembly.
I did a quick fix for that but it brought some other problems, but it should be quite easy to fix as soon as I spend some time on it (I wanted to do a proof of concept very quickly in a few hours as a first step before going further, because I wasn't totally sure it would work out).

@kg
Squared Interactive member
kg commented Jun 21, 2012

That's really cool. I didn't know AsyncBridge existed, it looks really useful. If you want to share your fork with me, I can probably look into the TypeInfoProvider problem.

@xen2

Ok, it seems working on more advanced test cases, after cleaning it up and implementing some missing functions.
I can now loop over await, etc...

Here are the problems I still have/or have fixed on my own:

  • ref this doesn't work with struct+interface This is the last thing I have to fix by hand in auto-generated AsyncBridge javascript. Here is a test case reproducing the two bugs.

If it is too difficult to fix (I think it might require some heavy changes), it only happens in a single 10-lines function of AsyncBridge and I can easily make it external (see next point), so don't bother too much.

using System;

public interface I {
    int Value { get; set; }
    void Method();
}

public struct A : I {
    public int Value { get; set; }

    public void Method() {
        Method2(ref this);
        I test = this;
        test.Value = 12;
        Console.WriteLine(this.Value);
    }

    public void Method2<T>(ref T a) where T : I {
        I copy = a;
        a.Value += 2;
        Console.WriteLine(copy.Value);
    }
}

public static class Program {
    public static void Test<T>(ref T obj) where T : I {
        obj.Value = 4;
        obj.Method();
        Console.WriteLine(obj.Value);
    }

    public static void Main (string[] args) {
        var obj = new A();
        Test(ref obj);
    }
}
  • I realized having ImplementExternals available even if it is not defined in a proxy is helpful to override method (such as the one failing with previous test case, or also to override method defined in the assembly without having to modify it -- an example would be TaskEx.Delay which is defined in AsyncBridge but need to be rewritten for JS ; I also had the requirement for my own assemblies so I figure this could be useful).
    I already did it and will provide a patch if you agree with doing it (basically, I allow method to be implemented externally even if they have been defined with a simple Method instead of ExternalMethod).

  • Previous issue about TypeReference. Already fixed, I only have a small bug left, but I think I can probably take care of it and you can review the patch (and do it another way if I did it wrong, but at least it will show you what was my problem). I can also provide an assembly (.exe) which exhibit the issue if required.

@xen2

BTW, exception handling seems to work fine as well!

@kg
Squared Interactive member
kg commented Jun 24, 2012

Oh, I didn't realize you were reporting a new issue - I'll get to stuff like that faster if you assign it a new issue# so that I see it in my inbox.

I'm not sure if replacing actual implementations with externals is a great idea. I think I would at least want it to be something you explicitly have to opt into; otherwise it would be very easy to make mistakes. I actually used to support that by accident and it caused a lot of bugs.

@kg
Squared Interactive member
kg commented Jun 24, 2012

Wow, that test case is actually a serious problem. 'I copy = a' makes a copy of a, even though T has no struct constraint. It's not possible to determine that a struct copy is needed there at compile time - it has to be done at runtime...

Actually, I think copy isn't actually ever a copy, since it has an interface pointer (which is a by-reference type) - the interface pointer must be a boxed copy of the struct.

@xen2

Ok, will make new issue from now on!

For externals, maybe that's something I could do as setting per assembly. Or maybe another way to declare them would be a much better solution (i.e. use $.OverrideMethod instead of $.Method), because that's something the developer really intends to do for specific function.

For generics, .NET can differentiate them at runtime during JITing (generics do a unique codegen for all reference type -- everything is similar except new T() which generates Activator.CreateInstance -- and do separate codegen for each struct type because it will generates different assembly code). Of course, JSIL doesn't have this option, so I guess copy need to go through specific functions, probably similar to default(T) code I did some time ago (check IsValueType at runtime).

@kg
Squared Interactive member
kg commented Jun 24, 2012

As of d5148b4, struct semantics should be close enough that your code might work unmodified. If it doesn't, you can try adding a ' : struct' constraint to your generic parameters (it will generate copies for those generic parameters now). In the future I will probably come up with a complete fix.

'ref this' generates working code now, also (but it creates a new Variable for each call, so it could be more efficient).

@xen2

Thanks, this improve the situation (no more missing .value). However, as you pointed out, it still doesn't work without a struct constraint. Anyway, no big deal since I can just add the constraint or make it external for now.

BTW, just in case, here is a simple test case for this:

using System;

public interface I {}
public struct A : I {}

public static class Program
{
    public static void Function<T>(ref T obj)
        where T : I
    {
        I localObj1 = obj;
        var localObj2 = obj;
        Console.WriteLine(object.ReferenceEquals(localObj1, obj) ? "true" : "false");
        Console.WriteLine(object.ReferenceEquals(localObj2, obj) ? "true" : "false");
    }

    public static void Main(string[] args) {
        var a = new A();
        Function(ref a);
    }
}
@xen2

Ok, have been putting async on top of websocket, it seems to work quite well in practice (that makes sharing network code between C# and javascript much more easy as both of them can be async the same way).

I will probably sort out my changes and make patches in the next days.

@kg
Squared Interactive member
kg commented Jul 3, 2012

6cf8588 brings semantics even closer to the real .NET runtime. Reference equality semantics are different, but unconstrained generic parameters will generate conditional copies (that cause a struct copy when T is a struct, but not when T is a reference type or primitive). Constrained parameters will do the 'right thing' so you don't end up with conditional copies for ': class' parameters, and you get unconditional copies for ': struct' parameters.

Let me know if this isn't enough for the code to work unmodified.

@xen2

Thanks, I could make everything work unmodified, that's a good milestone!

@iskiselev iskiselev added a commit to iskiselev/JSIL that referenced this issue Apr 5, 2014
@iskiselev iskiselev Initial async/await support (#105, #108) 9c205b2
@iskiselev iskiselev added a commit to iskiselev/JSIL that referenced this issue Jun 11, 2014
@iskiselev iskiselev Initial async/await support (#105, #108) 9cb7515
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment