Variant 1 — C++ Shared Path (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 handler ABI

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);

drogonR owns every input pointer; they are valid for the duration of the call and must not be retained.

The handler writes:

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


A complete example

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


Threading rule (critical)

Native handlers run on Drogon’s worker thread pool, not on the R main thread. They MUST NOT:

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.


Memory ownership cheat-sheet

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.


Where this fits

For the plumber drop-in, see vignette("mode-plumber-shim", package = "drogonR"). For R-side handlers, see vignette("mode-native", package = "drogonR").