OpenMP and CMake in an R package

The spatial-network analysis R package alcyon functions as an interface to the C++ sala library, originally a part of depthmapX. Because of this, alcyon adopted sala’s configuration and thus leverages CMake to build C++ code i.e sala and some Rcpp glue code to interact with the R parts of the package. The sala library itself recently gained the ability to do analysis using OpenMP, meaning that support for the latter must be provided when building alcyon.

Assuming CMakeLists files include `find_package(OpenMP)`, the suggested solution for R packages with C++ is to put these lines in the Makevars file, as suggested in Writing R extensions:

PKG_CXXFLAGS = $(SHLIB_OPENMP_CXXFLAGS)
PKG_LIBS = $(SHLIB_OPENMP_CXXFLAGS)

The two SHLIB_* variables provide the required arguments to build with OpenMP (for example “-fompenmp” on linux). While this does work, and enables CMake to find OpenMP, various other problems emerge:

  1. Making progress reports is tricky because OpenMP and R do not mix well, and if objects created by R/Rcpp are accessed from a separate thread, then the application can crash. The solution for alcyon was to only really update the R/Rcpp objects if omp_get_thread_num() == 0, i.e. only from the master thread
  2. The above SHLIB_* variables are set when R itself is built. This means that these variables will be empty if R has not been built using OpenMP (for example, with win-builder’s current Debian install). This is fine if the system does not have the library installed, however, if it it does, then CMake will actually find it and try to use it. Because PKG_LIBS will not have the required arguments, it’s possible that CMake will actually manage to compile using OpenMP, but the linker will not be able to find it. To solve this alcyon specifically examines $R_HOME/etc/Makeconf (the place where the build-time R variables are stored) for the SHLIB_* variables. If those are not found, OpenMP is disabled in CMake as well. This all happens during `configure`
    R_HAS_OMP="${R_HOME}/bin/Rscript" -e 'mkcnf <- readLines(paste0(R.home(), "/etc/Makeconf")); ompopt = sub("SHLIB_OPENMP_CXXFLAGS = (.*?)$", "\\\\1", mkcnf[[grep("SHLIB_OPENMP_CXXFLAGS", mkcnf)]]); ompopt = trimws(ompopt); cat(ompopt == "-fopenmp")'
    When CMake is called later in configure we can then disable OpenMP using an input variable.
  3. Finally, to allow for building with OpenMP even if R has not been built with it, we need to force the argument to appear in the package’s Makevars. It’s not possible for Makevars to be provided with the argument during the build process, therefore alcyon had to be switched to require autoconf. Makevars has been renamed Makevars.in and configure has been renamed configure.ac. running autoconf in the package’s main directory will create configure which will in turn create Makevars. Makevars.in now contains an extra autoconf variable which is set during autoconf:
    PKG_CXXFLAGS = $(SHLIB_OPENMP_CXXFLAGS)
    PKG_LIBS = $(SHLIB_OPENMP_CXXFLAGS) @EXTRA_LIBS@

    The new variable @EXTRA_LIBS@ is replaced during autoconf with the required argument (“-fopenmp” on linux) only if R has not been built with OpenMP and we’ve requested that the package actually needs to be built with OpenMP.