Re: [PHP-DEV] Protected destructors

On 27.09.2024 at 14:32, Jonathan Vollebregt wrote:

Long story short I'd like to suggest:

1. Allow the engine to call protected destructors (again)
2. Warning when declaring a private destructor as with other magic methods
3. Documentation update to confirm private destructors aren't allowed

Does this sound good?

Hmm, I wonder about the use-cases of userland destructors. It seems to
me they are mostly useful for sanity checks, and maybe to close
resources. Are there others?

If not, I wouldn't worry much about the visibility of destructors,
because resources are scheduled for replacement anyway.

And since manually calling magic methods came up in the ticket: in my
opinion, whenever you have `->__` in production code, you're doing
something wrong.

Christoph

On 28.09.2024 at 16:21, Jonathan Vollebregt wrote:

Hmm, I wonder about the use-cases of userland destructors. It seems to
me they are mostly useful for sanity checks, and maybe to close
resources. Are there others?

If not, I wouldn't worry much about the visibility of destructors,
because resources are scheduled for replacement anyway.

Besides closing resources and killing processes I've seen them store
data to disk for caching, remove temp files, call callbacks/dispatch
events, change state on other objects, dump stored errors to error_log
in a loop in an error handler...

Okay. My point is that you cannot know (unless there are no circular
dependencies) *when* a destructor is called by the engine; it may be
called during some GC run, or during the request shutdown sequence. As
it's now, that happens pretty early during shutdown, but that *might*
change when stream resources are converted to objects. So you cannot be
absolutely sure that everything works as expected in destructors. This
is a general issue for garbage collected languages; some of these have
no destructors at all, for such reasons.

It looks like there's quite a lot of use-cases for them (Which can go
wrong if called twice) that don't necessarily require resources to be
involved

Like I said, I wouldn't be particularly worried about clients calling a
destructor manually (that's a bit different for the engine, since
segfaultish conditions should be avoided).

But I don't have a strong opinion about the visibility of destructors
anyway. I'm fine with allowing protected constructors.

Christoph

On Sat, Sep 28, 2024, at 16:46, Christoph M. Becker wrote:

On 28.09.2024 at 16:21, Jonathan Vollebregt wrote:

Hmm, I wonder about the use-cases of userland destructors. It seems to

me they are mostly useful for sanity checks, and maybe to close

resources. Are there others?

If not, I wouldn’t worry much about the visibility of destructors,

because resources are scheduled for replacement anyway.

Besides closing resources and killing processes I’ve seen them store

data to disk for caching, remove temp files, call callbacks/dispatch

events, change state on other objects, dump stored errors to error_log

in a loop in an error handler…

Okay. My point is that you cannot know (unless there are no circular

dependencies) when a destructor is called by the engine; it may be

called during some GC run, or during the request shutdown sequence. As

it’s now, that happens pretty early during shutdown, but that might

change when stream resources are converted to objects. So you cannot be

absolutely sure that everything works as expected in destructors. This

is a general issue for garbage collected languages; some of these have

no destructors at all, for such reasons.

It’s undocumented, AFAIK, but the destructor behavior is pretty dependable atm. For example, local variables are almost always destructed at the end of the current (function) scope. I am not sure what streams have to do with GC, but I can’t see how streams would change this behavior.

It looks like there’s quite a lot of use-cases for them (Which can go

wrong if called twice) that don’t necessarily require resources to be

involved

Like I said, I wouldn’t be particularly worried about clients calling a

destructor manually (that’s a bit different for the engine, since

segfaultish conditions should be avoided).

But I don’t have a strong opinion about the visibility of destructors

anyway. I’m fine with allowing protected constructors.

Christoph

Destructors should (IMHO) be public. Not necessarily because they can be called, but classes with destructors hint at underlying behavior when destructed. For performance, you might want to defer that by retaining a reference. If a class has a hidden destructor, you have to go read the code to find it.

That’s my 2¢

— Rob

On Sep 28, 2024, at 13:23, Jonathan Vollebregt <jnv@jnvsor.net> wrote:

Okay. My point is that you cannot know (unless there are no circular
dependencies) *when* a destructor is called by the engine;

The benefit of non-public visibility isn't when it's called, but how many times it's called. If you can declare your destructor non-public you can be confident it'll only be called once per instance (By the engine)

Or is there a scenario where the engine will call a destructor more than once on the same instance?

Destructors should (IMHO) be public. Not necessarily because they can be called, but classes with destructors hint at underlying behavior when destructed. For performance, you might want to defer that by retaining a reference. If a class has a hidden destructor, you have to go read the code to find it.

Wouldn't you have to read the code to see if it had a public destructor too?

I would argue that, semantically, destructors should be *private*. You should never need to know if a class has a destructor, and you should never call it manually. The engine should automatically handle calling parent destructors when necessary.

If there really is some logic in the destructor that a user of a class might legitimately want to use, then that should be exposed in a separate method (with a more appropriate name, and an implementation that handles that it might be called more than once) and have the destructor call that method.

-John

Hi Christoph,

On Sat, Sep 28, 2024 at 4:47 PM Christoph M. Becker <cmbecker69@gmx.de> wrote:

> Besides closing resources and killing processes I've seen them store
> data to disk for caching, remove temp files, call callbacks/dispatch
> events, change state on other objects, dump stored errors to error_log
> in a loop in an error handler...

Okay. My point is that you cannot know (unless there are no circular
dependencies) *when* a destructor is called by the engine; it may be
called during some GC run, or during the request shutdown sequence. As
it's now, that happens pretty early during shutdown, but that *might*
change when stream resources are converted to objects. So you cannot be
absolutely sure that everything works as expected in destructors. This
is a general issue for garbage collected languages; some of these have
no destructors at all, for such reasons.

I agree. To expand on this, I think that the use of destructors should
be discouraged for many reasons:

1. Objects with destructors slow down garbage collection due to
potential resurrections. It's not possible to detect resurrections, so
in the presence of destructors the GC has to forget most of its state,
run destructors, and restart.
2. They may be called in undefined order during GC and shutdown,
including after their dependencies have already been destroyed
3. They may cause concurrency issues (even in non-concurrent
programs) because they can be called at any time by the GC
4. They give a false sense of security as destructors will not be
called in case of fatal errors or crashes
5. Using them to manage non-memory resources (such as file
descriptors, temp files, ...) is not a good idea because the GC is
only triggered by memory metrics. The process may run out of file
descriptors before it triggers a garbage collection, for example.

Part of these problems would be solved by disabling the GC, but
ensuring that large code bases are free of cycles is not an easy task.

Java has deprecated destructors (finalizers) [2] and recommends using
Cleaners [1] instead. Cleaners would resolve the 1st issue, and
partially the 2nd one (for GC, not shutdown), but I'm not entirely
sure they are transposable. Ruby's ObjectSpace.define_finalizer is
similar to Cleaners, AFAIK, in that the finalizer must not reference
the object.

Some use-cases of destructors could be replaced with patterns like
Python's with() [3], Java's try-with [4], or Go's defer [5].

[1] Cleaner (Java SE 9 & JDK 9 )
[2] Object (Java SE 9 & JDK 9 )
[3] 8. Compound statements — Python 3.12.7 documentation
[4] The try-with-resources Statement
[5] Effective Go - The Go Programming Language

Best Regards,
Arnaud

On Oct 1, 2024, at 5:12 AM, Arnaud Le Blanc arnaud.lb@gmail.com wrote:

Some use-cases of destructors could be replaced with patterns like
Python’s with() [3], Java’s try-with [4], or Go’s defer [5].

defer would be neat in PHP. --Kent

On Tue, Oct 1, 2024, at 10:39 AM, K Sandvik wrote:

On Oct 1, 2024, at 5:12 AM, Arnaud Le Blanc <arnaud.lb@gmail.com> wrote:

Some use-cases of destructors could be replaced with patterns like
Python's with() [3], Java's try-with [4], or Go's defer [5].

defer would be neat in PHP. --Kent

I would have said with() would be neat in PHP. :slight_smile:

--Larry Garfield

On Tue, 1 Oct 2024, at 19:29, Larry Garfield wrote:

I would have said with() would be neat in PHP. :slight_smile:

I have been considering for a while proposing Context Managers [Python's with(), not to be confused with VisualBasic & JavaScript unrelated feature with the same keyword].

My primary example use case is safe database transactions, which I've seen implemented in PHP in two ways:

1) Callback style, where the code to run in a transaction has to be wrapped in a function, usually an anonymous closure. This is often cited as a use case for implicit capture in closures, but even with that it adds a layer of indirection, and changes the meaning of "return" and "yield" inside the wrapped block.

2) "Resource Acquisition Is Initialization" style, where the destructor rolls back the transaction if it hasn't been committed or rolled back manually. This requires fewer changes to the wrapped code, but as Arnaud points out, it's not 100% reliable / predictable in PHP, due to details of the GC.

Context Managers present a third option, where the code in the transaction remains a normal sequence of statements, but there is a more explicit guarantee about what will happen when the with{} block is exited. The Python design document has interesting background on what they included and excluded: PEP 343 – The “with” Statement | peps.python.org

C#'s "using statement" is similar, but explicitly designed for ensuring the correct "disposal" of an object rather than hooking entry to and exit from a "context": using statement - ensure the correct use of disposable objects - C# reference | Microsoft Learn

Regards,
--
Rowan Tommins
[IMSoP]

On Tue, Oct 1, 2024, at 2:06 PM, Rowan Tommins [IMSoP] wrote:

On Tue, 1 Oct 2024, at 19:29, Larry Garfield wrote:

I would have said with() would be neat in PHP. :slight_smile:

I have been considering for a while proposing Context Managers
[Python's with(), not to be confused with VisualBasic & JavaScript
unrelated feature with the same keyword].

My primary example use case is safe database transactions, which I've
seen implemented in PHP in two ways:

1) Callback style, where the code to run in a transaction has to be
wrapped in a function, usually an anonymous closure. This is often
cited as a use case for implicit capture in closures, but even with
that it adds a layer of indirection, and changes the meaning of
"return" and "yield" inside the wrapped block.

2) "Resource Acquisition Is Initialization" style, where the destructor
rolls back the transaction if it hasn't been committed or rolled back
manually. This requires fewer changes to the wrapped code, but as
Arnaud points out, it's not 100% reliable / predictable in PHP, due to
details of the GC.

Context Managers present a third option, where the code in the
transaction remains a normal sequence of statements, but there is a
more explicit guarantee about what will happen when the with{} block is
exited. The Python design document has interesting background on what
they included and excluded: PEP 343 – The “with” Statement | peps.python.org

C#'s "using statement" is similar, but explicitly designed for ensuring
the correct "disposal" of an object rather than hooking entry to and
exit from a "context":
using statement - ensure the correct use of disposable objects - C# reference | Microsoft Learn

Regards,
--
Rowan Tommins
[IMSoP]

I would support having Python-esque context managers in PHP, and would be happy to help make it happen (if research and English writing would be useful to whoever is doing the implementation).

--Larry Garfield