Finalizers & Guardians


Overview

Eta provides two weak-lifecycle tools for heap objects:

Use them when an object has external lifecycle work, such as closing ports or releasing network handles, and you want cleanup to happen even if user code forgets an explicit close.

Finalizers and guardians are not deterministic resource-management primitives. They are a safety net. Explicit close/release calls should remain the primary path in normal program logic.


Primitive API

PrimitiveSignatureMeaning
register-finalizer!(obj proc) -> #tRegister or replace one finalizer for obj. Finalizer is invoked as (proc obj).
unregister-finalizer!(obj) -> booleanRemove the finalizer registration for obj.
make-guardian() -> guardianCreate a guardian object.
guardian-track!(guardian obj) -> #tTrack obj weakly under guardian.
guardian-collect(guardian) -> obj or #fPop one ready object from the guardian queue, or #f when empty.

Semantics

Resurrection

If a finalizer stores its argument into a live root (for example a global or another reachable object), the object is resurrected and remains live in later collections until those roots are removed.


When To Use

Prefer finalizers for safety cleanup on objects that wrap external resources:

Use guardians when user code wants explicit post-mortem handling of dead objects without executing cleanup code immediately.


Example Pattern (Ports)

(module m
  (import std.io)
  (begin
    (define p (open-output-string))

    ;; Safety net: explicit close is still recommended in normal flow.
    (register-finalizer! p
      (lambda (port)
        (close-port port)))

    ;; ... program logic ...
    (close-port p)))

The explicit close-port handles normal control flow; the finalizer covers forgotten or exceptional paths.


Runtime Notes

Implementation lives in side tables in:

Behavioral tests are in: