dr_*_cpp)The dr_*_cpp() family registers a route whose handler is
a C function exported by another R package. Drogon’s
worker threads call the handler directly — the request never reaches the
R main thread, so nothing in the hot path acquires the R
interpreter.
This is the only variant where R is not in the loop. It’s intended for inference packages whose work is already in C/C++ (ggmlR, llamaR, sd2R, embedding/classifier packages) and that want to serve HTTP without paying the R round-trip per request.
For the request shape and overall picture see
vignette("drogonR", package = "drogonR").
The header lives in drogonR’s installed include directory:
$(R_HOME_DIR)/library/drogonR/include/drogonR.h
A package using it adds drogonR to LinkingTo: so R’s
build machinery puts that directory on the compiler’s -I
path:
Package: yourPackage
Imports: drogonR
LinkingTo: drogonR
The signature every dr_*_cpp() handler must match:
#include <drogonR.h>
typedef int (*drogonr_unary_handler_t)(
const char *body, size_t body_len,
const char *query,
const char *const *path_params, size_t path_params_n,
const char *const *headers, size_t headers_n,
char **out_body, size_t *out_len,
int *out_status,
char **out_content_type);body / body_len — request body bytes; not
NUL-terminated.query — raw query string (everything after
?), or NULL.path_params[i] — captured route parameters in the order
they appear in the path pattern ("/items/:id/sub/:slug"
gives path_params[0] = "<id>",
path_params[1] = "<slug>").headers[2*i] / headers[2*i+1] — flat
(name, value) pairs. Drogon lowercases header
names, so match against "x-trace", not
"X-Trace".drogonR owns every input pointer; they are valid for the duration of the call and must not be retained.
The handler writes:
*out_body — malloc()’d response body
(drogonR free()s it after sending). NULL is
allowed if *out_len == 0.*out_len — number of bytes in
*out_body.*out_status — HTTP status code.*out_content_type — optional malloc()’d
MIME string. If left at the initial NULL, drogonR sends
application/octet-stream.Return value: 0 on success, non-zero to signal failure
(drogonR sends a generic 500 and free()s
*out_body / *out_content_type if the handler
allocated them before bailing out).
This is the test backend drogonR uses internally
(inst/test-backend/drogonRtestbackend/src/backend.c),
trimmed to the two routes the bench uses.
#include <drogonR.h>
#include <R.h>
#include <R_ext/Rdynload.h>
#include <stdlib.h>
#include <string.h>
static char *dupbytes(const char *data, size_t n) {
char *out = (char*) malloc(n > 0 ? n : 1);
if (out && data && n > 0) memcpy(out, data, n);
return out;
}
static char *dupcstr(const char *s) {
size_t n = strlen(s);
char *out = (char*) malloc(n + 1);
if (out) memcpy(out, s, n + 1);
return out;
}
/* /ping — fixed JSON {"ok":true} */
static int h_ping_json(const char *body, size_t body_len,
const char *query,
const char *const *path, size_t path_n,
const char *const *hdrs, size_t hdrs_n,
char **out_body, size_t *out_len,
int *out_status, char **out_content_type) {
static const char k[] = "{\"ok\":true}";
*out_body = dupbytes(k, sizeof(k) - 1);
*out_len = sizeof(k) - 1;
*out_status = 200;
*out_content_type = dupcstr("application/json");
return 0;
}
/* /echo — echo the body back as text/plain */
static int h_echo(const char *body, size_t body_len,
const char *query,
const char *const *path, size_t path_n,
const char *const *hdrs, size_t hdrs_n,
char **out_body, size_t *out_len,
int *out_status, char **out_content_type) {
*out_body = dupbytes(body, body_len);
*out_len = body_len;
*out_status = 200;
*out_content_type = dupcstr("text/plain; charset=utf-8");
return 0;
}
void R_init_yourPackage(DllInfo *dll) {
R_RegisterCCallable("yourPackage", "ping", (DL_FUNC) h_ping_json);
R_RegisterCCallable("yourPackage", "echo", (DL_FUNC) h_echo);
R_useDynamicSymbols(dll, FALSE);
}R-side wiring — note that registration is eager:
drogonR resolves the symbol via R_GetCCallable() at
dr_*_cpp() time, so a typo surfaces immediately, not on the
first request.
library(drogonR)
app <- dr_app() |>
dr_get_cpp ("/ping", package = "yourPackage", callable = "ping") |>
dr_post_cpp("/echo", package = "yourPackage", callable = "echo")
dr_serve(app, port = 8080L, threads = 4L)The four registration helpers are dr_get_cpp,
dr_post_cpp, dr_put_cpp,
dr_delete_cpp. All four take
(app, path, package, callable).
Native handlers run on Drogon’s worker thread pool, not on the R main thread. They MUST NOT:
<Rinternals.h>
(Rf_*, PROTECT, R_alloc, …)SEXPRf_eval, no
R_tryCatch, …)R is single-threaded; doing any of the above from a worker thread is undefined behaviour, typically a crash you’ll see only under load.
Configuration that requires R (loading models, reading args, building
caches) belongs on the R side, before dr_serve() is called.
Pass the result to your C handlers through whatever your package already
uses internally — globals, an opaque pointer in
R_ExternalPtrAddr(), etc.
| Pointer | Allocated by | Freed by | Lifetime |
|---|---|---|---|
body, query |
drogonR | drogonR | duration of the call |
path_params[i] |
drogonR | drogonR | duration of the call |
headers[i] |
drogonR | drogonR | duration of the call |
*out_body |
handler (malloc) |
drogonR | until response sent |
*out_content_type |
handler (malloc) |
drogonR | until response sent |
If the handler returns non-zero, drogonR still free()s
any allocated out-pointers — so it is safe to allocate them before
discovering the failure path, no leak.
dr_*_cpp() with dr_get() /
dr_post() on the same app. The slow / configuration / admin
endpoints can stay in R, the hot inference endpoint goes through C.For the plumber drop-in, see
vignette("mode-plumber-shim", package = "drogonR"). For
R-side handlers, see
vignette("mode-native", package = "drogonR").