--- title: "FFI Types" output: rmarkdown::html_vignette vignette: > %\VignetteIndexEntry{FFI Types} %\VignetteEngine{knitr::rmarkdown} %\VignetteEncoding{UTF-8} --- ```{r, include = FALSE} knitr::opts_chunk$set( collapse = TRUE, comment = "#>" ) tcc_bind <- Rtinycc::tcc_bind tcc_compile <- Rtinycc::tcc_compile tcc_ffi <- Rtinycc::tcc_ffi tcc_free <- Rtinycc::tcc_free tcc_malloc <- Rtinycc::tcc_malloc tcc_source <- Rtinycc::tcc_source ``` This article describes the FFI type names implemented in `Rtinycc`. The mapping below is taken from the package type table and wrapper-generation code, not from a separate design document. ## Scalar Types The core scalar types are: - signed integers: `i8`, `i16`, `i32`, `i64` - unsigned integers: `u8`, `u16`, `u32`, `u64` - floating point: `f32`, `f64` - logical: `bool` - string: `cstring` - opaque pointer: `ptr` - direct R object: `sexp` - return-only sentinel: `void` `Rtinycc` converts R values to these C types inside generated wrappers, but the R-side scalar carriers depend on what R can represent directly: - `i8`, `i16`, `i32`, `u8`, and `u16` are mediated through R integer scalars - `u32`, `i64`, `u64`, `f32`, and `f64` are mediated through R numeric (`double`) coercion and boxing - `bool` uses R logical - `cstring` uses an R character scalar So the FFI names are C-side type names, not promises that R has a matching native scalar type for every width. Integer-like paths are validated, not just cast silently, and `i64` / `u64` are only exact up to `2^53` on the R side. ## A Minimal Example ```{r} ffi <- tcc_ffi() |> tcc_source( " int add_i32(int a, int b) { return a + b; } double mul_f64(double x, double y) { return x * y; } int negate_bool(_Bool x) { return !x; } " ) |> tcc_bind( add_i32 = list(args = list("i32", "i32"), returns = "i32"), mul_f64 = list(args = list("f64", "f64"), returns = "f64"), negate_bool = list(args = list("bool"), returns = "bool") ) |> tcc_compile() ffi$add_i32(2L, 3L) ffi$mul_f64(2, 4) ffi$negate_bool(TRUE) ``` ## String and Pointer Types `cstring` and `ptr` are intentionally different: - `cstring` converts an R character scalar into a C `char *` view for the call - `ptr` passes an external pointer address through unchanged ```{r} ffi_str <- tcc_ffi() |> tcc_source( " const char* echo_cstring(const char* s) { return s; } void* echo_ptr(void* p) { return p; } " ) |> tcc_bind( echo_cstring = list(args = list("cstring"), returns = "cstring"), echo_ptr = list(args = list("ptr"), returns = "ptr") ) |> tcc_compile() ptr <- tcc_malloc(8) ffi_str$echo_cstring("hello") inherits(ffi_str$echo_ptr(ptr), "externalptr") tcc_free(ptr) ``` The important semantic difference is discussed in the boundary-semantics article: returned `cstring` values are copied into R strings, while returned `ptr` values stay as raw addresses. ## Array Types The implemented array input types are: - `raw` - `integer_array` - `numeric_array` - `logical_array` - `character_array` - `cstring_array` The first four map directly onto R vectors. `character_array` passes the underlying `CHARSXP` cells as a read-only `SEXP *`, not a `char **`. Use `cstring_array` when the C side expects a temporary `const char **`. ```{r} ffi_arr <- tcc_ffi() |> tcc_source( " int first_int(int* x) { return x[0]; } double second_num(double* x) { return x[1]; } " ) |> tcc_bind( first_int = list(args = list("integer_array"), returns = "i32"), second_num = list(args = list("numeric_array"), returns = "f64") ) |> tcc_compile() ffi_arr$first_int(as.integer(c(10, 20, 30))) ffi_arr$second_num(c(1.5, 2.5)) ``` ## Direct R Objects with `sexp` `sexp` passes the R object through the wrapper without conversion. ```{r} ffi_sexp <- tcc_ffi() |> tcc_source( " #include SEXP id_sexp(SEXP x) { return x; } " ) |> tcc_bind( id_sexp = list(args = list("sexp"), returns = "sexp") ) |> tcc_compile() ffi_sexp$id_sexp(list(a = 1, b = 2)) ``` This is the lowest-friction way to cross the boundary when you want to work in terms of the R C API directly rather than the stricter scalar/vector FFI types.