--- title: "FFI Objects, Structs, and Callbacks" output: rmarkdown::html_vignette vignette: > %\VignetteIndexEntry{FFI Objects, Structs, and Callbacks} %\VignetteEngine{knitr::rmarkdown} %\VignetteEncoding{UTF-8} --- ```{r, include = FALSE} knitr::opts_chunk$set( collapse = TRUE, comment = "#>" ) tcc_bind <- Rtinycc::tcc_bind tcc_callback <- Rtinycc::tcc_callback tcc_callback_async_drain <- Rtinycc::tcc_callback_async_drain tcc_callback_close <- Rtinycc::tcc_callback_close tcc_callback_ptr <- Rtinycc::tcc_callback_ptr tcc_compile <- Rtinycc::tcc_compile tcc_ffi <- Rtinycc::tcc_ffi tcc_source <- Rtinycc::tcc_source tcc_struct <- Rtinycc::tcc_struct ``` This 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`. ```{r} 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) ffi_struct$struct_point_free(pt) ``` This 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:`. 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_callback` object owns the registered R function - `tcc_callback_ptr()` returns the user-data handle that C trampolines expect ```{r} 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) 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:` 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`, and `u64` async arguments and returns are marshalled through R numeric (`double`), so only exact integer values up to `2^53` are 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.