In a real set of bindings we have a C finalizer that closes the handle automatically. When testing this we have code which looks something like the simplified example below:
let handle_closed = ref 0
let final _ = incr handle_closed
let () = assert (!handle_closed = 0)
let () =
let obj = ref [] in
Gc.finalise final obj
let () = Gc.full_major ()
(* this assert fails in OCaml 5, but not in OCaml <= 4 *)
let () = assert (!handle_closed = 1)
In OCaml 5, Gc.full_major does not collect the handle and run the finalizer (neither in the simplified example above, nor in the real program). However this test worked in OCaml <= 4.
I verified using OCAMLRUNPARAM that the GC is really being run 3 times. Is there some reason why the handle would not be collected? Is there a way to force it?
Note if it matters we have enabled flambda. I don’t know if that is related.
I would expect obj to be lifted to an immortal toplevel binding. It should be possible to prevent this by installing the finalizer in a noinline function and calling it. See section 8.2.
Here, even if obj does not appear in the .mli file, it is considered a toplevel value and never collected.
With flambda, the criterion for toplevel values is not syntactic anymore; anything not under a function can be turned into a toplevel value, including obj in your example.
So the solution usually looks like this:
let[@inline never][@local never] run () =
(* Put all your code here *)
let () = run ()
We’ve actually had similar issues in the compiler’s testsuite; we decided that changing the testsuite to wrap every test in a function was the easiest solution, but if you want something better it should be possible to add a compilation flag for tests so that the compiler introduces a wrapper for you automatically. It would mean waiting for at least 5.2 though.