# drogonR — Three Ways to Serve HTTP from R

drogonR wraps the Drogon C++ HTTP framework and exposes it to R. The same server can be driven through three different APIs depending on how much R you want in the request hot path:

1. `dr_*_cpp()` — C++ shared path. Handlers are pure C functions in another R package, resolved via `R_GetCCallable()` and called on Drogon’s worker threads. R is not in the hot path. Intended for inference packages (ggmlR, llamaR, sd2R) that already do their work in C++.
2. `dr_app()` + `dr_get()` / `dr_post()` / … — drogonR native. Handlers are R functions; the bridge marshals each request onto the main R thread, runs the closure, and ships the response back. Full control over routes, middleware, response shape. Recommended for new APIs written in R.
3. `drogonR::pr_run()` — plumber drop-in. Existing `plumber::pr_run(pr)` becomes `drogonR::pr_run(pr)` with no other changes. The shim translates the plumber router into drogonR routes and serves them via `dr_serve()`. Recommended when you have a plumber codebase and want a faster runtime without rewriting it.

The variants share the same Drogon server underneath; only the registration path differs.

## Choosing a variant

| cpp-shared (`dr_*_cpp`) | native (`dr_get`) | plumber shim (`pr_run`) | plumber (baseline)
--- | --- | --- | --- | ---
Handler language | C / C++ | R | R (plumber convention) | R
Calls into R per request | 0 | 1 | 1 | 1
Code change vs plumber | rewrite handlers in C | rewrite using `dr_app()` | one line (`pr_run`) | —
Best for | inference / hot loops | new R-side APIs | existing plumber apps | dev / one-offs

The cpp-shared variant is the only one that bypasses R entirely on the request path. The native and shim variants both run an R closure per request — the difference between them is the calling convention (drogonR-native gets a `req` object; the shim emulates plumber’s positional / named-arg matching).

## Performance snapshot

`wrk -t4 -c50 -d30s` against four servers running the same two routes on `localhost`. Workload is intentionally trivial — `/ping` returns a fixed `{"ok":true}`, `/ping-text` returns `"ok"` — so the numbers measure the framework overhead, not the handler cost. See `tools/bench/run.sh` for the harness.

Variant | `/ping` (JSON) | `/ping-text` (plain)
--- | --- | ---
drogonR cpp-shared | 239 428 rps, 200 µs | 234 753 rps, 202 µs
drogonR native | 116 159 rps, 822 µs | 218 163 rps, 252 µs
drogonR plumber-shim | 94 400 rps, 591 µs | 99 276 rps, 583 µs
plumber (baseline) | 1 078 rps, 44.5 ms | 1 069 rps, 44.9 ms

(rps = requests per second, single-host loopback; latency is the wrk average.) Two things to read out of this:

- The cpp-shared path leaves R entirely — its throughput is bounded by Drogon and the kernel, not by anything in this package.
- Even when an R handler runs per request, the native path is ~100× plumber on JSON and ~200× on plain text, because requests are marshaled onto the main R thread once per dispatch loop instead of per request, and the response is written from C++ without going back through plumber’s R-level filter chain.

The shim is slower than native for the same workload — it pays the cost of plumber’s argument-matching convention (path / query / body lookup, default-serialiser dispatch) on every call. It’s still well above plumber itself because the I/O loop is C++.

## A minimal example of each

Variant 1 — cpp-shared (in your inference package):

    // In yourPackage/src/handlers.c
    #include <drogonR.h>
    #include <R_ext/Rdynload.h>

    static int h_predict(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) {
        /* run inference, fill out_body via malloc(), set status, ctype */
        return 0;
    }

    void R_init_yourPackage(DllInfo *dll) {
        R_RegisterCCallable("yourPackage", "predict", (DL_FUNC) h_predict);
    }

    # In your serving script
    library(drogonR)
    app <- dr_app() |>
      dr_post_cpp("/predict", "yourPackage", "predict")
    dr_serve(app, port = 8080L)

Variant 2 — drogonR native:

    library(drogonR)
    app <- dr_app() |>
      dr_get("/users/:id", function(req) {
        dr_json(list(id = req$params[["id"]], ok = TRUE))
      })
    dr_serve(app, port = 8080L)

Variant 3 — plumber shim:

    # Existing plumber.R, unchanged:
    library(plumber)
    pr <- pr() |>
      pr_get("/users/<id>", function(id) list(id = id, ok = TRUE))

    # Only this line changes — drogonR::, not plumber::
    drogonR::pr_run(pr, port = 8080L)

## Where to go next

- `vignette("mode-cpp-shared", package = "drogonR")` — full ABI for variant 1, including memory ownership and the threading rule.
- `vignette("mode-native", package = "drogonR")` — `req`/`res` shape, response helpers, middleware, lifecycle of `dr_serve()`.
- `vignette("mode-plumber-shim", package = "drogonR")` — exact subset of plumber the shim implements, what triggers an explicit error, what’s silently accepted-and-ignored.