Skip to content

Specify UNIQUE_THANDLE and declare its API - #665

Closed
Parth Aggarwal (parth21999) wants to merge 11 commits into
masterfrom
paaggarwal-microsoft-unique-thandle-module
Closed

Parth Aggarwal (parth21999) wants to merge 11 commits into
masterfrom
paaggarwal-microsoft-unique-thandle-module

Conversation

@parth21999

@parth21999 Parth Aggarwal (parth21999) commented Oct 28, 2026 •

Copy link
Copy Markdown
Member

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_effect objects have exactly one owner at a time as they move from the request to the batch to the executor. THANDLE lets 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, like jmp_buf, of a slot that holds a const pointer to the object. A function with a UNIQUE_THANDLE(T) parameter gets a pointer to the caller's handle, so when it takes the object, the caller's handle becomes NULL.
  • The compiler rejects copying, assigning and returning a handle, and writing the pointer in a handle. With warnings as errors, as in c-pal builds, it also rejects moving or disposing an object through a 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_DECLARE declares UNIQUE_THANDLE_INITIALIZE_MOVE and UNIQUE_THANDLE_DISPOSE, both as MOCKABLE_FUNCTIONs. No move disposes its destination, so every dispose is explicit. UNIQUE_THANDLE_NULL initializes a handle to NULL.
  • common/devdoc/unique_thandle_requirements.md adds SRS_UNIQUE_THANDLE_43_001, 002 and 008 to 016. 003 to 007 belonged to UNIQUE_THANDLE_MOVE, which was removed during review.
  • win32/CMakeLists.txt and linux/CMakeLists.txt add the two headers to pal_common_h_files.

What still compiles: explicit copies, such as UNIQUE_THANDLE(T) copy = { *handle };, copying a struct that holds a handle, memcpy and casts. So does UNIQUE_THANDLE_ADOPT with UNIQUE_THANDLE_GET_T inside the .c file that defines T.

This is the bottom PR of a stack. #666 adds the macros that define the API, real_unique_thandle_helper.h and 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 no Codes_/Tests_ tags yet, so traceability treats the new requirements as new.

Validation

  • The headers compile with and without mocks with MSVC 18 (/W4 /WX) and gcc 11.4 (-Wall -Werror).
  • pal_win32 builds. c_pal_traceability excludes the 11 new requirements and reports 0 failing, and c_pal_repo_validation passes.

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>
@parth21999

Copy link
Copy Markdown
Member Author

/AzurePipelines run

@azure-pipelines

Copy link
Copy Markdown
Azure Pipelines:
There may be pipelines that require an authorized user to comment /azp run to run.

@azure-pipelines

Copy link
Copy Markdown
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>
@parth21999

Copy link
Copy Markdown
Member Author

/AzurePipelines run

@azure-pipelines

Copy link
Copy Markdown
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>
@parth21999

Copy link
Copy Markdown
Member Author

/AzurePipelines run

@azure-pipelines

Copy link
Copy Markdown
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>
@parth21999

Copy link
Copy Markdown
Member Author

/AzurePipelines run

@azure-pipelines

Copy link
Copy Markdown
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>
@parth21999

Copy link
Copy Markdown
Member Author

/AzurePipelines run

@azure-pipelines

Copy link
Copy Markdown
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>
@parth21999

Copy link
Copy Markdown
Member Author

/AzurePipelines run

@azure-pipelines

Copy link
Copy Markdown
Azure Pipelines:
Successfully started running 1 pipeline(s).

@azure-pipelines

Copy link
Copy Markdown
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>
@parth21999

Copy link
Copy Markdown
Member Author

/AzurePipelines run

@azure-pipelines

Copy link
Copy Markdown
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.

@parth21999

Copy link
Copy Markdown
Member Author

/AzurePipelines run

@azure-pipelines

Copy link
Copy Markdown
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.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A handle is an array of one slot

I absolutely like this idea.

#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.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

slot

may I suggest not using "slot", which is 404 in C.

Here's suggestion: array of size 1. Or single element array. Or array with 1 element.

#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.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

like jmp_buf

implementation detail. We should not know or care how jm_buf is implemented.

#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.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

functions that return a handle

But why reject functions that return a UNIQUE_HANDLE?

What wrong with wanting UNIQUE_THANDLE T = lala_create();?


## Overview

`unique_thandle` is a module that provides macros for handles that have a single owner.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

owner

What are these "owners"? Are they... threads? Are they functions? Are they data structures? What are owners?


`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`.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

handle

probably UNIQUE_HANDLE you want to say here.


`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`:

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ADOPT

"adopt".


UNIQUE_THANDLE_TYPE_DECLARE(ITEM);

int item_create(int value, UNIQUE_THANDLE(ITEM) item);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ITEM

at this point, ITEM is an incomplete type.

Who creates "item" for me? Is item here some sort of hidden out parameter? Why is it then not with "*"?

@anporumb anporumb Nov 5, 2026 •

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

UNIQUE_THANDLE(ITEM) item = UNIQUE_THANDLE_NULL;

Line 16 of common/inc/c_pal/unique_thandle_ll.h says The compiler rejects UNIQUE_THANDLE(T) copy = handle;.

Which is it?

The object can be moved with `UNIQUE_THANDLE_INITIALIZE_MOVE` or disposed with `UNIQUE_THANDLE_DISPOSE`.

```c
/*list.h*/

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

list

list is way too complicated as an example here. Do we need it?

/*item.c*/
void misuses(UNIQUE_THANDLE(ITEM) item, UNIQUE_THANDLE(OTHER) other)
{
UNIQUE_THANDLE(ITEM) temp = UNIQUE_THANDLE_NULL;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

UNIQUE_THANDLE_NULL

must be some magic here

temp = item;

/*writes the pointer in a handle: temp and item would both own the item*/
temp[0].object = item[0].object;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this is a bonus

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);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

pointer

pointer is not type UNIQUE_THANDLE(ITEM) so no wonder. why mention it.

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);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

UNIQUE_THANDLE_INITIALIZE_MOVE

as with move semantics, if I see something in C that changes its argumenets, then I fully expect to pass those arguments by taking their addresses. In thi case:

UNIQUE_THANDLE_INITIALIZE_MOVE(ITEM)(&temp, &other);

UNIQUE_THANDLE_TYPE_DECLARE(OTHER);

/*item.c*/
void misuses(UNIQUE_THANDLE(ITEM) item, UNIQUE_THANDLE(OTHER) other)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

misuses

you needto add the exact compiler error you're seeing in every case. I am not compiler.

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

@anporumb anporumb Nov 5, 2026 •

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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; \

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

const struct UNIQUE_THANDLE_OBJECT_TAG(T)* const volatile object;

which one of these const/volatile can be eliminated/moved to the typedef?

} 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)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

_UNIQUE_INITIALIZE_MOVE

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.

@anporumb
anporumb dismissed their stale review November 5, 2026 01:45

revoking review

@anporumb anporumb left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🕐


## Overview

`unique_thandle` is a module that provides macros for handles that have a single owner.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

owner

So, are you building std::unique_ptr?
We can, but why do we need it?


`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`.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

UNIQUE_THANDLE

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."

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🕐

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants