Some C APIs do not just call functions immediately. They store a
function pointer and call it later. ccallback() creates a C
function pointer that enters R when the foreign code invokes it.
Use this article when a C API asks for an int (*)(...),
event handler, comparison function, completion hook, or any other
callback pointer. The main task is not just creating the pointer; it is
also keeping the R function and its state alive for exactly as long as C
can call it.
A minimal callback
This callback has the C shape:
The callback signature follows the same call-signature format used by
dyncall().
Recursive callback call
A callback pointer is just another foreign function pointer. It can be passed back into a call if the signature expects a pointer.
factorial_callback <- function(x, fun) {
if (x < 2L) {
1L
} else {
x * dyncall(fun, "ip)i", x - 1L, fun)
}
}
factorial_ptr <- ccallback("ip)i", factorial_callback)
dyncall(factorial_ptr, "ip)i", 6L, factorial_ptr)
#> [1] 720This example is small, but it shows the same lifetime rule as larger
APIs: keep factorial_ptr reachable in R for as long as C
may call it.
Sorting with C qsort
qsort() is a classic C API that accepts a comparison
callback:
The comparison callback receives pointers to two elements. The R
callback reads the double values with
unpack().
libc_names <- c("msvcrt", "c", "c.so.6")
libc <- new.env(parent = globalenv())
dynbind(libc_names, "qsort(pLLp)v;", envir = libc)
#> dynbind report
#> library: libc.so.6
#> unresolved symbols: 0
compare_double <- function(px, py) {
x <- unpack(px, 0L, "d")
y <- unpack(py, 0L, "d")
if (x < y) {
-1L
} else if (x > y) {
1L
} else {
0L
}
}
set.seed(42)
x <- rnorm(12L)
expected <- sort(x)
compare_ptr <- ccallback("pp)i", compare_double)
libc$qsort(x, length(x), 8L, compare_ptr)
#> NULL
all.equal(x, expected)
#> [1] TRUEThe callback object must remain assigned to compare_ptr
until qsort() returns. For APIs that store callbacks beyond
the registration call, store the callback object somewhere with the same
lifetime as the foreign registration.
Aggregate values in callbacks
Callbacks can receive and return registered aggregate types by value
with the same <Type> syntax used by
dyncall().
cstruct("Point{dd}x y;")
point <- cdata(Point)
point$x <- 1.5
point$y <- 2.25
point_sum <- ccallback("<Point>)d", function(p) {
p$x + p$y
})
dyncall(point_sum, "<Point>)d", point)
#> [1] 3.75Aggregate arguments arrive as raw-backed struct objects,
so field access uses the same $ helpers as ordinary
cdata() objects. Aggregate return callbacks must return a
raw-backed object of the exact registered type.
Immediate and stored callbacks
Callback lifetime depends on whether C calls the pointer during the current function call or stores it for later.
| C API pattern | Example | R-side lifetime rule |
|---|---|---|
| Immediate callback | qsort() |
keep the callback object alive until the call returns |
| Stored callback | GUI, audio, event, or async APIs | keep the callback object alive until the foreign registration is removed |
qsort() is comparatively safe because it finishes before
control returns to R. Event loops are riskier because C may call the
pointer long after the registration function has returned.
For stored callbacks, make the owner explicit. A convenient rule is: the R object that represents the foreign registration should also keep the callback pointer and any R state used by the callback.
registration <- new.env(parent = emptyenv())
registration$handle <- foreign_handle
registration$state <- list(count = 0L)
registration$callback <- ccallback("ip)v", function(code, userdata) {
registration$state$count <- registration$state$count + 1L
NULL
})
foreign_register_callback(registration$handle, registration$callback)When the C API provides an unregister or destroy function, call it before dropping the last R reference to the owner object.
Error and lifetime rules
Callbacks cross from C into R, so mistakes are more severe than ordinary R function errors.
- The callback signature must match the exact C callback type.
- Keep the callback object alive in R while foreign code may call it.
- Do not let C call a callback after its R state has been cleaned up.
- Avoid throwing errors through foreign event loops. Handle errors inside the callback and return a C-level failure value when the API supports one.
- For aggregate callback returns, return a raw-backed
structobject of the exact expected type and size. A mismatch disables the callback. - Aggregate callbacks require an implemented 64-bit x86 or ARM64 dyncallback backend; unsupported platforms fail when the callback is created.
When a callback does throw an R error, rdyncall disables that callback pointer for later invocations. The status helpers make that state inspectable:
checked_callback <- ccallback("i)i", function(x) {
if (x < 0L) {
stop("negative values are not supported")
}
x
})
dyncall(checked_callback, "i)i", -1L)
callback_is_active(checked_callback)
callback_last_error(checked_callback)
callback_status(checked_callback)Create a new callback when you need to recover after an error. The status helpers are intentionally read-only and do not re-enable disabled callbacks.
Use tryCatch() inside callbacks that may run under
foreign control. Store the R error somewhere inspectable, and return a
value that the C API documents as a failure.
last_error <- NULL
compare_ptr <- ccallback("pp)i", function(px, py) {
tryCatch(
compare_double(px, py),
error = function(err) {
last_error <<- err
0L
}
)
})This pattern does not make all foreign event loops safe, but it prevents an R condition from being the mechanism that crosses the C callback boundary.
A safe registration pattern
For APIs that register callbacks, keep a small R object that owns both the foreign handle and the callback pointer.
make_registration <- function(handle) {
state <- new.env(parent = emptyenv())
state$handle <- handle
state$callback <- ccallback("ip)v", function(code, userdata) {
# Handle the event and return a C-compatible value.
invisible(NULL)
})
foreign_register_callback(handle, state$callback)
state
}The important part is not the exact wrapper shape; it is that the callback pointer remains reachable for at least as long as the C library can use it. For a real API, pair this with the corresponding unregister or destroy function and clear the R reference only after C no longer holds the pointer.
Next steps
- Use signatures to translate callback prototypes and callback pointer arguments.
- Use structs, unions, and memory when callback arguments are raw pointers to C data.
- Use FFI safety before registering callbacks that may be called asynchronously or from foreign event loops.
- Use troubleshooting when a callback crashes, returns nonsense, or is called after R state has been cleaned up.