---
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:<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_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:<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`, 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.
