@@ -410,159 +410,233 @@ expand.grid.alt <- function(seq1, seq2) {
410410}
411411
412412
413- # ' Compile one or more model-related functions
413+ # ' Compile model-related C/C++ code
414414# '
415415# ' @description
416- # ' Compiles one or more objects of class `parfn`, `obsfn`, or `prdfn` into
417- # ' shared libraries (`.so` or `.dll`).
416+ # ' Compiles one or more model-related objects of class `parfn`, `obsfn`, or
417+ # ' `prdfn` into dynamically loadable shared libraries (`.so` on Unix-alike
418+ # ' systems, `.dll` on Windows).
418419# '
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.
420+ # ' The function automatically detects C and C++ source files associated
421+ # ' with each object based on its `modelname`, compiles them, and links them
422+ # ' into shared libraries which are loaded into the current R session.
429423# '
430424# ' @param ... One or more objects of class `parfn`, `obsfn`, or `prdfn`.
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- # '
425+ # ' Corresponding source files (e.g. `model.c`) are detected
426+ # ' automatically based on the current `modelname` .
427+ # ' @param output Optional character string. If provided, all detected source
428+ # ' files are compiled and linked into a single shared library named
429+ # ' `paste0(output, .Platform$dynlib.ext)`. If `NULL`, each source file is
430+ # ' compiled and linked into its own shared library.
431+ # ' @param args Optional character string of additional compiler or linker
432+ # ' flags passed to `R CMD SHLIB`. If `NULL` or empty, compilation defaults
433+ # ' to `-O3 -DNDEBUG`.
434+ # ' @param cores Integer specifying the number of CPU cores used for parallel
435+ # ' compilation of individual source files. Parallel compilation is enabled
436+ # ' when `cores > 1`.
443437# ' @param verbose Logical. If `TRUE`, compiler and linker output is printed
444438# ' to the R console.
445439# '
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.
449- # '
450440# ' @details
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.
441+ # ' Any previously loaded shared libraries with matching names are unloaded
442+ # ' prior to linking. Successfully linked libraries are loaded automatically.
443+ # '
444+ # ' When `output` is specified, the `modelname` of each input object is
445+ # ' overwritten with `output` to ensure consistent symbol naming across all
446+ # ' compiled routines. In addition, the attributes `sourcefiles` (C/C++
447+ # ' source files) and `objfiles` (corresponding object files) are added to
448+ # ' each object for diagnostic purposes.
458449# '
459450# ' @return
460- # ' Invisibly returns `TRUE` if compilation succeeds.
451+ # ' Invisibly returns `TRUE` if compilation and linking succeed.
452+ # '
453+ # ' @examples
454+ # ' \dontrun{
455+ # ' ## Compile a single model into separate shared libraries
456+ # ' x <- odemodel(reactions)
457+ # ' compile(x)
458+ # '
459+ # ' ## Compile multiple models into a single shared library
460+ # ' compile(x, g, output = "combined")
461+ # ' }
461462# '
462463# ' @export
463464compile <- function (... , output = NULL , args = NULL , cores = 1 , verbose = FALSE ) {
465+
464466 objects <- list (... )
465467 obj.names <- as.character(substitute(list (... )))[- 1 ]
468+ if (! length(objects )) stop(" No objects provided." )
469+
470+ # --- helpers ---------------------------------------------------------------
471+ Rbin <- shQuote(file.path(R.home(" bin" ), " R" ))
472+ cfg <- function (x ) system(paste(Rbin , " CMD config" , x ), intern = TRUE )
466473
467- # --- collect all source files ------------------------------------------------
474+ # --- collect source files -- ------------------------------------------------
468475 files <- NULL
469476 for (i in seq_along(objects )) {
470477 if (inherits(objects [[i ]], c(" obsfn" , " parfn" , " prdfn" ))) {
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 )
478+ fn <- modelname(objects [[i ]])
479+ fn <- outer(
480+ fn ,
481+ c(" " , " _deriv" , " _s" , " _s2" , " _sdcv" , " _dfdx" , " _dfdp" ),
482+ paste0
483+ )
484+ f <- c(paste0(fn , " .c" ), paste0(fn , " .cpp" ))
485+ files <- union(files , f [file.exists(f )])
476486 }
477487 }
488+ if (! length(files ))
489+ stop(" No source files found." )
478490
479- if (length( files ) == 0 )
480- stop( " No source files found for compilation (no .c or .cpp files). " )
491+ roots <- sub( " \\ .(c|cpp)$ " , " " , files )
492+ so <- .Platform $ dynlib.ext
481493
482- .so <- .Platform $ dynlib.ext
494+ has_c <- any(grepl(" \\ .c$" , files , ignore.case = TRUE ))
495+ has_cxx <- any(grepl(" \\ .cpp$" , files , ignore.case = TRUE ))
483496
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"
488- } else {
489- " -std=c++20 -fPIC"
490- }
497+ # --- flags -----------------------------------------------------------------
498+ opt <- if (is.null(args ) || ! nzchar(args )) " -O3 -DNDEBUG" else args
499+ inc <- paste0(" -I" , shQuote(system.file(" include" , package = " CppODE" )))
500+
501+ cflags <- if (.Platform $ OS.type == " windows" ) opt else paste(" -fPIC" , opt )
502+ cxxflags <- paste(
503+ " -std=c++20" ,
504+ if (.Platform $ OS.type == " windows" ) opt else paste(" -fPIC" , opt )
505+ )
491506
492507 Sys.setenv(
493- PKG_CPPFLAGS = paste(include_flags , collapse = " " ),
508+ PKG_CPPFLAGS = inc ,
509+ PKG_CFLAGS = cflags ,
494510 PKG_CXXFLAGS = cxxflags
495511 )
512+ on.exit(Sys.setenv(PKG_CPPFLAGS = " " , PKG_CFLAGS = " " , PKG_CXXFLAGS = " " ), add = TRUE )
513+
514+ # --- report toolchain ------------------------------------------------------
515+ strip_std <- function (x ) trimws(gsub(" (^| )-std=[^ ]+" , " " , x ))
516+ get_std <- function (x ) if (grepl(" -std=" , x )) sub(" .*-std=([^ ]+).*" , " \\ 1" , x ) else NA
517+
518+ if (has_c ) {
519+ std <- get_std(cflags )
520+ cat(sprintf(
521+ " using C compiler: %s%s [%s]\n " ,
522+ strip_std(cfg(" CC" )),
523+ if (! is.na(std )) sprintf(" (standard: %s)" , std ) else " " ,
524+ trimws(gsub(" (^| )-std=[^ ]+" , " " , cflags ))
525+ ))
526+ }
496527
497- # -- set automatic optimization flags -----------------------------------------
498- optflags <- if (is.null(args ) || ! nzchar(args )) " -O3 -DNDEBUG" else args
528+ if (has_cxx ) {
529+ std <- get_std(cxxflags )
530+ cat(sprintf(
531+ " using C++ compiler: %s%s [%s]\n " ,
532+ strip_std(cfg(" CXX" )),
533+ if (! is.na(std )) sprintf(" (standard: %s)" , std ) else " " ,
534+ trimws(gsub(" (^| )-std=[^ ]+" , " " , cxxflags ))
535+ ))
536+ }
499537
500- # --- set up parallel backend if needed ---------------------------------------
538+ # --- parallel backend (foreach) --------------------------------------------
539+ `%doit%` <- foreach :: `%do%`
501540 if (cores > 1 ) {
502- if (Sys.info()[[ " sysname " ]] == " Windows " ) {
541+ if (.Platform $ OS.type == " windows " ) {
503542 cl <- parallel :: makeCluster(cores )
543+ on.exit(parallel :: stopCluster(cl ), add = TRUE )
504544 doParallel :: registerDoParallel(cl )
505- parallel :: clusterCall(cl , function (x ) .libPaths(x ), .libPaths())
506545 } else {
507546 doParallel :: registerDoParallel(cores = cores )
547+ on.exit(doParallel :: stopImplicitCluster(), add = TRUE )
508548 }
509- `%mydo%` <- foreach :: `%dopar%`
510- } else {
511- `%mydo%` <- foreach :: `%do%`
549+ `%doit%` <- foreach :: `%dopar%`
512550 }
513551
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 )
552+ # --- unload old libs --------------------------------------------------------
553+ invisible (lapply(c(roots , output ), function (r )
554+ if (! is.null(r )) try(dyn.unload(paste0(r , so )), silent = TRUE )
555+ ))
525556
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 )
557+ # --- compile ----------------------------------------------------------------
558+ foreach :: foreach(i = seq_along(files )) %doit % {
559+ out <- system(paste(Rbin , " CMD COMPILE" , shQuote(files [i ])), intern = TRUE )
560+ if (verbose ) cat(out , sep = " \n " )
531561 }
532562
533- if (cores > 1 && Sys.info()[[" sysname" ]] == " Windows" ) {
534- parallel :: stopCluster(cl )
535- }
536-
537- # --- link --------------------------------------------------------------------
563+ # --- link -------------------------------------------------------------------
538564 if (is.null(output )) {
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 )
565+
566+ # # set metadata for each object
567+ for (i in seq_along(objects )) {
568+ src <- files [grepl(paste0(" ^" , modelname(objects [[i ]])), files )]
569+ obj <- sub(" \\ .(c|cpp)$" , " .o" , src )
570+ attr(objects [[i ]], " sourcefiles" ) <- src
571+ attr(objects [[i ]], " objfiles" ) <- obj
545572 }
573+
574+ # # load individual shared libraries
575+ for (r in roots )
576+ dyn.load(paste0(r , so ))
577+
546578 } else {
547- # Combined shared lib
579+
548580 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 ])))
581+ eval(parse(
582+ text = paste0(" modelname(" , obj.names [i ], " ) <<- '" , output , " '" )
583+ ))
584+ src <- files [grepl(paste0(" ^" , output ), files )]
585+ obj <- sub(" \\ .(c|cpp)$" , " .o" , src )
586+ attr(objects [[i ]], " sourcefiles" ) <- src
587+ attr(objects [[i ]], " objfiles" ) <- obj
554588 }
555589
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 ))
590+ out <- system(paste(
591+ Rbin , " CMD SHLIB" ,
592+ paste(shQuote(sub(" \\ .(c|cpp)$" , " .o" , files )), collapse = " " ),
593+ " -o" , paste0(output , so ),
594+ opt
595+ ), intern = TRUE )
596+
597+ if (verbose ) cat(out , sep = " \n " )
598+
599+ dyn.load(paste0(output , so ))
560600 }
561601
562602 invisible (TRUE )
563603}
564604
565605
606+
607+ # ' Get objfiles attribute
608+ # '
609+ # ' @description The objfiles attribute contains the paths to compiled object files
610+ # ' associated with a dMod function object.
611+ # '
612+ # ' @param ... objects of type `prdfn`, `parfn`, `objfn`
613+ # ' @return character vector of object file paths
614+ # '
615+ # ' @export
616+ getObjfiles <- function (... ) {
617+ Reduce(" union" , lapply(list (... ), function (x ) attr(x , " objfiles" )))
618+ }
619+
620+
621+ # ' Get sourcefiles attribute
622+ # '
623+ # ' @description
624+ # ' The `sourcefiles` attribute contains the paths to C or C++ source files
625+ # ' associated with a compiled dMod function object.
626+ # '
627+ # ' @param ... Objects of type `prdfn`, `parfn`, or `obsfn`.
628+ # '
629+ # ' @return
630+ # ' A character vector of source file paths.
631+ # '
632+ # ' @export
633+ getSourcefiles <- function (... ) {
634+ Reduce(" union" , lapply(list (... ), function (x ) attr(x , " sourcefiles" )))
635+ }
636+
637+
638+
639+
566640# ' Determine loaded DLLs available in working directory
567641# '
568642# ' @return Character vector with the names of the loaded DLLs available in the working directory
@@ -598,12 +672,7 @@ loadDLL <- function(...) {
598672 try(dyn.unload(f ), silent = TRUE )
599673 dyn.load(f )
600674 }
601-
602-
603675 message(" The following local files were dynamically loaded: " , paste(files , collapse = " , " ))
604-
605-
606-
607676}
608677
609678
0 commit comments