From 6416566b48ec67390cf7fd318f458afbaef3034d Mon Sep 17 00:00:00 2001 From: Parth Aggarwal Date: Wed, 28 Oct 2026 13:24:10 -0700 Subject: [PATCH 01/11] Specify UNIQUE_THANDLE and declare its API Add the requirements and declaration-only headers for UNIQUE_THANDLE, a single-owner handle in the spirit of std::unique_ptr. UNIQUE_THANDLE(T) is a THANDLE(T) whose reference count is always 1; its headers declare only UNIQUE_THANDLE_MOVE, UNIQUE_THANDLE_INITIALIZE_MOVE and UNIQUE_THANDLE_DISPOSE, so the handle cannot be copied through its API. - common/devdoc/unique_thandle_requirements.md: SRS_UNIQUE_THANDLE_43_001..016 - common/devdoc/real_unique_thandle_helper_requirements.md: how to back mocked UNIQUE_THANDLE functions with real ones in unit tests - common/inc/c_pal/unique_thandle_ll.h and unique_thandle.h: the type, function names and the declare macros - unique_thandle_ut: compiles the declare macros with and without mocks The define macros, the reals helper and their unit tests follow separately. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- ...real_unique_thandle_helper_requirements.md | 104 +++++++++++++++++ common/devdoc/unique_thandle_requirements.md | 110 ++++++++++++++++++ common/inc/c_pal/unique_thandle.h | 16 +++ common/inc/c_pal/unique_thandle_ll.h | 39 +++++++ common/tests/CMakeLists.txt | 1 + common/tests/unique_thandle_ut/CMakeLists.txt | 17 +++ .../unique_thandle_ut/unique_thandle_ut.c | 61 ++++++++++ .../unique_thandle_ut/unique_thandle_ut_pch.h | 29 +++++ linux/CMakeLists.txt | 2 + win32/CMakeLists.txt | 2 + 10 files changed, 381 insertions(+) create mode 100644 common/devdoc/real_unique_thandle_helper_requirements.md create mode 100644 common/devdoc/unique_thandle_requirements.md create mode 100644 common/inc/c_pal/unique_thandle.h create mode 100644 common/inc/c_pal/unique_thandle_ll.h create mode 100644 common/tests/unique_thandle_ut/CMakeLists.txt create mode 100644 common/tests/unique_thandle_ut/unique_thandle_ut.c create mode 100644 common/tests/unique_thandle_ut/unique_thandle_ut_pch.h diff --git a/common/devdoc/real_unique_thandle_helper_requirements.md b/common/devdoc/real_unique_thandle_helper_requirements.md new file mode 100644 index 000000000..8bef5479b --- /dev/null +++ b/common/devdoc/real_unique_thandle_helper_requirements.md @@ -0,0 +1,104 @@ +# real_unique_thandle_helper.h Requirements + +## Overview + +`real_unique_thandle_helper.h` is a helper for mocking opaque `UNIQUE_THANDLE` types in unit tests. It is the `UNIQUE_THANDLE` counterpart of `real_thandle_helper.h`. + +It defines the `UNIQUE_THANDLE` functions for a stand-in type and hooks them behind the mocked `UNIQUE_THANDLE_MOVE`, `UNIQUE_THANDLE_INITIALIZE_MOVE` and `UNIQUE_THANDLE_DISPOSE` of the original type. This provides a real implementation of moving and memory management for the original type, but not the behavior of the type itself. + +## How to use: + +See the example in `common\tests\real_unique_thandle_helper_ut\real_unique_thandle_helper_ut.c`. + +1. In the unit test, enable mocks and include `gballoc_hl.h`, `gballoc_hl_redirect.h`, `interlocked.h`, `interlocked_hl.h` and `unique_thandle.h`, along with the header of the type you want to mock. + +```c +#include "umock_c/umock_c_ENABLE_MOCKS.h" // ============================== ENABLE_MOCKS + +#include "c_pal/gballoc_hl.h" +#include "c_pal/gballoc_hl_redirect.h" +#include "c_pal/interlocked.h" +#include "c_pal/interlocked_hl.h" +#include "c_pal/unique_thandle.h" + +#include "mocked_struct.h" // A file that declares an incomplete type MOCKED_STRUCT and UNIQUE_THANDLE_TYPE_DECLARE(MOCKED_STRUCT) + +#include "umock_c/umock_c_DISABLE_MOCKS.h" // ============================== DISABLE_MOCKS +``` + +2. After disabling mocks, include `real_gballoc_hl.h`, `real_interlocked.h`, `real_interlocked_hl.h` and `real_unique_thandle_helper.h`. + +```c +#include "real_gballoc_hl.h" +#include "real_interlocked.h" +#include "real_interlocked_hl.h" + +#include "real_unique_thandle_helper.h" +``` + +3. Include `real_interlocked_renames.h` to temporarily use the real interlocked functions instead of the mocked ones. The `THANDLE` functions under `UNIQUE_THANDLE` use interlocked functions, so this avoids setting interlocked expectations for moves and disposes. + +```c +#include "real_interlocked_renames.h" // IWYU pragma: keep +``` + +4. Define a stand-in type to use in the tests, then call `REAL_UNIQUE_THANDLE_DECLARE` and `REAL_UNIQUE_THANDLE_DEFINE` to define the `UNIQUE_THANDLE` functions for it. + +```c +typedef struct MOCKED_STRUCT_TAG +{ + uint8_t dummy; +} MOCKED_STRUCT; + +REAL_UNIQUE_THANDLE_DECLARE(MOCKED_STRUCT); + +REAL_UNIQUE_THANDLE_DEFINE(MOCKED_STRUCT); +``` + +5. Include `real_interlocked_undo_rename.h` to go back to the mocked interlocked functions. + +```c +#include "real_interlocked_undo_rename.h" // IWYU pragma: keep +``` + +6. In the test suite initialization, call `REGISTER_REAL_UNIQUE_THANDLE_MOCK_HOOK` to hook the real implementation behind the mocked `UNIQUE_THANDLE` functions of the type. + +```c + REGISTER_REAL_UNIQUE_THANDLE_MOCK_HOOK(MOCKED_STRUCT); +``` + +Now, calls in the code under test to `UNIQUE_THANDLE_MOVE(MOCKED_STRUCT)`, `UNIQUE_THANDLE_INITIALIZE_MOVE(MOCKED_STRUCT)` and `UNIQUE_THANDLE_DISPOSE(MOCKED_STRUCT)` are recorded by the mocks and run the real functions of `REAL_MOCKED_STRUCT`. + +Tests create instances with `UNIQUE_THANDLE_MALLOC(REAL_MOCKED_STRUCT)`. The mocked `UNIQUE_THANDLE` functions only take `UNIQUE_THANDLE(MOCKED_STRUCT)*` arguments, which `umock_c` treats as `void*`. If other mocked functions take or return `UNIQUE_THANDLE(MOCKED_STRUCT)` by value, for example a mocked create function, register it with `REGISTER_UMOCK_ALIAS_TYPE(UNIQUE_THANDLE(MOCKED_STRUCT), void*)`. + +## Exposed API + +```c +#define REAL_UNIQUE_THANDLE_DECLARE(TYPE) +#define REAL_UNIQUE_THANDLE_DEFINE(TYPE) +#define REGISTER_REAL_UNIQUE_THANDLE_MOCK_HOOK(TYPE) +``` + +### REAL_UNIQUE_THANDLE_DECLARE(TYPE) + +```c +#define REAL_UNIQUE_THANDLE_DECLARE(TYPE) +``` + +Declares a new `REAL_TYPE` type for the structure `TYPE` and the `UNIQUE_THANDLE` functions for it. + +### REAL_UNIQUE_THANDLE_DEFINE(TYPE) + +```c +#define REAL_UNIQUE_THANDLE_DEFINE(TYPE) +``` + +Defines the `UNIQUE_THANDLE` functions for `REAL_TYPE`, using the "real" gballoc functions. + +### REGISTER_REAL_UNIQUE_THANDLE_MOCK_HOOK(TYPE) + +```c +#define REGISTER_REAL_UNIQUE_THANDLE_MOCK_HOOK(TYPE) +``` + +Hooks the mocked `UNIQUE_THANDLE_MOVE(TYPE)`, `UNIQUE_THANDLE_INITIALIZE_MOVE(TYPE)` and `UNIQUE_THANDLE_DISPOSE(TYPE)` to the real functions of `REAL_TYPE`. diff --git a/common/devdoc/unique_thandle_requirements.md b/common/devdoc/unique_thandle_requirements.md new file mode 100644 index 000000000..db425bea6 --- /dev/null +++ b/common/devdoc/unique_thandle_requirements.md @@ -0,0 +1,110 @@ +# unique_thandle_requirements + +## Overview + +`unique_thandle` is a module that provides macros for handles that have a single owner, like `std::unique_ptr` in C++. + +`UNIQUE_THANDLE(T)` is a `THANDLE(T)` whose reference count is always 1. `UNIQUE_THANDLE_TYPE_DECLARE` declares only the functions that move and dispose the handle. It does not declare `THANDLE_ASSIGN` or `THANDLE_INITIALIZE`, so code that includes the header cannot copy a `UNIQUE_THANDLE(T)` through its API. `UNIQUE_THANDLE_TYPE_DEFINE` still defines the `THANDLE` functions of `T`, so the .c file that defines `T` must not call `THANDLE_ASSIGN`, `THANDLE_INITIALIZE` or `THANDLE_MOVE` on it. + +Ownership moves only through `UNIQUE_THANDLE(T)*` parameters and through return values. A `UNIQUE_THANDLE(T)` passed by value is borrowed. C cannot prevent a raw copy of a handle (for example `UNIQUE_THANDLE(T) copy = handle;`), and this module does not detect one in general. `UNIQUE_THANDLE_MOVE` only refuses a move when both of its arguments hold the same handle. + +A type is either a `THANDLE` or a `UNIQUE_THANDLE`, never both: declaring it as a `THANDLE` too would make the copy functions available. A `UNIQUE_THANDLE(T)` variable must not be moved or disposed from multiple threads at the same time. + +## Exposed API + +```c +/*the type of the handle that wraps T*/ +#define UNIQUE_THANDLE(T) + +/*to be used in a header file*/ +#define UNIQUE_THANDLE_TYPE_DECLARE(T) + +/*to be used in a .c file*/ +#define UNIQUE_THANDLE_TYPE_DEFINE(T) +#define UNIQUE_THANDLE_TYPE_DEFINE_WITH_MALLOC_FUNCTIONS(T, malloc_function, malloc_flex_function, free_function) + +/*declared by UNIQUE_THANDLE_TYPE_DECLARE*/ +MOCKABLE_FUNCTION(, void, UNIQUE_THANDLE_MOVE(T), UNIQUE_THANDLE(T)*, t1, UNIQUE_THANDLE(T)*, t2); +MOCKABLE_FUNCTION(, void, UNIQUE_THANDLE_INITIALIZE_MOVE(T), UNIQUE_THANDLE(T)*, t1, UNIQUE_THANDLE(T)*, t2); +MOCKABLE_FUNCTION(, void, UNIQUE_THANDLE_DISPOSE(T), UNIQUE_THANDLE(T)*, t); + +/*defined by UNIQUE_THANDLE_TYPE_DEFINE for use in the .c file, the same as THANDLE_MALLOC, THANDLE_MALLOC_FLEX, THANDLE_FREE and THANDLE_GET_T*/ +static T* UNIQUE_THANDLE_MALLOC(T)(void(*dispose)(T*)); +static T* UNIQUE_THANDLE_MALLOC_FLEX(T)(void(*dispose)(T*), size_t nmemb, size_t size); +static void UNIQUE_THANDLE_FREE(T)(T* t); +static T* UNIQUE_THANDLE_GET_T(T)(UNIQUE_THANDLE(T) t); +``` + +`unique_thandle_ll.h` also provides `UNIQUE_THANDLE_LL_TYPE_DECLARE(C, T)`, `UNIQUE_THANDLE_LL_TYPE_DEFINE(C, T)` and `UNIQUE_THANDLE_LL_TYPE_DEFINE_WITH_MALLOC_FUNCTIONS(C, T, malloc_function, malloc_flex_function, free_function)`. Like `THANDLE_LL_TYPE_DECLARE` and `THANDLE_LL_TYPE_DEFINE`, they name the functions after `C` and wrap the type `T`. + +## UNIQUE_THANDLE_TYPE_DEFINE(T) + +```c +#define UNIQUE_THANDLE_TYPE_DEFINE(T) +``` + +`UNIQUE_THANDLE_TYPE_DEFINE` defines the functions declared by `UNIQUE_THANDLE_TYPE_DECLARE` and the memory functions `UNIQUE_THANDLE_MALLOC(T)`, `UNIQUE_THANDLE_MALLOC_FLEX(T)`, `UNIQUE_THANDLE_FREE(T)` and `UNIQUE_THANDLE_GET_T(T)`. + +**SRS_UNIQUE_THANDLE_43_001: [** `UNIQUE_THANDLE_TYPE_DEFINE` shall use `malloc`, `malloc_flex` and `free` to allocate and free memory. **]** + +## UNIQUE_THANDLE_TYPE_DEFINE_WITH_MALLOC_FUNCTIONS(T, malloc_function, malloc_flex_function, free_function) + +```c +#define UNIQUE_THANDLE_TYPE_DEFINE_WITH_MALLOC_FUNCTIONS(T, malloc_function, malloc_flex_function, free_function) +``` + +`UNIQUE_THANDLE_TYPE_DEFINE_WITH_MALLOC_FUNCTIONS` behaves like `UNIQUE_THANDLE_TYPE_DEFINE`, with memory functions specific to `T`. + +**SRS_UNIQUE_THANDLE_43_002: [** `UNIQUE_THANDLE_TYPE_DEFINE_WITH_MALLOC_FUNCTIONS` shall use `malloc_function`, `malloc_flex_function` and `free_function` to allocate and free memory. **]** + +## UNIQUE_THANDLE_MOVE(T) + +```c +MOCKABLE_FUNCTION(, void, UNIQUE_THANDLE_MOVE(T), UNIQUE_THANDLE(T)*, t1, UNIQUE_THANDLE(T)*, t2); +``` + +`UNIQUE_THANDLE_MOVE` moves `*t2` into `*t1` and disposes the handle that `*t1` held. + +**SRS_UNIQUE_THANDLE_43_003: [** If `t1` is `NULL` then `UNIQUE_THANDLE_MOVE` shall return. **]** + +**SRS_UNIQUE_THANDLE_43_004: [** If `t2` is `NULL` then `UNIQUE_THANDLE_MOVE` shall return. **]** + +**SRS_UNIQUE_THANDLE_43_005: [** If `*t1` is not `NULL` and `*t1` is equal to `*t2` then `UNIQUE_THANDLE_MOVE` shall return without changing `*t1` and `*t2`. **]** + +**SRS_UNIQUE_THANDLE_43_006: [** `UNIQUE_THANDLE_MOVE` shall store `*t2` in `*t1` and set `*t2` to `NULL`. **]** + +**SRS_UNIQUE_THANDLE_43_007: [** If `*t1` was not `NULL` then, after updating `*t1` and `*t2`, `UNIQUE_THANDLE_MOVE` shall call the `dispose` function of the previous `*t1`, if it is not `NULL`, and free its memory. **]** + +## UNIQUE_THANDLE_INITIALIZE_MOVE(T) + +```c +MOCKABLE_FUNCTION(, void, UNIQUE_THANDLE_INITIALIZE_MOVE(T), UNIQUE_THANDLE(T)*, t1, UNIQUE_THANDLE(T)*, t2); +``` + +`UNIQUE_THANDLE_INITIALIZE_MOVE` moves `*t2` into `*t1`, where `*t1` is not initialized. For example, `*t1` is a field of a structure that has just been allocated. + +**SRS_UNIQUE_THANDLE_43_008: [** If `t1` is `NULL` then `UNIQUE_THANDLE_INITIALIZE_MOVE` shall return. **]** + +**SRS_UNIQUE_THANDLE_43_009: [** If `t2` is `NULL` then `UNIQUE_THANDLE_INITIALIZE_MOVE` shall return. **]** + +**SRS_UNIQUE_THANDLE_43_010: [** If `t1` is equal to `t2` then `UNIQUE_THANDLE_INITIALIZE_MOVE` shall return without changing `*t1`. **]** + +**SRS_UNIQUE_THANDLE_43_011: [** `UNIQUE_THANDLE_INITIALIZE_MOVE` shall store `*t2` in `*t1` and set `*t2` to `NULL`, without disposing the previous content of `*t1`. **]** + +## UNIQUE_THANDLE_DISPOSE(T) + +```c +MOCKABLE_FUNCTION(, void, UNIQUE_THANDLE_DISPOSE(T), UNIQUE_THANDLE(T)*, t); +``` + +`UNIQUE_THANDLE_DISPOSE` disposes the handle in `*t` and sets `*t` to `NULL`. + +**SRS_UNIQUE_THANDLE_43_012: [** If `t` is `NULL` then `UNIQUE_THANDLE_DISPOSE` shall return. **]** + +**SRS_UNIQUE_THANDLE_43_013: [** If `*t` is `NULL` then `UNIQUE_THANDLE_DISPOSE` shall return. **]** + +**SRS_UNIQUE_THANDLE_43_014: [** `UNIQUE_THANDLE_DISPOSE` shall set `*t` to `NULL`. **]** + +**SRS_UNIQUE_THANDLE_43_015: [** `UNIQUE_THANDLE_DISPOSE` shall then call the `dispose` function passed when the handle was created, if it is not `NULL`. **]** + +**SRS_UNIQUE_THANDLE_43_016: [** `UNIQUE_THANDLE_DISPOSE` shall free the memory of the handle. **]** diff --git a/common/inc/c_pal/unique_thandle.h b/common/inc/c_pal/unique_thandle.h new file mode 100644 index 000000000..165c34c2a --- /dev/null +++ b/common/inc/c_pal/unique_thandle.h @@ -0,0 +1,16 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +#ifndef UNIQUE_THANDLE_H +#define UNIQUE_THANDLE_H + +#include "macro_utils/macro_utils.h" + +#include "c_pal/unique_thandle_ll.h" + +/*macro to be used in headers*/ +/*introduces UNIQUE_THANDLE(T) for a previously defined type T and declares the functions that move and dispose it*/ +#define UNIQUE_THANDLE_TYPE_DECLARE(T) \ + UNIQUE_THANDLE_LL_TYPE_DECLARE(T, T) + +#endif /*UNIQUE_THANDLE_H*/ diff --git a/common/inc/c_pal/unique_thandle_ll.h b/common/inc/c_pal/unique_thandle_ll.h new file mode 100644 index 000000000..ac1d6bb51 --- /dev/null +++ b/common/inc/c_pal/unique_thandle_ll.h @@ -0,0 +1,39 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +#ifndef UNIQUE_THANDLE_LL_H +#define UNIQUE_THANDLE_LL_H + +#include "macro_utils/macro_utils.h" + +#include "c_pal/thandle_ll.h" + +#include "umock_c/umock_c_prod.h" + +/*a UNIQUE_THANDLE(T) is a THANDLE(T) that has a single owner, so its reference count is always 1*/ +#define UNIQUE_THANDLE(T) THANDLE(T) + +/*given a previous type T, these are the names of the memory functions that UNIQUE_THANDLE_LL_TYPE_DEFINE defines for T. They are the THANDLE memory functions*/ +#define UNIQUE_THANDLE_MALLOC(C) THANDLE_MALLOC(C) +#define UNIQUE_THANDLE_MALLOC_FLEX(C) THANDLE_MALLOC_FLEX(C) +#define UNIQUE_THANDLE_FREE(C) THANDLE_FREE(C) +#define UNIQUE_THANDLE_GET_T(C) THANDLE_GET_T(C) + +/*given a previous type T, UNIQUE_THANDLE_MOVE introduces a new name for a function that moves a handle into an initialized handle and disposes the handle it held*/ +#define UNIQUE_THANDLE_MOVE(C) MU_C2(C, _UNIQUE_MOVE) + +/*given a previous type T, UNIQUE_THANDLE_INITIALIZE_MOVE introduces a new name for a function that moves a handle into an uninitialized handle*/ +#define UNIQUE_THANDLE_INITIALIZE_MOVE(C) MU_C2(C, _UNIQUE_INITIALIZE_MOVE) + +/*given a previous type T, UNIQUE_THANDLE_DISPOSE introduces a new name for a function that disposes a handle and sets it to NULL*/ +#define UNIQUE_THANDLE_DISPOSE(C) MU_C2(C, _UNIQUE_DISPOSE) + +/*macro to be used in headers*/ +/*introduces UNIQUE_THANDLE(T) and declares the functions that move and dispose it, named after C. No function that copies the handle is declared*/ +#define UNIQUE_THANDLE_LL_TYPE_DECLARE(C, T) \ + THANDLE_MACRO(T); \ + MOCKABLE_FUNCTION(, void, UNIQUE_THANDLE_MOVE(C), UNIQUE_THANDLE(T)*, t1, UNIQUE_THANDLE(T)*, t2); \ + MOCKABLE_FUNCTION(, void, UNIQUE_THANDLE_INITIALIZE_MOVE(C), UNIQUE_THANDLE(T)*, t1, UNIQUE_THANDLE(T)*, t2); \ + MOCKABLE_FUNCTION(, void, UNIQUE_THANDLE_DISPOSE(C), UNIQUE_THANDLE(T)*, t); \ + +#endif /*UNIQUE_THANDLE_LL_H*/ diff --git a/common/tests/CMakeLists.txt b/common/tests/CMakeLists.txt index a99e47637..38547049a 100644 --- a/common/tests/CMakeLists.txt +++ b/common/tests/CMakeLists.txt @@ -23,6 +23,7 @@ if(${run_unittests}) build_test_folder(real_thandle_log_context_handle_ut) build_test_folder(tqueue_ut) build_test_folder(timed_test_suite_ut) + build_test_folder(unique_thandle_ut) endif() if(${run_int_tests}) diff --git a/common/tests/unique_thandle_ut/CMakeLists.txt b/common/tests/unique_thandle_ut/CMakeLists.txt new file mode 100644 index 000000000..d5a461a0f --- /dev/null +++ b/common/tests/unique_thandle_ut/CMakeLists.txt @@ -0,0 +1,17 @@ +#Copyright (c) Microsoft. All rights reserved. +#Licensed under the MIT license. See LICENSE file in the project root for full license information. + +set(theseTestsName unique_thandle_ut) + +set(${theseTestsName}_test_files + ${theseTestsName}.c +) + +set(${theseTestsName}_h_files + ../../inc/c_pal/unique_thandle.h + ../../inc/c_pal/unique_thandle_ll.h +) + +build_test_artifacts(${theseTestsName} "tests/c_pal" ADDITIONAL_LIBS c_pal c_pal_reals + ENABLE_TEST_FILES_PRECOMPILED_HEADERS "${CMAKE_CURRENT_LIST_DIR}/unique_thandle_ut_pch.h" +) diff --git a/common/tests/unique_thandle_ut/unique_thandle_ut.c b/common/tests/unique_thandle_ut/unique_thandle_ut.c new file mode 100644 index 000000000..63c7f6f43 --- /dev/null +++ b/common/tests/unique_thandle_ut/unique_thandle_ut.c @@ -0,0 +1,61 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +#include "unique_thandle_ut_pch.h" + +/*a type whose UNIQUE_THANDLE functions are declared without mocks and named after a name other than the type*/ +typedef struct LL_STRUCT_TAG LL_STRUCT; +UNIQUE_THANDLE_LL_TYPE_DECLARE(LL_NAME, LL_STRUCT); + +MU_DEFINE_ENUM_STRINGS(UMOCK_C_ERROR_CODE, UMOCK_C_ERROR_CODE_VALUES) + +static void on_umock_c_error(UMOCK_C_ERROR_CODE error_code) +{ + ASSERT_FAIL("umock_c reported error :%" PRI_MU_ENUM "", MU_ENUM_VALUE(UMOCK_C_ERROR_CODE, error_code)); +} + +BEGIN_TEST_SUITE(TEST_SUITE_NAME_FROM_CMAKE) + +TEST_SUITE_INITIALIZE(suite_init) +{ + ASSERT_ARE_EQUAL(int, 0, umock_c_init(on_umock_c_error)); +} + +TEST_SUITE_CLEANUP(suite_cleanup) +{ + umock_c_deinit(); +} + +TEST_FUNCTION_INITIALIZE(method_init) +{ + umock_c_reset_all_calls(); +} + +TEST_FUNCTION_CLEANUP(method_cleanup) +{ +} + +TEST_FUNCTION(UNIQUE_THANDLE_TYPE_DECLARE_declares_mockable_move_initialize_move_and_dispose) // no-srs +{ + // arrange + UNIQUE_THANDLE(MOCKED_STRUCT) t1 = NULL; + UNIQUE_THANDLE(MOCKED_STRUCT) t2 = NULL; + UNIQUE_THANDLE(LL_STRUCT) ll = NULL; + + STRICT_EXPECTED_CALL(UNIQUE_THANDLE_MOVE(MOCKED_STRUCT)(&t1, &t2)); + STRICT_EXPECTED_CALL(UNIQUE_THANDLE_INITIALIZE_MOVE(MOCKED_STRUCT)(&t1, &t2)); + STRICT_EXPECTED_CALL(UNIQUE_THANDLE_DISPOSE(MOCKED_STRUCT)(&t1)); + + // act + UNIQUE_THANDLE_MOVE(MOCKED_STRUCT)(&t1, &t2); + UNIQUE_THANDLE_INITIALIZE_MOVE(MOCKED_STRUCT)(&t1, &t2); + UNIQUE_THANDLE_DISPOSE(MOCKED_STRUCT)(&t1); + + // assert + ASSERT_ARE_EQUAL(char_ptr, umock_c_get_expected_calls(), umock_c_get_actual_calls()); + ASSERT_IS_NULL(t1); + ASSERT_IS_NULL(t2); + ASSERT_IS_NULL(ll); +} + +END_TEST_SUITE(TEST_SUITE_NAME_FROM_CMAKE) diff --git a/common/tests/unique_thandle_ut/unique_thandle_ut_pch.h b/common/tests/unique_thandle_ut/unique_thandle_ut_pch.h new file mode 100644 index 000000000..5bbaa63e7 --- /dev/null +++ b/common/tests/unique_thandle_ut/unique_thandle_ut_pch.h @@ -0,0 +1,29 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +// Precompiled header for unique_thandle_ut + +#ifndef UNIQUE_THANDLE_UT_PCH_H +#define UNIQUE_THANDLE_UT_PCH_H + +#include + +#include "macro_utils/macro_utils.h" // IWYU pragma: keep + +#include "testrunnerswitcher.h" + +#include "umock_c/umock_c.h" + +#include "c_pal/unique_thandle.h" +#include "c_pal/unique_thandle_ll.h" // IWYU pragma: keep + +/*a type whose UNIQUE_THANDLE functions are mocked*/ +typedef struct MOCKED_STRUCT_TAG MOCKED_STRUCT; + +#include "umock_c/umock_c_ENABLE_MOCKS.h" // ============================== ENABLE_MOCKS + +UNIQUE_THANDLE_TYPE_DECLARE(MOCKED_STRUCT); + +#include "umock_c/umock_c_DISABLE_MOCKS.h" // ============================== DISABLE_MOCKS + +#endif // UNIQUE_THANDLE_UT_PCH_H diff --git a/linux/CMakeLists.txt b/linux/CMakeLists.txt index 9105f713f..d649cfeb4 100644 --- a/linux/CMakeLists.txt +++ b/linux/CMakeLists.txt @@ -15,6 +15,8 @@ set(pal_common_h_files ../common/inc/c_pal/thandle_log_context_handle.h ../common/inc/c_pal/tqueue.h ../common/inc/c_pal/tqueue_ll.h + ../common/inc/c_pal/unique_thandle.h + ../common/inc/c_pal/unique_thandle_ll.h ) set(pal_common_c_files diff --git a/win32/CMakeLists.txt b/win32/CMakeLists.txt index fc1ee57e9..90a4ff3c3 100644 --- a/win32/CMakeLists.txt +++ b/win32/CMakeLists.txt @@ -14,6 +14,8 @@ set(pal_common_h_files ../common/inc/c_pal/thandle_log_context_handle.h ../common/inc/c_pal/tqueue.h ../common/inc/c_pal/tqueue_ll.h + ../common/inc/c_pal/unique_thandle.h + ../common/inc/c_pal/unique_thandle_ll.h ) set(pal_common_c_files From ef43ba5029c0bb384301c450b56302dcb50115a7 Mon Sep 17 00:00:00 2001 From: Parth Aggarwal Date: Wed, 28 Oct 2026 18:18:53 -0700 Subject: [PATCH 02/11] Do not define copy functions for UNIQUE_THANDLE, drop the C++ comparison The UNIQUE_THANDLE macros declare and define only the functions that move and dispose a handle and the memory functions of the type, so no code can copy a UNIQUE_THANDLE through an API, not even the .c file that defines the type. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- common/devdoc/unique_thandle_requirements.md | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/common/devdoc/unique_thandle_requirements.md b/common/devdoc/unique_thandle_requirements.md index db425bea6..6a9aa678e 100644 --- a/common/devdoc/unique_thandle_requirements.md +++ b/common/devdoc/unique_thandle_requirements.md @@ -2,13 +2,13 @@ ## Overview -`unique_thandle` is a module that provides macros for handles that have a single owner, like `std::unique_ptr` in C++. +`unique_thandle` is a module that provides macros for handles that have a single owner. -`UNIQUE_THANDLE(T)` is a `THANDLE(T)` whose reference count is always 1. `UNIQUE_THANDLE_TYPE_DECLARE` declares only the functions that move and dispose the handle. It does not declare `THANDLE_ASSIGN` or `THANDLE_INITIALIZE`, so code that includes the header cannot copy a `UNIQUE_THANDLE(T)` through its API. `UNIQUE_THANDLE_TYPE_DEFINE` still defines the `THANDLE` functions of `T`, so the .c file that defines `T` must not call `THANDLE_ASSIGN`, `THANDLE_INITIALIZE` or `THANDLE_MOVE` on it. +`UNIQUE_THANDLE(T)` is a `THANDLE(T)` whose reference count is always 1. `UNIQUE_THANDLE_TYPE_DECLARE` and `UNIQUE_THANDLE_TYPE_DEFINE` declare and define only the functions that move and dispose the handle, and the memory functions of `T`. They do not declare or define `THANDLE_ASSIGN`, `THANDLE_INITIALIZE`, `THANDLE_MOVE` or `THANDLE_INITIALIZE_MOVE` for `T`, so no code can copy a `UNIQUE_THANDLE(T)` through an API, not even the .c file that defines `T`. Ownership moves only through `UNIQUE_THANDLE(T)*` parameters and through return values. A `UNIQUE_THANDLE(T)` passed by value is borrowed. C cannot prevent a raw copy of a handle (for example `UNIQUE_THANDLE(T) copy = handle;`), and this module does not detect one in general. `UNIQUE_THANDLE_MOVE` only refuses a move when both of its arguments hold the same handle. -A type is either a `THANDLE` or a `UNIQUE_THANDLE`, never both: declaring it as a `THANDLE` too would make the copy functions available. A `UNIQUE_THANDLE(T)` variable must not be moved or disposed from multiple threads at the same time. +A type is either a `THANDLE` or a `UNIQUE_THANDLE`, never both. A `UNIQUE_THANDLE(T)` variable must not be moved or disposed from multiple threads at the same time. ## Exposed API @@ -43,7 +43,7 @@ static T* UNIQUE_THANDLE_GET_T(T)(UNIQUE_THANDLE(T) t); #define UNIQUE_THANDLE_TYPE_DEFINE(T) ``` -`UNIQUE_THANDLE_TYPE_DEFINE` defines the functions declared by `UNIQUE_THANDLE_TYPE_DECLARE` and the memory functions `UNIQUE_THANDLE_MALLOC(T)`, `UNIQUE_THANDLE_MALLOC_FLEX(T)`, `UNIQUE_THANDLE_FREE(T)` and `UNIQUE_THANDLE_GET_T(T)`. +`UNIQUE_THANDLE_TYPE_DEFINE` defines the functions declared by `UNIQUE_THANDLE_TYPE_DECLARE` and the memory functions `UNIQUE_THANDLE_MALLOC(T)`, `UNIQUE_THANDLE_MALLOC_FLEX(T)`, `UNIQUE_THANDLE_FREE(T)` and `UNIQUE_THANDLE_GET_T(T)`. It does not define `THANDLE_ASSIGN`, `THANDLE_INITIALIZE`, `THANDLE_MOVE` or `THANDLE_INITIALIZE_MOVE` for `T`. **SRS_UNIQUE_THANDLE_43_001: [** `UNIQUE_THANDLE_TYPE_DEFINE` shall use `malloc`, `malloc_flex` and `free` to allocate and free memory. **]** From 2ca4bec6fd2c0c9784878d7085074111ae16911f Mon Sep 17 00:00:00 2001 From: Parth Aggarwal Date: Thu, 29 Oct 2026 10:27:27 -0700 Subject: [PATCH 03/11] Describe UNIQUE_THANDLE without relating it to THANDLE The spec describes UNIQUE_THANDLE as a handle with a single owner that is created, moved and disposed, and no longer refers to THANDLE, which is an implementation detail. The memory functions get their own descriptions, and the rule that a type is never both a THANDLE and a UNIQUE_THANDLE is gone. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- .../real_unique_thandle_helper_requirements.md | 4 ++-- common/devdoc/unique_thandle_requirements.md | 14 +++++++++----- common/inc/c_pal/unique_thandle_ll.h | 2 +- 3 files changed, 12 insertions(+), 8 deletions(-) diff --git a/common/devdoc/real_unique_thandle_helper_requirements.md b/common/devdoc/real_unique_thandle_helper_requirements.md index 8bef5479b..89d485d46 100644 --- a/common/devdoc/real_unique_thandle_helper_requirements.md +++ b/common/devdoc/real_unique_thandle_helper_requirements.md @@ -2,7 +2,7 @@ ## Overview -`real_unique_thandle_helper.h` is a helper for mocking opaque `UNIQUE_THANDLE` types in unit tests. It is the `UNIQUE_THANDLE` counterpart of `real_thandle_helper.h`. +`real_unique_thandle_helper.h` is a helper for mocking opaque `UNIQUE_THANDLE` types in unit tests. It defines the `UNIQUE_THANDLE` functions for a stand-in type and hooks them behind the mocked `UNIQUE_THANDLE_MOVE`, `UNIQUE_THANDLE_INITIALIZE_MOVE` and `UNIQUE_THANDLE_DISPOSE` of the original type. This provides a real implementation of moving and memory management for the original type, but not the behavior of the type itself. @@ -36,7 +36,7 @@ See the example in `common\tests\real_unique_thandle_helper_ut\real_unique_thand #include "real_unique_thandle_helper.h" ``` -3. Include `real_interlocked_renames.h` to temporarily use the real interlocked functions instead of the mocked ones. The `THANDLE` functions under `UNIQUE_THANDLE` use interlocked functions, so this avoids setting interlocked expectations for moves and disposes. +3. Include `real_interlocked_renames.h` to temporarily use the real interlocked functions instead of the mocked ones. The `UNIQUE_THANDLE` functions use interlocked functions, so this avoids setting interlocked expectations for moves and disposes. ```c #include "real_interlocked_renames.h" // IWYU pragma: keep diff --git a/common/devdoc/unique_thandle_requirements.md b/common/devdoc/unique_thandle_requirements.md index 6a9aa678e..6a4d08734 100644 --- a/common/devdoc/unique_thandle_requirements.md +++ b/common/devdoc/unique_thandle_requirements.md @@ -4,11 +4,11 @@ `unique_thandle` is a module that provides macros for handles that have a single owner. -`UNIQUE_THANDLE(T)` is a `THANDLE(T)` whose reference count is always 1. `UNIQUE_THANDLE_TYPE_DECLARE` and `UNIQUE_THANDLE_TYPE_DEFINE` declare and define only the functions that move and dispose the handle, and the memory functions of `T`. They do not declare or define `THANDLE_ASSIGN`, `THANDLE_INITIALIZE`, `THANDLE_MOVE` or `THANDLE_INITIALIZE_MOVE` for `T`, so no code can copy a `UNIQUE_THANDLE(T)` through an API, not even the .c file that defines `T`. +`UNIQUE_THANDLE(T)` is a handle to an object of type `T` that has exactly one owner at a time. The .c file that defines `T` creates the object with `UNIQUE_THANDLE_MALLOC` or `UNIQUE_THANDLE_MALLOC_FLEX`. Each owner then either moves the handle to the next owner, with `UNIQUE_THANDLE_MOVE` or `UNIQUE_THANDLE_INITIALIZE_MOVE`, or disposes the object with `UNIQUE_THANDLE_DISPOSE`. No function copies a handle, so no code can give an object a second owner through an API, not even the .c file that defines `T`. Ownership moves only through `UNIQUE_THANDLE(T)*` parameters and through return values. A `UNIQUE_THANDLE(T)` passed by value is borrowed. C cannot prevent a raw copy of a handle (for example `UNIQUE_THANDLE(T) copy = handle;`), and this module does not detect one in general. `UNIQUE_THANDLE_MOVE` only refuses a move when both of its arguments hold the same handle. -A type is either a `THANDLE` or a `UNIQUE_THANDLE`, never both. A `UNIQUE_THANDLE(T)` variable must not be moved or disposed from multiple threads at the same time. +A `UNIQUE_THANDLE(T)` variable must not be moved or disposed from multiple threads at the same time. ## Exposed API @@ -28,14 +28,18 @@ MOCKABLE_FUNCTION(, void, UNIQUE_THANDLE_MOVE(T), UNIQUE_THANDLE(T)*, t1, UNIQUE MOCKABLE_FUNCTION(, void, UNIQUE_THANDLE_INITIALIZE_MOVE(T), UNIQUE_THANDLE(T)*, t1, UNIQUE_THANDLE(T)*, t2); MOCKABLE_FUNCTION(, void, UNIQUE_THANDLE_DISPOSE(T), UNIQUE_THANDLE(T)*, t); -/*defined by UNIQUE_THANDLE_TYPE_DEFINE for use in the .c file, the same as THANDLE_MALLOC, THANDLE_MALLOC_FLEX, THANDLE_FREE and THANDLE_GET_T*/ +/*defined by UNIQUE_THANDLE_TYPE_DEFINE for use in the .c file that defines T*/ +/*allocates a T. dispose, if it is not NULL, is called when the object is disposed*/ static T* UNIQUE_THANDLE_MALLOC(T)(void(*dispose)(T*)); +/*allocates a T with an additional nmemb * size bytes for its flexible array member*/ static T* UNIQUE_THANDLE_MALLOC_FLEX(T)(void(*dispose)(T*), size_t nmemb, size_t size); +/*frees a T allocated by UNIQUE_THANDLE_MALLOC or UNIQUE_THANDLE_MALLOC_FLEX without calling dispose, for example when creating the object fails*/ static void UNIQUE_THANDLE_FREE(T)(T* t); +/*returns a pointer to the T of a handle, or NULL if t is NULL*/ static T* UNIQUE_THANDLE_GET_T(T)(UNIQUE_THANDLE(T) t); ``` -`unique_thandle_ll.h` also provides `UNIQUE_THANDLE_LL_TYPE_DECLARE(C, T)`, `UNIQUE_THANDLE_LL_TYPE_DEFINE(C, T)` and `UNIQUE_THANDLE_LL_TYPE_DEFINE_WITH_MALLOC_FUNCTIONS(C, T, malloc_function, malloc_flex_function, free_function)`. Like `THANDLE_LL_TYPE_DECLARE` and `THANDLE_LL_TYPE_DEFINE`, they name the functions after `C` and wrap the type `T`. +`unique_thandle_ll.h` also provides `UNIQUE_THANDLE_LL_TYPE_DECLARE(C, T)`, `UNIQUE_THANDLE_LL_TYPE_DEFINE(C, T)` and `UNIQUE_THANDLE_LL_TYPE_DEFINE_WITH_MALLOC_FUNCTIONS(C, T, malloc_function, malloc_flex_function, free_function)`. They name the functions after `C` and wrap the type `T`. ## UNIQUE_THANDLE_TYPE_DEFINE(T) @@ -43,7 +47,7 @@ static T* UNIQUE_THANDLE_GET_T(T)(UNIQUE_THANDLE(T) t); #define UNIQUE_THANDLE_TYPE_DEFINE(T) ``` -`UNIQUE_THANDLE_TYPE_DEFINE` defines the functions declared by `UNIQUE_THANDLE_TYPE_DECLARE` and the memory functions `UNIQUE_THANDLE_MALLOC(T)`, `UNIQUE_THANDLE_MALLOC_FLEX(T)`, `UNIQUE_THANDLE_FREE(T)` and `UNIQUE_THANDLE_GET_T(T)`. It does not define `THANDLE_ASSIGN`, `THANDLE_INITIALIZE`, `THANDLE_MOVE` or `THANDLE_INITIALIZE_MOVE` for `T`. +`UNIQUE_THANDLE_TYPE_DEFINE` defines the functions declared by `UNIQUE_THANDLE_TYPE_DECLARE` and the memory functions `UNIQUE_THANDLE_MALLOC(T)`, `UNIQUE_THANDLE_MALLOC_FLEX(T)`, `UNIQUE_THANDLE_FREE(T)` and `UNIQUE_THANDLE_GET_T(T)`. It defines no function that copies a handle. **SRS_UNIQUE_THANDLE_43_001: [** `UNIQUE_THANDLE_TYPE_DEFINE` shall use `malloc`, `malloc_flex` and `free` to allocate and free memory. **]** diff --git a/common/inc/c_pal/unique_thandle_ll.h b/common/inc/c_pal/unique_thandle_ll.h index ac1d6bb51..fb63d0799 100644 --- a/common/inc/c_pal/unique_thandle_ll.h +++ b/common/inc/c_pal/unique_thandle_ll.h @@ -10,7 +10,7 @@ #include "umock_c/umock_c_prod.h" -/*a UNIQUE_THANDLE(T) is a THANDLE(T) that has a single owner, so its reference count is always 1*/ +/*a handle to a T that has a single owner. It is implemented as a THANDLE(T) whose reference count is always 1*/ #define UNIQUE_THANDLE(T) THANDLE(T) /*given a previous type T, these are the names of the memory functions that UNIQUE_THANDLE_LL_TYPE_DEFINE defines for T. They are the THANDLE memory functions*/ From 7993e128ad6fd62fce2a4d0229a5587ad80bbe1d Mon Sep 17 00:00:00 2001 From: Parth Aggarwal Date: Thu, 29 Oct 2026 11:32:03 -0700 Subject: [PATCH 04/11] Remove UNIQUE_THANDLE_MOVE so that every dispose is explicit UNIQUE_THANDLE_MOVE disposed the object that its destination held, so a move could drop an object without an explicit dispose. Remove it and its requirements (SRS_UNIQUE_THANDLE_43_003 to 007). UNIQUE_THANDLE_INITIALIZE_MOVE is now the only move. Its destination is NULL or not initialized. An object that the destination holds must first be disposed with UNIQUE_THANDLE_DISPOSE or moved to another handle. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- ...real_unique_thandle_helper_requirements.md | 8 ++--- common/devdoc/unique_thandle_requirements.md | 29 +++++-------------- common/inc/c_pal/unique_thandle_ll.h | 6 +--- .../unique_thandle_ut/unique_thandle_ut.c | 4 +-- 4 files changed, 13 insertions(+), 34 deletions(-) diff --git a/common/devdoc/real_unique_thandle_helper_requirements.md b/common/devdoc/real_unique_thandle_helper_requirements.md index 89d485d46..cc493e019 100644 --- a/common/devdoc/real_unique_thandle_helper_requirements.md +++ b/common/devdoc/real_unique_thandle_helper_requirements.md @@ -4,7 +4,7 @@ `real_unique_thandle_helper.h` is a helper for mocking opaque `UNIQUE_THANDLE` types in unit tests. -It defines the `UNIQUE_THANDLE` functions for a stand-in type and hooks them behind the mocked `UNIQUE_THANDLE_MOVE`, `UNIQUE_THANDLE_INITIALIZE_MOVE` and `UNIQUE_THANDLE_DISPOSE` of the original type. This provides a real implementation of moving and memory management for the original type, but not the behavior of the type itself. +It defines the `UNIQUE_THANDLE` functions for a stand-in type and hooks them behind the mocked `UNIQUE_THANDLE_INITIALIZE_MOVE` and `UNIQUE_THANDLE_DISPOSE` of the original type. This provides a real implementation of moving and memory management for the original type, but not the behavior of the type itself. ## How to use: @@ -36,7 +36,7 @@ See the example in `common\tests\real_unique_thandle_helper_ut\real_unique_thand #include "real_unique_thandle_helper.h" ``` -3. Include `real_interlocked_renames.h` to temporarily use the real interlocked functions instead of the mocked ones. The `UNIQUE_THANDLE` functions use interlocked functions, so this avoids setting interlocked expectations for moves and disposes. +3. Include `real_interlocked_renames.h` to temporarily use the real interlocked functions instead of the mocked ones. The `UNIQUE_THANDLE` functions use interlocked functions, so this avoids setting interlocked expectations when tests create and dispose handles. ```c #include "real_interlocked_renames.h" // IWYU pragma: keep @@ -67,7 +67,7 @@ REAL_UNIQUE_THANDLE_DEFINE(MOCKED_STRUCT); REGISTER_REAL_UNIQUE_THANDLE_MOCK_HOOK(MOCKED_STRUCT); ``` -Now, calls in the code under test to `UNIQUE_THANDLE_MOVE(MOCKED_STRUCT)`, `UNIQUE_THANDLE_INITIALIZE_MOVE(MOCKED_STRUCT)` and `UNIQUE_THANDLE_DISPOSE(MOCKED_STRUCT)` are recorded by the mocks and run the real functions of `REAL_MOCKED_STRUCT`. +Now, calls in the code under test to `UNIQUE_THANDLE_INITIALIZE_MOVE(MOCKED_STRUCT)` and `UNIQUE_THANDLE_DISPOSE(MOCKED_STRUCT)` are recorded by the mocks and run the real functions of `REAL_MOCKED_STRUCT`. Tests create instances with `UNIQUE_THANDLE_MALLOC(REAL_MOCKED_STRUCT)`. The mocked `UNIQUE_THANDLE` functions only take `UNIQUE_THANDLE(MOCKED_STRUCT)*` arguments, which `umock_c` treats as `void*`. If other mocked functions take or return `UNIQUE_THANDLE(MOCKED_STRUCT)` by value, for example a mocked create function, register it with `REGISTER_UMOCK_ALIAS_TYPE(UNIQUE_THANDLE(MOCKED_STRUCT), void*)`. @@ -101,4 +101,4 @@ Defines the `UNIQUE_THANDLE` functions for `REAL_TYPE`, using the "real" gballoc #define REGISTER_REAL_UNIQUE_THANDLE_MOCK_HOOK(TYPE) ``` -Hooks the mocked `UNIQUE_THANDLE_MOVE(TYPE)`, `UNIQUE_THANDLE_INITIALIZE_MOVE(TYPE)` and `UNIQUE_THANDLE_DISPOSE(TYPE)` to the real functions of `REAL_TYPE`. +Hooks the mocked `UNIQUE_THANDLE_INITIALIZE_MOVE(TYPE)` and `UNIQUE_THANDLE_DISPOSE(TYPE)` to the real functions of `REAL_TYPE`. diff --git a/common/devdoc/unique_thandle_requirements.md b/common/devdoc/unique_thandle_requirements.md index 6a4d08734..f74fab2e7 100644 --- a/common/devdoc/unique_thandle_requirements.md +++ b/common/devdoc/unique_thandle_requirements.md @@ -4,9 +4,9 @@ `unique_thandle` is a module that provides macros for handles that have a single owner. -`UNIQUE_THANDLE(T)` is a handle to an object of type `T` that has exactly one owner at a time. The .c file that defines `T` creates the object with `UNIQUE_THANDLE_MALLOC` or `UNIQUE_THANDLE_MALLOC_FLEX`. Each owner then either moves the handle to the next owner, with `UNIQUE_THANDLE_MOVE` or `UNIQUE_THANDLE_INITIALIZE_MOVE`, or disposes the object with `UNIQUE_THANDLE_DISPOSE`. No function copies a handle, so no code can give an object a second owner through an API, not even the .c file that defines `T`. +`UNIQUE_THANDLE(T)` is a handle to an object of type `T` that has exactly one owner at a time. The .c file that defines `T` creates the object with `UNIQUE_THANDLE_MALLOC` or `UNIQUE_THANDLE_MALLOC_FLEX`. Each owner then either moves the handle to the next owner with `UNIQUE_THANDLE_INITIALIZE_MOVE`, or disposes the object with `UNIQUE_THANDLE_DISPOSE`. A move never disposes an object, so every dispose is explicit. No function copies a handle, so no code can give an object a second owner through an API, not even the .c file that defines `T`. -Ownership moves only through `UNIQUE_THANDLE(T)*` parameters and through return values. A `UNIQUE_THANDLE(T)` passed by value is borrowed. C cannot prevent a raw copy of a handle (for example `UNIQUE_THANDLE(T) copy = handle;`), and this module does not detect one in general. `UNIQUE_THANDLE_MOVE` only refuses a move when both of its arguments hold the same handle. +Ownership moves only through `UNIQUE_THANDLE(T)*` parameters and through return values. A `UNIQUE_THANDLE(T)` passed by value is borrowed. C cannot prevent a raw copy of a handle (for example `UNIQUE_THANDLE(T) copy = handle;`), and this module does not detect one. A `UNIQUE_THANDLE(T)` variable must not be moved or disposed from multiple threads at the same time. @@ -24,7 +24,6 @@ A `UNIQUE_THANDLE(T)` variable must not be moved or disposed from multiple threa #define UNIQUE_THANDLE_TYPE_DEFINE_WITH_MALLOC_FUNCTIONS(T, malloc_function, malloc_flex_function, free_function) /*declared by UNIQUE_THANDLE_TYPE_DECLARE*/ -MOCKABLE_FUNCTION(, void, UNIQUE_THANDLE_MOVE(T), UNIQUE_THANDLE(T)*, t1, UNIQUE_THANDLE(T)*, t2); MOCKABLE_FUNCTION(, void, UNIQUE_THANDLE_INITIALIZE_MOVE(T), UNIQUE_THANDLE(T)*, t1, UNIQUE_THANDLE(T)*, t2); MOCKABLE_FUNCTION(, void, UNIQUE_THANDLE_DISPOSE(T), UNIQUE_THANDLE(T)*, t); @@ -61,31 +60,17 @@ static T* UNIQUE_THANDLE_GET_T(T)(UNIQUE_THANDLE(T) t); **SRS_UNIQUE_THANDLE_43_002: [** `UNIQUE_THANDLE_TYPE_DEFINE_WITH_MALLOC_FUNCTIONS` shall use `malloc_function`, `malloc_flex_function` and `free_function` to allocate and free memory. **]** -## UNIQUE_THANDLE_MOVE(T) - -```c -MOCKABLE_FUNCTION(, void, UNIQUE_THANDLE_MOVE(T), UNIQUE_THANDLE(T)*, t1, UNIQUE_THANDLE(T)*, t2); -``` - -`UNIQUE_THANDLE_MOVE` moves `*t2` into `*t1` and disposes the handle that `*t1` held. - -**SRS_UNIQUE_THANDLE_43_003: [** If `t1` is `NULL` then `UNIQUE_THANDLE_MOVE` shall return. **]** - -**SRS_UNIQUE_THANDLE_43_004: [** If `t2` is `NULL` then `UNIQUE_THANDLE_MOVE` shall return. **]** - -**SRS_UNIQUE_THANDLE_43_005: [** If `*t1` is not `NULL` and `*t1` is equal to `*t2` then `UNIQUE_THANDLE_MOVE` shall return without changing `*t1` and `*t2`. **]** - -**SRS_UNIQUE_THANDLE_43_006: [** `UNIQUE_THANDLE_MOVE` shall store `*t2` in `*t1` and set `*t2` to `NULL`. **]** - -**SRS_UNIQUE_THANDLE_43_007: [** If `*t1` was not `NULL` then, after updating `*t1` and `*t2`, `UNIQUE_THANDLE_MOVE` shall call the `dispose` function of the previous `*t1`, if it is not `NULL`, and free its memory. **]** - ## UNIQUE_THANDLE_INITIALIZE_MOVE(T) ```c MOCKABLE_FUNCTION(, void, UNIQUE_THANDLE_INITIALIZE_MOVE(T), UNIQUE_THANDLE(T)*, t1, UNIQUE_THANDLE(T)*, t2); ``` -`UNIQUE_THANDLE_INITIALIZE_MOVE` moves `*t2` into `*t1`, where `*t1` is not initialized. For example, `*t1` is a field of a structure that has just been allocated. +`UNIQUE_THANDLE_INITIALIZE_MOVE` moves `*t2` into `*t1`, where `*t1` does not hold an object: it is either `NULL` or not initialized, for example a field of a structure that has just been allocated. It does not look at `*t1` and it never disposes an object. + +To move into a handle that holds an object, first dispose that object with `UNIQUE_THANDLE_DISPOSE` or move it to another handle. Otherwise the dispose function of the object never runs, and the object leaks with everything it owns. If the object owns `*t2`, for example `t2` points to one of its fields, first move `*t2` into a local handle, then dispose the object, then move the local handle into `*t1`. + +To set a handle that is not initialized to `NULL`, move a `NULL` handle into it. **SRS_UNIQUE_THANDLE_43_008: [** If `t1` is `NULL` then `UNIQUE_THANDLE_INITIALIZE_MOVE` shall return. **]** diff --git a/common/inc/c_pal/unique_thandle_ll.h b/common/inc/c_pal/unique_thandle_ll.h index fb63d0799..4e43c8de8 100644 --- a/common/inc/c_pal/unique_thandle_ll.h +++ b/common/inc/c_pal/unique_thandle_ll.h @@ -19,10 +19,7 @@ #define UNIQUE_THANDLE_FREE(C) THANDLE_FREE(C) #define UNIQUE_THANDLE_GET_T(C) THANDLE_GET_T(C) -/*given a previous type T, UNIQUE_THANDLE_MOVE introduces a new name for a function that moves a handle into an initialized handle and disposes the handle it held*/ -#define UNIQUE_THANDLE_MOVE(C) MU_C2(C, _UNIQUE_MOVE) - -/*given a previous type T, UNIQUE_THANDLE_INITIALIZE_MOVE introduces a new name for a function that moves a handle into an uninitialized handle*/ +/*given a previous type T, UNIQUE_THANDLE_INITIALIZE_MOVE introduces a new name for a function that moves a handle into a handle that is NULL or not initialized, without disposing anything*/ #define UNIQUE_THANDLE_INITIALIZE_MOVE(C) MU_C2(C, _UNIQUE_INITIALIZE_MOVE) /*given a previous type T, UNIQUE_THANDLE_DISPOSE introduces a new name for a function that disposes a handle and sets it to NULL*/ @@ -32,7 +29,6 @@ /*introduces UNIQUE_THANDLE(T) and declares the functions that move and dispose it, named after C. No function that copies the handle is declared*/ #define UNIQUE_THANDLE_LL_TYPE_DECLARE(C, T) \ THANDLE_MACRO(T); \ - MOCKABLE_FUNCTION(, void, UNIQUE_THANDLE_MOVE(C), UNIQUE_THANDLE(T)*, t1, UNIQUE_THANDLE(T)*, t2); \ MOCKABLE_FUNCTION(, void, UNIQUE_THANDLE_INITIALIZE_MOVE(C), UNIQUE_THANDLE(T)*, t1, UNIQUE_THANDLE(T)*, t2); \ MOCKABLE_FUNCTION(, void, UNIQUE_THANDLE_DISPOSE(C), UNIQUE_THANDLE(T)*, t); \ diff --git a/common/tests/unique_thandle_ut/unique_thandle_ut.c b/common/tests/unique_thandle_ut/unique_thandle_ut.c index 63c7f6f43..e0a22e146 100644 --- a/common/tests/unique_thandle_ut/unique_thandle_ut.c +++ b/common/tests/unique_thandle_ut/unique_thandle_ut.c @@ -35,19 +35,17 @@ TEST_FUNCTION_CLEANUP(method_cleanup) { } -TEST_FUNCTION(UNIQUE_THANDLE_TYPE_DECLARE_declares_mockable_move_initialize_move_and_dispose) // no-srs +TEST_FUNCTION(UNIQUE_THANDLE_TYPE_DECLARE_declares_mockable_initialize_move_and_dispose) // no-srs { // arrange UNIQUE_THANDLE(MOCKED_STRUCT) t1 = NULL; UNIQUE_THANDLE(MOCKED_STRUCT) t2 = NULL; UNIQUE_THANDLE(LL_STRUCT) ll = NULL; - STRICT_EXPECTED_CALL(UNIQUE_THANDLE_MOVE(MOCKED_STRUCT)(&t1, &t2)); STRICT_EXPECTED_CALL(UNIQUE_THANDLE_INITIALIZE_MOVE(MOCKED_STRUCT)(&t1, &t2)); STRICT_EXPECTED_CALL(UNIQUE_THANDLE_DISPOSE(MOCKED_STRUCT)(&t1)); // act - UNIQUE_THANDLE_MOVE(MOCKED_STRUCT)(&t1, &t2); UNIQUE_THANDLE_INITIALIZE_MOVE(MOCKED_STRUCT)(&t1, &t2); UNIQUE_THANDLE_DISPOSE(MOCKED_STRUCT)(&t1); From 229035134de5a2d85f010332cba1f9f281ad9f8b Mon Sep 17 00:00:00 2001 From: Parth Aggarwal Date: Fri, 30 Oct 2026 11:06:02 -0700 Subject: [PATCH 05/11] Keep only the UNIQUE_THANDLE headers and spec, describe positive behavior SRS_UNIQUE_THANDLE_43_010 and 011, the rest of the spec and the header comments now state only what the module does. This PR keeps only the unique_thandle headers and spec: real_unique_thandle_helper_requirements.md and the unique_thandle_ut tests move to the implementation PR. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- ...real_unique_thandle_helper_requirements.md | 104 ------------------ common/devdoc/unique_thandle_requirements.md | 18 +-- common/inc/c_pal/unique_thandle_ll.h | 4 +- common/tests/CMakeLists.txt | 1 - common/tests/unique_thandle_ut/CMakeLists.txt | 17 --- .../unique_thandle_ut/unique_thandle_ut.c | 59 ---------- .../unique_thandle_ut/unique_thandle_ut_pch.h | 29 ----- 7 files changed, 11 insertions(+), 221 deletions(-) delete mode 100644 common/devdoc/real_unique_thandle_helper_requirements.md delete mode 100644 common/tests/unique_thandle_ut/CMakeLists.txt delete mode 100644 common/tests/unique_thandle_ut/unique_thandle_ut.c delete mode 100644 common/tests/unique_thandle_ut/unique_thandle_ut_pch.h diff --git a/common/devdoc/real_unique_thandle_helper_requirements.md b/common/devdoc/real_unique_thandle_helper_requirements.md deleted file mode 100644 index cc493e019..000000000 --- a/common/devdoc/real_unique_thandle_helper_requirements.md +++ /dev/null @@ -1,104 +0,0 @@ -# real_unique_thandle_helper.h Requirements - -## Overview - -`real_unique_thandle_helper.h` is a helper for mocking opaque `UNIQUE_THANDLE` types in unit tests. - -It defines the `UNIQUE_THANDLE` functions for a stand-in type and hooks them behind the mocked `UNIQUE_THANDLE_INITIALIZE_MOVE` and `UNIQUE_THANDLE_DISPOSE` of the original type. This provides a real implementation of moving and memory management for the original type, but not the behavior of the type itself. - -## How to use: - -See the example in `common\tests\real_unique_thandle_helper_ut\real_unique_thandle_helper_ut.c`. - -1. In the unit test, enable mocks and include `gballoc_hl.h`, `gballoc_hl_redirect.h`, `interlocked.h`, `interlocked_hl.h` and `unique_thandle.h`, along with the header of the type you want to mock. - -```c -#include "umock_c/umock_c_ENABLE_MOCKS.h" // ============================== ENABLE_MOCKS - -#include "c_pal/gballoc_hl.h" -#include "c_pal/gballoc_hl_redirect.h" -#include "c_pal/interlocked.h" -#include "c_pal/interlocked_hl.h" -#include "c_pal/unique_thandle.h" - -#include "mocked_struct.h" // A file that declares an incomplete type MOCKED_STRUCT and UNIQUE_THANDLE_TYPE_DECLARE(MOCKED_STRUCT) - -#include "umock_c/umock_c_DISABLE_MOCKS.h" // ============================== DISABLE_MOCKS -``` - -2. After disabling mocks, include `real_gballoc_hl.h`, `real_interlocked.h`, `real_interlocked_hl.h` and `real_unique_thandle_helper.h`. - -```c -#include "real_gballoc_hl.h" -#include "real_interlocked.h" -#include "real_interlocked_hl.h" - -#include "real_unique_thandle_helper.h" -``` - -3. Include `real_interlocked_renames.h` to temporarily use the real interlocked functions instead of the mocked ones. The `UNIQUE_THANDLE` functions use interlocked functions, so this avoids setting interlocked expectations when tests create and dispose handles. - -```c -#include "real_interlocked_renames.h" // IWYU pragma: keep -``` - -4. Define a stand-in type to use in the tests, then call `REAL_UNIQUE_THANDLE_DECLARE` and `REAL_UNIQUE_THANDLE_DEFINE` to define the `UNIQUE_THANDLE` functions for it. - -```c -typedef struct MOCKED_STRUCT_TAG -{ - uint8_t dummy; -} MOCKED_STRUCT; - -REAL_UNIQUE_THANDLE_DECLARE(MOCKED_STRUCT); - -REAL_UNIQUE_THANDLE_DEFINE(MOCKED_STRUCT); -``` - -5. Include `real_interlocked_undo_rename.h` to go back to the mocked interlocked functions. - -```c -#include "real_interlocked_undo_rename.h" // IWYU pragma: keep -``` - -6. In the test suite initialization, call `REGISTER_REAL_UNIQUE_THANDLE_MOCK_HOOK` to hook the real implementation behind the mocked `UNIQUE_THANDLE` functions of the type. - -```c - REGISTER_REAL_UNIQUE_THANDLE_MOCK_HOOK(MOCKED_STRUCT); -``` - -Now, calls in the code under test to `UNIQUE_THANDLE_INITIALIZE_MOVE(MOCKED_STRUCT)` and `UNIQUE_THANDLE_DISPOSE(MOCKED_STRUCT)` are recorded by the mocks and run the real functions of `REAL_MOCKED_STRUCT`. - -Tests create instances with `UNIQUE_THANDLE_MALLOC(REAL_MOCKED_STRUCT)`. The mocked `UNIQUE_THANDLE` functions only take `UNIQUE_THANDLE(MOCKED_STRUCT)*` arguments, which `umock_c` treats as `void*`. If other mocked functions take or return `UNIQUE_THANDLE(MOCKED_STRUCT)` by value, for example a mocked create function, register it with `REGISTER_UMOCK_ALIAS_TYPE(UNIQUE_THANDLE(MOCKED_STRUCT), void*)`. - -## Exposed API - -```c -#define REAL_UNIQUE_THANDLE_DECLARE(TYPE) -#define REAL_UNIQUE_THANDLE_DEFINE(TYPE) -#define REGISTER_REAL_UNIQUE_THANDLE_MOCK_HOOK(TYPE) -``` - -### REAL_UNIQUE_THANDLE_DECLARE(TYPE) - -```c -#define REAL_UNIQUE_THANDLE_DECLARE(TYPE) -``` - -Declares a new `REAL_TYPE` type for the structure `TYPE` and the `UNIQUE_THANDLE` functions for it. - -### REAL_UNIQUE_THANDLE_DEFINE(TYPE) - -```c -#define REAL_UNIQUE_THANDLE_DEFINE(TYPE) -``` - -Defines the `UNIQUE_THANDLE` functions for `REAL_TYPE`, using the "real" gballoc functions. - -### REGISTER_REAL_UNIQUE_THANDLE_MOCK_HOOK(TYPE) - -```c -#define REGISTER_REAL_UNIQUE_THANDLE_MOCK_HOOK(TYPE) -``` - -Hooks the mocked `UNIQUE_THANDLE_INITIALIZE_MOVE(TYPE)` and `UNIQUE_THANDLE_DISPOSE(TYPE)` to the real functions of `REAL_TYPE`. diff --git a/common/devdoc/unique_thandle_requirements.md b/common/devdoc/unique_thandle_requirements.md index f74fab2e7..7f1da790e 100644 --- a/common/devdoc/unique_thandle_requirements.md +++ b/common/devdoc/unique_thandle_requirements.md @@ -4,11 +4,11 @@ `unique_thandle` is a module that provides macros for handles that have a single owner. -`UNIQUE_THANDLE(T)` is a handle to an object of type `T` that has exactly one owner at a time. The .c file that defines `T` creates the object with `UNIQUE_THANDLE_MALLOC` or `UNIQUE_THANDLE_MALLOC_FLEX`. Each owner then either moves the handle to the next owner with `UNIQUE_THANDLE_INITIALIZE_MOVE`, or disposes the object with `UNIQUE_THANDLE_DISPOSE`. A move never disposes an object, so every dispose is explicit. No function copies a handle, so no code can give an object a second owner through an API, not even the .c file that defines `T`. +`UNIQUE_THANDLE(T)` is a handle to an object of type `T` that has exactly one owner at a time. The .c file that defines `T` creates the object with `UNIQUE_THANDLE_MALLOC` or `UNIQUE_THANDLE_MALLOC_FLEX`. Each owner then either moves the handle to the next owner with `UNIQUE_THANDLE_INITIALIZE_MOVE`, or disposes the object with `UNIQUE_THANDLE_DISPOSE`. These two are the only functions that change a handle, even in the .c file that defines `T`, so an object is disposed only by `UNIQUE_THANDLE_DISPOSE` and keeps a single owner. -Ownership moves only through `UNIQUE_THANDLE(T)*` parameters and through return values. A `UNIQUE_THANDLE(T)` passed by value is borrowed. C cannot prevent a raw copy of a handle (for example `UNIQUE_THANDLE(T) copy = handle;`), and this module does not detect one. +Ownership moves only through `UNIQUE_THANDLE(T)*` parameters and through return values. A `UNIQUE_THANDLE(T)` passed by value is borrowed. C allows a raw copy of a handle, for example `UNIQUE_THANDLE(T) copy = handle;`. A raw copy gives the object a second owner, so it is a bug. -A `UNIQUE_THANDLE(T)` variable must not be moved or disposed from multiple threads at the same time. +Only one thread at a time may move or dispose a given `UNIQUE_THANDLE(T)` variable. ## Exposed API @@ -32,7 +32,7 @@ MOCKABLE_FUNCTION(, void, UNIQUE_THANDLE_DISPOSE(T), UNIQUE_THANDLE(T)*, t); static T* UNIQUE_THANDLE_MALLOC(T)(void(*dispose)(T*)); /*allocates a T with an additional nmemb * size bytes for its flexible array member*/ static T* UNIQUE_THANDLE_MALLOC_FLEX(T)(void(*dispose)(T*), size_t nmemb, size_t size); -/*frees a T allocated by UNIQUE_THANDLE_MALLOC or UNIQUE_THANDLE_MALLOC_FLEX without calling dispose, for example when creating the object fails*/ +/*frees the memory of a T allocated by UNIQUE_THANDLE_MALLOC or UNIQUE_THANDLE_MALLOC_FLEX, for example when creating the object fails*/ static void UNIQUE_THANDLE_FREE(T)(T* t); /*returns a pointer to the T of a handle, or NULL if t is NULL*/ static T* UNIQUE_THANDLE_GET_T(T)(UNIQUE_THANDLE(T) t); @@ -46,7 +46,7 @@ static T* UNIQUE_THANDLE_GET_T(T)(UNIQUE_THANDLE(T) t); #define UNIQUE_THANDLE_TYPE_DEFINE(T) ``` -`UNIQUE_THANDLE_TYPE_DEFINE` defines the functions declared by `UNIQUE_THANDLE_TYPE_DECLARE` and the memory functions `UNIQUE_THANDLE_MALLOC(T)`, `UNIQUE_THANDLE_MALLOC_FLEX(T)`, `UNIQUE_THANDLE_FREE(T)` and `UNIQUE_THANDLE_GET_T(T)`. It defines no function that copies a handle. +`UNIQUE_THANDLE_TYPE_DEFINE` defines the functions declared by `UNIQUE_THANDLE_TYPE_DECLARE` and the memory functions `UNIQUE_THANDLE_MALLOC(T)`, `UNIQUE_THANDLE_MALLOC_FLEX(T)`, `UNIQUE_THANDLE_FREE(T)` and `UNIQUE_THANDLE_GET_T(T)`. **SRS_UNIQUE_THANDLE_43_001: [** `UNIQUE_THANDLE_TYPE_DEFINE` shall use `malloc`, `malloc_flex` and `free` to allocate and free memory. **]** @@ -66,9 +66,9 @@ static T* UNIQUE_THANDLE_GET_T(T)(UNIQUE_THANDLE(T) t); MOCKABLE_FUNCTION(, void, UNIQUE_THANDLE_INITIALIZE_MOVE(T), UNIQUE_THANDLE(T)*, t1, UNIQUE_THANDLE(T)*, t2); ``` -`UNIQUE_THANDLE_INITIALIZE_MOVE` moves `*t2` into `*t1`, where `*t1` does not hold an object: it is either `NULL` or not initialized, for example a field of a structure that has just been allocated. It does not look at `*t1` and it never disposes an object. +`UNIQUE_THANDLE_INITIALIZE_MOVE` moves `*t2` into `*t1`, where `*t1` is either `NULL` or not initialized, for example a field of a structure that has just been allocated. -To move into a handle that holds an object, first dispose that object with `UNIQUE_THANDLE_DISPOSE` or move it to another handle. Otherwise the dispose function of the object never runs, and the object leaks with everything it owns. If the object owns `*t2`, for example `t2` points to one of its fields, first move `*t2` into a local handle, then dispose the object, then move the local handle into `*t1`. +To move into a handle that holds an object, first dispose that object with `UNIQUE_THANDLE_DISPOSE` or move it to another handle. If the object owns `*t2`, for example `t2` points to one of its fields, first move `*t2` into a local handle, then dispose the object, then move the local handle into `*t1`. To set a handle that is not initialized to `NULL`, move a `NULL` handle into it. @@ -76,9 +76,9 @@ To set a handle that is not initialized to `NULL`, move a `NULL` handle into it. **SRS_UNIQUE_THANDLE_43_009: [** If `t2` is `NULL` then `UNIQUE_THANDLE_INITIALIZE_MOVE` shall return. **]** -**SRS_UNIQUE_THANDLE_43_010: [** If `t1` is equal to `t2` then `UNIQUE_THANDLE_INITIALIZE_MOVE` shall return without changing `*t1`. **]** +**SRS_UNIQUE_THANDLE_43_010: [** If `t1` is equal to `t2` then `UNIQUE_THANDLE_INITIALIZE_MOVE` shall return. **]** -**SRS_UNIQUE_THANDLE_43_011: [** `UNIQUE_THANDLE_INITIALIZE_MOVE` shall store `*t2` in `*t1` and set `*t2` to `NULL`, without disposing the previous content of `*t1`. **]** +**SRS_UNIQUE_THANDLE_43_011: [** `UNIQUE_THANDLE_INITIALIZE_MOVE` shall store `*t2` in `*t1` and set `*t2` to `NULL`. **]** ## UNIQUE_THANDLE_DISPOSE(T) diff --git a/common/inc/c_pal/unique_thandle_ll.h b/common/inc/c_pal/unique_thandle_ll.h index 4e43c8de8..1f9b65f43 100644 --- a/common/inc/c_pal/unique_thandle_ll.h +++ b/common/inc/c_pal/unique_thandle_ll.h @@ -19,14 +19,14 @@ #define UNIQUE_THANDLE_FREE(C) THANDLE_FREE(C) #define UNIQUE_THANDLE_GET_T(C) THANDLE_GET_T(C) -/*given a previous type T, UNIQUE_THANDLE_INITIALIZE_MOVE introduces a new name for a function that moves a handle into a handle that is NULL or not initialized, without disposing anything*/ +/*given a previous type T, UNIQUE_THANDLE_INITIALIZE_MOVE introduces a new name for a function that moves a handle into a handle that is NULL or not initialized*/ #define UNIQUE_THANDLE_INITIALIZE_MOVE(C) MU_C2(C, _UNIQUE_INITIALIZE_MOVE) /*given a previous type T, UNIQUE_THANDLE_DISPOSE introduces a new name for a function that disposes a handle and sets it to NULL*/ #define UNIQUE_THANDLE_DISPOSE(C) MU_C2(C, _UNIQUE_DISPOSE) /*macro to be used in headers*/ -/*introduces UNIQUE_THANDLE(T) and declares the functions that move and dispose it, named after C. No function that copies the handle is declared*/ +/*introduces UNIQUE_THANDLE(T) and declares only the functions that move and dispose it, named after C*/ #define UNIQUE_THANDLE_LL_TYPE_DECLARE(C, T) \ THANDLE_MACRO(T); \ MOCKABLE_FUNCTION(, void, UNIQUE_THANDLE_INITIALIZE_MOVE(C), UNIQUE_THANDLE(T)*, t1, UNIQUE_THANDLE(T)*, t2); \ diff --git a/common/tests/CMakeLists.txt b/common/tests/CMakeLists.txt index 38547049a..a99e47637 100644 --- a/common/tests/CMakeLists.txt +++ b/common/tests/CMakeLists.txt @@ -23,7 +23,6 @@ if(${run_unittests}) build_test_folder(real_thandle_log_context_handle_ut) build_test_folder(tqueue_ut) build_test_folder(timed_test_suite_ut) - build_test_folder(unique_thandle_ut) endif() if(${run_int_tests}) diff --git a/common/tests/unique_thandle_ut/CMakeLists.txt b/common/tests/unique_thandle_ut/CMakeLists.txt deleted file mode 100644 index d5a461a0f..000000000 --- a/common/tests/unique_thandle_ut/CMakeLists.txt +++ /dev/null @@ -1,17 +0,0 @@ -#Copyright (c) Microsoft. All rights reserved. -#Licensed under the MIT license. See LICENSE file in the project root for full license information. - -set(theseTestsName unique_thandle_ut) - -set(${theseTestsName}_test_files - ${theseTestsName}.c -) - -set(${theseTestsName}_h_files - ../../inc/c_pal/unique_thandle.h - ../../inc/c_pal/unique_thandle_ll.h -) - -build_test_artifacts(${theseTestsName} "tests/c_pal" ADDITIONAL_LIBS c_pal c_pal_reals - ENABLE_TEST_FILES_PRECOMPILED_HEADERS "${CMAKE_CURRENT_LIST_DIR}/unique_thandle_ut_pch.h" -) diff --git a/common/tests/unique_thandle_ut/unique_thandle_ut.c b/common/tests/unique_thandle_ut/unique_thandle_ut.c deleted file mode 100644 index e0a22e146..000000000 --- a/common/tests/unique_thandle_ut/unique_thandle_ut.c +++ /dev/null @@ -1,59 +0,0 @@ -// Copyright (c) Microsoft. All rights reserved. -// Licensed under the MIT license. See LICENSE file in the project root for full license information. - -#include "unique_thandle_ut_pch.h" - -/*a type whose UNIQUE_THANDLE functions are declared without mocks and named after a name other than the type*/ -typedef struct LL_STRUCT_TAG LL_STRUCT; -UNIQUE_THANDLE_LL_TYPE_DECLARE(LL_NAME, LL_STRUCT); - -MU_DEFINE_ENUM_STRINGS(UMOCK_C_ERROR_CODE, UMOCK_C_ERROR_CODE_VALUES) - -static void on_umock_c_error(UMOCK_C_ERROR_CODE error_code) -{ - ASSERT_FAIL("umock_c reported error :%" PRI_MU_ENUM "", MU_ENUM_VALUE(UMOCK_C_ERROR_CODE, error_code)); -} - -BEGIN_TEST_SUITE(TEST_SUITE_NAME_FROM_CMAKE) - -TEST_SUITE_INITIALIZE(suite_init) -{ - ASSERT_ARE_EQUAL(int, 0, umock_c_init(on_umock_c_error)); -} - -TEST_SUITE_CLEANUP(suite_cleanup) -{ - umock_c_deinit(); -} - -TEST_FUNCTION_INITIALIZE(method_init) -{ - umock_c_reset_all_calls(); -} - -TEST_FUNCTION_CLEANUP(method_cleanup) -{ -} - -TEST_FUNCTION(UNIQUE_THANDLE_TYPE_DECLARE_declares_mockable_initialize_move_and_dispose) // no-srs -{ - // arrange - UNIQUE_THANDLE(MOCKED_STRUCT) t1 = NULL; - UNIQUE_THANDLE(MOCKED_STRUCT) t2 = NULL; - UNIQUE_THANDLE(LL_STRUCT) ll = NULL; - - STRICT_EXPECTED_CALL(UNIQUE_THANDLE_INITIALIZE_MOVE(MOCKED_STRUCT)(&t1, &t2)); - STRICT_EXPECTED_CALL(UNIQUE_THANDLE_DISPOSE(MOCKED_STRUCT)(&t1)); - - // act - UNIQUE_THANDLE_INITIALIZE_MOVE(MOCKED_STRUCT)(&t1, &t2); - UNIQUE_THANDLE_DISPOSE(MOCKED_STRUCT)(&t1); - - // assert - ASSERT_ARE_EQUAL(char_ptr, umock_c_get_expected_calls(), umock_c_get_actual_calls()); - ASSERT_IS_NULL(t1); - ASSERT_IS_NULL(t2); - ASSERT_IS_NULL(ll); -} - -END_TEST_SUITE(TEST_SUITE_NAME_FROM_CMAKE) diff --git a/common/tests/unique_thandle_ut/unique_thandle_ut_pch.h b/common/tests/unique_thandle_ut/unique_thandle_ut_pch.h deleted file mode 100644 index 5bbaa63e7..000000000 --- a/common/tests/unique_thandle_ut/unique_thandle_ut_pch.h +++ /dev/null @@ -1,29 +0,0 @@ -// Copyright (c) Microsoft. All rights reserved. -// Licensed under the MIT license. See LICENSE file in the project root for full license information. - -// Precompiled header for unique_thandle_ut - -#ifndef UNIQUE_THANDLE_UT_PCH_H -#define UNIQUE_THANDLE_UT_PCH_H - -#include - -#include "macro_utils/macro_utils.h" // IWYU pragma: keep - -#include "testrunnerswitcher.h" - -#include "umock_c/umock_c.h" - -#include "c_pal/unique_thandle.h" -#include "c_pal/unique_thandle_ll.h" // IWYU pragma: keep - -/*a type whose UNIQUE_THANDLE functions are mocked*/ -typedef struct MOCKED_STRUCT_TAG MOCKED_STRUCT; - -#include "umock_c/umock_c_ENABLE_MOCKS.h" // ============================== ENABLE_MOCKS - -UNIQUE_THANDLE_TYPE_DECLARE(MOCKED_STRUCT); - -#include "umock_c/umock_c_DISABLE_MOCKS.h" // ============================== DISABLE_MOCKS - -#endif // UNIQUE_THANDLE_UT_PCH_H From edd8fb6727b0854778c86542318643b4029e2f1e Mon Sep 17 00:00:00 2001 From: Parth Aggarwal Date: Fri, 30 Oct 2026 11:27:54 -0700 Subject: [PATCH 06/11] Explain how functions pass a UNIQUE_THANDLE, with examples The overview now lists the three ways a function passes a handle: a UNIQUE_THANDLE(T)* parameter takes the object, a return value or out argument gives it, and a by-value parameter uses it during the call. An example shows each one. The advice paragraph about moving into a handle that holds an object is removed. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- common/devdoc/unique_thandle_requirements.md | 27 +++++++++++++++++--- 1 file changed, 24 insertions(+), 3 deletions(-) diff --git a/common/devdoc/unique_thandle_requirements.md b/common/devdoc/unique_thandle_requirements.md index 7f1da790e..0aa0e4349 100644 --- a/common/devdoc/unique_thandle_requirements.md +++ b/common/devdoc/unique_thandle_requirements.md @@ -6,7 +6,30 @@ `UNIQUE_THANDLE(T)` is a handle to an object of type `T` that has exactly one owner at a time. The .c file that defines `T` creates the object with `UNIQUE_THANDLE_MALLOC` or `UNIQUE_THANDLE_MALLOC_FLEX`. Each owner then either moves the handle to the next owner with `UNIQUE_THANDLE_INITIALIZE_MOVE`, or disposes the object with `UNIQUE_THANDLE_DISPOSE`. These two are the only functions that change a handle, even in the .c file that defines `T`, so an object is disposed only by `UNIQUE_THANDLE_DISPOSE` and keeps a single owner. -Ownership moves only through `UNIQUE_THANDLE(T)*` parameters and through return values. A `UNIQUE_THANDLE(T)` passed by value is borrowed. C allows a raw copy of a handle, for example `UNIQUE_THANDLE(T) copy = handle;`. A raw copy gives the object a second owner, so it is a bug. +Functions pass a handle in one of three ways: + +- A function that takes the object has a `UNIQUE_THANDLE(T)*` parameter. It moves the object out with `UNIQUE_THANDLE_INITIALIZE_MOVE`, which sets the caller's handle to `NULL`. +- A function that gives an object to its caller returns a `UNIQUE_THANDLE(T)`, or moves the object into a `UNIQUE_THANDLE(T)*` out argument with `UNIQUE_THANDLE_INITIALIZE_MOVE`. +- A function that uses the object only during the call has a `UNIQUE_THANDLE(T)` parameter. The caller passes the handle by value and still owns the object after the call. + +For example, for a type `EFFECT` and a batch that owns effects: + +```c +UNIQUE_THANDLE(EFFECT) effect_create(int value); /*gives a new effect to the caller*/ +int effect_get_value(UNIQUE_THANDLE(EFFECT) effect); /*uses the effect during the call*/ +int batch_add(BATCH_HANDLE batch, UNIQUE_THANDLE(EFFECT)* effect); /*takes the effect when it succeeds*/ + +/*a caller*/ +UNIQUE_THANDLE(EFFECT) effect = effect_create(42); +int value = effect_get_value(effect); /*the caller still owns the effect*/ +int result = batch_add(batch, &effect); /*on success, the batch owns the effect and effect is NULL*/ +UNIQUE_THANDLE_DISPOSE(EFFECT)(&effect); /*disposes the effect when batch_add failed*/ + +/*batch_add, when it succeeds*/ +UNIQUE_THANDLE_INITIALIZE_MOVE(EFFECT)(&batch->effects[batch->count], effect); +``` + +A by-value parameter is a copy of the handle that is valid only during the call. C allows other copies, for example `UNIQUE_THANDLE(T) copy = handle;`, but such a copy gives the object a second owner, so it is a bug. Only one thread at a time may move or dispose a given `UNIQUE_THANDLE(T)` variable. @@ -68,8 +91,6 @@ MOCKABLE_FUNCTION(, void, UNIQUE_THANDLE_INITIALIZE_MOVE(T), UNIQUE_THANDLE(T)*, `UNIQUE_THANDLE_INITIALIZE_MOVE` moves `*t2` into `*t1`, where `*t1` is either `NULL` or not initialized, for example a field of a structure that has just been allocated. -To move into a handle that holds an object, first dispose that object with `UNIQUE_THANDLE_DISPOSE` or move it to another handle. If the object owns `*t2`, for example `t2` points to one of its fields, first move `*t2` into a local handle, then dispose the object, then move the local handle into `*t1`. - To set a handle that is not initialized to `NULL`, move a `NULL` handle into it. **SRS_UNIQUE_THANDLE_43_008: [** If `t1` is `NULL` then `UNIQUE_THANDLE_INITIALIZE_MOVE` shall return. **]** From be36c3087a0839b1af703629619922f502de7e59 Mon Sep 17 00:00:00 2001 From: Parth Aggarwal Date: Fri, 30 Oct 2026 12:11:27 -0700 Subject: [PATCH 07/11] Use a neutral example, explain what const blocks for a handle The passing example uses an ITEM and a list instead of a type from SRT. The overview now says that the compiler rejects an assignment to a handle but accepts an initialization from another handle, which gives the object a second owner. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- common/devdoc/unique_thandle_requirements.md | 22 ++++++++++---------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/common/devdoc/unique_thandle_requirements.md b/common/devdoc/unique_thandle_requirements.md index 0aa0e4349..a5e45eb0d 100644 --- a/common/devdoc/unique_thandle_requirements.md +++ b/common/devdoc/unique_thandle_requirements.md @@ -12,24 +12,24 @@ Functions pass a handle in one of three ways: - A function that gives an object to its caller returns a `UNIQUE_THANDLE(T)`, or moves the object into a `UNIQUE_THANDLE(T)*` out argument with `UNIQUE_THANDLE_INITIALIZE_MOVE`. - A function that uses the object only during the call has a `UNIQUE_THANDLE(T)` parameter. The caller passes the handle by value and still owns the object after the call. -For example, for a type `EFFECT` and a batch that owns effects: +For example, for a type `ITEM` and a list that owns items: ```c -UNIQUE_THANDLE(EFFECT) effect_create(int value); /*gives a new effect to the caller*/ -int effect_get_value(UNIQUE_THANDLE(EFFECT) effect); /*uses the effect during the call*/ -int batch_add(BATCH_HANDLE batch, UNIQUE_THANDLE(EFFECT)* effect); /*takes the effect when it succeeds*/ +UNIQUE_THANDLE(ITEM) item_create(int value); /*gives a new item to the caller*/ +int item_get_value(UNIQUE_THANDLE(ITEM) item); /*uses the item during the call*/ +int list_add(LIST_HANDLE list, UNIQUE_THANDLE(ITEM)* item); /*takes the item when it succeeds*/ /*a caller*/ -UNIQUE_THANDLE(EFFECT) effect = effect_create(42); -int value = effect_get_value(effect); /*the caller still owns the effect*/ -int result = batch_add(batch, &effect); /*on success, the batch owns the effect and effect is NULL*/ -UNIQUE_THANDLE_DISPOSE(EFFECT)(&effect); /*disposes the effect when batch_add failed*/ +UNIQUE_THANDLE(ITEM) item = item_create(42); +int value = item_get_value(item); /*the caller still owns the item*/ +int result = list_add(list, &item); /*on success, the list owns the item and item is NULL*/ +UNIQUE_THANDLE_DISPOSE(ITEM)(&item); /*disposes the item when list_add failed*/ -/*batch_add, when it succeeds*/ -UNIQUE_THANDLE_INITIALIZE_MOVE(EFFECT)(&batch->effects[batch->count], effect); +/*list_add, when it succeeds*/ +UNIQUE_THANDLE_INITIALIZE_MOVE(ITEM)(&list->items[list->count], item); ``` -A by-value parameter is a copy of the handle that is valid only during the call. C allows other copies, for example `UNIQUE_THANDLE(T) copy = handle;`, but such a copy gives the object a second owner, so it is a bug. +A by-value parameter is a copy of the handle that is valid only during the call. A handle is a const pointer, so the compiler rejects an assignment to a handle after its declaration, such as `copy = handle;`. The compiler accepts an initialization from another handle, such as `UNIQUE_THANDLE(T) copy = handle;`. Such a copy gives the object a second owner, so it is a bug: code moves a handle with `UNIQUE_THANDLE_INITIALIZE_MOVE`. Only one thread at a time may move or dispose a given `UNIQUE_THANDLE(T)` variable. From 45713dcb0e16dab4803452d57c0ba8553bc7d442 Mon Sep 17 00:00:00 2001 From: Parth Aggarwal Date: Fri, 30 Oct 2026 12:17:25 -0700 Subject: [PATCH 08/11] Remove the paragraph about copying a handle Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- common/devdoc/unique_thandle_requirements.md | 2 -- 1 file changed, 2 deletions(-) diff --git a/common/devdoc/unique_thandle_requirements.md b/common/devdoc/unique_thandle_requirements.md index a5e45eb0d..62b3d20f1 100644 --- a/common/devdoc/unique_thandle_requirements.md +++ b/common/devdoc/unique_thandle_requirements.md @@ -29,8 +29,6 @@ UNIQUE_THANDLE_DISPOSE(ITEM)(&item); /*disposes the item when list_add failed*/ UNIQUE_THANDLE_INITIALIZE_MOVE(ITEM)(&list->items[list->count], item); ``` -A by-value parameter is a copy of the handle that is valid only during the call. A handle is a const pointer, so the compiler rejects an assignment to a handle after its declaration, such as `copy = handle;`. The compiler accepts an initialization from another handle, such as `UNIQUE_THANDLE(T) copy = handle;`. Such a copy gives the object a second owner, so it is a bug: code moves a handle with `UNIQUE_THANDLE_INITIALIZE_MOVE`. - Only one thread at a time may move or dispose a given `UNIQUE_THANDLE(T)` variable. ## Exposed API From a81095e6aa3f55da067de5f08c3569bd649498fb Mon Sep 17 00:00:00 2001 From: Parth Aggarwal Date: Wed, 4 Nov 2026 09:34:39 -0800 Subject: [PATCH 09/11] Make UNIQUE_THANDLE a one-element array and show its misuses UNIQUE_THANDLE(T) is now a one-element array of a slot that holds a const pointer to a struct that is only declared. The compiler rejects copying, assigning and returning a handle and, with warnings as errors, moving or disposing a handle through a const UNIQUE_THANDLE(T). UNIQUE_THANDLE_NULL initializes a handle to NULL. The spec shows how an item is created, moved into a list and disposed, lists the misuses that the compiler rejects, and shows what a function can do with a non-const handle parameter. The headers declare the API; the macros that define it come with the implementation. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- common/devdoc/unique_thandle_requirements.md | 189 ++++++++++++++++--- common/inc/c_pal/unique_thandle.h | 5 +- common/inc/c_pal/unique_thandle_ll.h | 49 +++-- 3 files changed, 201 insertions(+), 42 deletions(-) diff --git a/common/devdoc/unique_thandle_requirements.md b/common/devdoc/unique_thandle_requirements.md index 62b3d20f1..515d31872 100644 --- a/common/devdoc/unique_thandle_requirements.md +++ b/common/devdoc/unique_thandle_requirements.md @@ -4,29 +4,159 @@ `unique_thandle` is a module that provides macros for handles that have a single owner. -`UNIQUE_THANDLE(T)` is a handle to an object of type `T` that has exactly one owner at a time. The .c file that defines `T` creates the object with `UNIQUE_THANDLE_MALLOC` or `UNIQUE_THANDLE_MALLOC_FLEX`. Each owner then either moves the handle to the next owner with `UNIQUE_THANDLE_INITIALIZE_MOVE`, or disposes the object with `UNIQUE_THANDLE_DISPOSE`. These two are the only functions that change a handle, even in the .c file that defines `T`, so an object is disposed only by `UNIQUE_THANDLE_DISPOSE` and keeps a single owner. +`UNIQUE_THANDLE(T)` is a handle to an object of type `T` that has exactly one owner at a time. A handle either holds an object or is `NULL`. -Functions pass a handle in one of three ways: +The .c file that defines `T` creates the object with `UNIQUE_THANDLE_MALLOC` or `UNIQUE_THANDLE_MALLOC_FLEX` and puts it in a handle with `UNIQUE_THANDLE_ADOPT`: -- A function that takes the object has a `UNIQUE_THANDLE(T)*` parameter. It moves the object out with `UNIQUE_THANDLE_INITIALIZE_MOVE`, which sets the caller's handle to `NULL`. -- A function that gives an object to its caller returns a `UNIQUE_THANDLE(T)`, or moves the object into a `UNIQUE_THANDLE(T)*` out argument with `UNIQUE_THANDLE_INITIALIZE_MOVE`. -- A function that uses the object only during the call has a `UNIQUE_THANDLE(T)` parameter. The caller passes the handle by value and still owns the object after the call. +```c +int item_create(int value, UNIQUE_THANDLE(ITEM) item) +{ + int result; + ITEM* new_item = UNIQUE_THANDLE_MALLOC(ITEM)(NULL); + if (new_item == NULL) + { + result = MU_FAILURE; + } + else + { + new_item->value = value; + UNIQUE_THANDLE_ADOPT(ITEM)(item, new_item); + result = 0; + } + return result; +} +``` + +The object can be moved with `UNIQUE_THANDLE_INITIALIZE_MOVE` or disposed with `UNIQUE_THANDLE_DISPOSE`. + +```c +/*a LIST_HANDLE points to a LIST, whose end is its last NODE; a NODE has next, the next NODE, and item, a UNIQUE_THANDLE(ITEM)*/ +int list_insert_num(LIST_HANDLE list, int n) +{ + int result; + + UNIQUE_THANDLE(ITEM) item = UNIQUE_THANDLE_NULL; + if (item_create(n, item) != 0) + { + result = MU_FAILURE; + } + else + { + if (list_insert_item(list, item) != 0) + { + result = MU_FAILURE; + } + else + { + result = 0; + goto all_ok; + } + UNIQUE_THANDLE_DISPOSE(ITEM)(item); + } +all_ok: + return result; +} + +int list_insert_item(LIST_HANDLE list, UNIQUE_THANDLE(ITEM) item) +{ + int result; + if (list == NULL || item == NULL) + { + result = MU_FAILURE; + } + else + { + NODE* node = malloc(sizeof(NODE)); + if (node == NULL) + { + result = MU_FAILURE; + } + else + { + node->next = NULL; + UNIQUE_THANDLE_INITIALIZE_MOVE(ITEM)(node->item, item); + list->end->next = node; + list->end = node; + result = 0; + } + } + return result; +} +``` + +The compiler rejects the following misuses: + +```c +/*temp is a UNIQUE_THANDLE(ITEM), pointer is an ITEM*, other is a UNIQUE_THANDLE(OTHER), and list->items is an array of UNIQUE_THANDLE(ITEM)*/ + +/*copies a handle: copy and item would both own the item*/ +UNIQUE_THANDLE(ITEM) copy = item; + +/*assigns a handle: list->items[0] and list->items[1] would both own the same item*/ +list->items[0] = list->items[1]; + +/*returns a handle: the caller and the list would both own the item*/ +UNIQUE_THANDLE(ITEM) list_get(LIST_HANDLE list, uint32_t index); + +/*writes the pointer in a handle: temp and item would both own the item*/ +temp[0].object = item[0].object; + +/*list_get_first_value reads the list*/ +int list_get_first_value(const LIST* list) +{ + /*warnings as errors: moves the first item, which the list owns, into temp*/ + UNIQUE_THANDLE_INITIALIZE_MOVE(ITEM)(temp, list->items[0]); + ... +} + +/*warnings as errors: makes a handle from a pointer to an item, which a handle may already own*/ +UNIQUE_THANDLE_INITIALIZE_MOVE(ITEM)(item, &pointer); + +/*warnings as errors: moves a handle to another type, OTHER, into a handle to an ITEM*/ +UNIQUE_THANDLE_INITIALIZE_MOVE(ITEM)(item, other); +``` + +Read-only copies should be passed as `const`: + +```c +/*item_get_value uses the item during the call*/ +int item_get_value(const UNIQUE_THANDLE(ITEM) item) +{ + UNIQUE_THANDLE(ITEM) temp = UNIQUE_THANDLE_NULL; + + UNIQUE_THANDLE_INITIALIZE_MOVE(ITEM)(temp, item); /* does not compile */ + + UNIQUE_THANDLE_DISPOSE(ITEM)(item); /* does not compile */ + + ITEM* item_ptr = UNIQUE_THANDLE_GET_T(ITEM)(item); + return item_ptr->value; +} +``` -For example, for a type `ITEM` and a list that owns items: +If a read-only copy is passed as non-const and the copy is modified, the original is also changed: ```c -UNIQUE_THANDLE(ITEM) item_create(int value); /*gives a new item to the caller*/ -int item_get_value(UNIQUE_THANDLE(ITEM) item); /*uses the item during the call*/ -int list_add(LIST_HANDLE list, UNIQUE_THANDLE(ITEM)* item); /*takes the item when it succeeds*/ - -/*a caller*/ -UNIQUE_THANDLE(ITEM) item = item_create(42); -int value = item_get_value(item); /*the caller still owns the item*/ -int result = list_add(list, &item); /*on success, the list owns the item and item is NULL*/ -UNIQUE_THANDLE_DISPOSE(ITEM)(&item); /*disposes the item when list_add failed*/ - -/*list_add, when it succeeds*/ -UNIQUE_THANDLE_INITIALIZE_MOVE(ITEM)(&list->items[list->count], item); +int item_get_value(UNIQUE_THANDLE(ITEM) item) +{ + UNIQUE_THANDLE(ITEM) temp = UNIQUE_THANDLE_NULL; + + UNIQUE_THANDLE_INITIALIZE_MOVE(ITEM)(temp, item); + + ITEM* item_ptr = UNIQUE_THANDLE_GET_T(ITEM)(temp); + + return item_ptr->value; +} + +{ + UNIQUE_THANDLE(ITEM) item = UNIQUE_THANDLE_NULL; + item_create(42, item); + + int val = item_get_value(item); + + /*item is now NULL: item_get_value moved the item into temp, which leaked it. Another call to item_get_value dereferences NULL*/ + + int val2 = item_get_value(item); +} ``` Only one thread at a time may move or dispose a given `UNIQUE_THANDLE(T)` variable. @@ -34,9 +164,12 @@ Only one thread at a time may move or dispose a given `UNIQUE_THANDLE(T)` variab ## Exposed API ```c -/*the type of the handle that wraps T*/ +/*the type of a handle to a T*/ #define UNIQUE_THANDLE(T) +/*initializes a handle to NULL, as in UNIQUE_THANDLE(T) handle = UNIQUE_THANDLE_NULL;*/ +#define UNIQUE_THANDLE_NULL + /*to be used in a header file*/ #define UNIQUE_THANDLE_TYPE_DECLARE(T) @@ -45,8 +178,8 @@ Only one thread at a time may move or dispose a given `UNIQUE_THANDLE(T)` variab #define UNIQUE_THANDLE_TYPE_DEFINE_WITH_MALLOC_FUNCTIONS(T, malloc_function, malloc_flex_function, free_function) /*declared by UNIQUE_THANDLE_TYPE_DECLARE*/ -MOCKABLE_FUNCTION(, void, UNIQUE_THANDLE_INITIALIZE_MOVE(T), UNIQUE_THANDLE(T)*, t1, UNIQUE_THANDLE(T)*, t2); -MOCKABLE_FUNCTION(, void, UNIQUE_THANDLE_DISPOSE(T), UNIQUE_THANDLE(T)*, t); +MOCKABLE_FUNCTION(, void, UNIQUE_THANDLE_INITIALIZE_MOVE(T), UNIQUE_THANDLE(T), t1, UNIQUE_THANDLE(T), t2); +MOCKABLE_FUNCTION(, void, UNIQUE_THANDLE_DISPOSE(T), UNIQUE_THANDLE(T), t); /*defined by UNIQUE_THANDLE_TYPE_DEFINE for use in the .c file that defines T*/ /*allocates a T. dispose, if it is not NULL, is called when the object is disposed*/ @@ -55,11 +188,13 @@ static T* UNIQUE_THANDLE_MALLOC(T)(void(*dispose)(T*)); static T* UNIQUE_THANDLE_MALLOC_FLEX(T)(void(*dispose)(T*), size_t nmemb, size_t size); /*frees the memory of a T allocated by UNIQUE_THANDLE_MALLOC or UNIQUE_THANDLE_MALLOC_FLEX, for example when creating the object fails*/ static void UNIQUE_THANDLE_FREE(T)(T* t); -/*returns a pointer to the T of a handle, or NULL if t is NULL*/ -static T* UNIQUE_THANDLE_GET_T(T)(UNIQUE_THANDLE(T) t); +/*puts a new object, which UNIQUE_THANDLE_MALLOC or UNIQUE_THANDLE_MALLOC_FLEX returned, in *t, which is NULL or not initialized*/ +static void UNIQUE_THANDLE_ADOPT(T)(UNIQUE_THANDLE(T) t, T* object); +/*returns a pointer to the T that *t holds, or NULL if t is NULL*/ +static T* UNIQUE_THANDLE_GET_T(T)(const UNIQUE_THANDLE(T) t); ``` -`unique_thandle_ll.h` also provides `UNIQUE_THANDLE_LL_TYPE_DECLARE(C, T)`, `UNIQUE_THANDLE_LL_TYPE_DEFINE(C, T)` and `UNIQUE_THANDLE_LL_TYPE_DEFINE_WITH_MALLOC_FUNCTIONS(C, T, malloc_function, malloc_flex_function, free_function)`. They name the functions after `C` and wrap the type `T`. +`unique_thandle_ll.h` also provides `UNIQUE_THANDLE_MACRO(T)`, `UNIQUE_THANDLE_LL_TYPE_DECLARE(C, T)`, `UNIQUE_THANDLE_LL_TYPE_DEFINE(C, T)` and `UNIQUE_THANDLE_LL_TYPE_DEFINE_WITH_MALLOC_FUNCTIONS(C, T, malloc_function, malloc_flex_function, free_function)`. `UNIQUE_THANDLE_MACRO(T)` introduces `UNIQUE_THANDLE(T)`, once per translation unit. The other three name the functions after `C` and wrap the type `T`. `UNIQUE_THANDLE_TYPE_DECLARE(T)` is `UNIQUE_THANDLE_MACRO(T)` followed by `UNIQUE_THANDLE_LL_TYPE_DECLARE(T, T)`. ## UNIQUE_THANDLE_TYPE_DEFINE(T) @@ -67,7 +202,7 @@ static T* UNIQUE_THANDLE_GET_T(T)(UNIQUE_THANDLE(T) t); #define UNIQUE_THANDLE_TYPE_DEFINE(T) ``` -`UNIQUE_THANDLE_TYPE_DEFINE` defines the functions declared by `UNIQUE_THANDLE_TYPE_DECLARE` and the memory functions `UNIQUE_THANDLE_MALLOC(T)`, `UNIQUE_THANDLE_MALLOC_FLEX(T)`, `UNIQUE_THANDLE_FREE(T)` and `UNIQUE_THANDLE_GET_T(T)`. +`UNIQUE_THANDLE_TYPE_DEFINE` defines the functions declared by `UNIQUE_THANDLE_TYPE_DECLARE`, the functions `UNIQUE_THANDLE_MALLOC(T)`, `UNIQUE_THANDLE_MALLOC_FLEX(T)`, `UNIQUE_THANDLE_FREE(T)`, `UNIQUE_THANDLE_ADOPT(T)` and `UNIQUE_THANDLE_GET_T(T)`, and `UNIQUE_THANDLE_INSPECT(T)`, which shows the object of a handle in a debugger. **SRS_UNIQUE_THANDLE_43_001: [** `UNIQUE_THANDLE_TYPE_DEFINE` shall use `malloc`, `malloc_flex` and `free` to allocate and free memory. **]** @@ -84,7 +219,7 @@ static T* UNIQUE_THANDLE_GET_T(T)(UNIQUE_THANDLE(T) t); ## UNIQUE_THANDLE_INITIALIZE_MOVE(T) ```c -MOCKABLE_FUNCTION(, void, UNIQUE_THANDLE_INITIALIZE_MOVE(T), UNIQUE_THANDLE(T)*, t1, UNIQUE_THANDLE(T)*, t2); +MOCKABLE_FUNCTION(, void, UNIQUE_THANDLE_INITIALIZE_MOVE(T), UNIQUE_THANDLE(T), t1, UNIQUE_THANDLE(T), t2); ``` `UNIQUE_THANDLE_INITIALIZE_MOVE` moves `*t2` into `*t1`, where `*t1` is either `NULL` or not initialized, for example a field of a structure that has just been allocated. @@ -102,7 +237,7 @@ To set a handle that is not initialized to `NULL`, move a `NULL` handle into it. ## UNIQUE_THANDLE_DISPOSE(T) ```c -MOCKABLE_FUNCTION(, void, UNIQUE_THANDLE_DISPOSE(T), UNIQUE_THANDLE(T)*, t); +MOCKABLE_FUNCTION(, void, UNIQUE_THANDLE_DISPOSE(T), UNIQUE_THANDLE(T), t); ``` `UNIQUE_THANDLE_DISPOSE` disposes the handle in `*t` and sets `*t` to `NULL`. diff --git a/common/inc/c_pal/unique_thandle.h b/common/inc/c_pal/unique_thandle.h index 165c34c2a..23f5fcb43 100644 --- a/common/inc/c_pal/unique_thandle.h +++ b/common/inc/c_pal/unique_thandle.h @@ -9,8 +9,9 @@ #include "c_pal/unique_thandle_ll.h" /*macro to be used in headers*/ -/*introduces UNIQUE_THANDLE(T) for a previously defined type T and declares the functions that move and dispose it*/ -#define UNIQUE_THANDLE_TYPE_DECLARE(T) \ +/*introduces UNIQUE_THANDLE(T) for a type T and declares the functions that move and dispose it*/ +#define UNIQUE_THANDLE_TYPE_DECLARE(T) \ + UNIQUE_THANDLE_MACRO(T) \ UNIQUE_THANDLE_LL_TYPE_DECLARE(T, T) #endif /*UNIQUE_THANDLE_H*/ diff --git a/common/inc/c_pal/unique_thandle_ll.h b/common/inc/c_pal/unique_thandle_ll.h index 1f9b65f43..5df0684f4 100644 --- a/common/inc/c_pal/unique_thandle_ll.h +++ b/common/inc/c_pal/unique_thandle_ll.h @@ -4,32 +4,55 @@ #ifndef UNIQUE_THANDLE_LL_H #define UNIQUE_THANDLE_LL_H +#include + #include "macro_utils/macro_utils.h" #include "c_pal/thandle_ll.h" #include "umock_c/umock_c_prod.h" -/*a handle to a T that has a single owner. It is implemented as a THANDLE(T) whose reference count is always 1*/ -#define UNIQUE_THANDLE(T) THANDLE(T) +/*UNIQUE_THANDLE(T) is a handle to a T that has a single owner. +A handle is an array of one slot, like jmp_buf. The compiler rejects UNIQUE_THANDLE(T) copy = handle;, assignments of handles and functions that return a handle. +A UNIQUE_THANDLE(T) parameter points to the caller's handle, and a const UNIQUE_THANDLE(T) parameter only reads it. +The slot holds a const pointer to a struct that is only declared. UNIQUE_THANDLE_ADOPT, UNIQUE_THANDLE_INITIALIZE_MOVE and UNIQUE_THANDLE_DISPOSE write the slot, and UNIQUE_THANDLE_GET_T converts its pointer to a T*.*/ +#define UNIQUE_THANDLE(T) MU_C2(T, _UNIQUE_THANDLE) -/*given a previous type T, these are the names of the memory functions that UNIQUE_THANDLE_LL_TYPE_DEFINE defines for T. They are the THANDLE memory functions*/ -#define UNIQUE_THANDLE_MALLOC(C) THANDLE_MALLOC(C) -#define UNIQUE_THANDLE_MALLOC_FLEX(C) THANDLE_MALLOC_FLEX(C) -#define UNIQUE_THANDLE_FREE(C) THANDLE_FREE(C) -#define UNIQUE_THANDLE_GET_T(C) THANDLE_GET_T(C) +/*initializes a UNIQUE_THANDLE(T) to NULL, as in UNIQUE_THANDLE(T) handle = UNIQUE_THANDLE_NULL;*/ +#define UNIQUE_THANDLE_NULL { { NULL } } + +/*given a previous type T, these are the struct tags of the slot of UNIQUE_THANDLE(T) and of the object the slot points to. The object struct tag is only declared*/ +#define UNIQUE_THANDLE_SLOT_TAG(T) MU_C2(T, _UNIQUE_SLOT) +#define UNIQUE_THANDLE_OBJECT_TAG(T) MU_C2(T, _UNIQUE_OBJECT) + +/*introduces UNIQUE_THANDLE(T), once per type T. The names of the types are built from T, so UNIQUE_THANDLE_LL_TYPE_DECLARE(C, T) for any C uses the same UNIQUE_THANDLE(T)*/ +#define UNIQUE_THANDLE_MACRO(T) \ + typedef struct UNIQUE_THANDLE_SLOT_TAG(T) \ + { \ + const struct UNIQUE_THANDLE_OBJECT_TAG(T)* const volatile object; \ + } UNIQUE_THANDLE(T)[1]; /*given a previous type T, UNIQUE_THANDLE_INITIALIZE_MOVE introduces a new name for a function that moves a handle into a handle that is NULL or not initialized*/ #define UNIQUE_THANDLE_INITIALIZE_MOVE(C) MU_C2(C, _UNIQUE_INITIALIZE_MOVE) -/*given a previous type T, UNIQUE_THANDLE_DISPOSE introduces a new name for a function that disposes a handle and sets it to NULL*/ +/*given a previous type T, UNIQUE_THANDLE_DISPOSE introduces a new name for a function that sets a handle to NULL and disposes the object it held*/ #define UNIQUE_THANDLE_DISPOSE(C) MU_C2(C, _UNIQUE_DISPOSE) +/*given a previous type T, these are the names of the static functions that UNIQUE_THANDLE_LL_TYPE_DEFINE defines for the .c file that defines T*/ +#define UNIQUE_THANDLE_MALLOC(C) THANDLE_MALLOC(C) +#define UNIQUE_THANDLE_MALLOC_FLEX(C) THANDLE_MALLOC_FLEX(C) +#define UNIQUE_THANDLE_FREE(C) THANDLE_FREE(C) +#define UNIQUE_THANDLE_ADOPT(C) MU_C2(C, _UNIQUE_ADOPT) +#define UNIQUE_THANDLE_GET_T(C) MU_C2(C, _UNIQUE_GET_T) + +/*given a previous type T, UNIQUE_THANDLE_INSPECT introduces a new name for a function that returns the THANDLE_WRAPPER_TYPE_NAME(T) of the object a handle holds - useful in debugging, like THANDLE_INSPECT. The function has no side-effects*/ +#define UNIQUE_THANDLE_INSPECT(C) MU_C2(C, _UNIQUE_INSPECT) + /*macro to be used in headers*/ -/*introduces UNIQUE_THANDLE(T) and declares only the functions that move and dispose it, named after C*/ -#define UNIQUE_THANDLE_LL_TYPE_DECLARE(C, T) \ - THANDLE_MACRO(T); \ - MOCKABLE_FUNCTION(, void, UNIQUE_THANDLE_INITIALIZE_MOVE(C), UNIQUE_THANDLE(T)*, t1, UNIQUE_THANDLE(T)*, t2); \ - MOCKABLE_FUNCTION(, void, UNIQUE_THANDLE_DISPOSE(C), UNIQUE_THANDLE(T)*, t); \ +/*declares the functions that move and dispose a UNIQUE_THANDLE(T), named after C. UNIQUE_THANDLE_MACRO(T) introduces UNIQUE_THANDLE(T) before*/ +/*unlike THANDLE_LL_TYPE_DECLARE, it declares only functions: a translation unit defines the slot struct of T once, and REAL_UNIQUE_THANDLE_DECLARE declares the functions of REAL_T for the UNIQUE_THANDLE(T) that UNIQUE_THANDLE_TYPE_DECLARE(T) introduced*/ +#define UNIQUE_THANDLE_LL_TYPE_DECLARE(C, T) \ + MOCKABLE_FUNCTION(, void, UNIQUE_THANDLE_INITIALIZE_MOVE(C), UNIQUE_THANDLE(T), t1, UNIQUE_THANDLE(T), t2); \ + MOCKABLE_FUNCTION(, void, UNIQUE_THANDLE_DISPOSE(C), UNIQUE_THANDLE(T), t); #endif /*UNIQUE_THANDLE_LL_H*/ From eda0f1111b44f578cbec830a692cfda94666355f Mon Sep 17 00:00:00 2001 From: Parth Aggarwal Date: Wed, 4 Nov 2026 11:07:56 -0800 Subject: [PATCH 10/11] Write the spec examples' types and misuses in C The examples now define what they use in C instead of English: - item.h and item.c declare and define ITEM. - list.h defines NODE, a LIST with a head node that holds no item, and LIST_HANDLE, and declares the list functions. - other.h declares OTHER, and the misuses are statements in item.c and list.c functions. A LIST is a linked list, so the misuses of list->items are now temp = item; and node_get_value(const NODE*). The const item_get_value example marks its two rejected lines as warnings as errors. The non-const example's caller is a function, int caller(void), that returns val + val2. Every example was checked with MSVC 18 (/W4 /WX) and gcc 11.4 (-Wall -Werror): the code that should compile compiles, and each misuse fails at its own line with the expected diagnostic, 4 in any build and 5 only with warnings as errors. The non-const example was run on Windows and under valgrind: the first item_get_value leaks the item and leaves it NULL, and the second reads address 0. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- common/devdoc/unique_thandle_requirements.md | 92 +++++++++++++++----- 1 file changed, 71 insertions(+), 21 deletions(-) diff --git a/common/devdoc/unique_thandle_requirements.md b/common/devdoc/unique_thandle_requirements.md index 515d31872..6c49a77c1 100644 --- a/common/devdoc/unique_thandle_requirements.md +++ b/common/devdoc/unique_thandle_requirements.md @@ -9,6 +9,21 @@ The .c file that defines `T` creates the object with `UNIQUE_THANDLE_MALLOC` or `UNIQUE_THANDLE_MALLOC_FLEX` and puts it in a handle with `UNIQUE_THANDLE_ADOPT`: ```c +/*item.h*/ +typedef struct ITEM_TAG ITEM; + +UNIQUE_THANDLE_TYPE_DECLARE(ITEM); + +int item_create(int value, UNIQUE_THANDLE(ITEM) item); + +/*item.c*/ +struct ITEM_TAG +{ + int value; +}; + +UNIQUE_THANDLE_TYPE_DEFINE(ITEM); + int item_create(int value, UNIQUE_THANDLE(ITEM) item) { int result; @@ -30,7 +45,25 @@ int item_create(int value, UNIQUE_THANDLE(ITEM) item) The object can be moved with `UNIQUE_THANDLE_INITIALIZE_MOVE` or disposed with `UNIQUE_THANDLE_DISPOSE`. ```c -/*a LIST_HANDLE points to a LIST, whose end is its last NODE; a NODE has next, the next NODE, and item, a UNIQUE_THANDLE(ITEM)*/ +/*list.h*/ +typedef struct NODE_TAG +{ + struct NODE_TAG* next; + UNIQUE_THANDLE(ITEM) item; +} NODE; + +typedef struct LIST_TAG +{ + NODE head; /*holds no item*/ + NODE* end; /*the last node, &head when the list is empty*/ +} LIST; + +typedef LIST* LIST_HANDLE; + +int list_insert_num(LIST_HANDLE list, int n); +int list_insert_item(LIST_HANDLE list, UNIQUE_THANDLE(ITEM) item); + +/*list.c*/ int list_insert_num(LIST_HANDLE list, int n) { int result; @@ -87,33 +120,47 @@ int list_insert_item(LIST_HANDLE list, UNIQUE_THANDLE(ITEM) item) The compiler rejects the following misuses: ```c -/*temp is a UNIQUE_THANDLE(ITEM), pointer is an ITEM*, other is a UNIQUE_THANDLE(OTHER), and list->items is an array of UNIQUE_THANDLE(ITEM)*/ +/*other.h*/ +typedef struct OTHER_TAG OTHER; + +UNIQUE_THANDLE_TYPE_DECLARE(OTHER); -/*copies a handle: copy and item would both own the item*/ -UNIQUE_THANDLE(ITEM) copy = item; +/*item.c*/ +void misuses(UNIQUE_THANDLE(ITEM) item, UNIQUE_THANDLE(OTHER) other) +{ + UNIQUE_THANDLE(ITEM) temp = UNIQUE_THANDLE_NULL; + ITEM* pointer = UNIQUE_THANDLE_GET_T(ITEM)(item); + + /*copies a handle: copy and item would both own the item*/ + UNIQUE_THANDLE(ITEM) copy = item; + + /*assigns a handle: temp and item would both own the item*/ + temp = item; -/*assigns a handle: list->items[0] and list->items[1] would both own the same item*/ -list->items[0] = list->items[1]; + /*writes the pointer in a handle: temp and item would both own the item*/ + temp[0].object = item[0].object; + + /*warnings as errors: makes a handle from a pointer to an item, which a handle may already own*/ + UNIQUE_THANDLE_INITIALIZE_MOVE(ITEM)(temp, &pointer); + + /*warnings as errors: moves a handle to another type, OTHER, into a handle to an ITEM*/ + UNIQUE_THANDLE_INITIALIZE_MOVE(ITEM)(temp, other); +} +/*list.h*/ /*returns a handle: the caller and the list would both own the item*/ UNIQUE_THANDLE(ITEM) list_get(LIST_HANDLE list, uint32_t index); -/*writes the pointer in a handle: temp and item would both own the item*/ -temp[0].object = item[0].object; - -/*list_get_first_value reads the list*/ -int list_get_first_value(const LIST* list) +/*list.c*/ +/*node_get_value reads a node*/ +int node_get_value(const NODE* node) { - /*warnings as errors: moves the first item, which the list owns, into temp*/ - UNIQUE_THANDLE_INITIALIZE_MOVE(ITEM)(temp, list->items[0]); + UNIQUE_THANDLE(ITEM) temp = UNIQUE_THANDLE_NULL; + + /*warnings as errors: moves the item, which the node owns, into temp*/ + UNIQUE_THANDLE_INITIALIZE_MOVE(ITEM)(temp, node->item); ... } - -/*warnings as errors: makes a handle from a pointer to an item, which a handle may already own*/ -UNIQUE_THANDLE_INITIALIZE_MOVE(ITEM)(item, &pointer); - -/*warnings as errors: moves a handle to another type, OTHER, into a handle to an ITEM*/ -UNIQUE_THANDLE_INITIALIZE_MOVE(ITEM)(item, other); ``` Read-only copies should be passed as `const`: @@ -124,9 +171,9 @@ int item_get_value(const UNIQUE_THANDLE(ITEM) item) { UNIQUE_THANDLE(ITEM) temp = UNIQUE_THANDLE_NULL; - UNIQUE_THANDLE_INITIALIZE_MOVE(ITEM)(temp, item); /* does not compile */ + UNIQUE_THANDLE_INITIALIZE_MOVE(ITEM)(temp, item); /*warnings as errors: does not compile*/ - UNIQUE_THANDLE_DISPOSE(ITEM)(item); /* does not compile */ + UNIQUE_THANDLE_DISPOSE(ITEM)(item); /*warnings as errors: does not compile*/ ITEM* item_ptr = UNIQUE_THANDLE_GET_T(ITEM)(item); return item_ptr->value; @@ -147,6 +194,7 @@ int item_get_value(UNIQUE_THANDLE(ITEM) item) return item_ptr->value; } +int caller(void) { UNIQUE_THANDLE(ITEM) item = UNIQUE_THANDLE_NULL; item_create(42, item); @@ -156,6 +204,8 @@ int item_get_value(UNIQUE_THANDLE(ITEM) item) /*item is now NULL: item_get_value moved the item into temp, which leaked it. Another call to item_get_value dereferences NULL*/ int val2 = item_get_value(item); + + return val + val2; } ``` From 8422d360de406e6dfa4d00180871f2c4b134ec96 Mon Sep 17 00:00:00 2001 From: Parth Aggarwal Date: Wed, 4 Nov 2026 11:21:55 -0800 Subject: [PATCH 11/11] Say that the second item_get_value call crashes Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- common/devdoc/unique_thandle_requirements.md | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/common/devdoc/unique_thandle_requirements.md b/common/devdoc/unique_thandle_requirements.md index 6c49a77c1..94f6deff5 100644 --- a/common/devdoc/unique_thandle_requirements.md +++ b/common/devdoc/unique_thandle_requirements.md @@ -201,9 +201,8 @@ int caller(void) int val = item_get_value(item); - /*item is now NULL: item_get_value moved the item into temp, which leaked it. Another call to item_get_value dereferences NULL*/ - - int val2 = item_get_value(item); + /*item is now NULL: item_get_value moved the item into temp, which leaked it. Another call to item_get_value dereferences NULL and crashes*/ + int val2 = item_get_value(item); // will crash return val + val2; }