0

I have implemented a c++ coroutine framework so that I can coawait my own coroutines and script sequences like so:

co_await obj->move_to(....)
obj->face(LEFT);
co_await wait(15);

This would be a coroutine that I update one frame at a time from my main program, and would execute the directions in order as if they are blocking.

This is the implementation of my coroutines for reference:

struct Script {
struct suspend_maybe {
    bool suspend;
    bool await_ready() noexcept { return !suspend; }
    void await_suspend(std::coroutine_handle<> c) noexcept {}
    void await_resume() noexcept { }
};

struct promise_type {
    std::coroutine_handle<promise_type> child;
    std::coroutine_handle<promise_type> caller;
    bool cancelled = false;
    void cancel() { cancelled = true; }

    Script get_return_object() {
        return { std::coroutine_handle<Script::promise_type>::from_promise(*this) };
    }
    suspend_maybe initial_suspend() noexcept {
        return { !caller };
    }
    std::suspend_always final_suspend() noexcept {
        return {};
    }
    void unhandled_exception() {}
    void return_void() {}
};

bool await_ready() noexcept { return false; }
bool await_suspend(std::coroutine_handle<Script::promise_type> c) noexcept {
    c.promise().child = h_;
    h_.promise().caller = c;
    return true;
}
void await_resume() noexcept { }

std::coroutine_handle<Script::promise_type> h_;
Script(std::coroutine_handle<Script::promise_type> h) :h_{ h } { }
operator std::coroutine_handle<Script::promise_type>() const { return h_; }

// Step update the coroutine, managing its control flow
void update() {
    auto& child = h_.promise().child;
    if (child && !(child.done() || child.promise().cancelled)) {
        child.promise().get_return_object().update();
    }
    else {
        if (child && (child.done() || child.promise().cancelled)) {
            child.destroy();
            child = nullptr;
        }
        h_.resume();
    }
}
};

 typedef std::coroutine_handle<Script::promise_type> CHandle;

And this is how I update the coroutines that are spawned by the script, in the main loop per every frame of rendering:

CHandle ScriptPlayer::spawn(CHandle script) {
    scripts.push_back(script);
    return script;
}

void ScriptPlayer::update() {
  for (int i = 0; i < static_cast<int>(scripts.size()); ) {
      Script{ scripts[i] }.update();
      if (play_next_script) break;

      // Check completion
      if (scripts[i].done() || scripts[i].promise().cancelled) {
          scripts[i].destroy();
          scripts.erase(scripts.begin() + i);
      }
      else i++;
  }
}

Then somewhere I have a lambda that I use to generate a Script and spawn it to the vector of coroutines that I update every frame:

auto terminate = [](ScriptPlayer* player, Object* fx) -> Script {
   co_await fx->wait_animation();
   player->destroy_object(fx);
}(player, fx);
player->spawn(terminate);

If I use the lambda function parameters like I did above, everything works perfectly fine. On the other hand, if I try to use the captures like so:

auto terminate = [player, fx]() -> Script {
   co_await fx->wait_animation();
   player->destroy_object(fx);
}();
player->spawn(terminate);

The whole thing crashes within "wait_animation" during runtime with a completely corrupt coroutine stack frame.

Why? I thought passing pointers by value through the captures would be the same as passing the same pointers through function arguments?

Lake
  • 4,072
  • 26
  • 36
  • 1
    "*I try to use the catch-block*" There is no `catch` block in that code. If you're talking about lambda *captures*, that's not a "catch block". It's not a block at all. – Nicol Bolas Feb 18 '23 at 23:12
  • @NicolBolas You're totally right, fixed the wrong vocabulary ^^; – Lake Feb 19 '23 at 16:59
  • @NicolBolas Ok wow, yep. Your post entirely explains my issue, I didn't even know how lambdas actually worked. I'm trying to wrap my head around how the function itself doesn't go out of scope when the lambda object does but I guess that's another story (I guess lambda functions must be compiled into some sort of actual c functions so that they are "static")... – Lake Feb 19 '23 at 17:17
  • Functions don't have lifetimes. *Objects* have lifetimes, but member functions of objects do not. A member function is just a function that gets a hidden parameter that points to the object it is being run on. You can get a pointer to any function (or a pointer to a member function) at any time, so long as the name is accessible to you. – Nicol Bolas Feb 19 '23 at 17:28
  • Makes perfect sense ^^ I just wonder sometimes about the magic that converts something like a lambda into actual c++ code, it's never too obvious lol – Lake Feb 19 '23 at 17:32
  • 1
    It's not "magic"; it's just an object of a compiler-generated class type. There is *nothing* that a lambda can do that you can't do manually. It's just syntactic sugar. Even "captured parameters" are just *member variables* of some object, which are initialized by a constructor call that is generated by the compiler. – Nicol Bolas Feb 19 '23 at 17:37

0 Answers0