--- title: "Variant 1 — C++ Shared Path (`dr_*_cpp`)" output: rmarkdown::html_vignette vignette: > %\VignetteIndexEntry{Variant 1 — C++ Shared Path} %\VignetteEngine{knitr::rmarkdown} %\VignetteEncoding{UTF-8} --- ```{r setup, include=FALSE} knitr::opts_chunk$set(eval = FALSE, comment = "#>") ``` 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: ```c #include 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] = ""`, `path_params[1] = ""`). * `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. ```c #include #include #include #include #include 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. ```r 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 `` (`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")`.