As it says in the manual there are rather pleasant init procedure conventions in Nim, using initFoo to create an object on the stack and newFoo to create one on the heap and store a reference.
type
Foo = object
i: int
FooRef = ref Foo
proc initFoo(): Foo =
result.i = 3
proc newFooRef(): FooRef =
new(result) # alloc heap memory
result[] = initFoo() # reuse normal init proc
So far so good. Now I have a situation where I'm interfacing with a C library that allocates its own memory, like so:
import mycwrapper
type
MyCHandle = int
FooManual = object
h: MyCHandle
i: int
proc initFooManual(): FooManual =
result.i = 3
result.h = init_my_c_wrapper()
Using initFooManual makes sense on the surface because no Ref is used but leads you to assume all the memory relevant to FooManual is on the stack and will be copied. But init_my_c_wrapper actually puts it on the heap and stores a pointer in the MyCHandle, so created objects behave more like ref objects.
let fooManual = initFooManual()
let fooManual2 = fooManual
my_c_wrapper_set_value(fooManual, 123)
assert my_c_wrapper_get_value(fooManual2) == 123 # this is not the case for normal stack objects, only ref objects
But using newFooManual is wrong too, because that would imply a new() operation is used, allocating GC memory. So a third convention seems to be called for.
What would be a good convention here? Use initFooManual anyway and document the behavior? Use mapFooManual to imply that mmap operations will be going on? Use allocFooManual? I haven't seen anything consistent in the wild and I had a hard time finding something relevant in the impure parts of stdlib.
Stable pointers for C interfaces is one of the few places where we use ref object - ergo, that becomes:
type FooRef = ref object
...
func init(T: type FooRef): T = T(i: 3, h: init_my_c_vrapper())
We tend to stick a Ref in the type name becomes ref semantics are problematic - in general, we only use ref object in cases where it's really necessary, otherwise we use ref T where needed instead.
https://status-im.github.io/nim-style-guide/language.objconstr.html - the convention we use is that new adds a layer of ref to the type - init doesn't - no need for another one in the C api case, I think.
Stable pointers for C interfaces is one of the few places where we use ref object
We do? What does it accomplish that storing a C handle in a non-ref handle doesn't?
In your example, one can assume that copying the handle is "wrong" - let's say it's a file handle - you're not supposed to copy file handles around because they don't have copy semantics in C (you're supposed to dup copies) - using ref object correctly represents that situation in nim.
The other example is when the C API requires a stable memory buffer - a typical scenario is:
type X = ref object
data: someCType
func init(T: type X): T =
let res = T()
res.c_init(addr res.data)
res