@@ -416,164 +416,147 @@ expand.grid.alt <- function(seq1, seq2) {
416416# ' Compiles one or more objects of class `parfn`, `obsfn`, or `prdfn` into
417417# ' shared libraries (`.so` or `.dll`).
418418# '
419+ # ' If `output` is `NULL`, each detected C/C++ source file is compiled and
420+ # ' linked into its own shared object.
421+ # '
422+ # ' If `output` is provided, all detected source files are compiled
423+ # ' (optionally in parallel if multiple files are present and `cores > 1`)
424+ # ' into object files (`.o`) and then linked together into a single shared
425+ # ' library with the specified name.
426+ # '
427+ # ' If no additional compiler flags are supplied via `args`, the compilation
428+ # ' defaults to using `-O3` for optimization.
429+ # '
419430# ' @param ... One or more objects of class `parfn`, `obsfn`, or `prdfn`.
420- # ' The corresponding C/C++ source files are automatically detected.
421- # ' @param output Optional base name for the combined shared object.
422- # ' @param args Optional compiler or linker arguments (e.g. `"-lm"`).
423- # ' @param verbose Logical; if `TRUE`, show compiler output.
424- # ' @param cores Number of parallel compilation jobs (ignored on Windows).
431+ # ' The corresponding C/C++ source files (e.g., `model.c`, `model.cpp`,
432+ # ' `model_deriv.c`) are automatically detected based on the model name.
433+ # '
434+ # ' @param output Optional character string. If supplied, all compiled object
435+ # ' files are linked into a single shared library named
436+ # ' `paste0(output, .Platform$dynlib.ext)`. If omitted, each source file is
437+ # ' built into a separate shared library.
438+ # '
439+ # ' @param args Optional character string containing additional flags passed
440+ # ' to `R CMD SHLIB` during compilation and linking (e.g., `"-leinspline"`).
441+ # ' If `NULL` or empty, the compiler is invoked with `-O3`.
442+ # '
443+ # ' @param verbose Logical. If `TRUE`, compiler and linker output is printed
444+ # ' to the R console.
445+ # '
446+ # ' @param cores Integer. Number of CPU cores used for parallel compilation
447+ # ' of individual source files into object files. Parallel compilation is
448+ # ' supported on all major operating systems.
425449# '
426450# ' @details
427- # ' Boost headers are provided via the BH package (declared in
428- # ' `LinkingTo: BH`) and no system-installed Boost libraries are required.
451+ # ' Compilation proceeds in two stages. First, each C/C++ source file is
452+ # ' compiled into an object file (`.o`), using parallel processing if enabled.
453+ # ' Second, the object files are linked into one or multiple shared libraries,
454+ # ' depending on whether `output` is specified. Any previously loaded shared
455+ # ' objects with matching names are automatically unloaded before linking.
456+ # ' The resulting shared libraries are loaded into the current R session upon
457+ # ' successful compilation.
429458# '
430- # ' @return Invisibly returns `TRUE` if compilation succeeds.
459+ # ' @return
460+ # ' Invisibly returns `TRUE` if compilation succeeds.
431461# '
432- # ' @import BH
433462# ' @export
434463compile <- function (... , output = NULL , args = NULL , cores = 1 , verbose = FALSE ) {
435- objects <- list (... )
464+ objects <- list (... )
436465 obj.names <- as.character(substitute(list (... )))[- 1 ]
437466
438-
439- # --- collect all source files ---
440- files <- character ()
467+ # --- collect all source files ------------------------------------------------
468+ files <- NULL
441469 for (i in seq_along(objects )) {
442470 if (inherits(objects [[i ]], c(" obsfn" , " parfn" , " prdfn" ))) {
443- filename <- modelname(objects [[i ]])
444- filename <- outer(filename , c(" " , " _deriv" , " _s" , " _s2" , " _sdcv" , " _dfdx" , " _dfdp" ), paste0 )
445- candidates <- c(paste0(filename , " .c" ), paste0(filename , " .cpp" ))
446- candidates <- candidates [file.exists(candidates )]
447- files <- union(files , candidates )
471+ filename <- modelname(objects [[i ]])
472+ filename <- outer(filename , c(" " , " _deriv" , " _s" , " _s2" , " _sdcv" , " _dfdx" , " _dfdp" ), paste0 )
473+ files.obj <- c(paste0(filename , " .c" ), paste0(filename , " .cpp" ))
474+ files.obj <- files.obj [file.exists(files.obj )]
475+ files <- union(files , files.obj )
448476 }
449477 }
450478
451- .so <- .Platform $ dynlib.ext
452-
453479 if (length(files ) == 0 )
454480 stop(" No source files found for compilation (no .c or .cpp files)." )
455481
456- roots <- vapply( files , function ( f ) sub( " \\ .[^.]+$ " , " " , f ), character ( 1 ))
482+ .so <- .Platform $ dynlib.ext
457483
458- # --- Clean up old compiled files ---
459- for (root in roots ) {
460- so_file <- paste0(root , .so )
461- o_file <- paste0(root , " .o" )
462- try(dyn.unload(so_file ), silent = TRUE )
463- if (file.exists(so_file )) {
464- if (verbose ) message(" Removing old: " , so_file )
465- unlink(so_file )
466- }
467- if (file.exists(o_file )) {
468- if (verbose ) message(" Removing old: " , o_file )
469- unlink(o_file )
470- }
484+ # -- include and compiler flags -----------------------------------------------
485+ include_flags <- c(paste0(" -I" , shQuote(system.file(" include" , package = " CppODE" ))))
486+ cxxflags <- if (Sys.info()[[" sysname" ]] == " Windows" ) {
487+ " -std=c++20 -O3 -DNDEBUG"
488+ } else {
489+ " -std=c++20 -O3 -DNDEBUG -fPIC"
471490 }
472491
473- # --- Compiler flags ---
474- if (Sys.info()[[" sysname" ]] == " Windows" ) cores <- 1
475-
476- include_flags <- paste(
477- paste0(" -I" , system.file(" include" , package = " CppODE" )),
478- paste0(" -I" , system.file(" include" , package = " BH" ))
492+ Sys.setenv(
493+ PKG_CPPFLAGS = paste(include_flags , collapse = " " ),
494+ PKG_CXXFLAGS = cxxflags
479495 )
480496
481- cxxflags <- if (Sys.info()[[" sysname" ]] == " Windows" ) {
482- " -std=c++20 -O2 -DNDEBUG -w"
497+ # -- set automatic optimization flags -----------------------------------------
498+ optflags <- if (is.null(args ) || ! nzchar(args )) " -O3" else args
499+
500+ # --- set up parallel backend if needed ---------------------------------------
501+ if (cores > 1 ) {
502+ if (Sys.info()[[" sysname" ]] == " Windows" ) {
503+ cl <- parallel :: makeCluster(cores )
504+ doParallel :: registerDoParallel(cl )
505+ parallel :: clusterCall(cl , function (x ) .libPaths(x ), .libPaths())
506+ } else {
507+ doParallel :: registerDoParallel(cores = cores )
508+ }
509+ `%mydo%` <- foreach :: `%dopar%`
483510 } else {
484- " -std=c++20 -O2 -DNDEBUG -fPIC -fno-var-tracking-assignments -w "
511+ `%mydo%` <- foreach :: `%do%`
485512 }
486513
487- # --- Helper: compile without loading ---
488- compile_one <- function (file , root ) {
489- old_cppflags <- Sys.getenv(" PKG_CPPFLAGS" , unset = NA )
490- old_cxxflags <- Sys.getenv(" PKG_CXXFLAGS" , unset = NA )
491-
492- Sys.setenv(
493- PKG_CPPFLAGS = include_flags ,
494- PKG_CXXFLAGS = cxxflags
495- )
496-
497- on.exit({
498- if (is.na(old_cppflags )) Sys.unsetenv(" PKG_CPPFLAGS" ) else Sys.setenv(PKG_CPPFLAGS = old_cppflags )
499- if (is.na(old_cxxflags )) Sys.unsetenv(" PKG_CXXFLAGS" ) else Sys.setenv(PKG_CXXFLAGS = old_cxxflags )
500- })
501-
502- cmd <- paste0(R.home(" bin" ), " /R CMD SHLIB " , shQuote(file ), " " , args )
503- result <- system(cmd , intern = ! verbose )
504-
505- if (! file.exists(paste0(root , .so ))) {
506- stop(" Compilation failed for " , file )
507- }
508-
509- invisible (root )
514+ # --- unload previously loaded libs -------------------------------------------
515+ all_roots <- unique(sub(" \\ .(c|cpp)$" , " " , basename(files ), ignore.case = TRUE ))
516+ for (r in all_roots ) {
517+ try(dyn.unload(paste0(r , .so )), silent = TRUE )
518+ }
519+ if (! is.null(output )) {
520+ try(dyn.unload(paste0(output , .so )), silent = TRUE )
521+ }
522+
523+ Rbin <- shQuote(file.path(R.home(" bin" ), " R" ))
524+ obj_files <- sub(" \\ .(c|cpp)$" , " .o" , files , ignore.case = TRUE )
525+
526+ # --- compile to object files in parallel -------------------------------------
527+ foreach :: foreach(i = seq_along(files )) %mydo % {
528+ if (file.exists(obj_files [i ])) file.remove(obj_files [i ])
529+ cmd <- paste(Rbin , " CMD SHLIB -c" , shQuote(files [i ]), optflags )
530+ system(cmd , intern = ! verbose )
510531 }
511532
533+ if (cores > 1 && Sys.info()[[" sysname" ]] == " Windows" ) {
534+ parallel :: stopCluster(cl )
535+ }
536+
537+ # --- link --------------------------------------------------------------------
512538 if (is.null(output )) {
513- if (verbose ) message(" Compiling " , length(files ), " model(s)..." )
514-
515- # Compile in parallel (without loading)
516- parallel :: mclapply(seq_along(files ), function (i ) {
517- compile_one(files [i ], roots [i ])
518- }, mc.cores = cores , mc.silent = ! verbose )
519-
520- # Load sequentially in correct order: base models first, then derived
521- # This ensures initmod etc. are available when sensitivity models load
522- base_roots <- roots [! grepl(" _(s2?|deriv|sdcv|dfdx|dfdp)$" , roots )]
523- derived_roots <- roots [grepl(" _(s2?|deriv|sdcv|dfdx|dfdp)$" , roots )]
524-
525- for (root in c(base_roots , derived_roots )) {
526- dyn.load(paste0(root , .so ))
527- if (verbose ) message(" \u 2713 Loaded " , root , .so )
539+ # Separate shared libs
540+ for (i in seq_along(files )) {
541+ ofile <- paste0(all_roots [i ], .so )
542+ cmd <- paste(Rbin , " CMD SHLIB" , shQuote(obj_files [i ]), " -o" , shQuote(ofile ), optflags )
543+ system(cmd , intern = ! verbose )
544+ dyn.load(ofile )
528545 }
529-
530546 } else {
531- # --- Combine all into one shared object ---
532- output <- sub(" \\ .so$" , " " , output )
533- output_so <- paste0(output , .so )
534- output_o <- paste0(output , " .o" )
535-
536- try(dyn.unload(output_so ), silent = TRUE )
537- if (file.exists(output_so )) {
538- if (verbose ) message(" Removing old: " , output_so )
539- unlink(output_so )
540- }
541- if (file.exists(output_o )) {
542- if (verbose ) message(" Removing old: " , output_o )
543- unlink(output_o )
544- }
545-
546- old_cppflags <- Sys.getenv(" PKG_CPPFLAGS" , unset = NA )
547- old_cxxflags <- Sys.getenv(" PKG_CXXFLAGS" , unset = NA )
548-
549- Sys.setenv(
550- PKG_CPPFLAGS = include_flags ,
551- PKG_CXXFLAGS = cxxflags
552- )
553-
554- on.exit({
555- if (is.na(old_cppflags )) Sys.unsetenv(" PKG_CPPFLAGS" ) else Sys.setenv(PKG_CPPFLAGS = old_cppflags )
556- if (is.na(old_cxxflags )) Sys.unsetenv(" PKG_CXXFLAGS" ) else Sys.setenv(PKG_CXXFLAGS = old_cxxflags )
557- })
558-
559- cmd <- paste0(
560- R.home(" bin" ), " /R CMD SHLIB " ,
561- paste(shQuote(files ), collapse = " " ),
562- " -o " , shQuote(output_so ), " " ,
563- args
564- )
565- if (verbose )
566- message(" Linking into shared library: " , output , .so )
567-
568- result <- system(cmd , intern = ! verbose )
569-
570- if (! file.exists(output_so )) {
571- stop(" Compilation failed for combined output" )
547+ # Combined shared lib
548+ for (i in seq_along(objects )) {
549+ eval(parse(text = sprintf(" modelname(%s) <<- '%s'" , obj.names [i ], output )))
550+ # Get only the .o files that belong to this object
551+ obj_modelname <- modelname(objects [[i ]])
552+ obj_o <- obj_files [grepl(paste0(" ^" , obj_modelname ), basename(obj_files ))]
553+ eval(parse(text = sprintf(" attr(%s, 'objfiles') <<- obj_o" , obj.names [i ])))
572554 }
573555
574- dyn.load(output_so )
575- if (verbose )
576- message(" \u 2713 Loaded " , output , .so )
556+ cmd <- paste(Rbin , " CMD SHLIB" , paste(shQuote(obj_files ), collapse = " " ),
557+ " -o" , shQuote(paste0(output , .so )), optflags )
558+ system(cmd , intern = ! verbose )
559+ dyn.load(paste0(output , .so ))
577560 }
578561
579562 invisible (TRUE )
0 commit comments