Here is a MCVE of an error I got.
type
RAII*[T] = object
resource: T
finalizer: proc (r: var T)
proc newRAII*[T](r: T, f: proc (r: var T)): RAII[T] =
RAII[T](resource: r, finalizer: f)
proc `=destroy`[T](r: var RAII[T]) =
r.finalizer(r.resource)
proc foo_with_exception() =
if(1==1):
raise newException(ValueError,"oops")
let guard = newRAII[int](
42,
proc(i: var int) =
echo "destroying " & $i)
proc bar() =
try:
foo_with_exception()
except ValueError as e:
echo "exception catched"
discard e
bar()
compile and run with nim 2.2.8
get SIGSEGV :
=>
SIGSEGV: Illegal storage access. (Attempt to read from nil?)
For me , with my C++ background, this code shall work. The guard object should never be instantiated at all, even less call the destructor and crash . What do you think ?
There's arguably a bug in how the compiler handles this, at least a compiler error is fitting here.
As far as I understand, =destroy is implemented as a try-finally construction to guarantee destruction on exiting the scope. The interaction with the control-flow breaking raise instruction leads to calling a destructor on a variable that has been instantiated, but hasn't been initialized yet, so the finalizer is still nil.
You can check it with --expandArc:foo_with_exception compiler command:
var guard # finalizer is nil here
try:
raise
(ref ValueError)(msg: "oops", parent: nil)
guard = newRAII(42, (proc (i: var int) = echo [`$`("destroying " & $_1i)], nil))
finally:
`=destroy`(guard)
This code runs fine:
type
RAII*[T] = object
resource: T
finalizer: proc (r: var T)
proc `=destroy`[T](r: var RAII[T]) =
if r.finalizer != nil: # added
r.finalizer(r.resource)
proc newRAII*[T](r: T, f: proc (r: var T)): RAII[T] =
RAII[T](resource: r, finalizer: f)
proc foo_with_exception() =
raise newException(ValueError,"oops")
let guard = newRAII(42, proc(i: var int) = echo "destroying " & $i)
proc main =
try:
foo_with_exception()
except ValueError as e:
echo "exception catched"
main() You would get a segfault with just
block:
var guard: RAII[int]
This is not a compiler bug. In nim, zero-initialized object is a valid object, and destructor has to be a no-op for such an object.
It's something the compiler should handle better, yes. It's a bug but it's not easy to fix. ;-)
Btw your RAII-Thing is just a template withLock or template withX. No need to use destructors for new control flow structures, Nim is not C++.