As Araq has noted, it is usually
not necessary to explicitly code an =destroy
proc for an object
or ref object
type - in most cases the compiler automatically generates whatever is necessary.
There are couple of circumstances, however, where an explicitly coded =destroy
proc is necessary:
- The object's fields includes values that are allocated and managed by a foreign (i.e. non-Nim) library
- The object containers pointers or
cursor
references that are explicitly managed by the code
There is another, less common, circumstance where explicitly coded =destroy
procs may be necessary. That is in test/debug scenarios, where one needs to verify
that object destructors have been actually called. Unfortunately there is no way
(that I am aware of) to determine whether an automatically generated destructor
has been called. The usual way around this is to compile
with -d:useMalloc
and then use valgrind
to find any memory leaks
(i.e. heap objects that haven't been destroyed).
Unfortunately, using valgrind
in the above manner is not always effective. When
the code in question makes use of a foreign library that manages a large number of
resources, such as opengl and sdl2, the output from valgrind
is overwhelmend by
messages triggered by the foreign library code, and it can be difficult if not
impossible to get at the messages that are purely relevant to the nim code.
This package is intended to provide the following:
- A simple way to implement a custom
=destroy
hook when one is required. - A simple facility that implements a custom
=destroy
hook with trace output for a test/debug situation, but that lets Nim automatically implement its own=destroy
hook for a normal situation.
For the first case the destructor
macro, together with the destroyFields
macro,
generates the implementation of the =destroy
hook
for an object
or ref object
type. There is also an option to
generate printing of trace messages for debug purposes, if desired.
The general form used to invoke the destructor
macro looks like (using method call
syntax):
DestructeeType.destructor([tagfield = x.<field name>]):
<custom destructor code>
DestructeeType.destroyFields(x.field1, ..., x.fieldN)
<more custom destructor code>
DestructeeType.destroyFields(x.fieldM, ..., x.fieldQ)
<still more custom destructor code>
<... etc.>
The macro arguments are:
DestructeeType
= the type (typedesc) of the entity being destroyedtagfield
(optional) = the field whose value identifies the individual instance being destroyed in the destructor's trace message. SeeDestructor trace messages
below. Default is no tag field
The body code of the macro invocation consists of two types of code statements:
- any custom user code required for the destructor
- one or more
destroyFields(...)
call statements, specifying fields for which=destroy
calls are to be generated. There is no limit to the number of field arguments in a singledestroyFields(...)
statement. An=destroy
call is generated for each field argument, in same order as it appears in thedestroyFields(...)
call.
The above code statements can be mingled in whatever way is appropriate.
If the destructee type is a subclass of a base type (i.e. the type is
object of
or ref object of
), then an =destroy
call for that base type is
generated at the end of the code. (Note that this is not done if the base type is
RootObj
or RootRef
.)
Note that invoking destructor
with no body code, such as below, will cause a
forward declaration of the =destroy
hook to be generated.
DestructeeType.destructor()
The template traceDestructor
invokes the destructor
macro to be invoked, but only
if the compile option -d:traceDestructors
is used, which meeans that trace messages
are printed.
if the compile option -d:traceDestructors
is not used, then nothing (actually,
a single discard
stataement) is generated, and the Nim compiler automatically generates
generates its own implementation of the =destroy
hook as part of its operation.
Use of the traceDestructor
template is similar to the destructor
macro:
DestructeeType.traceDestructor([tagfield = x.<field name>]):
<custom destructor code>
DestructeeType.destroyFields(x.field1, ..., x.fieldN)
<more custom destructor code>
DestructeeType.destroyFields(x.fieldM, ..., x.fieldQ)
<still more custom destructor code>
<... etc.>
A forward declaration would be generated by:
DestructeeType.traceDestructor()
Again, if the compile option -d:traceDestructors
is not used, then nothing is
generated.
A few simple examples for illustration (more examples can be found, and executed,
in the when isMainModule:
block of destructor.nim
).
The examples here are based on the following ref object
type definition:
type
SimpleT = ref object
name: string
otherString: string
First, the simplest possible destructor definition:
destructor(SimpleT):
destroyFields(x.name, x.otherString)
Note the use of x
(the default) to represent the entity being destroyed. Note
also that a single statement can be used for all of the object's fields.
Second, let's assume that the field name
is used to identify the instance of the
SimpleT
object. Note that this option only affects destructor trace messages
(see Destructor trace messages
below).
destructor(SimpleT, tagfield = x.name):
destroyFields(x.name, x.otherString)
Finally, let's put some custom user code into the destructor.
destructor(SimpleT, tagfield = x.name):
if x.otherString == "Call":
# If otherString is "Call" then use the Call convention to invoke destroyFields
destroyFields(x.name, x.otherString)
else:
# Otherwise use the Command convention to invoke destroyFields
destroyFields x.name, x.otherString
If the compile option -d:traceDestructors
is specified, then the destructor
macro will generate statements to print JSON-like trace messages at:
- the beginning (and end) of the
=destroy
body, - before (and after) the
=destroy
call each for each field, and - before (and after) the
=destroy
call for the base type.
The trace message printed after each call and at the end of the destructor is
merely a JSON element terminator with a comma },
.
For example, consider the following code:
type
SimpleT = ref object
name: string
otherString: string
TestT = ref object of RootRef
simpleX: SimpleT
destructor(SimpleT, tagfield = x.name):
destroyFields(x.name, x.otherString)
destructor(TestT):
destroyFields(x.simpleX)
when isMainModule:
proc testCase() =
let t1 {.used.} = TestT(simpleX: SimpleT(name: "ph name", otherString: "xfghxfg"))
testCase()
Compiling the above code with option "-d:traceDestructors" and executing it gives the following output:
"Destructor for type TestT": {
"destroy field simpleX of type SimpleT": {
"Destructor for type SimpleT with name = 'ph name'": {
"destroy field name of type string": {
},
"destroy field otherString of type string": {
},
},
},
},
Pasting the output into a JSON formatter gives:
"Destructor for type TestT": {
"destroy field simpleX of type SimpleT": {
"Destructor for type SimpleT with name = 'ph name'": {
"destroy field name of type string": {},
"destroy field otherString of type string": {},
},
},
},
Notice the following:
- Nested indentation showing that destroying field
simpleX
caused the destructor for typeSimpleT
to be invoked. - The identification of the
SimpleT
instance being destroyed was that whose tag fieldname
had the valueph name