type MyObject = ref object of RootRef
i: int
proc finalizeObject*[T](o: T) =
echo "finalizerCalled"
proc newMyObject*(): MyObject =
new(result, finalizeObject)
proc main =
var x = newMyObject()
x.i = 7
echo x.i * 2
main()
GC_FullCollect()
$ nim c t.nim
$ ./t
14
$ nim c -d:release t.nim
finalizerCalled
$ nim -v
Nim Compiler Version 0.18.1 [Linux: amd64]
Compiled at 2018-08-20
Copyright (c) 2006-2018 by Andreas Rumpf
git hash: b75808c7d992ea47f8d6abc656b881b2aa0f86de
active boot switches: -d:release
Well it collects garbage, not simply everything that has been allocated. For that we have:
proc deallocHeap*(runFinalizers = true; allowGcAfterwards = true)
It means a bit pattern on the stack that looks like a pointer is treated like a pointer and so keeps objects alive for longer than they really are alive.
How is this possible and what does it mean? It's weird and wrong behavior. Such unpredictable things should not happen, it fences off Nim from serious developers.
Local variable "x" went out of scope and definitely is garbage. Before "GC_FullCollect()" pointer to "x" should be out of the stack.
For me debug and release shows same expected result:
$ nim -r c gc.nim
....Debug Build...
14
finalizerCalled
$ nim -v
Nim Compiler Version 0.18.0 [Linux: amd64]
Copyright (c) 2006-2018 by Andreas Rumpf
git hash: 855956bf617f68ac0be3717329e9e1181e5dc0c6
active boot switches: -d:release
Maybe, something wrong with 0.18.1?
How to be sure that finalizers will be called after calling "GC_FullCollect"?
How is this possible and what does it mean? It's weird and wrong behavior. Such unpredictable things should not happen, it fences off Nim from serious developers.
Local variable "x" went out of scope and definitely is garbage. Before "GC_FullCollect()" pointer to "x" should be out of the stack.
I don't feel like explaining to you here how a semi-precise GC works or how many other different GC implementations take the same approach. Some "serious" developers know that finalizers are not destructors.
How to be sure that finalizers will be called after calling "GC_FullCollect"?
By writing non-toy Nim programs and studying their memory behaviour at runtime.
FWIW, I'm getting the finalizerCalled message on Nim v0.18.1 with release and non-release options.
$ nim -v
Nim Compiler Version 0.18.1 [MacOSX: amd64]
Compiled at 2018-05-28
Copyright (c) 2006-2018 by Andreas Rumpf
I'll complement Araq's (perhaps too blunt) answer by saying that few programmers generally rely on GC for deterministic and time-sensitive execution of finalizers (and few mainstream industrial platforms can provide such guarantees, especially under heavy multi-threaded & concurrent execution).
Where such behavior is needed, programmers generally adopt a design for lifecycle management of important objects/resources, such as using delimited lifetime blocks, custom ref-counting, custom allocators, etc.
Exceptions do happen and custom solutions can't help. In Nim there are no reliable ways to call "destructors" for heap objects, so hope only for finalizers and GC. However, the GC_fullCollect is misleading and does not always collect all the garbage (and not calling finalizers), for example when the realtime GC is activated. And there is no way to really force GC collect all garbage, if you need (I see no reason for such limiting).
The question is why GC_fullCollect sometimes don't collect all garbage on default configuration, maybe I inattentively studied the code. It set cycleThreshold to 0 and must allow to collect immediately.
https://nim-lang.org/docs/gc.html This page requires a warning that GC_fullCollect is sometimes useless and can do nothing. It is also worth noting that not everything that is definitely garbage is garbage for Nim.
Every program, that create temporary files, must delete own garbage
import os, httpclient
type Download = ref object
status: string
temp: string
done: bool
proc finalizeDownload*[Download](o: Download) =
if not o.done and o.temp.existsFile:
# remove temp file when exception occurs"
o.temp.removeFile
proc downloadFileExt*(url, path: string): Download =
new(result, finalizeDownload)
result.done = false
result.temp = path & ".part"
newHttpClient().downloadFile(url, result.temp)
result.status = "200" # inflexible httpclient
result.temp.moveFile(path)
result.done = true
proc main =
let url = "http://www.some.com/file.mp4"
let dl = downloadFileExt(url, url.extractFilename)
echo "downloaded " & url & " with status " & dl.status
try:
main()
finally:
GC_fullCollect()
Is this a toy program?
It misuses finalizers.
As it happens, Nim's GC is not "unprofessional" compared to others, but it's understandable how that could be falsely assumed.......... :D
Basically, the thing that is apparently the confusing factor is that Nim has a conservative garbage collector. This means that the [mark and sweep mechanism of the] GC works by scanning the stack for ref s. Thus, if the stack contains a value that looks like a ref (in terms of bit pattern), the object that ref points to is kept alive. This is how the GC can determine if something is garbage in the first place. So, if you call GC_FullCollect() while a ref is still around, the object will not be deleted, and the finalizer will not be called. This is correct behavior, though. If the GC deleted objects that were still reachable, use-after-free would be inevitable. A conservative GC must keep all objects that have at least one alias/ref/pointer/handle/whatever alive, because code expects to still be able to use the pointed-to value.
The cost-benefit analysis between this system and non-conservative garbage collection is not always clear-cut. For languages that use conventional compilers that are not made for GCs (and not, say bytecode interpreters, like Java), conservative garbage collection is the only practical and useful solution to the memory management dilemma. Nim compiles to C and C doesn't understand GC (not like JS does, where its a language builtin).
For programmers, having a nondeterministic GC means that you should not count on finalizers being called deterministically (naturally). You could use them to clean up a resource that must be reclaimed eventually, like memory, but for immediate reclamation of resources at the end of the scope, use try / finally with explicit deallocation/cleanup/whatever. This pattern of automatic memory management and manual resource management is quite widespread in GC'd languages. Java does the same thing. You must call File.close(). Sure, the runtime or your OS will close the file eventually, but cleaning it up immediately is the responsibility of the programmer.
To solve this problem, it seems that @Araq is working on implementing destructors for Nim. These will clean up stack (and heap??) resources implicitly at the end of the scope.
Araq, a few questions: