From 3431757601a638850edfe43b51090b730fe0d53f Mon Sep 17 00:00:00 2001 From: alex2772 Date: Fri, 6 Mar 2026 16:14:41 +0300 Subject: [PATCH 01/15] coroutine adjustments --- aui.core/src/AUI/Thread/AFuture.h | 56 +++++++++++++++---- .../src/AUI/Image/jpg/JpgImageLoader.cpp | 9 +++ aui.image/src/AUI/Image/jpg/JpgImageLoader.h | 2 + aui.json/src/AUI/Json/Conversion.h | 19 +++++++ 4 files changed, 75 insertions(+), 11 deletions(-) diff --git a/aui.core/src/AUI/Thread/AFuture.h b/aui.core/src/AUI/Thread/AFuture.h index a34b0e63e..b5b3f54a9 100644 --- a/aui.core/src/AUI/Thread/AFuture.h +++ b/aui.core/src/AUI/Thread/AFuture.h @@ -950,6 +950,7 @@ struct aui::impl::future::Future::CoPromiseType { { return std::suspend_never{}; } + auto unhandled_exception() const noexcept { future.supplyException(); } @@ -963,6 +964,32 @@ struct aui::impl::future::Future::CoPromiseType { } }; +template<> +struct aui::impl::future::Future::CoPromiseType { + AFuture future; + auto initial_suspend() const noexcept + { + return std::suspend_never{}; + } + + auto final_suspend() const noexcept + { + return std::suspend_never{}; + } + + auto unhandled_exception() const noexcept { + future.supplyException(); + } + + const AFuture& get_return_object() const noexcept { + return future; + } + + void return_void() const noexcept { + future.supplyValue(); + } +}; + template auto operator co_await(AFuture future) { struct Awaitable { @@ -972,20 +999,27 @@ auto operator co_await(AFuture future) { return future.hasResult(); } - T await_resume() { - return *future; + auto await_resume() { + if constexpr (std::is_same_v) { + *future; + } else { + return std::move(*future); + } } - - void await_suspend(std::coroutine_handle<> h) + void await_suspend(std::coroutine_handle<> handle) { - future.onSuccess([h](const int&) { - h.resume(); - }); - - future.onError([h](const AException&) { - h.resume(); - }); + // onSuccess and onError are called by supplyValue which might have been called on a different thread. + // if we keep this logic for coroutines, the callstack will grow very fast and inconsistent. + // for co_await, let's keep the caller thread. + // if user wants to magically switch threads using co_await, they'll figure out how to shoot their knee. + auto callback = [handle = std::move(handle), callerThread = AThread::current()](const auto&...) { + callerThread->enqueue([handle = std::move(handle)] { + handle.resume(); + }); + }; + future.onSuccess(callback); + future.onError(std::move(callback)); } }; diff --git a/aui.image/src/AUI/Image/jpg/JpgImageLoader.cpp b/aui.image/src/AUI/Image/jpg/JpgImageLoader.cpp index 302550bc6..c076a6c58 100644 --- a/aui.image/src/AUI/Image/jpg/JpgImageLoader.cpp +++ b/aui.image/src/AUI/Image/jpg/JpgImageLoader.cpp @@ -14,6 +14,9 @@ // #include "JpgImageLoader.h" + +#include "stb_image_write.h" + #include #include @@ -21,3 +24,9 @@ bool JpgImageLoader::matches(AByteBufferView buffer) { const uint8_t header[] = {0xff, 0xd8 }; return memcmp(header, buffer.data(), sizeof(header)) == 0; } + +void JpgImageLoader::save(aui::no_escape outputStream, AImageView image) { + stbi_write_jpg_to_func([](void *context, void *data, int size) { + reinterpret_cast(context)->write(reinterpret_cast(data), size); + }, reinterpret_cast(outputStream.ptr()), image.width(), image.height(), image.bytesPerPixel(), image.data(), image.width() * image.bytesPerPixel()); +} diff --git a/aui.image/src/AUI/Image/jpg/JpgImageLoader.h b/aui.image/src/AUI/Image/jpg/JpgImageLoader.h index 4b92ef6c3..1c6a87770 100644 --- a/aui.image/src/AUI/Image/jpg/JpgImageLoader.h +++ b/aui.image/src/AUI/Image/jpg/JpgImageLoader.h @@ -21,6 +21,8 @@ class JpgImageLoader: public StbImageLoader { public: bool matches(AByteBufferView buffer) override; + + API_AUI_IMAGE static void save(aui::no_escape outputStream, AImageView image); }; diff --git a/aui.json/src/AUI/Json/Conversion.h b/aui.json/src/AUI/Json/Conversion.h index 1b1b85022..baab42655 100644 --- a/aui.json/src/AUI/Json/Conversion.h +++ b/aui.json/src/AUI/Json/Conversion.h @@ -435,6 +435,25 @@ struct AJsonConv { } }; +template +struct AJsonConv> { + static AJson toJson(const AMap& map) { + AJson::Object object; + for (const auto&[k, v] : map) { + object[k] = aui::to_json(v); + } + return std::move(object); + } + static void fromJson(const AJson& json, AMap& dst) { + auto& object = json.asObject(); + dst.clear(); + + for (const auto&[k, v]: object) { + dst[k] = aui::from_json(v); + } + } +}; + template struct AJsonConv>> { From b82af9de064b610c9475e9922e64cffe2dd452ad Mon Sep 17 00:00:00 2001 From: alex2772 Date: Fri, 6 Mar 2026 16:15:21 +0300 Subject: [PATCH 02/15] enable coroutines by default (#125) --- aui.core/CMakeLists.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/aui.core/CMakeLists.txt b/aui.core/CMakeLists.txt index 0f95fdcca..bce7c0316 100644 --- a/aui.core/CMakeLists.txt +++ b/aui.core/CMakeLists.txt @@ -1,7 +1,7 @@ cmake_minimum_required(VERSION 3.16) option(AUI_CATCH_UNHANDLED "Catch segfault" ON) -option(AUI_COROUTINES "Use C++20 coroutines" OFF) +option(AUI_COROUTINES "Use C++20 coroutines" ON) option(AUI_ENABLE_ASAN "Enable AddressSanitizer" OFF) option(AUI_ENABLE_DEATH_TESTS "Enable GTest death tests" ON) From 8df876a240cb2c890edb5b40b52ebd81db790938 Mon Sep 17 00:00:00 2001 From: alex2772 Date: Fri, 6 Mar 2026 16:29:25 +0300 Subject: [PATCH 03/15] u --- aui.image/src/AUI/Image/jpg/JpgImageLoader.cpp | 4 ++-- aui.image/src/AUI/Image/jpg/JpgImageLoader.h | 2 +- aui.json/src/AUI/Json/Conversion.h | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/aui.image/src/AUI/Image/jpg/JpgImageLoader.cpp b/aui.image/src/AUI/Image/jpg/JpgImageLoader.cpp index c076a6c58..4eb5face2 100644 --- a/aui.image/src/AUI/Image/jpg/JpgImageLoader.cpp +++ b/aui.image/src/AUI/Image/jpg/JpgImageLoader.cpp @@ -25,8 +25,8 @@ bool JpgImageLoader::matches(AByteBufferView buffer) { return memcmp(header, buffer.data(), sizeof(header)) == 0; } -void JpgImageLoader::save(aui::no_escape outputStream, AImageView image) { +void JpgImageLoader::save(aui::no_escape outputStream, AImageView image, int quality) { stbi_write_jpg_to_func([](void *context, void *data, int size) { reinterpret_cast(context)->write(reinterpret_cast(data), size); - }, reinterpret_cast(outputStream.ptr()), image.width(), image.height(), image.bytesPerPixel(), image.data(), image.width() * image.bytesPerPixel()); + }, reinterpret_cast(outputStream.ptr()), image.width(), image.height(), image.bytesPerPixel(), image.data(), quality); } diff --git a/aui.image/src/AUI/Image/jpg/JpgImageLoader.h b/aui.image/src/AUI/Image/jpg/JpgImageLoader.h index 1c6a87770..a50d18b5f 100644 --- a/aui.image/src/AUI/Image/jpg/JpgImageLoader.h +++ b/aui.image/src/AUI/Image/jpg/JpgImageLoader.h @@ -22,7 +22,7 @@ class JpgImageLoader: public StbImageLoader { public: bool matches(AByteBufferView buffer) override; - API_AUI_IMAGE static void save(aui::no_escape outputStream, AImageView image); + API_AUI_IMAGE static void save(aui::no_escape outputStream, AImageView image, int quality = 90); }; diff --git a/aui.json/src/AUI/Json/Conversion.h b/aui.json/src/AUI/Json/Conversion.h index baab42655..bb8496836 100644 --- a/aui.json/src/AUI/Json/Conversion.h +++ b/aui.json/src/AUI/Json/Conversion.h @@ -449,7 +449,7 @@ struct AJsonConv> { dst.clear(); for (const auto&[k, v]: object) { - dst[k] = aui::from_json(v); + dst.emplace(k, aui::from_json(v)); } } }; From 1d5b5d31e17662e6a0adb548d3b6e81449223d15 Mon Sep 17 00:00:00 2001 From: alex2772 Date: Sat, 7 Mar 2026 09:25:24 +0300 Subject: [PATCH 04/15] u --- .github/workflows/build.yml | 10 ++++---- aui.core/src/AUI/Thread/AFuture.h | 2 +- aui.core/tests/CoroutinesTest.cpp | 38 ++++++++++++++++++++++++------- 3 files changed, 36 insertions(+), 14 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index a4ec67b0c..45b1888af 100755 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -255,18 +255,18 @@ jobs: wget https://apt.llvm.org/llvm.sh chmod +x llvm.sh sudo ./llvm.sh 20 all - echo "CC=clang-20" >> $GITHUB_ENV - echo "CXX=clang++-20" >> $GITHUB_ENV + echo "CC=clang" >> $GITHUB_ENV + echo "CXX=clang++" >> $GITHUB_ENV - name: Install MacOS dependencies if: runner.os == 'macOS' run: brew install cmake ninja llvm@18 - - name: Setup Clang 18 for macOS + - name: Setup Clang for macOS if: matrix.compiler == 'Clang' && runner.os == 'macOS' run: | - echo "CC=$(brew --prefix llvm@18)/bin/clang " >> $GITHUB_ENV - echo "CXX=$(brew --prefix llvm@18)/bin/clang++ " >> $GITHUB_ENV + echo "CC=$(brew --prefix llvm)/bin/clang " >> $GITHUB_ENV + echo "CXX=$(brew --prefix llvm)/bin/clang++ " >> $GITHUB_ENV - name: Setup AppleClang for macOS if: matrix.compiler == 'Default' && runner.os == 'macOS' diff --git a/aui.core/src/AUI/Thread/AFuture.h b/aui.core/src/AUI/Thread/AFuture.h index b5b3f54a9..47bc3a72d 100644 --- a/aui.core/src/AUI/Thread/AFuture.h +++ b/aui.core/src/AUI/Thread/AFuture.h @@ -578,7 +578,7 @@ namespace aui::impl::future { * AFuture longOperation(); * AFuture myFunction() { * int resultOfLongOperation = co_await longOperation(); - * return resultOfLongOperation + 1; + * co_return resultOfLongOperation + 1; * } * ``` * diff --git a/aui.core/tests/CoroutinesTest.cpp b/aui.core/tests/CoroutinesTest.cpp index 22f0c4160..cd353b2f3 100644 --- a/aui.core/tests/CoroutinesTest.cpp +++ b/aui.core/tests/CoroutinesTest.cpp @@ -13,6 +13,9 @@ #include #include "AUI/Common/AException.h" +#include "AUI/Thread/AAsyncHolder.h" +#include "AUI/Thread/AEventLoop.h" + #include #include #include @@ -31,12 +34,20 @@ AFuture longTask() { } TEST(Coroutines, CoAwait) { - auto future = []() -> AFuture { + /// [co_await1] + AAsyncHolder async; + async << []() -> AFuture<> { auto v228 = co_await longTask(); - co_return v228 + 322; + EXPECT_EQ(v228, 228); + co_return; }(); - auto v = *future; - EXPECT_EQ(v, 228 + 322); + + AEventLoop loop; + IEventLoop::Handle h(&loop); + while (async.size() > 0) { + loop.iteration(); + } + /// [co_await1] } AFuture longTaskException() { @@ -47,11 +58,22 @@ AFuture longTaskException() { } TEST(Coroutines, CoAwaitException) { - auto future = []() -> AFuture { - auto v228 = co_await longTaskException(); - co_return v228 + 322; + AAsyncHolder async; + auto future = []() -> AFuture<> { + try { + auto v228 = co_await longTaskException(); + } catch (const AException& e) { + co_return; + } + GTEST_NONFATAL_FAILURE_("exception was not reported"); }(); - EXPECT_ANY_THROW(*future); + + + AEventLoop loop; + IEventLoop::Handle h(&loop); + while (async.size() > 0) { + loop.iteration(); + } } #endif From b6afb4d37fe6cdb08c3903d066491cac39701bc2 Mon Sep 17 00:00:00 2001 From: alex2772 Date: Sat, 7 Mar 2026 09:33:08 +0300 Subject: [PATCH 05/15] u --- .github/workflows/build.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 45b1888af..67cd8274c 100755 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -265,8 +265,8 @@ jobs: - name: Setup Clang for macOS if: matrix.compiler == 'Clang' && runner.os == 'macOS' run: | - echo "CC=$(brew --prefix llvm)/bin/clang " >> $GITHUB_ENV - echo "CXX=$(brew --prefix llvm)/bin/clang++ " >> $GITHUB_ENV + echo "CC=$(brew --prefix llvm@20)/bin/clang " >> $GITHUB_ENV + echo "CXX=$(brew --prefix llvm@20)/bin/clang++ " >> $GITHUB_ENV - name: Setup AppleClang for macOS if: matrix.compiler == 'Default' && runner.os == 'macOS' From 9bc3893f8661216ca7ce98f4c560c3b23ff43da9 Mon Sep 17 00:00:00 2001 From: alex2772 Date: Sat, 7 Mar 2026 11:08:54 +0300 Subject: [PATCH 06/15] u --- .github/workflows/build.yml | 13 ++----------- 1 file changed, 2 insertions(+), 11 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 67cd8274c..680f64ea1 100755 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -245,22 +245,13 @@ jobs: if: runner.os == 'Linux' uses: awalsh128/cache-apt-pkgs-action@9f7a885e33ff6f8166b600e3e74a981ec11b92c3 with: - packages: pkg-config zlib1g-dev libssl-dev libcrypt-dev libcurl4-openssl-dev libgtk-3-dev libepoxy-dev libdbus-1-dev libfontconfig-dev ninja-build libpulse-dev + packages: pkg-config zlib1g-dev libssl-dev libcrypt-dev libcurl4-openssl-dev libgtk-3-dev libepoxy-dev libdbus-1-dev libfontconfig-dev ninja-build libpulse-dev clang execute_install_scripts: true version: 4.0 - - name: Clang Toolchain Installation for Linux - if: matrix.compiler == 'Clang' && runner.os == 'Linux' - run: | - wget https://apt.llvm.org/llvm.sh - chmod +x llvm.sh - sudo ./llvm.sh 20 all - echo "CC=clang" >> $GITHUB_ENV - echo "CXX=clang++" >> $GITHUB_ENV - - name: Install MacOS dependencies if: runner.os == 'macOS' - run: brew install cmake ninja llvm@18 + run: brew install cmake ninja llvm@20 - name: Setup Clang for macOS if: matrix.compiler == 'Clang' && runner.os == 'macOS' From 919301202c913ebdfe5ef0697f8f8cb9a5c2c417 Mon Sep 17 00:00:00 2001 From: alex2772 Date: Sat, 7 Mar 2026 12:16:31 +0300 Subject: [PATCH 07/15] u --- aui.core/CMakeLists.txt | 10 +++++++++- aui.core/tests/CoroutinesTest.cpp | 2 +- aui.core/tests/StackfulCoroutinesTest.cpp | 2 +- 3 files changed, 11 insertions(+), 3 deletions(-) diff --git a/aui.core/CMakeLists.txt b/aui.core/CMakeLists.txt index bce7c0316..a16969364 100644 --- a/aui.core/CMakeLists.txt +++ b/aui.core/CMakeLists.txt @@ -1,7 +1,15 @@ cmake_minimum_required(VERSION 3.16) option(AUI_CATCH_UNHANDLED "Catch segfault" ON) -option(AUI_COROUTINES "Use C++20 coroutines" ON) + +set(_use_coroutines ON) +if (AUI_COMPILER_CLANG) + if (CMAKE_CXX_COMPILER_VERSION VERSION_LESS 19.0.0) # compiler crashes + set(_use_coroutines OFF) + endif () +endif() + +option(AUI_COROUTINES "Use C++20 coroutines" ${_use_coroutines}) option(AUI_ENABLE_ASAN "Enable AddressSanitizer" OFF) option(AUI_ENABLE_DEATH_TESTS "Enable GTest death tests" ON) diff --git a/aui.core/tests/CoroutinesTest.cpp b/aui.core/tests/CoroutinesTest.cpp index cd353b2f3..2b9247717 100644 --- a/aui.core/tests/CoroutinesTest.cpp +++ b/aui.core/tests/CoroutinesTest.cpp @@ -50,7 +50,7 @@ TEST(Coroutines, CoAwait) { /// [co_await1] } -AFuture longTaskException() { +static AFuture longTaskException() { return AUI_THREADPOOL -> int { AThread::sleep(10ms); // long tamssk throw AException("Whoops! Something bad happened"); diff --git a/aui.core/tests/StackfulCoroutinesTest.cpp b/aui.core/tests/StackfulCoroutinesTest.cpp index 789a67963..293d5ed47 100644 --- a/aui.core/tests/StackfulCoroutinesTest.cpp +++ b/aui.core/tests/StackfulCoroutinesTest.cpp @@ -59,7 +59,7 @@ TEST(StackfulCoroutines, CoAwait) { future2.wait(AFutureWait::JUST_WAIT); } -AFuture longTaskException() { +static AFuture longTaskException() { return AUI_THREADPOOL -> int { AThread::sleep(10ms); // long tamssk throw AException("Whoops! Something bad happened"); From 9478bddc533022a144b9d37de3c811843cb1e219 Mon Sep 17 00:00:00 2001 From: alex2772 Date: Sat, 7 Mar 2026 16:04:08 +0300 Subject: [PATCH 08/15] u --- aui.core/CMakeLists.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/aui.core/CMakeLists.txt b/aui.core/CMakeLists.txt index a16969364..6df3e3b70 100644 --- a/aui.core/CMakeLists.txt +++ b/aui.core/CMakeLists.txt @@ -4,7 +4,7 @@ option(AUI_CATCH_UNHANDLED "Catch segfault" ON) set(_use_coroutines ON) if (AUI_COMPILER_CLANG) - if (CMAKE_CXX_COMPILER_VERSION VERSION_LESS 19.0.0) # compiler crashes + if (CMAKE_CXX_COMPILER_VERSION VERSION_LESS 21.0.0) # compiler crashes set(_use_coroutines OFF) endif () endif() From a78063d60e066b99de190b43f379e176a1b7f6ce Mon Sep 17 00:00:00 2001 From: alex2772 Date: Sun, 8 Mar 2026 11:22:10 +0300 Subject: [PATCH 09/15] astring view split --- aui.core/src/AUI/Common/AChar.cpp | 5 +++++ aui.core/src/AUI/Common/AChar.h | 2 ++ aui.core/src/AUI/Common/AString.cpp | 8 ++++++-- aui.core/src/AUI/Common/AString.h | 3 ++- aui.core/src/AUI/Common/AStringView.cpp | 13 +++++++------ aui.core/src/AUI/Common/AStringView.h | 3 ++- 6 files changed, 24 insertions(+), 10 deletions(-) diff --git a/aui.core/src/AUI/Common/AChar.cpp b/aui.core/src/AUI/Common/AChar.cpp index 4c4015546..b6d27c5e3 100644 --- a/aui.core/src/AUI/Common/AChar.cpp +++ b/aui.core/src/AUI/Common/AChar.cpp @@ -40,3 +40,8 @@ AStaticVector AChar::toUtf8() const noexcept { } return {}; // Invalid Unicode code point } + +std::string AChar::toString() const { + auto tmp = toUtf8(); + return std::string(tmp.begin(), tmp.end()); +} diff --git a/aui.core/src/AUI/Common/AChar.h b/aui.core/src/AUI/Common/AChar.h index da6338106..ad97078ff 100644 --- a/aui.core/src/AUI/Common/AChar.h +++ b/aui.core/src/AUI/Common/AChar.h @@ -12,6 +12,7 @@ #pragma once #include +#include template class AStaticVector; @@ -84,6 +85,7 @@ class AChar { } AStaticVector toUtf8() const noexcept; + std::string toString() const; constexpr char32_t codepoint() const noexcept { return mValue; diff --git a/aui.core/src/AUI/Common/AString.cpp b/aui.core/src/AUI/Common/AString.cpp index 67af1fccc..1fd2b07f6 100644 --- a/aui.core/src/AUI/Common/AString.cpp +++ b/aui.core/src/AUI/Common/AString.cpp @@ -575,8 +575,12 @@ void AString::erase(size_t u_pos, size_t u_count) { erase(begin() + u_pos, begin() + u_pos + u_count); } -AStringVector AString::split(AChar c) const { - return view().split(c); +AStringVector AString::split(AChar separator) const { + return view().split(separator); +} + +AStringVector AString::split(AStringView separator) const { + return view().split(separator); } // NOLINTEND(cppcoreguidelines-avoid-magic-numbers,cppcoreguidelines-pro-bounds-pointer-arithmetic) diff --git a/aui.core/src/AUI/Common/AString.h b/aui.core/src/AUI/Common/AString.h index 2044ebf77..eff4347c8 100644 --- a/aui.core/src/AUI/Common/AString.h +++ b/aui.core/src/AUI/Common/AString.h @@ -478,7 +478,8 @@ class API_AUI_CORE AString: public std::string { return view().lowercase(); } - AStringVector split(AChar c) const; + AStringVector split(AChar separator) const; + AStringVector split(AStringView separator) const; AString& replaceAll(AChar from, AChar to); diff --git a/aui.core/src/AUI/Common/AStringView.cpp b/aui.core/src/AUI/Common/AStringView.cpp index 0d9e3a3a0..da36ee753 100644 --- a/aui.core/src/AUI/Common/AStringView.cpp +++ b/aui.core/src/AUI/Common/AStringView.cpp @@ -1045,28 +1045,29 @@ bool AStringView::toBool() const { (d[3] == 'e' || d[3] == 'E'); } -AStringVector AStringView::split(AChar c) const { +AStringVector AStringView::split(AStringView separator) const { if (empty()) { return {}; } - auto utf8c = c.toUtf8(); - if (utf8c.empty()) return {}; - std::string separator_utf8(utf8c.begin(), utf8c.end()); AStringVector result; result.reserve(length() / 10); for (size_type s = 0;;) { - auto next = super::find(separator_utf8, s); + auto next = super::find(separator.bytes(), s); if (next == npos) { result << substr(s); break; } result << substr(s, next - s); - s = next + separator_utf8.length(); + s = next + separator.bytes().length(); } return result; } +AStringVector AStringView::split(AChar separator) const { + return split(AStringView(separator.toString())); +} + AString AStringView::removedAll(AChar c) { AString copy{*this}; copy.removeAll(c); diff --git a/aui.core/src/AUI/Common/AStringView.h b/aui.core/src/AUI/Common/AStringView.h index 139161e08..29a5230da 100644 --- a/aui.core/src/AUI/Common/AStringView.h +++ b/aui.core/src/AUI/Common/AStringView.h @@ -237,7 +237,8 @@ class API_AUI_CORE AStringView: public std::string_view { AString removedAll(AChar c); - AStringVector split(AChar c) const; + AStringVector split(AStringView separator) const; + AStringVector split(AChar separator) const; constexpr iterator begin() const noexcept { return AUtf8ConstIterator(data(), data(), data() + size(), 0); From 08096c1cf8e737b54c759f694aec6d7385cb15e3 Mon Sep 17 00:00:00 2001 From: alex2772 Date: Sun, 8 Mar 2026 11:22:36 +0300 Subject: [PATCH 10/15] added std::valarray json converter --- aui.json/src/AUI/Json/Conversion.h | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/aui.json/src/AUI/Json/Conversion.h b/aui.json/src/AUI/Json/Conversion.h index bb8496836..eee357a28 100644 --- a/aui.json/src/AUI/Json/Conversion.h +++ b/aui.json/src/AUI/Json/Conversion.h @@ -435,6 +435,28 @@ struct AJsonConv { } }; +template +struct AJsonConv> { + static AJson toJson(const std::valarray& v) { + AJson::Array array; + if constexpr (ranges::sized_range) { + array.reserve(v.size()); + } + for (const auto& elem : v) { + array << aui::to_json(elem); + } + return std::move(array); + } + static void fromJson(const AJson& json, std::valarray& dst) { + auto& array = json.asArray(); + dst.resize(array.size()); + size_t i = 0; + for (const auto& elem : array) { + dst[i++] = aui::from_json(elem); + } + } +}; + template struct AJsonConv> { static AJson toJson(const AMap& map) { From 5e93ef4e95014df7494ca37a8edac3971a06d343 Mon Sep 17 00:00:00 2001 From: alex2772 Date: Sun, 8 Mar 2026 11:44:03 +0300 Subject: [PATCH 11/15] json exponents support --- aui.core/src/AUI/Util/ATokenizer.cpp | 16 -------- aui.json/src/AUI/Json/Serialization.cpp | 30 ++++++++++++--- aui.json/tests/Basic.cpp | 51 +++++++++++++++++++++++++ 3 files changed, 75 insertions(+), 22 deletions(-) diff --git a/aui.core/src/AUI/Util/ATokenizer.cpp b/aui.core/src/AUI/Util/ATokenizer.cpp index 9c476dfb0..c2f62d85b 100644 --- a/aui.core/src/AUI/Util/ATokenizer.cpp +++ b/aui.core/src/AUI/Util/ATokenizer.cpp @@ -149,22 +149,6 @@ T ATokenizer::readIntImpl() { case '7': case '8': case '9': - case 'x': - case 'X': - - // hex - case 'a': - case 'A': - case 'b': - case 'B': - case 'c': - case 'C': - case 'd': - case 'D': - case 'e': - case 'E': - case 'f': - case 'F': mTemporaryAStringBuffer << c; break; case '-': diff --git a/aui.json/src/AUI/Json/Serialization.cpp b/aui.json/src/AUI/Json/Serialization.cpp index 2fa815531..fb12f80cf 100644 --- a/aui.json/src/AUI/Json/Serialization.cpp +++ b/aui.json/src/AUI/Json/Serialization.cpp @@ -90,25 +90,43 @@ static AJson read(ATokenizer& t) { unexpectedToken(s); } - case '\"': + case '\"': { AString result = t.readStringUntilUnescaped('\"'); result.replaceAll("\\\\", "\\"); return result; + } + + case '-': { // negative number? + goto minus; + break; + } } - if (isdigit(uint8_t(t.getLastCharacter())) || t.getLastCharacter() == '-') { + if (isdigit(uint8_t(t.getLastCharacter()))) { + minus: bool isMinus = t.getLastCharacter() == '-'; t.reverseByte(); auto longInt = t.readLongInt(); - if (t.readChar() == '.') { + auto c = t.readChar(); + if (c == 'e' || c == 'E') { + auto exponent = t.readLongInt(); + return static_cast(longInt) * std::pow(10.0, exponent); + } + if (c == '.') { // double auto currentColumn = t.getColumn(); auto remainder = t.readLongInt(); auto digitCount = t.getColumn() - currentColumn; - auto integer = double(longInt); + auto integer = static_cast(longInt); double s = isMinus ? -1.0 : 1.0; - return integer + double(remainder) / std::pow(10.0, digitCount - 1) * s; + double value = integer + static_cast(remainder) / std::pow(10.0, digitCount - 1) * s; + if (t.getLastCharacter() == 'e' || t.getLastCharacter() == 'E') { + t.readChar(); + auto exponent = t.readLongInt(); + value *= std::pow(10.0, exponent); + } + return value; } t.reverseByte(); int basicInt = longInt; @@ -188,4 +206,4 @@ void ASerializable::write(IOutputStream& os, const AJson& value) { void ASerializable::read(IInputStream& is, AJson& dst) { ATokenizer t(aui::ptr::fake_shared(&is)); dst = ::read(t); -} \ No newline at end of file +} diff --git a/aui.json/tests/Basic.cpp b/aui.json/tests/Basic.cpp index 6de44526b..6f8d7f2c2 100644 --- a/aui.json/tests/Basic.cpp +++ b/aui.json/tests/Basic.cpp @@ -115,7 +115,58 @@ TEST(Json, NegativeNumber) EXPECT_EQ(v["msg"].asString(), "error"); } +TEST(Json, ExponentNumber1) +{ + auto v = AJson::fromString(R"({"code":-6.94e-7,"msg":"error"})"); + EXPECT_DOUBLE_EQ(v["code"].asNumber(), -6.94e-7); + EXPECT_EQ(v["msg"].asString(), "error"); +} + +TEST(Json, ExponentNumber2) +{ + auto v = AJson::fromString(R"({"code":-6.94e7,"msg":"error"})"); + EXPECT_DOUBLE_EQ(v["code"].asNumber(), -6.94e7); + EXPECT_EQ(v["msg"].asString(), "error"); +} +TEST(Json, ExponentNumber3) +{ + auto v = AJson::fromString(R"({"code":6.94e7,"msg":"error"})"); + EXPECT_DOUBLE_EQ(v["code"].asNumber(), 6.94e7); + EXPECT_EQ(v["msg"].asString(), "error"); +} + +TEST(Json, ExponentNumber4) +{ + auto v = AJson::fromString(R"({"code":6.94e11,"msg":"error"})"); + EXPECT_DOUBLE_EQ(v["code"].asNumber(), 6.94e11); + EXPECT_EQ(v["msg"].asString(), "error"); +} + +TEST(Json, ExponentNumber5) +{ + auto v = AJson::fromString(R"({"code":6.94e-11,"msg":"error"})"); + EXPECT_DOUBLE_EQ(v["code"].asNumber(), 6.94e-11); + EXPECT_EQ(v["msg"].asString(), "error"); +} + +TEST(Json, ExponentNumber6) +{ + auto v = AJson::fromString(R"({"code":6.94e-11})"); + EXPECT_DOUBLE_EQ(v["code"].asNumber(), 6.94e-11); +} + +TEST(Json, ExponentNumber7) +{ + auto v = AJson::fromString(R"({"code":6e10})"); + EXPECT_DOUBLE_EQ(v["code"].asNumber(), 6e10); +} + +TEST(Json, ExponentNumber8) +{ + auto v = AJson::fromString(R"({"code":6E10})"); + EXPECT_DOUBLE_EQ(v["code"].asNumber(), 6e10); +} TEST(Json, Long) { From 5faac6c9528513669e72dea4e67984c55bcee6e0 Mon Sep 17 00:00:00 2001 From: alex2772 Date: Wed, 11 Mar 2026 14:48:58 +0300 Subject: [PATCH 12/15] u --- aui.core/src/AUI/Common/AStringView.cpp | 4 ++-- aui.core/src/AUI/Thread/AFuture.h | 2 +- aui.core/src/AUI/Thread/AThread.cpp | 10 ++++++++++ aui.core/src/AUI/Thread/AThread.h | 19 +++++++++++++++++++ 4 files changed, 32 insertions(+), 3 deletions(-) diff --git a/aui.core/src/AUI/Common/AStringView.cpp b/aui.core/src/AUI/Common/AStringView.cpp index da36ee753..c3090bf15 100644 --- a/aui.core/src/AUI/Common/AStringView.cpp +++ b/aui.core/src/AUI/Common/AStringView.cpp @@ -1054,11 +1054,11 @@ AStringVector AStringView::split(AStringView separator) const { for (size_type s = 0;;) { auto next = super::find(separator.bytes(), s); if (next == npos) { - result << substr(s); + result << super::substr(s); break; } - result << substr(s, next - s); + result << super::substr(s, next - s); s = next + separator.bytes().length(); } return result; diff --git a/aui.core/src/AUI/Thread/AFuture.h b/aui.core/src/AUI/Thread/AFuture.h index 47bc3a72d..969d18fcd 100644 --- a/aui.core/src/AUI/Thread/AFuture.h +++ b/aui.core/src/AUI/Thread/AFuture.h @@ -621,7 +621,7 @@ namespace aui::impl::future { * AFuture may execute the task (if not default-constructed) on the caller thread instead of waiting. See AFuture::wait * for details. */ -template +template class AFuture final: public aui::impl::future::Future { private: using super = typename aui::impl::future::Future; diff --git a/aui.core/src/AUI/Thread/AThread.cpp b/aui.core/src/AUI/Thread/AThread.cpp index 9f403f573..544133ec5 100644 --- a/aui.core/src/AUI/Thread/AThread.cpp +++ b/aui.core/src/AUI/Thread/AThread.cpp @@ -21,6 +21,8 @@ #include "AUI/Thread/AFuture.h" #include "AUI/Thread/AMutexWrapper.h" #include "IEventLoop.h" +#include "AUI/Common/ATimer.h" + #include #include #include @@ -289,3 +291,11 @@ const _& AThread::main() noexcept { static auto main = current(); // initialized by AUI_ENTRY. return main; } + +AFuture<> AThread::asyncSleep(std::chrono::milliseconds duration) { + AFuture<> future; + ATimer::scheduler().enqueue(duration, [future] { + future.supplyValue(); + }); + return future; +} diff --git a/aui.core/src/AUI/Thread/AThread.h b/aui.core/src/AUI/Thread/AThread.h index dc79675ba..21dedc90c 100644 --- a/aui.core/src/AUI/Thread/AThread.h +++ b/aui.core/src/AUI/Thread/AThread.h @@ -25,6 +25,9 @@ class IEventLoop; class AString; class AConditionVariable; +template +class AFuture; + /** * @brief Represents an abstract thread which might be not created with AThread. * @ingroup core @@ -224,9 +227,25 @@ class API_AUI_CORE AThread : public AAbstractThread, public AObject { * Most operation systems guarantee that elasped time will be greater than specified. * AThread::interrupt() is supported. * @param duration sleep duration. + * @details + * Blocking sleep. */ static void sleep(std::chrono::milliseconds duration); + /** + * @brief Sleep for specified duration. + * Most operation systems guarantee that elasped time will be greater than specified. + * AThread::interrupt() is supported. + * @param duration sleep duration. + * @details + * Async sleep. + * + * ```cpp + * co_await AThread::asyncSleep(1s); + * ``` + */ + static AFuture<> asyncSleep(std::chrono::milliseconds duration); + /** * @return current thread. */ From 4b02638e710456a0c1f3234d958c4fcfdb689d47 Mon Sep 17 00:00:00 2001 From: alex2772 Date: Wed, 11 Mar 2026 19:04:53 +0300 Subject: [PATCH 13/15] added exception handling for AAsyncHolder futures --- aui.core/src/AUI/Thread/AAsyncHolder.h | 26 ++++++++++++++++++++++++++ 1 file changed, 26 insertions(+) diff --git a/aui.core/src/AUI/Thread/AAsyncHolder.h b/aui.core/src/AUI/Thread/AAsyncHolder.h index 677c18a98..ffa0fb9cc 100644 --- a/aui.core/src/AUI/Thread/AAsyncHolder.h +++ b/aui.core/src/AUI/Thread/AAsyncHolder.h @@ -68,6 +68,14 @@ class AAsyncHolder { return f.inner().get() == impl; }); }); + + future.onError([this, impl](const AException& e) { + std::unique_lock lock(mSync); + mOnException(e); + mFutureSet.removeIf([&](const AFuture<>& f) { + return f.inner().get() == impl; + }); + }); } else { auto uniquePtr = std::make_unique>(future); auto it = mCustomTypeFutures.insert(mCustomTypeFutures.end(), std::move(uniquePtr)); @@ -80,6 +88,14 @@ class AAsyncHolder { AUI_ASSERT(!mCustomTypeFutures.empty()); mCustomTypeFutures.erase(it); }); + + future.onError([this, it](const AException& result) { + AUI_ASSERTX(!mDead, "you have concurrency issues"); + std::unique_lock lock(mSync); + mOnException(result); + AUI_ASSERT(!mCustomTypeFutures.empty()); + mCustomTypeFutures.erase(it); + }); } return *this; } @@ -111,6 +127,13 @@ class AAsyncHolder { } } + [[nodiscard]] std::function getOnException() const { return mOnException; } + + void setOnException(std::function callback) { + std::unique_lock lock(mSync); + mOnException = std::move(callback); + } + private: struct IFuture { virtual ~IFuture() = default; @@ -150,4 +173,7 @@ class AAsyncHolder { mutable AMutex mSync; AFutureSet<> mFutureSet; std::list<_unique> mCustomTypeFutures; + std::function mOnException = [](const AException& e) { + ALogger::err("AAsyncHolder") << "Unhandled exception in AFuture: " << e; + }; }; From b5186aaf5f78ad6756c04aad3992ac8d3a07d319 Mon Sep 17 00:00:00 2001 From: alex2772 Date: Thu, 2 Apr 2026 14:42:37 +0300 Subject: [PATCH 14/15] added image format conversion utilities and UnixIoThread::waitForEvent --- .../src/AUI/Platform/unix/UnixIoThread.cpp | 9 +++ aui.core/src/AUI/Platform/unix/UnixIoThread.h | 21 +++++++ aui.image/src/AUI/Image/AImage.h | 28 +++++++++- aui.image/src/AUI/Image/AImageView.h | 56 ++++++++++++++++++- .../src/AUI/Image/jpg/JpgImageLoader.cpp | 8 ++- 5 files changed, 116 insertions(+), 6 deletions(-) diff --git a/aui.core/src/AUI/Platform/unix/UnixIoThread.cpp b/aui.core/src/AUI/Platform/unix/UnixIoThread.cpp index 00f456151..8999e53d5 100644 --- a/aui.core/src/AUI/Platform/unix/UnixIoThread.cpp +++ b/aui.core/src/AUI/Platform/unix/UnixIoThread.cpp @@ -98,6 +98,15 @@ void UnixIoThread::unregisterCallback(int fd) noexcept { }); } +AFuture> UnixIoThread::waitForEvent(int fd, ABitField flags) { + AFuture> future; + registerCallback(fd, flags, [this, future, fd](ABitField event) { + unregisterCallback(fd); + future.supplyValue(event); + }); + return future; +} + UnixIoThread::UnixIoThread() noexcept: mThread(_new([&] { AThread::setName("AUI IO"); UnixIoEventLoop loop(*this); diff --git a/aui.core/src/AUI/Platform/unix/UnixIoThread.h b/aui.core/src/AUI/Platform/unix/UnixIoThread.h index 00bc3fcea..bf67fa9dd 100644 --- a/aui.core/src/AUI/Platform/unix/UnixIoThread.h +++ b/aui.core/src/AUI/Platform/unix/UnixIoThread.h @@ -52,6 +52,27 @@ class API_AUI_CORE UnixIoThread { return inst().mThread; } + /** + * @brief A simplified version of registerCallback/unregisterCallback for waiting events on file descriptor. + * @param fd file descriptor to watch on. + * @param flags which events to listen + * @return AFuture with bitfield on which events was triggerred + * @details + * ```cpp + * AAsyncHolder async; + * async << []() -> AFuture<> { + * int signalFd = signalfd(-1, &procMask, SFD_CLOEXEC); + * for (;;) { + * co_await UnixIoThread::inst().waitForEvent(signalFd, UnixPollEvent::IN); + * read(signalFd, ...); + * } + * close(signalFd); + * }; + * ``` + */ + [[nodiscard]] + AFuture> waitForEvent(int fd, ABitField flags); + private: friend class UnixIoEventLoop; _ mThread; diff --git a/aui.image/src/AUI/Image/AImage.h b/aui.image/src/AUI/Image/AImage.h index e74631ae7..26422c06c 100644 --- a/aui.image/src/AUI/Image/AImage.h +++ b/aui.image/src/AUI/Image/AImage.h @@ -146,6 +146,11 @@ class AFormattedImage : public AImage { return accumulator / (width() * height()); } + + [[nodiscard]] + AFormattedImageView view() const noexcept { + return AFormattedImageView(buffer(), size()); + } }; template @@ -154,4 +159,25 @@ auto AImage::visit(Visitor&& visitor) { static constexpr int format = std::decay_t::FORMAT; return visitor(const_cast&>(reinterpret_cast&>(image))); }); -} \ No newline at end of file +} + +template +void AImageView::convert(aui::invocable> auto&& consumer) const { + if (format() == desiredFormat) { + // easy path: format on runtime was matched with the desired one, so we pass it as it + consumer(AFormattedImageView(buffer(), size())); + return; + } + // hard path: need to convert + AFormattedImage converted(size()); + + // convert + visit([&](const auto& source) { + using source_image_t = std::decay_t; + static constexpr auto sourceFormat = (APixelFormat::Value)source_image_t::FORMAT; + std::transform(source.begin(), source.end(), converted.begin(), aui::pixel_format::convert); + }); + + // pass to the consumer. + consumer(converted.view()); +} diff --git a/aui.image/src/AUI/Image/AImageView.h b/aui.image/src/AUI/Image/AImageView.h index 523e39d36..9dfa9db9c 100644 --- a/aui.image/src/AUI/Image/AImageView.h +++ b/aui.image/src/AUI/Image/AImageView.h @@ -161,8 +161,60 @@ class API_AUI_IMAGE AImageView { [[nodiscard]] AImage resizedLinearDownscale(glm::uvec2 newSize) const; + /** + * @brief Converts (if needed) the image to the format `desiredFormat` known at compile time. The image is then + * passed to `consumer`. + * @tparam desiredFormat the image format to convert to + * @param consumer the consumer function that will accept the image view of format `desiredFormat` + * @details + * Guarantees that the image passed into consumer is in the pixel format `desiredFormat`. + * + * ## Two possible paths + * ### 1) The image is already in desiredFormat + * + * Then the function avoids any work and passes the existing image view directly to consumer. + * + * - No conversion + * - No allocation + * - Minimal overhead + * - This is the fast path. + * + * ### 2) The image is in a different format + * + * Then the function: + * + * - creates an owning image object, + * - converts the pixels into `desiredFormat`, + * - passes the converted image view to `consumer`. + * + * So the callback always receives the format it asked for. + * + * ## Why it exists + * + * This is useful when you want code that only works with one pixel format, but the input may come in several + * formats. For example, you can write an image saver that expects RGBA and let convert handle the conversion if + * needed. + * + * The function is a template on the target format, so the desired pixel format is known at compile time. That lets + * the API be type-safe and efficient. + * + * ## In short + * “Give me an image in format X, and I’ll call your callback with an image view in format X, converting only if + * necessary.” + */ + template + void convert(aui::invocable> auto && consumer) const; + + /** + * @brief Converts the image to the format `desiredFormat` known at runtime. + * @param desiredFormat the image format to convert to + * @return Owning, type-erased representation of an image in `desiredFormat` + * @details + * In contrast to the template version of `convert`, this overload is easier to use at the cost of runtime overhead. + */ [[nodiscard]] - AImage convert(APixelFormat format) const; + AImage convert(APixelFormat desiredFormat) const; + /** * @brief Shortcut to buffer().data(). @@ -182,7 +234,7 @@ class API_AUI_IMAGE AImageView { * @brief Same as AImageView but all universal AColor methods replaced with concrete specific AFormattedColor type thus * can be used by performance critical code. */ -template +template class AFormattedImageView : public AImageView { public: using Color = AFormattedColor; diff --git a/aui.image/src/AUI/Image/jpg/JpgImageLoader.cpp b/aui.image/src/AUI/Image/jpg/JpgImageLoader.cpp index 4eb5face2..a9b7e8a9e 100644 --- a/aui.image/src/AUI/Image/jpg/JpgImageLoader.cpp +++ b/aui.image/src/AUI/Image/jpg/JpgImageLoader.cpp @@ -26,7 +26,9 @@ bool JpgImageLoader::matches(AByteBufferView buffer) { } void JpgImageLoader::save(aui::no_escape outputStream, AImageView image, int quality) { - stbi_write_jpg_to_func([](void *context, void *data, int size) { - reinterpret_cast(context)->write(reinterpret_cast(data), size); - }, reinterpret_cast(outputStream.ptr()), image.width(), image.height(), image.bytesPerPixel(), image.data(), quality); + image.convert([&](AFormattedImageView converted) { + stbi_write_jpg_to_func([](void *context, void *data, int size) { + reinterpret_cast(context)->write(reinterpret_cast(data), size); + }, reinterpret_cast(outputStream.ptr()), converted.width(), converted.height(), converted.bytesPerPixel(), converted.data(), quality); + }); } From bc0f6c0839d090dffd7d3582aeac85cd669e2943 Mon Sep 17 00:00:00 2001 From: alex2772 Date: Tue, 14 Apr 2026 12:56:19 +0300 Subject: [PATCH 15/15] u --- aui.json/src/AUI/Json/Conversion.h | 3 +++ 1 file changed, 3 insertions(+) diff --git a/aui.json/src/AUI/Json/Conversion.h b/aui.json/src/AUI/Json/Conversion.h index eee357a28..a01863eb7 100644 --- a/aui.json/src/AUI/Json/Conversion.h +++ b/aui.json/src/AUI/Json/Conversion.h @@ -11,7 +11,10 @@ #pragma once +#include + #include + #include #include #include