Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Figure out how to avoid reference cycles for listener patterns, especially InventoryListener #6565

Open
dktapps opened this issue Dec 8, 2024 · 0 comments
Labels
Category: Core Related to internal functionality Type: Cleanup Removes or deprecates API methods or behaviour

Comments

@dktapps
Copy link
Member

dktapps commented Dec 8, 2024

Problem description

An inventory listener is strongly referenced by an inventory.

Since inventory listeners are typically used to dispatch updates to an owner (e.g. a delegate), this means that the listener needs to keep a ref to its owner.

However, in order to remove the listener once it's no longer needed, the owner must also keep a ref to the listener so it can be disposed of.

Since a listener keeping a hard ref to its owner would prevent the owner's __destruct() from being called (thereby requiring explicit disposal of dead owner objects), listeners therefore often have to use this kind of nasty weakref shit to avoid a reference cycle:

if(($strongThis = $weakThis->get()) !== null){
$strongThis->backingInventoryChanging = true;
try{
$strongThis->onSlotChange($slot, $oldItem);
}finally{
$strongThis->backingInventoryChanging = false;
}
}
},

I've thought about this problem a few times, since it creates a lot of boilerplate code in places that would otherwise be simple.

This is not just a problem for InventoryListener. It appears in various other places too, like:

  • ChunkListener (has to be manually unregistered on Player teardown)
  • CraftingDataCache (currently weakrefs CraftingManager to avoid cycles)
  • event\Listener (has to be unregistered on Plugin teardown, but tracks owner info - this only works because there is a registry of active Plugins - there's no registry for ChunkListener, InventoryListener etc)
  • PermissibleInternal <-> PermissionAttachment - currently gets around this by having PermissibleInternal encapsulated inside PermissibleBase, which has no cycles - however this only works for as long as it's not necessary to dispatch PermissionAttachment updates further up the reference chain

Possible solutions

  • Listener weakref owner (lots of nasty boilerplate)
  • Some kind of utility wrapper to turn a Closure with strong $this into weak $this - an approach used by amphp but an awful hack that I don't like
  • Listenee weakref listener (requires owner to keep a hard ref to keep the listener alive - annoying for static analysis)
  • API break: change listener lists to use WeakMap<owner, listener>, and have owners passed as parameters instead of keeping them as $this - feels like the least hacky solution but also more syntactically obnoxious

Alternative solutions that don't require API changes

@dktapps dktapps added Category: Core Related to internal functionality Type: Cleanup Removes or deprecates API methods or behaviour labels Dec 8, 2024
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Category: Core Related to internal functionality Type: Cleanup Removes or deprecates API methods or behaviour
Projects
None yet
Development

No branches or pull requests

1 participant