C++20のコルーチンを使ってSiv3D for Webでも非同期処理

Siv3D for Web でも非同期処理がしたかったので、 C++20 で追加されたコルーチンを使ってシングルスレッドで動く非同期処理ライブラリ [CoroAsync](https://github.com/Raclamusi/CoroAsync) を作りました。
このライブラリは Siv3D for Web 以外の環境でも使うことができます。

# Siv3D for Web でも非同期処理したい

皆さんは **Siv3D for Web** ([OpenSiv3D for Web](https://siv3d.kamenokosoft.com/ja/index)) を使っていますか？
Siv3D for Web はブラウザで動く Web アプリケーションを C++20 を使って作れるライブラリです。

Siv3D の機能を**ほぼ**そのまま使えるため、 Siv3D 製アプリの Web 版の作成が非常に簡単にできます。
Web 版を公開すれば、ユーザのインストールの手間がなくなったり、スマホからでもアクセスできるようになったりするため、より多くの人に作ったアプリを触ってもらえるでしょう。

<img width="400" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/643544/1222a02f-29b8-f8f6-6d45-0ab1c69aaa70.png">

しかし、 Siv3D for Web では使えない機能がいくつかあります[^web-specific-notes]。
その1つがマルチスレッドです。
Siv3D for Web はシングルスレッドで動作する設計であるため、スレッドによる非同期処理ができません。

[^web-specific-notes]: https://siv3d.kamenokosoft.com/ja/trouble-shooting/web-specific-notes

Siv3D for Web でスレッドを利用しているアプリの Web 版を作るには、スレッドを使わない形に書き換える必要があります。
しかし、非同期処理を諦めると処理中にアプリがフリーズするようなことになりかねません。
どうにかして、**スレッドを使わずに非同期処理**がしたいです。

ところで、 JavaScript はシングルスレッドで動作しますが、 async/await によって非同期処理が可能です。
C++ で 同じようなことができないでしょうか？

# C++20 のコルーチン

**できます。そう、 C++20 なら！**

C++20 では[コルーチン](https://cpprefjp.github.io/lang/cpp20/coroutines.html)が導入されました。
コルーチンは関数と似ていますが、処理の途中で中断し、途中から再開することができます。
これにより、複数の処理を、少しずつ、入れ替えながら実行することで非同期処理を実現できます。

しかし、 C++20 で追加されたのはコルーチンに関する言語仕様と構文、低レベルライブラリだけで、非同期タスクやジェネレータといったライブラリはまだ導入されていません。

# CoroAsync

**ないなら作るしかねぇよな！**

というわけで、コルーチンで非同期処理をするためのライブラリ、 [CoroAsync](https://github.com/Raclamusi/CoroAsync) を作りました！
ヘッダオンリーなライブラリなので、ダウンロードしてインクルードするだけで使えます。

```c++:簡単な使用例
# include <iostream>
# include "CoroAsync/Task.hpp"

// タスクの定義
cra::Task<> CountDown(int id)
{
    for (int i = 5; i > 0; --i)
    {
        std::cout << "task(" << id << "): " << i << '\n';
        co_await 0;  // 中断ポイント
    }
}

int main()
{
    auto task1 = CountDown(1);
    auto task2 = CountDown(2);

    task1.wait();
    task2.wait();
}
```

```text:出力
task(1): 5
task(2): 5
task(1): 4
task(2): 4
task(1): 3
task(2): 3
task(1): 2
task(2): 2
task(1): 1
task(2): 1
```

使い方に関しては、[サンプル](https://github.com/Raclamusi/CoroAsync/blob/main/Sample.cpp?ts=4)やヘッダファイルのコメントを読んでください。

GitHub: https://github.com/Raclamusi/CoroAsync
サンプル (GitHub): https://github.com/Raclamusi/CoroAsync/blob/main/Sample.cpp?ts=4
サンプル (Wandbox): https://wandbox.org/permlink/qEg1Vyl1TiD5Xix6

<details>
    <summary>サンプルを Qiita で直接見たい場合はこちら</summary>

```c++:Sample.cpp
# include <iostream>
# include <chrono>
# include <type_traits>
# include "CoroAsync/Task.hpp"
# include "CoroAsync/Utility.hpp"

using namespace std::literals::chrono_literals;

// 非同期処理するタスクの定義
//     cra::Task<Type> を戻り値の型に指定し、 co_await もしくは co_return を少なくとも1回使用する。
//     cra::Task<void> は cra::Task<> とも書ける。
cra::Task<> FuncAsync(int id)
{
	std::cout << "FuncAsync(" << id << "): begin\n";

	// co_await で中断ポイントを指定
	//     co_await relTime
	//         タスクを中断し、 relTime の時間経過を待ってからタスクキューの末尾に追加する。
	//         relTime の型は std::uint32_t もしくは std::chrono::duration<Rep, Period> であり、
	//         std::uint32_t の場合は co_await std::chrono::milliseconds{ relTime } と同じ効果を持つ。
	//         relTime の値が 0 以下の時、タスクは何も待たずにタスクキューの末尾に追加される。
	//         void に評価される。
	co_await 0;

	for (int i = 1; i <= 3; ++i)
	{
		std::cout << "FuncAsync(" << id << "): " << i << '\n';
		co_await 100ms;
	}
	std::cout << "FuncAsync(" << id << "): end\n";
}

// 返り値のあるタスクの定義
cra::Task<unsigned int> FibonacciAsync(unsigned int n)
{
	if (n <= 1)
	{
		// co_return でタスクの結果を返して終了
		co_return n;
	}

	// co_await で中断ポイントを指定
	//     co_await task
	//         タスクを中断し、指定したタスクの完了を待ってからタスクキューの末尾に追加する。
	//         task の型は cra::Task<Type> である。
	//         Type に評価される。
	auto ret = co_await FibonacciAsync(n - 1) + co_await FibonacciAsync(n - 2);

	co_return ret;
}

int main()
{
	{
		// タスクの作成
		//     戻り値は必ず受け取るようにする。
		auto task1 = FuncAsync(1);
		auto task2 = FuncAsync(2);
		auto task3 = FuncAsync(3);

		// タスクを待機
		// タイムアウトを指定して待機
		std::cout << "main: wait 150ms for task1\n";
		task1.wait_for(150ms);
		// タスクの完了まで待機
		std::cout << "main: wait for task2\n";
		task2.wait();

		// タスクの完了を確認
		std::cout << "main: task1 is " << (task1.isReady() ? "ready" : "not ready") << "\n";
		std::cout << "main: task2 is " << (task2.isReady() ? "ready" : "not ready") << "\n";
		std::cout << "main: task3 is " << (task3.isReady() ? "ready" : "not ready") << "\n";

		// タスクを中断
		//     以下の場合、タスクは中断され、それ以降は実行されない。
		//       - cra::Task::destroy を呼び出した場合
		//       - 別のタスクや空のタスクが代入された場合
		//       - デストラクタが呼び出された場合
		// t3.destroy();
		// t3 = {};
	}
	std::cout << "----------\n";
	{
		auto fib5 = FibonacciAsync(5);
		auto fib10 = FibonacciAsync(10);

		// タスクの結果を取得
		//     タスクが完了していない場合、完了まで待機する。
		std::cout << "main: fib5 = " << fib5.get() << "\n";
		std::cout << "main: fib10 = " << fib10.get() << "\n";

		// タスクを生成して結果を即取得
		std::cout << "main: fib15 = " << FibonacciAsync(15).get() << "\n";
	}
	std::cout << "----------\n";
	{
		// ラムダ式によるタスクの定義
		auto getPiAsync = []() -> cra::Task<double>
		{
			co_return 3.14159;
		};

		auto piTask = getPiAsync();
		auto pi = piTask.get();
		std::cout << "main: pi = " << pi << "\n";

		// タスクの定義と作成、待機を同時にすることも可能
		auto e = []() -> cra::Task<double> { co_return 2.71828; }().get();
		std::cout << "main: e = " << e << "\n";
	}
	std::cout << "----------\n";
	{
		auto t1 = []() -> cra::Task<std::string> { co_return "Hello"; }();
		auto t2 = []() -> cra::Task<std::string> { co_return "Good-bye"; }();

		// すべてのタスクの完了を待つタスクの作成
		//     cra::WhenAll は渡したタスクがすべて完了するのを待つタスクを返す。
		//     タスクは値で受け取るため、その場で作成するか、ムーブする必要がある。
		//     結果は std::tuple で受け取れる。このとき、結果の型が void であるものは除かれる。
		//     例えば、 WhenAll(Task<int>, Task<>, Task<std::string>) の戻り値の型は Task<std::tuple<int, std::string>> である。
		auto whenAllTask = cra::WhenAll(
			[]() -> cra::Task<int> { co_return 42; }(),
			FuncAsync(4),
			std::move(t1)
		);
		auto [a, b] = whenAllTask.get();
		std::cout << "main: WhenAll result: { " << a << ", " << b << " }\n";

		// いずれかのタスクの完了を待つタスクの作成
		//     cra::WhenAny は渡したタスクがいずれかが完了するのを待つタスクを返す。
		//     タスクを値で受け取るものと、非 const 参照で受け取るものがオーバーロードで用意されている。
		//     最初に完了したタスクに対して処理を行いつつ、最終的にはすべてのタスクの完了を待ちたい場合などに、後者を用いることができる。
		//     結果は最初に完了したタスク（参照で渡した場合は参照）が std::variant で受け取れる。
		//     std::variant::index() で何番目のタスクが最初に完了したかを知ることができる。
		auto whenAnyTask = cra::WhenAny(
			[]() -> cra::Task<> { co_await 24h; }(),
			FuncAsync(5),
			std::move(t2)
		);
		auto whenAnyResult = whenAnyTask.get();
		std::cout << "main: t" << whenAnyResult.index() << " was done first in WhenAny(t0, t1, t2)\n";
		std::visit([]<class T>(cra::Task<T>& task) {
			std::cout << "main: WhenAny result: ";
			if constexpr (std::is_void_v<T>) std::cout << "(void)";
			else std::cout << task.get();
			std::cout << "\n";
		}, whenAnyResult);
	}
}
```

</details>

# Siv3D for Web で非同期処理

ライブラリができたので、実際に Siv3D for Web で非同期処理をしてみます。

Siv3D for Web の `System::Update()` がコルーチンの中で呼ばれると unreachable エラーが発生する問題があるので、 [Siv3D 向けサンプル](https://github.com/Raclamusi/CoroAsync/blob/main/SampleForSiv3D.cpp?ts=4)のような書き方は Siv3D for Web ではできないことに注意して実装します（おそらく https://github.com/nokotan/OpenSiv3D/issues/11 と同じような問題）。

完成したものがこちらです。

https://raclamusi.github.io/CoroAsync/SampleForSiv3DForWeb.html

ソース: https://github.com/Raclamusi/CoroAsync/blob/main/SampleForSiv3DForWeb.cpp?ts=4

カーソルの追従、カウントアップ、バーの表示が非同期で実行されています！

![CaptureOfSampleForSiv3DForWeb.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/643544/77327631-03ee-3060-1bf6-d6dd80134708.gif)

# GCC のバグに苦しまされた話

GCC のコルーチンには co_await に渡すオブジェクトをコピーするバグがあるようで、そのままではコピー不可な `cra::Task` のオブジェクトが co_await に渡せませんでした[^gcc-bug]。
CoroAsync では、ラッパクラスを作ってコピーを回避しています。

[^gcc-bug]: https://twitter.com/onihusube9/status/1595359093417725952

ほかにも「 Visual Studio では動くけど GCC では動かない」といったことがいくつかあり、その対応がかなり大変でした。
マジでこれどうにかしてくれ。

# 参考サイト

- [コルーチン - cpprefjp C++日本語リファレンス](https://cpprefjp.github.io/lang/cpp20/coroutines.html)
- [C++コルーチン拡張メモ - Qiita](https://qiita.com/yohhoy/items/aeb3c01d02d0f640c067)