nimble-dev / nimble

The base NIMBLE package for R
http://R-nimble.org
BSD 3-Clause "New" or "Revised" License
155 stars 22 forks source link

precleanCompilation deletes .o files created by Rcpp::sourceCpp, causing compilation to fail for C++ linked via nimbleExternalCall #1461

Open jmhewitt opened 3 months ago

jmhewitt commented 3 months ago

I noticed nimble's new precleanCompilation option #1393 in version 1.1.0, introduced to fix #1368, caused some of my project code to break on OSX (M2, Ventura 13.1) and Linux (Rocky Linux 8.6). I can keep my project code running by setting nimble::nimbleOptions(precleanCompilation=FALSE), but wanted to document the issue in case others run into the same. I also wanted to ask if you thought it might be possible to re-implement the precleanCompilation option to be more precise in which files it deletes? Or, can you recommend another fix? I recognize this is probably a low-priority need. An MWE to reproduce and resolve the issue is below.

I use Rcpp::sourceCpp (Rcpp version 1.0.11) to manage C++ include paths while compiling cpp files with dependencies on the RcppEigen and BH packages, for example, so I can call Boost's differential equation solver from nimble models. The precleanCompilation option deletes object files Rcpp::sourceCpp creates, which prevents nimble models with nimbleExternalCall functions from compiling. The MWE does only depends on Rcpp and nimbleExternalCall, though.

Thanks!

# MWE modified from help("nimbleExternalCall")

library(nimble)

sink('add1.h')
cat('
 extern "C" {
 void my_internal_function(double *p, double*ans, int n);
 }
')
sink()
sink('add1.cpp') 
cat('
 #include <cstdio>
 #include "add1.h"
 void my_internal_function(double *p, double *ans, int n) {
   printf("In my_internal_function\\n");
     /* cat reduces the double slash to single slash */ 
   for(int i = 0; i < n; i++) 
     ans[i] = p[i] + 1.0;
 }
')
sink()

# modification: use Rcpp::sourceCpp to compile instead of direct system call
# system('g++ add1.cpp -c -o add1.o')

# compile files
Rcpp::sourceCpp(file = 'add1.cpp')

# find compiled file
ofile = normalizePath(
  path = dir(
    path = getOption("rcpp.cache.dir", tempdir()), 
    pattern = 'add1.o', 
    full.names = TRUE, 
    recursive = TRUE
  ),
  winslash = '/'
)

Radd1 <- nimbleExternalCall(
  function(x = double(1), ans = double(1), n = integer()){}, 
  Cfun =  'my_internal_function',
  headerFile = file.path(getwd(), 'add1.h'), 
  returnType = void(),
  oFile = ofile
)

# check: Rcpp output file exists
file.exists(ofile)

# Error: will not compile when  nimbleOptions('precleanCompilation') == TRUE
CRadd1 <- compileNimble(Radd1)

# precleanCompilation unintentionally deletes the required object file
file.exists(ofile)

# rebuild object file
Rcpp::sourceCpp(file = 'add1.cpp')

# Success: will compile when precleanCompilation is disabled
nimbleOptions(precleanCompilation=FALSE)
CRadd1 <- compileNimble(Radd1)
paciorek commented 3 months ago

Note from Perry: Given that the --preclean fixed a bug we only ever saw during testing, we could decide that when nimbleExternalCall has been used, it will always turn off the --preclean option. Or we could at least add a note to its roxygen.

jmhewitt commented 2 months ago

Both sound like fine options since I agree that at the moment it mostly feels like two corner cases bumping into each other, thanks!

paciorek commented 1 month ago

For now I am just adding a note to the roxygen for nimbleExternalCall. On quick glance I didn't see an easy way to "partially" preclean.