# FFI Objects, Structs, and Callbacks

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

    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)
    #> NULL

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

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