FFI Types

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:

Rtinycc converts R values to these C types inside generated wrappers, but the R-side scalar carriers depend on what R can represent directly:

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

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] FALSE

String and Pointer Types

cstring and ptr are intentionally different:

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")
#> [1] "hello"
inherits(ffi_str$echo_ptr(ptr), "externalptr")
#> [1] TRUE

tcc_free(ptr)
#> NULL

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:

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 **.

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.5

Direct R Objects with sexp

sexp 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] 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.