FFI Objects, Structs, and Callbacks
Source:vignettes/ffi-objects-and-callbacks.Rmd
ffi-objects-and-callbacks.RmdThis vignette covers two patterns that come up quickly in real bindings:
- exposing C structs through generated helper methods
- passing R functions into compiled code as callbacks
Working with Struct Helpers
Struct helpers are generated from a declarative description. In the
example below, Rtinycc creates allocation, field getter,
field setter, and free helpers for a simple
struct point.
ffi_struct <- tcc_ffi() |>
tcc_source(
"
struct point {
double x;
double y;
};
double point_norm2(struct point* p) {
return p->x * p->x + p->y * p->y;
}
"
) |>
tcc_struct("point", accessors = c(x = "f64", y = "f64")) |>
tcc_bind(
point_norm2 = list(args = list("ptr"), returns = "f64")
) |>
tcc_compile()
pt <- ffi_struct$struct_point_new()
pt <- ffi_struct$struct_point_set_x(pt, 3)
pt <- ffi_struct$struct_point_set_y(pt, 4)
ffi_struct$point_norm2(pt)
#> [1] 25
ffi_struct$struct_point_free(pt)
#> NULLThis keeps the C layout explicit while still giving you a usable R-facing surface.
Named nested struct fields can also be modeled directly with
struct:<name>. Those getters return borrowed nested
views and setters copy bytes from a source struct object of the matching
nested type.
Registering Callbacks
Callbacks let compiled C code invoke an R function through a generated trampoline. The callback object and the callback pointer play different roles:
- the
tcc_callbackobject owns the registered R function -
tcc_callback_ptr()returns the user-data handle that C trampolines expect
cb <- tcc_callback(
function(x) x * 2,
signature = "double (*)(double)"
)
cb_ptr <- tcc_callback_ptr(cb)
ffi_cb <- tcc_ffi() |>
tcc_source(
"
double apply_cb(double (*cb)(void* ctx, double), void* ctx, double x) {
return cb(ctx, x);
}
"
) |>
tcc_bind(
apply_cb = list(
args = list("callback:double(double)", "ptr", "f64"),
returns = "f64"
)
) |>
tcc_compile()
ffi_cb$apply_cb(cb, cb_ptr, 5)
#> [1] 10
tcc_callback_close(cb)tcc_callback_close() is recommended when you want
deterministic invalidation and prompt release of the preserved R
function. If you simply drop all references, finalizers will still clean
up the callback eventually.
Async Callback Caveats
callback_async:<signature> is the safe path for
worker-thread callbacks, but its contract is narrower than the
synchronous trampoline path:
- the callback registry currently holds at most 256 live callbacks at once
-
i64,u32, andu64async arguments and returns are marshalled through R numeric (double), so only exact integer values up to2^53are exact - queued async callbacks run when the main thread services R’s event
loop; in tests or tight compute loops, call
tcc_callback_async_drain()explicitly
Soundness Notes
The callback contract is deliberately explicit:
- the callback signature must match what the C code expects
- pointer arguments are passed through as external pointers
- callback errors are converted into warnings plus a type-appropriate default return value
That explicitness is part of what keeps Rtinycc
predictable as a systems interface rather than a partial compiler
front-end.