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. Upgrades compare the retained file with the current template and warn about any differences, including custom edits. Review those differences or run upgrade_miniextendr_package(configure_ac = TRUE) to replace the file with the current template.

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