The hardware and bandwidth for this mirror is donated by dogado GmbH, the Webhosting and Full Service-Cloud Provider. Check out our Wordpress Tutorial.
If you wish to report a bug, or if you are interested in having us mirror your free-software or open-source project, please feel free to contact us at mirror[@]dogado.de.

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

These binaries (installable software) and packages are in development.
They may not be fully stable and should be used with caution. We make no claims about them.
Health stats visible at Monitor.