Julia setup for R packages that wrap a Julia engine.
juliabridge collects the patterns you otherwise learn the hard way when building an R package that calls Julia:
- which Julia binary to use when several are installed;
- which Julia version a pinned project needs;
- how to install Julia packages without leaving the depot in an unstable state;
- how to load
.jlbridge files reliably; - how to manage lazy initialisation;
- how to recover when the Julia process goes away;
- how to keep a Julia object alive from R and release it safely.
It is small and opinionated. In each consuming package it replaces about 100 lines of brittle boilerplate with about 5.
Backend
juliabridge runs Julia through JuliaConnectoR, in a separate process from R. The main alternative, JuliaCall, embeds Julia in the R process. Both work for simple cases. With JuliaCall, though, R and Julia share memory, threads, signal handlers and the dynamic linker. A problem in any of them crashes both. We have seen segfaults from LD_LIBRARY_PATH poisoning of subprocess Julia, from Pkg.activate(<path>) followed by Pkg.instantiate() in in-process JuliaCall, and from accessing fields of NamedTuple results via julia_eval. With JuliaConnectoR, a Julia segfault closes a TCP socket while R keeps running. A crashed simulation can then be recovered. Out-of-process Julia also leaves a much shorter list of things you must avoid.
JuliaCall has a larger ecosystem and more frequent commits, but most of that activity is platform-compatibility work. Its in-process linking problems cannot be fixed without changing its architecture.
Consumer packages call juliabridge::eval_julia(), call_julia() and import_julia() and never touch JuliaConnectoR directly. The choice of backend stays in one place.
Usage in a consuming R package
# In your package's R/zzz.R (or similar):
.mypkg_env <- new.env(parent = emptyenv())
#' @export
setup_mypkg <- function(install = TRUE) {
juliabridge::julia_ready(
packages = c("EpiBranch", "Distributions", "Random"),
github = c(EpiBranch = "epiforecasts/EpiBranch.jl"),
state_env = .mypkg_env,
install = install
)
juliabridge::julia_load_bridge(
package = "mypkg",
files = c("dist_lookup.jl", "simulate.jl")
)
}
.ensure_julia <- function() {
juliabridge::ensure_julia(.mypkg_env, setup_mypkg)
}Then in any function that touches Julia:
my_function <- function(x) {
.ensure_julia()
juliabridge::call_julia("MyJuliaPkg.do_something", x)
}API
-
julia_bin()resolves the Julia binary, checkingJULIACONNECTOR_JULIABIN, thenJULIA_BINDIR, thenPATH. -
julia_ready(packages, github, state_env, install, project, match_manifest, verbose)installs the required Julia packages in a subprocess, then starts the JuliaConnectoR server and loads them withusing. Once setup has completed, later calls return immediately.- With
project = "<path>", it activates and instantiates a pinned Julia project (e.g.inst/julia/Project.toml) and setsJULIA_PROJECTfor the R session. The user’s default Julia environment is not modified, although packages are still downloaded into the shared Julia depot. We recommend this for reproducible installs. - With
match_manifest = TRUE(the default), it also uses the Julia version the project’sManifest.tomlwas resolved with, installing it via juliaup where available. A manifest pins standard libraries that exist only on that version. A binary the user chose throughJULIACONNECTOR_JULIABINorJULIA_BINDIRtakes precedence. A JuliaConnectoR server that is already running, for instance one another package started, keeps its Julia.
- With
-
julia_load_bridge(package, files, verbose)loads.jlfiles frominst/julia/of the calling package viajuliaEval. -
ensure_julia(state_env, init_fn)is a lazy-initialisation guard. Call it at the top of any function that uses Julia. -
julia_alive(state_env, probe)checks that the Julia session still holds what you need. If it does not, it clears the setup flag. The nextjulia_ready()call then sets Julia up again. -
eval_julia(code),call_julia(name, ...)andimport_julia(module)wrapjuliaEval,juliaCallandjuliaImport.get_julia(),assign_julia()andcommand_julia()cover the remaining common operations. -
manifest_julia_version(project)reads the Julia version aManifest.tomlwas resolved with.juliaup_julia(version)installs a Julia version with juliaup and returns its binary, leaving the user’s default channel alone. -
julia_handle(handle, session, state_env)holds a Julia object from R by an integer handle, paired with a token for the Julia session that created it. Both sides compare the token before acting on a handle. A handle from an old session then fails that check. The help page lists the three definitions the Julia side needs. -
julia_release_pending(state_env, release)releases the Julia objects of handles R has garbage-collected. Because finalisers can run partway through another Julia call, they only queue a release. Call this immediately before a Julia call of your own. -
julia_handle_owned(x, state_env)reports whether a handle’s Julia object still belongs to this session. It returnsFALSEfor a handle loaded from a saved R object. It also returnsFALSEfor one created beforejulia_ready()last set Julia up. -
julia_spec(.fn, ...)records a Julia constructor call as an R object, rendered to Julia source only when it runs, so a call can be built, printed and checked without starting Julia. Unnamed arguments become positional and named ones keyword arguments, R types map across (2Lstays an integer,2becomes2.0), and aNULLkeyword argument is dropped so the Julia default applies. -
julia(code)inserts Julia source verbatim, for what R cannot express such as a function.as_julia(x)returns the rendered code;ascii = TRUEescapes keyword names outside ASCII so they survive transfer on any platform. -
assert_role(x, roles)checks that a spec is what a slot expects, in whatever vocabulary the calling package uses, so a composition mistake is reported in R with the argument named rather than arriving from inside a Julia constructor.
Out of scope
- Initialising in
.onLoad. Eager initialisation there interacts badly with other compiled backends (notably Stan) and can crash R while the package attaches. Useensure_julia(). - A Julia REPL. That is JuliaConnectoR’s job.
Status
Used in ringbpjl; migrations of forecastbaselines, EpiAwareR, and epinow2julia are in flight.