From 6e72d213e3e04a8e7da5b2e327fe77a3b77d681f Mon Sep 17 00:00:00 2001 From: dev0 Date: Thu, 18 Dec 2025 04:42:07 +0530 Subject: [PATCH] Release v1.1.0 --- .clang-format | 13 + .clangd | 3 + .github/workflows/ci.yaml | 65 +++ .gitignore | 53 ++ .vscode/launch.json | 27 ++ .vscode/settings.json | 4 + .vscode/tasks.json | 19 + CMake/FindDeps.cmake | 116 +++++ CMake/PatchMimalloc.cmake | 25 + CMakeLists.txt | 64 +++ CMakePresets.json | 99 ++++ LICENSE | 177 +++++++ README.md | 164 +++++++ Src/CMakeLists.txt | 2 + Src/IACore/CMakeLists.txt | 71 +++ Src/IACore/imp/cpp/AsyncOps.cpp | 178 +++++++ Src/IACore/imp/cpp/DataOps.cpp | 316 ++++++++++++ Src/IACore/imp/cpp/FileOps.cpp | 281 +++++++++++ Src/IACore/imp/cpp/HttpClient.cpp | 243 ++++++++++ Src/IACore/imp/cpp/IACore.cpp | 78 +++ Src/IACore/imp/cpp/IPC.cpp | 452 +++++++++++++++++ Src/IACore/imp/cpp/JSON.cpp | 44 ++ Src/IACore/imp/cpp/Logger.cpp | 72 +++ Src/IACore/imp/cpp/ProcessOps.cpp | 337 +++++++++++++ Src/IACore/imp/cpp/SocketOps.cpp | 105 ++++ Src/IACore/imp/cpp/StreamReader.cpp | 58 +++ Src/IACore/imp/cpp/StreamWriter.cpp | 96 ++++ Src/IACore/imp/cpp/StringOps.cpp | 100 ++++ Src/IACore/inc/IACore/ADT/RingBuffer.hpp | 225 +++++++++ Src/IACore/inc/IACore/AsyncOps.hpp | 73 +++ Src/IACore/inc/IACore/DataOps.hpp | 49 ++ Src/IACore/inc/IACore/DynamicLib.hpp | 178 +++++++ Src/IACore/inc/IACore/Environment.hpp | 103 ++++ Src/IACore/inc/IACore/FileOps.hpp | 49 ++ Src/IACore/inc/IACore/HttpClient.hpp | 195 ++++++++ Src/IACore/inc/IACore/IACore.hpp | 41 ++ Src/IACore/inc/IACore/IATest.hpp | 333 +++++++++++++ Src/IACore/inc/IACore/IPC.hpp | 151 ++++++ Src/IACore/inc/IACore/JSON.hpp | 58 +++ Src/IACore/inc/IACore/Logger.hpp | 118 +++++ Src/IACore/inc/IACore/PCH.hpp | 586 +++++++++++++++++++++++ Src/IACore/inc/IACore/ProcessOps.hpp | 67 +++ Src/IACore/inc/IACore/SocketOps.hpp | 106 ++++ Src/IACore/inc/IACore/StreamReader.hpp | 103 ++++ Src/IACore/inc/IACore/StreamWriter.hpp | 65 +++ Src/IACore/inc/IACore/StringOps.hpp | 28 ++ Src/IACore/inc/IACore/Utils.hpp | 151 ++++++ Tests/CMakeLists.txt | 4 + Tests/Regression/CMakeLists.txt | 0 Tests/Subjects/CMakeLists.txt | 1 + Tests/Subjects/LongProcess/Main.cpp | 12 + Tests/Unit/CCompile.c | 39 ++ Tests/Unit/CMakeLists.txt | 37 ++ Tests/Unit/Environment.cpp | 179 +++++++ Tests/Unit/Main.cpp | 35 ++ Tests/Unit/ProcessOps.cpp | 253 ++++++++++ Tests/Unit/RingBuffer.cpp | 107 +++++ Tests/Unit/StreamReader.cpp | 176 +++++++ Tests/Unit/Utils.cpp | 222 +++++++++ logo.svg | 23 + 60 files changed, 7029 insertions(+) create mode 100644 .clang-format create mode 100644 .clangd create mode 100644 .github/workflows/ci.yaml create mode 100644 .gitignore create mode 100644 .vscode/launch.json create mode 100644 .vscode/settings.json create mode 100644 .vscode/tasks.json create mode 100644 CMake/FindDeps.cmake create mode 100644 CMake/PatchMimalloc.cmake create mode 100644 CMakeLists.txt create mode 100644 CMakePresets.json create mode 100644 LICENSE create mode 100644 README.md create mode 100644 Src/CMakeLists.txt create mode 100644 Src/IACore/CMakeLists.txt create mode 100644 Src/IACore/imp/cpp/AsyncOps.cpp create mode 100644 Src/IACore/imp/cpp/DataOps.cpp create mode 100644 Src/IACore/imp/cpp/FileOps.cpp create mode 100644 Src/IACore/imp/cpp/HttpClient.cpp create mode 100644 Src/IACore/imp/cpp/IACore.cpp create mode 100644 Src/IACore/imp/cpp/IPC.cpp create mode 100644 Src/IACore/imp/cpp/JSON.cpp create mode 100644 Src/IACore/imp/cpp/Logger.cpp create mode 100644 Src/IACore/imp/cpp/ProcessOps.cpp create mode 100644 Src/IACore/imp/cpp/SocketOps.cpp create mode 100644 Src/IACore/imp/cpp/StreamReader.cpp create mode 100644 Src/IACore/imp/cpp/StreamWriter.cpp create mode 100644 Src/IACore/imp/cpp/StringOps.cpp create mode 100644 Src/IACore/inc/IACore/ADT/RingBuffer.hpp create mode 100644 Src/IACore/inc/IACore/AsyncOps.hpp create mode 100644 Src/IACore/inc/IACore/DataOps.hpp create mode 100644 Src/IACore/inc/IACore/DynamicLib.hpp create mode 100644 Src/IACore/inc/IACore/Environment.hpp create mode 100644 Src/IACore/inc/IACore/FileOps.hpp create mode 100644 Src/IACore/inc/IACore/HttpClient.hpp create mode 100644 Src/IACore/inc/IACore/IACore.hpp create mode 100644 Src/IACore/inc/IACore/IATest.hpp create mode 100644 Src/IACore/inc/IACore/IPC.hpp create mode 100644 Src/IACore/inc/IACore/JSON.hpp create mode 100644 Src/IACore/inc/IACore/Logger.hpp create mode 100644 Src/IACore/inc/IACore/PCH.hpp create mode 100644 Src/IACore/inc/IACore/ProcessOps.hpp create mode 100644 Src/IACore/inc/IACore/SocketOps.hpp create mode 100644 Src/IACore/inc/IACore/StreamReader.hpp create mode 100644 Src/IACore/inc/IACore/StreamWriter.hpp create mode 100644 Src/IACore/inc/IACore/StringOps.hpp create mode 100644 Src/IACore/inc/IACore/Utils.hpp create mode 100644 Tests/CMakeLists.txt create mode 100644 Tests/Regression/CMakeLists.txt create mode 100644 Tests/Subjects/CMakeLists.txt create mode 100644 Tests/Subjects/LongProcess/Main.cpp create mode 100644 Tests/Unit/CCompile.c create mode 100644 Tests/Unit/CMakeLists.txt create mode 100644 Tests/Unit/Environment.cpp create mode 100644 Tests/Unit/Main.cpp create mode 100644 Tests/Unit/ProcessOps.cpp create mode 100644 Tests/Unit/RingBuffer.cpp create mode 100644 Tests/Unit/StreamReader.cpp create mode 100644 Tests/Unit/Utils.cpp create mode 100644 logo.svg diff --git a/.clang-format b/.clang-format new file mode 100644 index 0000000..c354725 --- /dev/null +++ b/.clang-format @@ -0,0 +1,13 @@ +--- +BasedOnStyle: Microsoft +IndentWidth: 4 +SortIncludes: false +--- +Language: Cpp +IndentPPDirectives: AfterHash +FixNamespaceComments: true +NamespaceIndentation: All +SeparateDefinitionBlocks: Always +SpaceAfterCStyleCast: true +SpaceAfterLogicalNot: false +SpaceAfterTemplateKeyword: false diff --git a/.clangd b/.clangd new file mode 100644 index 0000000..c8865a6 --- /dev/null +++ b/.clangd @@ -0,0 +1,3 @@ +Index: + PathExclude: + - .*/EmbeddedResources\.cpp \ No newline at end of file diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml new file mode 100644 index 0000000..fb0a7be --- /dev/null +++ b/.github/workflows/ci.yaml @@ -0,0 +1,65 @@ +name: CI + +on: + push: + branches: [ "main" ] + pull_request: + branches: [ "main" ] + +jobs: + # ----------------------------------------------------------------------------- + # Linux Build (Ubuntu 22.04) + # Uses the 'linux-ci-release' preset + # ----------------------------------------------------------------------------- + build-linux: + name: Linux (Clang / Release) + runs-on: ubuntu-latest + + steps: + - name: Checkout Code + uses: actions/checkout@v4 + + - name: Install System Dependencies + run: | + sudo apt-get update + sudo apt-get install -y ninja-build clang libssl-dev + + - name: Configure CMake + run: cmake --preset linux-ci + + - name: Build + run: cmake --build --preset linux-ci-release + + - name: Run Unit Tests + run: ./out/build/linux-ci/bin/Release/IACore_Test_Suite + + # ----------------------------------------------------------------------------- + # Windows Build + # Uses the 'windows-debug' preset (Clang-CL + VCPKG) + # ----------------------------------------------------------------------------- + build-windows: + name: Windows (Clang-CL / Debug) + runs-on: windows-latest + + steps: + - name: Checkout Code + uses: actions/checkout@v4 + + - name: Setup Ninja + run: choco install ninja + + - name: Setup VCPKG Environment + run: echo "VCPKG_ROOT=$env:VCPKG_INSTALLATION_ROOT" >> $env:GITHUB_ENV + + - name: Install OpenSSL via VCPKG + run: vcpkg install openssl:x64-windows + + - name: Configure CMake + run: cmake --preset windows-default + + - name: Build + run: cmake --build --preset windows-debug + + - name: Run Unit Tests + run: ./out/build/windows-default/bin/Debug/IACore_Test_Suite.exe + \ No newline at end of file diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..5e2e34b --- /dev/null +++ b/.gitignore @@ -0,0 +1,53 @@ +# ---> C++ +# Prerequisites +*.d + +# Compiled Object files +*.slo +*.lo +*.o +*.obj + +# Precompiled Headers +*.gch +*.pch + +# Compiled Dynamic libraries +*.so +*.dylib +*.dll + +# Fortran module files +*.mod +*.smod + +# Compiled Static libraries +*.lai +*.la +*.a +*.lib + +# Executables +*.exe +*.out +*.app + +# ---> VisualStudioCode +.vscode/* +!.vscode/settings.json +!.vscode/tasks.json +!.vscode/extensions.json +!.vscode/*.code-snippets + +# Local History for Visual Studio Code +.history/ + +# Built Visual Studio Code Extensions +*.vsix + +[Ss]andbox/ + +.cache/ +.local/ +out/ +[Bb]uild diff --git a/.vscode/launch.json b/.vscode/launch.json new file mode 100644 index 0000000..36a129a --- /dev/null +++ b/.vscode/launch.json @@ -0,0 +1,27 @@ +{ + "version": "0.2.0", + "configurations": [ + { + "name": "Debug (Linux)", + "type": "cppdbg", + "request": "launch", + "program": "${command:cmake.launchTargetPath}", + "args": [ + "dummy" + ], + "stopAtEntry": false, + "cwd": "${workspaceFolder}/", + "environment": [], + "externalConsole": false, + "MIMode": "gdb", + "setupCommands": [ + { + "description": "Enable pretty-printing for gdb", + "text": "-enable-pretty-printing", + "ignoreFailures": true + } + ], + "preLaunchTask": "CMake Build" + } + ] +} \ No newline at end of file diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000..c0dc405 --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,4 @@ +{ + "editor.formatOnSave": true, + "C_Cpp.clang_format_fallbackStyle": "Microsoft" +} \ No newline at end of file diff --git a/.vscode/tasks.json b/.vscode/tasks.json new file mode 100644 index 0000000..3cd9419 --- /dev/null +++ b/.vscode/tasks.json @@ -0,0 +1,19 @@ +{ + "version": "2.0.0", + "tasks": [ + { + "label": "CMake Build", + "type": "cmake", + "command": "build", + "group": { + "kind": "build", + "isDefault": true + }, + "presentation": { + "reveal": "silent", + "panel": "shared" + }, + "detail": "Builds the currently selected CMake Preset" + } + ] +} \ No newline at end of file diff --git a/CMake/FindDeps.cmake b/CMake/FindDeps.cmake new file mode 100644 index 0000000..15142f8 --- /dev/null +++ b/CMake/FindDeps.cmake @@ -0,0 +1,116 @@ +include(FetchContent) + +set(BUILD_SHARED_LIBS OFF CACHE INTERNAL "Force static libs") + +set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -Wno-error=int-conversion") +add_compile_definitions(-D_ITERATOR_DEBUG_LEVEL=0) + +FetchContent_Declare( + httplib + GIT_REPOSITORY https://github.com/yhirose/cpp-httplib.git + GIT_TAG v0.28.0 + SYSTEM + EXCLUDE_FROM_ALL +) + +FetchContent_Declare( + nlohmann_json + GIT_REPOSITORY https://github.com/nlohmann/json.git + GIT_TAG v3.12.0 + SYSTEM + EXCLUDE_FROM_ALL +) + +FetchContent_Declare( + glaze + GIT_REPOSITORY https://github.com/stephenberry/glaze.git + GIT_TAG v5.0.0 + SYSTEM + EXCLUDE_FROM_ALL +) + +FetchContent_Declare( + simdjson + GIT_REPOSITORY https://github.com/simdjson/simdjson.git + GIT_TAG v4.2.2 + SYSTEM + EXCLUDE_FROM_ALL +) + +FetchContent_Declare( + ZLIB + GIT_REPOSITORY https://github.com/zlib-ng/zlib-ng.git + GIT_TAG 2.3.2 + SYSTEM + EXCLUDE_FROM_ALL +) + +FetchContent_Declare( + zstd + GIT_REPOSITORY https://github.com/facebook/zstd.git + GIT_TAG v1.5.7 + SOURCE_SUBDIR build/cmake + SYSTEM + EXCLUDE_FROM_ALL +) + +FetchContent_Declare( + mimalloc + GIT_REPOSITORY https://github.com/microsoft/mimalloc.git + GIT_TAG v3.0.10 + SYSTEM + EXCLUDE_FROM_ALL + PATCH_COMMAND ${CMAKE_COMMAND} + -DSOURCE_DIR= + -P ${CMAKE_CURRENT_SOURCE_DIR}/CMake/PatchMimalloc.cmake +) + +FetchContent_Declare( + tl-expected + GIT_REPOSITORY https://github.com/TartanLlama/expected.git + GIT_TAG v1.3.1 + SYSTEM + EXCLUDE_FROM_ALL +) + +FetchContent_Declare( + unordered_dense + GIT_REPOSITORY https://github.com/martinus/unordered_dense.git + GIT_TAG v4.8.1 + SYSTEM + EXCLUDE_FROM_ALL +) + +FetchContent_Declare( + pugixml + GIT_REPOSITORY https://github.com/zeux/pugixml.git + GIT_TAG v1.15 + SYSTEM + EXCLUDE_FROM_ALL +) + +set(MI_OVERRIDE ON CACHE BOOL "" FORCE) +set(MI_BUILD_STATIC ON CACHE BOOL "" FORCE) +set(MI_BUILD_TESTS OFF CACHE BOOL "" FORCE) +set(MI_BUILD_SHARED OFF CACHE BOOL "" FORCE) + +set(EXPECTED_BUILD_TESTS OFF CACHE BOOL "" FORCE) + +set(HTTPLIB_REQUIRE_OPENSSL OFF CACHE BOOL "" FORCE) +set(HTTPLIB_REQUIRE_ZLIB OFF CACHE BOOL "" FORCE) + +set(ZSTD_BUILD_SHARED OFF CACHE BOOL "" FORCE) +set(ZSTD_BUILD_STATIC ON CACHE BOOL "" FORCE) + +set(ZLIB_COMPAT ON CACHE BOOL "" FORCE) +set(ZLIB_ENABLE_TESTS OFF CACHE BOOL "" FORCE) + +FetchContent_MakeAvailable(ZLIB zstd) + +if(TARGET libzstd_static AND NOT TARGET zstd::libzstd) + add_library(zstd::libzstd ALIAS libzstd_static) +endif() + +FetchContent_MakeAvailable(httplib pugixml nlohmann_json glaze simdjson tl-expected unordered_dense mimalloc) + +find_package(OpenSSL REQUIRED) diff --git a/CMake/PatchMimalloc.cmake b/CMake/PatchMimalloc.cmake new file mode 100644 index 0000000..4e3cac4 --- /dev/null +++ b/CMake/PatchMimalloc.cmake @@ -0,0 +1,25 @@ +# Get the file path from the command line argument +set(TARGET_FILE "${SOURCE_DIR}/src/prim/windows/prim.c") + +# Read the file +file(READ "${TARGET_FILE}" FILE_CONTENT) + +# Check if the patch is already applied +string(FIND "${FILE_CONTENT}" "struct _TEB* const teb" ALREADY_PATCHED) + +if(ALREADY_PATCHED EQUAL -1) + message(STATUS "Patching mimalloc source: ${TARGET_FILE}") + + # Perform the replacement + string(REPLACE + "_TEB* const teb = NtCurrentTeb()" + "struct _TEB* const teb = NtCurrentTeb()" + FILE_CONTENT + "${FILE_CONTENT}" + ) + + # Write the file back only if changes were made + file(WRITE "${TARGET_FILE}" "${FILE_CONTENT}") +else() + message(STATUS "mimalloc source is already patched. Skipping.") +endif() \ No newline at end of file diff --git a/CMakeLists.txt b/CMakeLists.txt new file mode 100644 index 0000000..2f36567 --- /dev/null +++ b/CMakeLists.txt @@ -0,0 +1,64 @@ +cmake_minimum_required(VERSION 3.28 FATAL_ERROR) + +set(CMAKE_CXX_STANDARD 20) +set(CMAKE_CXX_EXTENSIONS OFF) +set(CMAKE_CXX_STANDARD_REQUIRED ON) +set(CMAKE_EXPORT_COMPILE_COMMANDS ON) + +set(CMAKE_RUNTIME_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/bin) +set(CMAKE_ARCHIVE_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/lib) +set(CMAKE_LIBRARY_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/lib) + +project(IACore) + +enable_language(C) + +include(CMake/FindDeps.cmake) + +# Default to ON if root, OFF if dependency +option(IACore_BUILD_TESTS "Build unit tests" ${PROJECT_IS_TOP_LEVEL}) + +message(STATUS "Configured IACore for Multi-Config (Debug/Release rules generated)") + +message(STATUS "Detected Compiler ID: ${CMAKE_CXX_COMPILER_ID}") +# Check if the compiler is MSVC (cl.exe), but allow Clang acting like MSVC (clang-cl) +if (CMAKE_CXX_COMPILER_ID STREQUAL "MSVC") + if (NOT CMAKE_CXX_COMPILER_ID MATCHES "Clang") + message(FATAL_ERROR + "\n\n" + "-------------------------------------------------------------\n" + "CRITICAL ERROR: Unsupported Compiler Detected (MSVC/cl.exe)\n" + "-------------------------------------------------------------\n" + "IACore requires GCC or Clang to compile.\n" + "For compiling with IACore on Windows, please use Clang Tools for MSVC.\n" + "-------------------------------------------------------------\n" + ) + endif() +endif() + +if(MSVC) + add_compile_options(/W4) + if(CMAKE_CXX_COMPILER_ID MATCHES "Clang") + add_compile_options( + -Wno-c++98-compat + -Wno-c++98-compat-pedantic + ) + endif() + +elseif(CMAKE_CXX_COMPILER_ID MATCHES "Clang" OR CMAKE_CXX_COMPILER_ID MATCHES "GNU") + add_compile_options( + -Wall -Wextra -Wpedantic + -Wno-language-extension-token + ) +endif() + +add_subdirectory(Src/) + +if(IACore_BUILD_TESTS) + add_subdirectory(Tests) +endif() + +# Local Development Sandbox (Not tracked in git) +if(EXISTS "${CMAKE_CURRENT_SOURCE_DIR}/Sandbox") + add_subdirectory("${CMAKE_CURRENT_SOURCE_DIR}/Sandbox") +endif() diff --git a/CMakePresets.json b/CMakePresets.json new file mode 100644 index 0000000..6d4363e --- /dev/null +++ b/CMakePresets.json @@ -0,0 +1,99 @@ +{ + "version": 3, + "cmakeMinimumRequired": { + "major": 3, + "minor": 28 + }, + "configurePresets": [ + { + "name": "base-common", + "hidden": true, + "generator": "Ninja Multi-Config", + "binaryDir": "${sourceDir}/out/build/${presetName}", + "cacheVariables": { + "CMAKE_EXPORT_COMPILE_COMMANDS": "ON", + "CMAKE_C_COMPILER": "clang", + "CMAKE_CXX_COMPILER": "clang++" + } + }, + { + "name": "windows-base", + "hidden": true, + "inherits": "base-common", + "condition": { + "type": "equals", + "lhs": "${hostSystemName}", + "rhs": "Windows" + }, + "cacheVariables": { + "CMAKE_C_COMPILER": "clang-cl", + "CMAKE_CXX_COMPILER": "clang-cl", + "CMAKE_TOOLCHAIN_FILE": "$env{VCPKG_ROOT}/scripts/buildsystems/vcpkg.cmake", + "VCPKG_TARGET_TRIPLET": "x64-windows" + } + }, + { + "name": "linux-base", + "hidden": true, + "inherits": "base-common", + "condition": { + "type": "notEquals", + "lhs": "${hostSystemName}", + "rhs": "Windows" + } + }, + { + "name": "windows-default", + "displayName": "Windows (Clang-CL + VCPKG)", + "description": "Windows build using Clang-CL and VCPKG dependencies", + "inherits": "windows-base" + }, + { + "name": "linux-default", + "displayName": "Linux (Clang)", + "description": "Linux build using Clang", + "inherits": "linux-base" + }, + { + "name": "linux-ci", + "displayName": "Linux CI Build", + "description": "Linux CI Build", + "inherits": "linux-base", + "cacheVariables": { + "IS_CI_BUILD": "ON" + } + } + ], + "buildPresets": [ + { + "name": "windows-debug", + "configurePreset": "windows-default", + "configuration": "Debug", + "condition": { + "type": "equals", + "lhs": "${hostSystemName}", + "rhs": "Windows" + } + }, + { + "name": "linux-debug", + "configurePreset": "linux-default", + "configuration": "Debug", + "condition": { + "type": "notEquals", + "lhs": "${hostSystemName}", + "rhs": "Windows" + } + }, + { + "name": "linux-ci-release", + "configurePreset": "linux-ci", + "configuration": "Release", + "condition": { + "type": "notEquals", + "lhs": "${hostSystemName}", + "rhs": "Windows" + } + } + ] +} \ No newline at end of file diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..f433b1a --- /dev/null +++ b/LICENSE @@ -0,0 +1,177 @@ + + 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 diff --git a/README.md b/README.md new file mode 100644 index 0000000..3a75693 --- /dev/null +++ b/README.md @@ -0,0 +1,164 @@ +# IACore (Independent Architecture Core) + +
+ IACore Logo +
+ + License + C++ Standard + Platform + +

+ The Battery-Included Foundation for High-Performance C++ Applications. +

+
+ +## 📖 Description +IACore is a high-performance, battery-included C++20 foundation library designed to eliminate "dependency hell." It bundles essential systems—IPC, Logging, Networking, Compression, and Async Scheduling—into a single, coherent API. + +Originally developed as the internal core for IASoft (PVT) LTD., it is now open-source to provide a standardized bedrock for C++ applications where performance matters. + +## ✨ Features + +* **🚀 High-Performance IPC:** Shared-Memory Ring Buffers with wait-free SPSC synchronization. +* **🌐 Networking:** Integrated HTTP/HTTPS client (wrapper around `cpp-httplib` with automatic Zlib/Gzip handling). +* **🧵 Async Scheduler:** A job system with high/normal priority queues. +* **💾 File I/O:** Memory-mapped file operations and optimized binary stream readers/writers. +* **📦 Compression:** Unified API for Zlib, Gzip, and Zstd. +* **📜 Logging:** Thread-safe, colored console and disk logging. +* **⚡ Modern C++:** Heavily utilizes modern C++20 concepts, `std::span`, and `tl::expected` for error handling. + +## 🛠️ Integration + +IACore is built with CMake. You can include it in your project via `FetchContent` or by adding it as a subdirectory. + +**Note:** On Windows, you must have **VCPKG** installed and the `VCPKG_ROOT` environment variable set, for OpenSSL support. + +### CMake Example +```cmake +add_subdirectory(IACore) + +add_executable(MyApp Main.cpp) +target_link_libraries(MyApp PRIVATE IACore) +``` + +## 📦 Dependencies +IACore manages its own dependencies via CMake's FetchContent. You do not need to install these manually: + +* **Networking:** `cpp-httplib` +* **Compression:** `zlib-ng` & `zstd` +* **Utilities:** `tl-expected` & `unordered_dense` +* **JSON:** `glaze` + +**Note:** Following dependencies are not directly used by IACore, but bundles them (+ helper wrappers) for user convenience: `nlohmann_json`, `simdjson`, `pugixml` + +## 💡 Usage Examples +### 1. IPC (Manager & Node) +IACore provides a manager/node architecture using shared memory. + +#### Manager: +```C++ +#include + +// Spawns a child process and connects via Shared Memory +auto nodeID = manager.SpawnNode("MyChildNodeExe"); +manager.WaitTillNodeIsOnline(*nodeID); + +// Send data with zero-copy overhead +String msg = "Hello Node"; +manager.SendPacket(*nodeID, 100, {(PCUINT8)msg.data(), msg.size()}); +``` + +#### Node: +```C++ +#include + +class Node : public IACore::IPC_Node { +public: + void OnSignal(uint8_t signal) override { + // Handle signals + } + + void OnPacket(uint16_t packetID, std::span payload) override { + // Handle packets + } +}; + +int main(int argc, char* argv[]) { + // The connection string is passed as the first argument by the Manager + if (argc < 2) return -1; + + Node node; + // Connect back to the manager via Shared Memory + if (!node.Connect(argv[1])) return -1; + + while(true) { + node.Update(); + } + return 0; +} +``` + +### 2. Async Jobs +```C++ + +#include + +// Initialize worker threads (hardware_concurrency - 2) +IACore::AsyncOps::InitializeScheduler(); + +// Schedule a task +IACore::AsyncOps::Schedule *mySchedule = new IACore::AsyncOps::Schedule(); + +IACore::AsyncOps::ScheduleTask([](auto workerID) { + printf("Running on worker %d\n", workerID); +}, 0, mySchedule); + +// Wait for completion +IACore::AsyncOps::WaitForScheduleCompletion(mySchedule); +``` + +### 3. HTTP Request +```C++ +#include + +IACore::HttpClient client("https://api.example.com"); +auto res = client.JsonGet("/data", {}); + +if (res) { + std::cout << "Data: " << res->value << "\n"; +} else { + std::cerr << "Error: " << res.error() << "\n"; +} +``` + +## 🤝 Contributing + +We welcome contributions from the community! + +### What we accept immediately: +* **📚 Documentation:** Improvements to comments, the README, or external docs. +* **🧪 Tests:** New unit tests (in `Tests/`) to improve coverage or reproduce bugs. +* **💡 Examples:** New usage examples or sample projects. +* **🐛 Bug Reports:** detailed issues describing reproduction steps are highly valued. + +### Core Library Policy (`Src/` Directory) +Currently, **we are not accepting Pull Requests that modify the core source code (`Src/`)**. + +If you find a critical bug in `Src/`, please open an **Issue** rather than a PR, and the core team will implement a fix ASAP. + +## ⚖️ License + +This project is licensed under the Apache License Version 2.0. + +## 🤖 Use of Generative AI at IASoft + +While we never let Generative AI to make architecural/design decisions, and we hand-write almost all of the implementations, we do use Generative AI (Google Gemini) for the following and *(ONLY following)* tasks: + +1) **Controlled Repititive Boilerplate Generation:** Each and **every single line of AI generated code** is **manually reviewed** one-by-one for hallucinations and logic errors. Trust, but verify. + +2) **Concept Research and Development:** Design pattern comparisions, cost-benefit analysis and as a second pair of eyes to evalute and critique our design decisions. + +3) **Documentation:** Repititive method doc strings (parameter, return value descriptions) are done mostly using Generative AI. + +4) **Code Review:** Automated logic checking and static analysis on top of deterministic tools. diff --git a/Src/CMakeLists.txt b/Src/CMakeLists.txt new file mode 100644 index 0000000..86f4de2 --- /dev/null +++ b/Src/CMakeLists.txt @@ -0,0 +1,2 @@ + +add_subdirectory(IACore/) diff --git a/Src/IACore/CMakeLists.txt b/Src/IACore/CMakeLists.txt new file mode 100644 index 0000000..9c2ddee --- /dev/null +++ b/Src/IACore/CMakeLists.txt @@ -0,0 +1,71 @@ +set(SRC_FILES + "imp/cpp/IPC.cpp" + "imp/cpp/JSON.cpp" + "imp/cpp/IACore.cpp" + "imp/cpp/Logger.cpp" + "imp/cpp/FileOps.cpp" + "imp/cpp/AsyncOps.cpp" + "imp/cpp/DataOps.cpp" + "imp/cpp/SocketOps.cpp" + "imp/cpp/StringOps.cpp" + "imp/cpp/ProcessOps.cpp" + "imp/cpp/HttpClient.cpp" + "imp/cpp/StreamReader.cpp" + "imp/cpp/StreamWriter.cpp" +) + +add_library(IACore STATIC ${SRC_FILES}) + +target_include_directories(IACore PUBLIC inc/) +target_include_directories(IACore PRIVATE imp/hpp/) + +target_link_libraries(IACore PUBLIC + zlibstatic + libzstd_static + tl::expected + glaze::glaze + pugixml::pugixml + simdjson::simdjson + nlohmann_json::nlohmann_json + unordered_dense::unordered_dense +) + +target_link_libraries(IACore PRIVATE + httplib::httplib + OpenSSL::SSL + OpenSSL::Crypto +) + +target_link_libraries(IACore PUBLIC mimalloc-static) + +if(WIN32) + if(MSVC) + target_link_options(IACore PUBLIC "/INCLUDE:mi_version") + else() + target_link_options(IACore PUBLIC "") + endif() +endif() + +target_precompile_headers(IACore PUBLIC inc/IACore/PCH.hpp) + +set(NO_EXCEPT_FLAG "$,/EHs-c-,-fno-exceptions>") +target_compile_options(IACore PRIVATE ${NO_EXCEPT_FLAG}) +target_compile_options(IACore INTERFACE + $<$>>:${NO_EXCEPT_FLAG}> +) + +define_property(TARGET PROPERTY USE_EXCEPTIONS + BRIEF_DOCS "If ON, this target is allowed to use C++ exceptions." + FULL_DOCS "Prevents IACore from propagating -fno-exceptions to this target." +) + +target_compile_definitions(IACore PRIVATE + CPPHTTPLIB_OPENSSL_SUPPORT + CPPHTTPLIB_ZLIB_SUPPORT + NOMINMAX +) + +target_compile_definitions(IACore PUBLIC + $<$:__IA_DEBUG=1> + $<$:__IA_DEBUG=0> +) diff --git a/Src/IACore/imp/cpp/AsyncOps.cpp b/Src/IACore/imp/cpp/AsyncOps.cpp new file mode 100644 index 0000000..c3ba984 --- /dev/null +++ b/Src/IACore/imp/cpp/AsyncOps.cpp @@ -0,0 +1,178 @@ +// IACore-OSS; The Core Library for All IA Open Source Projects +// Copyright (C) 2025 IAS (ias@iasoft.dev) +// +// 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 + +namespace IACore +{ + Mutex AsyncOps::s_queueMutex; + ConditionVariable AsyncOps::s_wakeCondition; + Vector AsyncOps::s_scheduleWorkers; + Deque AsyncOps::s_highPriorityQueue; + Deque AsyncOps::s_normalPriorityQueue; + + VOID AsyncOps::RunTask(IN Function task) + { + JoiningThread(task).detach(); + } + + VOID AsyncOps::InitializeScheduler(IN UINT8 workerCount) + { + if (!workerCount) + workerCount = std::max((UINT32) 2, std::thread::hardware_concurrency() - 2); + for (UINT32 i = 0; i < workerCount; i++) + s_scheduleWorkers.emplace_back(AsyncOps::ScheduleWorkerLoop, i + 1); + } + + VOID AsyncOps::TerminateScheduler() + { + for (auto &w : s_scheduleWorkers) + { + w.request_stop(); + } + + s_wakeCondition.notify_all(); + + for (auto &w : s_scheduleWorkers) + { + if (w.joinable()) + { + w.join(); + } + } + + s_scheduleWorkers.clear(); + } + + VOID AsyncOps::ScheduleTask(IN Function task, IN TaskTag tag, IN Schedule *schedule, + IN Priority priority) + { + IA_ASSERT(s_scheduleWorkers.size() && "Scheduler must be initialized before calling this function"); + + schedule->Counter.fetch_add(1); + { + ScopedLock lock(s_queueMutex); + if (priority == Priority::High) + s_highPriorityQueue.emplace_back(ScheduledTask{tag, schedule, IA_MOVE(task)}); + else + s_normalPriorityQueue.emplace_back(ScheduledTask{tag, schedule, IA_MOVE(task)}); + } + s_wakeCondition.notify_one(); + } + + VOID AsyncOps::CancelTasksOfTag(IN TaskTag tag) + { + ScopedLock lock(s_queueMutex); + + auto cancelFromQueue = [&](Deque &queue) { + for (auto it = queue.begin(); it != queue.end(); /* no increment here */) + { + if (it->Tag == tag) + { + if (it->ScheduleHandle->Counter.fetch_sub(1) == 1) + it->ScheduleHandle->Counter.notify_all(); + + it = queue.erase(it); + } + else + ++it; + } + }; + + cancelFromQueue(s_highPriorityQueue); + cancelFromQueue(s_normalPriorityQueue); + } + + VOID AsyncOps::WaitForScheduleCompletion(IN Schedule *schedule) + { + IA_ASSERT(s_scheduleWorkers.size() && "Scheduler must be initialized before calling this function"); + + while (schedule->Counter.load() > 0) + { + ScheduledTask task; + BOOL foundTask{FALSE}; + { + UniqueLock lock(s_queueMutex); + if (!s_highPriorityQueue.empty()) + { + task = IA_MOVE(s_highPriorityQueue.front()); + s_highPriorityQueue.pop_front(); + foundTask = TRUE; + } + else if (!s_normalPriorityQueue.empty()) + { + task = IA_MOVE(s_normalPriorityQueue.front()); + s_normalPriorityQueue.pop_front(); + foundTask = TRUE; + } + } + if (foundTask) + { + task.Task(MainThreadWorkerID); + if (task.ScheduleHandle->Counter.fetch_sub(1) == 1) + task.ScheduleHandle->Counter.notify_all(); + } + else + { + auto currentVal = schedule->Counter.load(); + if (currentVal > 0) + schedule->Counter.wait(currentVal); + } + } + } + + AsyncOps::WorkerID AsyncOps::GetWorkerCount() + { + return static_cast(s_scheduleWorkers.size() + 1); // +1 for MainThread (Work Stealing) + } + + VOID AsyncOps::ScheduleWorkerLoop(IN StopToken stopToken, IN WorkerID workerID) + { + while (!stopToken.stop_requested()) + { + ScheduledTask task; + BOOL foundTask{FALSE}; + { + UniqueLock lock(s_queueMutex); + + s_wakeCondition.wait(lock, [&stopToken] { + return !s_highPriorityQueue.empty() || !s_normalPriorityQueue.empty() || stopToken.stop_requested(); + }); + + if (stopToken.stop_requested() && s_highPriorityQueue.empty() && s_normalPriorityQueue.empty()) + return; + + if (!s_highPriorityQueue.empty()) + { + task = IA_MOVE(s_highPriorityQueue.front()); + s_highPriorityQueue.pop_front(); + foundTask = TRUE; + } + else if (!s_normalPriorityQueue.empty()) + { + task = IA_MOVE(s_normalPriorityQueue.front()); + s_normalPriorityQueue.pop_front(); + foundTask = TRUE; + } + } + if (foundTask) + { + task.Task(workerID); + if (task.ScheduleHandle->Counter.fetch_sub(1) == 1) + task.ScheduleHandle->Counter.notify_all(); + } + } + } +} // namespace IACore \ No newline at end of file diff --git a/Src/IACore/imp/cpp/DataOps.cpp b/Src/IACore/imp/cpp/DataOps.cpp new file mode 100644 index 0000000..ad68902 --- /dev/null +++ b/Src/IACore/imp/cpp/DataOps.cpp @@ -0,0 +1,316 @@ +// IACore-OSS; The Core Library for All IA Open Source Projects +// Copyright (C) 2025 IAS (ias@iasoft.dev) +// +// 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 + +#include +#include + +namespace IACore +{ + STATIC CONSTEXPR UINT32 CRC32_TABLE[] = { + /* CRC polynomial 0xedb88320 */ + 0x00000000, 0x77073096, 0xee0e612c, 0x990951ba, 0x076dc419, 0x706af48f, 0xe963a535, 0x9e6495a3, 0x0edb8832, + 0x79dcb8a4, 0xe0d5e91e, 0x97d2d988, 0x09b64c2b, 0x7eb17cbd, 0xe7b82d07, 0x90bf1d91, 0x1db71064, 0x6ab020f2, + 0xf3b97148, 0x84be41de, 0x1adad47d, 0x6ddde4eb, 0xf4d4b551, 0x83d385c7, 0x136c9856, 0x646ba8c0, 0xfd62f97a, + 0x8a65c9ec, 0x14015c4f, 0x63066cd9, 0xfa0f3d63, 0x8d080df5, 0x3b6e20c8, 0x4c69105e, 0xd56041e4, 0xa2677172, + 0x3c03e4d1, 0x4b04d447, 0xd20d85fd, 0xa50ab56b, 0x35b5a8fa, 0x42b2986c, 0xdbbbc9d6, 0xacbcf940, 0x32d86ce3, + 0x45df5c75, 0xdcd60dcf, 0xabd13d59, 0x26d930ac, 0x51de003a, 0xc8d75180, 0xbfd06116, 0x21b4f4b5, 0x56b3c423, + 0xcfba9599, 0xb8bda50f, 0x2802b89e, 0x5f058808, 0xc60cd9b2, 0xb10be924, 0x2f6f7c87, 0x58684c11, 0xc1611dab, + 0xb6662d3d, 0x76dc4190, 0x01db7106, 0x98d220bc, 0xefd5102a, 0x71b18589, 0x06b6b51f, 0x9fbfe4a5, 0xe8b8d433, + 0x7807c9a2, 0x0f00f934, 0x9609a88e, 0xe10e9818, 0x7f6a0dbb, 0x086d3d2d, 0x91646c97, 0xe6635c01, 0x6b6b51f4, + 0x1c6c6162, 0x856530d8, 0xf262004e, 0x6c0695ed, 0x1b01a57b, 0x8208f4c1, 0xf50fc457, 0x65b0d9c6, 0x12b7e950, + 0x8bbeb8ea, 0xfcb9887c, 0x62dd1ddf, 0x15da2d49, 0x8cd37cf3, 0xfbd44c65, 0x4db26158, 0x3ab551ce, 0xa3bc0074, + 0xd4bb30e2, 0x4adfa541, 0x3dd895d7, 0xa4d1c46d, 0xd3d6f4fb, 0x4369e96a, 0x346ed9fc, 0xad678846, 0xda60b8d0, + 0x44042d73, 0x33031de5, 0xaa0a4c5f, 0xdd0d7cc9, 0x5005713c, 0x270241aa, 0xbe0b1010, 0xc90c2086, 0x5768b525, + 0x206f85b3, 0xb966d409, 0xce61e49f, 0x5edef90e, 0x29d9c998, 0xb0d09822, 0xc7d7a8b4, 0x59b33d17, 0x2eb40d81, + 0xb7bd5c3b, 0xc0ba6cad, 0xedb88320, 0x9abfb3b6, 0x03b6e20c, 0x74b1d29a, 0xead54739, 0x9dd277af, 0x04db2615, + 0x73dc1683, 0xe3630b12, 0x94643b84, 0x0d6d6a3e, 0x7a6a5aa8, 0xe40ecf0b, 0x9309ff9d, 0x0a00ae27, 0x7d079eb1, + 0xf00f9344, 0x8708a3d2, 0x1e01f268, 0x6906c2fe, 0xf762575d, 0x806567cb, 0x196c3671, 0x6e6b06e7, 0xfed41b76, + 0x89d32be0, 0x10da7a5a, 0x67dd4acc, 0xf9b9df6f, 0x8ebeeff9, 0x17b7be43, 0x60b08ed5, 0xd6d6a3e8, 0xa1d1937e, + 0x38d8c2c4, 0x4fdff252, 0xd1bb67f1, 0xa6bc5767, 0x3fb506dd, 0x48b2364b, 0xd80d2bda, 0xaf0a1b4c, 0x36034af6, + 0x41047a60, 0xdf60efc3, 0xa867df55, 0x316e8eef, 0x4669be79, 0xcb61b38c, 0xbc66831a, 0x256fd2a0, 0x5268e236, + 0xcc0c7795, 0xbb0b4703, 0x220216b9, 0x5505262f, 0xc5ba3bbe, 0xb2bd0b28, 0x2bb45a92, 0x5cb36a04, 0xc2d7ffa7, + 0xb5d0cf31, 0x2cd99e8b, 0x5bdeae1d, 0x9b64c2b0, 0xec63f226, 0x756aa39c, 0x026d930a, 0x9c0906a9, 0xeb0e363f, + 0x72076785, 0x05005713, 0x95bf4a82, 0xe2b87a14, 0x7bb12bae, 0x0cb61b38, 0x92d28e9b, 0xe5d5be0d, 0x7cdcefb7, + 0x0bdbdf21, 0x86d3d2d4, 0xf1d4e242, 0x68ddb3f8, 0x1fda836e, 0x81be16cd, 0xf6b9265b, 0x6fb077e1, 0x18b74777, + 0x88085ae6, 0xff0f6a70, 0x66063bca, 0x11010b5c, 0x8f659eff, 0xf862ae69, 0x616bffd3, 0x166ccf45, 0xa00ae278, + 0xd70dd2ee, 0x4e048354, 0x3903b3c2, 0xa7672661, 0xd06016f7, 0x4969474d, 0x3e6e77db, 0xaed16a4a, 0xd9d65adc, + 0x40df0b66, 0x37d83bf0, 0xa9bcae53, 0xdebb9ec5, 0x47b2cf7f, 0x30b5ffe9, 0xbdbdf21c, 0xcabac28a, 0x53b39330, + 0x24b4a3a6, 0xbad03605, 0xcdd70693, 0x54de5729, 0x23d967bf, 0xb3667a2e, 0xc4614ab8, 0x5d681b02, 0x2a6f2b94, + 0xb40bbe37, 0xc30c8ea1, 0x5a05df1b, 0x2d02ef8d, + }; + + // FNV-1a 32-bit Constants + constexpr uint32_t FNV1A_32_PRIME = 0x01000193; // 16777619 + constexpr uint32_t FNV1A_32_OFFSET = 0x811c9dc5; // 2166136261 + + UINT32 DataOps::Hash(IN CONST String &string) + { + uint32_t hash = FNV1A_32_OFFSET; + for (char c : string) + { + hash ^= static_cast(c); + hash *= FNV1A_32_PRIME; + } + return hash; + } + + UINT32 DataOps::Hash(IN Span data) + { + uint32_t hash = FNV1A_32_OFFSET; + const uint8_t *ptr = static_cast(data.data()); + + for (size_t i = 0; i < data.size(); ++i) + { + hash ^= ptr[i]; + hash *= FNV1A_32_PRIME; + } + return hash; + } + + UINT32 DataOps::CRC32(IN Span _data) + { + UINT32 crc32 = 0xFFFFFFFF; + +#define UPDC32(octet, crc) (CRC32_TABLE[((crc) ^ ((UINT8) octet)) & 0xff] ^ ((crc) >> 8)) + + auto data = _data.data(); + auto size = _data.size(); + for (; size; --size, ++data) + { + crc32 = UPDC32(*data, crc32); + } + +#undef UPDC32 + + return ~crc32; + } + + DataOps::CompressionType DataOps::DetectCompression(IN Span data) + { + if (data.size() < 2) + return CompressionType::None; + + // Check for GZIP Magic Number (0x1F 0x8B) + if (data[0] == 0x1F && data[1] == 0x8B) + return CompressionType::Gzip; + + // Check for ZLIB Magic Number (starts with 0x78) + // 0x78 = Deflate compression with 32k window size + if (data[0] == 0x78 && (data[1] == 0x01 || data[1] == 0x9C || data[1] == 0xDA)) + return CompressionType::Zlib; + + return CompressionType::None; + } + + Expected, String> DataOps::ZlibInflate(IN Span data) + { + z_stream zs{}; + zs.zalloc = Z_NULL; + zs.zfree = Z_NULL; + zs.opaque = Z_NULL; + + // 15 + 32 = Auto-detect Gzip or Zlib + if (inflateInit2(&zs, 15 + 32) != Z_OK) + return MakeUnexpected("Failed to initialize zlib inflate"); + + zs.next_in = const_cast(data.data()); + zs.avail_in = static_cast(data.size()); + + Vector outBuffer; + // Start with 2x input size. + size_t guessSize = data.size() < 1024 ? data.size() * 4 : data.size() * 2; + outBuffer.resize(guessSize); + + zs.next_out = reinterpret_cast(outBuffer.data()); + zs.avail_out = static_cast(outBuffer.size()); + + int ret; + do + { + if (zs.avail_out == 0) + { + size_t currentPos = zs.total_out; + + size_t newSize = outBuffer.size() * 2; + outBuffer.resize(newSize); + + zs.next_out = reinterpret_cast(outBuffer.data() + currentPos); + + zs.avail_out = static_cast(newSize - currentPos); + } + + ret = inflate(&zs, Z_NO_FLUSH); + + } while (ret == Z_OK); + + inflateEnd(&zs); + + if (ret != Z_STREAM_END) + return MakeUnexpected("Failed to inflate: corrupt data or stream error"); + + outBuffer.resize(zs.total_out); + + return outBuffer; + } + + Expected, String> DataOps::ZlibDeflate(IN Span data) + { + z_stream zs{}; + zs.zalloc = Z_NULL; + zs.zfree = Z_NULL; + zs.opaque = Z_NULL; + + if (deflateInit(&zs, Z_DEFAULT_COMPRESSION) != Z_OK) + return MakeUnexpected("Failed to initialize zlib deflate"); + + zs.next_in = const_cast(data.data()); + zs.avail_in = static_cast(data.size()); + + Vector outBuffer; + + outBuffer.resize(deflateBound(&zs, data.size())); + + zs.next_out = reinterpret_cast(outBuffer.data()); + zs.avail_out = static_cast(outBuffer.size()); + + int ret = deflate(&zs, Z_FINISH); + + if (ret != Z_STREAM_END) + { + deflateEnd(&zs); + return MakeUnexpected("Failed to deflate, ran out of buffer memory"); + } + + outBuffer.resize(zs.total_out); + + deflateEnd(&zs); + return outBuffer; + } + + Expected, String> DataOps::ZstdInflate(IN Span data) + { + unsigned long long const contentSize = ZSTD_getFrameContentSize(data.data(), data.size()); + + if (contentSize == ZSTD_CONTENTSIZE_ERROR) + return MakeUnexpected("Failed to inflate: Not valid ZSTD compressed data"); + + if (contentSize != ZSTD_CONTENTSIZE_UNKNOWN) + { + // FAST PATH: We know the size + Vector outBuffer; + outBuffer.resize(static_cast(contentSize)); + + size_t const dSize = ZSTD_decompress(outBuffer.data(), outBuffer.size(), data.data(), data.size()); + + if (ZSTD_isError(dSize)) + return MakeUnexpected(std::format("Failed to inflate: {}", ZSTD_getErrorName(dSize))); + + return outBuffer; + } + + ZSTD_DCtx *dctx = ZSTD_createDCtx(); + Vector outBuffer; + outBuffer.resize(data.size() * 2); + + ZSTD_inBuffer input = {data.data(), data.size(), 0}; + ZSTD_outBuffer output = {outBuffer.data(), outBuffer.size(), 0}; + + size_t ret; + do + { + ret = ZSTD_decompressStream(dctx, &output, &input); + + if (ZSTD_isError(ret)) + { + ZSTD_freeDCtx(dctx); + return MakeUnexpected(std::format("Failed to inflate: {}", ZSTD_getErrorName(ret))); + } + + if (output.pos == output.size) + { + size_t newSize = outBuffer.size() * 2; + outBuffer.resize(newSize); + output.dst = outBuffer.data(); + output.size = newSize; + } + + } while (ret != 0); + + outBuffer.resize(output.pos); + ZSTD_freeDCtx(dctx); + + return outBuffer; + } + + Expected, String> DataOps::ZstdDeflate(IN Span data) + { + size_t const maxDstSize = ZSTD_compressBound(data.size()); + + Vector outBuffer; + outBuffer.resize(maxDstSize); + + size_t const compressedSize = ZSTD_compress(outBuffer.data(), maxDstSize, data.data(), data.size(), 3); + + if (ZSTD_isError(compressedSize)) + return MakeUnexpected(std::format("Failed to deflate: {}", ZSTD_getErrorName(compressedSize))); + + outBuffer.resize(compressedSize); + return outBuffer; + } + + Expected, String> DataOps::GZipDeflate(IN Span data) + { + z_stream zs{}; + zs.zalloc = Z_NULL; + zs.zfree = Z_NULL; + zs.opaque = Z_NULL; + + // WindowBits = 15 + 16 (31) = Enforce GZIP encoding + // MemLevel = 8 (default) + // Strategy = Z_DEFAULT_STRATEGY + if (deflateInit2(&zs, Z_DEFAULT_COMPRESSION, Z_DEFLATED, 15 + 16, 8, Z_DEFAULT_STRATEGY) != Z_OK) + return MakeUnexpected("Failed to initialize gzip deflate"); + + zs.next_in = const_cast(data.data()); + zs.avail_in = static_cast(data.size()); + + Vector outBuffer; + + outBuffer.resize(deflateBound(&zs, data.size()) + 1024); // Additional 1KB buffer for safety + + zs.next_out = reinterpret_cast(outBuffer.data()); + zs.avail_out = static_cast(outBuffer.size()); + + int ret = deflate(&zs, Z_FINISH); + + if (ret != Z_STREAM_END) + { + deflateEnd(&zs); + return MakeUnexpected("Failed to deflate"); + } + + outBuffer.resize(zs.total_out); + + deflateEnd(&zs); + return outBuffer; + } + + Expected, String> DataOps::GZipInflate(IN Span data) + { + return ZlibInflate(data); + } +} // namespace IACore \ No newline at end of file diff --git a/Src/IACore/imp/cpp/FileOps.cpp b/Src/IACore/imp/cpp/FileOps.cpp new file mode 100644 index 0000000..9922dbb --- /dev/null +++ b/Src/IACore/imp/cpp/FileOps.cpp @@ -0,0 +1,281 @@ +// IACore-OSS; The Core Library for All IA Open Source Projects +// Copyright (C) 2025 IAS (ias@iasoft.dev) +// +// 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 + +namespace IACore +{ + UnorderedMap> FileOps::s_mappedFiles; + + VOID FileOps::UnmapFile(IN PCUINT8 mappedPtr) + { + if (!s_mappedFiles.contains(mappedPtr)) + return; + const auto handles = s_mappedFiles.extract(mappedPtr)->second; +#if IA_PLATFORM_WINDOWS + ::UnmapViewOfFile(std::get<1>(handles)); + ::CloseHandle(std::get<2>(handles)); + + if (std::get<0>(handles) != INVALID_HANDLE_VALUE) + ::CloseHandle(std::get<0>(handles)); +#elif IA_PLATFORM_UNIX + ::munmap(std::get<1>(handles), (SIZE_T) std::get<2>(handles)); + const auto fd = (INT32) ((UINT64) std::get<0>(handles)); + if (fd != -1) + ::close(fd); +#endif + } + + Expected FileOps::MapSharedMemory(IN CONST String &name, IN SIZE_T size, IN BOOL isOwner) + { +#if IA_PLATFORM_WINDOWS + int wchars_num = MultiByteToWideChar(CP_UTF8, 0, name.c_str(), -1, NULL, 0); + std::wstring wName(wchars_num, 0); + MultiByteToWideChar(CP_UTF8, 0, name.c_str(), -1, &wName[0], wchars_num); + + HANDLE hMap = NULL; + if (isOwner) + hMap = CreateFileMappingW(INVALID_HANDLE_VALUE, NULL, PAGE_READWRITE, (DWORD) (size >> 32), + (DWORD) (size & 0xFFFFFFFF), wName.c_str()); + else + hMap = OpenFileMappingW(FILE_MAP_ALL_ACCESS, FALSE, wName.c_str()); + + if (hMap == NULL) + return MakeUnexpected( + std::format("Failed to {} shared memory '{}'", isOwner ? "owner" : "consumer", name.c_str())); + + const auto result = static_cast(MapViewOfFile(hMap, FILE_MAP_ALL_ACCESS, 0, 0, size)); + if (result == NULL) + { + CloseHandle(hMap); + return MakeUnexpected(std::format("Failed to map view of shared memory '{}'", name.c_str())); + } + + s_mappedFiles[result] = std::make_tuple((PVOID) INVALID_HANDLE_VALUE, (PVOID) result, (PVOID) hMap); + return result; + +#elif IA_PLATFORM_UNIX + int fd = -1; + if (isOwner) + { + fd = shm_open(name.c_str(), O_RDWR | O_CREAT | O_TRUNC, 0666); + if (fd != -1) + { + if (ftruncate(fd, size) == -1) + { + close(fd); + shm_unlink(name.c_str()); + return MakeUnexpected(std::format("Failed to truncate shared memory '{}'", name.c_str())); + } + } + } + else + fd = shm_open(name.c_str(), O_RDWR, 0666); + + if (fd == -1) + return MakeUnexpected( + std::format("Failed to {} shared memory '{}'", isOwner ? "owner" : "consumer", name.c_str())); + + void *addr = mmap(nullptr, size, PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0); + if (addr == MAP_FAILED) + { + close(fd); + return MakeUnexpected(std::format("Failed to mmap shared memory '{}'", name.c_str())); + } + + const auto result = static_cast(addr); + + s_mappedFiles[result] = std::make_tuple((PVOID) ((UINT64) fd), (PVOID) addr, (PVOID) size); + return result; + +#endif + } + + VOID FileOps::UnlinkSharedMemory(IN CONST String &name) + { + if (name.empty()) + return; +#if IA_PLATFORM_UNIX + shm_unlink(name.c_str()); +#endif + } + + Expected FileOps::MapFile(IN CONST FilePath &path, OUT SIZE_T &size) + { +#if IA_PLATFORM_WINDOWS + + const auto handle = CreateFileA(path.string().c_str(), GENERIC_READ, FILE_SHARE_READ, NULL, OPEN_EXISTING, + FILE_ATTRIBUTE_NORMAL | FILE_FLAG_SEQUENTIAL_SCAN, NULL); + + if (handle == INVALID_HANDLE_VALUE) + return MakeUnexpected(std::format("Failed to open {} for memory mapping", path.string().c_str())); + + LARGE_INTEGER fileSize; + if (!GetFileSizeEx(handle, &fileSize)) + { + CloseHandle(handle); + return MakeUnexpected(std::format("Failed to get size of {} for memory mapping", path.string().c_str())); + } + size = static_cast(fileSize.QuadPart); + if (size == 0) + { + CloseHandle(handle); + return MakeUnexpected(std::format("Failed to get size of {} for memory mapping", path.string().c_str())); + } + + auto hmap = CreateFileMappingW(handle, NULL, PAGE_READONLY, 0, 0, NULL); + if (hmap == NULL) + { + CloseHandle(handle); + return MakeUnexpected(std::format("Failed to memory map {}", path.string().c_str())); + } + + const auto result = static_cast(MapViewOfFile(hmap, FILE_MAP_READ, 0, 0, 0)); + if (result == NULL) + { + CloseHandle(handle); + CloseHandle(hmap); + return MakeUnexpected(std::format("Failed to memory map {}", path.string().c_str())); + } + s_mappedFiles[result] = std::make_tuple((PVOID) handle, (PVOID) result, (PVOID) hmap); + return result; + +#elif IA_PLATFORM_UNIX + + const auto handle = open(path.string().c_str(), O_RDONLY); + if (handle == -1) + return MakeUnexpected(std::format("Failed to open {} for memory mapping", path.string().c_str())); + struct stat sb; + if (fstat(handle, &sb) == -1) + { + close(handle); + return MakeUnexpected(std::format("Failed to get stats of {} for memory mapping", path.string().c_str())); + } + size = static_cast(sb.st_size); + if (size == 0) + { + close(handle); + return MakeUnexpected(std::format("Failed to get size of {} for memory mapping", path.string().c_str())); + } + void *addr = mmap(nullptr, size, PROT_READ, MAP_PRIVATE, handle, 0); + if (addr == MAP_FAILED) + { + close(handle); + return MakeUnexpected(std::format("Failed to memory map {}", path.string().c_str())); + } + const auto result = static_cast(addr); + madvise(addr, size, MADV_SEQUENTIAL); + s_mappedFiles[result] = std::make_tuple((PVOID) ((UINT64) handle), (PVOID) addr, (PVOID) size); + return result; +#endif + } + + Expected FileOps::StreamToFile(IN CONST FilePath &path, IN BOOL overwrite) + { + if (!overwrite && FileSystem::exists(path)) + return MakeUnexpected(std::format("File aready exists: {}", path.string().c_str())); + return StreamWriter(path); + } + + Expected FileOps::StreamFromFile(IN CONST FilePath &path) + { + if (!FileSystem::exists(path)) + return MakeUnexpected(std::format("File does not exist: {}", path.string().c_str())); + return StreamReader(path); + } + + Expected FileOps::ReadTextFile(IN CONST FilePath &path) + { + const auto f = fopen(path.string().c_str(), "r"); + if (!f) + return MakeUnexpected(std::format("Failed to open file: {}", path.string().c_str())); + String result; + fseek(f, 0, SEEK_END); + result.resize(ftell(f)); + fseek(f, 0, SEEK_SET); + fread(result.data(), 1, result.size(), f); + fclose(f); + return result; + } + + Expected, String> FileOps::ReadBinaryFile(IN CONST FilePath &path) + { + const auto f = fopen(path.string().c_str(), "rb"); + if (!f) + return MakeUnexpected(std::format("Failed to open file: {}", path.string().c_str())); + Vector result; + fseek(f, 0, SEEK_END); + result.resize(ftell(f)); + fseek(f, 0, SEEK_SET); + fread(result.data(), 1, result.size(), f); + fclose(f); + return result; + } + + Expected FileOps::WriteTextFile(IN CONST FilePath &path, IN CONST String &contents, + IN BOOL overwrite) + { + const char *mode = overwrite ? "w" : "wx"; + const auto f = fopen(path.string().c_str(), mode); + if (!f) + { + if (!overwrite && errno == EEXIST) + return MakeUnexpected(std::format("File already exists: {}", path.string().c_str())); + return MakeUnexpected(std::format("Failed to write to file: {}", path.string().c_str())); + } + const auto result = fwrite(contents.data(), 1, contents.size(), f); + fputc(0, f); + fclose(f); + return result; + } + + Expected FileOps::WriteBinaryFile(IN CONST FilePath &path, IN Span contents, + IN BOOL overwrite) + { + const char *mode = overwrite ? "w" : "wx"; + const auto f = fopen(path.string().c_str(), mode); + if (!f) + { + if (!overwrite && errno == EEXIST) + return MakeUnexpected(std::format("File already exists: {}", path.string().c_str())); + return MakeUnexpected(std::format("Failed to write to file: {}", path.string().c_str())); + } + const auto result = fwrite(contents.data(), 1, contents.size(), f); + fclose(f); + return result; + } + + FilePath FileOps::NormalizeExecutablePath(IN CONST FilePath &path) + { + FilePath result = path; + +#if IA_PLATFORM_WINDOWS + if (!result.has_extension()) + result.replace_extension(".exe"); + +#elif IA_PLATFORM_UNIX + if (result.extension() == ".exe") + result.replace_extension(""); + + if (result.is_relative()) + { + String pathStr = result.string(); + if (!pathStr.starts_with("./") && !pathStr.starts_with("../")) + result = "./" + pathStr; + } +#endif + return result; + } +} // namespace IACore \ No newline at end of file diff --git a/Src/IACore/imp/cpp/HttpClient.cpp b/Src/IACore/imp/cpp/HttpClient.cpp new file mode 100644 index 0000000..1597969 --- /dev/null +++ b/Src/IACore/imp/cpp/HttpClient.cpp @@ -0,0 +1,243 @@ +// IACore-OSS; The Core Library for All IA Open Source Projects +// Copyright (C) 2025 IAS (ias@iasoft.dev) +// +// 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 +#include + +#include + +namespace IACore +{ + httplib::Headers BuildHeaders(IN Span headers, IN PCCHAR defaultContentType) + { + httplib::Headers out; + bool hasContentType = false; + + for (const auto &h : headers) + { + std::string key = HttpClient::HeaderTypeToString(h.first); + out.emplace(key, h.second); + + if (h.first == HttpClient::EHeaderType::CONTENT_TYPE) + hasContentType = true; + } + + if (!hasContentType && defaultContentType) + out.emplace("Content-Type", defaultContentType); + return out; + } + + HttpClient::HttpClient(IN CONST String &host) + : m_client(new httplib::Client(host)), m_lastResponseCode(EResponseCode::INTERNAL_SERVER_ERROR) + { + } + + HttpClient::~HttpClient() + { + if (m_client) + delete static_cast(m_client); + } + + String HttpClient::PreprocessResponse(IN CONST String &response) + { + const auto responseBytes = Span{(PCUINT8) response.data(), response.size()}; + const auto compression = DataOps::DetectCompression(responseBytes); + switch (compression) + { + case DataOps::CompressionType::Gzip: { + const auto data = DataOps::GZipInflate(responseBytes); + if (!data) + return response; + return String((PCCHAR) data->data(), data->size()); + } + + case DataOps::CompressionType::Zlib: { + const auto data = DataOps::ZlibInflate(responseBytes); + if (!data) + return response; + return String((PCCHAR) data->data(), data->size()); + } + + case DataOps::CompressionType::None: + default: + break; + } + return response; + } + + Expected HttpClient::RawGet(IN CONST String &path, IN Span headers, + IN PCCHAR defaultContentType) + { + auto httpHeaders = BuildHeaders(headers, defaultContentType); + + static_cast(m_client)->enable_server_certificate_verification(false); + auto res = static_cast(m_client)->Get( + (!path.empty() && path[0] != '/') ? ('/' + path).c_str() : path.c_str(), httpHeaders); + + if (res) + { + m_lastResponseCode = static_cast(res->status); + if (res->status >= 200 && res->status < 300) + return PreprocessResponse(res->body); + else + return MakeUnexpected(std::format("HTTP Error {} : {}", res->status, res->body)); + } + + return MakeUnexpected(std::format("Network Error: {}", httplib::to_string(res.error()))); + } + + Expected HttpClient::RawPost(IN CONST String &path, IN Span headers, + IN CONST String &body, IN PCCHAR defaultContentType) + { + auto httpHeaders = BuildHeaders(headers, defaultContentType); + + String contentType = defaultContentType; + if (httpHeaders.count("Content-Type")) + { + const auto t = httpHeaders.find("Content-Type"); + contentType = t->second; + httpHeaders.erase(t); + } + + static_cast(m_client)->set_keep_alive(true); + static_cast(m_client)->enable_server_certificate_verification(false); + auto res = static_cast(m_client)->Post( + (!path.empty() && path[0] != '/') ? ('/' + path).c_str() : path.c_str(), httpHeaders, body, + contentType.c_str()); + + if (res) + { + m_lastResponseCode = static_cast(res->status); + if (res->status >= 200 && res->status < 300) + return PreprocessResponse(res->body); + else + return MakeUnexpected(std::format("HTTP Error {} : {}", res->status, res->body)); + } + + return MakeUnexpected(std::format("Network Error: {}", httplib::to_string(res.error()))); + } +} // namespace IACore + +namespace IACore +{ + HttpClient::Header HttpClient::CreateHeader(IN EHeaderType key, IN CONST String &value) + { + return std::make_pair(key, value); + } + + String HttpClient::UrlEncode(IN CONST String &value) + { + std::stringstream escaped; + escaped.fill('0'); + escaped << std::hex << std::uppercase; + + for (char c : value) + { + if (std::isalnum(static_cast(c)) || c == '-' || c == '_' || c == '.' || c == '~') + escaped << c; + else + escaped << '%' << std::setw(2) << static_cast(static_cast(c)); + } + + return escaped.str(); + } + + String HttpClient::UrlDecode(IN CONST String &value) + { + String result; + result.reserve(value.length()); + + for (size_t i = 0; i < value.length(); ++i) + { + if (value[i] == '%' && i + 2 < value.length()) + { + std::string hexStr = value.substr(i + 1, 2); + char decodedChar = static_cast(std::strtol(hexStr.c_str(), nullptr, 16)); + result += decodedChar; + i += 2; + } + else if (value[i] == '+') + result += ' '; + else + result += value[i]; + } + + return result; + } + + String HttpClient::HeaderTypeToString(IN EHeaderType type) + { + switch (type) + { + case EHeaderType::ACCEPT: + return "Accept"; + case EHeaderType::ACCEPT_CHARSET: + return "Accept-Charset"; + case EHeaderType::ACCEPT_ENCODING: + return "Accept-Encoding"; + case EHeaderType::ACCEPT_LANGUAGE: + return "Accept-Language"; + case EHeaderType::AUTHORIZATION: + return "Authorization"; + case EHeaderType::CACHE_CONTROL: + return "Cache-Control"; + case EHeaderType::CONNECTION: + return "Connection"; + case EHeaderType::CONTENT_LENGTH: + return "Content-Length"; + case EHeaderType::CONTENT_TYPE: + return "Content-Type"; + case EHeaderType::COOKIE: + return "Cookie"; + case EHeaderType::DATE: + return "Date"; + case EHeaderType::EXPECT: + return "Expect"; + case EHeaderType::HOST: + return "Host"; + case EHeaderType::IF_MATCH: + return "If-Match"; + case EHeaderType::IF_MODIFIED_SINCE: + return "If-Modified-Since"; + case EHeaderType::IF_NONE_MATCH: + return "If-None-Match"; + case EHeaderType::ORIGIN: + return "Origin"; + case EHeaderType::PRAGMA: + return "Pragma"; + case EHeaderType::PROXY_AUTHORIZATION: + return "Proxy-Authorization"; + case EHeaderType::RANGE: + return "Range"; + case EHeaderType::REFERER: + return "Referer"; + case EHeaderType::TE: + return "TE"; + case EHeaderType::UPGRADE: + return "Upgrade"; + case EHeaderType::USER_AGENT: + return "User-Agent"; + case EHeaderType::VIA: + return "Via"; + case EHeaderType::WARNING: + return "Warning"; + } + } + + BOOL HttpClient::IsSuccessResponseCode(IN EResponseCode code) + { + return (INT32) code >= 200 && (INT32) code < 300; + } +} // namespace IACore \ No newline at end of file diff --git a/Src/IACore/imp/cpp/IACore.cpp b/Src/IACore/imp/cpp/IACore.cpp new file mode 100644 index 0000000..3bce997 --- /dev/null +++ b/Src/IACore/imp/cpp/IACore.cpp @@ -0,0 +1,78 @@ +// IACore-OSS; The Core Library for All IA Open Source Projects +// Copyright (C) 2025 IAS (ias@iasoft.dev) +// +// 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 +#include + +namespace IACore +{ + HighResTimePoint g_startTime{}; + std::thread::id g_mainThreadID{}; + + VOID Initialize() + { + g_mainThreadID = std::this_thread::get_id(); + g_startTime = HighResClock::now(); + Logger::Initialize(); + } + + VOID Terminate() + { + Logger::Terminate(); + } + + UINT64 GetUnixTime() + { + auto now = std::chrono::system_clock::now(); + return std::chrono::duration_cast(now.time_since_epoch()).count(); + } + + UINT64 GetTicksCount() + { + return std::chrono::duration_cast(HighResClock::now() - g_startTime).count(); + } + + FLOAT64 GetSecondsCount() + { + return std::chrono::duration_cast(HighResClock::now() - g_startTime).count(); + } + + FLOAT32 GetRandom() + { + return static_cast(rand()) / static_cast(RAND_MAX); + } + + UINT32 GetRandom(IN UINT32 seed) + { + srand(seed); + return (UINT32) GetRandom(0, UINT32_MAX); + } + + INT64 GetRandom(IN INT64 min, IN INT64 max) + { + const auto t = static_cast(rand()) / static_cast(RAND_MAX); + return min + (max - min) * t; + } + + VOID Sleep(IN UINT64 milliseconds) + { + std::this_thread::sleep_for(std::chrono::milliseconds(milliseconds)); + } + + BOOL IsMainThread() + { + return std::this_thread::get_id() == g_mainThreadID; + } +} // namespace IACore \ No newline at end of file diff --git a/Src/IACore/imp/cpp/IPC.cpp b/Src/IACore/imp/cpp/IPC.cpp new file mode 100644 index 0000000..345efd3 --- /dev/null +++ b/Src/IACore/imp/cpp/IPC.cpp @@ -0,0 +1,452 @@ +// IACore-OSS; The Core Library for All IA Open Source Projects +// Copyright (C) 2025 IAS (ias@iasoft.dev) +// +// 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 + +#include +#include + +namespace IACore +{ + struct IPC_ConnectionDescriptor + { + String SocketPath; + String SharedMemPath; + UINT32 SharedMemSize; + + String Serialize() CONST + { + return std::format("{}|{}|{}|", SocketPath, SharedMemPath, SharedMemSize); + } + + STATIC IPC_ConnectionDescriptor Deserialize(IN CONST String &data) + { + enum class EParseState + { + SocketPath, + SharedMemPath, + SharedMemSize + }; + + IPC_ConnectionDescriptor result{}; + + SIZE_T t{}; + EParseState state{EParseState::SocketPath}; + for (SIZE_T i = 0; i < data.size(); i++) + { + if (data[i] != '|') + continue; + + switch (state) + { + case EParseState::SocketPath: + result.SocketPath = data.substr(t, i - t); + state = EParseState::SharedMemPath; + break; + + case EParseState::SharedMemPath: + result.SharedMemPath = data.substr(t, i - t); + state = EParseState::SharedMemSize; + break; + + case EParseState::SharedMemSize: { + if (std::from_chars(&data[t], &data[i], result.SharedMemSize).ec != std::errc{}) + return {}; + goto done_parsing; + } + } + t = i + 1; + } + + done_parsing: + return result; + } + }; +} // namespace IACore + +namespace IACore +{ + IPC_Node::~IPC_Node() + { + SocketOps::Close(m_socket); // SocketOps gracefully handles INVALID_SOCKET + } + + Expected IPC_Node::Connect(IN PCCHAR connectionString) + { + auto desc = IPC_ConnectionDescriptor::Deserialize(connectionString); + m_shmName = desc.SharedMemPath; + + m_socket = SocketOps::CreateUnixSocket(); + if (!SocketOps::ConnectUnixSocket(m_socket, desc.SocketPath.c_str())) + return MakeUnexpected("Failed to create an unix socket"); + + auto mapRes = FileOps::MapSharedMemory(desc.SharedMemPath, desc.SharedMemSize, FALSE); + if (!mapRes.has_value()) + return MakeUnexpected("Failed to map the shared memory"); + + m_sharedMemory = mapRes.value(); + + auto *layout = reinterpret_cast(m_sharedMemory); + + if (layout->Meta.Magic != 0x49414950) // "IAIP" + return MakeUnexpected("Invalid shared memory header signature"); + + if (layout->Meta.Version != 1) + return MakeUnexpected("IPC version mismatch"); + + PUINT8 moniDataPtr = m_sharedMemory + layout->MONI_DataOffset; + PUINT8 minoDataPtr = m_sharedMemory + layout->MINO_DataOffset; + + MONI = std::make_unique( + &layout->MONI_Control, Span(moniDataPtr, static_cast(layout->MONI_DataSize)), FALSE); + + MINO = std::make_unique( + &layout->MINO_Control, Span(minoDataPtr, static_cast(layout->MINO_DataSize)), FALSE); + +#if IA_PLATFORM_WINDOWS + u_long mode = 1; + ioctlsocket(m_socket, FIONBIO, &mode); +#else + fcntl(m_socket, F_SETFL, O_NONBLOCK); +#endif + + m_receiveBuffer.resize(UINT16_MAX + 1); + + return {}; + } + + VOID IPC_Node::Update() + { + if (!MONI) + return; + + RingBufferView::PacketHeader header; + + // Process all available messages from Manager + while (MONI->Pop(header, Span(m_receiveBuffer.data(), m_receiveBuffer.size()))) + OnPacket(header.ID, {m_receiveBuffer.data(), header.PayloadSize}); + + UINT8 signal; + const auto res = recv(m_socket, (CHAR *) &signal, 1, 0); + if (res == 1) + OnSignal(signal); + else if (res == 0 || (res < 0 && !SocketOps::IsWouldBlock())) + { + SocketOps::Close(m_socket); + FileOps::UnlinkSharedMemory(m_shmName); + + // Manager disconnected, exit immediately + exit(-1); + } + } + + VOID IPC_Node::SendSignal(IN UINT8 signal) + { + if (IS_VALID_SOCKET(m_socket)) + send(m_socket, (const char *) &signal, sizeof(signal), 0); + } + + VOID IPC_Node::SendPacket(IN UINT16 packetID, IN Span payload) + { + MINO->Push(packetID, payload); + } +} // namespace IACore + +namespace IACore +{ + VOID IPC_Manager::NodeSession::SendSignal(IN UINT8 signal) + { + if (IS_VALID_SOCKET(DataSocket)) + send(DataSocket, (const char *) &signal, sizeof(signal), 0); + } + + VOID IPC_Manager::NodeSession::SendPacket(IN UINT16 packetID, IN Span payload) + { + // Protect the RingBuffer write cursor from concurrent threads + ScopedLock lock(SendMutex); + MONI->Push(packetID, payload); + } + + IPC_Manager::IPC_Manager() + { + // SocketOps is smart enough to track multiple inits + SocketOps::Initialize(); + + m_receiveBuffer.resize(UINT16_MAX + 1); + } + + IPC_Manager::~IPC_Manager() + { + for (auto &session : m_activeSessions) + { + ProcessOps::TerminateProcess(session->ProcessHandle); + FileOps::UnmapFile(session->MappedPtr); + FileOps::UnlinkSharedMemory(session->SharedMemName); + SocketOps::Close(session->DataSocket); + } + m_activeSessions.clear(); + + for (auto &session : m_pendingSessions) + { + ProcessOps::TerminateProcess(session->ProcessHandle); + FileOps::UnmapFile(session->MappedPtr); + FileOps::UnlinkSharedMemory(session->SharedMemName); + SocketOps::Close(session->ListenerSocket); + } + m_pendingSessions.clear(); + + // SocketOps is smart enough to track multiple terminates + SocketOps::Terminate(); + } + + VOID IPC_Manager::Update() + { + const auto now = SteadyClock::now(); + + for (INT32 i = m_pendingSessions.size() - 1; i >= 0; i--) + { + auto &session = m_pendingSessions[i]; + + if (now - session->CreationTime > std::chrono::seconds(5)) + { + ProcessOps::TerminateProcess(session->ProcessHandle); + + FileOps::UnmapFile(session->MappedPtr); + FileOps::UnlinkSharedMemory(session->SharedMemName); + SocketOps::Close(session->DataSocket); + + m_pendingSessions.erase(m_pendingSessions.begin() + i); + continue; + } + + SocketHandle newSock = accept(session->ListenerSocket, NULL, NULL); + + if (IS_VALID_SOCKET(newSock)) + { + session->DataSocket = newSock; + session->IsReady = TRUE; + + // Set Data Socket to Non-Blocking +#if IA_PLATFORM_WINDOWS + u_long mode = 1; + ioctlsocket(session->DataSocket, FIONBIO, &mode); +#else + fcntl(session->DataSocket, F_SETFL, O_NONBLOCK); +#endif + + SocketOps::Close(session->ListenerSocket); + session->ListenerSocket = INVALID_SOCKET; + + const auto sessionID = session->ProcessHandle->ID.load(); + const auto sessionPtr = session.get(); + m_activeSessions.push_back(std::move(session)); + m_pendingSessions.erase(m_pendingSessions.begin() + i); + m_activeSessionMap[sessionID] = sessionPtr; + } + } + + for (INT32 i = m_activeSessions.size() - 1; i >= 0; i--) + { + auto &node = m_activeSessions[i]; + + auto nodeID = node->ProcessHandle->ID.load(); + + RingBufferView::PacketHeader header; + + while (node->MINO->Pop(header, Span(m_receiveBuffer.data(), m_receiveBuffer.size()))) + OnPacket(nodeID, header.ID, {m_receiveBuffer.data(), header.PayloadSize}); + + UINT8 signal; + const auto res = recv(node->DataSocket, (CHAR *) &signal, 1, 0); + + if (res == 1) + OnSignal(nodeID, signal); + else if (res == 0 || (res < 0 && !SocketOps::IsWouldBlock())) + { + ProcessOps::TerminateProcess(node->ProcessHandle); + + FileOps::UnmapFile(node->MappedPtr); + FileOps::UnlinkSharedMemory(node->SharedMemName); + SocketOps::Close(node->DataSocket); + + m_activeSessions.erase(m_activeSessions.begin() + i); + m_activeSessionMap.erase(nodeID); + } + } + } + + Expected IPC_Manager::SpawnNode(IN CONST FilePath &executablePath, + IN UINT32 sharedMemorySize) + { + auto session = std::make_unique(); + + static Atomic s_idGen{0}; + UINT32 sid = ++s_idGen; + +#if IA_PLATFORM_WINDOWS + char tempPath[MAX_PATH]; + GetTempPathA(MAX_PATH, tempPath); + String sockPath = std::format("{}\\ia_sess_{}.sock", tempPath, sid); +#else + String sockPath = std::format("/tmp/ia_sess_{}.sock", sid); +#endif + + session->ListenerSocket = SocketOps::CreateUnixSocket(); + if (!SocketOps::BindUnixSocket(session->ListenerSocket, sockPath.c_str())) + return MakeUnexpected("Failed to bind unique socket"); + + if (listen(session->ListenerSocket, 1) != 0) + return MakeUnexpected("Failed to listen on unique socket"); + +#if IA_PLATFORM_WINDOWS + u_long mode = 1; + ioctlsocket(session->ListenerSocket, FIONBIO, &mode); +#else + fcntl(session->ListenerSocket, F_SETFL, O_NONBLOCK); +#endif + + String shmName = std::format("ia_shm_{}", sid); + auto mapRes = FileOps::MapSharedMemory(shmName, sharedMemorySize, TRUE); + if (!mapRes.has_value()) + return MakeUnexpected("Failed to map shared memory"); + + PUINT8 mappedPtr = mapRes.value(); + + auto *layout = reinterpret_cast(mappedPtr); + + layout->Meta.Magic = 0x49414950; + layout->Meta.Version = 1; + layout->Meta.TotalSize = sharedMemorySize; + + UINT64 headerSize = IPC_SharedMemoryLayout::GetHeaderSize(); + UINT64 usableBytes = sharedMemorySize - headerSize; + + UINT64 halfSize = (usableBytes / 2); + halfSize -= (halfSize % 64); + + layout->MONI_DataOffset = headerSize; + layout->MONI_DataSize = halfSize; + + layout->MINO_DataOffset = headerSize + halfSize; + layout->MINO_DataSize = halfSize; + + session->MONI = std::make_unique( + &layout->MONI_Control, Span(mappedPtr + layout->MONI_DataOffset, layout->MONI_DataSize), TRUE); + + session->MINO = std::make_unique( + &layout->MINO_Control, Span(mappedPtr + layout->MINO_DataOffset, layout->MINO_DataSize), TRUE); + + IPC_ConnectionDescriptor desc; + desc.SocketPath = sockPath; + desc.SharedMemPath = shmName; + desc.SharedMemSize = sharedMemorySize; + + String args = std::format("\"{}\"", desc.Serialize()); + + session->ProcessHandle = ProcessOps::SpawnProcessAsync( + FileOps::NormalizeExecutablePath(executablePath).string(), args, + [sid](IN StringView line) { + UNUSED(sid); + UNUSED(line); +#if __IA_DEBUG + puts(std::format(__CC_MAGENTA "[Node:{}:STDOUT|STDERR]: {}" __CC_DEFAULT, sid, line).c_str()); +#endif + }, + [sid](IN Expected result) { + UNUSED(sid); + UNUSED(result); +#if __IA_DEBUG + if (!result) + puts(std::format(__CC_RED "Failed to spawn Node: {} with error '{}'" __CC_DEFAULT, sid, + result.error()) + .c_str()); + else + puts(std::format(__CC_RED "[Node: {}]: Exited with code {}" __CC_DEFAULT, sid, *result).c_str()); +#endif + }); + + // Give some time for child node to stablize + std::this_thread::sleep_for(std::chrono::seconds(1)); + if (!session->ProcessHandle->IsActive()) + return MakeUnexpected(std::format("Failed to spawn the child process \"{}\"", executablePath.string())); + + auto processID = session->ProcessHandle->ID.load(); + + session->CreationTime = SteadyClock::now(); + m_pendingSessions.push_back(std::move(session)); + + return processID; + } + + BOOL IPC_Manager::WaitTillNodeIsOnline(IN NativeProcessID nodeID) + { + BOOL isPending = true; + while (isPending) + { + isPending = false; + for (auto it = m_pendingSessions.begin(); it != m_pendingSessions.end(); it++) + { + if (it->get()->ProcessHandle->ID.load() == nodeID) + { + isPending = true; + break; + } + } + Update(); + } + return m_activeSessionMap.contains(nodeID); + } + + VOID IPC_Manager::ShutdownNode(IN NativeProcessID nodeID) + { + const auto itNode = m_activeSessionMap.find(nodeID); + if (itNode == m_activeSessionMap.end()) + return; + + auto &node = itNode->second; + + ProcessOps::TerminateProcess(node->ProcessHandle); + FileOps::UnmapFile(node->MappedPtr); + FileOps::UnlinkSharedMemory(node->SharedMemName); + SocketOps::Close(node->DataSocket); + + for (auto it = m_activeSessions.begin(); it != m_activeSessions.end(); it++) + { + if (it->get() == node) + { + m_activeSessions.erase(it); + break; + } + } + + m_activeSessionMap.erase(itNode); + } + + VOID IPC_Manager::SendSignal(IN NativeProcessID node, IN UINT8 signal) + { + const auto itNode = m_activeSessionMap.find(node); + if (itNode == m_activeSessionMap.end()) + return; + itNode->second->SendSignal(signal); + } + + VOID IPC_Manager::SendPacket(IN NativeProcessID node, IN UINT16 packetID, IN Span payload) + { + const auto itNode = m_activeSessionMap.find(node); + if (itNode == m_activeSessionMap.end()) + return; + itNode->second->SendPacket(packetID, payload); + } +} // namespace IACore \ No newline at end of file diff --git a/Src/IACore/imp/cpp/JSON.cpp b/Src/IACore/imp/cpp/JSON.cpp new file mode 100644 index 0000000..303d33b --- /dev/null +++ b/Src/IACore/imp/cpp/JSON.cpp @@ -0,0 +1,44 @@ +// IACore-OSS; The Core Library for All IA Open Source Projects +// Copyright (C) 2025 IAS (ias@iasoft.dev) +// +// 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 + +namespace IACore +{ + Expected JSON::Parse(IN CONST String &json) + { + const auto parseResult = nlohmann::json::parse(json, nullptr, false, true); + if (parseResult.is_discarded()) + return MakeUnexpected("Failed to parse JSON"); + return parseResult; + } + + Expected, simdjson::dom::object>, String> JSON::ParseReadOnly( + IN CONST String &json) + { + auto parser = std::make_shared(); + + simdjson::error_code error{}; + simdjson::dom::object object; + if ((error = parser->parse(json).get(object))) + return MakeUnexpected(std::format("Failed to parse JSON : {}", simdjson::error_message(error))); + return std::make_pair(IA_MOVE(parser), IA_MOVE(object)); + } + + String JSON::Encode(IN nlohmann::json data) + { + return data.dump(); + } +} // namespace IACore \ No newline at end of file diff --git a/Src/IACore/imp/cpp/Logger.cpp b/Src/IACore/imp/cpp/Logger.cpp new file mode 100644 index 0000000..e166410 --- /dev/null +++ b/Src/IACore/imp/cpp/Logger.cpp @@ -0,0 +1,72 @@ +// IACore-OSS; The Core Library for All IA Open Source Projects +// Copyright (C) 2025 IAS (ias@iasoft.dev) +// +// 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 +#include +#include + +namespace IACore +{ + Logger::ELogLevel Logger::s_logLevel{Logger::ELogLevel::WARN}; + std::ofstream Logger::s_logFile{}; + + VOID Logger::Initialize() + { + } + + VOID Logger::Terminate() + { + if (s_logFile.is_open()) + { + s_logFile.flush(); + s_logFile.close(); + } + } + + BOOL Logger::EnableLoggingToDisk(IN PCCHAR filePath) + { + if (s_logFile.is_open()) + { + s_logFile.flush(); + s_logFile.close(); + } + s_logFile.open(filePath); + return s_logFile.is_open(); + } + + VOID Logger::SetLogLevel(IN ELogLevel logLevel) + { + s_logLevel = logLevel; + } + + VOID Logger::FlushLogs() + { + std::cout.flush(); + if (s_logFile) + s_logFile.flush(); + } + + VOID Logger::LogInternal(IN PCCHAR prefix, IN PCCHAR tag, IN String &&msg) + { + const auto outLine = std::format("[{:>8.3f}]: [{}]: {}", GetSecondsCount(), tag, msg); + std::cout << prefix << outLine << "\033[39m\n"; + if (s_logFile) + { + s_logFile.write(outLine.data(), outLine.size()); + s_logFile.put('\n'); + s_logFile.flush(); + } + } +} // namespace IACore \ No newline at end of file diff --git a/Src/IACore/imp/cpp/ProcessOps.cpp b/Src/IACore/imp/cpp/ProcessOps.cpp new file mode 100644 index 0000000..5638991 --- /dev/null +++ b/Src/IACore/imp/cpp/ProcessOps.cpp @@ -0,0 +1,337 @@ +// IACore-OSS; The Core Library for All IA Open Source Projects +// Copyright (C) 2025 IAS (ias@iasoft.dev) +// +// 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 + +namespace IACore +{ + // --------------------------------------------------------------------- + // Output Buffering Helper + // Splits raw chunks into lines, preserving partial lines across chunks + // --------------------------------------------------------------------- + struct LineBuffer + { + String Accumulator; + Function &Callback; + + VOID Append(IN PCCHAR data, IN SIZE_T size); + VOID Flush(); + }; +} // namespace IACore + +namespace IACore +{ + NativeProcessID ProcessOps::GetCurrentProcessID() + { +#if IA_PLATFORM_WINDOWS + return ::GetCurrentProcessId(); +#else + return getpid(); +#endif + } + + Expected ProcessOps::SpawnProcessSync(IN CONST String &command, IN CONST String &args, + IN Function onOutputLineCallback) + { + Atomic id; +#if IA_PLATFORM_WINDOWS + return SpawnProcessWindows(command, args, onOutputLineCallback, id); +#else + return SpawnProcessPosix(command, args, onOutputLineCallback, id); +#endif + } + + SharedPtr ProcessOps::SpawnProcessAsync(IN CONST String &command, IN CONST String &args, + IN Function onOutputLineCallback, + IN Function)> onFinishCallback) + { + SharedPtr handle = std::make_shared(); + handle->IsRunning = true; + + handle->ThreadHandle = + JoiningThread([=, h = handle.get(), cmd = IA_MOVE(command), args = std::move(args)]() mutable { + +#if IA_PLATFORM_WINDOWS + auto result = SpawnProcessWindows(cmd, args, onOutputLineCallback, h->ID); +#else + auto result = SpawnProcessPosix(cmd, args, onOutputLineCallback, h->ID); +#endif + + h->IsRunning = false; + + if (!onFinishCallback) + return; + + if (!result) + onFinishCallback(MakeUnexpected(result.error())); + else + onFinishCallback(*result); + }); + + return handle; + } + + VOID ProcessOps::TerminateProcess(IN CONST SharedPtr &handle) + { + if (!handle || !handle->IsActive()) + return; + + NativeProcessID pid = handle->ID.load(); + if (pid == 0) + return; + +#if IA_PLATFORM_WINDOWS + HANDLE hProcess = OpenProcess(PROCESS_TERMINATE, FALSE, pid); + if (hProcess != NULL) + { + ::TerminateProcess(hProcess, 9); + CloseHandle(hProcess); + } +#else + kill(pid, SIGKILL); +#endif + } +} // namespace IACore + +namespace IACore +{ +#if IA_PLATFORM_WINDOWS + Expected ProcessOps::SpawnProcessWindows(IN CONST String &command, IN CONST String &args, + IN Function onOutputLineCallback, + OUT Atomic &id) + { + SECURITY_ATTRIBUTES saAttr = {sizeof(SECURITY_ATTRIBUTES), NULL, TRUE}; // Allow inheritance + HANDLE hRead = NULL, hWrite = NULL; + + if (!CreatePipe(&hRead, &hWrite, &saAttr, 0)) + return tl::make_unexpected("Failed to create pipe"); + + // Ensure the read handle to the pipe for STDOUT is NOT inherited + if (!SetHandleInformation(hRead, HANDLE_FLAG_INHERIT, 0)) + return tl::make_unexpected("Failed to secure pipe handles"); + + STARTUPINFOA si = {sizeof(STARTUPINFOA)}; + si.dwFlags |= STARTF_USESTDHANDLES; + si.hStdOutput = hWrite; + si.hStdError = hWrite; // Merge stderr + si.hStdInput = NULL; // No input + + PROCESS_INFORMATION pi = {0}; + + // Windows command line needs to be mutable and concatenated + String commandLine = std::format("\"{}\" {}", command, args); + + BOOL success = CreateProcessA(NULL, commandLine.data(), NULL, NULL, TRUE, 0, NULL, NULL, &si, &pi); + + // Close write end in parent, otherwise ReadFile never returns EOF! + CloseHandle(hWrite); + + if (!success) + { + CloseHandle(hRead); + return tl::make_unexpected(String("CreateProcess failed: ") + std::to_string(GetLastError())); + } + + id.store(pi.dwProcessId); + + // Read Loop + LineBuffer lineBuf{"", onOutputLineCallback}; + DWORD bytesRead; + CHAR buffer[4096]; + + while (ReadFile(hRead, buffer, sizeof(buffer), &bytesRead, NULL) && bytesRead != 0) + { + lineBuf.Append(buffer, bytesRead); + } + lineBuf.Flush(); + + // NOW we wait for exit code + DWORD exitCode = 0; + WaitForSingleObject(pi.hProcess, INFINITE); + GetExitCodeProcess(pi.hProcess, &exitCode); + + CloseHandle(pi.hProcess); + CloseHandle(pi.hThread); + CloseHandle(hRead); + id.store(0); + + return static_cast(exitCode); + } +#endif + +#if IA_PLATFORM_UNIX + Expected ProcessOps::SpawnProcessPosix(IN CONST String &command, IN CONST String &args, + IN Function onOutputLineCallback, + OUT Atomic &id) + { + int pipefd[2]; + if (pipe(pipefd) == -1) + return tl::make_unexpected("Failed to create pipe"); + + pid_t pid = fork(); + + if (pid == -1) + { + return tl::make_unexpected("Failed to fork process"); + } + else if (pid == 0) + { + // --- Child Process --- + close(pipefd[0]); + + dup2(pipefd[1], STDOUT_FILENO); + dup2(pipefd[1], STDERR_FILENO); + close(pipefd[1]); + + std::vector argStorage; // To keep strings alive + std::vector argv; + + std::string cmdStr = command; + argv.push_back(cmdStr.data()); + + // Manual Quote-Aware Splitter + std::string currentToken; + bool inQuotes = false; + bool isEscaped = false; + + for (char c : args) + { + if (isEscaped) + { + // Previous char was '\', so we treat this char literally. + currentToken += c; + isEscaped = false; + continue; + } + + if (c == '\\') + { + // Escape sequence start + isEscaped = true; + continue; + } + + if (c == '\"') + { + // Toggle quote state + inQuotes = !inQuotes; + continue; + } + + if (c == ' ' && !inQuotes) + { + // Token boundary + if (!currentToken.empty()) + { + argStorage.push_back(currentToken); + currentToken.clear(); + } + } + else + { + currentToken += c; + } + } + + if (!currentToken.empty()) + { + argStorage.push_back(currentToken); + } + + // Build char* array from the std::string storage + for (auto &s : argStorage) + { + argv.push_back(s.data()); + } + argv.push_back(nullptr); + + execvp(argv[0], argv.data()); + _exit(127); + } + else + { + // --- Parent Process --- + id.store(pid); + + close(pipefd[1]); + + LineBuffer lineBuf{"", onOutputLineCallback}; + char buffer[4096]; + ssize_t count; + + while ((count = read(pipefd[0], buffer, sizeof(buffer))) > 0) + { + lineBuf.Append(buffer, count); + } + lineBuf.Flush(); + close(pipefd[0]); + + int status; + waitpid(pid, &status, 0); + + id.store(0); + if (WIFEXITED(status)) + return WEXITSTATUS(status); + return -1; + } + } +#endif +} // namespace IACore + +namespace IACore +{ + VOID LineBuffer::Append(IN PCCHAR data, IN SIZE_T size) + { + SIZE_T start = 0; + for (SIZE_T i = 0; i < size; ++i) + { + if (data[i] == '\n' || data[i] == '\r') + { + // Flush Accumulator + current chunk + if (!Accumulator.empty()) + { + Accumulator.append(data + start, i - start); + if (!Accumulator.empty()) + Callback(Accumulator); + Accumulator.clear(); + } + else + { + if (i > start) + Callback(StringView(data + start, i - start)); + } + + // Skip \r\n sequence if needed, or just start next + if (data[i] == '\r' && i + 1 < size && data[i + 1] == '\n') + i++; + start = i + 1; + } + } + // Save remaining partial line + if (start < size) + { + Accumulator.append(data + start, size - start); + } + } + + VOID LineBuffer::Flush() + { + if (!Accumulator.empty()) + { + Callback(Accumulator); + Accumulator.clear(); + } + } +} // namespace IACore \ No newline at end of file diff --git a/Src/IACore/imp/cpp/SocketOps.cpp b/Src/IACore/imp/cpp/SocketOps.cpp new file mode 100644 index 0000000..63f4909 --- /dev/null +++ b/Src/IACore/imp/cpp/SocketOps.cpp @@ -0,0 +1,105 @@ +// IACore-OSS; The Core Library for All IA Open Source Projects +// Copyright (C) 2025 IAS (ias@iasoft.dev) +// +// 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 + +namespace IACore +{ + INT32 SocketOps::s_initCount{0}; + + VOID SocketOps::Close(IN SocketHandle sock) + { + if (sock == INVALID_SOCKET) + return; + CLOSE_SOCKET(sock); + } + + BOOL SocketOps::Listen(IN SocketHandle sock, IN INT32 queueSize) + { + return listen(sock, queueSize) == 0; + } + + SocketHandle SocketOps::CreateUnixSocket() + { + return socket(AF_UNIX, SOCK_STREAM, 0); + } + + BOOL SocketOps::BindUnixSocket(IN SocketHandle sock, IN PCCHAR path) + { + if (!IS_VALID_SOCKET(sock)) + return FALSE; + + UNLINK_FILE(path); + + sockaddr_un addr{}; + addr.sun_family = AF_UNIX; + + size_t maxLen = sizeof(addr.sun_path) - 1; + + strncpy(addr.sun_path, path, maxLen); + + if (bind(sock, (struct sockaddr *) &addr, sizeof(addr)) == -1) + return FALSE; + + return TRUE; + } + + BOOL SocketOps::ConnectUnixSocket(IN SocketHandle sock, IN PCCHAR path) + { + if (!IS_VALID_SOCKET(sock)) + return FALSE; + + sockaddr_un addr{}; + addr.sun_family = AF_UNIX; + + size_t maxLen = sizeof(addr.sun_path) - 1; + + strncpy(addr.sun_path, path, maxLen); + + if (connect(sock, (struct sockaddr *) &addr, sizeof(addr)) == -1) + return FALSE; + + return TRUE; + } + + BOOL SocketOps::IsPortAvailable(IN UINT16 port, IN INT32 type) + { + SocketHandle sock = socket(AF_INET, type, IPPROTO_UDP); + if (!IS_VALID_SOCKET(sock)) + return false; + + sockaddr_in addr{}; + addr.sin_family = AF_INET; + addr.sin_port = htons(port); + addr.sin_addr.s_addr = htonl(INADDR_ANY); + + bool isFree = false; + if (bind(sock, (struct sockaddr *) &addr, sizeof(addr)) == 0) + isFree = true; + + CLOSE_SOCKET(sock); + + return isFree; + } + + BOOL SocketOps::IsWouldBlock() + { +#if IA_PLATFORM_WINDOWS + return WSAGetLastError() == WSAEWOULDBLOCK; +#else + return errno == EWOULDBLOCK || errno == EAGAIN; +#endif + } +} // namespace IACore \ No newline at end of file diff --git a/Src/IACore/imp/cpp/StreamReader.cpp b/Src/IACore/imp/cpp/StreamReader.cpp new file mode 100644 index 0000000..23d0a29 --- /dev/null +++ b/Src/IACore/imp/cpp/StreamReader.cpp @@ -0,0 +1,58 @@ +// IACore-OSS; The Core Library for All IA Open Source Projects +// Copyright (C) 2025 IAS (ias@iasoft.dev) +// +// 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 +#include +#include + +namespace IACore +{ + StreamReader::StreamReader(IN CONST FilePath &path) : m_storageType(EStorageType::OWNING_MMAP) + { + const auto t = FileOps::MapFile(path, m_dataSize); + if (!t) + { + Logger::Error("Failed to memory map file {}", path.string()); + return; + } + m_data = *t; + } + + StreamReader::StreamReader(IN Vector &&data) + : m_owningVector(IA_MOVE(data)), m_storageType(EStorageType::OWNING_VECTOR) + { + m_data = m_owningVector.data(); + m_dataSize = m_owningVector.size(); + } + + StreamReader::StreamReader(IN Span data) + : m_data(data.data()), m_dataSize(data.size()), m_storageType(EStorageType::NON_OWNING) + { + } + + StreamReader::~StreamReader() + { + switch (m_storageType) + { + case EStorageType::OWNING_MMAP: + FileOps::UnmapFile(m_data); + break; + + case EStorageType::NON_OWNING: + case EStorageType::OWNING_VECTOR: + break; + } + } +} // namespace IACore \ No newline at end of file diff --git a/Src/IACore/imp/cpp/StreamWriter.cpp b/Src/IACore/imp/cpp/StreamWriter.cpp new file mode 100644 index 0000000..1552773 --- /dev/null +++ b/Src/IACore/imp/cpp/StreamWriter.cpp @@ -0,0 +1,96 @@ +// IACore-OSS; The Core Library for All IA Open Source Projects +// Copyright (C) 2025 IAS (ias@iasoft.dev) +// +// 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 +#include + +namespace IACore +{ + StreamWriter::StreamWriter() : m_storageType(EStorageType::OWNING_VECTOR) + { + m_owningVector.resize(m_capacity = 256); + m_buffer = m_owningVector.data(); + } + + StreamWriter::StreamWriter(IN Span data) + : m_buffer(data.data()), m_capacity(data.size()), m_storageType(EStorageType::NON_OWNING) + { + } + + StreamWriter::StreamWriter(IN CONST FilePath &path) : m_filePath(path), m_storageType(EStorageType::OWNING_FILE) + { + IA_RELEASE_ASSERT(!path.empty()); + const auto f = fopen(m_filePath.string().c_str(), "wb"); + if (!f) + { + Logger::Error("Failed to open file for writing {}", m_filePath.string().c_str()); + return; + } + fputc(0, f); + fclose(f); + + m_owningVector.resize(m_capacity = 256); + m_buffer = m_owningVector.data(); + } + + StreamWriter::~StreamWriter() + { + switch (m_storageType) + { + case EStorageType::OWNING_FILE: { + IA_RELEASE_ASSERT(!m_filePath.empty()); + const auto f = fopen(m_filePath.string().c_str(), "wb"); + if (!f) + { + Logger::Error("Failed to open file for writing {}", m_filePath.string().c_str()); + return; + } + fwrite(m_owningVector.data(), 1, m_owningVector.size(), f); + fclose(f); + } + break; + + case EStorageType::OWNING_VECTOR: + case EStorageType::NON_OWNING: + break; + } + } + +#define HANDLE_OUT_OF_CAPACITY(_size) \ + if B_UNLIKELY ((m_cursor + _size) > m_capacity) \ + { \ + if (m_storageType == EStorageType::NON_OWNING) \ + return false; \ + m_owningVector.resize(m_capacity + (_size << 1)); \ + m_capacity = m_owningVector.size(); \ + m_buffer = m_owningVector.data(); \ + } + + BOOL StreamWriter::Write(IN UINT8 byte, IN SIZE_T count) + { + HANDLE_OUT_OF_CAPACITY(count); + memset(&m_buffer[m_cursor], byte, count); + m_cursor += count; + return true; + } + + BOOL StreamWriter::Write(IN PCVOID buffer, IN SIZE_T size) + { + HANDLE_OUT_OF_CAPACITY(size); + memcpy(&m_buffer[m_cursor], buffer, size); + m_cursor += size; + return true; + } +} // namespace IACore \ No newline at end of file diff --git a/Src/IACore/imp/cpp/StringOps.cpp b/Src/IACore/imp/cpp/StringOps.cpp new file mode 100644 index 0000000..abf83a3 --- /dev/null +++ b/Src/IACore/imp/cpp/StringOps.cpp @@ -0,0 +1,100 @@ +// IACore-OSS; The Core Library for All IA Open Source Projects +// Copyright (C) 2025 IAS (ias@iasoft.dev) +// +// 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 + +namespace IACore +{ + CONST String BASE64_CHAR_TABLE = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/"; + + String StringOps::EncodeBase64(IN Span data) + { + String result; + result.reserve(((data.size() + 2) / 3) * 4); + for (size_t i = 0; i < data.size(); i += 3) + { + uint32_t value = 0; + INT32 num_bytes = 0; + for (INT32 j = 0; j < 3 && (i + j) < data.size(); ++j) + { + value = (value << 8) | data[i + j]; + num_bytes++; + } + for (INT32 j = 0; j < num_bytes + 1; ++j) + { + if (j < 4) + { + result += BASE64_CHAR_TABLE[(value >> (6 * (3 - j))) & 0x3F]; + } + } + if (num_bytes < 3) + { + for (INT32 j = 0; j < (3 - num_bytes); ++j) + { + result += '='; + } + } + } + return result; + } + + Vector StringOps::DecodeBase64(IN CONST String &data) + { + Vector result; + + CONST AUTO isBase64 = [](UINT8 c) { return (isalnum(c) || (c == '+') || (c == '/')); }; + + INT32 in_len = data.size(); + INT32 i = 0, j = 0, in_ = 0; + UINT8 tmpBuf0[4], tmpBuf1[3]; + + while (in_len-- && (data[in_] != '=') && isBase64(data[in_])) + { + tmpBuf0[i++] = data[in_]; + in_++; + if (i == 4) + { + for (i = 0; i < 4; i++) + tmpBuf0[i] = BASE64_CHAR_TABLE.find(tmpBuf0[i]); + + tmpBuf1[0] = (tmpBuf0[0] << 2) + ((tmpBuf0[1] & 0x30) >> 4); + tmpBuf1[1] = ((tmpBuf0[1] & 0xf) << 4) + ((tmpBuf0[2] & 0x3c) >> 2); + tmpBuf1[2] = ((tmpBuf0[2] & 0x3) << 6) + tmpBuf0[3]; + + for (i = 0; (i < 3); i++) + result.push_back(tmpBuf1[i]); + i = 0; + } + } + + if (i) + { + for (j = i; j < 4; j++) + tmpBuf0[j] = 0; + + for (j = 0; j < 4; j++) + tmpBuf0[j] = BASE64_CHAR_TABLE.find(tmpBuf0[j]); + + tmpBuf1[0] = (tmpBuf0[0] << 2) + ((tmpBuf0[1] & 0x30) >> 4); + tmpBuf1[1] = ((tmpBuf0[1] & 0xf) << 4) + ((tmpBuf0[2] & 0x3c) >> 2); + tmpBuf1[2] = ((tmpBuf0[2] & 0x3) << 6) + tmpBuf0[3]; + + for (j = 0; (j < i - 1); j++) + result.push_back(tmpBuf1[j]); + } + + return result; + } +} // namespace IACore \ No newline at end of file diff --git a/Src/IACore/inc/IACore/ADT/RingBuffer.hpp b/Src/IACore/inc/IACore/ADT/RingBuffer.hpp new file mode 100644 index 0000000..94b1e48 --- /dev/null +++ b/Src/IACore/inc/IACore/ADT/RingBuffer.hpp @@ -0,0 +1,225 @@ +// IACore-OSS; The Core Library for All IA Open Source Projects +// Copyright (C) 2025 IAS (ias@iasoft.dev) +// +// 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. + +#pragma once + +#include + +namespace IACore +{ + class RingBufferView + { + public: + STATIC CONSTEXPR UINT16 PACKET_ID_SKIP = 0; + + struct ControlBlock + { + struct alignas(64) + { + Atomic WriteOffset{0}; + } Producer; + + struct alignas(64) + { + Atomic ReadOffset{0}; + // Capacity is effectively constant after init, + // so it doesn't cause false sharing invalidations. + UINT32 Capacity{0}; + } Consumer; + }; + + static_assert(offsetof(ControlBlock, Consumer) == 64, "False sharing detected in ControlBlock"); + + // All of the data in ring buffer will be stored as packets + struct PacketHeader + { + PacketHeader() : ID(0), PayloadSize(0) + { + } + + PacketHeader(IN UINT16 id) : ID(id), PayloadSize(0) + { + } + + PacketHeader(IN UINT16 id, IN UINT16 payloadSize) : ID(id), PayloadSize(payloadSize) + { + } + + UINT16 ID{}; + UINT16 PayloadSize{}; + }; + + public: + INLINE RingBufferView(IN Span buffer, IN BOOL isOwner); + INLINE RingBufferView(IN ControlBlock *controlBlock, IN Span buffer, IN BOOL isOwner); + + INLINE INT32 Pop(OUT PacketHeader &outHeader, OUT Span outBuffer); + INLINE BOOL Push(IN UINT16 packetID, IN Span data); + + INLINE ControlBlock *GetControlBlock(); + + private: + PUINT8 m_dataPtr{}; + UINT32 m_capacity{}; + ControlBlock *m_controlBlock{}; + + private: + INLINE VOID WriteWrapped(IN UINT32 offset, IN PCVOID data, IN UINT32 size); + INLINE VOID ReadWrapped(IN UINT32 offset, OUT PVOID outData, IN UINT32 size); + }; +} // namespace IACore + +namespace IACore +{ + RingBufferView::RingBufferView(IN Span buffer, IN BOOL isOwner) + { + IA_ASSERT(buffer.size() > sizeof(ControlBlock)); + + m_controlBlock = reinterpret_cast(buffer.data()); + m_dataPtr = buffer.data() + sizeof(ControlBlock); + + m_capacity = static_cast(buffer.size()) - sizeof(ControlBlock); + + if (isOwner) + { + m_controlBlock->Consumer.Capacity = m_capacity; + m_controlBlock->Producer.WriteOffset.store(0, std::memory_order_release); + m_controlBlock->Consumer.ReadOffset.store(0, std::memory_order_release); + } + else + IA_ASSERT(m_controlBlock->Consumer.Capacity == m_capacity); + } + + RingBufferView::RingBufferView(IN ControlBlock *controlBlock, IN Span buffer, IN BOOL isOwner) + { + IA_ASSERT(controlBlock != nullptr); + IA_ASSERT(buffer.size() > 0); + + m_controlBlock = controlBlock; + m_dataPtr = buffer.data(); + m_capacity = static_cast(buffer.size()); + + if (isOwner) + { + m_controlBlock->Consumer.Capacity = m_capacity; + m_controlBlock->Producer.WriteOffset.store(0, std::memory_order_release); + m_controlBlock->Consumer.ReadOffset.store(0, std::memory_order_release); + } + } + + INT32 RingBufferView::Pop(OUT PacketHeader &outHeader, OUT Span outBuffer) + { + UINT32 write = m_controlBlock->Producer.WriteOffset.load(std::memory_order_acquire); + UINT32 read = m_controlBlock->Consumer.ReadOffset.load(std::memory_order_relaxed); + UINT32 cap = m_capacity; + + if (read == write) + return 0; // Empty + + ReadWrapped(read, &outHeader, sizeof(PacketHeader)); + + if (outHeader.PayloadSize > outBuffer.size()) + return -static_cast(outHeader.PayloadSize); + + if (outHeader.PayloadSize > 0) + { + UINT32 dataReadOffset = (read + sizeof(PacketHeader)) % cap; + ReadWrapped(dataReadOffset, outBuffer.data(), outHeader.PayloadSize); + } + + // Move read pointer forward + UINT32 newReadOffset = (read + sizeof(PacketHeader) + outHeader.PayloadSize) % cap; + m_controlBlock->Consumer.ReadOffset.store(newReadOffset, std::memory_order_release); + + return outHeader.PayloadSize; + } + + BOOL RingBufferView::Push(IN UINT16 packetID, IN Span data) + { + IA_ASSERT(data.size() <= UINT16_MAX); + + const UINT32 totalSize = sizeof(PacketHeader) + static_cast(data.size()); + + UINT32 read = m_controlBlock->Consumer.ReadOffset.load(std::memory_order_acquire); + UINT32 write = m_controlBlock->Producer.WriteOffset.load(std::memory_order_relaxed); + UINT32 cap = m_capacity; + + UINT32 freeSpace = (read <= write) ? (m_capacity - write) + read : (read - write); + + // Ensure to always leave 1 byte empty to prevent Read == Write ambiguity (Wait-Free Ring Buffer standard) + if (freeSpace <= totalSize) + return FALSE; + + PacketHeader header{packetID, static_cast(data.size())}; + WriteWrapped(write, &header, sizeof(PacketHeader)); + + UINT32 dataWriteOffset = (write + sizeof(PacketHeader)) % cap; + + if (data.size() > 0) + { + WriteWrapped(dataWriteOffset, data.data(), static_cast(data.size())); + } + + UINT32 newWriteOffset = (dataWriteOffset + data.size()) % cap; + m_controlBlock->Producer.WriteOffset.store(newWriteOffset, std::memory_order_release); + + return TRUE; + } + + RingBufferView::ControlBlock *RingBufferView::GetControlBlock() + { + return m_controlBlock; + } + + VOID RingBufferView::WriteWrapped(IN UINT32 offset, IN PCVOID data, IN UINT32 size) + { + if (offset + size <= m_capacity) + { + // Contiguous write + memcpy(m_dataPtr + offset, data, size); + } + else + { + // Split write + UINT32 firstChunk = m_capacity - offset; + UINT32 secondChunk = size - firstChunk; + + const UINT8 *src = static_cast(data); + + memcpy(m_dataPtr + offset, src, firstChunk); + memcpy(m_dataPtr, src + firstChunk, secondChunk); + } + } + + VOID RingBufferView::ReadWrapped(IN UINT32 offset, OUT PVOID outData, IN UINT32 size) + { + if (offset + size <= m_capacity) + { + // Contiguous read + memcpy(outData, m_dataPtr + offset, size); + } + else + { + // Split read + UINT32 firstChunk = m_capacity - offset; + UINT32 secondChunk = size - firstChunk; + + UINT8 *dst = static_cast(outData); + + memcpy(dst, m_dataPtr + offset, firstChunk); + memcpy(dst + firstChunk, m_dataPtr, secondChunk); + } + } +} // namespace IACore \ No newline at end of file diff --git a/Src/IACore/inc/IACore/AsyncOps.hpp b/Src/IACore/inc/IACore/AsyncOps.hpp new file mode 100644 index 0000000..c92edb3 --- /dev/null +++ b/Src/IACore/inc/IACore/AsyncOps.hpp @@ -0,0 +1,73 @@ +// IACore-OSS; The Core Library for All IA Open Source Projects +// Copyright (C) 2025 IAS (ias@iasoft.dev) +// +// 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. + +#pragma once + +#include + +namespace IACore +{ + class AsyncOps + { + public: + using TaskTag = UINT64; + using WorkerID = UINT16; + + STATIC CONSTEXPR WorkerID MainThreadWorkerID = 0; + + enum class Priority : UINT8 + { + High, + Normal + }; + + struct Schedule + { + Atomic Counter{0}; + }; + + public: + STATIC VOID InitializeScheduler(IN UINT8 workerCount = 0); + STATIC VOID TerminateScheduler(); + + STATIC VOID ScheduleTask(IN Function task, IN TaskTag tag, IN Schedule *schedule, + IN Priority priority = Priority::Normal); + + STATIC VOID CancelTasksOfTag(IN TaskTag tag); + + STATIC VOID WaitForScheduleCompletion(IN Schedule *schedule); + + STATIC VOID RunTask(IN Function task); + + STATIC WorkerID GetWorkerCount(); + + private: + struct ScheduledTask + { + TaskTag Tag{}; + Schedule *ScheduleHandle{}; + Function Task{}; + }; + + STATIC VOID ScheduleWorkerLoop(IN StopToken stopToken, IN WorkerID workerID); + + private: + STATIC Mutex s_queueMutex; + STATIC ConditionVariable s_wakeCondition; + STATIC Vector s_scheduleWorkers; + STATIC Deque s_highPriorityQueue; + STATIC Deque s_normalPriorityQueue; + }; +} // namespace IACore \ No newline at end of file diff --git a/Src/IACore/inc/IACore/DataOps.hpp b/Src/IACore/inc/IACore/DataOps.hpp new file mode 100644 index 0000000..2501d3c --- /dev/null +++ b/Src/IACore/inc/IACore/DataOps.hpp @@ -0,0 +1,49 @@ +// IACore-OSS; The Core Library for All IA Open Source Projects +// Copyright (C) 2025 IAS (ias@iasoft.dev) +// +// 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. + +#pragma once + +#include + +namespace IACore +{ + class DataOps + { + public: + enum class CompressionType + { + None, + Gzip, + Zlib + }; + + public: + STATIC UINT32 Hash(IN CONST String &string); + STATIC UINT32 Hash(IN Span data); + + STATIC UINT32 CRC32(IN Span data); + + STATIC CompressionType DetectCompression(IN Span data); + + STATIC Expected, String> GZipInflate(IN Span data); + STATIC Expected, String> GZipDeflate(IN Span data); + + STATIC Expected, String> ZlibInflate(IN Span data); + STATIC Expected, String> ZlibDeflate(IN Span data); + + STATIC Expected, String> ZstdInflate(IN Span data); + STATIC Expected, String> ZstdDeflate(IN Span data); + }; +} // namespace IACore \ No newline at end of file diff --git a/Src/IACore/inc/IACore/DynamicLib.hpp b/Src/IACore/inc/IACore/DynamicLib.hpp new file mode 100644 index 0000000..d50659e --- /dev/null +++ b/Src/IACore/inc/IACore/DynamicLib.hpp @@ -0,0 +1,178 @@ +// IACore-OSS; The Core Library for All IA Open Source Projects +// Copyright (C) 2025 IAS (ias@iasoft.dev) +// +// 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. + +#pragma once + +#include + +#if IA_PLATFORM_WINDOWS +# include +# include +#else +# include +#endif + +namespace IACore +{ + + class DynamicLib + { + public: + DynamicLib() : m_handle(nullptr) + { + } + + DynamicLib(DynamicLib &&other) NOEXCEPT : m_handle(other.m_handle) + { + other.m_handle = nullptr; + } + + DynamicLib &operator=(DynamicLib &&other) NOEXCEPT + { + if (this != &other) + { + Unload(); // Free current if exists + m_handle = other.m_handle; + other.m_handle = nullptr; + } + return *this; + } + + DynamicLib(CONST DynamicLib &) = delete; + DynamicLib &operator=(CONST DynamicLib &) = delete; + + ~DynamicLib() + { + Unload(); + } + + // Automatically detects extension (.dll/.so) if not provided + NO_DISCARD("Check for load errors") + + STATIC tl::expected Load(CONST String &searchPath, CONST String &name) + { + namespace fs = std::filesystem; + + fs::path fullPath = fs::path(searchPath) / name; + + if (!fullPath.has_extension()) + { +#if IA_PLATFORM_WINDOWS + fullPath += ".dll"; +#elif IA_PLATFORM_MAC + fullPath += ".dylib"; +#else + fullPath += ".so"; +#endif + } + + DynamicLib lib; + +#if IA_PLATFORM_WINDOWS + HMODULE h = LoadLibraryA(fullPath.string().c_str()); + if (!h) + { + return tl::make_unexpected(GetWindowsError()); + } + lib.m_handle = CAST(h, PVOID); +#else + // RTLD_LAZY: Resolve symbols only as code executes (Standard for plugins) + void *h = dlopen(fullPath.c_str(), RTLD_LAZY | RTLD_LOCAL); + if (!h) + { + // dlerror returns a string describing the last error + const char *err = dlerror(); + return tl::make_unexpected(String(err ? err : "Unknown dlopen error")); + } + lib.m_handle = h; +#endif + + return IA_MOVE(lib); + } + + NO_DISCARD("Check if symbol exists") + + tl::expected GetSymbol(CONST String &name) CONST + { + if (!m_handle) + return tl::make_unexpected(String("Library not loaded")); + + PVOID sym = nullptr; + +#if IA_PLATFORM_WINDOWS + sym = CAST(GetProcAddress(CAST(m_handle, HMODULE), name.c_str()), PVOID); + if (!sym) + return tl::make_unexpected(GetWindowsError()); +#else + // Clear any previous error + dlerror(); + sym = dlsym(m_handle, name.c_str()); + const char *err = dlerror(); + if (err) + return tl::make_unexpected(String(err)); +#endif + + return sym; + } + + // Template helper for casting + template tl::expected GetFunction(CONST String &name) CONST + { + auto res = GetSymbol(name); + if (!res) + return tl::make_unexpected(res.error()); + return REINTERPRET(*res, FuncT); + } + + VOID Unload() + { + if (m_handle) + { +#if IA_PLATFORM_WINDOWS + FreeLibrary(CAST(m_handle, HMODULE)); +#else + dlclose(m_handle); +#endif + m_handle = nullptr; + } + } + + BOOL IsLoaded() CONST + { + return m_handle != nullptr; + } + + private: + PVOID m_handle; + +#if IA_PLATFORM_WINDOWS + STATIC String GetWindowsError() + { + DWORD errorID = ::GetLastError(); + if (errorID == 0) + return String(); + + LPSTR messageBuffer = nullptr; + size_t size = FormatMessageA( + FORMAT_MESSAGE_ALLOCATE_BUFFER | FORMAT_MESSAGE_FROM_SYSTEM | FORMAT_MESSAGE_IGNORE_INSERTS, NULL, + errorID, MAKELANGID(LANG_NEUTRAL, SUBLANG_DEFAULT), (LPSTR) &messageBuffer, 0, NULL); + + String message(messageBuffer, size); + LocalFree(messageBuffer); + return String("Win32 Error: ") + message; + } +#endif + }; +} // namespace IACore \ No newline at end of file diff --git a/Src/IACore/inc/IACore/Environment.hpp b/Src/IACore/inc/IACore/Environment.hpp new file mode 100644 index 0000000..d9dae26 --- /dev/null +++ b/Src/IACore/inc/IACore/Environment.hpp @@ -0,0 +1,103 @@ +// IACore-OSS; The Core Library for All IA Open Source Projects +// Copyright (C) 2025 IAS (ias@iasoft.dev) +// +// 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. + +#pragma once + +#include + +namespace IACore +{ + class Environment + { + public: + STATIC Optional Find(CONST String &name) + { +#if IA_PLATFORM_WINDOWS + DWORD bufferSize = GetEnvironmentVariableA(name.c_str(), nullptr, 0); + + if (bufferSize == 0) + { + // DWORD 0 means failed + return std::nullopt; + } + + // 2. Allocate (bufferSize includes the null terminator request) + String result; + result.resize(bufferSize); + + // 3. Fetch + // Returns num chars written excluding null terminator + DWORD actualSize = GetEnvironmentVariableA(name.c_str(), result.data(), bufferSize); + + if (actualSize == 0 || actualSize > bufferSize) + { + return std::nullopt; + } + + // Resize down to exclude the null terminator and any slack + result.resize(actualSize); + return result; + +#else + // getenv returns a pointer to the environment area + const char *val = std::getenv(name.c_str()); + if (val == nullptr) + { + return std::nullopt; + } + return String(val); +#endif + } + + STATIC String Get(CONST String &name, CONST String &defaultValue = "") + { + return Find(name).value_or(defaultValue); + } + + STATIC BOOL Set(CONST String &name, CONST String &value) + { + if (name.empty()) + return FALSE; + +#if IA_PLATFORM_WINDOWS + return SetEnvironmentVariableA(name.c_str(), value.c_str()) != 0; +#else + // Returns 0 on success, -1 on error + return setenv(name.c_str(), value.c_str(), 1) == 0; +#endif + } + + STATIC BOOL Unset(CONST String &name) + { + if (name.empty()) + return FALSE; + +#if IA_PLATFORM_WINDOWS + return SetEnvironmentVariableA(name.c_str(), nullptr) != 0; +#else + return unsetenv(name.c_str()) == 0; +#endif + } + + STATIC BOOL Exists(CONST String &name) + { +#if IA_PLATFORM_WINDOWS + return GetEnvironmentVariableA(name.c_str(), nullptr, 0) > 0; +#else + return std::getenv(name.c_str()) != nullptr; +#endif + } + }; +} // namespace IACore \ No newline at end of file diff --git a/Src/IACore/inc/IACore/FileOps.hpp b/Src/IACore/inc/IACore/FileOps.hpp new file mode 100644 index 0000000..030d774 --- /dev/null +++ b/Src/IACore/inc/IACore/FileOps.hpp @@ -0,0 +1,49 @@ +// IACore-OSS; The Core Library for All IA Open Source Projects +// Copyright (C) 2025 IAS (ias@iasoft.dev) +// +// 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. + +#pragma once + +#include +#include + +namespace IACore +{ + class FileOps + { + public: + STATIC FilePath NormalizeExecutablePath(IN CONST FilePath &path); + + public: + STATIC VOID UnmapFile(IN PCUINT8 mappedPtr); + STATIC Expected MapFile(IN CONST FilePath &path, OUT SIZE_T &size); + + // @param `isOwner` TRUE to allocate/truncate. FALSE to just open. + STATIC Expected MapSharedMemory(IN CONST String &name, IN SIZE_T size, IN BOOL isOwner); + STATIC VOID UnlinkSharedMemory(IN CONST String &name); + + STATIC Expected StreamFromFile(IN CONST FilePath &path); + STATIC Expected StreamToFile(IN CONST FilePath &path, IN BOOL overwrite = false); + + STATIC Expected ReadTextFile(IN CONST FilePath &path); + STATIC Expected, String> ReadBinaryFile(IN CONST FilePath &path); + STATIC Expected WriteTextFile(IN CONST FilePath &path, IN CONST String &contents, + IN BOOL overwrite = false); + STATIC Expected WriteBinaryFile(IN CONST FilePath &path, IN Span contents, + IN BOOL overwrite = false); + + private: + STATIC UnorderedMap> s_mappedFiles; + }; +} // namespace IACore \ No newline at end of file diff --git a/Src/IACore/inc/IACore/HttpClient.hpp b/Src/IACore/inc/IACore/HttpClient.hpp new file mode 100644 index 0000000..684c3fe --- /dev/null +++ b/Src/IACore/inc/IACore/HttpClient.hpp @@ -0,0 +1,195 @@ +// IACore-OSS; The Core Library for All IA Open Source Projects +// Copyright (C) 2025 IAS (ias@iasoft.dev) +// +// 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. + +#pragma once + +#include + +namespace IACore +{ + class HttpClient + { + public: + enum class EHeaderType + { + ACCEPT, + ACCEPT_CHARSET, + ACCEPT_ENCODING, + ACCEPT_LANGUAGE, + AUTHORIZATION, + CACHE_CONTROL, + CONNECTION, + CONTENT_LENGTH, + CONTENT_TYPE, + COOKIE, + DATE, + EXPECT, + HOST, + IF_MATCH, + IF_MODIFIED_SINCE, + IF_NONE_MATCH, + ORIGIN, + PRAGMA, + PROXY_AUTHORIZATION, + RANGE, + REFERER, + TE, + UPGRADE, + USER_AGENT, + VIA, + WARNING + }; + + enum class EResponseCode : INT32 + { + // 1xx Informational + CONTINUE = 100, + SWITCHING_PROTOCOLS = 101, + PROCESSING = 102, + EARLY_HINTS = 103, + + // 2xx Success + OK = 200, + CREATED = 201, + ACCEPTED = 202, + NON_AUTHORITATIVE_INFORMATION = 203, + NO_CONTENT = 204, + RESET_CONTENT = 205, + PARTIAL_CONTENT = 206, + MULTI_STATUS = 207, + ALREADY_REPORTED = 208, + IM_USED = 226, + + // 3xx Redirection + MULTIPLE_CHOICES = 300, + MOVED_PERMANENTLY = 301, + FOUND = 302, + SEE_OTHER = 303, + NOT_MODIFIED = 304, + USE_PROXY = 305, + TEMPORARY_REDIRECT = 307, + PERMANENT_REDIRECT = 308, + + // 4xx Client Error + BAD_REQUEST = 400, + UNAUTHORIZED = 401, + PAYMENT_REQUIRED = 402, + FORBIDDEN = 403, + NOT_FOUND = 404, + METHOD_NOT_ALLOWED = 405, + NOT_ACCEPTABLE = 406, + PROXY_AUTHENTICATION_REQUIRED = 407, + REQUEST_TIMEOUT = 408, + CONFLICT = 409, + GONE = 410, + LENGTH_REQUIRED = 411, + PRECONDITION_FAILED = 412, + PAYLOAD_TOO_LARGE = 413, + URI_TOO_LONG = 414, + UNSUPPORTED_MEDIA_TYPE = 415, + RANGE_NOT_SATISFIABLE = 416, + EXPECTATION_FAILED = 417, + IM_A_TEAPOT = 418, + MISDIRECTED_REQUEST = 421, + UNPROCESSABLE_ENTITY = 422, + LOCKED = 423, + FAILED_DEPENDENCY = 424, + TOO_EARLY = 425, + UPGRADE_REQUIRED = 426, + PRECONDITION_REQUIRED = 428, + TOO_MANY_REQUESTS = 429, + REQUEST_HEADER_FIELDS_TOO_LARGE = 431, + UNAVAILABLE_FOR_LEGAL_REASONS = 451, + + // 5xx Server Error + INTERNAL_SERVER_ERROR = 500, + NOT_IMPLEMENTED = 501, + BAD_GATEWAY = 502, + SERVICE_UNAVAILABLE = 503, + GATEWAY_TIMEOUT = 504, + HTTP_VERSION_NOT_SUPPORTED = 505, + VARIANT_ALSO_NEGOTIATES = 506, + INSUFFICIENT_STORAGE = 507, + LOOP_DETECTED = 508, + NOT_EXTENDED = 510, + NETWORK_AUTHENTICATION_REQUIRED = 511 + }; + + using Header = KeyValuePair; + + public: + HttpClient(IN CONST String &host); + ~HttpClient(); + + public: + Expected RawGet(IN CONST String &path, IN Span headers, + IN PCCHAR defaultContentType = "application/x-www-form-urlencoded"); + Expected RawPost(IN CONST String &path, IN Span headers, IN CONST String &body, + IN PCCHAR defaultContentType = "application/x-www-form-urlencoded"); + + template + Expected<_response_type, String> JsonGet(IN CONST String &path, IN Span headers); + + template + Expected<_response_type, String> JsonPost(IN CONST String &path, IN Span headers, + IN CONST _payload_type &body); + + public: + STATIC String UrlEncode(IN CONST String &value); + STATIC String UrlDecode(IN CONST String &value); + + STATIC String HeaderTypeToString(IN EHeaderType type); + STATIC Header CreateHeader(IN EHeaderType key, IN CONST String &value); + + STATIC BOOL IsSuccessResponseCode(IN EResponseCode code); + + public: + EResponseCode LastResponseCode() + { + return m_lastResponseCode; + } + + private: + PVOID m_client{}; + EResponseCode m_lastResponseCode; + + private: + String PreprocessResponse(IN CONST String &response); + }; + + template + Expected<_response_type, String> HttpClient::JsonGet(IN CONST String &path, IN Span headers) + { + const auto rawResponse = RawGet(path, headers, "application/json"); + if (!rawResponse) + return MakeUnexpected(rawResponse.error()); + if (LastResponseCode() != EResponseCode::OK) + return MakeUnexpected(std::format("Server responded with code {}", (INT32) LastResponseCode())); + return JSON::ParseToStruct<_response_type>(*rawResponse); + } + + template + Expected<_response_type, String> HttpClient::JsonPost(IN CONST String &path, IN Span headers, + IN CONST _payload_type &body) + { + const auto encodedBody = IA_TRY(JSON::EncodeStruct(body)); + const auto rawResponse = RawPost(path, headers, encodedBody, "application/json"); + if (!rawResponse) + return MakeUnexpected(rawResponse.error()); + if (LastResponseCode() != EResponseCode::OK) + return MakeUnexpected(std::format("Server responded with code {}", (INT32) LastResponseCode())); + return JSON::ParseToStruct<_response_type>(*rawResponse); + } +} // namespace IACore \ No newline at end of file diff --git a/Src/IACore/inc/IACore/IACore.hpp b/Src/IACore/inc/IACore/IACore.hpp new file mode 100644 index 0000000..08d24e7 --- /dev/null +++ b/Src/IACore/inc/IACore/IACore.hpp @@ -0,0 +1,41 @@ +// IACore-OSS; The Core Library for All IA Open Source Projects +// Copyright (C) 2025 IAS (ias@iasoft.dev) +// +// 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. + +#pragma once + +#include + +#ifdef __cplusplus + +namespace IACore +{ + // Must be called from main thread + VOID Initialize(); + // Must be called from same thread as Initialize + VOID Terminate(); + + UINT64 GetUnixTime(); + UINT64 GetTicksCount(); + FLOAT64 GetSecondsCount(); + + FLOAT32 GetRandom(); + UINT32 GetRandom(IN UINT32 seed); + INT64 GetRandom(IN INT64 min, IN INT64 max); + + BOOL IsMainThread(); + VOID Sleep(IN UINT64 milliseconds); +} // namespace IACore + +#endif diff --git a/Src/IACore/inc/IACore/IATest.hpp b/Src/IACore/inc/IACore/IATest.hpp new file mode 100644 index 0000000..2e6eb21 --- /dev/null +++ b/Src/IACore/inc/IACore/IATest.hpp @@ -0,0 +1,333 @@ +// IACore-OSS; The Core Library for All IA Open Source Projects +// Copyright (C) 2025 IAS (ias@iasoft.dev) +// +// 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. + +#pragma once + +#include + +#ifdef __cplusplus + +# include + +// ----------------------------------------------------------------------------- +// Macros +// ----------------------------------------------------------------------------- + +# define valid_iatest_runner(type) iatest::_valid_iatest_runner::value_type + +# define __iat_micro_test(call) \ + if (!(call)) \ + return FALSE + +# define IAT_CHECK(v) __iat_micro_test(_test((v), #v)) +# define IAT_CHECK_NOT(v) __iat_micro_test(_test_not((v), "NOT " #v)) +# define IAT_CHECK_EQ(lhs, rhs) __iat_micro_test(_test_eq((lhs), (rhs), #lhs " == " #rhs)) +# define IAT_CHECK_NEQ(lhs, rhs) __iat_micro_test(_test_neq((lhs), (rhs), #lhs " != " #rhs)) + +# define IAT_CHECK_APPROX(lhs, rhs) __iat_micro_test(_test_approx((lhs), (rhs), #lhs " ~= " #rhs)) + +# define IAT_UNIT(func) _test_unit([this]() { return this->func(); }, #func) +# define IAT_NAMED_UNIT(n, func) _test_unit([this]() { return this->func(); }, n) + +# define IAT_BLOCK(name) class name : public ia::iatest::block + +# define IAT_BEGIN_BLOCK(_group, _name) \ + class _group##_##_name : public ia::iatest::block \ + { \ + public: \ + PCCHAR name() CONST OVERRIDE \ + { \ + return #_group "::" #_name; \ + } \ + \ + private: + +# define IAT_END_BLOCK() \ + } \ + ; + +# define IAT_BEGIN_TEST_LIST() \ + public: \ + VOID declareTests() OVERRIDE \ + { +# define IAT_ADD_TEST(name) IAT_UNIT(name) +# define IAT_END_TEST_LIST() \ + } \ + \ + private: + +namespace ia::iatest +{ + template std::string ToString(CONST T &value) + { + if constexpr (std::is_arithmetic_v) + { + return std::to_string(value); + } + else if constexpr (std::is_same_v || std::is_same_v) + { + return std::string("\"") + value + "\""; + } + else + { + return "{Object}"; // Fallback for complex types + } + } + + template std::string ToString(T *value) + { + if (value == NULLPTR) + return "nullptr"; + std::stringstream ss; + ss << "ptr(" << (void *) value << ")"; + return ss.str(); + } + + DEFINE_TYPE(functor_t, std::function); + + struct unit_t + { + std::string Name; + functor_t Functor; + }; + + class block + { + public: + virtual ~block() = default; + PURE_VIRTUAL(PCCHAR name() CONST); + PURE_VIRTUAL(VOID declareTests()); + + std::vector &units() + { + return m_units; + } + + protected: + template BOOL _test_eq(IN CONST T1 &lhs, IN CONST T2 &rhs, IN PCCHAR description) + { + if (lhs != rhs) + { + print_fail(description, ToString(lhs), ToString(rhs)); + return FALSE; + } + return TRUE; + } + + template BOOL _test_neq(IN CONST T1 &lhs, IN CONST T2 &rhs, IN PCCHAR description) + { + if (lhs == rhs) + { + print_fail(description, ToString(lhs), "NOT " + ToString(rhs)); + return FALSE; + } + return TRUE; + } + + template BOOL _test_approx(IN T lhs, IN T rhs, IN PCCHAR description) + { + static_assert(std::is_floating_point_v, "Approx only works for floats/doubles"); + T diff = std::abs(lhs - rhs); + if (diff > static_cast(0.0001)) + { + print_fail(description, ToString(lhs), ToString(rhs)); + return FALSE; + } + return TRUE; + } + + BOOL _test(IN BOOL value, IN PCCHAR description) + { + if (!value) + { + printf(__CC_BLUE " %s... " __CC_RED "FAILED" __CC_DEFAULT "\n", description); + return FALSE; + } + return TRUE; + } + + BOOL _test_not(IN BOOL value, IN PCCHAR description) + { + if (value) + { + printf(__CC_BLUE " %s... " __CC_RED "FAILED" __CC_DEFAULT "\n", description); + return FALSE; + } + return TRUE; + } + + VOID _test_unit(IN functor_t functor, IN PCCHAR name) + { + m_units.push_back({name, functor}); + } + + private: + VOID print_fail(PCCHAR desc, std::string v1, std::string v2) + { + printf(__CC_BLUE " %s... " __CC_RED "FAILED" __CC_DEFAULT "\n", desc); + printf(__CC_RED " Expected: %s" __CC_DEFAULT "\n", v2.c_str()); + printf(__CC_RED " Actual: %s" __CC_DEFAULT "\n", v1.c_str()); + } + + std::vector m_units; + }; + + template + concept valid_block_class = std::derived_from; + + template class runner + { + public: + runner() + { + } + + ~runner() + { + summarize(); + } + + template + requires valid_block_class + VOID testBlock(); + + private: + VOID summarize(); + + private: + SIZE_T m_testCount{0}; + SIZE_T m_failCount{0}; + SIZE_T m_blockCount{0}; + }; + + template + template + requires valid_block_class + VOID runner::testBlock() + { + m_blockCount++; + block_class b; + b.declareTests(); + + printf(__CC_MAGENTA "Testing [%s]..." __CC_DEFAULT "\n", b.name()); + + for (auto &v : b.units()) + { + m_testCount++; + if constexpr (isVerbose) + { + printf(__CC_YELLOW " Testing %s...\n" __CC_DEFAULT, v.Name.c_str()); + } + + BOOL result = FALSE; + try + { + result = v.Functor(); + } + catch (const std::exception &e) + { + printf(__CC_RED " CRITICAL EXCEPTION in %s: %s\n" __CC_DEFAULT, v.Name.c_str(), e.what()); + result = FALSE; + } + catch (...) + { + printf(__CC_RED " UNKNOWN CRITICAL EXCEPTION in %s\n" __CC_DEFAULT, v.Name.c_str()); + result = FALSE; + } + + if (!result) + { + m_failCount++; + if constexpr (stopOnFail) + { + summarize(); + exit(-1); + } + } + } + fputs("\n", stdout); + } + + template VOID runner::summarize() + { + printf(__CC_GREEN + "\n-----------------------------------\n\t SUMMARY\n-----------------------------------\n"); + + if (!m_failCount) + { + printf("\n\tALL TESTS PASSED!\n\n"); + } + else + { + FLOAT64 successRate = + (100.0 * static_cast(m_testCount - m_failCount) / static_cast(m_testCount)); + printf(__CC_RED "%zu OUT OF %zu TESTS FAILED\n" __CC_YELLOW "Success Rate: %.2f%%\n", m_failCount, + m_testCount, successRate); + } + + printf(__CC_MAGENTA "Ran %zu test(s) across %zu block(s)\n" __CC_GREEN + "-----------------------------------" __CC_DEFAULT "\n", + m_testCount, m_blockCount); + } + + template struct _valid_iatest_runner : std::false_type + { + }; + + template + struct _valid_iatest_runner> : std::true_type + { + }; + + using DefaultRunner = runner; + + class TestRegistry + { + public: + using TestEntry = std::function; + + static std::vector &GetEntries() + { + static std::vector entries; + return entries; + } + + static int RunAll() + { + DefaultRunner r; + auto &entries = GetEntries(); + printf(__CC_CYAN "[IATest] Discovered %zu Test Blocks\n\n" __CC_DEFAULT, entries.size()); + + for (auto &entry : entries) + { + entry(r); + } + // The destructor of 'r' will automatically print the summary + return 0; + } + }; + + template struct AutoRegister + { + AutoRegister() + { + TestRegistry::GetEntries().push_back([](DefaultRunner &r) { r.testBlock(); }); + } + }; +} // namespace ia::iatest + +# define IAT_REGISTER_ENTRY(Group, Name) static ia::iatest::AutoRegister _iat_reg_##Group##_##Name; + +#endif // __cplusplus \ No newline at end of file diff --git a/Src/IACore/inc/IACore/IPC.hpp b/Src/IACore/inc/IACore/IPC.hpp new file mode 100644 index 0000000..f9dacb7 --- /dev/null +++ b/Src/IACore/inc/IACore/IPC.hpp @@ -0,0 +1,151 @@ +// IACore-OSS; The Core Library for All IA Open Source Projects +// Copyright (C) 2025 IAS (ias@iasoft.dev) +// +// 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. + +#pragma once + +#include +#include +#include + +namespace IACore +{ + using IPC_PacketHeader = RingBufferView::PacketHeader; + + struct alignas(64) IPC_SharedMemoryLayout + { + // ========================================================= + // SECTION 1: METADATA & HANDSHAKE + // ========================================================= + struct Header + { + UINT32 Magic; // 0x49414950 ("IAIP") + UINT32 Version; // 1 + UINT64 TotalSize; // Total size of SHM block + } Meta; + + // Pad to ensure MONI starts on a fresh cache line (64 bytes) + UINT8 _pad0[64 - sizeof(Header)]; + + // ========================================================= + // SECTION 2: RING BUFFER CONTROL BLOCKS + // ========================================================= + + // RingBufferView ControlBlock is already 64-byte aligned internally. + RingBufferView::ControlBlock MONI_Control; + RingBufferView::ControlBlock MINO_Control; + + // ========================================================= + // SECTION 3: DATA BUFFER OFFSETS + // ========================================================= + + UINT64 MONI_DataOffset; + UINT64 MONI_DataSize; + + UINT64 MINO_DataOffset; + UINT64 MINO_DataSize; + + // Pad to ensure the actual Data Buffer starts on a fresh cache line + UINT8 _pad1[64 - (sizeof(UINT64) * 4)]; + + static constexpr size_t GetHeaderSize() + { + return sizeof(IPC_SharedMemoryLayout); + } + }; + + // Static assert to ensure manual padding logic is correct + static_assert(sizeof(IPC_SharedMemoryLayout) % 64 == 0, "IPC Layout is not cache-line aligned!"); + + class IPC_Node + { + public: + virtual ~IPC_Node(); + + // When Manager spawns a node, `connectionString` is passed + // as the first command line argument + Expected Connect(IN PCCHAR connectionString); + + VOID Update(); + + VOID SendSignal(IN UINT8 signal); + VOID SendPacket(IN UINT16 packetID, IN Span payload); + + protected: + PURE_VIRTUAL(VOID OnSignal(IN UINT8 signal)); + PURE_VIRTUAL(VOID OnPacket(IN UINT16 packetID, IN Span payload)); + + private: + String m_shmName; + PUINT8 m_sharedMemory{}; + Vector m_receiveBuffer; + SocketHandle m_socket{INVALID_SOCKET}; + + UniquePtr MONI; // Manager Out, Node In + UniquePtr MINO; // Manager In, Node Out + }; + + class IPC_Manager + { + struct NodeSession + { + SteadyTimePoint CreationTime{}; + SharedPtr ProcessHandle; + + Mutex SendMutex; + + String SharedMemName; + PUINT8 MappedPtr{}; + + SocketHandle ListenerSocket{INVALID_SOCKET}; + SocketHandle DataSocket{INVALID_SOCKET}; + + UniquePtr MONI; // Manager Out, Node In + UniquePtr MINO; // Manager In, Node Out + + BOOL IsReady{FALSE}; + + VOID SendSignal(IN UINT8 signal); + VOID SendPacket(IN UINT16 packetID, IN Span payload); + }; + + public: + STATIC CONSTEXPR UINT32 DEFAULT_NODE_SHARED_MEMORY_SIZE = SIZE_MB(4); + + public: + IPC_Manager(); + virtual ~IPC_Manager(); + + VOID Update(); + + Expected SpawnNode(IN CONST FilePath &executablePath, + IN UINT32 sharedMemorySize = DEFAULT_NODE_SHARED_MEMORY_SIZE); + BOOL WaitTillNodeIsOnline(IN NativeProcessID node); + + VOID ShutdownNode(IN NativeProcessID node); + + VOID SendSignal(IN NativeProcessID node, IN UINT8 signal); + VOID SendPacket(IN NativeProcessID node, IN UINT16 packetID, IN Span payload); + + protected: + PURE_VIRTUAL(VOID OnSignal(IN NativeProcessID node, IN UINT8 signal)); + PURE_VIRTUAL(VOID OnPacket(IN NativeProcessID node, IN UINT16 packetID, IN Span payload)); + + private: + Vector m_receiveBuffer; + Vector> m_activeSessions; + Vector> m_pendingSessions; + UnorderedMap m_activeSessionMap; + }; +} // namespace IACore \ No newline at end of file diff --git a/Src/IACore/inc/IACore/JSON.hpp b/Src/IACore/inc/IACore/JSON.hpp new file mode 100644 index 0000000..21f8a6c --- /dev/null +++ b/Src/IACore/inc/IACore/JSON.hpp @@ -0,0 +1,58 @@ +// IACore-OSS; The Core Library for All IA Open Source Projects +// Copyright (C) 2025 IAS (ias@iasoft.dev) +// +// 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. + +#pragma once + +#include + +#include +#include +#include + +namespace IACore +{ + class JSON + { + private: + STATIC CONSTEXPR AUTO GLAZE_JSON_OPTS = glz::opts{.error_on_unknown_keys = false}; + + public: + STATIC Expected Parse(IN CONST String &json); + STATIC Expected, simdjson::dom::object>, String> ParseReadOnly( + IN CONST String &json); + template STATIC Expected<_object_type, String> ParseToStruct(IN CONST String &json); + + STATIC String Encode(IN nlohmann::json data); + template STATIC Expected EncodeStruct(IN CONST _object_type &data); + }; + + template Expected<_object_type, String> JSON::ParseToStruct(IN CONST String &json) + { + _object_type result{}; + const auto parseError = glz::read_json(result, json); + if (parseError) + return MakeUnexpected(std::format("JSON Error: {}", glz::format_error(parseError, json))); + return result; + } + + template Expected JSON::EncodeStruct(IN CONST _object_type &data) + { + String result; + const auto encodeError = glz::write_json(data, result); + if (encodeError) + return MakeUnexpected(std::format("JSON Error: {}", glz::format_error(encodeError))); + return result; + } +} // namespace IACore \ No newline at end of file diff --git a/Src/IACore/inc/IACore/Logger.hpp b/Src/IACore/inc/IACore/Logger.hpp new file mode 100644 index 0000000..ff5e8c0 --- /dev/null +++ b/Src/IACore/inc/IACore/Logger.hpp @@ -0,0 +1,118 @@ +// IACore-OSS; The Core Library for All IA Open Source Projects +// Copyright (C) 2025 IAS (ias@iasoft.dev) +// +// 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. + +#pragma once + +#include + +namespace IACore +{ + class Logger + { + public: + enum class ELogLevel + { + VERBOSE, + INFO, + WARN, + ERROR + }; + + public: + STATIC BOOL EnableLoggingToDisk(IN PCCHAR filePath); + STATIC VOID SetLogLevel(IN ELogLevel logLevel); + + template STATIC VOID Status(FormatterString fmt, Args &&...args) + { + LogStatus(std::vformat(fmt.get(), std::make_format_args(args...))); + } + + template STATIC VOID Info(FormatterString fmt, Args &&...args) + { + LogInfo(std::vformat(fmt.get(), std::make_format_args(args...))); + } + + template STATIC VOID Warn(FormatterString fmt, Args &&...args) + { + LogWarn(std::vformat(fmt.get(), std::make_format_args(args...))); + } + + template STATIC VOID Error(FormatterString fmt, Args &&...args) + { + LogError(std::vformat(fmt.get(), std::make_format_args(args...))); + } + + STATIC VOID FlushLogs(); + + private: +#if IA_DISABLE_LOGGING > 0 + STATIC VOID LogStatus(IN String &&msg) + { + UNUSED(msg); + } + + STATIC VOID LogInfo(IN String &&msg) + { + UNUSED(msg); + } + + STATIC VOID LogWarn(IN String &&msg) + { + UNUSED(msg); + } + + STATIC VOID LogError(IN String &&msg) + { + UNUSED(msg); + } +#else + STATIC VOID LogStatus(IN String &&msg) + { + if (s_logLevel <= ELogLevel::VERBOSE) + LogInternal(__CC_WHITE, "STATUS", IA_MOVE(msg)); + } + + STATIC VOID LogInfo(IN String &&msg) + { + if (s_logLevel <= ELogLevel::INFO) + LogInternal(__CC_GREEN, "INFO", IA_MOVE(msg)); + } + + STATIC VOID LogWarn(IN String &&msg) + { + if (s_logLevel <= ELogLevel::WARN) + LogInternal(__CC_YELLOW, "WARN", IA_MOVE(msg)); + } + + STATIC VOID LogError(IN String &&msg) + { + if (s_logLevel <= ELogLevel::ERROR) + LogInternal(__CC_RED, "ERROR", IA_MOVE(msg)); + } +#endif + + STATIC VOID LogInternal(IN PCCHAR prefix, IN PCCHAR tag, IN String &&msg); + + private: + STATIC ELogLevel s_logLevel; + STATIC std::ofstream s_logFile; + + STATIC VOID Initialize(); + STATIC VOID Terminate(); + + friend VOID Initialize(); + friend VOID Terminate(); + }; +} // namespace IACore \ No newline at end of file diff --git a/Src/IACore/inc/IACore/PCH.hpp b/Src/IACore/inc/IACore/PCH.hpp new file mode 100644 index 0000000..c37bb8e --- /dev/null +++ b/Src/IACore/inc/IACore/PCH.hpp @@ -0,0 +1,586 @@ +// IACore-OSS; The Core Library for All IA Open Source Projects +// Copyright (C) 2025 IAS (ias@iasoft.dev) +// +// 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. + +#pragma once + +// ------------------------------------------------------------------------- +// Platform Detection +// ------------------------------------------------------------------------- +#if defined(WIN32) || defined(_WIN32) || defined(__WIN32__) || defined(__NT__) +# ifdef _WIN64 +# define IA_PLATFORM_WIN64 1 +# define IA_PLATFORM_WINDOWS 1 +# else +# define IA_PLATFORM_WIN32 1 +# define IA_PLATFORM_WINDOWS 1 +# endif +#elif __APPLE__ +# include +# if TARGET_OS_IPHONE || TARGET_IPHONE_SIMULATOR || TARGET_OS_MACCATALYST +# define IA_PLATFORM_IOS 1 +# elif TARGET_OS_MAC +# define IA_PLATFORM_MAC 1 +# endif +# define IA_PLATFORM_UNIX 1 +#elif __ANDROID__ +# define IA_PLATFORM_ANDROID 1 +# define IA_PLATFORM_LINUX 1 +# define IA_PLATFORM_UNIX 1 +#elif __linux__ +# define IA_PLATFORM_LINUX 1 +# define IA_PLATFORM_UNIX 1 +#elif __unix__ +# define IA_PLATFORM_UNIX 1 +#endif + +#if IA_PLATFORM_WIN32 || IA_PLATFORM_WIN64 +# ifndef WIN32_LEAN_AND_MEAN +# define WIN32_LEAN_AND_MEAN +# endif +# ifndef NOMINMAX +# define NOMINMAX +# endif +# include +# undef VOID +# undef ERROR +#elif IA_PLATFORM_UNIX +# include +# include +# include +# include +# include +# include +# include +#endif + +// ----------------------------------------------------------------------------- +// Configuration Macros +// ----------------------------------------------------------------------------- + +#define IA_CHECK(o) (o > 0) + +#if defined(__IA_DEBUG) && __IA_DEBUG +# define __DEBUG_MODE__ +# define __BUILD_MODE_NAME "debug" +# define DEBUG_ONLY(v) v +# ifndef _DEBUG +# define _DEBUG +# endif +#else +# define __RELEASE_MODE__ +# define __BUILD_MODE_NAME "release" +# ifndef NDEBUG +# define NDEBUG +# endif +# ifndef __OPTIMIZE__ +# define __OPTIMIZE__ +# endif +# define DEBUG_ONLY(f) +#endif + +#if IA_CHECK(IA_PLATFORM_WIN64) || IA_CHECK(IA_PLATFORM_UNIX) +# define IA_CORE_PLATFORM_FEATURES 1 +#else +# define IA_CORE_PLATFORM_FEATURES 0 +# warning "IACore Unsupported Platform: Platform specific features will be disabled" +#endif + +#include +#include +#include +#include +#include + +#ifdef __cplusplus +# include +# include +# include +# include +# include +# include +# include +# include +# include +# include +# include +# include +# include +# include +# include +# include +# include +# include +# include + +# include +# include +# include +# include +# include +# include +# include +# include +# include + +# include +# include + +#else +# include +# include +# include +# include +#endif + +// ----------------------------------------------------------------------------- +// Security Macros +// ----------------------------------------------------------------------------- + +#define IA_PANIC(msg) \ + { \ + fprintf(stderr, "PANIC: %s\n", msg); \ + __builtin_trap(); \ + } + +// Advanced Security features are not included in OSS builds +// (OSS version does not implement 'IAC_CHECK_*'s) +#define IACORE_SECURITY_LEVEL 0 + +#define IAC_SEC_LEVEL(v) (IACORE_SECURITY_LEVEL >= v) + +#if __IA_DEBUG || IAC_SEC_LEVEL(1) +# define __IAC_OVERFLOW_CHECKS 1 +#else +# define __IAC_OVERFLOW_CHECKS 0 +#endif +#if __IA_DEBUG || IAC_SEC_LEVEL(2) +# define __IAC_SANITY_CHECKS 1 +#else +# define __IAC_SANITY_CHECKS 0 +#endif + +// ----------------------------------------------------------------------------- +// Language Abstraction Macros +// ----------------------------------------------------------------------------- + +#define AUTO auto +#define CONST const +#define STATIC static +#define EXTERN extern + +#ifdef __cplusplus +# define VIRTUAL virtual +# define OVERRIDE override +# define CONSTEXPR constexpr +# define NOEXCEPT noexcept +# define NULLPTR nullptr +# define IA_MOVE(...) std::move(__VA_ARGS__) +# define DECONST(t, v) const_cast(v) +# define NORETURN [[noreturn]] +#else +# define VIRTUAL +# define OVERRIDE +# define CONSTEXPR const +# define NOEXCEPT +# define NULLPTR NULL +# define IA_MOVE(...) (__VA_ARGS__) +# define DECONST(t, v) ((t) (v)) +# define NORETURN +#endif + +#define DEFINE_TYPE(t, v) typedef v t +#define FORWARD_DECLARE(t, i) t i + +#ifdef __cplusplus +# define PURE_VIRTUAL(f) VIRTUAL f = 0 +#endif + +// ----------------------------------------------------------------------------- +// Attributes & Compiler Intrinsics +// ----------------------------------------------------------------------------- + +#define INLINE inline +#if defined(__clang__) || defined(__GNUC__) || defined(__GNUG__) +# define ALWAYS_INLINE __attribute__((always_inline)) inline +#elif defined(_MSC_VER) +# define ALWAYS_INLINE __forceinline +#else +# define ALWAYS_INLINE inline +#endif + +#define UNUSED(v) ((void) v); + +#if defined(__cplusplus) +# define NO_DISCARD(s) [[nodiscard(s)]] +# define B_LIKELY(cond) (cond) [[likely]] +# define B_UNLIKELY(cond) (cond) [[unlikely]] +#else +# define NO_DISCARD(s) +# define B_LIKELY(cond) (cond) +# define B_UNLIKELY(cond) (cond) +#endif + +#define __INTERNAL_IA_STRINGIFY(value) #value +#define IA_STRINGIFY(value) __INTERNAL_IA_STRINGIFY(value) + +#define ALIGN(a) __attribute__((aligned(a))) + +#define ASM(...) __asm__ volatile(__VA_ARGS__) + +#ifndef NULL +# define NULL 0 +#endif + +#ifndef _WIN32 +# undef TRUE +# undef FALSE +# ifdef __cplusplus +# define FALSE false +# define TRUE true +# else +# define FALSE 0 +# define TRUE 1 +# endif +#endif + +// Parameter Annotations +#define IN +#define OUT +#define INOUT + +// ----------------------------------------------------------------------------- +// Extern C Handling +// ----------------------------------------------------------------------------- + +#ifdef __cplusplus +# define IA_EXTERN_C_BEGIN \ + extern "C" \ + { +# define IA_EXTERN_C_END } +# define C_DECL(f) extern "C" f +#else +# define IA_EXTERN_C_BEGIN +# define IA_EXTERN_C_END +# define C_DECL(f) f +#endif + +// ----------------------------------------------------------------------------- +// Utilities +// ----------------------------------------------------------------------------- + +#ifdef __cplusplus +# define CAST(v, t) (static_cast(v)) +# define REINTERPRET(v, t) (reinterpret_cast(v)) +#else +# define CAST(v, t) ((t) (v)) +#endif + +// Templates and Aliases +#ifdef __cplusplus +# define ALIAS_FUNCTION(alias, function) \ + template auto alias(Args &&...args) -> decltype(function(std::forward(args)...)) \ + { \ + return function(std::forward(args)...); \ + } + +# define ALIAS_TEMPLATE_FUNCTION(t, alias, function) \ + template \ + auto alias(Args &&...args) -> decltype(function(std::forward(args)...)) \ + { \ + return function(std::forward(args)...); \ + } +#endif + +// Assertions +#define IA_RELEASE_ASSERT(v) assert((v)) +#define IA_RELEASE_ASSERT_MSG(v, m) assert((v) && m) + +#if defined(__DEBUG_MODE__) +# define IA_ASSERT(v) IA_RELEASE_ASSERT(v) +# define IA_ASSERT_MSG(v, m) IA_RELEASE_ASSERT_MSG(v, m) +#else +# define IA_ASSERT(v) +# define IA_ASSERT_MSG(v, m) +#endif + +#define IA_ASSERT_EQ(a, b) IA_ASSERT((a) == (b)) +#define IA_ASSERT_GE(a, b) IA_ASSERT((a) >= (b)) +#define IA_ASSERT_LE(a, b) IA_ASSERT(a <= b) +#define IA_ASSERT_LT(a, b) IA_ASSERT(a < b) +#define IA_ASSERT_GT(a, b) IA_ASSERT(a > b) +#define IA_ASSERT_IMPLIES(a, b) IA_ASSERT(!(a) || (b)) +#define IA_ASSERT_NOT_NULL(v) IA_ASSERT(((v) != NULLPTR)) + +#define IA_UNREACHABLE(msg) IA_RELEASE_ASSERT_MSG(FALSE, "Unreachable code: " msg) + +#define IA_TRY_PURE(expr) \ + { \ + auto _ia_res = (expr); \ + if (!_ia_res) \ + { \ + return tl::make_unexpected(std::move(_ia_res.error())); \ + } \ + } + +#define IA_TRY(expr) \ + __extension__({ \ + auto _ia_res = (expr); \ + if (!_ia_res) \ + { \ + return tl::make_unexpected(std::move(_ia_res.error())); \ + } \ + std::move(*_ia_res); \ + }) + +#define IA_CONCAT_IMPL(x, y) x##y +#define IA_CONCAT(x, y) IA_CONCAT_IMPL(x, y) +#define IA_UNIQUE_NAME(prefix) IA_CONCAT(prefix, __LINE__) + +#define SIZE_KB(v) (v * 1024) +#define SIZE_MB(v) (v * 1024 * 1024) +#define SIZE_GB(v) (v * 1024 * 1024 * 1024) + +#define ENSURE_BINARY_COMPATIBILITY(A, B) \ + static_assert(sizeof(A) == sizeof(B), \ + #A ", " #B " size mismatch! Do not add virtual functions or new member variables."); + +// ----------------------------------------------------------------------------- +// Limits & Versioning +// ----------------------------------------------------------------------------- + +#ifdef __cplusplus +# define IA_MAX_POSSIBLE_SIZE (static_cast(0x7FFFFFFFFFFFF)) +#else +# define IA_MAX_POSSIBLE_SIZE ((SIZE_T) (0x7FFFFFFFFFFFF)) +#endif + +#define IA_MAX_PATH_LENGTH 4096 +#define IA_MAX_STRING_LENGTH (IA_MAX_POSSIBLE_SIZE >> 8) + +#define IA_VERSION_TYPE uint64_t +#define IA_MAKE_VERSION(major, minor, patch) \ + ((((uint64_t) (major) & 0xFFFFFF) << 40) | (((uint64_t) (minor) & 0xFFFFFF) << 16) | ((uint64_t) (patch) & 0xFFFF)) + +// ----------------------------------------------------------------------------- +// DLL Export/Import +// ----------------------------------------------------------------------------- + +#if defined(_MSC_VER) +# define IA_DLL_EXPORT __declspec(dllexport) +# define IA_DLL_IMPORT __declspec(dllimport) +#elif defined(__GNUC__) +# define IA_DLL_EXPORT __attribute__((visibility("default"))) +# define IA_DLL_IMPORT +#else +# define IA_DLL_EXPORT +# define IA_DLL_IMPORT +#endif + +// ----------------------------------------------------------------------------- +// Console Colors (ANSI Escape Codes) +// ----------------------------------------------------------------------------- + +#define __CC_BLACK "\033[30m" +#define __CC_RED "\033[31m" +#define __CC_GREEN "\033[32m" +#define __CC_YELLOW "\033[33m" +#define __CC_BLUE "\033[34m" +#define __CC_MAGENTA "\033[35m" +#define __CC_CYAN "\033[36m" +#define __CC_WHITE "\033[37m" +#define __CC_DEFAULT "\033[39m" + +// ------------------------------------------------------------------------- +// Base Types +// ------------------------------------------------------------------------- +typedef void VOID; + +#ifndef _WIN32 +# ifdef __cplusplus +typedef bool BOOL; +# else +typedef _Bool BOOL; +# endif +#endif + +typedef char CHAR; +typedef uint16_t CHAR16; + +typedef int8_t INT8; +typedef int16_t INT16; +typedef int32_t INT32; +typedef int64_t INT64; + +typedef uint8_t UINT8; +typedef uint16_t UINT16; +typedef uint32_t UINT32; +typedef uint64_t UINT64; + +typedef float FLOAT32; +typedef double FLOAT64; + +typedef INT32 INT; +typedef UINT32 UINT; +typedef size_t SIZE_T; + +#ifdef __cplusplus +typedef std::make_signed_t SSIZE_T; +typedef std::align_val_t ALIGN_T; +#else +typedef ptrdiff_t SSIZE_T; +typedef size_t ALIGN_T; +#endif + +// ------------------------------------------------------------------------- +// Pointer Types +// ------------------------------------------------------------------------- +typedef VOID *PVOID; +typedef BOOL *PBOOL; +typedef CHAR *PCHAR; +typedef CHAR16 *PCHAR16; + +typedef INT8 *PINT8; +typedef INT16 *PINT16; +typedef INT32 *PINT32; +typedef INT64 *PINT64; + +typedef UINT8 *PUINT8; +typedef UINT16 *PUINT16; +typedef UINT32 *PUINT32; +typedef UINT64 *PUINT64; + +typedef INT *PINT; +typedef UINT *PUINT; + +typedef FLOAT32 *PFLOAT32; +typedef FLOAT64 *PFLOAT64; + +// ------------------------------------------------------------------------- +// Const Pointer Types +// ------------------------------------------------------------------------- +typedef CONST VOID *PCVOID; +typedef CONST BOOL *PCBOOL; +typedef CONST CHAR *PCCHAR; +typedef CONST CHAR16 *PCCHAR16; + +typedef CONST INT8 *PCINT8; +typedef CONST INT16 *PCINT16; +typedef CONST INT32 *PCINT32; +typedef CONST INT64 *PCINT64; + +typedef CONST UINT8 *PCUINT8; +typedef CONST UINT16 *PCUINT16; +typedef CONST UINT32 *PCUINT32; +typedef CONST UINT64 *PCUINT64; + +typedef CONST INT *PCINT; +typedef CONST UINT *PCUINT; +typedef CONST SIZE_T *PCSIZE; +typedef CONST SSIZE_T *PCSSIZE; + +typedef CONST FLOAT32 *PCFLOAT32; +typedef CONST FLOAT64 *PCFLOAT64; + +// ------------------------------------------------------------------------- +// GUID Structure +// ------------------------------------------------------------------------- +#ifndef _WIN32 +typedef struct _IA_GUID +{ + UINT32 Data1; + UINT16 Data2; + UINT16 Data3; + UINT8 Data4[8]; + +# ifdef __cplusplus + bool operator==(const _IA_GUID &other) const + { + return __builtin_memcmp(this, &other, sizeof(_IA_GUID)) == 0; + } + + bool operator!=(const _IA_GUID &other) const + { + return !(*this == other); + } +# endif +} GUID; +#endif + +STATIC INLINE BOOL IA_GUID_Equals(CONST GUID *a, CONST GUID *b) +{ + if (a == NULLPTR || b == NULLPTR) + return FALSE; + return memcmp(a, b, sizeof(GUID)) == 0; +} + +// ------------------------------------------------------------------------- +// Numeric Constants +// ------------------------------------------------------------------------- +#ifdef __cplusplus +STATIC CONSTEXPR FLOAT32 FLOAT32_EPSILON = std::numeric_limits::epsilon(); +STATIC CONSTEXPR FLOAT64 FLOAT64_EPSILON = std::numeric_limits::epsilon(); +#else +STATIC CONST FLOAT32 FLOAT32_EPSILON = FLT_EPSILON; +STATIC CONST FLOAT64 FLOAT64_EPSILON = DBL_EPSILON; +#endif + +// ------------------------------------------------------------------------- +// Containers and Helpers +// ------------------------------------------------------------------------- +#ifdef __cplusplus + +template using Function = std::function<_function_type>; +template using InitializerList = std::initializer_list<_value_type>; +template using Array = std::array<_value_type, count>; +template using Vector = std::vector<_value_type>; +template using Optional = std::optional<_value_type>; +template using UnorderedSet = ankerl::unordered_dense::set<_key_type>; +template using Span = std::span<_value_type>; +template +using UnorderedMap = ankerl::unordered_dense::map<_key_type, _value_type>; +template using Atomic = std::atomic<_value_type>; +template using SharedPtr = std::shared_ptr<_value_type>; +template using UniquePtr = std::unique_ptr<_value_type>; +template using Deque = std::deque<_value_type>; +template using Pair = std::pair<_type_a, _type_b>; +template using Tuple = std::tuple; +template using KeyValuePair = std::pair<_key_type, _value_type>; + +template +using Expected = tl::expected<_expected_type, _unexpected_type>; +ALIAS_FUNCTION(MakeUnexpected, tl::make_unexpected); + +using String = std::string; +using StringView = std::string_view; +using StringStream = std::stringstream; + +using SteadyClock = std::chrono::steady_clock; +using SteadyTimePoint = std::chrono::time_point; +using HighResClock = std::chrono::high_resolution_clock; +using HighResTimePoint = std::chrono::time_point; + +using Mutex = std::mutex; +using StopToken = std::stop_token; +using ScopedLock = std::scoped_lock; +using UniqueLock = std::unique_lock; +using JoiningThread = std::jthread; +using ConditionVariable = std::condition_variable; + +namespace FileSystem = std::filesystem; +using FilePath = FileSystem::path; + +template using FormatterString = std::format_string; + +#endif diff --git a/Src/IACore/inc/IACore/ProcessOps.hpp b/Src/IACore/inc/IACore/ProcessOps.hpp new file mode 100644 index 0000000..9f721f2 --- /dev/null +++ b/Src/IACore/inc/IACore/ProcessOps.hpp @@ -0,0 +1,67 @@ +// IACore-OSS; The Core Library for All IA Open Source Projects +// Copyright (C) 2025 IAS (ias@iasoft.dev) +// +// 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. + +#pragma once + +#include + +#if IA_PLATFORM_WINDOWS +using NativeProcessID = DWORD; +#elif IA_PLATFORM_UNIX +using NativeProcessID = pid_t; +#else +# error "This platform does not support IACore ProcessOps" +#endif + +namespace IACore +{ + struct ProcessHandle + { + Atomic ID{0}; + Atomic IsRunning{false}; + + BOOL IsActive() CONST + { + return IsRunning && ID != 0; + } + + private: + JoiningThread ThreadHandle; + + friend class ProcessOps; + }; + + class ProcessOps + { + public: + STATIC NativeProcessID GetCurrentProcessID(); + + STATIC Expected SpawnProcessSync(IN CONST String &command, IN CONST String &args, + IN Function onOutputLineCallback); + STATIC SharedPtr SpawnProcessAsync(IN CONST String &command, IN CONST String &args, + IN Function onOutputLineCallback, + IN Function)> onFinishCallback); + + STATIC VOID TerminateProcess(IN CONST SharedPtr &handle); + + private: + STATIC Expected SpawnProcessWindows(IN CONST String &command, IN CONST String &args, + IN Function onOutputLineCallback, + OUT Atomic &id); + STATIC Expected SpawnProcessPosix(IN CONST String &command, IN CONST String &args, + IN Function onOutputLineCallback, + OUT Atomic &id); + }; +} // namespace IACore \ No newline at end of file diff --git a/Src/IACore/inc/IACore/SocketOps.hpp b/Src/IACore/inc/IACore/SocketOps.hpp new file mode 100644 index 0000000..ddec94b --- /dev/null +++ b/Src/IACore/inc/IACore/SocketOps.hpp @@ -0,0 +1,106 @@ +// IACore-OSS; The Core Library for All IA Open Source Projects +// Copyright (C) 2025 IAS (ias@iasoft.dev) +// +// 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. + +#pragma once + +#include + +#if IA_PLATFORM_WINDOWS + +# include +# include +# include +# pragma comment(lib, "ws2_32.lib") +# define CLOSE_SOCKET(s) closesocket(s) +# define IS_VALID_SOCKET(s) (s != INVALID_SOCKET) +# define UNLINK_FILE(p) DeleteFileA(p) +using SocketHandle = SOCKET; + +#elif IA_PLATFORM_UNIX + +# include +# include +# include +# include +# define CLOSE_SOCKET(s) close(s) +# define IS_VALID_SOCKET(s) (s >= 0) +# define INVALID_SOCKET -1 +# define UNLINK_FILE(p) unlink(p) +using SocketHandle = int; + +#else + +# error "IACore SocketOps is not supported on this platform." + +#endif + +namespace IACore +{ + class SocketOps + { + public: + // SocketOps correctly handles multiple calls to Initialize and Terminate. Make sure + // every Initialize call is paired with a corresponding Terminate call + STATIC VOID Initialize() + { + s_initCount++; + if (s_initCount > 1) + return; +#if IA_PLATFORM_WINDOWS + WSADATA wsaData; + WSAStartup(MAKEWORD(2, 2), &wsaData); +#endif + } + + // SocketOps correctly handles multiple calls to Initialize and Terminate. Make sure + // every Initialize call is paired with a corresponding Terminate call + STATIC VOID Terminate() + { + s_initCount--; + if (s_initCount > 0) + return; +#if IA_PLATFORM_WINDOWS + WSACleanup(); +#endif + } + + STATIC BOOL IsPortAvailableTCP(IN UINT16 port) + { + return IsPortAvailable(port, SOCK_STREAM); + } + + STATIC BOOL IsPortAvailableUDP(IN UINT16 port) + { + return IsPortAvailable(port, SOCK_DGRAM); + } + + STATIC BOOL IsWouldBlock(); + + STATIC VOID Close(IN SocketHandle sock); + + STATIC BOOL Listen(IN SocketHandle sock, IN INT32 queueSize = 5); + + STATIC SocketHandle CreateUnixSocket(); + + STATIC BOOL BindUnixSocket(IN SocketHandle sock, IN PCCHAR path); + STATIC BOOL ConnectUnixSocket(IN SocketHandle sock, IN PCCHAR path); + + private: + STATIC BOOL IsPortAvailable(IN UINT16 port, IN INT32 type); + + private: + STATIC INT32 s_initCount; + }; +} // namespace IACore \ No newline at end of file diff --git a/Src/IACore/inc/IACore/StreamReader.hpp b/Src/IACore/inc/IACore/StreamReader.hpp new file mode 100644 index 0000000..abd4865 --- /dev/null +++ b/Src/IACore/inc/IACore/StreamReader.hpp @@ -0,0 +1,103 @@ +// IACore-OSS; The Core Library for All IA Open Source Projects +// Copyright (C) 2025 IAS (ias@iasoft.dev) +// +// 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. + +#pragma once + +#include + +namespace IACore +{ + class StreamReader + { + enum class EStorageType + { + NON_OWNING, + OWNING_MMAP, + OWNING_VECTOR, + }; + + public: + INLINE Expected Read(IN PVOID buffer, IN SIZE_T size); + template NO_DISCARD("Check for EOF") Expected Read(); + + VOID Skip(SIZE_T amount) + { + m_cursor = std::min(m_cursor + amount, m_dataSize); + } + + VOID Seek(SIZE_T pos) + { + m_cursor = (pos > m_dataSize) ? m_dataSize : pos; + } + + SIZE_T Cursor() CONST + { + return m_cursor; + } + + SIZE_T Size() CONST + { + return m_dataSize; + } + + SIZE_T Remaining() CONST + { + return m_dataSize - m_cursor; + } + + BOOL IsEOF() CONST + { + return m_cursor >= m_dataSize; + } + + public: + StreamReader(IN CONST FilePath &path); + explicit StreamReader(IN Vector &&data); + explicit StreamReader(IN Span data); + ~StreamReader(); + + private: + PCUINT8 m_data{}; + SIZE_T m_cursor{}; + SIZE_T m_dataSize{}; + Vector m_owningVector; + CONST EStorageType m_storageType; + }; + + Expected StreamReader::Read(IN PVOID buffer, IN SIZE_T size) + { + if B_UNLIKELY ((m_cursor + size > m_dataSize)) + return MakeUnexpected(String("Unexpected EOF while reading")); + + std::memcpy(buffer, &m_data[m_cursor], size); + m_cursor += size; + + return {}; + } + + template NO_DISCARD("Check for EOF") Expected StreamReader::Read() + { + constexpr SIZE_T size = sizeof(T); + + if B_UNLIKELY ((m_cursor + size > m_dataSize)) + return MakeUnexpected(String("Unexpected EOF while reading")); + + T value; + std::memcpy(&value, &m_data[m_cursor], size); + m_cursor += size; + + return value; + } +} // namespace IACore \ No newline at end of file diff --git a/Src/IACore/inc/IACore/StreamWriter.hpp b/Src/IACore/inc/IACore/StreamWriter.hpp new file mode 100644 index 0000000..2375396 --- /dev/null +++ b/Src/IACore/inc/IACore/StreamWriter.hpp @@ -0,0 +1,65 @@ +// IACore-OSS; The Core Library for All IA Open Source Projects +// Copyright (C) 2025 IAS (ias@iasoft.dev) +// +// 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. + +#pragma once + +#include + +namespace IACore +{ + class StreamWriter + { + enum class EStorageType + { + NON_OWNING, + OWNING_FILE, + OWNING_VECTOR, + }; + + public: + BOOL Write(IN UINT8 byte, IN SIZE_T count); + BOOL Write(IN PCVOID buffer, IN SIZE_T size); + template BOOL Write(IN CONST T &value); + + PCUINT8 Data() CONST + { + return m_buffer; + } + + SIZE_T Cursor() CONST + { + return m_cursor; + } + + public: + StreamWriter(); + explicit StreamWriter(IN Span data); + explicit StreamWriter(IN CONST FilePath &path); + ~StreamWriter(); + + private: + PUINT8 m_buffer{}; + SIZE_T m_cursor{}; + SIZE_T m_capacity{}; + FilePath m_filePath{}; + Vector m_owningVector; + CONST EStorageType m_storageType; + }; + + template BOOL StreamWriter::Write(IN CONST T &value) + { + return Write(&value, sizeof(T)); + } +} // namespace IACore \ No newline at end of file diff --git a/Src/IACore/inc/IACore/StringOps.hpp b/Src/IACore/inc/IACore/StringOps.hpp new file mode 100644 index 0000000..c57cb2e --- /dev/null +++ b/Src/IACore/inc/IACore/StringOps.hpp @@ -0,0 +1,28 @@ +// IACore-OSS; The Core Library for All IA Open Source Projects +// Copyright (C) 2025 IAS (ias@iasoft.dev) +// +// 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. + +#pragma once + +#include + +namespace IACore +{ + class StringOps + { + public: + STATIC String EncodeBase64(IN Span data); + STATIC Vector DecodeBase64(IN CONST String &data); + }; +} // namespace IACore \ No newline at end of file diff --git a/Src/IACore/inc/IACore/Utils.hpp b/Src/IACore/inc/IACore/Utils.hpp new file mode 100644 index 0000000..f5a9d0c --- /dev/null +++ b/Src/IACore/inc/IACore/Utils.hpp @@ -0,0 +1,151 @@ +// IACore-OSS; The Core Library for All IA Open Source Projects +// Copyright (C) 2025 IAS (ias@iasoft.dev) +// +// 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. + +#pragma once + +#include + +#include + +namespace IACore +{ + + class Utils + { + public: + INLINE STATIC String BinaryToHexString(std::span data) + { + STATIC CONSTEXPR char LUT[] = "0123456789ABCDEF"; + String res; + res.reserve(data.size() * 2); + + for (UINT8 b : data) + { + res.push_back(LUT[(b >> 4) & 0x0F]); + res.push_back(LUT[b & 0x0F]); + } + return res; + } + + INLINE STATIC tl::expected, String> HexStringToBinary(CONST StringView &hex) + { + if (hex.size() % 2 != 0) + { + return tl::make_unexpected(String("Hex string must have even length")); + } + + Vector out; + out.reserve(hex.size() / 2); + + for (SIZE_T i = 0; i < hex.size(); i += 2) + { + char high = hex[i]; + char low = hex[i + 1]; + + auto fromHexChar = [](char c) -> int { + if (c >= '0' && c <= '9') + return c - '0'; + if (c >= 'A' && c <= 'F') + return c - 'A' + 10; + if (c >= 'a' && c <= 'f') + return c - 'a' + 10; + return -1; + }; + + int h = fromHexChar(high); + int l = fromHexChar(low); + + if (h == -1 || l == -1) + { + return tl::make_unexpected(String("Invalid hex character found")); + } + + out.push_back(CAST((h << 4) | l, UINT8)); + } + + return out; + } + + template INLINE STATIC VOID Sort(Range &&range) + { + std::ranges::sort(range); + } + + template INLINE STATIC auto BinarySearchLeft(Range &&range, CONST T &value) + { + return std::ranges::lower_bound(range, value); + } + + template INLINE STATIC auto BinarySearchRight(Range &&range, CONST T &value) + { + return std::ranges::upper_bound(range, value); + } + + template INLINE STATIC void HashCombine(UINT64 &seed, CONST T &v) + { + UINT64 h; + + if constexpr (std::is_constructible_v) + { + std::string_view sv(v); + auto hasher = ankerl::unordered_dense::hash(); + h = hasher(sv); + } + else + { + auto hasher = ankerl::unordered_dense::hash(); + h = hasher(v); + } + + seed ^= h + 0x9e3779b97f4a7c15 + (seed << 6) + (seed >> 2); + } + + template INLINE STATIC UINT64 ComputeHash(CONST Args &...args) + { + UINT64 seed = 0; + (HashCombine(seed, args), ...); + return seed; + } + + template + INLINE STATIC UINT64 ComputeHashFlat(CONST T &obj, MemberPtrs... members) + { + UINT64 seed = 0; + (HashCombine(seed, obj.*members), ...); + return seed; + } + }; +} // namespace IACore + +// ----------------------------------------------------------------------------- +// MACRO: IA_MAKE_HASHABLE +// +// Injects the specialization for ankerl::unordered_dense::hash. +// +// Usage: +// struct Vector3 { float x, y, z; }; +// IA_MAKE_HASHABLE(Vector3, &Vector3::x, &Vector3::y, &Vector3::z) +// ----------------------------------------------------------------------------- +#define IA_MAKE_HASHABLE(Type, ...) \ + template<> struct ankerl::unordered_dense::hash \ + { \ + using is_avalanching = void; \ + NO_DISCARD("Hash value should be used") \ + UINT64 operator()(CONST Type &v) const NOEXCEPT \ + { \ + /* Pass the object and the list of member pointers */ \ + return IACore::Utils::ComputeHashFlat(v, __VA_ARGS__); \ + } \ + }; diff --git a/Tests/CMakeLists.txt b/Tests/CMakeLists.txt new file mode 100644 index 0000000..a4bccdc --- /dev/null +++ b/Tests/CMakeLists.txt @@ -0,0 +1,4 @@ + +add_subdirectory(Subjects/) +add_subdirectory(Unit/) +add_subdirectory(Regression/) diff --git a/Tests/Regression/CMakeLists.txt b/Tests/Regression/CMakeLists.txt new file mode 100644 index 0000000..e69de29 diff --git a/Tests/Subjects/CMakeLists.txt b/Tests/Subjects/CMakeLists.txt new file mode 100644 index 0000000..6248438 --- /dev/null +++ b/Tests/Subjects/CMakeLists.txt @@ -0,0 +1 @@ +add_executable(LongProcess LongProcess/Main.cpp) diff --git a/Tests/Subjects/LongProcess/Main.cpp b/Tests/Subjects/LongProcess/Main.cpp new file mode 100644 index 0000000..803803b --- /dev/null +++ b/Tests/Subjects/LongProcess/Main.cpp @@ -0,0 +1,12 @@ +#include +#include + +int main(int, char **) +{ + std::cout << "Started!\n"; + std::cout.flush(); + std::this_thread::sleep_for(std::chrono::seconds(5)); + std::cout << "Ended!\n"; + std::cout.flush(); + return 100; +} \ No newline at end of file diff --git a/Tests/Unit/CCompile.c b/Tests/Unit/CCompile.c new file mode 100644 index 0000000..17c65d4 --- /dev/null +++ b/Tests/Unit/CCompile.c @@ -0,0 +1,39 @@ +// IACore-OSS; The Core Library for All IA Open Source Projects +// Copyright (C) 2025 IAS (ias@iasoft.dev) +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +#if defined(__cplusplus) +#error "CRITICAL: This file MUST be compiled as C to test ABI compatibility." +#endif + +#include + +#if TRUE != 1 +#error "TRUE macro is broken in C mode" +#endif + +int main(void) { + IA_VERSION_TYPE version = IA_MAKE_VERSION(1, 0, 0); + + IA_ASSERT(version > 0); + + UNUSED(version); + + int32_t myNumber = 10; + + (void)myNumber; + + return 0; +} diff --git a/Tests/Unit/CMakeLists.txt b/Tests/Unit/CMakeLists.txt new file mode 100644 index 0000000..cdb66bd --- /dev/null +++ b/Tests/Unit/CMakeLists.txt @@ -0,0 +1,37 @@ +# ------------------------------------------------ +# C Compile Test (Keep separate to verify C ABI) +# ------------------------------------------------ +add_executable(IACore_Test_C_ABI "CCompile.c") +set_target_properties(IACore_Test_C_ABI PROPERTIES + C_STANDARD 99 + C_STANDARD_REQUIRED ON + LINKER_LANGUAGE C +) +target_link_libraries(IACore_Test_C_ABI PRIVATE IACore) + +# ------------------------------------------------ +# The Unified C++ Test Suite +# ------------------------------------------------ +set(TEST_SOURCES + Main.cpp + Utils.cpp + Environment.cpp + ProcessOps.cpp + StreamReader.cpp + RingBuffer.cpp +) + +add_executable(IACore_Test_Suite ${TEST_SOURCES}) + +# Enable exceptions for testing framework +target_compile_options(IACore_Test_Suite PRIVATE -fexceptions) +set_target_properties(IACore_Test_Suite PROPERTIES USE_EXCEPTIONS ON) + +target_link_libraries(IACore_Test_Suite PRIVATE IACore) + +# Copy necessary runtime assets +add_custom_command(TARGET IACore_Test_Suite POST_BUILD + COMMAND ${CMAKE_COMMAND} -E copy_if_different + $ + $/LongProcess${CMAKE_EXECUTABLE_SUFFIX} +) \ No newline at end of file diff --git a/Tests/Unit/Environment.cpp b/Tests/Unit/Environment.cpp new file mode 100644 index 0000000..fb56e39 --- /dev/null +++ b/Tests/Unit/Environment.cpp @@ -0,0 +1,179 @@ +// IACore-OSS; The Core Library for All IA Open Source Projects +// Copyright (C) 2025 IAS (ias@iasoft.dev) +// +// 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 + +#include + +using namespace IACore; + +// ----------------------------------------------------------------------------- +// Constants +// ----------------------------------------------------------------------------- +static const char *TEST_KEY = "IA_TEST_ENV_VAR_12345"; +static const char *TEST_VAL = "Hello World"; + +// ----------------------------------------------------------------------------- +// Test Block Definition +// ----------------------------------------------------------------------------- + +IAT_BEGIN_BLOCK(Core, Environment) + +// ------------------------------------------------------------------------- +// 1. Basic Set and Get (The Happy Path) +// ------------------------------------------------------------------------- +BOOL TestBasicCycle() +{ + // 1. Ensure clean slate + Environment::Unset(TEST_KEY); + IAT_CHECK_NOT(Environment::Exists(TEST_KEY)); + + // 2. Set + BOOL setRes = Environment::Set(TEST_KEY, TEST_VAL); + IAT_CHECK(setRes); + IAT_CHECK(Environment::Exists(TEST_KEY)); + + // 3. Find (Optional) + auto opt = Environment::Find(TEST_KEY); + IAT_CHECK(opt.has_value()); + IAT_CHECK_EQ(*opt, String(TEST_VAL)); + + // 4. Get (Direct) + String val = Environment::Get(TEST_KEY); + IAT_CHECK_EQ(val, String(TEST_VAL)); + + // Cleanup + Environment::Unset(TEST_KEY); + return TRUE; +} + +// ------------------------------------------------------------------------- +// 2. Overwriting Values +// ------------------------------------------------------------------------- +BOOL TestOverwrite() +{ + Environment::Set(TEST_KEY, "ValueA"); + IAT_CHECK_EQ(Environment::Get(TEST_KEY), String("ValueA")); + + // Overwrite + Environment::Set(TEST_KEY, "ValueB"); + IAT_CHECK_EQ(Environment::Get(TEST_KEY), String("ValueB")); + + Environment::Unset(TEST_KEY); + return TRUE; +} + +// ------------------------------------------------------------------------- +// 3. Unset Logic +// ------------------------------------------------------------------------- +BOOL TestUnset() +{ + Environment::Set(TEST_KEY, "To Be Deleted"); + IAT_CHECK(Environment::Exists(TEST_KEY)); + + BOOL unsetRes = Environment::Unset(TEST_KEY); + IAT_CHECK(unsetRes); + + // Verify it is actually gone + IAT_CHECK_NOT(Environment::Exists(TEST_KEY)); + + // Find should return nullopt + auto opt = Environment::Find(TEST_KEY); + IAT_CHECK_NOT(opt.has_value()); + + return TRUE; +} + +// ------------------------------------------------------------------------- +// 4. Default Value Fallbacks +// ------------------------------------------------------------------------- +BOOL TestDefaults() +{ + const char *ghostKey = "IA_THIS_KEY_DOES_NOT_EXIST"; + + // Ensure it really doesn't exist + Environment::Unset(ghostKey); + + // Test Get with implicit default ("") + String empty = Environment::Get(ghostKey); + IAT_CHECK(empty.empty()); + + // Test Get with explicit default + String fallback = Environment::Get(ghostKey, "MyDefault"); + IAT_CHECK_EQ(fallback, String("MyDefault")); + + return TRUE; +} + +// ------------------------------------------------------------------------- +// 5. Empty Strings vs Null/Unset +// ------------------------------------------------------------------------- +// Does Set(Key, "") create an existing empty variable, or unset it? +// Standard POSIX/Windows API behavior is that it EXISTS, but is empty. +BOOL TestEmptyValue() +{ + Environment::Set(TEST_KEY, ""); + +#if IA_PLATFORM_WINDOWS + // Windows behavior: Setting to empty string removes the variable + // IAT_CHECK_NOT(Environment::Exists(TEST_KEY)); + // auto opt = Environment::Find(TEST_KEY); + // IAT_CHECK_NOT(opt.has_value()); +#else + // POSIX behavior: Variable exists but is empty + IAT_CHECK(Environment::Exists(TEST_KEY)); + auto opt = Environment::Find(TEST_KEY); + IAT_CHECK(opt.has_value()); + IAT_CHECK(opt->empty()); +#endif + + // Cleanup + Environment::Unset(TEST_KEY); + IAT_CHECK_NOT(Environment::Exists(TEST_KEY)); + + return TRUE; +} + +// ------------------------------------------------------------------------- +// 6. Validation / Bad Input +// ------------------------------------------------------------------------- +BOOL TestBadInput() +{ + // Setting an empty key should fail gracefully + BOOL res = Environment::Set("", "Value"); + IAT_CHECK_NOT(res); + + // Unsetting an empty key should fail + BOOL resUnset = Environment::Unset(""); + IAT_CHECK_NOT(resUnset); + + return TRUE; +} + +// ------------------------------------------------------------------------- +// Registration +// ------------------------------------------------------------------------- +IAT_BEGIN_TEST_LIST() +IAT_ADD_TEST(TestBasicCycle); +IAT_ADD_TEST(TestOverwrite); +IAT_ADD_TEST(TestUnset); +IAT_ADD_TEST(TestDefaults); +IAT_ADD_TEST(TestEmptyValue); +IAT_ADD_TEST(TestBadInput); +IAT_END_TEST_LIST() + +IAT_END_BLOCK() + +IAT_REGISTER_ENTRY(Core, Environment) diff --git a/Tests/Unit/Main.cpp b/Tests/Unit/Main.cpp new file mode 100644 index 0000000..6f67532 --- /dev/null +++ b/Tests/Unit/Main.cpp @@ -0,0 +1,35 @@ +// IACore-OSS; The Core Library for All IA Open Source Projects +// Copyright (C) 2025 IAS (ias@iasoft.dev) +// +// 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 +#include + +int main(int argc, char *argv[]) +{ + UNUSED(argc); + UNUSED(argv); + + printf(__CC_GREEN "\n===============================================================\n"); + printf(" IACore (Independent Architecture Core) - Unit Test Suite\n"); + printf("===============================================================\n" __CC_DEFAULT "\n"); + + IACore::Initialize(); + + int result = ia::iatest::TestRegistry::RunAll(); + + IACore::Terminate(); + + return result; +} \ No newline at end of file diff --git a/Tests/Unit/ProcessOps.cpp b/Tests/Unit/ProcessOps.cpp new file mode 100644 index 0000000..17751ac --- /dev/null +++ b/Tests/Unit/ProcessOps.cpp @@ -0,0 +1,253 @@ +// IACore-OSS; The Core Library for All IA Open Source Projects +// Copyright (C) 2025 IAS (ias@iasoft.dev) +// +// 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 + +#include + +using namespace IACore; + +// ----------------------------------------------------------------------------- +// Platform Abstraction for Test Commands +// ----------------------------------------------------------------------------- +#if IA_PLATFORM_WINDOWS +# define CMD_ECHO_EXE "cmd.exe" +# define CMD_ARG_PREFIX "/c echo" +# define NULL_DEVICE "NUL" +#else +# define CMD_ECHO_EXE "/bin/echo" +# define CMD_ARG_PREFIX "" +# define NULL_DEVICE "/dev/null" +#endif + +IAT_BEGIN_BLOCK(Core, ProcessOps) + +// ------------------------------------------------------------------------- +// 1. Basic Execution (Exit Code 0) +// ------------------------------------------------------------------------- +BOOL TestBasicRun() +{ + // Simple "echo hello" + String captured; + + auto result = ProcessOps::SpawnProcessSync(CMD_ECHO_EXE, CMD_ARG_PREFIX " HelloIA", + [&](StringView line) { captured = line; }); + + IAT_CHECK(result.has_value()); + IAT_CHECK_EQ(*result, 0); // Exit code 0 + + // We check if "HelloIA" is contained or equal. + IAT_CHECK(captured.find("HelloIA") != String::npos); + + return TRUE; +} + +// ------------------------------------------------------------------------- +// 2. Argument Parsing +// ------------------------------------------------------------------------- +BOOL TestArguments() +{ + Vector lines; + + // Echo two distinct words. + // Windows: cmd.exe /c echo one two + // Linux: /bin/echo one two + String args = String(CMD_ARG_PREFIX) + " one two"; + if (args[0] == ' ') + args.erase(0, 1); // cleanup space if prefix empty + + auto result = + ProcessOps::SpawnProcessSync(CMD_ECHO_EXE, args, [&](StringView line) { lines.push_back(String(line)); }); + + IAT_CHECK_EQ(*result, 0); + IAT_CHECK(lines.size() > 0); + + // Output should contain "one two" + IAT_CHECK(lines[0].find("one two") != String::npos); + + return TRUE; +} + +// ------------------------------------------------------------------------- +// 3. Error / Non-Zero Exit Codes +// ------------------------------------------------------------------------- +BOOL TestExitCodes() +{ + // We need a command that returns non-zero. + // Windows: cmd /c exit 1 + // Linux: /bin/sh -c "exit 1" + + String cmd, arg; + +#if IA_PLATFORM_WINDOWS + cmd = "cmd.exe"; + arg = "/c exit 42"; +#else + cmd = "/bin/sh"; + arg = "-c \"exit 42\""; // quotes needed for sh -c +#endif + + auto result = ProcessOps::SpawnProcessSync(cmd, arg, [](StringView) {}); + + IAT_CHECK(result.has_value()); + IAT_CHECK_EQ(*result, 42); + + return TRUE; +} + +// ------------------------------------------------------------------------- +// 4. Missing Executable Handling +// ------------------------------------------------------------------------- +BOOL TestMissingExe() +{ + // Try to run a random string + auto result = ProcessOps::SpawnProcessSync("sdflkjghsdflkjg", "", [](StringView) {}); + + // Windows: CreateProcess usually fails -> returns unexpected + // Linux: execvp fails inside child, returns 127 via waitpid + +#if IA_PLATFORM_WINDOWS + IAT_CHECK_NOT(result.has_value()); // Should be an error string +#else + // Linux fork succeeds, but execvp fails, returning 127 + IAT_CHECK(result.has_value()); + IAT_CHECK_EQ(*result, 127); +#endif + + return TRUE; +} + +// ------------------------------------------------------------------------- +// 5. Line Buffer Logic (The 4096 split test) +// ------------------------------------------------------------------------- +BOOL TestLargeOutput() +{ + // Need to generate output larger than the internal 4096 buffer + // to ensure the "partial line" logic works when a line crosses a buffer boundary. + + String massiveString; + massiveString.reserve(5000); + for (int i = 0; i < 500; ++i) + massiveString += "1234567890"; // 5000 chars + + String cmd, arg; + +#if IA_PLATFORM_WINDOWS + cmd = "cmd.exe"; + // Windows has command line length limits (~8k), 5k is safe. + arg = "/c echo " + massiveString; +#else + cmd = "/bin/echo"; + arg = massiveString; +#endif + + String captured; + auto result = ProcessOps::SpawnProcessSync(cmd, arg, [&](StringView line) { captured += line; }); + + IAT_CHECK(result.has_value()); + IAT_CHECK_EQ(*result, 0); + + // If the LineBuffer failed to stitch chunks, the length wouldn't match + // or we would get multiple callbacks if we expected 1 line. + IAT_CHECK_EQ(captured.length(), massiveString.length()); + + return TRUE; +} + +// ------------------------------------------------------------------------- +// 6. Multi-Line Handling +// ------------------------------------------------------------------------- +BOOL TestMultiLine() +{ + // Windows: cmd /c "echo A && echo B" + // Linux: /bin/sh -c "echo A; echo B" + + String cmd, arg; +#if IA_PLATFORM_WINDOWS + cmd = "cmd.exe"; + arg = "/c \"echo LineA && echo LineB\""; +#else + cmd = "/bin/sh"; + arg = "-c \"echo LineA; echo LineB\""; +#endif + + int lineCount = 0; + bool foundA = false; + bool foundB = false; + + UNUSED(ProcessOps::SpawnProcessSync(cmd, arg, [&](StringView line) { + lineCount++; + if (line.find("LineA") != String::npos) + foundA = true; + if (line.find("LineB") != String::npos) + foundB = true; + })); + + IAT_CHECK(foundA); + IAT_CHECK(foundB); + // We expect at least 2 lines. + // (Windows sometimes echoes the command itself depending on echo settings, but we check contents) + IAT_CHECK(lineCount >= 2); + + return TRUE; +} + +// ------------------------------------------------------------------------- +// 6. Complex Command Line Arguments Handling +// ------------------------------------------------------------------------- +BOOL TestComplexArguments() +{ + // Should parse as 3 arguments: + // 1. -DDEFINED_MSG="Hello World" + // 2. -v + // 3. path/to/file + String complexArgs = "-DDEFINED_MSG=\\\"Hello World\\\" -v path/to/file"; + + String cmd = CMD_ECHO_EXE; + + String finalArgs; +#if IA_PLATFORM_WINDOWS + finalArgs = "/c echo " + complexArgs; +#else + finalArgs = complexArgs; +#endif + + String captured; + auto result = ProcessOps::SpawnProcessSync(cmd, finalArgs, [&](StringView line) { captured += line; }); + + IAT_CHECK(result.has_value()); + IAT_CHECK_EQ(*result, 0); + + // Verify the quotes were preserved in the output + IAT_CHECK(captured.find("Hello World") != String::npos); + return TRUE; +} + +// ------------------------------------------------------------------------- +// Registration +// ------------------------------------------------------------------------- +IAT_BEGIN_TEST_LIST() +IAT_ADD_TEST(TestBasicRun); +IAT_ADD_TEST(TestArguments); +IAT_ADD_TEST(TestExitCodes); +IAT_ADD_TEST(TestMissingExe); +IAT_ADD_TEST(TestLargeOutput); +IAT_ADD_TEST(TestMultiLine); +IAT_ADD_TEST(TestComplexArguments); +IAT_END_TEST_LIST() + +IAT_END_BLOCK() + +IAT_REGISTER_ENTRY(Core, ProcessOps) diff --git a/Tests/Unit/RingBuffer.cpp b/Tests/Unit/RingBuffer.cpp new file mode 100644 index 0000000..b7eb0bb --- /dev/null +++ b/Tests/Unit/RingBuffer.cpp @@ -0,0 +1,107 @@ +// IACore-OSS; The Core Library for All IA Open Source Projects +// Copyright (C) 2025 IAS (ias@iasoft.dev) +// +// 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 +#include + +using namespace IACore; + +IAT_BEGIN_BLOCK(Core, RingBuffer) + +// ------------------------------------------------------------------------- +// 1. Basic Push Pop +// ------------------------------------------------------------------------- +BOOL TestPushPop() +{ + // Allocate raw memory for the ring buffer + // ControlBlock (128 bytes) + Data + std::vector memory(sizeof(RingBufferView::ControlBlock) + 1024); + + // Initialize as OWNER (Producer) + RingBufferView producer(std::span(memory), TRUE); + + // Initialize as CONSUMER (Pointer to same memory) + RingBufferView consumer(std::span(memory), FALSE); + + // Data to send + String msg = "Hello RingBuffer"; + BOOL pushed = producer.Push(1, {(const UINT8 *) msg.data(), msg.size()}); + IAT_CHECK(pushed); + + // Read back + RingBufferView::PacketHeader header; + UINT8 readBuf[128]; + + INT32 bytesRead = consumer.Pop(header, std::span(readBuf, 128)); + + IAT_CHECK_EQ(header.ID, (UINT16) 1); + IAT_CHECK_EQ(bytesRead, (INT32) msg.size()); + + String readMsg((char *) readBuf, bytesRead); + IAT_CHECK_EQ(readMsg, msg); + + return TRUE; +} + +// ------------------------------------------------------------------------- +// 2. Wrap Around +// ------------------------------------------------------------------------- +BOOL TestWrapAround() +{ + // Small buffer to force wrapping quickly + // Capacity will be 100 bytes + std::vector memory(sizeof(RingBufferView::ControlBlock) + 100); + RingBufferView rb(std::span(memory), TRUE); + + // Fill buffer to near end + // Push 80 bytes + std::vector junk(80, 0xFF); + rb.Push(1, junk); + + // Pop them to advance READ cursor + RingBufferView::PacketHeader header; + UINT8 outBuf[100]; + rb.Pop(header, outBuf); + + // Now READ and WRITE are near index 80. + // Pushing 40 bytes should trigger a wrap-around (split write) + std::vector wrapData(40, 0xAA); + BOOL pushed = rb.Push(2, wrapData); + IAT_CHECK(pushed); + + // Pop and verify integrity + INT32 popSize = rb.Pop(header, outBuf); + IAT_CHECK_EQ(popSize, 40); + + // Check if data is intact + BOOL match = TRUE; + for (int i = 0; i < 40; i++) + { + if (outBuf[i] != 0xAA) + match = FALSE; + } + IAT_CHECK(match); + + return TRUE; +} + +IAT_BEGIN_TEST_LIST() +IAT_ADD_TEST(TestPushPop); +IAT_ADD_TEST(TestWrapAround); +IAT_END_TEST_LIST() + +IAT_END_BLOCK() + +IAT_REGISTER_ENTRY(Core, RingBuffer) diff --git a/Tests/Unit/StreamReader.cpp b/Tests/Unit/StreamReader.cpp new file mode 100644 index 0000000..7ee8f7d --- /dev/null +++ b/Tests/Unit/StreamReader.cpp @@ -0,0 +1,176 @@ +// IACore-OSS; The Core Library for All IA Open Source Projects +// Copyright (C) 2025 IAS (ias@iasoft.dev) +// +// 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 + +#include + +using namespace IACore; + +IAT_BEGIN_BLOCK(Core, StreamReader) + +// ------------------------------------------------------------------------- +// 1. Basic Primitive Reading (UINT8) +// ------------------------------------------------------------------------- +BOOL TestReadUint8() +{ + UINT8 data[] = {0xAA, 0xBB, 0xCC}; + StreamReader reader(data); + + // Read First Byte + auto val1 = reader.Read(); + IAT_CHECK(val1.has_value()); + IAT_CHECK_EQ(*val1, 0xAA); + IAT_CHECK_EQ(reader.Cursor(), (SIZE_T) 1); + + // Read Second Byte + auto val2 = reader.Read(); + IAT_CHECK_EQ(*val2, 0xBB); + + return TRUE; +} + +// ------------------------------------------------------------------------- +// 2. Multi-byte Reading (Endianness check) +// ------------------------------------------------------------------------- +BOOL TestReadMultiByte() +{ + // 0x04030201 in Little Endian memory layout + // IACore always assumes a Little Endian machine + UINT8 data[] = {0x01, 0x02, 0x03, 0x04}; + StreamReader reader(data); + + auto val = reader.Read(); + IAT_CHECK(val.has_value()); + + IAT_CHECK_EQ(*val, (UINT32) 0x04030201); + + IAT_CHECK_EQ(reader.Cursor(), (SIZE_T) 4); + IAT_CHECK(reader.IsEOF()); + + return TRUE; +} + +// ------------------------------------------------------------------------- +// 3. Floating Point (Approx check) +// ------------------------------------------------------------------------- +BOOL TestReadFloat() +{ + FLOAT32 pi = 3.14159f; + // Bit-cast float to bytes for setup + UINT8 data[4]; + std::memcpy(data, &pi, 4); + + StreamReader reader(data); + auto val = reader.Read(); + + IAT_CHECK(val.has_value()); + IAT_CHECK_APPROX(*val, pi); + + return TRUE; +} + +// ------------------------------------------------------------------------- +// 4. Batch Buffer Reading +// ------------------------------------------------------------------------- +BOOL TestReadBuffer() +{ + UINT8 src[] = {1, 2, 3, 4, 5}; + UINT8 dst[3] = {0}; + StreamReader reader(src); + + // Read 3 bytes into dst + auto res = reader.Read(dst, 3); + IAT_CHECK(res.has_value()); + + // Verify dst content + IAT_CHECK_EQ(dst[0], 1); + IAT_CHECK_EQ(dst[1], 2); + IAT_CHECK_EQ(dst[2], 3); + + // Verify cursor + IAT_CHECK_EQ(reader.Cursor(), (SIZE_T) 3); + + return TRUE; +} + +// ------------------------------------------------------------------------- +// 5. Navigation (Seek, Skip, Remaining) +// ------------------------------------------------------------------------- +BOOL TestNavigation() +{ + UINT8 data[10] = {0}; // Zero init + StreamReader reader(data); + + IAT_CHECK_EQ(reader.Remaining(), (SIZE_T) 10); + + // Skip + reader.Skip(5); + IAT_CHECK_EQ(reader.Cursor(), (SIZE_T) 5); + IAT_CHECK_EQ(reader.Remaining(), (SIZE_T) 5); + + // Skip clamping + reader.Skip(100); // Should clamp to 10 + IAT_CHECK_EQ(reader.Cursor(), (SIZE_T) 10); + IAT_CHECK(reader.IsEOF()); + + // Seek + reader.Seek(2); + IAT_CHECK_EQ(reader.Cursor(), (SIZE_T) 2); + IAT_CHECK_EQ(reader.Remaining(), (SIZE_T) 8); + IAT_CHECK_NOT(reader.IsEOF()); + + return TRUE; +} + +// ------------------------------------------------------------------------- +// 6. Error Handling (EOF Protection) +// ------------------------------------------------------------------------- +BOOL TestBoundaryChecks() +{ + UINT8 data[] = {0x00, 0x00}; + StreamReader reader(data); + + // Valid read + UNUSED(reader.Read()); + IAT_CHECK(reader.IsEOF()); + + // Invalid Read Primitive + auto val = reader.Read(); + IAT_CHECK_NOT(val.has_value()); // Should be unexpected + + // Invalid Batch Read + UINT8 buf[1]; + auto batch = reader.Read(buf, 1); + IAT_CHECK_NOT(batch.has_value()); + + return TRUE; +} + +// ------------------------------------------------------------------------- +// Registration +// ------------------------------------------------------------------------- +IAT_BEGIN_TEST_LIST() +IAT_ADD_TEST(TestReadUint8); +IAT_ADD_TEST(TestReadMultiByte); +IAT_ADD_TEST(TestReadFloat); +IAT_ADD_TEST(TestReadBuffer); +IAT_ADD_TEST(TestNavigation); +IAT_ADD_TEST(TestBoundaryChecks); +IAT_END_TEST_LIST() + +IAT_END_BLOCK() + +IAT_REGISTER_ENTRY(Core, StreamReader) diff --git a/Tests/Unit/Utils.cpp b/Tests/Unit/Utils.cpp new file mode 100644 index 0000000..904d853 --- /dev/null +++ b/Tests/Unit/Utils.cpp @@ -0,0 +1,222 @@ +// IACore-OSS; The Core Library for All IA Open Source Projects +// Copyright (C) 2025 IAS (ias@iasoft.dev) +// +// 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 + +#include + +using namespace IACore; + +// ----------------------------------------------------------------------------- +// Test Structs for Hashing (Must be defined at Global Scope) +// ----------------------------------------------------------------------------- + +struct TestVec3 +{ + FLOAT32 x, y, z; + + // Equality operator required for hash maps, though strictly + // the hash function itself doesn't need it, it's good practice to test both. + bool operator==(const TestVec3 &other) const + { + return x == other.x && y == other.y && z == other.z; + } +}; + +// Inject the hash specialization into the ankerl namespace +// This proves the macro works structurally +IA_MAKE_HASHABLE(TestVec3, &TestVec3::x, &TestVec3::y, &TestVec3::z); + +// ----------------------------------------------------------------------------- +// Test Block Definition +// ----------------------------------------------------------------------------- + +IAT_BEGIN_BLOCK(Core, Utils) + +// ------------------------------------------------------------------------- +// 1. Binary <-> Hex String Conversion +// ------------------------------------------------------------------------- +BOOL TestHexConversion() +{ + // A. Binary To Hex + UINT8 bin[] = {0xDE, 0xAD, 0xBE, 0xEF, 0x00, 0xFF}; + String hex = IACore::Utils::BinaryToHexString(bin); + + IAT_CHECK_EQ(hex, String("DEADBEEF00FF")); + + // B. Hex To Binary (Valid Upper) + auto resUpper = IACore::Utils::HexStringToBinary("DEADBEEF00FF"); + IAT_CHECK(resUpper.has_value()); + IAT_CHECK_EQ(resUpper->size(), (SIZE_T) 6); + IAT_CHECK_EQ((*resUpper)[0], 0xDE); + IAT_CHECK_EQ((*resUpper)[5], 0xFF); + + // C. Hex To Binary (Valid Lower/Mixed) + auto resLower = IACore::Utils::HexStringToBinary("deadbeef00ff"); + IAT_CHECK(resLower.has_value()); + IAT_CHECK_EQ((*resLower)[0], 0xDE); + + // D. Round Trip Integrity + Vector original = {1, 2, 3, 4, 5}; + String s = IACore::Utils::BinaryToHexString(original); + auto back = IACore::Utils::HexStringToBinary(s); + IAT_CHECK(back.has_value()); + IAT_CHECK_EQ(original.size(), back->size()); + IAT_CHECK_EQ(original[2], (*back)[2]); + + return TRUE; +} + +// ------------------------------------------------------------------------- +// 2. Hex Error Handling +// ------------------------------------------------------------------------- +BOOL TestHexErrors() +{ + // Odd Length + auto odd = IACore::Utils::HexStringToBinary("ABC"); + IAT_CHECK_NOT(odd.has_value()); + + // Invalid Characters + auto invalid = IACore::Utils::HexStringToBinary("ZZTOP"); + IAT_CHECK_NOT(invalid.has_value()); + + // Empty string is valid (empty vector) + auto empty = IACore::Utils::HexStringToBinary(""); + IAT_CHECK(empty.has_value()); + IAT_CHECK_EQ(empty->size(), (SIZE_T) 0); + + return TRUE; +} + +// ------------------------------------------------------------------------- +// 3. Algorithms: Sorting +// ------------------------------------------------------------------------- +BOOL TestSort() +{ + Vector nums = {5, 1, 4, 2, 3}; + + IACore::Utils::Sort(nums); + + IAT_CHECK_EQ(nums[0], 1); + IAT_CHECK_EQ(nums[1], 2); + IAT_CHECK_EQ(nums[2], 3); + IAT_CHECK_EQ(nums[3], 4); + IAT_CHECK_EQ(nums[4], 5); + + return TRUE; +} + +// ------------------------------------------------------------------------- +// 4. Algorithms: Binary Search (Left/Right) +// ------------------------------------------------------------------------- +BOOL TestBinarySearch() +{ + // Must be sorted for Binary Search + Vector nums = {10, 20, 20, 20, 30}; + + // Search Left (Lower Bound) -> First element >= value + auto itLeft = IACore::Utils::BinarySearchLeft(nums, 20); + IAT_CHECK(itLeft != nums.end()); + IAT_CHECK_EQ(*itLeft, 20); + IAT_CHECK_EQ(std::distance(nums.begin(), itLeft), 1); // Index 1 is first 20 + + // Search Right (Upper Bound) -> First element > value + auto itRight = IACore::Utils::BinarySearchRight(nums, 20); + IAT_CHECK(itRight != nums.end()); + IAT_CHECK_EQ(*itRight, 30); // Points to 30 + IAT_CHECK_EQ(std::distance(nums.begin(), itRight), 4); // Index 4 + + // Search for non-existent + auto itFail = IACore::Utils::BinarySearchLeft(nums, 99); + IAT_CHECK(itFail == nums.end()); + + return TRUE; +} + +// ------------------------------------------------------------------------- +// 5. Hashing Basics +// ------------------------------------------------------------------------- +BOOL TestHashBasics() +{ + UINT64 h1 = IACore::Utils::ComputeHash(10, 20.5f, "Hello"); + UINT64 h2 = IACore::Utils::ComputeHash(10, 20.5f, "Hello"); + UINT64 h3 = IACore::Utils::ComputeHash(10, 20.5f, "World"); + + // Determinism + IAT_CHECK_EQ(h1, h2); + + // Differentiation + IAT_CHECK_NEQ(h1, h3); + + // Order sensitivity (Golden ratio combine should care about order) + // Hash(A, B) != Hash(B, A) + UINT64 orderA = IACore::Utils::ComputeHash(1, 2); + UINT64 orderB = IACore::Utils::ComputeHash(2, 1); + IAT_CHECK_NEQ(orderA, orderB); + + return TRUE; +} + +// ------------------------------------------------------------------------- +// 6. Macro Verification (IA_MAKE_HASHABLE) +// ------------------------------------------------------------------------- +BOOL TestHashMacro() +{ + TestVec3 v1{1.0f, 2.0f, 3.0f}; + TestVec3 v2{1.0f, 2.0f, 3.0f}; + TestVec3 v3{1.0f, 2.0f, 4.0f}; + + ankerl::unordered_dense::hash hasher; + + UINT64 h1 = hasher(v1); + UINT64 h2 = hasher(v2); + UINT64 h3 = hasher(v3); + + IAT_CHECK_EQ(h1, h2); // Same content = same hash + IAT_CHECK_NEQ(h1, h3); // Different content = different hash + + // ------------------------------------------------------------- + // Verify ComputeHash integration + // ------------------------------------------------------------- + + UINT64 hManual = 0; + IACore::Utils::HashCombine(hManual, v1); + + UINT64 hWrapper = IACore::Utils::ComputeHash(v1); + + // This proves ComputeHash found the specialization and mixed it correctly + IAT_CHECK_EQ(hManual, hWrapper); + + // Verify the avalanche effect took place (hWrapper should NOT be h1) + IAT_CHECK_NEQ(h1, hWrapper); + + return TRUE; +} + +// ------------------------------------------------------------------------- +// Registration +// ------------------------------------------------------------------------- +IAT_BEGIN_TEST_LIST() +IAT_ADD_TEST(TestHexConversion); +IAT_ADD_TEST(TestHexErrors); +IAT_ADD_TEST(TestSort); +IAT_ADD_TEST(TestBinarySearch); +IAT_ADD_TEST(TestHashBasics); +IAT_ADD_TEST(TestHashMacro); +IAT_END_TEST_LIST() + +IAT_END_BLOCK() + +IAT_REGISTER_ENTRY(Core, Utils) diff --git a/logo.svg b/logo.svg new file mode 100644 index 0000000..9642a5e --- /dev/null +++ b/logo.svg @@ -0,0 +1,23 @@ + + + + + + + + + + + + + + + IACore + + INDEPENDENT ARCHITECTURE + + \ No newline at end of file