jm object NAME --preset reader — reader (external source → output)¶
A reader opens an external source — file, socket, pipe, mmap'd region — and yields data on demand. Unlike a generator (which produces from internal state), a reader has a side input it must acquire, position within, and release.
Concrete examples: a binary file reader for a custom format, a CSV row reader, a WAV / PNG / Parquet loader, a TCP socket consumer, a mmap'd shared-memory channel reader, or any source where the data lives outside the process.
--preset reader bundles --no-step (the standard step() interface
doesn't fit I/O) with a filepath:const char * init-param. The scaffold
builds and tests green. Add the read() / seek() / close() methods
yourself with jm method; this preset formalises the asymmetry
init-params solve — init_params for the user-facing ctor (filepath),
state for internal bookkeeping (fd, position).
Command¶
jm object NAME --preset reader \
--init-param header_bytes:size_t:0 \
--state fd:int:-1 \
--state file_size:size_t:0 \
--state position:size_t:0
The filepath:const char * init-param comes from the preset; the rest
are yours. Then add the I/O verbs:
jm method NAME read --param n:size_t --out-type "float _Complex"
jm method NAME seek --param sample_index:size_t --return-type int
jm method NAME close --return-type void
jm method defaults --return-type to float _Complex when omitted, so
close needs an explicit --return-type void to get a real void C
signature — without it you'd get a close() that returns (and discards)
a bogus complex value.
What you get¶
native/inc/NAME/NAME_core.h¶
typedef struct {
int fd;
size_t file_size;
size_t position;
} NAME_state_t;
/* Ctor takes init_params; state stays internal. */
NAME_state_t *NAME_create(const char *filepath, size_t header_bytes);
void NAME_destroy(NAME_state_t *state);
void NAME_reset(NAME_state_t *state);
float _Complex NAME_read(NAME_state_t *state, size_t n, float _Complex *out);
int NAME_seek(NAME_state_t *state, size_t sample_index);
void NAME_close(NAME_state_t *state);
read()'s C return type follows jm method's default (float _Complex,
since the command above doesn't pass --return-type) even though the
Python binding ignores it — the sample count comes from n, and the data
goes into out. Pass --return-type size_t if you want the C-level
return value to mean something (e.g. "samples actually read"); the
generated Python wrapper's behavior doesn't change either way.
native/src/NAME/NAME_core.c¶
NAME_state_t *
NAME_create(const char *filepath, size_t header_bytes)
{
NAME_state_t *obj = calloc(1, sizeof(*obj));
if (!obj)
return NULL;
obj->fd = -1;
obj->file_size = 0;
obj->position = 0;
return obj;
}
void
NAME_destroy(NAME_state_t *state)
{
free(state);
}
/* <<IMPLEMENT: read >> */
float _Complex
NAME_read(NAME_state_t *state, size_t n, float _Complex *out)
{
(void)state; (void)n; (void)out;
return (float _Complex)0.0f + 0.0f * I;
}
/* <<IMPLEMENT: seek >> */
int
NAME_seek(NAME_state_t *state, size_t sample_index)
{
(void)state; (void)sample_index;
return (int)0;
}
/* <<IMPLEMENT: close >> */
void
NAME_close(NAME_state_t *state)
{
(void)state;
}
filepath and header_bytes are constructor init-params, not state —
they never reach NAME_create()'s body automatically (init-params carry
no auto-assignment the way --state does). You open the file, seed
file_size/position, and wire fd yourself.
What you fill in¶
The open()/read()/seek()/close() logic against state->fd.
A typical fill-in:
NAME_state_t *
NAME_create(const char *filepath, size_t header_bytes)
{
NAME_state_t *obj = calloc(1, sizeof(*obj));
if (!obj) return NULL;
obj->fd = open(filepath, O_RDONLY);
if (obj->fd < 0) { free(obj); return NULL; }
struct stat st;
if (fstat(obj->fd, &st) < 0) { close(obj->fd); free(obj); return NULL; }
obj->file_size = (size_t)st.st_size;
obj->position = header_bytes;
return obj;
}
void
NAME_destroy(NAME_state_t *state)
{
if (!state) return;
if (state->fd >= 0) close(state->fd);
free(state);
}
float _Complex
NAME_read(NAME_state_t *state, size_t n, float _Complex *out)
{
ssize_t bytes = read(state->fd, out, n * sizeof(*out));
if (bytes > 0) state->position += (size_t)bytes;
return 0;
}
int
NAME_seek(NAME_state_t *state, size_t sample_index)
{
off_t off = (off_t)(sample_index * sizeof(float _Complex));
if (lseek(state->fd, off, SEEK_SET) == (off_t)-1) return -1;
state->position = (size_t)off;
return 0;
}
void
NAME_close(NAME_state_t *state)
{
if (state->fd >= 0) { close(state->fd); state->fd = -1; }
}
Other common shapes:
- Wire-format demultiplexing (separate I and Q from interleaved bytes).
- Header parsing (use
header_bytesto skip a file header). - Type conversion (read int16 from disk, return float _Complex).
- Endianness swap on read.
Python usage¶
from <pkg> import NAME
rdr = NAME(filepath="capture.iq", header_bytes=0)
chunk = rdr.read(4096) # → (4096,) complex64
rdr.seek(0)
rdr.close()
Concrete types¶
| Slot | Accepts | Rejects | Default |
|---|---|---|---|
--init-param name:T:D |
Path/filename strings use const char *. Any scalar, T[], T[][], string_enum:a,b,c. |
T[N] (fixed length — that's --state territory). |
filepath:"const char *", header_bytes:size_t:0 |
--state field:T:D |
Any scalar. The file descriptor pattern is fd:int:-1. |
const char * (use an --init-param to receive the path, then store the parsed fd / size). |
fd:int:-1, file_size:size_t:0, position:size_t:0 |
Method return / output (out_type = "T") |
Any array element type. read() returns a T[] ndarray sized from the requested sample count. |
bool, int, const char *, long double _Complex. |
float _Complex |
const char * is the load-bearing type here. It is a valid init-param
(PyArg parses the Python str; lifetime is managed on the Python side)
but is not a valid state field — persist the parsed result (the
fd, a size_t) instead. This is the asymmetry the reader preset
exists to formalise.