Memory Management
SPIRES is a C library, and all memory management is explicit. There is no garbage collector, no reference counting, and no automatic cleanup. Understanding the ownership rules for every allocation is essential to writing correct SPIRES programs.
This page documents the ownership model for every SPIRES type and function that allocates memory.
Ownership Principles
SPIRES follows three consistent rules:
-
The library allocates, the library frees: Objects created by
spires_reservoir_create()must be destroyed byspires_reservoir_destroy(). Never callfree()on a reservoir pointer. -
Returned arrays are caller-owned: Functions that return
double *(such asspires_run()andspires_copy_reservoir_state()) allocate the return buffer withmalloc(). The caller owns this memory and must callfree()when done. -
Input parameters are caller-owned: Pointers passed into SPIRES functions (such as
neuron_params, input arrays, and target arrays) remain owned by the caller. SPIRES reads from them but does not free them, retain them, or modify them.
Reservoir Lifecycle
Creation
spires_reservoir *r = NULL;
spires_status s = spires_reservoir_create(&cfg, &r);spires_reservoir_create() allocates all internal memory for the reservoir: weight matrices, neuron states, history buffers (for fractional neurons), and metadata. On success, r points to a fully initialized reservoir. On failure, r is NULL and no memory has been leaked.
The cfg struct is read during creation but not retained. You may modify or discard cfg after spires_reservoir_create() returns.
Destruction
spires_reservoir_destroy(r);spires_reservoir_destroy() frees all memory associated with the reservoir, including internal weight matrices, state arrays, and history buffers. After this call, r is a dangling pointer. Do not dereference it.
It is safe to pass NULL to spires_reservoir_destroy() — it is a no-op.
Reset
spires_reservoir_reset(r);spires_reservoir_reset() zeroes out the neuron states and history buffers without freeing or reallocating any memory. The reservoir retains its weight matrices and trained readout weights. This is used to restart the reservoir dynamics from a clean initial state without the cost of destroying and recreating the reservoir.
Functions That Allocate
spires_run()
double *predictions = spires_run(r, input_series, series_length);Returns a malloc-allocated array of length series_length * num_outputs. The caller must free this array:
/* Use predictions */
for (int i = 0; i < series_length; i++) {
printf("%.4f\n", predictions[i]);
}
/* Free when done */
free(predictions);If the function fails (e.g., due to invalid arguments or allocation failure), it returns NULL. Always check the return value.
spires_copy_reservoir_state()
double *state = spires_copy_reservoir_state(r);Returns a malloc-allocated array of length num_neurons containing a snapshot of the current membrane potentials. The caller must free this array:
/* Inspect the state */
for (int i = 0; i < num_neurons; i++) {
printf("Neuron %d: %.4f mV\n", i, state[i]);
}
/* Free when done */
free(state);Returns NULL on failure.
spires_compute_output()
double *output = spires_compute_output(r);Returns a malloc-allocated array of length num_outputs containing the current readout output (computed from the current reservoir state and the trained weights). The caller must free this array:
free(output);Functions That Do Not Allocate
spires_read_reservoir_state()
double buffer[500]; /* caller-provided buffer */
spires_status s = spires_read_reservoir_state(r, buffer, 500);This function writes the current reservoir state into a caller-provided buffer. No allocation is performed by SPIRES. The caller is responsible for ensuring the buffer is large enough (at least num_neurons elements).
This is the preferred function for performance-critical code that reads the state frequently, as it avoids repeated malloc/free cycles.
spires_step()
spires_status s = spires_step(r, input_vector);Advances the reservoir by one time step. No memory is allocated. The input_vector is read but not retained.
spires_train_ridge() and spires_train_online()
spires_status s = spires_train_ridge(r, input, target, length, lambda);
spires_status s = spires_train_online(r, input, target, length, eta);These functions allocate temporary internal memory during training (for the state matrix in ridge regression, or for intermediate computations) but free it before returning. The only persistent side effect is the updated readout weights stored inside the reservoir.
The input and target arrays are read but not retained. The caller retains ownership.
The neuron_params Pointer
The neuron_params field in spires_reservoir_config is a void * pointer to a caller-allocated parameter struct:
spires_flif_params params = {
.alpha = 0.5,
.tau_m = 20.0,
/* ... */
};
cfg.neuron_params = ¶ms;
spires_reservoir_create(&cfg, &r);
/* params can now be modified, reused, or freed */
/* SPIRES has copied the values it needs */SPIRES reads the pointed-to struct during spires_reservoir_create() and copies the values into its internal data structures. After creation, the caller may do whatever it wants with the original struct — it is not referenced again.
If neuron_params is NULL, SPIRES uses built-in default parameters.
Common Patterns
Basic Lifecycle
spires_reservoir *r = NULL;
spires_status s = spires_reservoir_create(&cfg, &r);
if (s != SPIRES_OK) { /* handle error */ }
/* Use the reservoir */
s = spires_train_ridge(r, input, target, len, lambda);
double *pred = spires_run(r, test_input, test_len);
/* Clean up */
free(pred);
spires_reservoir_destroy(r);Multiple Runs with Reset
spires_reservoir *r = NULL;
spires_reservoir_create(&cfg, &r);
for (int trial = 0; trial < 10; trial++) {
spires_reservoir_reset(r); /* fresh state, same weights */
double *pred = spires_run(r, test_input, test_len);
/* evaluate trial */
free(pred); /* free each prediction buffer */
}
spires_reservoir_destroy(r);Reading State Without Allocation
int N = spires_num_neurons(r);
double *buf = malloc(N * sizeof(double));
for (int t = 0; t < time_steps; t++) {
spires_step(r, &input[t]);
spires_read_reservoir_state(r, buf, N);
/* process buf without allocation overhead */
}
free(buf); /* free once at the end */Memory Leak Checklist
When debugging memory issues in a SPIRES program, verify:
- Every
spires_reservoir_create()has a matchingspires_reservoir_destroy(). - Every non-
NULLreturn fromspires_run()is passed tofree(). - Every non-
NULLreturn fromspires_copy_reservoir_state()is passed tofree(). - Every non-
NULLreturn fromspires_compute_output()is passed tofree(). - Error paths destroy the reservoir and free any allocated buffers before returning.
Tools such as Valgrind (valgrind --leak-check=full ./my_program) or AddressSanitizer (gcc -fsanitize=address) can detect leaks at runtime.