How miniextendr R packages pull headers from other R packages (anything listed in LinkingTo:) into the C-shim compilation step.

🔗Why it exists

R packages can expose a C API: header files in inst/include/ plus R_RegisterCCallable()-registered function pointers. Examples:

  • cli/progress.h (progress bars)
  • Matrix/Matrix.h (sparse matrices)
  • vctrs/vctrs.h (record vectors)
  • arrow/ArrowSchema.h (Arrow C Data Interface)

A miniextendr package that wants to call one of these APIs from Rust typically does so via a tiny C shim (often generated by bindgen --wrap-static-fns) that wraps the static inline functions in the foreign header, so Rust can link against ordinary extern symbols.

That shim is a .c file in src/. R’s build system will compile every .c in src/ automatically, but only with the compile flags we give it via PKG_CPPFLAGS. R has no way to know that cli_progress_shim.c needs -I<path-to-cli>/include on the command line.

NATIVE_PKG_CPPFLAGS is the autoconf substitution variable that carries those -I flags from configure time down into Makevars.

🔗The wiring

Two files must agree. Breaking either one breaks the build.

🔗1. configure.ac: discover the include paths

dnl ---- Native R package include paths (for bindgen C shim compilation) ----
dnl Packages listed in LinkingTo: need their include/ paths for compiling
dnl the C shim files generated by bindgen (static inline wrapper functions).
NATIVE_PKG_CPPFLAGS=""

dnl cli - progress bar C API
CLI_INCLUDE=$("${R_HOME}/bin/Rscript" -e "cat(system.file('include', package='cli'))")
if test -n "$CLI_INCLUDE" && test -d "$CLI_INCLUDE"; then
  NATIVE_PKG_CPPFLAGS="$NATIVE_PKG_CPPFLAGS -I$CLI_INCLUDE"
  AC_MSG_NOTICE([cli include: $CLI_INCLUDE])
else
  AC_MSG_WARN([cli package not found - native CLI bindings will not compile])
fi
AC_SUBST([NATIVE_PKG_CPPFLAGS])

CLI_INCLUDE is just a local shell variable. Its name is descriptive, not magic. For each foreign package listed in LinkingTo: you add one Rscript -e "cat(system.file('include', package='<name>'))" block and append its result to NATIVE_PKG_CPPFLAGS. Skip if the package is missing. You still want configure to succeed (the user may not need the C API).

AC_SUBST([NATIVE_PKG_CPPFLAGS]) is what exposes the variable to Makevars.in as @NATIVE_PKG_CPPFLAGS@.

🔗2. Makevars.in: inject into the C compile

NATIVE_PKG_CPPFLAGS = @NATIVE_PKG_CPPFLAGS@

PKG_CPPFLAGS = $(NATIVE_PKG_CPPFLAGS)

R CMD INSTALL compiles every .c in src/ with $(CC) $(CPPFLAGS) $(CPICFLAGS) $(PKG_CPPFLAGS) ..., adding our discovered include paths to the preprocessor search.

See R_BUILD_SYSTEM.md for how PKG_CPPFLAGS fits into R’s overall Makefile include chain, and NATIVE_R_PACKAGES.md for the full recipe (including the OBJECTS → cargo rustc -C link-arg= link step that makes the shim’s symbols visible to the Rust crate).

🔗The failure mode

If Makevars.in references @NATIVE_PKG_CPPFLAGS@ but configure.ac forgot the matching AC_SUBST, autoconf leaves the literal token in the generated Makevars:

clang -arch arm64 ... -DNDEBUG @NATIVE_PKG_CPPFLAGS@ -I'/.../cli/include' ...
clang: error: no such file or directory: '@NATIVE_PKG_CPPFLAGS@'
make: *** [cli_progress_shim.o] Error 1

autoconf only substitutes @VAR@ tokens whose names appear in AC_SUBST(...) - any others pass through as-is. Since @NATIVE_PKG_CPPFLAGS@ looks like a filename to the compiler, you get this specific error.

Fix: either add the AC_SUBST([NATIVE_PKG_CPPFLAGS]) block to configure.ac, or - if the package has no C shims - drop the @NATIVE_PKG_CPPFLAGS@ line from Makevars.in.

🔗Why the split name

The framework uses the name NATIVE_PKG_CPPFLAGS rather than putting the -I flags directly into PKG_CPPFLAGS so that downstream users can still add their own PKG_CPPFLAGS additions in Makevars.in without clobbering the framework-managed include paths:

NATIVE_PKG_CPPFLAGS = @NATIVE_PKG_CPPFLAGS@

PKG_CPPFLAGS = $(NATIVE_PKG_CPPFLAGS) -DMY_FEATURE=1

Think of NATIVE_PKG_CPPFLAGS as “flags the configure step discovered automatically for LinkingTo: packages” and PKG_CPPFLAGS as “the final set of C preprocessor flags that reach cc.”

🔗Template sync trap

minirextendr’s Makevars.in template ships with @NATIVE_PKG_CPPFLAGS@ already wired in - scaffolded packages get it for free. But upgrade_miniextendr_package() does not rewrite configure.ac by default (configure_ac = FALSE) because users often customise it with feature flags.

So if you upgrade an older package whose configure.ac predates the NATIVE_PKG_CPPFLAGS block, the new Makevars.in will reference a substitution that configure.ac doesn’t define. This showed up as a regression in A2-ai/dvs2#126 when the upgrade between A2-ai/dvs2#123 refreshed Makevars.in without the matching configure.ac edit.

If you customise configure.ac and keep the stock Makevars.in, you are responsible for keeping the AC_SUBST([NATIVE_PKG_CPPFLAGS]) block (or deleting the @NATIVE_PKG_CPPFLAGS@ reference on the Makevars side).