RUNTIME VALUE CONVERSIONS
Reuse a C context across completed calls while reclaiming values your application no longer needs.
Each zero-initialized PC owns the allocations behind its Runtime values. Copying a PV copies a handle, not its backing storage. By default, values remain allocated until pv_destroy. C callers may now explicitly reclaim unreachable values with pv_collect.
Collection is precise and non-moving. It follows supported value references and reclaims unreachable cycles as well as ordinary discarded values. Live strings, objects, and container storage keep their addresses. Released capacity becomes available to subsequent Runtime allocations in that same context.
int pv_collect(PC *context, size_t root_count,
const PV *roots, size_t *reclaimed_bytes);
Call this only when the context is idle, between completed pv_call or pv_call_kw operations. Supply every PV your C code will still need. These retained values are called roots. Supply NULL for roots only when root_count is zero. The optional reclaimed_bytes pointer receives the released allocation bytes, including allocation metadata.
A return of 1 means collection completed; 0 means it was refused and no arena allocation was freed. Refusal leaves the existing error and resource counters unchanged, and writes zero to reclaimed_bytes if supplied. Check the return value. A context with an error should be destroyed after recording that error.
Collection is opt-in. It does not happen inside Python loops, between Python statements, or automatically when a memory limit is reached. Split a long-running C-driven workload into completed calls to create collection opportunities. Keep using pv_destroy when the context is finished.
Convert this source using Runtime and save the output as memory-example.c.
def make_batch(seed):
return [str(seed + i) for i in range(20)]
def main():
print(make_batch(0)[0])
return 0
This source prints 0 when built with the standalone entry wrapper. The following C caller instead makes 1,000 calls in one context. It retains the first batch and each current batch, allowing previous discarded batches to be reclaimed.
#include "memory-example.c"
int main(void) {
PC context = {0};
int status = 1;
PV saved = pv_none();
/* pv_fn1 is make_batch for this exact example. */
PV function = pv_top_function(&context, pv_fn1);
if (context.error) goto done;
for (int i = 0; i < 1000; ++i) {
PV argument = pv_i(i);
PV batch = pv_call(&context, function, 1, &argument);
if (context.error) goto done;
if (i == 0) saved = batch;
PV roots[2] = {saved, batch};
if (!pv_collect(&context, 2, roots, NULL)) {
fputs("Collection refused\n", stderr);
goto done;
}
}
PV first = pv_getitem(&context, saved, pv_i(0));
if (context.error) goto done;
pv_print_ex(&context, 1, &first, pv_none(), pv_none());
if (!context.error) status = 0;
done:
if (context.error) fprintf(stderr, "%s\n", context.error);
pv_destroy(&context);
return status;
}
gcc -std=c11 memory-caller.c -o memory-caller -lm
Save the caller as memory-caller.c, compile only that file, and omit PYCF_RUNTIME_MAIN. Run ./memory-caller or .\memory-caller.exe in Windows PowerShell. Expected output is 0. Find the generated function symbol in your own conversion details; do not assume another source uses pv_fn1 for the same function.
| Reference | Collection behavior |
|---|---|
| Caller-retained PVs | Pass every live returned value, dynamic closure, bound method, container, or iterator in roots on every collection. |
| Context state | Module bindings, registered functions, mutable defaults, builtin values, and class cells are retained automatically. Their reachable values stay live. |
| Shared references and cycles | Live aliases remain valid. A cycle is reclaimed only when unreachable from all roots. |
| Containers and iterators | Only current list items and live dictionary/set entries keep values alive. Views and iterators retain their referenced containers and inputs. |
| File results | Discarded read buffers return their retained-file-buffer capacity. A rooted closed file handle remains usable for its closed-state feedback. |
| Borrowed raw pointers | Keep the owning PV rooted. A raw pointer alone is not a root. Caller-owned copies of text/data are independent. |
Use valid PVs belonging to the context, caller-owned storage for the roots array, and a separate caller-owned size_t for the optional byte count. Do not keep using an omitted value after collection. Do not access the same context concurrently or invoke collection from a callback or a directly called generated function body.
Collection is refused during active Runtime calls, module initialization, active file scopes, outstanding errors, or terminal failures. It also refuses a missing/oversized roots array, an unrecognized heap reference, inconsistent graph metadata, or failure to allocate its temporary allocation index. A failed trace performs no sweep.
The default root limit is 4,096, configurable through PYCF_RUNTIME_ROOTS at C build time. The collector uses an iterative work list and a temporary index of one pointer per arena allocation, outside the arena. That index is bounded by PYCF_RUNTIME_BYTES; allocation failure is reported by a zero return. Large live graphs take proportionally more time; collection has no wall-clock deadline.
Allocation metadata is included in context.bytes. Collection returns arena and retained-file-buffer capacity, but does not reset step counts, call limits, cumulative file-operation counts, or handle serials. Mutable defaults that keep accumulating values remain live and can still exhaust memory. A single long-running call can still reach its limits before any collection opportunity.
The internal allocation and closure structures changed. Regenerate C and rebuild callers together; do not mix runtime structure layouts from different generated versions. Existing pv_call, PV value handles, and pv_destroy usage remain supported.
See memory/work limits, file limits, and output validation and ForgeLens.