The following example doesn't leak when compiled with nim c --gc:arc main.nim && valgrind --leak-check=full ./main, but it does leak 16 bytes when compiled with nim c --gc:arc -d:useMalloc main.nim && valgrind --leak-check=full ./main
type ComponentC = ref object
c: int
func newComponentC(): ComponentC =
result = new ComponentC
result[].c = 150
proc test() =
var r: ref int8 = cast[ ref int8 ]( new ComponentC )
cast[ ref ComponentC ]( r )[] = newComponentC()
echo cast[ ref ComponentC ]( r )[].c
test()
Is this program not supposed to be leak free (maybe because of all the weird casts), or is this a bug in ARC+malloc?
Output without malloc:
==17934== Command: ./main
==17934==
150
==17934==
==17934== HEAP SUMMARY:
==17934== in use at exit: 0 bytes in 0 blocks
==17934== total heap usage: 1 allocs, 1 frees, 1,024 bytes allocated
==17934==
==17934== All heap blocks were freed -- no leaks are possible
Output with malloc:
==17906== Command: ./main
==17906==
150
==17906==
==17906== HEAP SUMMARY:
==17906== in use at exit: 16 bytes in 1 blocks
==17906== total heap usage: 4 allocs, 3 frees, 1,097 bytes allocated
==17906==
==17906== 16 bytes in 1 blocks are definitely lost in loss record 1 of 1
==17906== at 0x48435FF: calloc (vg_replace_malloc.c:1117)
==17906== by 0x109A97: alloc0Impl_system_1727
==17906== by 0x10A288: alignedAlloc0_system_1891
==17906== by 0x10DA35: nimNewObj
==17906== by 0x10F18B: new_ec83ystem_6
==17906== by 0x10F2BD: newComponentC_ec83ystem_4
==17906== by 0x10F438: test_ec83ystem_36
==17906== by 0x10F68E: NimMainModule
==17906== by 0x10F593: NimMainInner
==17906== by 0x10F5CF: NimMain
==17906== by 0x10F61D: main
==17906==
==17906== LEAK SUMMARY:
==17906== definitely lost: 16 bytes in 1 blocks
==17906== indirectly lost: 0 bytes in 0 blocks
==17906== possibly lost: 0 bytes in 0 blocks
==17906== still reachable: 0 bytes in 0 blocks
==17906== suppressed: 0 bytes in 0 blocks
I don't know about the leak, but shouldn't this code not work? ref ComponentC becomes ref ref int, no? I assume this is what you meant to write:
type ComponentC = ref object
c: int
func newComponentC(): ComponentC =
result = new ComponentC
result[].c = 150
proc test() =
var r: ref int8 = cast[ ref int8 ]( new ComponentC )
cast[ ptr ComponentC ]( addr r )[] = newComponentC()
echo cast[ ptr ComponentC ]( addr r )[].c
test() # 150
I fixed the issues with the first example. This still has the same leak issue:
type ComponentC = ref object
c: int
func newComponentC(): ComponentC =
result = new ComponentC
result[].c = 150
proc test() =
var r: ref seq[int8]
doAssert r == nil
static: doAssert typeof(new seq[ComponentC]) is ref seq[ComponentC]
r = cast[ref seq[int8]](new seq[ComponentC])
cast[ref seq[ComponentC]](r)[].add newComponentC()
echo cast[ref seq[ComponentC]](r)[][0].c
test()
as the issue happens only with --gc:arc -d:useMalloc
The point is that valgrind can see the leak only with --gc:arc -d:useMalloc.
See my book:
the expandarc looks the same when compiler with or without -d:useMalloc, and as the version without the malloc doesn't leak, I assumed that there can't be a missing destroyer. To be honest, I don't quite understand the output:
--expandArc: test
var
r
:tmpD
:tmpD_1
r =
wasMoved(:tmpD)
`=copy`(:tmpD, cast[ref seq[int8]](
:tmpD_1 = new seq[ComponentC]
:tmpD_1))
:tmpD
add(cast[ref seq[ComponentC]](r)[], new_1 ComponentC)
`=destroy`(:tmpD_1)
`=destroy_1`(r)
-- end of expandArc
for this
type ComponentC = ref object
c: int
proc test() =
var r: ref seq[int8]
r = cast[ref seq[int8]](new seq[ComponentC])
cast[ref seq[ComponentC]](r)[].add new ComponentC
test()
My guess: In this expandarc output, :tmpD_1 is a temporary variable introduced to hold the new seq[Component] call. It it then in effect cast and copied into r, that increments the underlying reference counter, both ``r`` and ``:tmpD_1`` shares a reference to the same buffer but with different types, which is, outside of inherited types, a precarious state of affairs. Then ``:tmpD_1`` is destroyed *first, that decreases the refcount, then r is destroyed, which decreases the refcount to 0 and triggers the desctruction of the not really underlying seq[int8], which fails to destroys the ComponentC previously added.
Funnily this code “works”:
type ComponentC = ref object
c: int
proc test() =
var r0 = new seq[ComponentC]
var r: ref seq[int8]
r = cast[ref seq[int8]](r0)
cast[ref seq[ComponentC]](r)[].add new ComponentC
test()
Here r0 is destroyed after r, and so the added ComponentC is destroyed.For completeness I'll mention how I solved the issue (at least how I believe I solved the issue).
For that I will shortly describe what I actually wanted to achieve. I wanted to create a small entity component manager. For that I need a type TypeVector, that can do something like this:
type Big = ref object
c: array[10000, int]
var t: TypeVector
for e in t.get(int):
static: doAssert e is int
echo e
for e in t.get(float):
static: doAssert e is float
echo e
for e in t.get(Big):
static: doAssert e is Big
echo e
# t.get(Big) should return a different seq than t.get(float)
# so one seq for each type and not different types in one seq
For that I wrote the following:
type TypeVector = object
vec: seq[ref seq[int8]]
func get(t: var TypeVector, T: typedesc): var seq[T] =
# typeID(T: typedesc): int = returns unique IDs for each type, starting at 0
let id = typeId(T)
if t.vec.len <= id:
t.vec.setLen(id + 1)
if t.vec[id] == nil: =
t.vec[id] = cast[ref seq[int8]](new seq[T])
assert TypeVector.vec[id] != nil
cast[ref seq[T]](TypeVector.vec[id])[]
As guibar pointed out quite well, this can easily lead to leaks, as the Nim compiler doesn't know to also destroy stuff of type T when it eventually destroys TypeVector.vec.
So my solution is to add custom functions to the destroyer of TypeVector that destroy the correct types:
type TypeVector = object
vec: seq[ref seq[int8]]
destroyFunctions: Table[int, proc(vec: ref seq[int8])]
proc `=destroy`(x: var TypeVector) =
for id, destroyFunction in x.destroyFunctions.mpairs:
destroyFunction(x.vec[id])
`=destroy`(x.vec)
`=destroy`(x.destroyFunctions)
func get(t: var TypeVector, T: typedesc): var seq[T] =
let id = typeId(T)
if t.vec.len <= id:
t.vec.setLen(id + 1)
if t.vec[id] == nil:
t.destroyFunctions[id] = proc(vec: ref seq[int8]) =
doAssert vec != nil
for e in cast[ref seq[T]](vec)[].mitems:
`=destroy`(e)
t.vec[id] = cast[ref seq[int8]](new seq[T])
cast[ref seq[T]](t.vec[id])[]
I wasn't sure if this could somehow lead to double frees, but I tested it and valgrind seems to be happy with it :D