Using the Garbage Collector to free user resources

You need to watch out what type of res you attach a weak finalizer to! Caveats:

  • GHC optimization may unbox/rebox datatypes. So the same res value may be backed by a different closure at runtime, and the finalizer may fire prematurely.
  • GHC parallel garbage collector may create multiple copies of the same closure to avoid CAS overhead (see rts/sm/Evac.c · master · Glasgow Haskell Compiler / GHC · GitLab, some closures are copied using copy_tag_nolock instead of copy). So again, same Haskell value may be different closure some time later!
  • If you’re using nonmoving garbage collector, there were some recently merged fixes that enable weak finalizers to fire, but the fixes will only be in 9.6 IIRC

In general, you should not attach a weak finalizer to any lifted value given all the footguns. The standard libraries do use weak finalizers, but the finalizers are attached to the underlying unlifted closure like MutVar# and etc, those closures will stay the same throughout that Haskell value’s lifetime.

For your use case, I suggest adding a dummy IORef to your resource datatype, and use mkWeakIORef to add finalizer to it, instead of adding finalizer to the resource type itself. It’s fine to throw away the Weak, the finalizer will be effective even if Weak# is unreachable.

EDIT: I played a bit further with the idea of a lightweight Ticket type that one can add to their resource datatype for robust finalization logic, the implementation has less overhead than using IORef: StgTicket.cmm · GitHub

5 Likes