Skip to content

zend_portability: Add ZEND_CONTAINER_OF()#21903

Open
TimWolla wants to merge 5 commits intophp:masterfrom
TimWolla:container-of
Open

zend_portability: Add ZEND_CONTAINER_OF()#21903
TimWolla wants to merge 5 commits intophp:masterfrom
TimWolla:container-of

Conversation

@TimWolla
Copy link
Copy Markdown
Member

Changes made with Coccinelle:

@@
type T_container;
identifier member;
expression e;
@@

- (T_container *)(((char *)(e)) - offsetof(T_container, member))
+ ZEND_CONTAINER_OF(e, T_container, member)

@@
type T_container;
identifier member;
expression e;
typedef uintptr_t;
@@

- (T_container *)(((uintptr_t)(e)) - offsetof(T_container, member))
+ ZEND_CONTAINER_OF(e, T_container, member)

@@
type T_container;
identifier member;
expression e;
@@

- (const T_container *)(((char *)(e)) - offsetof(T_container, member))
+ ZEND_CONTAINER_OF(e, T_container, member)

@@
type T_container;
identifier member;
expression e;
typedef uintptr_t;
@@

- (const T_container *)(((uintptr_t)(e)) - offsetof(T_container, member))
+ ZEND_CONTAINER_OF(e, T_container, member)

Comment thread Zend/zend_portability.h Outdated
@ndossche
Copy link
Copy Markdown
Member

ndossche commented May 2, 2026

I think this relies on UB (pointer arithmetic on NULL)

@TimWolla
Copy link
Copy Markdown
Member Author

TimWolla commented May 2, 2026

pointer arithmetic on NULL

That branch is never evaluated; it's also the “accepted” solution to define container_of() in a type-safe way without statement expressions. So of all possible UB, this seems to be reasonably safe.

@ndossche
Copy link
Copy Markdown
Member

ndossche commented May 2, 2026

pointer arithmetic on NULL

That branch is never evaluated

This doesn't matter for UB.

it's also the “accepted” solution to define container_of() in a type-safe way without statement expressions.

The Linux kernel is compiled with various flags that makes various UB defined, so I'm not so sure this is a valid comparison.

@iluuu1994
Copy link
Copy Markdown
Member

I believe it's not UB unless executed, but we can probably achieve something similar with static assets?

@Girgias
Copy link
Copy Markdown
Member

Girgias commented May 2, 2026

I believe it's not UB unless executed, but we can probably achieve something similar with static assets?

The issue is that compilers can assume UB never happens, and then start removing stuff or doing whatever they want if that code is present (see famous blog article about UB: https://devblogs.microsoft.com/oldnewthing/20140627-00/?p=633)

@iluuu1994
Copy link
Copy Markdown
Member

iluuu1994 commented May 2, 2026

So, I have a different understanding.

and then start removing stuff

Yes, but this branch is already provably dead. UB allows the compiler to make some assumptions. For example:

  • If some branch contains UB, it's not reachable and can thus be removed. You may wonder why you're even allowed to write such code, but it's pretty common for dead branches to be generated from macros. And probably even more so with C++ templates.
  • If an operation on some variable would produce UB with value x, the variable does not hold that value. For example, this allows the compiler to optimize away repeated NULL checks, even on systems that technically have a zero page, or if you were to catch the segfault signal.

In other words, my understanding is: UB in dead branches has an effect on the result of the program, i.e. the compiler can assume the program is correct, and if the branch were reachable the program would not be correct, so the branch is unreachable. But that doesn't mean UB in dead branches makes the behavior of your program undefined if the branches are actually never reached.

That's my understanding, though to be honest, the C spec is not nearly explicit enough about this...

@ndossche
Copy link
Copy Markdown
Member

ndossche commented May 3, 2026

Unsure, but now I wonder if we even need this.
Doesn't this work: (zend_object *)(obj + 1) - 1, at least for the objects. If std is at the end this should work for all types.

@TimWolla
Copy link
Copy Markdown
Member Author

TimWolla commented May 3, 2026

Doesn't this work:

It likely would, but my understanding is that it would be equally UB. You can't just cast between pointers of different types. (and this already affected the original implementation, you may go to char *, but may not go from char * to something else).

My suggestion would be to just go with the established way of writing a container_of macro that reasonable compilers will compile to the expected machine code. php-src is already quite reliant on all kinds of technically-UB code, such as the generic zend_function struct.

@ndossche
Copy link
Copy Markdown
Member

ndossche commented May 3, 2026

Okay then

Comment thread Zend/zend_portability.h Outdated
@TimWolla
Copy link
Copy Markdown
Member Author

TimWolla commented May 8, 2026

Ah. C++. I'll see if I can add a template-variant.

Comment thread Zend/zend_portability.h Outdated
TimWolla added 5 commits May 9, 2026 15:35
Changes made with Coccinelle:

    @@
    type T_container;
    identifier member;
    expression e;
    @@

    - (T_container *)(((char *)(e)) - offsetof(T_container, member))
    + ZEND_CONTAINER_OF(e, T_container, member)

    @@
    type T_container;
    identifier member;
    expression e;
    typedef uintptr_t;
    @@

    - (T_container *)(((uintptr_t)(e)) - offsetof(T_container, member))
    + ZEND_CONTAINER_OF(e, T_container, member)

    @@
    type T_container;
    identifier member;
    expression e;
    @@

    - (const T_container *)(((char *)(e)) - offsetof(T_container, member))
    + ZEND_CONTAINER_OF(e, T_container, member)

    @@
    type T_container;
    identifier member;
    expression e;
    typedef uintptr_t;
    @@

    - (const T_container *)(((uintptr_t)(e)) - offsetof(T_container, member))
    + ZEND_CONTAINER_OF(e, T_container, member)
A follow-up change to the new `ZEND_CONTAINER_OF()` macro will preserve the
`const`-ness of the input pointer. By wrapping this macro inside a macro rather
an an inline function we can preserve the `const`-ness across all layers.
@TimWolla TimWolla requested a review from devnexen May 9, 2026 13:36
Comment thread Zend/zend_portability.h
return reinterpret_cast<T*>(reinterpret_cast<char*>(ptr) - offset);
}

# define ZEND_CONTAINER_OF(ptr, Type, member) zend_container_of<Type, decltype(Type::member)>(ptr, offsetof(Type, member))
Copy link
Copy Markdown
Member

@devnexen devnexen May 9, 2026

Choose a reason for hiding this comment

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

I was about to say something about the decltype but in another hand it can catch ptr/member mismatches so ...

Copy link
Copy Markdown
Member

@devnexen devnexen May 9, 2026

Choose a reason for hiding this comment

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

speaking of this, once C++ 20 is recognised widely as minimum, C++20 concepts can help reinforce types contracts here ... we ll see :)

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

I was about to say something about the decltype but in another hand it can catch ptr/member mismatches so ...

Yes, that was the goal. I tested the macro in a standalone C++ file to verify that it handles both const and other types correctly - just like the C23 version does.

If there already is a way to improve the definition right now (e.g. by avoiding two overloads to handle const/non-const), I'm happy to take suggestions. I don't regularly do C++, so while I'm somewhat familiar with what it can offer in theory, I am not comfortable applying that.

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.

it s way easier to do such things with C++20 but somewhat doable with older standards. let me think for a moment.

Copy link
Copy Markdown
Member

@devnexen devnexen May 9, 2026

Choose a reason for hiding this comment

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

it seems doable https://godbolt.org/z/no6MhGase std::conditional_t might have been nice to have but it s a c++14 thing...

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

I believe the minimum we support is C++17.

Unfortunately your suggestion is taking const-ness from M, not from ptr. Thus https://godbolt.org/z/hh37h6ene doesn't compile.

Given that my current solution works, it's probably not worth spending further time on this.

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.

ah my bad you re right :)

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

Projects

None yet

Development

Successfully merging this pull request may close these issues.

7 participants