On 09/11/2025 21:46, Seifeddine Gmati wrote:
This is the exact reason why, in the `use()` thread, I suggested a
`Disposable` interface should **not** have an `enter()` method. The
language should just guarantee a single `dispose()` method is called
when the scope is exited (successfully or not).


Remember, the Context Manager is not the same as the resource being managed, and exitContext() is not equivalent to dispose().

In fact, it occurs to me that a C#-style using() block can be written as a Context Manager with only a few lines of code:

interface Disposable {
    public function dispose(): void;
}
class Disposer implements ContextManager
{
    private function __construct(private Disposable $resource) {}
    public static function of(Disposable $resource): ContextManager {
        return new self($resource);
    }
    public function enterContext(): Disposable {
        return $this->resource;
    }
    public function exitContext(): void {
        $this->resource->dispose();
    }
}


Then to use it, you just pass in whatever object you want:

with(Disposer::of(new MyDisposableResource) as $foo) {
    // ...
    $foo->whatever();
    // ...
} // guaranteed call to $foo->dispose() here


Which is pretty close to a direct port of C#:

using($foo = new MyDisposableResource) {
    // ...
    $foo->whatever();
    // ...
} // guaranteed call to $foo->dispose() here


You're right, but the `use()` proposal has a clear path to solve this,
which is far superior IMO to the manual checks required by the
`ContextManager` design. As discussed in the `use()` thread, we could
later introduce:

1.  A **`Resource`** marker interface: This would tell the engine to
handle the object specially (e.g., use weak references in backtraces
to prevent accidental refcount inflation).

2.  A **`Disposable`** interface: This would have the `dispose()`
logic and could (and probably should) also be a `Resource`.


Neither of these is specific to the use() block. #1 is just a general language feature - as someone else pointed out, it could be similar to #[SensitiveParameter]. #2 is, as shown above, trivial to implement using a context manager, without even needing additional language support.


Furthermore, a `Disposable` interface opens up the possibility of
being supported *outside* the `use()` construct entirely. The engine
could guarantee `dispose()` is called whenever the object goes out of
*any* scope, just like a destructor but with crucial exception
awareness:


This is an interesting idea, but again doesn't seem to have any special relationship to the use() proposal. It's also not what C# or Hack mean by "Disposable", so that would probably be a poor choice of name. It sounds more like an extension of the current __destruct(), which could potentially be as simple as adding a parameter to that.


With these (future) additions, the `use()` construct could be enhanced
to **enforce a no-escape policy** specifically for `Resource`s. If
`use($r = get_resource())` finishes and `$r` still has references, the
engine could throw an error.

This combination would *programmatically prevent* the "use after free"
pitfall you described, rather than relying on manual checks inside
every single method.


Unless the error happens at compile time (probably impossible in PHP's current workflow), or crashes the application with an uncatchable error (yikes!), I don't think it's possible to make that guarantee. Consider this code:

try {
    use($r = get_resource()) {
        SomeClass::$staticVar = $r;
    }
}
catch ( Throwable $e ) {
    log_and_continue($e);
}
SomeClass::$staticVar->doSomething();


What is the value of SomeClass::$staticVar? I can only see three options:

1. It is still the open resource from get_resource(); the call succeeds, but the resource has leaked

2. It is a closed resource, and the call to doSomething() throws an "object already disposed" Error

3. It has been forcibly unset, and the call to doSomething() throws a "method call on null" Error

(I don't think #3 is actually possible in the current Engine, because we only track the reference *count*, not a reference *list*; but it's at least theoretically possible.)

And that's leaving aside the fact that a lot of resources can end up in unusable states anyway, such as a network connection being closed from the other end.

All of those powerful safety checks can be added in the future. I
don't see why we need to cram them into the initial `use()` RFC.


Requiring a "double opt-in" (the use() block and an interface) makes the feature less reliable - you can't see at a glance if the use() block is performing the extra cleanup, or just creating a variable scope.

I think it's better to have a specific block that can *only* be used with objects implementing the appropriate interface, like using+IDisposable (C# and Hack) or try+AutoCloseable (Java https://docs.oracle.com/javase/tutorial/essential/exceptions/tryResourceClose.html).

If we want a general-purpose "block scoping" feature alongside that, we can discuss exactly how that should look and operate, as I replied to Tim here: https://externals.io/message/129059#129188


--
Rowan Tommins
[IMSoP]

Reply via email to