diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 0483483..3e36e9a 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -5,7 +5,6 @@ on: [push, pull_request] jobs: test: runs-on: ubuntu-latest - strategy: matrix: toolchain: ['clang', 'gcc'] @@ -15,17 +14,22 @@ jobs: c-compiler: gcc cxx-compiler: g++ - toolchain: clang - c-compiler: clang-11 - cxx-compiler: clang++-11 - + c-compiler: clang + cxx-compiler: clang++ steps: - uses: actions/checkout@v2 - name: Install Dependencies - run: sudo apt install gcc-multilib g++-multilib clang-tidy-9 clang-format-9 + run: sudo apt install gcc-multilib g++-multilib clang-tidy-12 - name: Configure CMake - run: cmake -B ${{ github.workspace }}/build -DCMAKE_BUILD_TYPE=${{ matrix.build_type }} -DCMAKE_C_COMPILER=${{ matrix.c-compiler }} -DCMAKE_CXX_COMPILER=${{ matrix.cxx-compiler }} tests + run: > + cmake + -B ${{ github.workspace }}/build + -DCMAKE_BUILD_TYPE=${{ matrix.build_type }} + -DCMAKE_C_COMPILER=${{ matrix.c-compiler }} + -DCMAKE_CXX_COMPILER=${{ matrix.cxx-compiler }} + tests - name: Build working-directory: ${{github.workspace}}/build @@ -36,24 +40,23 @@ jobs: run: make test sonarcloud: + if: endsWith(github.base_ref, 'master') || endsWith(github.ref, 'master') + runs-on: ubuntu-latest env: SONAR_SCANNER_VERSION: 4.6.1.2450 SONAR_SERVER_URL: "https://sonarcloud.io" BUILD_WRAPPER_OUT_DIR: build_wrapper_output_directory GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }} - CC: gcc-9 - CXX: g++-9 - - runs-on: ubuntu-latest - + CC: gcc + CXX: g++ steps: - uses: actions/checkout@v2 with: fetch-depth: 0 - name: Install Dependencies - run: sudo apt install g++-9 g++-9-multilib gcc-9-multilib + run: sudo apt install g++ g++-multilib gcc-multilib - name: Set up JDK 11 uses: actions/setup-java@v1 @@ -72,7 +75,7 @@ jobs: SONAR_SCANNER_DOWNLOAD_URL: https://binaries.sonarsource.com/Distribution/sonar-scanner-cli/sonar-scanner-cli-${{ env.SONAR_SCANNER_VERSION }}-linux.zip run: | mkdir -p $HOME/.sonar - curl -sSLo $HOME/.sonar/sonar-scanner.zip ${{ env.SONAR_SCANNER_DOWNLOAD_URL }} + curl -sSLo $HOME/.sonar/sonar-scanner.zip ${{ env.SONAR_SCANNER_DOWNLOAD_URL }} unzip -o $HOME/.sonar/sonar-scanner.zip -d $HOME/.sonar/ echo "$HOME/.sonar/sonar-scanner-${{ env.SONAR_SCANNER_VERSION }}-linux/bin" >> $GITHUB_PATH @@ -89,8 +92,30 @@ jobs: cmake tests -DCMAKE_BUILD_TYPE=Debug -DNO_STATIC_ANALYSIS=1 build-wrapper-linux-x86-64 --out-dir ${{ env.BUILD_WRAPPER_OUT_DIR }} make all make test - gcov-10 --preserve-paths --long-file-names $(find CMakeFiles/test_private_cov.dir -name '*.gcno') + gcov --preserve-paths --long-file-names $(find CMakeFiles/test_general_cov.dir -name '*.gcno') + gcov --preserve-paths --long-file-names $(find CMakeFiles/test_private_cov.dir -name '*.gcno') - name: Run sonar-scanner - run: | - sonar-scanner --define sonar.host.url="${{ env.SONAR_SERVER_URL }}" --define sonar.cfamily.build-wrapper-output="${{ env.BUILD_WRAPPER_OUT_DIR }}" --define sonar.login=${{ secrets.SONAR_TOKEN }} + run: > + sonar-scanner + --define sonar.projectKey="pavel-kirienko_o1heap" + --define sonar.organization="pavel-kirienko" + --define sonar.sources="o1heap/" + --define sonar.sourceEncoding="UTF-8" + --define sonar.cfamily.gcov.reportsPath="." + --define sonar.cfamily.build-wrapper-output="${{ env.BUILD_WRAPPER_OUT_DIR }}" + --define sonar.cfamily.cache.enabled="false" + --define sonar.cfamily.threads=1 + --define sonar.host.url="${{ env.SONAR_SERVER_URL }}" + --define sonar.login=${{ secrets.SONAR_TOKEN }} + + style_check: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v2 + - uses: DoozyX/clang-format-lint-action@v0.12 + with: + source: './o1heap ./tests' + exclude: './tests/catch' + extensions: 'c,h,cpp,hpp' + clangFormatVersion: 12 diff --git a/.gitignore b/.gitignore index b34c797..55064e8 100644 --- a/.gitignore +++ b/.gitignore @@ -52,6 +52,8 @@ Mkfile.old dkms.conf # IDE & outputs -.idea/ +**/.idea/* +!**/.idea/dictionaries +!**/.idea/dictionaries/* cmake-build-*/ build/ diff --git a/README.md b/README.md index 6ada559..6bfb2f4 100644 --- a/README.md +++ b/README.md @@ -16,7 +16,7 @@ which makes it suitable for use in high-integrity embedded systems. The codebase is implemented in C99/C11 following MISRA C:2012, with several intended deviations which are unavoidable due to the fact that a memory allocator has to rely on inherently unsafe operations to fulfill its purpose. -The codebase is extremely compact (<500 SLoC) and is therefore trivial to validate. +The codebase is extremely compact (<500 LoC) and is therefore trivial to validate. The allocator is designed to be portable across all conventional architectures, from 8-bit to 64-bit systems. Multi-threaded environments are supported with the help of external synchronization hooks provided by the application. @@ -77,9 +77,9 @@ a memory allocation request even if there is enough free memory due to its subop By definition, if the amount of memory available to the allocator is not less than *H*, then the state of catastrophic fragmentation cannot occur. -Memory allocators used in general-purpose (non real-time) applications often leverage a different class of algorithms +Memory allocators used in general-purpose (non-real-time) applications often leverage a different class of algorithms which may feature poorer worst-case performance metrics but perform (much) better on average. -For a hard real-time system, the average case performance is generally less relevant +For a hard real-time system, the average case performance is generally less relevant, so it can be excluded from analysis. The above-defined theoretical worst-case upper bound H may be prohibitively high for some @@ -90,7 +90,7 @@ the probability of a (de-)allocation sequence that results in catastrophic fragm When combined with an acceptable failure probability and a set of adequate assumptions about the behaviors of the application, this property may allow the designer to drastically reduce the amount of memory dedicated to the heap while ensuring a sufficient degree of predictability and reliability. -The methods of such optimization are outside of the scope of this document; +The methods of such optimization are outside the scope of this document; interested readers are advised to consult with the referred publications. Following some of the ideas expressed in the discussion about memory caching in real-time systems in [Herter 2014], @@ -173,15 +173,18 @@ and its internal data structures are not damaged. ### Build configuration options -The preprocessor options given below can be overridden (e.g., using the `-D` compiler flag, depending on the compiler) -to fine-tune the implementation. +The preprocessor options given below can be overridden to fine-tune the implementation. None of them are mandatory to use. +#### O1HEAP_CONFIG_HEADER + +Define this optional macro like `O1HEAP_CONFIG_HEADER="path/to/my_o1heap_config.h"` to pass build configuration macros. +This is useful because some build systems do not allow passing function-like macros via command line flags. + #### O1HEAP_ASSERT(x) The macro `O1HEAP_ASSERT(x)` can be defined to customize the assertion handling or to disable it. To disable assertion checks, the macro should expand into `(void)(x)`. - If not specified, the macro expands into the standard assertion check macro `assert(x)` as defined in ``. #### O1HEAP_LIKELY(x) @@ -195,7 +198,7 @@ or into the original expression `(x)` if no such hinting is desired. If not specified, the macro expands as follows: - For some well-known compilers the macro automatically expands into appropriate branch weighting intrinsics. -For example, for GCC, Clang, and ARM Compiler, it expands into `__builtin_expect((x), 1)`. + For example, for GCC, Clang, and ARM Compiler, it expands into `__builtin_expect((x), 1)`. - For other (unknown) compilers it expands into the original expression with no modifications: `(x)`. ## Development @@ -204,9 +207,7 @@ For example, for GCC, Clang, and ARM Compiler, it expands into `__builtin_expect The following tools should be available locally to conduct library development: -- GCC v9 or newer. -- Clang and Clang-Tools v9 or newer. -- CMake v3.12 or newer. +- Modern versions of CMake, GCC, Clang, and Clang-Tools. - An AMD64 machine. - (optional) Valgrind. @@ -223,6 +224,10 @@ Compliance is enforced through the following means: Please refer to the continuous integration configuration to see how to invoke the tests. +### Releasing + +Update the version number macro in the header file and create a new git tag like `1.0`. + ### MISRA compliance MISRA compliance is enforced with the help of the following tools: @@ -252,6 +257,17 @@ An exception applies for the case of false-positive (invalid) warnings -- those - [Dynamic Memory Allocation In SQLite](https://sqlite.org/malloc.html) -- on Robson proof and deterministic fragmentation. - *[Russian]* [Динамическая память в системах жёсткого реального времени](https://habr.com/ru/post/486650/) -- issues with dynamic memory allocation in modern embedded RTOS and related popular misconceptions. +## Changelog + +### v2.0 + +- Remove critical section hooks to enhance MISRA conformance [#4](https://github.com/pavel-kirienko/o1heap/issues/4) +- Add support for config header via `O1HEAP_CONFIG_HEADER` [#5](https://github.com/pavel-kirienko/o1heap/issues/5) + +### v1.0 + +The first release. + ## License The library is available under the terms of the MIT License. diff --git a/o1heap/.clang-tidy b/o1heap/.clang-tidy index e04e195..3eaee0c 100644 --- a/o1heap/.clang-tidy +++ b/o1heap/.clang-tidy @@ -16,6 +16,9 @@ Checks: >- -google-readability-todo, -readability-avoid-const-params-in-decls, -llvm-header-guard, +CheckOptions: + - key: readability-function-cognitive-complexity.Threshold + value: '199' WarningsAsErrors: '*' HeaderFilterRegex: '.*' AnalyzeTemporaryDtors: false diff --git a/o1heap/o1heap.c b/o1heap/o1heap.c index ec5235d..37b5f7e 100644 --- a/o1heap/o1heap.c +++ b/o1heap/o1heap.c @@ -19,6 +19,12 @@ // ---------------------------------------- BUILD CONFIGURATION OPTIONS ---------------------------------------- +/// Define this macro to include build configuration header. This is an alternative to the -D compiler flag. +/// Usage example with CMake: "-DO1HEAP_CONFIG_HEADER=\"${CMAKE_CURRENT_SOURCE_DIR}/my_o1heap_config.h\"" +#ifdef O1HEAP_CONFIG_HEADER +# include O1HEAP_CONFIG_HEADER +#endif + /// The assertion macro defaults to the standard assert(). /// It can be overridden to manually suppress assertion checks or use a different error handling policy. #ifndef O1HEAP_ASSERT @@ -27,8 +33,6 @@ #endif /// Branch probability annotations are used to improve the worst case execution time (WCET). They are entirely optional. -/// A stock implementation is provided for some well-known compilers; for other compilers it defaults to nothing. -/// If you are using a different compiler, consider overriding this value. #ifndef O1HEAP_LIKELY # if defined(__GNUC__) || defined(__clang__) || defined(__CC_ARM) // Intentional violation of MISRA: branch hinting macro cannot be replaced with a function definition. @@ -39,9 +43,7 @@ #endif /// This option is used for testing only. Do not use in production. -#if defined(O1HEAP_EXPOSE_INTERNALS) && O1HEAP_EXPOSE_INTERNALS -# define O1HEAP_PRIVATE -#else +#ifndef O1HEAP_PRIVATE # define O1HEAP_PRIVATE static inline #endif @@ -101,9 +103,6 @@ struct O1HeapInstance Fragment* bins[NUM_BINS_MAX]; ///< Smallest fragments are in the bin at index 0. size_t nonempty_bin_mask; ///< Bit 1 represents a non-empty bin; bin at index 0 is for the smallest fragments. - O1HeapHook critical_section_enter; - O1HeapHook critical_section_leave; - O1HeapDiagnostics diagnostics; }; @@ -115,14 +114,12 @@ static_assert(INSTANCE_SIZE_PADDED >= sizeof(O1HeapInstance), "Invalid instance static_assert((INSTANCE_SIZE_PADDED % O1HEAP_ALIGNMENT) == 0U, "Invalid instance footprint computation"); /// True if the argument is an integer power of two or zero. -O1HEAP_PRIVATE bool isPowerOf2(const size_t x); O1HEAP_PRIVATE bool isPowerOf2(const size_t x) { return (x & (x - 1U)) == 0U; } /// Special case: if the argument is zero, returns zero. -O1HEAP_PRIVATE uint8_t log2Floor(const size_t x); O1HEAP_PRIVATE uint8_t log2Floor(const size_t x) { size_t tmp = x; @@ -138,32 +135,20 @@ O1HEAP_PRIVATE uint8_t log2Floor(const size_t x) } /// Special case: if the argument is zero, returns zero. -O1HEAP_PRIVATE uint8_t log2Ceil(const size_t x); O1HEAP_PRIVATE uint8_t log2Ceil(const size_t x) { - return (uint8_t)(log2Floor(x) + (isPowerOf2(x) ? 0U : 1U)); + return (uint8_t) (log2Floor(x) + (isPowerOf2(x) ? 0U : 1U)); } /// Raise 2 into the specified power. /// You might be tempted to do something like (1U << power). WRONG! We humans are prone to forgetting things. /// If you forget to cast your 1U to size_t or ULL, you may end up with undefined behavior. -O1HEAP_PRIVATE size_t pow2(const uint8_t power); O1HEAP_PRIVATE size_t pow2(const uint8_t power) { return ((size_t) 1U) << power; } -O1HEAP_PRIVATE void invoke(const O1HeapHook hook); -O1HEAP_PRIVATE void invoke(const O1HeapHook hook) -{ - if (hook != NULL) - { - hook(); - } -} - /// Links two fragments so that their next/prev pointers point to each other; left goes before right. -O1HEAP_PRIVATE void interlink(Fragment* const left, Fragment* const right); O1HEAP_PRIVATE void interlink(Fragment* const left, Fragment* const right) { if (O1HEAP_LIKELY(left != NULL)) @@ -176,8 +161,7 @@ O1HEAP_PRIVATE void interlink(Fragment* const left, Fragment* const right) } } -/// Adds a new block into the appropriate bin and updates the lookup mask. -O1HEAP_PRIVATE void rebin(O1HeapInstance* const handle, Fragment* const fragment); +/// Adds a new fragment into the appropriate bin and updates the lookup mask. O1HEAP_PRIVATE void rebin(O1HeapInstance* const handle, Fragment* const fragment) { O1HEAP_ASSERT(handle != NULL); @@ -187,7 +171,7 @@ O1HEAP_PRIVATE void rebin(O1HeapInstance* const handle, Fragment* const fragment const uint8_t idx = log2Floor(fragment->header.size / FRAGMENT_SIZE_MIN); // Round DOWN when inserting. O1HEAP_ASSERT(idx < NUM_BINS_MAX); // Add the new fragment to the beginning of the bin list. - // I.e., each allocation will be returning the least-recently-used fragment -- good for caching. + // I.e., each allocation will be returning the most-recently-used fragment -- good for caching. fragment->next_free = handle->bins[idx]; fragment->prev_free = NULL; if (O1HEAP_LIKELY(handle->bins[idx] != NULL)) @@ -198,8 +182,7 @@ O1HEAP_PRIVATE void rebin(O1HeapInstance* const handle, Fragment* const fragment handle->nonempty_bin_mask |= pow2(idx); } -/// Removes the specified block from its bin. -O1HEAP_PRIVATE void unbin(O1HeapInstance* const handle, const Fragment* const fragment); +/// Removes the specified fragment from its bin. O1HEAP_PRIVATE void unbin(O1HeapInstance* const handle, const Fragment* const fragment) { O1HEAP_ASSERT(handle != NULL); @@ -231,10 +214,7 @@ O1HEAP_PRIVATE void unbin(O1HeapInstance* const handle, const Fragment* const fr // ---------------------------------------- PUBLIC API IMPLEMENTATION ---------------------------------------- -O1HeapInstance* o1heapInit(void* const base, - const size_t size, - const O1HeapHook critical_section_enter, - const O1HeapHook critical_section_leave) +O1HeapInstance* o1heapInit(void* const base, const size_t size) { O1HeapInstance* out = NULL; if ((base != NULL) && ((((size_t) base) % O1HEAP_ALIGNMENT) == 0U) && @@ -242,10 +222,8 @@ O1HeapInstance* o1heapInit(void* const base, { // Allocate the core heap metadata structure in the beginning of the arena. O1HEAP_ASSERT(((size_t) base) % sizeof(O1HeapInstance*) == 0U); - out = (O1HeapInstance*) base; - out->nonempty_bin_mask = 0U; - out->critical_section_enter = critical_section_enter; - out->critical_section_leave = critical_section_leave; + out = (O1HeapInstance*) base; + out->nonempty_bin_mask = 0U; for (size_t i = 0; i < NUM_BINS_MAX; i++) { out->bins[i] = NULL; @@ -311,8 +289,6 @@ void* o1heapAllocate(O1HeapInstance* const handle, const size_t amount) O1HEAP_ASSERT(optimal_bin_index < NUM_BINS_MAX); const size_t candidate_bin_mask = ~(pow2(optimal_bin_index) - 1U); - invoke(handle->critical_section_enter); - // Find the smallest non-empty bin we can use. const size_t suitable_bins = handle->nonempty_bin_mask & candidate_bin_mask; const size_t smallest_bin_mask = suitable_bins & ~(suitable_bins - 1U); // Clear all bits but the lowest. @@ -363,10 +339,6 @@ void* o1heapAllocate(O1HeapInstance* const handle, const size_t amount) out = ((uint8_t*) frag) + O1HEAP_ALIGNMENT; } } - else - { - invoke(handle->critical_section_enter); - } // Update the diagnostics. if (O1HEAP_LIKELY(handle->diagnostics.peak_request_size < amount)) @@ -378,7 +350,6 @@ void* o1heapAllocate(O1HeapInstance* const handle, const size_t amount) handle->diagnostics.oom_count++; } - invoke(handle->critical_section_leave); return out; } @@ -402,8 +373,6 @@ void o1heapFree(O1HeapInstance* const handle, void* const pointer) O1HEAP_ASSERT(frag->header.size <= handle->diagnostics.capacity); O1HEAP_ASSERT((frag->header.size % FRAGMENT_SIZE_MIN) == 0U); - invoke(handle->critical_section_enter); - // Even if we're going to drop the fragment later, mark it free anyway to prevent double-free. frag->header.used = false; @@ -449,8 +418,6 @@ void o1heapFree(O1HeapInstance* const handle, void* const pointer) { rebin(handle, frag); } - - invoke(handle->critical_section_leave); } } @@ -459,8 +426,6 @@ bool o1heapDoInvariantsHold(const O1HeapInstance* const handle) O1HEAP_ASSERT(handle != NULL); bool valid = true; - invoke(handle->critical_section_enter); - // Check the bin mask consistency. for (size_t i = 0; i < NUM_BINS_MAX; i++) // Dear compiler, feel free to unroll this loop. { @@ -469,11 +434,9 @@ bool o1heapDoInvariantsHold(const O1HeapInstance* const handle) valid = valid && (mask_bit_set == bin_nonempty); } - // Create a local copy of the diagnostics struct to check later and release the critical section early. + // Create a local copy of the diagnostics struct. const O1HeapDiagnostics diag = handle->diagnostics; - invoke(handle->critical_section_leave); - // Capacity check. valid = valid && (diag.capacity <= FRAGMENT_SIZE_MAX) && (diag.capacity >= FRAGMENT_SIZE_MIN) && ((diag.capacity % FRAGMENT_SIZE_MIN) == 0U); @@ -501,8 +464,6 @@ bool o1heapDoInvariantsHold(const O1HeapInstance* const handle) O1HeapDiagnostics o1heapGetDiagnostics(const O1HeapInstance* const handle) { O1HEAP_ASSERT(handle != NULL); - invoke(handle->critical_section_enter); const O1HeapDiagnostics out = handle->diagnostics; - invoke(handle->critical_section_leave); return out; } diff --git a/o1heap/o1heap.h b/o1heap/o1heap.h index 079277d..fb776b4 100644 --- a/o1heap/o1heap.h +++ b/o1heap/o1heap.h @@ -28,7 +28,7 @@ extern "C" { #endif /// The semantic version number of this distribution. -#define O1HEAP_VERSION_MAJOR 1 +#define O1HEAP_VERSION_MAJOR 2 /// The guaranteed alignment depends on the platform pointer width. #define O1HEAP_ALIGNMENT (sizeof(void*) * 4U) @@ -36,19 +36,16 @@ extern "C" { /// The definition is private, so the user code can only operate on pointers. This is done to enforce encapsulation. typedef struct O1HeapInstance O1HeapInstance; -/// A hook function invoked by the allocator. NULL hooks are silently not invoked (not an error). -typedef void (*O1HeapHook)(void); - /// Runtime diagnostic information. This information can be used to facilitate runtime self-testing, /// as required by certain safety-critical development guidelines. /// If assertion checks are not disabled, the library will perform automatic runtime self-diagnostics that trigger /// an assertion failure if a heap corruption is detected. -/// Health checks and validation can be done with @ref o1heapDoInvariantsHold(). +/// Health checks and validation can be done with o1heapDoInvariantsHold(). typedef struct { /// The total amount of memory available for serving allocation requests (heap size). /// The maximum allocation size is (capacity - O1HEAP_ALIGNMENT). - /// This parameter does not include the overhead used up by @ref O1HeapInstance and arena alignment. + /// This parameter does not include the overhead used up by O1HeapInstance and arena alignment. /// This parameter is constant. size_t capacity; @@ -69,20 +66,12 @@ typedef struct uint64_t oom_count; } O1HeapDiagnostics; -/// The arena base pointer shall be aligned at @ref O1HEAP_ALIGNMENT, otherwise NULL is returned. +/// The arena base pointer shall be aligned at O1HEAP_ALIGNMENT, otherwise NULL is returned. /// /// The total heap capacity cannot exceed approx. (SIZE_MAX/2). If the arena size allows for a larger heap, /// the excess will be silently truncated away (no error). This is not a realistic use case because a typical /// application is unlikely to be able to dedicate that much of the address space for the heap. /// -/// The critical section enter/leave callbacks will be invoked when the allocator performs an atomic transaction. -/// There is at most one atomic transaction per allocation/deallocation. -/// Either or both of the callbacks may be NULL if locking is not needed (i.e., the heap is not shared). -/// It is guaranteed that a critical section will never be entered recursively. -/// It is guaranteed that 'enter' is invoked the same number of times as 'leave', unless either of them are NULL. -/// It is guaranteed that 'enter' is invoked before 'leave', unless either of them are NULL. -/// The callbacks are never invoked from the initialization function itself. -/// /// The function initializes a new heap instance allocated in the provided arena, taking some of its space for its /// own needs (normally about 40..600 bytes depending on the architecture, but this parameter is not characterized). /// A pointer to the newly initialized instance is returned. @@ -92,24 +81,19 @@ typedef struct /// An initialized instance does not hold any resources. Therefore, if the instance is no longer needed, /// it can be discarded without any de-initialization procedures. /// -/// The time complexity is unspecified. -O1HeapInstance* o1heapInit(void* const base, - const size_t size, - const O1HeapHook critical_section_enter, - const O1HeapHook critical_section_leave); +/// The heap is not thread-safe; external synchronization may be required. +O1HeapInstance* o1heapInit(void* const base, const size_t size); /// The semantics follows malloc() with additional guarantees the full list of which is provided below. /// /// If the allocation request is served successfully, a pointer to the newly allocated memory fragment is returned. -/// The returned pointer is guaranteed to be aligned at @ref O1HEAP_ALIGNMENT. +/// The returned pointer is guaranteed to be aligned at O1HEAP_ALIGNMENT. /// /// If the allocation request cannot be served due to the lack of memory or its excessive fragmentation, /// a NULL pointer is returned. /// -/// The function is executed in constant time (unless the critical section management hooks are used and are not -/// constant-time). The allocated memory is NOT zero-filled (because zero-filling is a variable-complexity operation). -/// -/// The function may invoke critical_section_enter and critical_section_leave at most once each (NULL hooks ignored). +/// The function is executed in constant time. +/// The allocated memory is NOT zero-filled (because zero-filling is a variable-complexity operation). void* o1heapAllocate(O1HeapInstance* const handle, const size_t amount); /// The semantics follows free() with additional guarantees the full list of which is provided below. @@ -117,23 +101,18 @@ void* o1heapAllocate(O1HeapInstance* const handle, const size_t amount); /// If the pointer does not point to a previously allocated block and is not NULL, the behavior is undefined. /// Builds where assertion checks are enabled may trigger an assertion failure for some invalid inputs. /// -/// The function is executed in constant time (unless the critical section management hooks are used and are not -/// constant-time). -/// -/// The function may invoke critical_section_enter and critical_section_leave at most once each (NULL hooks ignored). +/// The function is executed in constant time. void o1heapFree(O1HeapInstance* const handle, void* const pointer); /// Performs a basic sanity check on the heap. /// This function can be used as a weak but fast method of heap corruption detection. -/// It invokes critical_section_enter once (unless NULL) and then critical_section_leave once (unless NULL). /// If the handle pointer is NULL, the behavior is undefined. /// The time complexity is constant. /// The return value is truth if the heap looks valid, falsity otherwise. bool o1heapDoInvariantsHold(const O1HeapInstance* const handle); -/// Samples and returns a copy of the diagnostic information, see @ref O1HeapDiagnostics. +/// Samples and returns a copy of the diagnostic information, see O1HeapDiagnostics. /// This function merely copies the structure from an internal storage, so it is fast to return. -/// It invokes critical_section_enter once (unless NULL) and then critical_section_leave once (unless NULL). /// If the handle pointer is NULL, the behavior is undefined. O1HeapDiagnostics o1heapGetDiagnostics(const O1HeapInstance* const handle); diff --git a/sonar-project.properties b/sonar-project.properties deleted file mode 100644 index d62d4b8..0000000 --- a/sonar-project.properties +++ /dev/null @@ -1,9 +0,0 @@ -sonar.projectKey=pavel-kirienko_o1heap -sonar.organization=pavel-kirienko - -sonar.sources=o1heap -sonar.sourceEncoding=UTF-8 - -sonar.cfamily.gcov.reportsPath=. -sonar.cfamily.build-wrapper-output=sonar-dump -sonar.cfamily.cache.enabled=false diff --git a/tests/.clang-tidy b/tests/.clang-tidy index aae3d86..e747551 100644 --- a/tests/.clang-tidy +++ b/tests/.clang-tidy @@ -18,11 +18,14 @@ Checks: >- -google-readability-todo, -readability-avoid-const-params-in-decls, -llvm-header-guard, + -llvm-qualified-auto, -cppcoreguidelines-pro-type-reinterpret-cast, -misc-non-private-member-variables-in-classes, -cppcoreguidelines-pro-bounds-pointer-arithmetic, -cppcoreguidelines-avoid-magic-numbers, -readability-magic-numbers, + -readability-function-cognitive-complexity, + -readability-qualified-auto, WarningsAsErrors: '*' HeaderFilterRegex: 'internal.hpp' AnalyzeTemporaryDtors: false diff --git a/tests/.idea/dictionaries/pavel.xml b/tests/.idea/dictionaries/pavel.xml new file mode 100644 index 0000000..f32082d --- /dev/null +++ b/tests/.idea/dictionaries/pavel.xml @@ -0,0 +1,18 @@ + + + + deallocation + herter + kirienko + noninfringement + nosonar + ogasawara + rebin + uavcan + unbin + wcet + wcmc + wmissing + + + \ No newline at end of file diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index 68b46fe..0827fe5 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -11,7 +11,7 @@ # OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR # OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. # -# Copyright (c) 2020 UAVCAN Development Team +# Copyright (c) 2020 Pavel Kirienko # Authors: Pavel Kirienko cmake_minimum_required(VERSION 3.12) @@ -29,23 +29,23 @@ set(library_dir "${CMAKE_SOURCE_DIR}/../o1heap") # If not suppressed, the tools used here shall be available, otherwise the build will fail. if (NOT NO_STATIC_ANALYSIS) # clang-tidy (separate config files per directory) - find_program(clang_tidy NAMES clang-tidy-10 clang-tidy-9 clang-tidy) + find_program(clang_tidy NAMES clang-tidy-12 clang-tidy) if (NOT clang_tidy) message(FATAL_ERROR "Could not locate clang-tidy") endif () message(STATUS "Using clang-tidy: ${clang_tidy}") - set(CMAKE_C_CLANG_TIDY ${clang_tidy}) + set(CMAKE_C_CLANG_TIDY ${clang_tidy}) set(CMAKE_CXX_CLANG_TIDY ${clang_tidy}) # clang-format - find_program(clang_format NAMES clang-format-10 clang-format-9 clang-format) + find_program(clang_format NAMES clang-format-12 clang-format) if (NOT clang_format) message(FATAL_ERROR "Could not locate clang-format") endif () file( - GLOB format_files - ${library_dir}/*.[ch] - ${CMAKE_SOURCE_DIR}/*.[ch]pp + GLOB format_files + ${library_dir}/*.[ch] + ${CMAKE_SOURCE_DIR}/*.[ch]pp ) message(STATUS "Using clang-format: ${clang_format}; files: ${format_files}") add_custom_target(format COMMAND ${clang_format} -i -fallback-style=none -style=file --verbose ${format_files}) @@ -89,5 +89,13 @@ function(gen_test_matrix name files compile_definitions) endif () endfunction() -gen_test_matrix(test_private test_private.cpp O1HEAP_EXPOSE_INTERNALS=1) -gen_test_matrix(test_general test_general.cpp "") +gen_test_matrix( + test_private + test_private.cpp + "O1HEAP_CONFIG_HEADER=\"${CMAKE_CURRENT_SOURCE_DIR}/cfg_test_internal.h\"" +) +gen_test_matrix( + test_general + test_general.cpp + "" +) diff --git a/tests/cfg_test_internal.h b/tests/cfg_test_internal.h new file mode 100644 index 0000000..f383dfe --- /dev/null +++ b/tests/cfg_test_internal.h @@ -0,0 +1,7 @@ +// This is a config header for O1Heap. It is included from o1heap.c. + +#define O1HEAP_PRIVATE + +#ifdef __GNUC__ +# pragma GCC diagnostic ignored "-Wmissing-declarations" // NOLINT +#endif diff --git a/tests/internal.hpp b/tests/internal.hpp index f6e48db..45c4fba 100644 --- a/tests/internal.hpp +++ b/tests/internal.hpp @@ -11,7 +11,7 @@ // OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR // OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. // -// Copyright (c) 2020 UAVCAN Development Team +// Copyright (c) 2020 Pavel Kirienko // Authors: Pavel Kirienko #ifndef O1HEAP_TESTS_INTERNAL_HPP_INCLUDED @@ -38,7 +38,6 @@ auto isPowerOf2(const std::size_t x) -> bool; auto log2Floor(const std::size_t x) -> std::uint8_t; auto log2Ceil(const std::size_t x) -> std::uint8_t; auto pow2(const std::uint8_t power) -> std::size_t; -void invoke(const O1HeapHook hook); } struct Fragment; @@ -136,7 +135,7 @@ struct Fragment final Fragment(const Fragment&&) = delete; ~Fragment() = delete; auto operator=(const Fragment&) -> Fragment& = delete; - auto operator=(const Fragment &&) -> Fragment& = delete; + auto operator=(const Fragment&&) -> Fragment& = delete; }; /// Please maintain the fields in exact sync with the private definition in o1heap.c! @@ -146,9 +145,6 @@ struct O1HeapInstance final std::size_t nonempty_bin_mask = 0; - O1HeapHook critical_section_enter = nullptr; - O1HeapHook critical_section_leave = nullptr; - /// The same data is available via getDiagnostics(). The duplication is intentional. O1HeapDiagnostics diagnostics{}; @@ -266,7 +262,7 @@ struct O1HeapInstance final O1HeapInstance(const O1HeapInstance&&) = delete; ~O1HeapInstance() = delete; auto operator=(const O1HeapInstance&) -> O1HeapInstance& = delete; - auto operator=(const O1HeapInstance &&) -> O1HeapInstance& = delete; + auto operator=(const O1HeapInstance&&) -> O1HeapInstance& = delete; private: void validateCore() const @@ -389,7 +385,7 @@ struct O1HeapInstance final } }; -static_assert(O1HEAP_VERSION_MAJOR == 1); +static_assert(O1HEAP_VERSION_MAJOR == 2); } // namespace internal diff --git a/tests/test_general.cpp b/tests/test_general.cpp index 2a7e1df..5e4a410 100644 --- a/tests/test_general.cpp +++ b/tests/test_general.cpp @@ -11,7 +11,7 @@ // OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR // OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. // -// Copyright (c) 2020 UAVCAN Development Team +// Copyright (c) 2020 Pavel Kirienko // Authors: Pavel Kirienko #include "internal.hpp" @@ -20,47 +20,6 @@ #include #include -namespace cs -{ -namespace -{ -volatile std::uint64_t g_cnt_enter = 0; -volatile std::uint64_t g_cnt_leave = 0; - -void ensureNotInside() -{ - REQUIRE(g_cnt_enter == g_cnt_leave); -} - -void enter() -{ - ensureNotInside(); - g_cnt_enter++; -} - -void leave() -{ - g_cnt_leave++; - ensureNotInside(); -} - -void resetCounters() -{ - ensureNotInside(); - g_cnt_enter = 0; - g_cnt_leave = 0; -} - -void validateAndReset(const std::uint64_t cnt) -{ - REQUIRE(g_cnt_enter == cnt); - REQUIRE(g_cnt_leave == cnt); - resetCounters(); -} - -} // namespace -} // namespace cs - namespace { constexpr std::size_t KiB = 1024U; @@ -87,26 +46,19 @@ auto getRandomByte() return static_cast(dis(gen)); } -auto init(void* const base, - const std::size_t size, - const O1HeapHook critical_section_enter = nullptr, - const O1HeapHook critical_section_leave = nullptr) +auto init(void* const base, const std::size_t size) { using internal::Fragment; // Fill the beginning of the arena with random bytes (the entire arena may be too slow to fill). std::generate_n(reinterpret_cast(base), std::min(1 * MiB, size), getRandomByte); - const auto heap = reinterpret_cast( - o1heapInit(base, size, critical_section_enter, critical_section_leave)); + const auto heap = reinterpret_cast(o1heapInit(base, size)); if (heap != nullptr) { REQUIRE(reinterpret_cast(heap) % O1HEAP_ALIGNMENT == 0U); - REQUIRE(heap->critical_section_enter == critical_section_enter); - REQUIRE(heap->critical_section_leave == critical_section_leave); - heap->validate(); REQUIRE(heap->nonempty_bin_mask > 0U); @@ -158,12 +110,9 @@ TEST_CASE("General: init") alignas(128) std::array arena{}; - REQUIRE(nullptr == init(nullptr, 0U, nullptr, nullptr)); - REQUIRE(nullptr == init(nullptr, 0U, &cs::enter, &cs::leave)); - REQUIRE(nullptr == init(arena.data(), 0U, nullptr, nullptr)); - REQUIRE(nullptr == init(arena.data(), 0U, &cs::enter, &cs::leave)); - REQUIRE(nullptr == init(arena.data(), 99U, nullptr, nullptr)); // Too small. - REQUIRE(nullptr == init(arena.data(), 99U, &cs::enter, &cs::leave)); + REQUIRE(nullptr == init(nullptr, 0U)); + REQUIRE(nullptr == init(arena.data(), 0U)); + REQUIRE(nullptr == init(arena.data(), 99U)); // Too small. // Check various offsets and sizes to make sure the initialization is done correctly in all cases. for (auto offset = 0U; offset < 7U; offset++) @@ -171,19 +120,12 @@ TEST_CASE("General: init") for (auto size = 99U; size < 5100U; size += 111U) { REQUIRE(arena.size() >= size); - cs::resetCounters(); - auto heap = init(arena.data() + offset, - size - offset, - (offset % 2U == 0U) ? &cs::enter : nullptr, - (offset % 4U == 0U) ? &cs::leave : nullptr); - REQUIRE(cs::g_cnt_enter == 0); - REQUIRE(cs::g_cnt_leave == 0); + auto heap = init(arena.data() + offset, size - offset); if (heap != nullptr) { REQUIRE(size >= sizeof(internal::O1HeapInstance) + Fragment::SizeMin); REQUIRE(reinterpret_cast(heap) >= reinterpret_cast(arena.data())); REQUIRE(reinterpret_cast(heap) % O1HEAP_ALIGNMENT == 0U); - REQUIRE(heap->doInvariantsHold()); } } @@ -196,19 +138,14 @@ TEST_CASE("General: allocate: OOM") constexpr auto ArenaSize = MiB256 + MiB; std::shared_ptr arena(static_cast(std::aligned_alloc(64U, ArenaSize))); - auto heap = init(arena.get(), ArenaSize, &cs::enter, &cs::leave); + auto heap = init(arena.get(), ArenaSize); REQUIRE(heap != nullptr); - cs::resetCounters(); REQUIRE(heap->getDiagnostics().capacity > ArenaSize - 1024U); REQUIRE(heap->getDiagnostics().capacity < ArenaSize); REQUIRE(heap->getDiagnostics().oom_count == 0); - REQUIRE(cs::g_cnt_enter == 3); - REQUIRE(cs::g_cnt_enter == 3); REQUIRE(nullptr == heap->allocate(ArenaSize)); // Too large REQUIRE(heap->getDiagnostics().oom_count == 1); - REQUIRE(cs::g_cnt_enter == 5); - REQUIRE(cs::g_cnt_enter == 5); REQUIRE(nullptr == heap->allocate(ArenaSize - O1HEAP_ALIGNMENT)); // Too large REQUIRE(heap->getDiagnostics().oom_count == 2); @@ -242,18 +179,15 @@ TEST_CASE("General: allocate: smallest") constexpr auto ArenaSize = MiB * 300U; std::shared_ptr arena(static_cast(std::aligned_alloc(64U, ArenaSize))); - auto heap = init(arena.get(), ArenaSize, &cs::enter, &cs::leave); + auto heap = init(arena.get(), ArenaSize); REQUIRE(heap != nullptr); - cs::resetCounters(); void* const mem = heap->allocate(1U); - REQUIRE(((cs::g_cnt_enter == 1) && (cs::g_cnt_leave == 1))); REQUIRE(mem != nullptr); REQUIRE(heap->getDiagnostics().oom_count == 0); REQUIRE(heap->getDiagnostics().peak_allocated == Fragment::SizeMin); REQUIRE(heap->getDiagnostics().allocated == Fragment::SizeMin); REQUIRE(heap->getDiagnostics().peak_request_size == 1); - REQUIRE(((cs::g_cnt_enter == 5) && (cs::g_cnt_leave == 5))); auto& frag = Fragment::constructFromAllocatedMemory(mem); REQUIRE(frag.header.size == (O1HEAP_ALIGNMENT * 2U)); @@ -277,7 +211,6 @@ TEST_CASE("General: allocate: size_t overflow") std::shared_ptr arena(static_cast(std::aligned_alloc(64U, ArenaSize))); auto heap = init(arena.get(), ArenaSize); - cs::resetCounters(); REQUIRE(heap != nullptr); REQUIRE(heap->diagnostics.capacity > (ArenaSize - 1024U)); REQUIRE(heap->diagnostics.capacity < ArenaSize); @@ -325,7 +258,7 @@ TEST_CASE("General: free") using internal::Fragment; alignas(128U) std::array arena{}; - auto heap = init(arena.data(), std::size(arena), &cs::enter, &cs::leave); + auto heap = init(arena.data(), std::size(arena)); REQUIRE(heap != nullptr); REQUIRE(nullptr == heap->allocate(0U)); @@ -335,8 +268,6 @@ TEST_CASE("General: free") REQUIRE(heap->diagnostics.peak_request_size == 0U); REQUIRE(heap->diagnostics.oom_count == 0U); - cs::resetCounters(); - std::size_t allocated = 0U; std::size_t peak_allocated = 0U; std::size_t peak_request_size = 0U; @@ -368,10 +299,8 @@ TEST_CASE("General: free") REQUIRE(heap->diagnostics.allocated == allocated); REQUIRE(heap->diagnostics.peak_allocated == peak_allocated); REQUIRE(heap->diagnostics.peak_request_size == peak_request_size); - cs::validateAndReset(1); heap->matchFragments(reference); REQUIRE(heap->doInvariantsHold()); - cs::validateAndReset(1); return p; }; @@ -387,19 +316,16 @@ TEST_CASE("General: free") REQUIRE(allocated >= frag.header.size); allocated -= frag.header.size; heap->free(p); - cs::validateAndReset(1); } else { heap->free(p); - cs::validateAndReset(0); } REQUIRE(heap->diagnostics.allocated == allocated); REQUIRE(heap->diagnostics.peak_allocated == peak_allocated); REQUIRE(heap->diagnostics.peak_request_size == peak_request_size); heap->matchFragments(reference); REQUIRE(heap->doInvariantsHold()); - cs::validateAndReset(1); }; constexpr auto X = true; // used @@ -554,7 +480,7 @@ TEST_CASE("General: random A") constexpr auto ArenaSize = MiB * 300U; std::shared_ptr arena(static_cast(std::aligned_alloc(64U, ArenaSize))); std::generate_n(arena.get(), ArenaSize, getRandomByte); // Random-fill the ENTIRE arena! - auto heap = init(arena.get(), ArenaSize, cs::enter, cs::leave); + auto heap = init(arena.get(), ArenaSize); REQUIRE(heap != nullptr); std::vector pointers; @@ -641,7 +567,7 @@ TEST_CASE("General: invariant checker") using internal::Fragment; alignas(128U) std::array arena{}; - auto heap = init(arena.data(), std::size(arena), &cs::enter, &cs::leave); + auto heap = init(arena.data(), std::size(arena)); REQUIRE(heap != nullptr); REQUIRE(heap->doInvariantsHold()); auto& dg = heap->diagnostics; diff --git a/tests/test_private.cpp b/tests/test_private.cpp index fb1fc7f..ff0d138 100644 --- a/tests/test_private.cpp +++ b/tests/test_private.cpp @@ -11,7 +11,7 @@ // OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR // OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. // -// Copyright (c) 2020 UAVCAN Development Team +// Copyright (c) 2020 Pavel Kirienko // Authors: Pavel Kirienko #include "internal.hpp" @@ -68,28 +68,3 @@ TEST_CASE("Private: pow2") REQUIRE(pow2(8) == 256); REQUIRE(pow2(9) == 512); } - -namespace -{ -std::uint64_t g_hook_invocation_count = 0; - -extern "C" void hook() -{ - g_hook_invocation_count++; -} - -} // namespace - -TEST_CASE("Private: invokeHook") -{ - using internal::invoke; - REQUIRE(g_hook_invocation_count == 0); - invoke(hook); - REQUIRE(g_hook_invocation_count == 1); - invoke(hook); - REQUIRE(g_hook_invocation_count == 2); - invoke(nullptr); - REQUIRE(g_hook_invocation_count == 2); - invoke(nullptr); - REQUIRE(g_hook_invocation_count == 2); -}