asopi tech
asopi techIndie Developer

ASMATI・TATSUMIEXPERIMENT NOTE 03

Following a Nim ref object Through C Pointers and Reference Counting

Published: Aug 27, 2026Reading time: ~7 min

Asmati Tatsumi — “What High-Level Languages Do at the Low Level,” Part 3

This series explains how the Nim compiler translates Nim programs into C by reading the generated C code. Part 3 passes a Nim ref object to functions (proc declarations) and explains the C pointer representation and the reference-management implementation generated by Nim’s ORC (Optimized Reference Counting) memory manager.

1. Conditions

This experiment compiles two functions that accept a Nim ref object in debug mode. The recorded conditions are:

  • OS: macOS 26.5.2
  • CPU: Apple Silicon
  • Nim: 2.2.10
  • C compiler: Apple clang 21.0.0
  • Memory manager: ORC (Optimized Reference Counting)
  • Build mode: debug (opt: none)
  • Nim compiler executable: x86_64, running through Rosetta
  • Generated executable: arm64

2. Nim memory management with ORC

Nim ref[T] stores a reference to an object rather than the object body itself. In this generated C, the body of a ref object becomes a structure, and the reference becomes a pointer to that structure. The pointer holds the address of the referenced object, while a nil reference is represented by a null pointer.

ARC (Automatic Reference Counting) is a Nim memory-management strategy in which the compiler inserts reference-count updates. The count increases when another reference retains an object and decreases when that reference is released. An object is destroyed when its reference count reaches zero.

ORC (Optimized Reference Counting) adds cycle collection to ARC. When objects reference one another, their counts can remain above zero even after the program no longer uses the cycle. ORC can collect those cyclic references. Destructors and Move Semantics for Nim 2.2.10 explains that ref under ARC and ORC uses runtime hooks and reference counting, and that ORC ships with a cycle collector. The official Introduction to ARC/ORC in Nim illustrates how ORC collects cycles that ARC alone would retain.

This experiment first identifies the pointers used for function parameters and return values, then explains the generated C function eqcopy used when a reference is returned. eqcopy increments the source reference count, decrements and, when required, destroys the reference previously held by the destination, and finally assigns the new pointer.

3. Two functions that accept a ref object

The source defines Reading as a ref object with one value. The function readValue accepts a Reading and returns -1 for nil or the value field for a non-nil reference. The function keepReading returns the Reading that it receives.

ref_proc.nim
type
  Reading = ref object
    value: int

proc readValue(reading: Reading): int {.noinline.} =
  if reading.isNil:
    -1
  else:
    reading.value

proc keepReading(reading: Reading): Reading {.noinline.} =
  reading

let present = Reading(value: 42)
let missing: Reading = nil

echo "present=", readValue(present)
echo "missing=", readValue(missing)
echo "keptIsNil=", keepReading(missing).isNil

The type Reading is a managed reference to an object with the integer field value. The variable present points to a Reading whose value is 42, while missing is a nil reference of the same type.

Both references are passed to readValue. The nil reference is also passed to keepReading, and isNil checks the returned value. The same source therefore exposes non-nil field access, the nil branch, and a reference-typed return value.

4. Compiling and running the program

The following video compiles the Nim source and runs the generated executable.

The video compiles the Nim source and checks the results for a populated reference and a nil reference.

動画を開く

The video runs these two commands separately.

nim c --nimcache:observed/nimcache \
  -o:observed/bin/ref_proc \
  src/ref_proc.nim
./observed/bin/ref_proc

The executable prints:

present=42
missing=-1
keptIsNil=true

The populated reference produces 42, and the nil reference follows the branch that returns -1. Returning the nil reference from keepReading also produces a nil value.

5. The ref object body and reference type

In generated C, the Reading object body becomes a structure. Parameters and return values of type Reading become pointers to that structure. The relevant type and function declarations are:

@mref_proc.nim.c
typedef struct tyObject_ReadingcolonObjectType___8EkEo4jsvRYTKsMVQRmgZQ
  tyObject_ReadingcolonObjectType___8EkEo4jsvRYTKsMVQRmgZQ;

struct tyObject_ReadingcolonObjectType___8EkEo4jsvRYTKsMVQRmgZQ {
  NI value;
};

N_LIB_PRIVATE N_NOINLINE(NI, readValue__ref95proc_u4)(
  tyObject_ReadingcolonObjectType___8EkEo4jsvRYTKsMVQRmgZQ* reading_p0);

N_LIB_PRIVATE N_NOINLINE(
  tyObject_ReadingcolonObjectType___8EkEo4jsvRYTKsMVQRmgZQ*,
  keepReading__ref95proc_u10)(
    tyObject_ReadingcolonObjectType___8EkEo4jsvRYTKsMVQRmgZQ* reading_p0);

The structure tyObject_Reading... is the object body for Reading. Its value field uses NI, which resolves to int64_t under these conditions, as shown in Part 1.

The parameter reading_p0 of readValue__ref95proc_u4 is a tyObject_Reading...*. Both the parameter and return value of keepReading__ref95proc_u10 use the same pointer type. At these generated C function boundaries, a Nim Reading appears as a pointer to the object body.

N_NOINLINE corresponds to the {.noinline.} pragma in the Nim source. It keeps the two functions from being inlined, allowing each signature and function body to remain visible in generated C.

6. Nil check and field access

The generated body of readValue contains both the nil check and the field access.

@mref_proc.nim.c
N_LIB_PRIVATE N_NOINLINE(NI, readValue__ref95proc_u4)(
    tyObject_ReadingcolonObjectType___8EkEo4jsvRYTKsMVQRmgZQ* reading_p0) {
  NI result;
  if (!(reading_p0 == 0)) goto LA4_;
  result = ((NI)-1);
  goto LA2_;
LA4_: ;
  result = (*reading_p0).value;
LA2_: ;
  return result;
}

The Nim expression reading.isNil becomes the pointer comparison reading_p0 == 0. For a nil argument, the function assigns -1 to result.

For a non-nil argument, execution moves to LA4_, where (*reading_p0).value dereferences the pointer and reads the value field. The field access appears only in the non-nil branch. Both branches return through return result.

The isNil definition for ref T is a compiler built-in that tests whether a reference is nil. In this generated C, the test is visible as a null-pointer comparison.

7. How nil is represented

Generated C uses NIM_NIL for an explicit nil value. The C branch in the nimbase.h shipped with Nim 2.2.10 defines it as:

nimbase.h
#ifndef __cplusplus
#  include <stdbool.h>
#  define NIM_NIL ((void*)0)
#endif

The generated file is compiled as C, so NIM_NIL is ((void*)0). This value initializes let missing: Reading = nil and the return slot in keepReading shown below.

The condition in readValue uses reading_p0 == 0, while explicit nil values are emitted as NIM_NIL. The two forms both represent a null pointer.

8. The eqcopy helper generated for an ORC-managed reference

The function keepReading does not simply return its parameter pointer. It initializes a return slot to nil and calls the generated helper eqcopy.

@mref_proc.nim.c
N_LIB_PRIVATE N_NOINLINE(
    tyObject_ReadingcolonObjectType___8EkEo4jsvRYTKsMVQRmgZQ*,
    keepReading__ref95proc_u10)(
      tyObject_ReadingcolonObjectType___8EkEo4jsvRYTKsMVQRmgZQ* reading_p0) {
  tyObject_ReadingcolonObjectType___8EkEo4jsvRYTKsMVQRmgZQ* result;
  result = NIM_NIL;
  eqcopy___ref95proc_u19(&result, reading_p0);
  return result;
}

The local variable result is a pointer to a Reading object body and starts as NIM_NIL. The call to eqcopy___ref95proc_u19 receives the address of the destination slot and the source parameter reading_p0. The helper contains these operations:

@mref_proc.nim.c
N_LIB_PRIVATE N_NIMCALL(void, eqcopy___ref95proc_u19)(
    tyObject_ReadingcolonObjectType___8EkEo4jsvRYTKsMVQRmgZQ** dest_p0,
    tyObject_ReadingcolonObjectType___8EkEo4jsvRYTKsMVQRmgZQ* src_p1) {
  {
    if (!src_p1) goto LA3_;
    nimIncRef(src_p1);
  }
LA3_: ;
  {
    NIM_BOOL T7_;
    T7_ = (NIM_BOOL)0;
    T7_ = nimDecRefIsLast((*dest_p0));
    if (!T7_) goto LA8_;
    eqdestroy___ref95proc_u34((&(*(*dest_p0))));
    nimRawDispose((*dest_p0), ((NI)8));
  }
LA8_: ;
  (*dest_p0) = src_p1;
}

The generated C defines the function eqcopy___ref95proc_u19 in the form N_NIMCALL(void, eqcopy___ref95proc_u19). The first argument to N_NIMCALL, void, is the return type, and the second argument, eqcopy___ref95proc_u19, is the function name. This part denotes a function named eqcopy___ref95proc_u19 that returns no value. Part 1 explains the definition and expansion of N_NIMCALL itself.

For a non-nil source, nimIncRef increments its reference count. If the current destination is the last reference to its object, the nimDecRefIsLast branch destroys that previous value and releases its memory. The helper then assigns the source pointer to the destination.

In keepReading, result is initialized to nil immediately before this call, so there is no previous destination object to release. The generated eqcopy helper nevertheless contains the increment, decrement, destruction, and assignment required for a managed-reference copy.

The ORC reference management introduced in Section 2 appears in this generated C as eqcopy, nimIncRef, and nimDecRefIsLast.

9. Reading the pointer representation separately from its management

In this experiment, the ref object body becomes a C structure, while function parameters and return values become pointers to that structure. The nil check becomes a null-pointer comparison, and the non-nil field access dereferences the pointer.

The return value of keepReading, however, requires more than a pointer assignment. The generated eqcopy function shows the ORC implementation. This article confirms both how pointers appear in function signatures and how their lifecycle is managed in the generated implementation.

The Nim source, generated C excerpt, execution output, and verification script are available in asmati-lab experiment 005. Its CI verifies the Reading structure, both function signatures, the nil branch, field access, the reference-typed return value, and the executable output.

References