zubin’s R functions are built on a header-only C99 library, and any package can use the same code: the buffer, the cursor, the typed reads and writes, and the record layouts with their unpack and pack kernels. This page is for the author of such a package.
Linking
There is nothing to link. Every function is
static inline in a header, so a package that includes them
carries its own copy of what it uses, and zubin need not be installed
when it runs. The whole recipe is three files:
DESCRIPTION (an excerpt):
LinkingTo: zubin, zufast
NAMESPACE:
useDynLib(zubintest, .registration = TRUE)
src/Makevars:
PKG_CFLAGS = $(C_VISIBILITY)
LinkingTo names both zubin and zufast because it is not
transitive: R adds each named package’s include/ directory
to the compiler’s path, and zubin’s headers include zufast’s
<zufast/bits.h> and
<zufast/utf8.h>. $(C_VISIBILITY) hides
your symbols; the headers emit nothing that is not already
static, so two packages that both use zubin never bind to
each other’s copies.
The contract is source compatibility, as for zufast: within major
version 1 a function, type, macro or enumerator that exists keeps
existing with the same meaning, enumerator values are permanent, and
struct fields are only ever appended. Initialise every struct through
its zb_* function, never by aggregate initialisation, so an
appended field has a defined value. ZUBIN_VERSION_NUMBER is
10000 * major + 100 * minor + patch.
Any number of translation units may include the headers; there is no
implementation macro to define, and they compile as C99 and as C++11.
The fixture package tools/zubintest, whose files are what
this page shows, is built, checked and tested on Linux, macOS and
Windows on every change, and then tested again with zubin and zufast
uninstalled.
The headers
| Header | What it holds |
|---|---|
<zubin.h> |
everything below except zubin-r.h
|
<zubin/status.h> |
zb_status and zb_status_string()
|
<zubin/rw.h> |
zb_rd_<type><order>() and
zb_wr_...() for every type at any alignment,
zb_u128, and reads at a width chosen at run time |
<zubin/cursor.h> |
zb_cur: checked sequential reads |
<zubin/buf.h> |
zb_buf: a buffer with ownership, a hard cap and checked
growth |
<zubin/layout.h> |
record layouts, the spec parser, and the unpack and pack kernels, native-order ones included |
<zubin/column.h> |
bitmaps, bit-packed booleans, validity bitmaps to and from R’s missing values, byte swapping |
<zubin-r.h> |
a zb_buf owned by an R external pointer, foreign memory
with its release function, and R’s serialization over both; the only
header that includes R’s |
Every function that can fail returns a zb_status, which
is ZB_OK (0) on success; results come back through
out-parameters. On failure the inputs are unchanged: a
cursor that returns ZB_ERR_EOF has not moved, a buffer that
returns ZB_ERR_LIMIT holds what it held. Nothing in the
headers prints, aborts, exits or allocates, except buf.h,
and that only through malloc, realloc and
free.
Reading: the cursor
A cursor is a pointer, a length and a 0-based position, so every
error a reader reports is “at byte c.pos”. One read per
type and byte order: zb_cur_u32le(),
zb_cur_f64be(), zb_cur_bytes() to borrow a run
of bytes, zb_cur_seek() and zb_cur_skip().
/* Translation unit 2: a checked sequential read of a header. Includes one
area header, then the umbrella, the two ways the README allows. */
#include <zubin/cursor.h>
#include <zubin.h>
#include "zubintest.h"
/* magic:u32be version:u16le count:u32le name:6 bytes; returns c(magic,
version, count, the position after the header), or the status's name and
the position where the read failed. */
SEXP zt_header(SEXP x)
{
zb_cur c;
uint32_t magic = 0, count = 0;
uint16_t version = 0;
const uint8_t *name = NULL;
zb_status st;
SEXP out;
zb_cur_init(&c, XLENGTH(x) ? RAW(x) : NULL, (size_t)XLENGTH(x));
if ((st = zb_cur_u32be(&c, &magic)) || (st = zb_cur_u16le(&c, &version)) ||
(st = zb_cur_u32le(&c, &count)) || (st = zb_cur_bytes(&c, &name, 6))) {
out = PROTECT(Rf_allocVector(STRSXP, 2));
SET_STRING_ELT(out, 0, Rf_mkChar(zb_status_string(st)));
SET_STRING_ELT(out, 1, Rf_mkChar(c.pos == 0 ? "0" : c.pos == 4 ? "4" : c.pos == 6 ? "6" : "10"));
UNPROTECT(1);
return out;
}
out = Rf_allocVector(REALSXP, 4);
REAL(out)[0] = magic;
REAL(out)[1] = version;
REAL(out)[2] = count;
REAL(out)[3] = (double)c.pos;
return out;
}Records: layouts and kernels
A layout is parsed from the same specification string
bin_layout() takes, into a field array the caller supplies,
so the parser allocates nothing. Size the array with
zb_layout_count_fields(); names in the result are borrowed
pointers into the specification, which must outlive the layout.
The kernels then process one field of n records in one
strided loop, into one typed column: zb_unpack_i32() for
the small integer types and bool,
zb_unpack_f64() for u32 and the floating
types, zb_unpack_i64() and zb_unpack_f64x()
(exact or an error) for the 64-bit integers, and
zb_unpack_bytes() for b and s.
Array fields are written column-major, element k of record
i at dst[k * n + i]. The
zb_pack_*() kernels are the inverses, and refuse a value
that does not fit rather than wrapping it. The kernels do not
bounds-check: prove
offset + (n - 1) * stride + size <= length first.
/* Translation unit 1: parse a layout and unpack records with its kernels.
Includes the umbrella header. */
#include <zubin.h>
#include "zubintest.h"
/* Every numeric field of every whole record of x, as doubles, one list
element per field; or the status's name when the spec is malformed. */
SEXP zt_unpack(SEXP spec, SEXP x)
{
const char *s = CHAR(STRING_ELT(spec, 0));
size_t n = strlen(s), pos = 0, len = (size_t)XLENGTH(x), nrec;
uint32_t nf = zb_layout_count_fields(s, n), j;
zb_field *fields = (zb_field *)R_alloc(nf, sizeof(zb_field));
zb_layout l;
zb_status st = zb_layout_parse(s, n, 0, 0, fields, nf, &l, &pos);
SEXP out;
if (st) return Rf_mkString(zb_status_string(st));
nrec = len / l.size;
out = PROTECT(Rf_allocVector(VECSXP, l.nfields));
for (j = 0; j < l.nfields; j++) {
const zb_field *f = &l.fields[j];
size_t bad = 0, i;
SEXP col = Rf_allocVector(REALSXP, (R_xlen_t)(nrec * f->count));
SET_VECTOR_ELT(out, j, col);
if (zb_unpack_f64(RAW(x), nrec, l.size, f, REAL(col)) == ZB_OK) continue;
if (zb_unpack_f64x(RAW(x), nrec, l.size, f, REAL(col), &bad) == ZB_OK) continue;
{
int32_t *tmp = (int32_t *)R_alloc(nrec * f->count + 1, sizeof(int32_t));
if (zb_unpack_i32(RAW(x), nrec, l.size, f, tmp, 1, &bad) == ZB_OK) {
for (i = 0; i < nrec * f->count; i++) REAL(col)[i] = tmp[i];
continue;
}
}
SET_VECTOR_ELT(out, j, R_NilValue); /* b, s, x: not numbers */
}
UNPROTECT(1);
return out;
}Writing: the buffer, and buffers owned by R
A zb_buf is a pointer, a length, a capacity, a hard cap
max (0 for none), flags, and a release
function with the owner it frees: the shape of the Arrow C
data interface. zb_buf_alloc() makes a growable buffer,
zb_buf_borrow() a window onto your own memory (writable up
to its length, never grown), and zb_buf_release() frees
whatever needs freeing. Growth doubles to 64 MiB and grows by half
after, never past max: an append that would is
ZB_ERR_LIMIT, sets ZB_BUF_HIT_LIMIT, and
changes nothing. There is no bare size arithmetic: sums and products
that would overflow are ZB_ERR_MEMORY.
Ownership is a flag, never the identity of release:
every translation unit has its own copy of a header-only function, so
comparing function pointers across units is meaningless.
In an R package, heap memory that must survive a longjmp from
Rf_error() or R_CheckUserInterrupt() must be
owned by R before the first call that can jump.
zb_r_buf_new() creates the external pointer and registers
its finalizer before the buffer exists; zb_r_buf_free()
releases it early, and the finalizer then does nothing:
/* Translation unit 3: a buffer owned by R, through <zubin-r.h>. */
#include <zubin-r.h>
#include "zubintest.h"
/* Appends the doubles in `values` as f32be, then borrows the raw vector
`tail` and appends its bytes, and returns the buffer's bytes. The buffer is
owned by an external pointer from before it exists, so the error below
cannot leak it. */
SEXP zt_build(SEXP values, SEXP tail)
{
zb_status st;
zb_buf view, *b;
R_xlen_t i;
SEXP out, ptr = PROTECT(zb_r_buf_new(16, 1 << 20, &st));
if (st) Rf_error("zt_build: %s", zb_status_string(st));
b = zb_r_buf_get(ptr);
for (i = 0; i < XLENGTH(values); i++) {
if (zb_put_f32be(b, (float)REAL(values)[i])) Rf_error("zt_build: put failed");
}
zb_r_buf_borrow(&view, tail);
if (zb_put_bytes(b, view.data, view.len)) Rf_error("zt_build: the cap was reached");
out = PROTECT(zb_r_buf_to_raw(b));
zb_r_buf_free(ptr);
UNPROTECT(2);
return out;
}zb_r_buf_get() allocates nothing, so nothing needs
protecting around it, and returns NULL for an external
pointer that is not one of these buffers or has been freed (one restored
from a saved session, say).