Repository navigation
Specify UNIQUE_THANDLE and declare its API - #665
Parth Aggarwal (parth21999) wants to merge 11 commits into
Conversation
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>
|
/AzurePipelines run |
|
Azure Pipelines: There may be pipelines that require an authorized user to comment /azp run to run. |
|
Azure Pipelines: Successfully started running 1 pipeline(s). |
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>
|
/AzurePipelines run |
|
Azure Pipelines: Successfully started running 1 pipeline(s). |
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>
|
/AzurePipelines run |
|
Azure Pipelines: Successfully started running 1 pipeline(s). |
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>
|
/AzurePipelines run |
|
Azure Pipelines: Successfully started running 1 pipeline(s). |
…vior 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>
|
/AzurePipelines run |
|
Azure Pipelines: Successfully started running 1 pipeline(s). |
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>
|
/AzurePipelines run |
|
Azure Pipelines: Successfully started running 1 pipeline(s). |
|
Azure Pipelines will not run the associated pipelines, because the pull request was updated after the run command was issued. Review the pull request again and issue a new run command. |
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
|
/AzurePipelines run |
|
Azure Pipelines will not run the associated pipelines, because the pull request was updated after the run command was issued. Review the pull request again and issue a new run command. |
|
/AzurePipelines run |
|
Azure Pipelines: Successfully started running 1 pipeline(s). |
| #include "umock_c/umock_c_prod.h" | ||
|
|
||
| /*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. |
| #include "umock_c/umock_c_prod.h" | ||
|
|
||
| /*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. |
| #include "umock_c/umock_c_prod.h" | ||
|
|
||
| /*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. |
| #include "umock_c/umock_c_prod.h" | ||
|
|
||
| /*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. |
|
|
||
| ## Overview | ||
|
|
||
| `unique_thandle` is a module that provides macros for handles that have a single owner. |
|
|
||
| `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. A handle either holds an object or is `NULL`. |
|
|
||
| `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`. | ||
|
|
||
| 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`: |
|
|
||
| UNIQUE_THANDLE_TYPE_DECLARE(ITEM); | ||
|
|
||
| int item_create(int value, UNIQUE_THANDLE(ITEM) item); |
There was a problem hiding this comment.
ok, it seems in this case that item is an out argument.
Can we please have in C out arguments with "*". Like:
int item_create(int value, UNIQUE_THANDLE(ITEM)* item);
Otherwise NOBODY will guess that this is an out argument.
| { | ||
| int result; | ||
|
|
||
| UNIQUE_THANDLE(ITEM) item = UNIQUE_THANDLE_NULL; |
| The object can be moved with `UNIQUE_THANDLE_INITIALIZE_MOVE` or disposed with `UNIQUE_THANDLE_DISPOSE`. | ||
|
|
||
| ```c | ||
| /*list.h*/ |
| /*item.c*/ | ||
| void misuses(UNIQUE_THANDLE(ITEM) item, UNIQUE_THANDLE(OTHER) other) | ||
| { | ||
| UNIQUE_THANDLE(ITEM) temp = UNIQUE_THANDLE_NULL; |
| temp = item; | ||
|
|
||
| /*writes the pointer in a handle: temp and item would both own the item*/ | ||
| temp[0].object = item[0].object; |
| 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); |
| 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); |
| UNIQUE_THANDLE_TYPE_DECLARE(OTHER); | ||
|
|
||
| /*item.c*/ | ||
| void misuses(UNIQUE_THANDLE(ITEM) item, UNIQUE_THANDLE(OTHER) other) |
| 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 and crashes*/ | ||
| int val2 = item_get_value(item); // will crash |
There was a problem hiding this comment.
no. just no.
there's no indication to the reader of this code that "item" is changed in between lines 202 and 205.
I hate to say it, but this is very unidiomatic C. Completely non-canon. Goes against the orthodoxy. /flails hands helplessly
What does unique_ptr in C++ in this case? When you pass a unique_ptr to a function as an argument, after the function returns, does it disappear from the caller, or does it stay with the caller? Don't you need like some explicit "move" to explicitly make the item nullptr even in C++? This hidden NULL-ification is unwanted
Read it as: this syntax is "special" so the user needs to pay a steep price both using it and reviewing it. Probably frustrating to write too.
| #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) |
There was a problem hiding this comment.
These are closing remarks.
Description.txt mentions that this unique_thandle is needed for some geo reasons.
Whatever is happening in geo... why is this is the only way to possibly solve it? I think we are not building the right solution here for that problem.
But by using UNIQUE_THANDLE the problem/solution will become even cloudier and more muddy and harder to investigate. Let's go to basics. I suspect a THANDLE "as is" might be equally good.
Here's example:
THANDLE(T) t =something_create();
some_api(t);
THANDLE_ASSIGN(T)(&t, NULL);
If this does not work - we have to understand why it doesn't work.
|
|
||
| ## Overview | ||
|
|
||
| `unique_thandle` is a module that provides macros for handles that have a single owner. |
|
|
||
| `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. A handle either holds an object or is `NULL`. |
There was a problem hiding this comment.
so this is the unique_shared_ptr, because THANDLE is shared_ptr.
I hate to curb enthusiasm, but this is likely not needed.
What is the use case for it?
"It makes transfer possible and explicit. It doesn't make frequent transfer good design. Typical healthy uses are short: a factory returns ownership, a container takes it with one move, and everything else borrows. An object passed through many owners usually means nobody really owns its lifetime.
That's what geo does: request → batch → executor, a transfer at every hop. Parth's own justification in !17302701 shows why: the move exists partly so "a request that kept its effects can't form a request → effect → request cycle (async effects hold their request)." So the transfers work around a reference cycle that THANDLE created. UNIQUE_THANDLE would add a second primitive to work around the first one.
A simpler design would give effects one stable owner (the request or the strand), have the batch and executor borrow them, and not let effects hold their request through a strong ref. Then there's nothing to transfer and no new primitive is needed."
There was a problem hiding this comment.
I'm going to agree here. unique_ptr in C++ is great because of RAII. I don't think you get the benefits (automatic resource cleanup) without that language feature and it feels half-baked for C.
Adds the requirements and the declarations for
UNIQUE_THANDLE, a handle to an object that has exactly one owner at a time.Why: in Azure-Messaging-GeoReplication,
srt_effectobjects have exactly one owner at a time as they move from the request to the batch to the executor.THANDLElets a handle be copied, so an effect can end up with two owners, for example when it is added to a batch twice. Tracked by ADO task 40374357.What:
UNIQUE_THANDLE(T)is a one-element array, likejmp_buf, of a slot that holds a const pointer to the object. A function with aUNIQUE_THANDLE(T)parameter gets a pointer to the caller's handle, so when it takes the object, the caller's handle becomesNULL.const UNIQUE_THANDLE(T)parameter, which is how a function declares that it only uses the object. The spec lists these misuses with an item and list example.UNIQUE_THANDLE_TYPE_DECLAREdeclaresUNIQUE_THANDLE_INITIALIZE_MOVEandUNIQUE_THANDLE_DISPOSE, both asMOCKABLE_FUNCTIONs. No move disposes its destination, so every dispose is explicit.UNIQUE_THANDLE_NULLinitializes a handle toNULL.common/devdoc/unique_thandle_requirements.mdaddsSRS_UNIQUE_THANDLE_43_001,002and008to016.003to007belonged toUNIQUE_THANDLE_MOVE, which was removed during review.win32/CMakeLists.txtandlinux/CMakeLists.txtadd the two headers topal_common_h_files.What still compiles: explicit copies, such as
UNIQUE_THANDLE(T) copy = { *handle };, copying a struct that holds a handle,memcpyand casts. So doesUNIQUE_THANDLE_ADOPTwithUNIQUE_THANDLE_GET_Tinside the .c file that definesT.This is the bottom PR of a stack. #666 adds the macros that define the API,
real_unique_thandle_helper.hand its doc, the unit tests, and configure-time checks that each misuse in the spec fails to compile with its expected diagnostic. This PR has noCodes_/Tests_tags yet, so traceability treats the new requirements as new.Validation
/W4 /WX) and gcc 11.4 (-Wall -Werror).pal_win32builds.c_pal_traceabilityexcludes the 11 new requirements and reports 0 failing, andc_pal_repo_validationpasses.