Variant 1 — C++ Shared Path (dr_*_cpp)

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

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

## 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:

- call any function from `<Rinternals.h>` (`Rf_*`, `PROTECT`, `R_alloc`, …)
- allocate or read any `SEXP`
- call back into the R interpreter (no `Rf_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.

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

- Pair `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.
- The bench shows ~240 k rps for a trivial cpp-shared handler vs ~116 k rps for the same response shape from an R handler. Real handlers with non-trivial work move the numbers, but the bridge cost is what differs.

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