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.
The core scalar types are:
i8, i16,
i32, i64u8, u16,
u32, u64f32, f64boolcstringptrsexpvoidRtinycc 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
scalarsu32, i64, u64,
f32, and f64 are mediated through R numeric
(double) coercion and boxingbool uses R logicalcstring uses an R character scalarSo 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.
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)
#> [1] 5
ffi$mul_f64(2, 4)
#> [1] 8
ffi$negate_bool(TRUE)
#> [1] FALSEcstring and ptr are intentionally
different:
cstring converts an R character scalar into a C
char * view for the callptr passes an external pointer address through
unchangedffi_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")
#> [1] "hello"
inherits(ffi_str$echo_ptr(ptr), "externalptr")
#> [1] TRUE
tcc_free(ptr)
#> NULLThe 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.
The implemented array input types are:
rawinteger_arraynumeric_arraylogical_arraycharacter_arraycstring_arrayThe 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 **.
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)))
#> [1] 10
ffi_arr$second_num(c(1.5, 2.5))
#> [1] 2.5sexpsexp passes the R object through the wrapper without
conversion.
ffi_sexp <- tcc_ffi() |>
tcc_source(
"
#include <Rinternals.h>
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))
#> $a
#> [1] 1
#>
#> $b
#> [1] 2This 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.