--- title: "FFI Boundary Semantics" output: rmarkdown::html_vignette vignette: > %\VignetteIndexEntry{FFI Boundary Semantics} %\VignetteEngine{knitr::rmarkdown} %\VignetteEncoding{UTF-8} --- This article is about what actually crosses the R/C boundary in `Rtinycc`: - when values are copied - when they are borrowed - when they stay as raw addresses - when wrappers allocate temporary storage The statements below are based on the implemented wrapper generator and runtime helpers. ## Scalar Inputs Are Converted Scalar inputs are converted at the boundary. For example: - `i8`, `i16`, `i32`, `u8`, `u16` use integer coercion plus range checks - `i64`, `u32`, `u64` use numeric coercion plus integer-value checks - `bool` rejects `NA` - `f32` and `f64` are read from R numerics So scalar arguments are not zero-copy views into R objects. They become C scalars inside the wrapper. ## Vector Inputs Are Usually Borrowed The array input types: - `raw` - `integer_array` - `numeric_array` - `logical_array` are passed as direct pointers into the underlying R vector storage. That means: - no extra buffer is allocated by the wrapper - C sees the existing vector data - mutation from C writes into the same memory region This is the main zero-copy part of the FFI boundary. ## `cstring_array` Is Rebuilt Per Call `cstring_array` is different. The wrapper allocates a temporary `const char **` with `R_alloc()` and fills it by translating each R string element. So: - the pointer array itself is allocated for the call - each element points at translated string data - this is not the same as passing a pre-existing C array through unchanged ## Returned Arrays Are Copied into Fresh R Vectors 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. ## Returned `cstring` Values Are Copied For `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. ## Returned `ptr` Values Stay as Pointers For `ptr` returns, the wrapper constructs an external pointer around the raw address. That means: - no pointee copy is made - ownership is not implied - the pointer may dangle if the underlying C storage goes away The same distinction matters for globals and struct fields. ## `sexp` Passes Through Directly `sexp` is the most direct boundary mode: - input `sexp` arguments are passed through as `SEXP` - returned `sexp` values are returned directly This is useful when you want the R C API contract rather than the stricter FFI conversion layer. ## Owned vs Borrowed Helper Pointers At the helper level: - `tcc_malloc()` and `tcc_cstring()` create owned external pointers - `tcc_data_ptr()` and `tcc_read_ptr()` return borrowed external pointers - struct field address helpers and many raw pointer returns are borrowed views - named nested struct getters such as `struct_outer_get_child()` return borrowed nested views into the owning struct storage Use `tcc_ptr_is_owned()` when you need to distinguish these cases in R code. ## Bitfields Are Scalar Helpers, Not Addressable Views Bitfield helpers behave like scalar getter/setter helpers at the R boundary, but that does **not** make them ordinary addressable fields. In particular: - bitfield getters return copied scalar values - bitfield setters write scalar values back through the compiler-managed bitfield storage - `tcc_field_addr()` and `tcc_container_of()` reject bitfield members So bitfields are intentionally excluded from the borrowed-address helper model. ## Serialization Boundary 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.