diff --git a/.clang-format b/.clang-format new file mode 100644 index 0000000..90bc40e --- /dev/null +++ b/.clang-format @@ -0,0 +1,7 @@ +--- +Language: Cpp +BasedOnStyle: Google + +IncludeBlocks: Preserve +IndentPPDirectives: AfterHash +InsertNewlineAtEOF: On diff --git a/.clang-tidy b/.clang-tidy new file mode 100644 index 0000000..db55850 --- /dev/null +++ b/.clang-tidy @@ -0,0 +1,48 @@ +Checks: + - core* + - cppcoreguidelines-* + - modernize* + - bugprone-* + - performance-* + - readability-* + - portability-* + - clang-diagnostic-* + - misc-* + - google-build-namespaces + - google-build-using-namespace + - google-default-arguments + - google-explicit-constructor + - google-readability-casting + - -modernize-use-trailing-return-type + - -modernize-use-nodiscard + - -modernize-concat-nested-namespaces + - -modernize-use-default-member-init + - -modernize-use-ranges + - -modernize-use-designated-initializers + - -misc-non-private-member-variables-in-classes + - -misc-use-internal-linkage + - -misc-use-anonymous-namespace + - -misc-include-cleaner + - -misc-static-assert + - -cppcoreguidelines-pro-type-vararg + - -cppcoreguidelines-non-private-member-variables-in-classes + - -cppcoreguidelines-special-member-functions + - -cppcoreguidelines-pro-type-reinterpret-cast + - -cppcoreguidelines-pro-type-union-access + - -cppcoreguidelines-pro-bounds-pointer-arithmetic + - -cppcoreguidelines-pro-type-member-init + - -cppcoreguidelines-owning-memory + - -cppcoreguidelines-avoid-const-or-ref-data-members + - -cppcoreguidelines-avoid-c-arrays + - -cppcoreguidelines-macro-usage + - -cppcoreguidelines-pro-bounds-constant-array-index + - -cppcoreguidelines-pro-bounds-array-to-pointer-decay + - -cppcoreguidelines-pro-type-static-cast-downcast + - -bugprone-easily-swappable-parameters + - -bugprone-assert-side-effect + - -readability-named-parameter + - -readability-identifier-length + +CheckOptions: + performance-move-const-arg.CheckTriviallyCopyableMove: false + cppcoreguidelines-avoid-do-while.IgnoreMacros: true diff --git a/.github/workflows/ci-cd-tests.yaml b/.github/workflows/ci-cd-tests.yaml new file mode 100644 index 0000000..4556fa9 --- /dev/null +++ b/.github/workflows/ci-cd-tests.yaml @@ -0,0 +1,107 @@ +# This starter workflow is for a CMake project running on multiple platforms. There is a different starter workflow if you just want a single platform. +# See: https://github.com/actions/starter-workflows/blob/main/ci/cmake-single-platform.yml +name: CMake + +on: + push: + branches: ["main"] + pull_request: + branches: ["main"] + + #Allows you to start workflow manually from the actions tab in the interface github.com + workflow_dispatch: + +jobs: + #CMake buildss + build: + name: ${{ matrix.config.name }} + runs-on: ${{ matrix.config.os }} + defaults: + run: + shell: ${{ matrix.config.shell }} {0} + + strategy: + # Set fail-fast to false to ensure that feedback is delivered for all matrix combinations. Consider changing this to true when your workflow is stable. + fail-fast: false + + matrix: + config: + - { + name: "Windows MSVC x64", + os: windows-latest, + shell: "powershell", + cmake_args: '-G "Visual Studio 18 2026" -A x64', + } + - { + name: "Windows MSVC x86", + os: windows-latest, + shell: "powershell", + cmake_args: '-G "Visual Studio 18 2026" -A Win32', + } + - { + name: "Windows MinGW", + os: windows-latest, + shell: "msys2", + cmake_args: "-GNinja", + } + - { + name: "Ubuntu GCC", + os: ubuntu-latest, + shell: "bash", + cmake_args: '-G "Unix Makefiles" -DCMAKE_C_COMPILER=gcc -DCMAKE_CXX_COMPILER=g++', + jobs_count: 4, + } + - { + name: "Ubuntu Clang", + os: ubuntu-latest, + shell: "bash", + cmake_args: '-G "Unix Makefiles" -DCMAKE_C_COMPILER=clang -DCMAKE_CXX_COMPILER=clang++', + jobs_count: 4, + } + - { + name: "macOS Clang", + os: macos-latest, + shell: "bash", + cmake_args: '-G "Unix Makefiles" -DCMAKE_C_COMPILER=gcc -DCMAKE_CXX_COMPILER=g++', + jobs_count: 4, + } + + steps: + - name: Set Windows enviroment + if: ${{ (matrix.config.os == 'windows-latest' && matrix.config.shell == 'powershell') }} + uses: ilammy/msvc-dev-cmd@v1 + + - name: Set MinGW enviroment + if: ${{ (matrix.config.os == 'windows-latest' && matrix.config.shell == 'msys2') }} + uses: msys2/setup-msys2@v2 + with: + msystem: MINGW64 + install: >- + git + base-devel + pacboy: >- + cmake:p + ninja:p + gcc:p + update: true + + - uses: actions/checkout@v4 + with: + submodules: recursive + + # Configure CMake in a 'build' subdirectory. `CMAKE_BUILD_TYPE` is only required if you are using a single-configuration generator such as make. + # See https://cmake.org/cmake/help/latest/variable/CMAKE_BUILD_TYPE.html?highlight=cmake_build_type + - name: Configure CMake + run: > + cmake -B build + ${{ matrix.config.cmake_args }} + -S ./ + + - name: Build + # Build your program with the given configuration. Note that --config is needed because the default Windows generator is a multi-config generator (Visual Studio generator). + run: cmake --build build --config Release --parallel ${{ matrix.config.jobs_count }} + + - name: Test + # Execute tests defined by the CMake configuration. Note that --build-config is needed because the default Windows generator is a multi-config generator (Visual Studio generator). + # See https://cmake.org/cmake/help/latest/manual/ctest.1.html for more detail + run: ctest --test-dir build --build-config Release --output-on-failure --parallel diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 0000000..3ab8416 --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,70 @@ +# Aether Objects Guide + +## Library + +`aether-objects` is a C++20 static library for persistent object graphs. It +provides object identities, domains, registration, serialization and persistence +interfaces, reference-counted pointers, and RAM or standard-filesystem storage. +Public headers are installed from `src/aether-objects`, and the library target is +`aether-objects` (alias `aether::objects`). + +## Coding Style and Contributor Agreement + +- Follow the repository `.clang-format` configuration (Google C++ style) and + format repository-owned C++ sources and headers before submitting changes. +- Preserve the C++20 language level, dependency versions, public include root, + target identity, and installation layout unless a change explicitly requires + them. +- Public header guards use `AETHER_OBJECTS__H_`, derived from the path + beneath `src/aether-objects`; closing comments must match the guard. +- Keep includes minimal. Preserve intentional exported or transitive includes + with their IWYU annotations. +- Raw pointers express nullable or non-owning access, never ownership. Use + `Ptr`, `PtrView`, and `ObjPtr` according to their established + lifetime contracts. +- Keep serialization formats, object IDs, ownership and reference tracking, + persistence behavior, and threading behavior compatible. +- Use `LOG_` macros for local debug logging. +- Manage dependencies through CPM; do not vendor, edit, or copy dependency + sources without explicit approval. + +## Modules + +### Pointer (`ptr`) + +`Ptr` supplies reference-counted ownership with reference-tree tracking to +reclaim cycles. `PtrView` is a nullable non-owning view and must be locked or +loaded before retaining or dereferencing its object. Avoid unnecessary `Ptr` +copies because releasing one can traverse the reference graph. + +### Objects and Domains (`obj`) + +Objects provide durable IDs, versions, registration, typed object pointers, and +domain membership. Domains coordinate object lookup, loading, saving, and +registration through the persistence interface. A valid `ObjPtr` can designate +an unloaded object; load it and retain the loaded pointer while using it. + +### Domain Storage (`domain_storage`) + +`IDomainStorage` defines persistence operations used by domains. The available +implementations are in-memory RAM storage and standard-filesystem storage. +Preserve their serialized representation and error behavior. + +### Logging (`log.h`) + +`LOG_` is the local debug logging boundary. It writes formatted messages in +debug builds unless `AE_NO_DEBUG_LOG` disables it, and compiles away in release +builds. Preserve this behavior. + +## Tests and Dependencies + +- Tests use Unity and are organized under `tests/` by subsystem. Test suites + expose a global `int test_()` entry and use the existing naming style. +- Configure builds with CMake in a separate build directory. Build and run + configured tests with `cmake --build --parallel` and + `ctest --test-dir --output-on-failure`. +- Use the configured build's `compile_commands.json` for changed-file + clang-tidy analysis. Treat findings that require public API, ownership, + persistence, threading, or platform changes as requiring review. +- Generated files, build outputs, and third-party dependency sources are not + formatted or edited. diff --git a/CMakeLists.txt b/CMakeLists.txt new file mode 100644 index 0000000..5af21e2 --- /dev/null +++ b/CMakeLists.txt @@ -0,0 +1,92 @@ +# Copyright 2026 Aethernet Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +cmake_minimum_required(VERSION 3.16) + +set(CMAKE_CXX_STANDARD 20) +set(CMAKE_CXX_STANDARD_REQUIRED ON) + +project(aether-objects VERSION 1.0.0 LANGUAGES CXX) + +# define is the project is the current root project +# root project builds tests and defines installation targets by default +if( ${CMAKE_PROJECT_NAME} STREQUAL ${PROJECT_NAME}) + set(IS_ROOT_PROJECT ON) +else() + set(IS_ROOT_PROJECT OFF) +endif() + +option(AE_BUILD_TESTS "Build tests" ${IS_ROOT_PROJECT} ) +option(AE_INSTALL "Install aether-objects library" ${IS_ROOT_PROJECT} ) +option(AE_NO_DEBUG_LOG "Do not print debug logs" !${IS_ROOT_PROJECT} ) + +include(cmake/CPM.cmake) + +CPMAddPackage( + NAME aether-miscpp + GIT_REPOSITORY "https://github.com/aethernetio/aether-miscpp.git" + GIT_TAG "main" + OPTIONS "AE_INSTALL ${AE_INSTALL}" "AE_BUILD_TESTS ${AE_BUILD_TESTS}" + EXCLUDE_FROM_ALL FALSE +) + + +add_library(${PROJECT_NAME} STATIC) +add_library(aether::objects ALIAS ${PROJECT_NAME}) + +target_sources(${PROJECT_NAME} PRIVATE + src/aether-objects/ptr/ptr.cpp + src/aether-objects/ptr/ptr_view.cpp + src/aether-objects/ptr/ref_tree.cpp + + src/aether-objects/obj/domain.cpp + src/aether-objects/obj/obj.cpp + src/aether-objects/obj/obj_id.cpp + src/aether-objects/obj/obj_ptr_base.cpp + src/aether-objects/obj/registry.cpp + + src/aether-objects/domain_storage/file_system_std_storage.cpp + src/aether-objects/domain_storage/ram_domain_storage.cpp +) + +target_include_directories(${PROJECT_NAME} PUBLIC + $ + $) + +target_link_libraries(${PROJECT_NAME} PUBLIC aether::miscpp) + +if(AE_NO_DEBUG_LOG) + target_compile_definitions(${PROJECT_NAME} PUBLIC "AE_NO_DEBUG_LOG=1") +endif() + +if(AE_BUILD_TESTS) + enable_testing() + add_subdirectory("tests") +endif() + +if(AE_INSTALL) + include(GNUInstallDirs) + + # define the installation target, install lib headers and export cmake config + install(TARGETS ${PROJECT_NAME} + EXPORT ${PROJECT_NAME}Targets + ) + + install(DIRECTORY src/aether-objects DESTINATION ${CMAKE_INSTALL_INCLUDEDIR}) + + install(EXPORT ${PROJECT_NAME}Targets + FILE ${PROJECT_NAME}Targets.cmake + DESTINATION ${CMAKE_INSTALL_LIBDIR}/cmake/${PROJECT_NAME} + ) +endif() diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..261eeb9 --- /dev/null +++ b/LICENSE @@ -0,0 +1,201 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/cmake/CPM.cmake b/cmake/CPM.cmake new file mode 100644 index 0000000..a4d7934 --- /dev/null +++ b/cmake/CPM.cmake @@ -0,0 +1,24 @@ +# SPDX-License-Identifier: MIT +# +# SPDX-FileCopyrightText: Copyright (c) 2019-2023 Lars Melchior and contributors + +set(CPM_DOWNLOAD_VERSION 0.42.3) +set(CPM_HASH_SUM "a609e875fd532b067174250f6abbc3dac22fe2d64869783fb1e80bda1625c844") + +if(CPM_SOURCE_CACHE) + set(CPM_DOWNLOAD_LOCATION "${CPM_SOURCE_CACHE}/cpm/CPM_${CPM_DOWNLOAD_VERSION}.cmake") +elseif(DEFINED ENV{CPM_SOURCE_CACHE}) + set(CPM_DOWNLOAD_LOCATION "$ENV{CPM_SOURCE_CACHE}/cpm/CPM_${CPM_DOWNLOAD_VERSION}.cmake") +else() + set(CPM_DOWNLOAD_LOCATION "${CMAKE_BINARY_DIR}/cmake/CPM_${CPM_DOWNLOAD_VERSION}.cmake") +endif() + +# Expand relative path. This is important if the provided path contains a tilde (~) +get_filename_component(CPM_DOWNLOAD_LOCATION ${CPM_DOWNLOAD_LOCATION} ABSOLUTE) + +file(DOWNLOAD + https://github.com/cpm-cmake/CPM.cmake/releases/download/v${CPM_DOWNLOAD_VERSION}/CPM.cmake + ${CPM_DOWNLOAD_LOCATION} EXPECTED_HASH SHA256=${CPM_HASH_SUM} +) + +include(${CPM_DOWNLOAD_LOCATION}) diff --git a/src/aether-objects/domain_storage/file_system_std_storage.cpp b/src/aether-objects/domain_storage/file_system_std_storage.cpp new file mode 100644 index 0000000..32cb7ed --- /dev/null +++ b/src/aether-objects/domain_storage/file_system_std_storage.cpp @@ -0,0 +1,202 @@ +/* + * Copyright 2026 Aethernet Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include "aether-objects/domain_storage/file_system_std_storage.h" + +#if defined AE_FILE_SYSTEM_STD_ENABLED + +# include +# include +# include +# include +# include +# include + +# include "aether-objects/log.h" + +namespace ae { + +class FstreamStorageWriter final : public IDomainStorageWriter { + public: + FstreamStorageWriter(DomainQuery q, std::ofstream&& f) + : query{std::move(q)}, file{std::move(f)} {} + + ~FstreamStorageWriter() override { + file.close(); + LOG_("Saved object id={}, class id={}, version={}, size={}", query.id, + query.class_id, static_cast(query.version), written_size); + } + + seri::SeriResult Write(seri::SizeWriteTag data) override { + auto const u_size = static_cast(data.size); + return Write(seri::DataTag{u_size}); + } + + seri::SeriResult Write(seri::DataWriteTag data) override { + file.write(reinterpret_cast(data.data), + static_cast(data.size)); + if (file.fail()) { + return Error{seri::write_error}; + } + written_size += data.size; + return Ok{seri::good}; + } + + private: + DomainQuery query; + std::ofstream file; + std::size_t written_size{}; +}; + +class FstreamStorageReader final : public IDomainStorageReader { + public: + explicit FstreamStorageReader(std::ifstream&& f) : file{std::move(f)} {} + ~FstreamStorageReader() override { file.close(); } + + seri::SeriResult Read(seri::SizeReadTag data) override { + std::uint32_t u_size{}; + TRY_RESULT(Read(seri::DataTag{u_size})); + data.size = static_cast(u_size); + return Ok{seri::good}; + } + + seri::SeriResult Read(seri::DataReadTag data) override { + if (file.eof()) { + return Error{seri::read_eof}; + } + + file.read(reinterpret_cast(data.data), + static_cast(data.size)); + if (file.bad()) { + return Error{seri::read_error}; + } + if (file.gcount() != static_cast(data.size)) { + return Error{file.eof() ? seri::read_eof : seri::read_error}; + } + return Ok{seri::good}; + } + + private: + std::ifstream file; +}; + +FileSystemStdStorage::FileSystemStdStorage() = default; + +FileSystemStdStorage::~FileSystemStdStorage() = default; + +std::unique_ptr FileSystemStdStorage::Store( + DomainQuery const& query) { + auto class_dir = std::filesystem::path("state") / + std::to_string(query.id.id()) / + std::to_string(query.class_id); + + std::filesystem::create_directories(class_dir); + auto version_data_path = class_dir / std::to_string(query.version); + std::ofstream f(version_data_path, + std::ios::out | std::ios::binary | std::ios::trunc); + + return std::make_unique(query, std::move(f)); +} + +ClassList FileSystemStdStorage::Enumerate(const ae::ObjId& obj_id) { + // collect unique classes + std::set classes; + + auto state_dir = std::filesystem::path{"state"}; + auto ec = std::error_code{}; + auto obj_dir = state_dir / std::to_string(obj_id.id()); + for (auto const& class_dir : + std::filesystem::directory_iterator(obj_dir, ec)) { + auto file_name = class_dir.path().filename().string(); + classes.insert(static_cast(std::stoul(file_name))); + } + LOG_("Enumerated classes {}", classes); + + if (ec) { + LOG_("Unable to open directory with error {}", ec.message()); + } + + return ClassList{classes.begin(), classes.end()}; +} + +DomainLoad FileSystemStdStorage::Load(DomainQuery const& query) { + auto object_dir = + std::filesystem::path("state") / std::to_string(query.id.id()); + auto ec = std::error_code{}; + if (!std::filesystem::exists(object_dir, ec)) { + return {DomainLoadResult::kEmpty, {}}; + } + + auto is_dir_empty = [&]() { + auto iter = std::filesystem::directory_iterator{object_dir}; + return std::filesystem::begin(iter) == std::filesystem::end(iter); + }; + if (is_dir_empty()) { + return {DomainLoadResult::kRemoved, {}}; + } + + auto class_dir = object_dir / std::to_string(query.class_id); + auto version_data_path = class_dir / std::to_string(query.version); + std::ifstream f(version_data_path, std::ios::in | std::ios::binary); + if (!f.good()) { + LOG_("Unable to open file {}", version_data_path.string()); + return DomainLoad{DomainLoadResult::kEmpty, {}}; + } + + LOG_("Loaded object id={}, class id={}, version={}", query.id, query.class_id, + static_cast(query.version)); + + return {DomainLoadResult::kLoaded, + std::make_unique(std::move(f))}; +} + +void FileSystemStdStorage::Remove(ae::ObjId const& obj_id) { + auto object_dir = + std::filesystem::path("state") / std::to_string(obj_id.id()); + auto ec = std::error_code{}; + if (!std::filesystem::exists(object_dir, ec)) { + std::filesystem::create_directory(object_dir); + return; + } + if (ec) { + LOG_("Unable to check if dir exists {} error {}", object_dir.string(), + ec.message()); + return; + } + + for (auto const& class_dir : + std::filesystem::directory_iterator(object_dir, ec)) { + auto ec2 = std::error_code{}; + std::filesystem::remove_all(class_dir.path(), ec2); + if (ec2) { + LOG_("Unable to remove dir {}, error {}", class_dir.path().string(), + ec2.message()); + continue; + } + LOG_("Object removed {}", obj_id); + } + if (ec) { + LOG_("Unable to open directory with error {}", ec.message()); + } +} + +void FileSystemStdStorage::CleanUp() { + std::filesystem::remove_all("state"); + LOG_("Removed all!", 0); +} +} // namespace ae + +#endif // AE_FILE_SYSTEM_STD_ENABLED diff --git a/src/aether-objects/domain_storage/file_system_std_storage.h b/src/aether-objects/domain_storage/file_system_std_storage.h new file mode 100644 index 0000000..9d169e1 --- /dev/null +++ b/src/aether-objects/domain_storage/file_system_std_storage.h @@ -0,0 +1,45 @@ +/* + * Copyright 2026 Aethernet Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#ifndef AETHER_OBJECTS_DOMAIN_STORAGE_FILE_SYSTEM_STD_STORAGE_H_ +#define AETHER_OBJECTS_DOMAIN_STORAGE_FILE_SYSTEM_STD_STORAGE_H_ + +#include + +#if (defined(__linux__) || defined(__unix__) || defined(__APPLE__) || \ + defined(__FreeBSD__) || defined(_WIN64) || defined(_WIN32)) + +# define AE_FILE_SYSTEM_STD_ENABLED 1 + +# include "aether-objects/obj/idomain_storage.h" + +namespace ae { +class FileSystemStdStorage : public IDomainStorage { + public: + FileSystemStdStorage(); + ~FileSystemStdStorage() override; + + std::unique_ptr Store( + DomainQuery const& query) override; + ClassList Enumerate(ObjId const& obj_id) override; + DomainLoad Load(DomainQuery const& query) override; + void Remove(ObjId const& obj_id) override; + void CleanUp() override; +}; +} // namespace ae + +#endif // supported desktop platforms +#endif // AETHER_OBJECTS_DOMAIN_STORAGE_FILE_SYSTEM_STD_STORAGE_H_ diff --git a/src/aether-objects/domain_storage/ram_domain_storage.cpp b/src/aether-objects/domain_storage/ram_domain_storage.cpp new file mode 100644 index 0000000..506ec6b --- /dev/null +++ b/src/aether-objects/domain_storage/ram_domain_storage.cpp @@ -0,0 +1,166 @@ +/* + * Copyright 2026 Aethernet Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include "aether-objects/domain_storage/ram_domain_storage.h" + +#include +#include +#include +#include + +#include "aether-objects/log.h" + +namespace ae { +class RamDomainStorageWriter final : public IDomainStorageWriter { + public: + RamDomainStorageWriter(DomainQuery q, RamDomainStorage& s) + : query{std::move(q)}, storage{&s} { + assert(!storage->write_lock); + } + ~RamDomainStorageWriter() override { + storage->SaveData(query, std::move(data_buffer)); + } + + seri::SeriResult Write(seri::SizeWriteTag data) override { + auto const u_size = static_cast(data.size); + return Write(seri::DataTag{u_size}); + } + + seri::SeriResult Write(seri::DataWriteTag data) override { + data_buffer.insert(std::end(data_buffer), + static_cast(data.data), + static_cast(data.data) + data.size); + return Ok{seri::good}; + } + + DomainQuery query; + RamDomainStorage* storage; + ObjectData data_buffer; +}; + +class RamDomainStorageReader final : public IDomainStorageReader { + public: + RamDomainStorageReader(ObjectData const& d, RamDomainStorage& s) + : storage{&s}, data_buffer{d.data(), d.size()} { + storage->write_lock = true; + } + ~RamDomainStorageReader() override { storage->write_lock = false; } + + seri::SeriResult Read(seri::SizeReadTag data) override { + std::uint32_t u_size{}; + TRY_RESULT(Read(seri::DataTag{u_size})); + data.size = static_cast(u_size); + return Ok{seri::good}; + } + + seri::SeriResult Read(seri::DataReadTag data) override { + if (data_buffer.size() < data.size) { + return Error{seri::read_eof}; + } + std::memcpy(data.data, data_buffer.data(), data.size); + data_buffer = data_buffer.subspan(data.size); + return Ok{seri::good}; + } + + RamDomainStorage* storage; + std::span data_buffer; +}; + +RamDomainStorage::RamDomainStorage() = default; + +RamDomainStorage::~RamDomainStorage() = default; + +std::unique_ptr RamDomainStorage::Store( + DomainQuery const& query) { + return std::make_unique(query, *this); +} + +ClassList RamDomainStorage::Enumerate(ObjId const& obj_id) { + auto obj_map_it = state.find(obj_id); + if (obj_map_it == std::end(state)) { + LOG_("Obj not found {}", obj_id); + return {}; + } + if (!obj_map_it->second) { + return {}; + } + + ClassList classes; + classes.reserve(obj_map_it->second->size()); + for (auto& [cls, _] : *obj_map_it->second) { + classes.emplace_back(cls); + } + LOG_("Enumerated for obj {} classes {}", obj_id, classes); + return classes; +} + +DomainLoad RamDomainStorage::Load(DomainQuery const& query) { + auto obj_map_it = state.find(query.id); + if (obj_map_it == std::end(state)) { + LOG_("Unable to find object id={}, class id={}, version={}", query.id, + query.class_id, static_cast(query.version)); + return {DomainLoadResult::kEmpty, {}}; + } + if (!obj_map_it->second) { + return {DomainLoadResult::kRemoved, {}}; + } + + auto class_map_it = obj_map_it->second->find(query.class_id); + if (class_map_it == std::end(*obj_map_it->second)) { + LOG_("Unable to find object id={}, class id={}, version={}", query.id, + query.class_id, static_cast(query.version)); + return {DomainLoadResult::kEmpty, {}}; + } + auto version_it = class_map_it->second.find(query.version); + if (version_it == std::end(class_map_it->second)) { + LOG_("Unable to find object id={}, class id={}, version={}", query.id, + query.class_id, static_cast(query.version)); + return {DomainLoadResult::kEmpty, {}}; + } + + LOG_("Loaded object id={}, class id={}, version={}, size={}", query.id, + query.class_id, static_cast(query.version), + version_it->second.size()); + + return {DomainLoadResult::kLoaded, + std::make_unique(version_it->second, *this)}; +} + +void RamDomainStorage::Remove(ObjId const& obj_id) { + auto obj_map_it = state.find(obj_id); + if (obj_map_it == std::end(state)) { + state.emplace(obj_id, std::nullopt); + return; + } + + obj_map_it->second.reset(); + LOG_("Removed object {}", obj_id); +} + +void RamDomainStorage::CleanUp() { state.clear(); } + +void RamDomainStorage::SaveData(DomainQuery const& query, ObjectData&& data) { + auto& objcect_classes = state[query.id]; + if (!objcect_classes) { + objcect_classes.emplace(); + } + auto& saved = (*objcect_classes)[query.class_id][query.version]; + saved = std::move(data); + LOG_("Saved object id={}, class id={}, version={}, size={}", query.id, + query.class_id, std::to_string(query.version), saved.size()); +} + +} // namespace ae diff --git a/src/aether-objects/domain_storage/ram_domain_storage.h b/src/aether-objects/domain_storage/ram_domain_storage.h new file mode 100644 index 0000000..af1c3ec --- /dev/null +++ b/src/aether-objects/domain_storage/ram_domain_storage.h @@ -0,0 +1,51 @@ +/* + * Copyright 2026 Aethernet Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#ifndef AETHER_OBJECTS_DOMAIN_STORAGE_RAM_DOMAIN_STORAGE_H_ +#define AETHER_OBJECTS_DOMAIN_STORAGE_RAM_DOMAIN_STORAGE_H_ + +#include +#include +#include + +#include "aether-objects/obj/idomain_storage.h" + +namespace ae { +class RamDomainStorage : public IDomainStorage { + public: + using Data = ObjectData; + using VersionData = std::map; + using ClassData = std::map; + using ObjClassData = std::map>; + + RamDomainStorage(); + ~RamDomainStorage() override; + + std::unique_ptr Store( + DomainQuery const& query) override; + + ClassList Enumerate(ObjId const& obj_id) override; + DomainLoad Load(DomainQuery const& query) override; + void Remove(ObjId const& obj_id) override; + void CleanUp() override; + + void SaveData(DomainQuery const& query, ObjectData&& data); + + ObjClassData state; + bool write_lock = false; +}; +} // namespace ae +#endif // AETHER_OBJECTS_DOMAIN_STORAGE_RAM_DOMAIN_STORAGE_H_ diff --git a/src/aether-objects/log.h b/src/aether-objects/log.h new file mode 100644 index 0000000..e624813 --- /dev/null +++ b/src/aether-objects/log.h @@ -0,0 +1,37 @@ +/* + * Copyright 2026 Aethernet Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +#ifndef AETHER_OBJECTS_LOG_H_ +#define AETHER_OBJECTS_LOG_H_ + +// IWYU pragma: begin_exports +#include +#include "aether-miscpp/format/format.h" +// IWYU pragma: end_exports + +#ifndef AE_NO_DEBUG_LOG +# define AE_NO_DEBUG_LOG 0 +#endif + +#if !defined NDEBUG && !AE_NO_DEBUG_LOG +# define LOG_(FORMAT_STR, ...) \ + do { \ + ::ae::Format(std::cout, FORMAT_STR "\n" __VA_OPT__(, ) __VA_ARGS__); \ + } while (false) +#else +# define LOG_(FORMAT_STR, ...) +#endif + +#endif // AETHER_OBJECTS_LOG_H_ diff --git a/src/aether-objects/obj/domain.cpp b/src/aether-objects/obj/domain.cpp new file mode 100644 index 0000000..9afd326 --- /dev/null +++ b/src/aether-objects/obj/domain.cpp @@ -0,0 +1,226 @@ +/* + * Copyright 2026 Aethernet Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include "aether-objects/obj/domain.h" + +#include + +#include "aether-objects/obj/obj.h" + +#include "aether-objects/log.h" + +namespace ae { + +DomainGraph::DomainGraph(Domain* domain) : domain(domain) { assert(domain); } + +Ptr DomainGraph::LoadRoot(ObjId obj_id) { + if (!obj_id.is_valid()) { + return {}; + } + // if already loaded + if (auto obj = domain->Find(obj_id); obj) { + return obj; + } + + auto* factory = domain->GetMostRelatedFactory(obj_id); + if (factory == nullptr) { + return {}; + } + + auto ptr = domain->ConstructObj(*factory, obj_id); + factory->load(this, ptr, obj_id); + return ptr; +} + +Ptr DomainGraph::LoadCopyImpl(ObjId ref_id, ObjId copy_id) { + if (!ref_id.is_valid() || !copy_id.is_valid()) { + return {}; + } + // if already loaded + if (auto obj = domain->Find(copy_id); obj) { + return obj; + } + + auto* factory = domain->GetMostRelatedFactory(ref_id); + if (factory == nullptr) { + assert(false); + return {}; + } + + auto ptr = domain->ConstructObj(*factory, copy_id); + // load new object with ref_id + factory->load(this, ptr, ref_id); + return ptr; +} + +void DomainGraph::SaveRoot(Ptr const& ptr, ObjId obj_id) { + if (!ptr) { + return; + } + if (auto* factory = domain->FindClassFactory(ptr->GetClassId()); + factory != nullptr) { + factory->save(this, ptr, obj_id); + } +} + +DomainLoad DomainGraph::GetReader(DomainQuery const& query) { + return domain->storage_->Load(query); +} + +std::unique_ptr DomainGraph::GetWriter( + DomainQuery const& query) { + auto writer = domain->storage_->Store(query); + assert(writer && "Writer must be created!"); + return writer; +} + +Domain::Domain(IDomainStorage& storage) + : storage_{&storage}, registry_{&Registry::GetRegistry()} {} + +Ptr Domain::ConstructObj(Factory const& factory, ObjId obj_id) { + Ptr o = factory.create(); + AddObject(obj_id, o); + o->domain = this; + o->obj_id = obj_id; + return o; +} + +bool Domain::IsLast(uint32_t class_id) const { + return registry_->relations.find(class_id) == registry_->relations.end(); +} + +bool Domain::IsExisting(uint32_t class_id) const { + return registry_->IsExisting(class_id); +} + +Ptr Domain::Find(ObjId obj_id) const { + if (auto it = id_objects_.find(obj_id.id()); it != id_objects_.end()) { + return it->second.Lock(); + } + return {}; +} + +void Domain::AddObject(ObjId id, Ptr const& obj) { + id_objects_[id.id()] = obj; +} + +void Domain::RemoveObject(Obj* ptr) { id_objects_.erase(ptr->obj_id.id()); } + +Factory* Domain::GetMostRelatedFactory(ObjId id) { + auto classes = storage_->Enumerate(id); +#ifndef NDEBUG + auto class_names = std::vector{}; + class_names.reserve(classes.size()); + for (auto cid : classes) { + class_names.emplace_back(registry_->ClassName(cid)); + } + + LOG_("For obj {} enumerated classes [{}]", id.id(), class_names); +#else + LOG_("For obj {} enumerated classes [{}]", id.id(), classes); +#endif + + // Remove all unsupported classes. + classes.erase( + std::remove_if(std::begin(classes), std::end(classes), + [this](auto const& c) { return !IsExisting(c); }), + std::end(classes)); + + if (classes.empty()) { + return nullptr; + } + + // Build inheritance chain. + // from base to derived. + std::sort(std::begin(classes), std::end(classes), + [this](auto left, auto right) { + if (registry_->GenerationDistance(right, left) > 0) { + return false; + } + if (registry_->GenerationDistance(left, right) >= 0) { + return true; + } + // All classes must be in one inheritance chain. + assert(false); + return false; + }); + + // Find the Final class for the most derived class provided and create it. + for (auto& f : registry_->factories) { + if (IsLast(f.first)) { + // check with most derived class + int distance = registry_->GenerationDistance(classes.back(), f.first); + if (distance >= 0) { + return &f.second; + } + } + } + + return nullptr; +} + +Factory* Domain::FindClassFactory(std::uint32_t class_id) { + return registry_->FindFactory(class_id); +} + +namespace seri { +SeriResult Serializer, std::string>::Deseri( + BinaryArchive& archive, Meta val) const { + std::size_t size{}; + TRY_RESULT(archive.buffer().Read(SizeTag{size})); + if (size > archive.max_container_load_size()) { + return Error{container_too_large}; + } + val.value.resize(size); + auto* data = reinterpret_cast(val.value.data()); + return archive.buffer().Read(DataReadTag{data, size}); +} + +SeriResult Serializer, std::string>::Seri( + BinaryArchive& archive, Meta val) const { + auto const size = val.value.size(); + TRY_RESULT(archive.buffer().Write(SizeTag{size})); + auto const* data = reinterpret_cast(val.value.data()); + TRY_RESULT(archive.buffer().Write(DataWriteTag{data, size})); + return Ok{good}; +} + +SeriResult +Serializer, std::vector>::Deseri( + BinaryArchive& archive, + Meta> val) const { + std::size_t size{}; + TRY_RESULT(archive.buffer().Read(SizeTag{size})); + if (size > archive.max_container_load_size()) { + return Error{container_too_large}; + } + val.value.resize(size); + auto* data = reinterpret_cast(val.value.data()); + return archive.buffer().Read(DataReadTag{data, size}); +} + +SeriResult +Serializer, std::vector>::Seri( + BinaryArchive& archive, + Meta const> val) const { + auto const size = val.value.size(); + TRY_RESULT(archive.buffer().Write(SizeTag{size})); + auto const* data = reinterpret_cast(val.value.data()); + TRY_RESULT(archive.buffer().Write(DataWriteTag{data, size})); + return Ok{good}; +} +} // namespace seri +} // namespace ae diff --git a/src/aether-objects/obj/domain.h b/src/aether-objects/obj/domain.h new file mode 100644 index 0000000..c760c88 --- /dev/null +++ b/src/aether-objects/obj/domain.h @@ -0,0 +1,315 @@ +/* + * Copyright 2026 Aethernet Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#ifndef AETHER_OBJECTS_OBJ_DOMAIN_H_ +#define AETHER_OBJECTS_OBJ_DOMAIN_H_ + +#include +#include +#include +#include +#include +#include + +#include "aether-miscpp/domain_visitor/domain_visitor.h" +#include "aether-miscpp/serialization/binary_archive.h" +#include "aether-miscpp/serialization/serialization.h" + +#include "aether-objects/ptr/ptr_view.h" + +#include "aether-objects/obj/idomain_storage.h" +#include "aether-objects/obj/obj_id.h" +#include "aether-objects/obj/registry.h" +#include "aether-objects/obj/version_iterator.h" + +namespace ae { +class Obj; +class Domain; +class DomainGraph; + +struct DomainCycleDetector { + struct Node { + bool operator==(const Node& other) const { + return id == other.id && class_id == other.class_id; + } + bool operator<(const Node& other) const { + return id < other.id || (id == other.id && class_id < other.class_id); + } + + ObjId id; + std::uint32_t class_id; + }; + + bool Add(std::uint32_t class_id, ObjId obj_id) { + auto [_, ok] = visited_nodes.insert(Node{obj_id, class_id}); + return ok; + } + + std::set visited_nodes; +}; + +struct DomainBuffer { + seri::SeriResult Write(seri::SizeWriteTag data) const { + if (writer == nullptr) { + return Error{seri::write_error}; + } + return writer->Write(data); + } + seri::SeriResult Write(seri::DataWriteTag data) const { + if (writer == nullptr) { + return Error{seri::write_error}; + } + return writer->Write(data); + } + + seri::SeriResult Read(seri::SizeReadTag data) const { + if (reader == nullptr) { + return Error{seri::read_error}; + } + return reader->Read(data); + } + seri::SeriResult Read(seri::DataReadTag data) const { + if (reader == nullptr) { + return Error{seri::read_error}; + } + return reader->Read(data); + } + + ObjId id; + DomainGraph* domain_graph{}; + IDomainStorageWriter* writer{nullptr}; + IDomainStorageReader* reader{nullptr}; +}; + +/** + * \brief Operations on graph of objects in domain. + * For load/save a new graph create a new DomainGraph. + */ +class DomainGraph { + public: + explicit DomainGraph(Domain* domain); + + // Load saved state of object. + Ptr LoadRoot(ObjId obj_id); + // Save state of object. + void SaveRoot(Ptr const& ptr, ObjId obj_id); + // Load a copy of object + template + Ptr LoadCopy(ObjId ref_id, ObjId copy_id); + + Ptr LoadCopyImpl(ObjId ref_id, ObjId copy_id); + + template + seri::SeriResult Load(T& obj, ObjId obj_id); + template + seri::SeriResult LoadVersion(Version version, T& obj, ObjId obj_id); + + template + seri::SeriResult Save(T const& obj, ObjId obj_id); + template + seri::SeriResult SaveVersion(Version version, T const& obj, ObjId obj_id); + + Domain* domain{}; + DomainCycleDetector cycle_detector{}; + + private: + DomainLoad GetReader(DomainQuery const& query); + std::unique_ptr GetWriter(DomainQuery const& query); +}; + +class Domain { + friend class DomainGraph; + + public: + explicit Domain(IDomainStorage& storage); + + // Search for the object by obj_id. + Ptr Find(ObjId obj_id) const; + + void AddObject(ObjId id, Ptr const& obj); + void RemoveObject(Obj* obj); + + private: + Ptr ConstructObj(Factory const& factory, ObjId id); + + bool IsLast(std::uint32_t class_id) const; + bool IsExisting(std::uint32_t class_id) const; + + Factory* FindClassFactory(std::uint32_t class_id); + Factory* GetMostRelatedFactory(ObjId id); + + IDomainStorage* storage_; + Registry* registry_; + + std::map> id_objects_; +}; + +template +Ptr DomainGraph::LoadCopy(ObjId ref_id, ObjId copy_id) { + return Ptr{LoadCopyImpl(ref_id, copy_id)}; +} + +template +seri::SeriResult DomainGraph::Load(T& obj, ObjId obj_id) { + if (!cycle_detector.Add(T::kClassId, obj_id)) { + return Ok{seri::good}; + } + + if constexpr (HasAnyVersionedLoad::value) { + auto result = seri::SeriResult{Ok{seri::good}}; + version_iterator( + obj, [this, obj_id, &result](auto version, auto& obj) { + if (result.IsErr()) { + return; + } + result = this->LoadVersion(version, obj, obj_id); + }); + return result; + } else { + return LoadVersion(T::kCurrentVersion, obj, obj_id); + } +} + +struct LoadVisitor { + void operator()(auto& v) { + if (res) { + res = bin_archive.Load(v); + } + } + + seri::BinaryArchive& bin_archive; + seri::SeriResult res{Ok{seri::good}}; +}; + +template +seri::SeriResult DomainGraph::LoadVersion(Version version, T& obj, + ObjId obj_id) { + auto load = GetReader({obj_id, T::kClassId, V}); + if (load.result != DomainLoadResult::kLoaded) { + return Ok{seri::good}; + } + + auto bin_archive = + seri::BinaryArchive{DomainBuffer{.id = obj_id, + .domain_graph = this, + .writer = {}, + .reader = load.reader.get()}}; + + auto load_visitor = LoadVisitor{bin_archive}; + + // if T has any versioned, it also must have Load for this version + if constexpr (HasAnyVersionedLoad::value) { + auto visitor = VersionNodeVisitor{load_visitor}; + obj.Load(version, visitor); + } else { + // load or deserialize object + domain_visitor::DomainVisit(obj, load_visitor); + } + return load_visitor.res; +} + +template +seri::SeriResult DomainGraph::Save(T const& obj, ObjId obj_id) { + if (!cycle_detector.Add(T::kClassId, obj_id)) { + return Ok{seri::good}; + } + + if constexpr (HasAnyVersionedSave::value) { + auto result = seri::SeriResult{Ok{seri::good}}; + version_iterator( + obj, [this, obj_id, &result](auto version, auto& obj) { + if (result.IsErr()) { + return; + } + result = this->SaveVersion(version, obj, obj_id); + }); + return result; + } else { + return SaveVersion(T::kCurrentVersion, obj, obj_id); + } +} + +struct SaveVisitor { + void operator()(auto const& v) { + if (res) { + res = bin_archive.Save(v); + } + } + + seri::BinaryArchive& bin_archive; + seri::SeriResult res{Ok{seri::good}}; +}; + +template +seri::SeriResult DomainGraph::SaveVersion(Version version, T const& obj, + ObjId obj_id) { + auto storage_writer = GetWriter({obj_id, T::kClassId, V}); + + auto bin_archive = + seri::BinaryArchive{DomainBuffer{.id = obj_id, + .domain_graph = this, + .writer = storage_writer.get(), + .reader = {}}}; + + auto save_visitor = SaveVisitor{bin_archive}; + + if constexpr (HasAnyVersionedSave::value) { + auto visitor = VersionNodeVisitor{save_visitor}; + obj.Save(version, visitor); + } else { + // load or deserialize object + domain_visitor::DomainVisit( + obj, save_visitor, + domain_visitor::PolicyConst{}); + } + return save_visitor.res; +} + +namespace seri { +template + requires(std::is_base_of_v) +struct Serializer, T> { + using Archive = BinaryArchive; + + SeriResult Seri(Archive& arch, Meta meta_val) const { + return arch.buffer().domain_graph->Save(meta_val.value, arch.buffer().id); + } + + SeriResult Deseri(Archive& arch, Meta meta_val) const { + return arch.buffer().domain_graph->Load(meta_val.value, arch.buffer().id); + } +}; + +template <> +struct Serializer, std::string> { + SeriResult Deseri(BinaryArchive& archive, + Meta val) const; + SeriResult Seri(BinaryArchive& archive, + Meta val) const; +}; + +template <> +struct Serializer, std::vector> { + SeriResult Deseri(BinaryArchive& archive, + Meta> val) const; + SeriResult Seri(BinaryArchive& archive, + Meta const> val) const; +}; + +} // namespace seri +} // namespace ae + +#endif // AETHER_OBJECTS_OBJ_DOMAIN_H_ diff --git a/src/aether-objects/obj/dummy_obj.h b/src/aether-objects/obj/dummy_obj.h new file mode 100644 index 0000000..71702be --- /dev/null +++ b/src/aether-objects/obj/dummy_obj.h @@ -0,0 +1,58 @@ +/* + * Copyright 2026 Aethernet Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#ifndef AETHER_OBJECTS_OBJ_DUMMY_OBJ_H_ +#define AETHER_OBJECTS_OBJ_DUMMY_OBJ_H_ + +#include "aether-objects/obj/obj.h" + +namespace ae { +/** + * \brief Use it to fill the holes in object states configured with different + * preprocessor directives + */ +class DummyObj : public Obj { + AE_OBJECT(DummyObj, Obj, 0) + + using Obj::Obj; + + public: + AE_OBJECT_REFLECT() +}; + +#define AE_DUMMY_OBJ(DERIVED) \ + protected: \ + friend class ae::Registrar; \ + friend ae::Ptr ae::MakePtr(); \ + \ + public: \ + _AE_OBJECT_FIELDS(crc32::from_literal(STR(DERIVED##dummy)).value, \ + DummyObj::kClassId, 0) \ + inline static auto registrar_ = \ + ::ae::Registrar(kClassId, kBaseClassId); \ + using Base = DummyObj; \ + using ptr = ::ae::ObjPtr; \ + Base& base_{*this}; \ + std::uint32_t GetClassId() const override { return kClassId; } \ + \ + public: \ + using DummyObj::DummyObj; \ + AE_OBJECT_REFLECT() \ + private: + +} // namespace ae + +#endif // AETHER_OBJECTS_OBJ_DUMMY_OBJ_H_ diff --git a/src/aether-objects/obj/idomain_storage.h b/src/aether-objects/obj/idomain_storage.h new file mode 100644 index 0000000..640017e --- /dev/null +++ b/src/aether-objects/obj/idomain_storage.h @@ -0,0 +1,99 @@ +/* + * Copyright 2026 Aethernet Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#ifndef AETHER_OBJECTS_OBJ_IDOMAIN_STORAGE_H_ +#define AETHER_OBJECTS_OBJ_IDOMAIN_STORAGE_H_ + +#include +#include +#include + +#include "aether-miscpp/serialization/binary_archive.h" + +#include "aether-objects/obj/obj_id.h" + +namespace ae { + +enum class DomainLoadResult : std::uint8_t { + kEmpty, //< There is not data for queury + kRemoved, //< Data for query was removed + kLoaded, //< Data loaded successfully +}; + +using ObjectData = std::vector; +using ClassList = std::vector; + +struct DomainQuery { + ObjId id; + std::uint32_t class_id; + std::uint8_t version; +}; + +// BinaryBuffer implementation for storage writer +class IDomainStorageWriter { + public: + virtual ~IDomainStorageWriter() = default; + + virtual seri::SeriResult Write(seri::SizeWriteTag data) = 0; + virtual seri::SeriResult Write(seri::DataWriteTag data) = 0; +}; + +// BinaryBuffer implementation for storage reader +class IDomainStorageReader { + public: + virtual ~IDomainStorageReader() = default; + + virtual seri::SeriResult Read(seri::SizeReadTag data) = 0; + virtual seri::SeriResult Read(seri::DataReadTag data) = 0; +}; + +struct DomainLoad { + DomainLoadResult result; + std::unique_ptr reader; +}; + +/** + * \brief IDomainFacility to store object's data. + */ +class IDomainStorage { + public: + virtual ~IDomainStorage() = default; + /** + * \brief Store an ObjectData by query. + */ + virtual std::unique_ptr Store( + DomainQuery const& query) = 0; + /** + * \brief Enumerate all classes for object. + */ + virtual ClassList Enumerate(ObjId const& obj_id) = 0; + /** + * \brief Load object data by query. + */ + virtual DomainLoad Load(DomainQuery const& query) = 0; + + /** + * \brief Remove object data by query + */ + virtual void Remove(ObjId const& obj_id) = 0; + /** + * \brief Clean up the whole storage. + */ + virtual void CleanUp() = 0; +}; +} // namespace ae + +#endif // AETHER_OBJECTS_OBJ_IDOMAIN_STORAGE_H_ diff --git a/src/aether-objects/obj/obj.cpp b/src/aether-objects/obj/obj.cpp new file mode 100644 index 0000000..3279321 --- /dev/null +++ b/src/aether-objects/obj/obj.cpp @@ -0,0 +1,44 @@ +/* + * Copyright 2026 Aethernet Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include "aether-objects/obj/obj.h" + +#include "aether-miscpp/crc.h" + +namespace ae { +Obj::Obj() = default; + +Obj::Obj(ObjProp prop) : domain{prop.domain}, obj_id{prop.id} {} + +Obj::~Obj() { + if (domain != nullptr) { + domain->RemoveObject(this); + } +} + +uint32_t Obj::GetClassId() const { return kClassId; } + +namespace domain_visitor { +std::size_t GetObjIndexImpl(Obj const* obj, std::uint32_t class_id) { + auto res = crc32::from_buffer( + reinterpret_cast(&class_id), sizeof(class_id)); + auto id = obj->obj_id.id(); + res = crc32::from_buffer(reinterpret_cast(&id), + sizeof(id), res); + return res.value; +} +} // namespace domain_visitor +} // namespace ae diff --git a/src/aether-objects/obj/obj.h b/src/aether-objects/obj/obj.h new file mode 100644 index 0000000..94a3b04 --- /dev/null +++ b/src/aether-objects/obj/obj.h @@ -0,0 +1,123 @@ +/* + * Copyright 2026 Aethernet Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +// Copyright 2016 Aether authors. All Rights Reserved. +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// http://www.apache.org/licenses/LICENSE-2.0 +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// ============================================================================= + +#ifndef AETHER_OBJECTS_OBJ_OBJ_H_ +#define AETHER_OBJECTS_OBJ_OBJ_H_ + +#include +#include + +#include "aether-miscpp/crc.h" +#include "aether-miscpp/reflect/reflect.h" + +#include "aether-objects/obj/domain.h" +#include "aether-objects/obj/obj_id.h" +#include "aether-objects/obj/obj_ptr.h" +#include "aether-objects/obj/registrar.h" // IWYU pragma: export +#include "aether-objects/obj/registry.h" + +namespace ae { +/** + * \brief Base class for all objects. + */ +class Obj { + friend class ObjectPtrBase; + + public: + using CurrentVersion = Version<0>; + using ptr = ObjPtr; + + static constexpr std::uint32_t kClassId = crc32::from_literal("Obj").value; + static constexpr std::uint32_t kBaseClassId = + crc32::from_literal("Obj").value; + + static constexpr CurrentVersion kCurrentVersion{}; + + Obj(); + explicit Obj(ObjProp prop); + virtual ~Obj(); + + virtual std::uint32_t GetClassId() const; + + AE_REFLECT(); + + Domain* domain{}; + ObjId obj_id; +}; + +namespace domain_visitor { +extern std::size_t GetObjIndexImpl(Obj const* obj, std::uint32_t class_id); + +template +struct ObjectIndex>> { + static std::size_t GetIndex(Obj const* obj) { + return GetObjIndexImpl(obj, T::kClassId); + } +}; +} // namespace domain_visitor +} // namespace ae + +#define _AE_OBJECT_FIELDS(CLASS_ID, BASE_CLASS_ID, VERSION) \ + static constexpr std::uint32_t kClassId = CLASS_ID; \ + static constexpr std::uint32_t kBaseClassId = BASE_CLASS_ID; \ + static constexpr std::uint32_t kVersion = VERSION; \ + using CurrentVersion = ::ae::Version; \ + static constexpr CurrentVersion kCurrentVersion{}; + +/** + * \brief Use it inside each derived class to register it with the object system + */ +#define AE_OBJECT(DERIVED, BASE, VERSION) \ + protected: \ + friend class ::ae::Registrar; \ + friend ::ae::Ptr ae::MakePtr(); \ + \ + public: \ + _AE_OBJECT_FIELDS(crc32::from_literal(#DERIVED).value, BASE::kClassId, \ + VERSION) \ + inline static auto registrar_ = \ + ::ae::Registrar(kClassId, kBaseClassId); \ + \ + using Base = BASE; \ + using ptr = ::ae::ObjPtr; \ + \ + Base& base_{*this}; \ + \ + std::uint32_t GetClassId() const override { return kClassId; } \ + \ + private: \ + /* add rest class's staff after */ + +/** + * \brief Obj class reflection + */ +#define AE_OBJECT_REFLECT(...) \ + AE_REFLECT(AE_REF_BASE(std::decay_t) __VA_OPT__(, ) \ + __VA_ARGS__) + +#endif // AETHER_OBJECTS_OBJ_OBJ_H_ diff --git a/src/aether-objects/obj/obj_id.cpp b/src/aether-objects/obj/obj_id.cpp new file mode 100644 index 0000000..65348bd --- /dev/null +++ b/src/aether-objects/obj/obj_id.cpp @@ -0,0 +1,30 @@ +/* + * Copyright 2026 Aethernet Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include "aether-objects/obj/obj_id.h" + +#include +#include + +namespace ae { +ObjId ObjId::GenerateUnique() { + static std::random_device dev; + static std::mt19937 rng(dev()); + static std::uniform_int_distribution dist6( + 10000, std::numeric_limits::max()); + return ObjId{static_cast(dist6(rng))}; +} +} // namespace ae diff --git a/src/aether-objects/obj/obj_id.h b/src/aether-objects/obj/obj_id.h new file mode 100644 index 0000000..33f3f61 --- /dev/null +++ b/src/aether-objects/obj/obj_id.h @@ -0,0 +1,146 @@ +/* + * Copyright 2026 Aethernet Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#ifndef AETHER_OBJECTS_OBJ_OBJ_ID_H_ +#define AETHER_OBJECTS_OBJ_OBJ_ID_H_ + +#include + +#include "aether-miscpp/format/format.h" +#include "aether-miscpp/reflect/reflect.h" +#include "aether-miscpp/serialization/serialization.h" + +namespace ae { + +class ObjId { + public: + using Type = std::uint32_t; + + static ObjId GenerateUnique(); + + constexpr ObjId() noexcept = default; + // NOLINTNEXTLINE(*explicit*) + constexpr ObjId(Type i) noexcept : id_{i} {} + + AE_REFLECT_MEMBERS(id_) + + constexpr Type id() const noexcept { return id_; } + constexpr bool is_valid() const noexcept { return id_ != 0; } + + constexpr bool operator<(ObjId const& i) const noexcept { + return id_ < i.id_; + } + constexpr bool operator!=(ObjId const& i) const noexcept { + return id_ != i.id_; + } + constexpr bool operator==(ObjId const& i) const noexcept { + return id_ == i.id_; + } + + constexpr ObjId& operator+=(Type i) noexcept { + id_ += i; + return *this; + } + constexpr ObjId operator+(Type i) const noexcept { return ObjId{id_ + i}; } + + private: + Type id_{0}; +}; + +template <> +struct Formatter : public Formatter { + template + void Format(ObjId value, FormatContext& ctx) const { + Formatter::Format(value.id(), ctx); + } +}; + +namespace seri { +template +struct Serializer { + SeriResult Seri(A& archive, Meta meta) const { + auto const id = meta.value.id(); + return archive.Save(Meta{id, meta.name}); + } + + SeriResult Deseri(A& archive, Meta meta) const { + ObjId::Type id{}; + TRY_RESULT((archive.Load(Meta{id, meta.name}))); + meta.value = ObjId{id}; + return Ok{good}; + } +}; +} // namespace seri + +class ObjFlags { + public: + using Type = std::uint8_t; + // defines for flag values + // none value + static constexpr Type kNone = 0x0; + // The object is not loaded with deserialization. Load method must be + // used for loading. + static constexpr Type kUnloadedByDefault = 0x1; + // current state the object is unloaded + static constexpr Type kUnloaded = 0x2; + + constexpr ObjFlags() noexcept = default; + // NOLINTNEXTLINE(*explicit*) + constexpr ObjFlags(Type v) noexcept : value_(v) {} + + AE_REFLECT_MEMBERS(value_) + + ObjFlags& operator=(Type v) noexcept { + value_ = v; + return *this; + } + + // NOLINTNEXTLINE(*explicit*) + constexpr operator Type() const noexcept { return value_; } + + private: + Type value_ = kNone; +}; + +template <> +struct Formatter : public Formatter { + template + void Format(ObjFlags value, FormatContext& ctx) const { + Formatter::Format( + static_cast(value), ctx); + } +}; + +namespace seri { +template +struct Serializer { + SeriResult Seri(A& archive, Meta meta) const { + ObjFlags::Type const flag = meta.value; + return archive.Save(Meta{flag, meta.name}); + } + + SeriResult Deseri(A& archive, Meta meta) const { + ObjFlags::Type flag{}; + TRY_RESULT((archive.Load(Meta{flag, meta.name}))); + meta.value = ObjFlags{flag}; + return Ok{good}; + } +}; +} // namespace seri + +} // namespace ae + +#endif // AETHER_OBJECTS_OBJ_OBJ_ID_H_ diff --git a/src/aether-objects/obj/obj_ptr.h b/src/aether-objects/obj/obj_ptr.h new file mode 100644 index 0000000..167df98 --- /dev/null +++ b/src/aether-objects/obj/obj_ptr.h @@ -0,0 +1,415 @@ +/* + * Copyright 2026 Aethernet Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#ifndef AETHER_OBJECTS_OBJ_OBJ_PTR_H_ +#define AETHER_OBJECTS_OBJ_OBJ_PTR_H_ + +#include +#include +#include +#include + +#include "aether-miscpp/domain_visitor/domain_visitor.h" // IWYU pragma: keep +#include "aether-miscpp/serialization/binary_archive.h" +#include "aether-miscpp/serialization/serialization.h" + +#include "aether-objects/obj/domain.h" +#include "aether-objects/obj/obj_id.h" +#include "aether-objects/obj/obj_ptr_base.h" +#include "aether-objects/obj/registry.h" +#include "aether-objects/ptr/ptr.h" + +namespace ae { + +namespace obj_ptr_internal { +template +struct IsObjType : std::false_type {}; + +template +struct IsObjType> : std::true_type {}; +} // namespace obj_ptr_internal + +struct ObjProp { + Domain* domain; + ObjId id; +}; + +struct CreateWith { + CreateWith(Domain* d) : domain{d} { // NOLINT(*explicit*) + assert(d != nullptr); + } + CreateWith(Domain& d) : domain{&d} {} // NOLINT(*explicit*) + + CreateWith&& with_id(ObjId id) && { + obj_id = id; + return std::move(*this); + } + + CreateWith&& with_flags(ObjFlags f) && { + flags = f; + return std::move(*this); + } + + Domain* domain; + std::optional obj_id; + std::optional flags; +}; + +/** + * \brief A typed, non-owning view of an ObjectPtrBase cache. + * + * The referenced cache and its ObjPtr owner must outlive this view and must not + * be reset or replaced while the view is used. + */ +template +class ProxyPtr { + public: + explicit ProxyPtr(Ptr const& ptr) noexcept : ptr_{ptr} {} + ProxyPtr(Ptr&&) = delete; + ProxyPtr(Ptr const&&) = delete; + + [[nodiscard]] explicit operator bool() const noexcept { + return static_cast(ptr_); + } + + [[nodiscard]] T* get() const noexcept { return static_cast(ptr_.get()); } + + [[nodiscard]] T* operator->() const noexcept { + assert(ptr_ && "Dereferencing uninitialized ProxyPtr"); + return get(); + } + + [[nodiscard]] T& operator*() const noexcept { + assert(ptr_ && "Dereferencing uninitialized ProxyPtr"); + return *get(); + } + + // Materializing Ptr is intentional when the caller needs ownership. + template + requires(std::is_convertible_v) + // NOLINTNEXTLINE(*explicit*) + operator Ptr() const noexcept { + return Ptr{ptr_}; + } + + private: + Ptr const& ptr_; +}; + +template +concept AbleToCast = std::is_base_of_v || std::is_base_of_v; + +template +class ObjPtr : public ObjectPtrBase { + template + friend class ObjPtr; + + friend struct seri::Serializer, ObjPtr>; + + public: + /** + * \brief Create new object with given arguments. + */ + template + static ObjPtr Create(CreateWith create_arg, TArgs&&... args); + + /** + * \brief Create new object ptr, but leave it in unloaded state. + */ + static ObjPtr Declare(CreateWith create_arg); + + /** + * \brief Make an obj ptr from the pointer to the object itself. + */ + static ObjPtr MakeFromThis(T* self); + + ObjPtr() noexcept : ObjectPtrBase{} {} + + ObjPtr(Domain* domain, ObjId obj_id, ObjFlags flags) + : ObjectPtrBase{domain, obj_id, flags} {} + + ObjPtr(Domain* domain, ObjId obj_id, ObjFlags flags, Ptr&& ptr) noexcept + : ObjectPtrBase{domain, obj_id, flags, std::move(ptr)} {} + + ObjPtr(Domain* domain, ObjId obj_id, ObjFlags flags, + Ptr const& ptr) noexcept + : ObjectPtrBase{domain, obj_id, flags, ptr} {} + + ObjPtr(ObjPtr const& ptr) noexcept = default; + ObjPtr(ObjPtr&& ptr) noexcept = default; + + template + requires(AbleToCast) + ObjPtr(ObjPtr ptr) noexcept // NOLINT(*explicit*) + : ObjectPtrBase{std::move(static_cast(ptr))} {} + + ObjPtr& operator=(ObjPtr const& ptr) noexcept = default; + ObjPtr& operator=(ObjPtr&& ptr) noexcept = default; + + template + requires(AbleToCast) + ObjPtr& operator=(ObjPtr ptr) noexcept { + ObjectPtrBase::operator=(std::move(static_cast(ptr))); + return *this; + } + + using ObjectPtrBase::domain; + using ObjectPtrBase::flags; + using ObjectPtrBase::id; + using ObjectPtrBase::is_loaded; + using ObjectPtrBase::is_valid; + using ObjectPtrBase::Reset; + using ObjectPtrBase::Save; + using ObjectPtrBase::SetFlags; + + T* operator->() &; + T* operator->() const&; + T* operator->() &&; + T* operator->() const&&; + T& operator*(); + T& operator*() const; + + explicit operator bool() const { return is_valid() && is_loaded(); } + + /** + * \brief Load current object to Ptr + */ + ProxyPtr Load() &; // NOLINT(*shadowing*) + ProxyPtr Load() const&; // NOLINT(*shadowing*) + Ptr Load() &&; // NOLINT(*shadowing*) + Ptr Load() const&&; // NOLINT(*shadowing*) + /** + * \brief Clone current object into new object + * \param obj_id Optional object ID to use for the new object, if not provided + * will be generated. + */ + ObjPtr Clone() const; + ObjPtr Clone(ObjId obj_id) const; + + template + auto WithLoaded(Func&& func) -> decltype(auto); + template + auto WithLoaded(Func&& func) const -> decltype(auto); +}; + +template +template +ObjPtr ObjPtr::Create(CreateWith create_arg, TArgs&&... args) { + static_assert(obj_ptr_internal::IsObjType::value, + "T Must be an object type"); + auto* domain = create_arg.domain; + auto obj_id = create_arg.obj_id.value_or(ObjId::GenerateUnique()); + auto flags = create_arg.flags.value_or(ObjFlags{}); + static_assert( + std::is_constructible_v, + "Class must be constructible with passed arguments and Domain*"); + auto ptr = MakePtr(ObjProp{domain, obj_id}, std::forward(args)...); + domain->AddObject(obj_id, ptr); + + return ObjPtr(domain, obj_id, flags, std::move(ptr)); +} + +template +ObjPtr ObjPtr::Declare(CreateWith create_arg) { + static_assert(obj_ptr_internal::IsObjType::value, + "T Must be an object type"); + auto* domain = create_arg.domain; + auto obj_id = create_arg.obj_id.value_or(ObjId::GenerateUnique()); + auto flags = create_arg.flags.value_or(ObjFlags::kUnloaded); + return ObjPtr(domain, obj_id, flags); +} + +template +ObjPtr ObjPtr::MakeFromThis(T* self) { + static_assert(obj_ptr_internal::IsObjType::value, + "T Must be an object type"); + auto id = self->obj_id; + auto* domain = self->domain; + assert(id.is_valid() && (domain != nullptr) && "Object must be in domain"); + auto ptr = MakePtrFromThis(self); + assert(ptr && "Object must be valid"); + return ObjPtr(domain, id, {}, std::move(ptr)); +} + +template +T* ObjPtr::operator->() & { + auto ptr = Load(); + assert(ptr && "Dereferencing invalid object"); + return ptr.get(); +} + +template +T* ObjPtr::operator->() const& { + auto ptr = Load(); + assert(ptr && "Dereferencing invalid object"); + return ptr.get(); +} + +template +T* ObjPtr::operator->() && { + auto ptr = std::move(*this).Load(); + assert(ptr && "Dereferencing invalid object"); + return ptr.get(); +} + +template +T* ObjPtr::operator->() const&& { + auto ptr = std::move(*this).Load(); + assert(ptr && "Dereferencing invalid object"); + return ptr.get(); +} + +template +T& ObjPtr::operator*() { + auto ptr = Load(); + assert(ptr && "Dereferencing invalid object"); + return *ptr; +} + +template +T& ObjPtr::operator*() const { + auto ptr = Load(); + assert(ptr && "Dereferencing invalid object"); + return *ptr; +} + +template +ProxyPtr ObjPtr::Load() & { + return ProxyPtr{ObjectPtrBase::LoadCached()}; +} + +template +ProxyPtr ObjPtr::Load() const& { + return ProxyPtr{ObjectPtrBase::LoadCached()}; +} + +template +Ptr ObjPtr::Load() && { + return Ptr{ObjectPtrBase::LoadCached()}; +} + +template +Ptr ObjPtr::Load() const&& { + return Ptr{ObjectPtrBase::LoadCached()}; +} + +template +ObjPtr ObjPtr::Clone() const { + return Clone(ObjId::GenerateUnique()); +} + +template +ObjPtr ObjPtr::Clone(ObjId obj_id) const { + assert(is_valid() && "Invalid clone ptr"); + assert(obj_id.is_valid() && "Invalid object ID"); + auto ptr = DomainGraph{domain()}.template LoadCopy(id(), obj_id); + return ObjPtr{domain(), obj_id, + static_cast(flags() & ~ObjFlags::kUnloaded), + std::move(ptr)}; +} + +template +auto WithLoadedImpl(U&& obj_ptr, Func&& func) -> decltype(auto) { + auto ptr = std::forward(obj_ptr).Load(); + using InvokeRes = decltype(std::invoke(std::forward(func), ptr)); + using ReturnType = std::conditional_t, bool, + std::optional>; + if (!ptr) { + return ReturnType{}; + } + if constexpr (std::is_void_v) { + std::invoke(std::forward(func), ptr); + return true; + } else { + return ReturnType{std::invoke(std::forward(func), ptr)}; + } +} + +template +template +auto ObjPtr::WithLoaded(Func&& func) -> decltype(auto) { + return WithLoadedImpl(*this, std::forward(func)); +} + +template +template +auto ObjPtr::WithLoaded(Func&& func) const -> decltype(auto) { + return WithLoadedImpl(*this, std::forward(func)); +} + +} // namespace ae + +namespace ae::seri { +template +struct Serializer, ObjPtr> { + using Archive = BinaryArchive; + + SeriResult Seri(Archive& archive, Meta const> meta) const { + return archive.Save(static_cast(meta.value)); + } + + SeriResult Deseri(Archive& archive, Meta> meta) const { + return archive.Load(static_cast(meta.value)); + } +}; +} // namespace ae::seri + +namespace ae::domain_visitor { +template +struct NodeVisitor> : NodeVisitor> { + using Policy = PolicyMatch; + using Base = NodeVisitor>; + void Visit(ae::ObjPtr& obj_ptr, CycleDetector& cycle_detector, + PtrRefDnv const& visitor) const { + Base::Visit(obj_ptr.cached(), cycle_detector, visitor); + } + + void Visit(ae::ObjPtr const& obj_ptr, CycleDetector& cycle_detector, + PtrRefDnv const& visitor) const { + Base::Visit(obj_ptr.cached(), cycle_detector, visitor); + } + + template + requires(!std::is_same_v>) + void Visit(ae::ObjPtr& obj_ptr, CycleDetector& cycle_detector, + Visitor&& visitor) const { + auto proxy = ae::ProxyPtr{obj_ptr.cached()}; + if (proxy) { + ApplyVisit(*proxy, cycle_detector, std::forward(visitor)); + } + } + + template + requires(!std::is_same_v>) + void Visit(ae::ObjPtr const& obj_ptr, CycleDetector& cycle_detector, + Visitor&& visitor) const { + auto proxy = ae::ProxyPtr{obj_ptr.cached()}; + if (proxy) { + ApplyVisit(*proxy, cycle_detector, std::forward(visitor)); + } + } + + private: + template + void ApplyVisit(U&& obj, CycleDetector& cycle_detector, + Visitor&& visitor) const { + domain_visitor::ApplyVisitor(std::forward(obj), cycle_detector, + std::forward(visitor)); + } +}; +} // namespace ae::domain_visitor + +#endif // AETHER_OBJECTS_OBJ_OBJ_PTR_H_ diff --git a/src/aether-objects/obj/obj_ptr_base.cpp b/src/aether-objects/obj/obj_ptr_base.cpp new file mode 100644 index 0000000..1ceea09 --- /dev/null +++ b/src/aether-objects/obj/obj_ptr_base.cpp @@ -0,0 +1,154 @@ +/* + * Copyright 2026 Aethernet Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include "aether-objects/obj/obj_ptr_base.h" + +#include + +#include "aether-objects/obj/domain.h" +#include "aether-objects/obj/obj.h" +#include "aether-objects/obj/obj_id.h" + +namespace ae { + +ObjectPtrBase::ObjectPtrBase() + : domain_{nullptr}, flags_{ObjFlags::kUnloaded} {} + +ObjectPtrBase::ObjectPtrBase(Domain* domain, ObjId obj_id, ObjFlags flags) + : domain_{domain}, id_{obj_id}, flags_{flags} {} + +ObjectPtrBase::ObjectPtrBase(Domain* domain, ObjId obj_id, ObjFlags flags, + Ptr ptr) noexcept + : domain_{domain}, id_{obj_id}, flags_{flags}, cached_{std::move(ptr)} {} + +ObjectPtrBase::ObjectPtrBase(ObjectPtrBase const& ptr) noexcept + : domain_{ptr.domain_}, id_{ptr.id_}, flags_{ptr.flags_} { + if (ptr.cached_) { + cached_ = ptr.cached_; + } +} + +ObjectPtrBase::ObjectPtrBase(ObjectPtrBase&& ptr) noexcept + : domain_{ptr.domain_}, id_{ptr.id_}, flags_{ptr.flags_} { + if (ptr.cached_) { + cached_ = std::move(ptr.cached_); + } +} + +ObjectPtrBase::~ObjectPtrBase() = default; + +ObjId ObjectPtrBase::id() const { return id_; } +ObjFlags ObjectPtrBase::flags() const { return flags_; } +Domain* ObjectPtrBase::domain() const { return domain_; } + +void ObjectPtrBase::SetFlags(ObjFlags flags) { flags_ = flags; } + +bool ObjectPtrBase::is_valid() const { return id_.is_valid(); } + +bool ObjectPtrBase::is_loaded() const { return static_cast(cached_); } + +Ptr const& ObjectPtrBase::LoadCached() { + if (cached_) { + return cached_; + } + if (!is_valid()) { + return cached_; + } + cached_ = DomainGraph{domain_}.LoadRoot(id_); + if (cached_) { + flags_ = flags_ & ~ObjFlags::kUnloaded; + } + return cached_; +} + +Ptr const& ObjectPtrBase::LoadCached() const { + return const_cast(this)->LoadCached(); // NOLINT(*const-cast) +} + +void ObjectPtrBase::Save() const { + if (cached_) { + DomainGraph{domain_}.SaveRoot(cached_, id_); + } +} + +void ObjectPtrBase::Reset() { + cached_.Reset(); + flags_ = flags_ & ObjFlags::kUnloaded; +} + +Ptr const& ObjectPtrBase::cached() const { return cached_; } +Ptr& ObjectPtrBase::cached() { return cached_; } + +ObjectPtrBase& ObjectPtrBase::operator=(ObjectPtrBase const& ptr) noexcept { + if (this != &ptr) { + domain_ = ptr.domain_; + id_ = ptr.id_; + flags_ = ptr.flags_; + if (ptr.cached_) { + cached_ = ptr.cached_; + } else { + cached_.Reset(); + } + } + return *this; +} + +ObjectPtrBase& ObjectPtrBase::operator=(ObjectPtrBase&& ptr) noexcept { + if (this != &ptr) { + domain_ = ptr.domain_; + id_ = ptr.id_; + flags_ = ptr.flags_; + + // Ptr move assignment swaps storage after resetting its destination. Move + // through a temporary and clear this cache first so ptr cannot receive the + // cache previously held by this ObjectPtrBase. + auto cache = std::move(ptr.cached_); + cached_ = Ptr{}; + cached_ = std::move(cache); + } + return *this; +} + +namespace seri { +using ObjPtrBaseSerializer = + Serializer, ObjectPtrBase>; + +SeriResult ObjPtrBaseSerializer::Seri(Archive& archive, + Meta meta) { + TRY_RESULT((archive.buffer().Write(DataTag{meta.value.id_}))); + TRY_RESULT((archive.buffer().Write(DataTag{meta.value.flags_}))); + if (meta.value.cached_) { + archive.buffer().domain_graph->SaveRoot(meta.value.cached_, meta.value.id_); + } + return Ok{seri::good}; +} + +SeriResult ObjPtrBaseSerializer::Deseri(Archive& archive, + Meta meta) { + meta.value.domain_ = archive.buffer().domain_graph->domain; + TRY_RESULT((archive.buffer().Read(DataTag{meta.value.id_}))); + TRY_RESULT((archive.buffer().Read(DataTag{meta.value.flags_}))); + meta.value.cached_.Reset(); + if (meta.value.is_valid() && + (meta.value.flags_ & ObjFlags::kUnloadedByDefault) == 0 && + (meta.value.flags_ & ObjFlags::kUnloaded) == 0) { + meta.value.cached_ = + archive.buffer().domain_graph->LoadRoot(meta.value.id_); + } + return Ok{seri::good}; +} +} // namespace seri +} // namespace ae diff --git a/src/aether-objects/obj/obj_ptr_base.h b/src/aether-objects/obj/obj_ptr_base.h new file mode 100644 index 0000000..d2fe4f2 --- /dev/null +++ b/src/aether-objects/obj/obj_ptr_base.h @@ -0,0 +1,81 @@ +/* + * Copyright 2026 Aethernet Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#ifndef AETHER_OBJECTS_OBJ_OBJ_PTR_BASE_H_ +#define AETHER_OBJECTS_OBJ_OBJ_PTR_BASE_H_ + +#include "aether-miscpp/serialization/serialization.h" + +#include "aether-objects/obj/domain.h" +#include "aether-objects/obj/obj_id.h" +#include "aether-objects/ptr/ptr.h" + +namespace ae { + +class Obj; + +class ObjectPtrBase { + friend struct seri::Serializer, + ObjectPtrBase>; + + public: + ObjectPtrBase(); + ObjectPtrBase(Domain* domain, ObjId obj_id, ObjFlags flags); + ObjectPtrBase(Domain* domain, ObjId obj_id, ObjFlags flags, + Ptr ptr) noexcept; + + ObjectPtrBase(ObjectPtrBase const& ptr) noexcept; + ObjectPtrBase(ObjectPtrBase&& ptr) noexcept; + ObjectPtrBase& operator=(ObjectPtrBase const& ptr) noexcept; + ObjectPtrBase& operator=(ObjectPtrBase&& ptr) noexcept; + ~ObjectPtrBase(); + + ObjId id() const; + ObjFlags flags() const; + Domain* domain() const; + + void SetFlags(ObjFlags flags); + + bool is_valid() const; + bool is_loaded() const; + Ptr const& LoadCached(); + Ptr const& LoadCached() const; + void Save() const; + void Reset(); + + Ptr const& cached() const; + Ptr& cached(); + + protected: + Domain* domain_; + ObjId id_; + ObjFlags flags_; + Ptr cached_; +}; + +namespace seri { +template <> +struct Serializer, ObjectPtrBase> { + using Archive = BinaryArchive; + + static SeriResult Seri(Archive& archive, Meta meta); + + static SeriResult Deseri(Archive& archive, Meta meta); +}; +} // namespace seri +} // namespace ae + +#endif // AETHER_OBJECTS_OBJ_OBJ_PTR_BASE_H_ diff --git a/src/aether-objects/obj/registrar.h b/src/aether-objects/obj/registrar.h new file mode 100644 index 0000000..e1ed48c --- /dev/null +++ b/src/aether-objects/obj/registrar.h @@ -0,0 +1,77 @@ +/* + * Copyright 2026 Aethernet Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#ifndef AETHER_OBJECTS_OBJ_REGISTRAR_H_ +#define AETHER_OBJECTS_OBJ_REGISTRAR_H_ + +#include + +#include "aether-miscpp/meta/type_index.h" + +#include "aether-objects/obj/domain.h" +#include "aether-objects/obj/registry.h" +#include "aether-objects/ptr/ptr.h" + +namespace ae { +template +class Registrar { + public: + Registrar(std::uint32_t cls_id, std::uint32_t base_id) noexcept { + Registry::GetRegistry().RegisterClass(cls_id, base_id, + { + Factory::CreateFunc(&Create), + Factory::LoadFunc(&Load), + Factory::SaveFunc(&Save), +#ifndef NDEBUG + std::string{GetTypeName()}, + cls_id, + base_id, +#endif // NDEBUG + }); + } + + private: + // like std::is_default_constructible but with respect to Registrar<-Ojb + // friendship + template + struct IsDefaultConstructible : std::false_type {}; + template + struct IsDefaultConstructible> + : std::true_type {}; + + static Ptr Create() { + if constexpr (std::is_abstract_v) { + assert(false && "Create called on abstract class"); + return {}; + } else { + static_assert(IsDefaultConstructible::value, + "AE_OBJECT class should be default constructible"); + return MakePtr(); + } + } + + static void Load(DomainGraph* domain_graph, Ptr& obj, ObjId obj_id) { + auto* self_ptr = static_cast(obj.get()); + domain_graph->Load(*self_ptr, obj_id); + } + + static void Save(DomainGraph* domain_graph, Ptr const& obj, ObjId id) { + auto const* self_ptr = static_cast(obj.get()); + domain_graph->Save(*self_ptr, id); + } +}; +} // namespace ae +#endif // AETHER_OBJECTS_OBJ_REGISTRAR_H_ diff --git a/src/aether-objects/obj/registry.cpp b/src/aether-objects/obj/registry.cpp new file mode 100644 index 0000000..7d93a62 --- /dev/null +++ b/src/aether-objects/obj/registry.cpp @@ -0,0 +1,126 @@ +/* + * Copyright 2026 Aethernet Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include "aether-objects/obj/registry.h" + +#include + +#include "aether-miscpp/crc.h" + +#include "aether-objects/log.h" + +namespace ae { +Registry& Registry::GetRegistry() { + static Registry registry; + return registry; +} + +void Registry::RegisterClass(uint32_t cls_id, std::uint32_t base_id, + Factory&& factory) { +#ifndef NDEBUG + // Fixme: Commented out to fix the build crash in MinGW + /*std::cout << "Registering class " << factory.class_name << " id " << cls_id + << " with base " << base_id << std::endl;*/ + + // TODO: move into compil-time. + // TODO: check at compile-time that the base_id to be existing class. + // Visual Studio 2017 has a bug with multiple static inline members + // initialization + // https://developercommunity.visualstudio.com/t/multiple-initializations-of-inline-static-data-mem/261624 + // Refer to + // https://learn.microsoft.com/en-us/cpp/overview/compiler-versions?view=msvc-170 +# if !defined(_MSC_VER) || _MSC_VER >= 1920 + auto no_duplication = factories.find(cls_id) == factories.end(); + assert(no_duplication && "Duplicate class id in registry"); +# endif // !defined(_MSC_VER) || _MSC_VER >= 1920 +#endif // !NDEBUG + factories.emplace(cls_id, std::move(factory)); + // TODO: maybe remove this check + if (base_id != crc32::from_literal("Obj").value) { + relations[base_id].push_back(cls_id); + } +} + +bool Registry::IsExisting(uint32_t class_id) { + return factories.find(class_id) != factories.end(); +} + +int Registry::GenerationDistanceInternal(std::uint32_t base_id, + std::uint32_t derived_id) { + auto d = relations.find(base_id); + // The base class is final. + if (d == relations.end()) { + return -1; + } + + for (auto& c : d->second) { + if (derived_id == c) { + return 1; + } + + int distance = GenerationDistanceInternal(c, derived_id); + if (distance >= 0) { + return distance + 1; + } + } + + return -1; +} + +int Registry::GenerationDistance(std::uint32_t base_id, + std::uint32_t derived_id) { + if (!IsExisting(base_id) || !IsExisting(derived_id)) { + return -1; + } + + if (base_id == derived_id) { + return 0; + } + + return GenerationDistanceInternal(base_id, derived_id); +} + +Factory* Registry::FindFactory(std::uint32_t base_id) { + auto it = factories.find(base_id); + if (it == factories.end()) { + return nullptr; + } + return &it->second; +} + +#ifndef NDEBUG +std::string_view Registry::ClassName(std::uint32_t class_id) { + if (class_id == crc32::from_literal("Obj").value) { + return "ae::Obj"; + } + auto* f = FindFactory(class_id); + if (f == nullptr) { + return "Unknown"; + } + return f->class_name; +} +#endif // !NDEBUG + +void Registry::Log() { +#ifndef NDEBUG + for (const auto& c : factories) { + LOG_("name {}, id {}, base_id {}", c.second.class_name, c.second.cls_id, + c.second.base_id); + } +#endif // !NDEBUG +} + +} // namespace ae diff --git a/src/aether-objects/obj/registry.h b/src/aether-objects/obj/registry.h new file mode 100644 index 0000000..45de85a --- /dev/null +++ b/src/aether-objects/obj/registry.h @@ -0,0 +1,80 @@ +/* + * Copyright 2026 Aethernet Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#ifndef AETHER_OBJECTS_OBJ_REGISTRY_H_ +#define AETHER_OBJECTS_OBJ_REGISTRY_H_ + +#include +#include +#include +#include + +#include "aether-objects/obj/obj_id.h" +#include "aether-objects/ptr/ptr.h" + +namespace ae { +class Obj; +class Domain; +class DomainGraph; + +struct Factory { + using CreateFunc = Ptr (*)(); + using LoadFunc = void (*)(DomainGraph* domain_graph, Ptr& obj, ObjId id); + using SaveFunc = void (*)(DomainGraph* domain_graph, Ptr const& obj, + ObjId id); + + CreateFunc create; + LoadFunc load; + SaveFunc save; +#ifndef NDEBUG + std::string class_name{}; + std::uint32_t cls_id{}; + std::uint32_t base_id{}; +#endif // !NDEBUG +}; + +class Registry { + public: + using Relations = std::unordered_map>; + using Factories = std::unordered_map; + + static Registry& GetRegistry(); + + void RegisterClass(std::uint32_t cls_id, std::uint32_t base_id, + Factory&& factory); + void Log(); + bool IsExisting(std::uint32_t class_id); + + int GenerationDistanceInternal(std::uint32_t base_id, + std::uint32_t derived_id); + + // Calculates distance from base to derived in generations: + // -1 - derived is not inherited directly or indirectly from base or any + // class doesn't exist. + int GenerationDistance(std::uint32_t base_id, std::uint32_t derived_id); + + Factory* FindFactory(std::uint32_t base_id); + +#ifndef NDEBUG + std::string_view ClassName(std::uint32_t class_id); +#endif // !NDEBUG + + Relations relations; + Factories factories; +}; +} // namespace ae + +#endif // AETHER_OBJECTS_OBJ_REGISTRY_H_ diff --git a/src/aether-objects/obj/version_iterator.h b/src/aether-objects/obj/version_iterator.h new file mode 100644 index 0000000..2c1c4d9 --- /dev/null +++ b/src/aether-objects/obj/version_iterator.h @@ -0,0 +1,203 @@ +/* + * Copyright 2026 Aethernet Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#ifndef AETHER_OBJECTS_OBJ_VERSION_ITERATOR_H_ +#define AETHER_OBJECTS_OBJ_VERSION_ITERATOR_H_ + +#include +#include +#include +#include + +#include "aether-miscpp/meta/index_sequence.h" + +namespace ae { +using VersionValueType = std::uint8_t; + +// use max version for compilation time optimization +// increase max version count if required +#ifdef MAX_OBJECT_VERSION +inline constexpr VersionValueType kMaxVersion = MAX_OBJECT_VERSION; +#else +inline constexpr VersionValueType kMaxVersion = 24; +#endif + +// Helper version tag for versioned function overloading +template + requires(V <= kMaxVersion) +struct Version : public std::integral_constant {}; + +// Traits for check supported versioned functionality + +struct Dnv; + +template +struct VersionLoadTrait { + template + struct Has : std::false_type {}; + template + struct Has().Load( + Version{}, std::declval()))>> : std::true_type {}; + + static constexpr bool value = Has::value; +}; + +template +struct VersionSaveTrait { + template + struct Has : std::false_type {}; + template + struct Has().Save( + Version{}, std::declval()))>> : std::true_type {}; + + static constexpr bool value = Has::value; +}; + +/** + * \brief Calculate min and max version supported by T and tested with + * VersionTrait + */ +template typename VersionTrait> +struct VersionBounds { + template + using i_seq = std::integer_sequence; + + template + static consteval auto CalcVersionBounds(i_seq) + -> std::pair { + constexpr auto arr = std::array{VersionTrait::value...}; + VersionValueType min{}; + VersionValueType max{}; + std::size_t i = 0; + for (; i < arr.size(); ++i) { + if (arr[i]) { + break; + } + } + if (i == arr.size()) { + return {0, 0}; + } + + min = static_cast(i); + + for (; i < arr.size(); ++i) { + if (!arr[i]) { + break; + } + } + max = static_cast(i - 1); + return {min, max}; + } + + static constexpr auto value = CalcVersionBounds( + std::make_integer_sequence()); +}; + +template typename VersionTrait> +struct HasAnyVersioned { + template + static consteval bool TestAny( + std::integer_sequence const&) { + return (VersionTrait::value || ...); + } + + static constexpr auto version_bounds = VersionBounds::value; + static constexpr bool value = + TestAny(make_range_sequence()); +}; + +template +struct HasAnyVersionedLoad : public HasAnyVersioned {}; +template +static constexpr inline bool HasAnyVersionedLoad_v = + HasAnyVersionedLoad::value; + +template +struct HasAnyVersionedSave : public HasAnyVersioned {}; +template +static constexpr inline bool HasAnyVersionedSave_v = + HasAnyVersionedSave::value; + +/** + * \brief Iterate for each version of the object for that VersionTrait returns + * true + * \tparam VersionTrait Trait to check if version is supported + * \tparam T Object type + * \tparam TFunc Function to call for each version with signature + * void(Version, T&) + * \param t Object to iterate + * \param func Function to call for each version + */ +template