Chapter 02: Adding USE_OPENCL and has_opencl() to Your Package

Kjell Nygren

2026-06-11

Overview

Chapter 01 covers getting OpenCL working for nmathopencl itself. This chapter covers the natural next step for package developers: adding USE_OPENCL and has_opencl() to a package of your own so that it can call OpenCL kernels (possibly using the nmathopencl kernel library) while still building cleanly on CRAN and on machines without a GPU SDK.

The two helper functions in this chapter are:

Function When to use
use_opencl_configure() New package, or package with no existing src/Makevars
port_to_opencl_configure() Package that already has a committed static src/Makevars

Both produce a configure script (Linux/macOS) and a configure.win script (Windows) that generate src/Makevars dynamically at install time.

Why a static src/Makevars breaks CRAN

Most Rcpp packages have a static committed src/Makevars along the lines of:

PKG_CXXFLAGS = $(SHLIB_OPENMP_CXXFLAGS)
PKG_LIBS     = $(SHLIB_OPENMP_CXXFLAGS) $(LAPACK_LIBS) $(BLAS_LIBS) $(FLIBS)

If you add OpenCL references directly to this file:

PKG_CXXFLAGS = $(SHLIB_OPENMP_CXXFLAGS) -DUSE_OPENCL -I/usr/include
PKG_LIBS     = $(SHLIB_OPENMP_CXXFLAGS) -lOpenCL $(LAPACK_LIBS) $(BLAS_LIBS) $(FLIBS)

your package will fail to compile on any machine without an OpenCL SDK – including CRAN’s build machines, which have no GPU SDK installed. The build aborts and no binary is produced.

The fix is a pair of configure scripts that probe for the SDK at install time and generate a CPU-only Makevars when no SDK is found. The package always compiles, and the GPU path is activated only where the SDK is genuinely present.

The configure → USE_OPENCL → has_opencl() chain

Three entities cooperate to give you CRAN-safe optional GPU acceleration:

configure / configure.win          (run by R CMD INSTALL)
  |
  |-- detects CL/cl.h + libOpenCL (+ runtime platform probe on Linux)
  |
  v
src/Makevars / src/Makevars.win    (generated; never committed)
  |
  |-- PKG_CXXFLAGS = ...  [ -DUSE_OPENCL ... ]
  |
  v
#ifdef USE_OPENCL                  (in your C++ source)
  |
  |-- guards all GPU code; package compiles cleanly either way
  |
  v
has_opencl()                       (in your R code)
  |
  `-- calls a compiled-in bool that mirrors the compile-time flag;
      returns TRUE only if -DUSE_OPENCL was set at install time

On Linux, the configure script goes one step further: it runs a small C probe (clGetPlatformIDs) to verify that at least one OpenCL platform is actually registered in /etc/OpenCL/vendors/, not just that the ICD loader is installed. configure.win (Windows) relies on header detection alone – the GPU driver installs the ICD (OpenCL.dll) together with itself.

Adding has_opencl() to your package

C++ side

Add a thin wrapper that exposes the compile-time flag at runtime. If you are using Rcpp::export attributes (the standard Rcpp workflow), add:

// src/opencl_status.cpp
#include <Rcpp.h>

// [[Rcpp::export]]
bool _mypkg_has_opencl_cpp() {
#ifdef USE_OPENCL
  return true;
#else
  return false;
#endif
}

compileAttributes() (run automatically during devtools::document()) will generate the required SEXP wrapper in RcppExports.cpp.

If you prefer a plain .Call() without Rcpp attributes, the nmathopencl source in src/nmathopencl_exports.cpp shows the equivalent plain-C form.

R side

# R/opencl_status.R

#' Check whether this package was built with OpenCL support
#'
#' @return Logical scalar: \code{TRUE} if the package was installed from source
#'   with an OpenCL SDK detected by the configure script; \code{FALSE} for
#'   prebuilt CRAN/R-Universe binaries and CPU-only source installs.
#' @export
has_opencl <- function() {
  .Call("_mypkg_has_opencl_cpp", PACKAGE = "mypkg")
}

Replace mypkg with your package name. The function costs nothing at runtime (one compiled-in bool comparison) and lets R code branch between GPU and CPU paths without any dynamic linking to OpenCL.

Case 1: New package with no existing src/Makevars

# From the root of your package:
use_opencl_configure()

This writes configure and configure.win to the package root, sets configure executable on Unix, and prints a setup checklist. Both scripts always succeed: when no OpenCL SDK is found they write a CPU-only Makevars.

Add the generated files to .gitignore (they are build artifacts):

src/Makevars
src/Makevars.win

Case 2: Existing package with a static src/Makevars

If your package already has a committed static src/Makevars, use:

port_to_opencl_configure()

The function:

  1. Reads your existing src/Makevars and extracts the values of PKG_CPPFLAGS, PKG_CXXFLAGS, PKG_CFLAGS, and PKG_LIBS.
  2. Renames src/Makevarssrc/Makevars.in (the maintained source template; commit this file).
  3. Generates configure (Linux/macOS) and configure.win (Windows) that read src/Makevars.in at install time, run OpenCL detection, and write the final src/Makevars with the OpenCL flags merged in (or omitted for CPU-only).
  4. Similarly handles src/Makevars.winsrc/Makevars.win.in if present; otherwise copies the generic configure.win template.
  5. Suggests .gitignore entries for the generated src/Makevars files.

After porting, maintain src/Makevars.in instead of src/Makevars. src/Makevars is generated at install time and should not be committed.

What is preserved

All content in src/Makevars.in that is not one of the four PKG_* key variables is passed through verbatim (comments, blank lines, and any other make variables you have defined). The four key variables are rebuilt by the configure script, incorporating your original base values plus the conditional OpenCL additions.

For example, if your src/Makevars.in contains:

# Package uses OpenMP and RcppParallel
PKG_CXXFLAGS = $(SHLIB_OPENMP_CXXFLAGS) -I"$(R_LIBRARY_DIR)/RcppParallel/include"
PKG_LIBS     = $(SHLIB_OPENMP_CXXFLAGS) $(LAPACK_LIBS) $(BLAS_LIBS) $(FLIBS) \
               -L"$(R_LIBRARY_DIR)/RcppParallel/lib" -ltbb

The generated src/Makevars on an OpenCL-enabled machine will be:

# Package uses OpenMP and RcppParallel
PKG_CXXFLAGS = $(SHLIB_OPENMP_CXXFLAGS) -I"$(R_LIBRARY_DIR)/RcppParallel/include" -DUSE_OPENCL -I/usr/include
PKG_LIBS     = -L/usr/lib/x86_64-linux-gnu -lOpenCL $(SHLIB_OPENMP_CXXFLAGS) $(LAPACK_LIBS) $(BLAS_LIBS) $(FLIBS) -L"$(R_LIBRARY_DIR)/RcppParallel/lib" -ltbb

And on a CPU-only machine src/Makevars.in is copied verbatim as src/Makevars, preserving the original flags exactly.

Caveats

Guarding OpenCL code in C++

All code that depends on OpenCL headers or the OpenCL runtime must be wrapped in #ifdef USE_OPENCL:

#include <Rcpp.h>

#ifdef USE_OPENCL
#include <CL/cl.h>
// ... OpenCL device setup, kernel compilation, dispatch ...
#endif

// [[Rcpp::export]]
Rcpp::NumericVector my_gpu_function(Rcpp::NumericVector x) {
#ifdef USE_OPENCL
  // GPU path
  return run_on_gpu(x);
#else
  // CPU fallback
  return run_on_cpu(x);
#endif
}

This is the same pattern used throughout nmathopencl and glmbayes. The preprocessor guards ensure the package compiles cleanly in both configurations from a single codebase.

Testing the CPU-only path before CRAN submission

Always verify the CPU-only build before submitting to CRAN:

# Linux / macOS: temporarily disable the configure script
mv configure configure.disabled
R CMD INSTALL --preclean .
Rscript -e "library(mypkg); stopifnot(!has_opencl())"
mv configure.disabled configure

# Restore the GPU-enabled build
R CMD INSTALL --preclean .

On Windows, rename configure.win similarly. This simulates what CRAN’s build machines experience and will expose any #ifdef USE_OPENCL guards you may have missed.

DESCRIPTION dependencies

If your package uses nmathopencl’s kernel-loading infrastructure or openclPort.h, add the following to DESCRIPTION:

LinkingTo: nmathopencl, Rcpp
Imports: nmathopencl

LinkingTo makes openclPort.h available at compile time for your C++ code. Imports makes opencltools kernel loaders available (opencltools::load_kernel_library, etc.; pass package = "nmathopencl" for this package’s inst/cl) at runtime. If you only use nmathopencl at the R level (not via LinkingTo), Imports alone is sufficient.

Migration note

use_opencl_configure() and port_to_opencl_configure() are currently in nmathopencl while opencltools completes its initial CRAN review. Once opencltools is available on CRAN, both functions will move there and nmathopencl will re-export them – the same pattern used for the Tier 4 kernel-authoring tools. The function signatures will not change; no action is required from downstream package authors.

For the full configure template source, detailed environment-variable documentation, and the migration plan, see:

system.file("configure-templates", "README.md", package = "nmathopencl")

mirror server hosted at Truenetwork, Russian Federation.