diff --git a/src/co/Generator.hxx b/src/co/Generator.hxx new file mode 100644 index 0000000000..f05608917b --- /dev/null +++ b/src/co/Generator.hxx @@ -0,0 +1,109 @@ +// SPDX-License-Identifier: BSD-2-Clause +// author: Max Kellermann + +#pragma once + +#include "UniqueHandle.hxx" + +#include +#include +#include +#include + +namespace Co { + +/** + * Task type for a coroutine that generates values. + * + * It is meant to be used in a range-based "for" loop. + */ +template +class Generator { +public: + struct promise_type { + std::optional value; + std::exception_ptr error; + + bool IsReady() const noexcept { + return value || error; + } + + [[nodiscard]] + Generator get_return_object() noexcept { + return Generator(std::coroutine_handle::from_promise(*this)); + } + + auto initial_suspend() noexcept { + /* don't suspend initially because we want the + first value to be available immediately, or + else operator*() below does not work */ + return std::suspend_never{}; + } + + auto final_suspend() noexcept { + return std::suspend_always{}; + } + + void unhandled_exception() noexcept { + error = std::current_exception(); + } + + template + std::suspend_always yield_value(U &&_value) { + value.emplace(std::forward(_value)); + return {}; + } + + void return_void() noexcept {} + }; + +private: + UniqueHandle coroutine; + + [[nodiscard]] + explicit Generator(std::coroutine_handle _coroutine) noexcept + :coroutine(_coroutine) + { + } + + struct end_iterator {}; + + struct iterator { + const std::coroutine_handle co; + + bool operator==(const end_iterator &) const noexcept { + return co.done() && !co.promise().IsReady(); + } + + iterator &operator++() { + auto &promise = co.promise(); + assert(!promise.error); + assert(promise.value); + + promise.value.reset(); + co.resume(); + return *this; + } + + T &&operator*() const { + auto &promise = co.promise(); + assert(promise.IsReady()); + + if (promise.error) + std::rethrow_exception(co.promise().error); + + return std::move(*promise.value); + } + }; + +public: + auto begin() const noexcept { + return iterator{coroutine.get()}; + } + + auto end() const noexcept{ + return end_iterator{}; + } +}; + +} // namespace Co diff --git a/src/co/UniqueHandle.hxx b/src/co/UniqueHandle.hxx new file mode 100644 index 0000000000..6e91ea90bc --- /dev/null +++ b/src/co/UniqueHandle.hxx @@ -0,0 +1,77 @@ +// SPDX-License-Identifier: BSD-2-Clause +// Copyright CM4all GmbH +// author: Max Kellermann + +#pragma once + +#include +#include + +namespace Co { + +/** + * Manage a std::coroutine_handle<> which is destroyed by the + * destructor. + */ +template +class UniqueHandle { + std::coroutine_handle value; + +public: + UniqueHandle() = default; + + explicit constexpr UniqueHandle(std::coroutine_handle h) noexcept + :value(h) {} + + UniqueHandle(UniqueHandle &&src) noexcept + :value(std::exchange(src.value, nullptr)) + { + } + + /* this overload allows casting a specialized handle to a + std::coroutine_handle */ + template + requires(std::is_void_v && !std::is_void_v

) + UniqueHandle(UniqueHandle

&&src) noexcept + :value(src.release()) + { + } + + ~UniqueHandle() noexcept { + if (value) + value.destroy(); + } + + auto &operator=(UniqueHandle &&src) noexcept { + using std::swap; + swap(value, src.value); + return *this; + } + + operator bool() const noexcept { + return (bool)value; + } + + const auto &get() const noexcept { + return value; + } + + const auto *operator->() const noexcept { + return &value; + } + +#ifdef __clang__ + /* the non-const overload is only needed for clang, because in + libc++11, some methods are not "const" */ + auto *operator->() noexcept { + return &value; + } +#endif + + [[nodiscard]] + auto release() noexcept { + return std::exchange(value, nullptr); + } +}; + +} // namespace Co diff --git a/src/command/StickerCommands.cxx b/src/command/StickerCommands.cxx index 82080f9bfd..7d7bc680ee 100644 --- a/src/command/StickerCommands.cxx +++ b/src/command/StickerCommands.cxx @@ -29,6 +29,7 @@ #include "db/DatabaseLock.hxx" #include #include "song/Filter.hxx" +#include "co/Generator.hxx" namespace { @@ -101,63 +102,30 @@ class DomainHandler { virtual CommandResult Find(const char *uri, const char *name, StickerOperator op, const char *value, const char *sort, bool descending, RangeArg window) { - auto data = CallbackContext{ - .name = name, - .sticker_type = sticker_type, - .response = response, - .is_song = StringIsEqual("song", sticker_type) - }; - - auto callback = [](const char *found_uri, const char *found_value, void *user_data) { - auto context = reinterpret_cast(user_data); - context->response.Fmt("{}: {}\n", - context->is_song ? "file" : context->sticker_type, found_uri); - sticker_print_value(context->response, context->name, found_value); - }; - - sticker_database.Find(sticker_type, - uri, - name, - op, value, - sort, descending, window, - callback, &data); + const bool is_song = StringIsEqual("song", sticker_type); + + for (const auto &i : sticker_database.Find(sticker_type, uri, + name, op, value, + sort, descending, window)) { + response.Fmt("{}: {}\n", is_song ? "file" : sticker_type, i.uri); + sticker_print_value(response, name, i.value); + } return CommandResult::OK; } virtual CommandResult Names() { - auto data = CallbackContext{ - .name = "", - .sticker_type = sticker_type, - .response = response, - .is_song = StringIsEqual("song", sticker_type) - }; - - auto callback = [](const char *found_value, void *user_data) { - auto context = reinterpret_cast(user_data); - context->response.Fmt("name: {}\n", found_value); - }; - - sticker_database.Names(callback, &data); + for (const char *name : sticker_database.Names()) + response.Fmt("name: {}\n", name); return CommandResult::OK; } CommandResult NamesTypes(const char *type) { - auto data = CallbackContext{ - .name = "", - .sticker_type = sticker_type, - .response = response, - .is_song = StringIsEqual("song", sticker_type) - }; - - auto callback = [](const char *found_value, const char *found_type, void *user_data) { - auto context = reinterpret_cast(user_data); - context->response.Fmt("name: {}\n", found_value); - context->response.Fmt("type: {}\n", found_type); - }; - - sticker_database.NamesTypes(type, callback, &data); + for (const auto &i : sticker_database.NamesTypes(type)) { + response.Fmt("name: {}\n", i.value); + response.Fmt("type: {}\n", i.type); + } return CommandResult::OK; } @@ -188,14 +156,6 @@ class DomainHandler { Response &response; const Database &database; StickerDatabase &sticker_database; - -private: - struct CallbackContext { - const char *const name; - const char *const sticker_type; - Response &response; - const bool is_song; - }; }; /** @@ -216,15 +176,12 @@ class SongHandler final : public DomainHandler { CommandResult Find(const char *uri, const char *name, StickerOperator op, const char *value, const char *sort, bool descending, RangeArg window) override { - struct sticker_song_find_data data = { - response, - name, - }; - - sticker_song_find(sticker_database, database, uri, data.name, - op, value, - sort, descending, window, - sticker_song_find_print_cb, &data); + for (const auto &i : sticker_song_find(sticker_database, database, uri, name, + op, value, + sort, descending, window)) { + song_print_uri(response, i.song); + sticker_print_value(response, name, i.value); + } return CommandResult::OK; } @@ -238,21 +195,6 @@ class SongHandler final : public DomainHandler { } private: - struct sticker_song_find_data { - Response &r; - const char *name; - }; - - static void - sticker_song_find_print_cb(const LightSong &song, const char *value, - void *user_data) - { - auto *data = (struct sticker_song_find_data *)user_data; - - song_print_uri(data->r, song); - sticker_print_value(data->r, data->name, value); - } - const LightSong* song = nullptr; }; diff --git a/src/lib/sqlite/Generator.cxx b/src/lib/sqlite/Generator.cxx new file mode 100644 index 0000000000..02d6e121bd --- /dev/null +++ b/src/lib/sqlite/Generator.cxx @@ -0,0 +1,31 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +// Copyright The Music Player Daemon Project + +#include "Generator.hxx" +#include "Row.hxx" +#include "Util.hxx" + +#include + +namespace Sqlite { + +Co::Generator +GenerateRows(sqlite3_stmt &stmt) +{ + while (true) { + int result = ExecuteBusy(&stmt); + switch (result) { + case SQLITE_ROW: + co_yield Row{stmt}; + break; + + case SQLITE_DONE: + co_return; + + default: + throw SqliteError{&stmt, result, "sqlite3_step() failed"}; + } + } +} + +} // namespace Sqlite diff --git a/src/lib/sqlite/Generator.hxx b/src/lib/sqlite/Generator.hxx new file mode 100644 index 0000000000..a8e425c3c6 --- /dev/null +++ b/src/lib/sqlite/Generator.hxx @@ -0,0 +1,17 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +// Copyright The Music Player Daemon Project + +#pragma once + +#include "co/Generator.hxx" + +struct sqlite3_stmt; + +namespace Sqlite { + +class Row; + +Co::Generator +GenerateRows(sqlite3_stmt &stmt); + +} // namespace Sqlite diff --git a/src/lib/sqlite/Row.hxx b/src/lib/sqlite/Row.hxx new file mode 100644 index 0000000000..98353fcddc --- /dev/null +++ b/src/lib/sqlite/Row.hxx @@ -0,0 +1,24 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +// Copyright The Music Player Daemon Project + +#pragma once + +#include + +namespace Sqlite { + +class Row { + sqlite3_stmt &stmt; + +public: + [[nodiscard]] + explicit constexpr Row(sqlite3_stmt &_stmt) noexcept + :stmt(_stmt) {} + + [[gnu::pure]] + const char *operator[](unsigned column) const noexcept { + return reinterpret_cast(sqlite3_column_text(&stmt, column)); + } +}; + +} // namespace Sqlite diff --git a/src/lib/sqlite/Util.hxx b/src/lib/sqlite/Util.hxx index 22979bd20d..3ce3793696 100644 --- a/src/lib/sqlite/Util.hxx +++ b/src/lib/sqlite/Util.hxx @@ -27,7 +27,7 @@ Prepare(sqlite3 *db, const char *sql) /** * Throws #SqliteError on error. */ -static void +static inline void Bind(sqlite3_stmt *stmt, unsigned i, const char *value) { int result = sqlite3_bind_text(stmt, i, value, -1, nullptr); @@ -67,7 +67,7 @@ BindAll(sqlite3_stmt *stmt, Args&&... args) * Call sqlite3_stmt() repepatedly until something other than * SQLITE_BUSY is returned. */ -static int +static inline int ExecuteBusy(sqlite3_stmt *stmt) { int result; @@ -83,7 +83,7 @@ ExecuteBusy(sqlite3_stmt *stmt) * * Throws #SqliteError on error. */ -static bool +static inline bool ExecuteRow(sqlite3_stmt *stmt) { int result = ExecuteBusy(stmt); @@ -102,7 +102,7 @@ ExecuteRow(sqlite3_stmt *stmt) * * Throws #SqliteError on error. */ -static void +static inline void ExecuteCommand(sqlite3_stmt *stmt) { int result = ExecuteBusy(stmt); @@ -136,26 +136,6 @@ ExecuteModified(sqlite3_stmt *stmt) return ExecuteChanges(stmt) > 0; } -template -static inline void -ExecuteForEach(sqlite3_stmt *stmt, F &&f) -{ - while (true) { - int result = ExecuteBusy(stmt); - switch (result) { - case SQLITE_ROW: - f(); - break; - - case SQLITE_DONE: - return; - - default: - throw SqliteError(stmt, result, "sqlite3_step() failed"); - } - } -} - } // namespace Sqlite #endif diff --git a/src/lib/sqlite/meson.build b/src/lib/sqlite/meson.build index 5e5bb3e568..6bdc775d21 100644 --- a/src/lib/sqlite/meson.build +++ b/src/lib/sqlite/meson.build @@ -15,6 +15,7 @@ sqlite = static_library( 'sqlite', 'Database.cxx', 'Error.cxx', + 'Generator.cxx', include_directories: inc, dependencies: [ sqlite_dep, diff --git a/src/sticker/Database.cxx b/src/sticker/Database.cxx index b80e30185c..86d2255866 100644 --- a/src/sticker/Database.cxx +++ b/src/sticker/Database.cxx @@ -3,6 +3,8 @@ #include "Database.hxx" #include "Sticker.hxx" +#include "lib/sqlite/Generator.hxx" +#include "lib/sqlite/Row.hxx" #include "lib/sqlite/Util.hxx" #include "fs/Path.hxx" #include "fs/NarrowPath.hxx" @@ -221,11 +223,11 @@ StickerDatabase::ListValues(std::map> &tab sqlite3_clear_bindings(s); }; - ExecuteForEach(s, [s, &table](){ - const char *name = (const char *)sqlite3_column_text(s, 0); - const char *value = (const char *)sqlite3_column_text(s, 1); + for (const auto &row : GenerateRows(*s)) { + const char *name = row[0]; + const char *value = row[1]; table.emplace(name, value); - }); + } } void @@ -446,16 +448,11 @@ StickerDatabase::BindFind(const char *type, const char *base_uri, std::unreachable(); } -void +Co::Generator StickerDatabase::Find(const char *type, const char *base_uri, const char *name, StickerOperator op, const char *value, - const char *sort, bool descending, RangeArg window, - void (*func)(const char *uri, const char *value, - void *user_data), - void *user_data) + const char *sort, bool descending, RangeArg window) { - assert(func != nullptr); - sqlite3_stmt *const s = BindFind(type, base_uri, name, op, value, sort, descending, window); assert(s != nullptr); @@ -463,11 +460,8 @@ StickerDatabase::Find(const char *type, const char *base_uri, const char *name, sqlite3_finalize(s); }; - ExecuteForEach(s, [s, func, user_data](){ - func((const char*)sqlite3_column_text(s, 0), - (const char*)sqlite3_column_text(s, 1), - user_data); - }); + for (const auto &row : GenerateRows(*s)) + co_yield FindRecord{row[0], row[1]}; } std::list @@ -479,18 +473,16 @@ StickerDatabase::GetUniqueStickers() AtScopeExit(s) { sqlite3_reset(s); }; - ExecuteForEach(s, [&s, &result]() { - result.emplace_back((const char*)sqlite3_column_text(s, 0), - (const char*)sqlite3_column_text(s, 1)); - }); + + for (const auto &row : GenerateRows(*s)) + result.emplace_back(row[0], row[1]); + return result; } -void -StickerDatabase::Names(void (*func)(const char *value, void *user_data), void *user_data) +Co::Generator +StickerDatabase::Names() { - assert(func != nullptr); - sqlite3_stmt *const s = stmt[STICKER_SQL_NAMES]; assert(s != nullptr); @@ -498,16 +490,13 @@ StickerDatabase::Names(void (*func)(const char *value, void *user_data), void *u sqlite3_reset(s); }; - ExecuteForEach(s, [s, func, user_data](){ - func((const char*)sqlite3_column_text(s, 0), user_data); - }); + for (const auto &row : GenerateRows(*s)) + co_yield row[0]; } -void -StickerDatabase::NamesTypes(const char *type, void (*func)(const char *value, const char *type, void *user_data), void *user_data) +Co::Generator +StickerDatabase::NamesTypes(const char *type) { - assert(func != nullptr); - sqlite3_stmt *const s = type == nullptr ? stmt[STICKER_SQL_NAMES_TYPES] : stmt[STICKER_SQL_NAMES_TYPES_BY_TYPE]; @@ -520,10 +509,8 @@ StickerDatabase::NamesTypes(const char *type, void (*func)(const char *value, co sqlite3_reset(s); }; - ExecuteForEach(s, [s, func, user_data](){ - func((const char*)sqlite3_column_text(s, 0), - (const char*)sqlite3_column_text(s, 1), user_data); - }); + for (const auto &row : GenerateRows(*s)) + co_yield NameTypeRecord{row[0], row[1]}; } void diff --git a/src/sticker/Database.hxx b/src/sticker/Database.hxx index 8aedfb99ed..41ac4e7f2f 100644 --- a/src/sticker/Database.hxx +++ b/src/sticker/Database.hxx @@ -1,7 +1,23 @@ // SPDX-License-Identifier: GPL-2.0-or-later // Copyright The Music Player Daemon Project -/* +#pragma once + +#include "Match.hxx" +#include "lib/sqlite/Database.hxx" +#include "protocol/RangeArg.hxx" + +#include + +#include +#include +#include + +namespace Co { template class Generator; } +class Path; +struct Sticker; + +/** * This is the sticker database library. It is the backend of all the * sticker code in MPD. * @@ -22,23 +38,6 @@ * ... * */ - -#ifndef MPD_STICKER_DATABASE_HXX -#define MPD_STICKER_DATABASE_HXX - -#include "Match.hxx" -#include "lib/sqlite/Database.hxx" -#include "protocol/RangeArg.hxx" - -#include - -#include -#include -#include - -class Path; -struct Sticker; - class StickerDatabase { enum SQL { SQL_GET, @@ -165,6 +164,11 @@ public: */ Sticker Load(const char *type, const char *uri); + struct FindRecord { + const char *uri; + const char *value; + }; + /** * Finds stickers with the specified name below the specified URI. * @@ -175,22 +179,24 @@ public: * @param op the comparison operator * @param value the operand */ - void Find(const char *type, const char *base_uri, const char *name, - StickerOperator op, const char *value, - const char *sort, bool descending, RangeArg window, - void (*func)(const char *uri, const char *value, - void *user_data), - void *user_data); + Co::Generator Find(const char *type, const char *base_uri, const char *name, + StickerOperator op, const char *value, + const char *sort, bool descending, RangeArg window); /** * Uniq and sorted list of all sticker names */ - void Names(void (*func)(const char *value, void *user_data), void *user_data); + Co::Generator Names(); + + struct NameTypeRecord { + const char *value; + const char *type; + }; /** * Uniq and sorted list of all sticker names by type */ - void NamesTypes(const char *type, void (*func)(const char *value, const char *type, void *user_data), void *user_data); + Co::Generator NamesTypes(const char *type); using StickerTypeUriPair = std::pair; @@ -221,5 +227,3 @@ private: StickerOperator op, const char *value, const char *sort, bool descending, RangeArg window); }; - -#endif diff --git a/src/sticker/SongSticker.cxx b/src/sticker/SongSticker.cxx index b7b695d8c4..31b61a4430 100644 --- a/src/sticker/SongSticker.cxx +++ b/src/sticker/SongSticker.cxx @@ -6,7 +6,9 @@ #include "Database.hxx" #include "song/LightSong.hxx" #include "db/Interface.hxx" +#include "co/Generator.hxx" #include "util/AllocatedString.hxx" +#include "util/StringCompare.hxx" #include #include @@ -75,64 +77,35 @@ sticker_song_get(StickerDatabase &db, const LightSong &song) return db.Load("song", uri.c_str()); } -namespace { -struct sticker_song_find_data { - const Database *db; - const char *base_uri; - size_t base_uri_length; - - void (*func)(const LightSong &song, const char *value, - void *user_data); - void *user_data; -}; -} // namespace - -static void -sticker_song_find_cb(const char *uri, const char *value, void *user_data) -{ - auto *data = - (struct sticker_song_find_data *)user_data; - - if (memcmp(uri, data->base_uri, data->base_uri_length) != 0) - /* should not happen, ignore silently */ - return; - - const Database *db = data->db; - try { - const LightSong *song = db->GetSong(uri); - data->func(*song, value, data->user_data); - db->ReturnSong(song); - } catch (...) { - } -} - -void +Co::Generator sticker_song_find(StickerDatabase &sticker_database, const Database &db, const char *base_uri, const char *name, StickerOperator op, const char *value, - const char *sort, bool descending, RangeArg window, - void (*func)(const LightSong &song, const char *value, - void *user_data), - void *user_data) + const char *sort, bool descending, RangeArg window) { - struct sticker_song_find_data data; - data.db = &db; - data.func = func; - data.user_data = user_data; + std::string_view base_uri_sv{base_uri}; AllocatedString allocated; - data.base_uri = base_uri; - if (*data.base_uri != 0) { + if (!base_uri_sv.empty()) { /* append slash to base_uri */ - allocated = AllocatedString{std::string_view{data.base_uri}, "/"sv}; - data.base_uri = allocated.c_str(); + allocated = AllocatedString{base_uri_sv, "/"sv}; + base_uri = allocated.c_str(); + base_uri_sv = base_uri; } else { /* searching in root directory - no trailing slash */ } - data.base_uri_length = strlen(data.base_uri); - - sticker_database.Find("song", data.base_uri, name, op, value, - sort, descending, window, - sticker_song_find_cb, &data); + for (const auto &i : sticker_database.Find("song", base_uri, name, op, value, + sort, descending, window)) { + if (!StringStartsWith(i.uri, base_uri_sv)) + /* should not happen, ignore silently */ + continue; + + try { + const LightSong *song = db.GetSong(i.uri); + co_yield FindSongStickerRecord{*song, i.value}; + db.ReturnSong(song); + } catch (...) { + } + } } diff --git a/src/sticker/SongSticker.hxx b/src/sticker/SongSticker.hxx index 92fbe52c69..fbae34430c 100644 --- a/src/sticker/SongSticker.hxx +++ b/src/sticker/SongSticker.hxx @@ -1,14 +1,14 @@ // SPDX-License-Identifier: GPL-2.0-or-later // Copyright The Music Player Daemon Project -#ifndef MPD_SONG_STICKER_HXX -#define MPD_SONG_STICKER_HXX +#pragma once #include "Match.hxx" #include "protocol/RangeArg.hxx" #include +namespace Co { template class Generator; } struct LightSong; struct Sticker; class Database; @@ -88,6 +88,11 @@ sticker_song_delete_value(StickerDatabase &db, Sticker sticker_song_get(StickerDatabase &db, const LightSong &song); +struct FindSongStickerRecord { + const LightSong &song; + const char *value; +}; + /** * Finds stickers with the specified name below the specified * directory. @@ -99,13 +104,8 @@ sticker_song_get(StickerDatabase &db, const LightSong &song); * @param base_uri the base directory to search in * @param name the name of the sticker */ -void +Co::Generator sticker_song_find(StickerDatabase &sticker_database, const Database &db, const char *base_uri, const char *name, StickerOperator op, const char *value, - const char *sort, bool descending, RangeArg window, - void (*func)(const LightSong &song, const char *value, - void *user_data), - void *user_data); - -#endif + const char *sort, bool descending, RangeArg window); diff --git a/test/co/TestGenerator.cxx b/test/co/TestGenerator.cxx new file mode 100644 index 0000000000..99e4a7232c --- /dev/null +++ b/test/co/TestGenerator.cxx @@ -0,0 +1,78 @@ +// SPDX-License-Identifier: BSD-2-Clause +// author: Max Kellermann + +#include "co/Generator.hxx" + +#include + +#include + +#ifdef __clang__ +#pragma GCC diagnostic ignored "-Wunreachable-code-loop-increment" +#endif + +static Co::Generator +Empty() +{ + co_return; +} + +TEST(Generator, Empty) +{ + for ([[maybe_unused]] int _ : Empty()) + FAIL(); +} + +static Co::Generator +CountTo3() +{ + co_yield 1; + co_yield 2; + co_yield 3; +} + +TEST(Generator, Three) +{ + int expected = 0; + for (int i : CountTo3()) + EXPECT_EQ(i, ++expected); + + EXPECT_EQ(expected, 3); +} + +struct Exception {}; + +static Co::Generator +ThrowAfter2() +{ + co_yield 1; + co_yield 2; + throw Exception{}; +} + +TEST(Generator, Throw) +{ + int expected = 0; + + try { + for (int i : ThrowAfter2()) + EXPECT_EQ(i, ++expected); + FAIL(); + } catch (const Exception &) { + } + + EXPECT_EQ(expected, 2); +} + +TEST(Generator, Break) +{ + int expected = 0; + + for (int i : CountTo3()) { + EXPECT_EQ(i, ++expected); + if (i == 2) + break; + } + + EXPECT_EQ(expected, 2); +} diff --git a/test/co/meson.build b/test/co/meson.build new file mode 100644 index 0000000000..64423006f7 --- /dev/null +++ b/test/co/meson.build @@ -0,0 +1,11 @@ +test( + 'TestCo', + executable( + 'TestCo', + 'TestGenerator.cxx', + include_directories: inc, + dependencies: [ + gtest_dep, + ], + ), +) diff --git a/test/meson.build b/test/meson.build index 84c52b0a31..5b736da56f 100644 --- a/test/meson.build +++ b/test/meson.build @@ -3,6 +3,7 @@ gtest_dep = dependency('gtest', main: true, fallback: ['gtest', 'gtest_main_dep']) subdir('util') +subdir('co') subdir('net') subdir('time') subdir('tag')