Skip to content

Commit 7fcadfd

Browse files
committed
Updated compile function to handle C/C++ files in parallel
1 parent d21f877 commit 7fcadfd

File tree

5 files changed

+249
-136
lines changed

5 files changed

+249
-136
lines changed

NAMESPACE

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -186,9 +186,11 @@ export(getEquations)
186186
export(getFluxes)
187187
export(getLinVars)
188188
export(getLocalDLLs)
189+
export(getObjfiles)
189190
export(getObservables)
190191
export(getParameters)
191192
export(getReactions)
193+
export(getSourcefiles)
192194
export(getStepIndices)
193195
export(getSteps)
194196
export(ggopen)

R/tools.R

Lines changed: 173 additions & 104 deletions
Original file line numberDiff line numberDiff line change
@@ -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
463464
compile <- 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

Comments
 (0)