Generator add-on for coroutines management

Started by
8 comments, last by gjl 7 years ago

Hi,

I am working on adding coroutines support to an application framework that uses anglescript for GUI scripting. After giving a try to the coroutines sample, it appears that it won't work for me: you cannot manage the coroutines from the scripts, as it requires that the ExecuteScripts function to be called on a regular basis by the C++ side. Also, calling yield from a context that was not registered will just do nothing.

So I have written a very simple Generator add-on that makes it easier to manage coroutines from scripts, using a very similar syntax as Javascript (not returning values for the moment). So the coroutines sample script becomes:


void main()                                
{                                                                               
    int count = 10;                        
    generator@ genB=createGenerator(thread,               
      dictionary = {{"count", 3},          
                    {"str", " B"}});       
  
    while( count-- > 0 )                   
    {                                      
      print("A :" + count + "\n");        
      genB.next();                             
    }                                                                           
}      

void thread(dictionary @args)             
{                                          
  int count = int(args["count"]);          
  string str = string(args["str"]);        
  while( count-- > 0 )                     
  {                                        
    print(str + ":" + count + "\n");      
    yield();                               
  }                                        
}

The major difference is that "main()" can be called from any context (no need to add it to the context manager). And you can then write your own coroutines dispatcher in Angelscript, like this very simplistic one:


class GenDispatcher
{
    bool next()
    {
        if(nextIndex>=tasks.length)
            nextIndex=0;

        if (nextIndex<tasks.length)
        {
            bool hasNext=tasks[nextIndex].next();
            if (!hasNext)
            {
                tasks.removeAt(nextIndex);
            }
            else
                nextIndex++;
        }
        return tasks.length!=0;
    }
    array<generator@> tasks;
    private int nextIndex=0;
};

Would you be interested in incorporating this add-on into angelscript? It is already running and I'll share the code anyways, but if it has to become an official add-on, some extra work is required, such as implementing the generic calling convention (which I do not use for the moment).

Advertisement

I'm interested in taking a closer look at this.

Making things both easier to use and more powerful at the same time is always good :)

AngelCode.com - game development and more - Reference DB - game developer references
AngelScript - free scripting library - BMFont - free bitmap font generator - Tower - free puzzle game

It is actually extremely simple for the moment. I reused the basic ideas from the coroutines add-on (actually copy/pasted the code). You can find the current prototype here (generator.h and cpp):

https://github.com/bluecataudio/angelscript-addons/tree/master/cpp

I am however still wondering a couple of things, so it may change quite a bit in the future. Your opinion is of course welcome!
?- should I keep the javascript-like syntax with next(), or do something like LUA (resume() etc.)? Since coroutines already exist with the coroutines add-on, I have chosen the javascript style so far, and it is also a bit more modern.
?- Does it make sense to implement values passing (back and forth) with yield like in javascript? Like:


// dummy example - incomplete syntax
coroutineProc(dictionary@ args)
{
  string x="";
  while(count-->0)
  {
    print("Hello" + x);
    x=yield(count);
  }
}

main()
{
   // create generator
   generator@ gen=...
   gen.next("A");
   print(gen.value);
   gen.next("B");
   print(gen.value);
}

To make the above example work nicely, the generator object should be a template with two arguments, so it makes the whole thing a bit heavier, since template functions are not supported in Angelcript. An alternative would be to use dictionaries or "any" for argument types, but it is not very elegant - I like Angelscript's type safety :-).

I have finally added support for values exchange between the caller and callee using any objects:

https://github.com/bluecataudio/angelscript-addons/tree/master/cpp

It was not as simple as I expected, because the VM is actually suspended AFTER the call to yield returns. But it now works. No change for the simple example (yield() and next() without arguments are still supported). But you can also write the following:


void main()
{
    int count = 10;
    generator@ genB = createGenerator(threadB,
        dictionary = { {"count", 3},
                      {"str", " B"} });
    generator@ genC = createGenerator(threadC,
        dictionary = { {"count", 5},
        {"str", " C"} });
    while (count-- > 0)
    {
        print("A :" + count + "\n");
        if (genB.next(count))
        {
            int bValue = -1;
            if (genB.value.retrieve(bValue))
            {
                print("A knows B is:" + bValue + "\n");
            }
        }
        if (genC.next(count))
        {
            int cValue = -1;
            if (genC.value.retrieve(cValue))
            {
                print("A knows C is:" + cValue + "\n");
            }
        }
    }
}

void threadB(dictionary @args)
{
    int count = int(args["count"]);
    string str = string(args["str"]);
    any valueSent;
    while (count-- > 0)
    {
        int fromA = -1;
        if (valueSent.retrieve(fromA)!=false)
            print(str + ":" + count + " - knows A is:" + fromA + "\n");
        else
            print(str + ":" + count + " - knows A is: ?\n");
        valueSent = yield(count);
    }
}

void threadC(dictionary @args)
{
    int count = int(args["count"]);
    string str = string(args["str"]);
    while (count-- > 0)
    {
        print(str + ":" + count + "\n");
        yield(count);
    }
}

Which outputs:


A :9
 B:2 - knows A is: ?
A knows B is:2
 C:4
A knows C is:4
A :8
 B:1 - knows A is:8
A knows B is:1
 C:3
A knows C is:3
A :7
 B:0 - knows A is:7
A knows B is:0
 C:2
A knows C is:2
A :6
 C:1
A knows C is:1
A :5
 C:0
A knows C is:0
A :4
A :3
A :2
A :1
A :0

Thanks for the contribution. I'll definitely take a closer look at this once I get the time.

I cannot comment much on the naming convention as I don't have much practice with co-routines myself. It's not a very common design pattern in C++ :)

At first glance I do find the name 'generator' is a bit odd. It implies that the co-routine is just used to generate values, but I can imagine that co-routines can be used for many other purposes as well, so perhaps the name should be a bit more generic? In that same line of thought, perhaps the method to resume the processing shouldn't be next(). next() goes well together with the name generator, but if the co-routine is just used to process something then perhaps next() isn't as intuitive.

AngelCode.com - game development and more - Reference DB - game developer references
AngelScript - free scripting library - BMFont - free bitmap font generator - Tower - free puzzle game

I definitely agree about naming. But I hate reinventing the wheel, so I copied this one from Javascript. The main difference with javascript though is that the iterator interface has a special meaning in the language, so you can do foreach() etc. on generators, which is something that does not exist (yet) in Angelscript.

On the other hand, generators are more generic than coroutines: it CAN be used as coroutines (especially this angelscript version, that does not have the limitations of javascript generators, such as calling yield in a subroutine). but it can also be used to create generic iterators that return a value for each step (see my example above).

So I am still thinking about it - any input is welcome! Even if I don't like the generator keyword, the fact that it already exists in a widespread language tells me not to reinvent another one. Also, when used as an iterator, resume() does not mean much (whereas next() in the context of a coroutine actually means nextStep()).

By the way here some more information about the javascript generators (no need to be a javascript guru):

https://blog.ragnarson.com/2016/12/15/javascript-generators.html

I am actually thinking about using the generators as is, and why not build specific coroutine class on top of is, in plain angelscript - the only difference would be naming, and the fact that you can launch a coroutine until the first yield, without having to call next() once.

Also, with coroutines, you may actually want to include a scheduler (dispatcher) to manage them, whereas a generator is just a lower level building block for coroutines, managing pause/resume and basic communication (managing return value and argument). You could also imagine that the scheduler class would actually be used as a coroutine factory, and you may want to assign them a priority etc.

An update has been posted. Generators will now pass the exception to the parent execution context.

https://github.com/bluecataudio/angelscript-addons/tree/master/cpp

Also, if you are using the JIT by BlindMind studios, you will need the following fix to avoid crashes when destroying a generator while its execution is not finished:

https://github.com/BlindMindStudios/AngelScript-JIT-Compiler/pull/24

Thanks for the update.

I haven't had the time to really look into this yet, but I haven't forgotten it ;)

AngelCode.com - game development and more - Reference DB - game developer references
AngelScript - free scripting library - BMFont - free bitmap font generator - Tower - free puzzle game

Still working on it anyway :-)

This topic is closed to new replies.

Advertisement