The hardware and bandwidth for this mirror is donated by dogado GmbH, the Webhosting and Full Service-Cloud Provider. Check out our Wordpress Tutorial.
If you wish to report a bug, or if you are interested in having us mirror your free-software or open-source project, please feel free to contact us at mirror[@]dogado.de.
This article is about what actually crosses the R/C boundary in
Rtinycc:
The statements below are based on the implemented wrapper generator and runtime helpers.
Scalar inputs are converted at the boundary. For example:
i8, i16, i32,
u8, u16 use integer coercion plus range
checksi64, u32, u64 use numeric
coercion plus integer-value checksbool rejects NAf32 and f64 are read from R numericsSo scalar arguments are not zero-copy views into R objects. They become C scalars inside the wrapper.
The array input types:
rawinteger_arraynumeric_arraylogical_arrayare passed as writable direct pointers into the underlying R vector storage. For ordinary already-materialized vectors, no extra buffer is allocated by the wrapper.
That means:
This is the main zero-copy part of the FFI boundary. One important
R-specific caveat is ALTREP: if an input vector is an ALTREP object,
asking R for a writable C data pointer can materialize that vector.
Checking ALTREP(x) in generated C could choose a different
policy, but using *_GET_REGION() into a temporary buffer
would be a different contract for these mutable types: it would need
copy-back behavior and would not automatically preserve pointer aliasing
when the same R vector is passed to multiple C arguments. Read-only
ALTREP-friendly array types would be a cleaner separate API.
tcc_call_symbol() Uses .C()-Style
Copy-In/Copy-OutWhen tcc_call_symbol() is called with extra arguments,
it follows the argument-type mapping of R’s .C() interface
rather than the zero-copy array contract used by tcc_ffi()
wrappers.
For atomic vectors and character vectors, Rtinycc copies inputs into
guarded mutable call storage and returns a list containing the
copied-back values. This means C mutations are visible in the returned
list, not by mutating the original R objects. Numeric vectors with
attr(x, "Csingle") are converted through a temporary
float * buffer, matching R’s legacy .C()
convention.
Lists and other non-atomic R objects follow the legacy
.C() read-only paths: lists are exposed as
SEXP *, while functions, environments, and other R objects
are exposed as SEXP. These values are borrowed only for the
duration of the call. C code must not mutate them through
tcc_call_symbol(), and if it stores a SEXP
beyond the call it must preserve and later release it with the R C API.
For ordinary lists, Rtinycc can pass the existing SEXP *
element storage read-only; for ALTREP or otherwise opaque list-like
vectors, it rebuilds a temporary call-lifetime SEXP * view
with VECTOR_ELT() so the path remains ALTREP-aware without
forcing writable data access.
Unlike R’s optional options(CBoundsCheck = TRUE),
tcc_call_symbol() checks guard bytes around its copied
atomic and character buffers by default. This can catch simple
underwrites and overwrites, but it is not a sandbox: far out-of-bounds
native writes are still bugs in the called C code. For character
arguments, C may edit the contents of each copied string buffer in
place, but it must not replace the char * elements in the
char ** array.
cstring_array Is Rebuilt Per Callcstring_array is different. The wrapper allocates a
temporary const char ** with R_alloc() and
fills it by translating each R string element.
So:
Array returns are always copied into a newly allocated R vector. The
wrapper uses the declared length_arg to size the R result,
then memcpy() copies the returned C buffer into that
vector.
If free = TRUE, the wrapper also frees the original
returned buffer after the copy.
So array returns are not borrowed views into C memory.
cstring Values Are CopiedFor cstring returns, the wrapper creates an R string
with mkString() when the returned pointer is non-NULL.
That means the resulting R value is a copy in R-managed memory, not a retained external pointer to the original C string.
ptr Values Stay as PointersFor ptr returns, the wrapper constructs an external
pointer around the raw address.
That means:
The same distinction matters for globals and struct fields.
sexp Passes Through Directlysexp is the most direct boundary mode:
sexp arguments are passed through as
SEXPsexp values are returned directlyThis is useful when you want the R C API contract rather than the stricter FFI conversion layer.
At the helper level:
tcc_malloc() and tcc_cstring() create
owned external pointerstcc_data_ptr() and tcc_read_ptr() return
borrowed external pointersstruct_outer_get_child() return borrowed nested views into
the owning struct storageUse tcc_ptr_is_owned() when you need to distinguish
these cases in R code.
Bitfield helpers behave like scalar getter/setter helpers at the R boundary, but that does not make them ordinary addressable fields.
In particular:
tcc_field_addr() and tcc_container_of()
reject bitfield membersSo bitfields are intentionally excluded from the borrowed-address helper model.
Compiled tcc_compiled objects store enough recipe
information to recompile after serialize() /
unserialize() or readRDS().
Raw pointers and raw tcc_state objects do not gain that
behavior. After serialization they are just dead addresses or invalid
states, not auto-reconstructed resources. The same applies to callback
tokens, struct/union external pointers, and helper allocations from
tcc_malloc() or tcc_cstring(): they do not
serialize as live native resources.
These binaries (installable software) and packages are in development.
They may not be fully stable and should be used with caution. We make no claims about them.
Health stats visible at Monitor.