| Title: | Bridge R Packages to Julia |
|---|---|
| Description: | Helper utilities for R packages that call into a Julia simulation engine. Provides a small, opinionated API for finding the Julia binary, installing required Julia packages, loading '.jl' bridge files, managing lazy initialisation, describing Julia calls in R and rendering them to Julia source, and holding Julia objects from R between calls. Uses JuliaConnectoR (out-of-process Julia via TCP) under the hood, chosen over JuliaCall to structurally avoid the embedded-Julia segfaults that otherwise leak into R from libR/libjulia symbol interactions, environment poisoning, and Pkg operations. |
| Authors: | Sebastian Funk [aut, cre] (ORCID: <https://orcid.org/0000-0002-2842-3406>) |
| Maintainer: | Sebastian Funk <[email protected]> |
| License: | MIT + file LICENSE |
| Version: | 0.1.0 |
| Built: | 2026-10-13 12:04:15 UTC |
| Source: | https://github.com/epiforecasts/juliabridge |
Render a spec as Julia code
as_julia(x, ascii = FALSE)as_julia(x, ascii = FALSE)
x |
A spec from |
ascii |
Logical. If |
A character string of Julia code that constructs the spec.
as_julia(julia_spec("Solver", 10L, tol = 1e-8)) # A keyword name outside ASCII travels as an escape greek <- list(1) names(greek) <- "\u03f5_t" as_julia(do.call(julia_spec, c("Process", greek)), ascii = TRUE)as_julia(julia_spec("Solver", 10L, tol = 1e-8)) # A keyword name outside ASCII travels as an escape greek <- list(1) names(greek) <- "\u03f5_t" as_julia(do.call(julia_spec, c("Process", greek)), ascii = TRUE)
The extension point for a calling package with its own way of describing a
value, such as a distribution object. Write a method returning either a
spec from julia_spec() or a plain R value, and it renders wherever
the value appears.
as_julia_value(x, ...)as_julia_value(x, ...)
x |
The value to render. |
... |
Passed to methods. |
A spec or an R value that renders on its own.
# A package with a value class of its own renders it like this: as_julia_value.my_interval <- function(x, ...) { julia_spec("Interval", x$lower, x$upper) }# A package with a value class of its own renders it like this: as_julia_value.my_interval <- function(x, ...) { julia_spec("Interval", x$lower, x$upper) }
Composition errors are worth catching in R, where the argument can be
named, rather than in Julia, where the error arrives from inside a
constructor. A julia() expression with no role is accepted for any role,
since the caller has said what it is by writing the code.
assert_role( x, roles, null_ok = FALSE, arg_name = deparse(substitute(x)), labels = NULL )assert_role( x, roles, null_ok = FALSE, arg_name = deparse(substitute(x)), labels = NULL )
x |
Object to check. |
roles |
Character vector of accepted roles. |
null_ok |
Logical. Whether |
arg_name |
Name used in error messages. |
labels |
Optional named character vector describing each role, used
in the error message, for instance
|
Invisibly TRUE.
transform <- julia_spec("LogTransform", .role = "transform") assert_role(transform, "transform") try(assert_role(transform, "model"))transform <- julia_spec("LogTransform", .role = "transform") assert_role(transform, "transform") try(assert_role(transform, "model"))
Main moduleEquivalent in spirit to JuliaCall::julia_assign(name, value). Useful
when porting code that previously used the assign-then-eval pattern.
Idiomatic JuliaConnectoR code prefers passing values directly via
call_julia(); this is provided for migration convenience.
assign_julia(name, value)assign_julia(name, value)
name |
Variable name to bind in |
value |
R value to convert and assign. |
Internally defines a small Julia helper __juliabridge_assign__! on
first use and calls it with the (Symbol, value) pair.
Invisibly NULL.
The function name may be module-qualified (e.g. "Distributions.mean").
Qualifying the name after using makes constructor names resolve
unambiguously.
call_julia(name, ...)call_julia(name, ...)
name |
Function name, optionally module-qualified. |
... |
Arguments passed to the Julia function. |
The Julia function's return value, converted to R where reasonable.
Equivalent in spirit to JuliaCall::julia_command(code): evaluate
Julia code without using the return value. Provided for migration
convenience; functionally identical to eval_julia() called for
its side effects.
command_julia(code)command_julia(code)
code |
A string of Julia code. |
Invisibly the result of juliaEval.
Convenience wrapper for the common "call my package's setup_*()
function the first time any Julia-using function is called" pattern.
Idempotent: once state_env$ready is TRUE, this is a no-op.
ensure_julia(state_env, init_fn)ensure_julia(state_env, init_fn)
state_env |
An environment shared with |
init_fn |
A zero-argument function that performs the package's
one-time setup (typically calling |
Invisibly TRUE.
## Not run: # Inside the consuming package: .my_pkg_env <- new.env(parent = emptyenv()) setup_my_pkg <- function() { julia_ready(packages = "MyJuliaPkg", state_env = .my_pkg_env) julia_load_bridge("mypkg", "bridge.jl") } my_function <- function() { ensure_julia(.my_pkg_env, setup_my_pkg) call_julia("MyJuliaPkg.do_something") } ## End(Not run)## Not run: # Inside the consuming package: .my_pkg_env <- new.env(parent = emptyenv()) setup_my_pkg <- function() { julia_ready(packages = "MyJuliaPkg", state_env = .my_pkg_env) julia_load_bridge("mypkg", "bridge.jl") } my_function <- function() { ensure_julia(.my_pkg_env, setup_my_pkg) call_julia("MyJuliaPkg.do_something") } ## End(Not run)
Evaluate Julia code
eval_julia(code)eval_julia(code)
code |
A string of Julia code. |
The result, converted to R where reasonable; otherwise a
JuliaProxy object.
eval_julia() and call_julia() return composite Julia values
(structs, Tuples, NamedTuples, Dicts) as proxy objects that reference
the value inside the Julia session. This function translates such a
proxy into a plain R object, e.g. a NamedTuple into a named list.
Values that are already plain R objects pass through unchanged.
get_julia(x)get_julia(x)
x |
A Julia proxy object (or an already-translated R value). |
The value fully translated into R data structures.
Returns a list/environment-like object of the module's exported names,
callable as R functions. Equivalent to juliaImport.
import_julia(module)import_julia(module)
module |
Name of the Julia module, e.g. |
A JuliaModuleImport object.
Marks a string as Julia source to be inserted verbatim when a spec is rendered, for arguments that cannot be expressed in R, such as functions or objects from other Julia packages.
julia(code, .role = NULL)julia(code, .role = NULL)
code |
Character string of Julia code. |
.role |
Optional role (see |
An object of class julia_code, which is also a
julia_spec and holds the code in $code. It has no $fn,
$args or $kwargs, so a package walking a call branches on
inherits(x, "julia_code") first.
# A Julia function, which has no R equivalent to render julia_spec("Sampler", transform = julia("identity"))# A Julia function, which has no R equivalent to render julia_spec("Sampler", transform = julia("identity"))
julia_ready() records that setup has run and returns immediately when
called again. That leaves a session stuck once its Julia process goes
away, whether it was stopped, interrupted or died: the packages are no
longer loaded, while the flag still says they are.
julia_alive(state_env, probe = NULL)julia_alive(state_env, probe = NULL)
state_env |
The environment given to |
probe |
Julia code returning a |
This checks the running session and clears the flag when the check
fails. The next julia_ready() call then sets Julia up again. The
environment itself is kept, because callers may use its identity.
The check itself may start Julia, because JuliaConnectoR starts a new
server whenever its connection has gone. The default probe is FALSE
in such a server: only the server julia_ready() ran in holds the
token it recorded.
TRUE when the session is usable, otherwise FALSE.
## Not run: if (!julia_alive(.my_pkg_env, "isdefined(Main, :MyBridge)")) { setup_my_pkg() } ## End(Not run)## Not run: if (!julia_alive(.my_pkg_env, "isdefined(Main, :MyBridge)")) { setup_my_pkg() } ## End(Not run)
Resolves the Julia binary path, used by julia_subprocess() to run
subprocess work (installing packages into the default depot) before
starting the JuliaConnectoR server.
julia_bin()julia_bin()
Detection order:
JULIACONNECTOR_JULIABIN env var (JuliaConnectoR's preferred
mechanism).
JULIA_BINDIR env var (Julia's own; joinpath(JULIA_BINDIR, "julia")).
The julia on the PATH (Sys.which("julia")).
Absolute path to the Julia executable, or "" if not found.
Some Julia results are too big, or too Julia-shaped, to convert: an MCMC chain a later call will sample from, a simulation state later summarised. The Julia side keeps such an object in a registry and returns an integer handle. R holds the handle in an environment whose finaliser queues it for release once nothing refers to it.
julia_handle(handle, session, state_env)julia_handle(handle, session, state_env)
handle |
Integer handle returned by the Julia side. |
session |
Token identifying the Julia session that made it. |
state_env |
The environment given to |
These functions deal with two hazards of that arrangement.
Handles are numbered per session. After a restart, an old handle names whatever object the new session gave that number. Each handle is therefore paired with a token for the session that made it. Both sides compare tokens before acting.
Saving an R object copies the environment by value but not its
finaliser. A reloaded object then points at a Julia object it does not
own. julia_handle_owned() detects this by comparing the state
environment recorded in the handle with the current one.
A consumer package defines three things on the Julia side, in its own module where they precompile:
const HANDLES = Dict{Int, Any}()
const NEXT_HANDLE = Ref(0)
const SESSION = Ref("")
function __init__()
SESSION[] = string(rand(Random.RandomDevice(), UInt128); base = 16)
return nothing
end
function keep!(x)
NEXT_HANDLE[] += 1
HANDLES[NEXT_HANDLE[]] = x
return NEXT_HANDLE[]
end
function release!(handle::Integer, session::AbstractString)
session == SESSION[] && delete!(HANDLES, Int(handle))
return nothing
end
A function returning a handle also returns SESSION[]. A function
acting on a handle takes that token too and refuses a mismatch.
An environment holding handle, session and the owning
state_env.
## Not run: result <- call_julia("MyBridge.fit", data) fit <- list( draws = result$draws, julia = julia_handle(result$handle, result$session, .my_pkg_env) ) ## End(Not run)## Not run: result <- call_julia("MyBridge.fit", data) fit <- list( draws = result$draws, julia = julia_handle(result$handle, result$session, .my_pkg_env) ) ## End(Not run)
Returns FALSE for a handle that arrived by saving and reloading,
whose Julia object belongs to the session that created it. It also
returns FALSE for a handle created before julia_ready() last set
Julia up, whose object went with the old Julia server. Callers use this
to tell the user the object came from disk or an earlier session.
julia_handle_owned(x, state_env)julia_handle_owned(x, state_env)
x |
A handle from |
state_env |
The environment given to |
TRUE when the current setup of state_env created the handle.
## Not run: if (!julia_handle_owned(fit$julia, .my_pkg_env)) { stop("This fit was loaded from disk; refit it to continue.") } ## End(Not run)## Not run: if (!julia_handle_owned(fit$julia, .my_pkg_env)) { stop("This fit was loaded from disk; refit it to continue.") } ## End(Not run)
inst/julia/ directoryReads each file as a string and evaluates it via
JuliaConnectoR::juliaEval() in the running Julia server. Bridge
files typically define helper functions used by the wrapping R
package; loading them once after package setup makes them available
to subsequent juliaCall() invocations.
julia_load_bridge(package, files, verbose = FALSE)julia_load_bridge(package, files, verbose = FALSE)
package |
Name of the calling R package (used as the |
files |
Character vector of |
verbose |
If |
Invisibly the character vector of paths that were loaded.
## Not run: julia_load_bridge("ringbpjl", c("dist_lookup.jl", "simulate.jl", "generation_time.jl")) ## End(Not run)## Not run: julia_load_bridge("ringbpjl", c("dist_lookup.jl", "simulate.jl", "generation_time.jl")) ## End(Not run)
Performs the steps needed to make Julia and a set of Julia packages callable from R via JuliaConnectoR:
julia_ready( packages, github = character(), state_env = new.env(parent = emptyenv()), install = TRUE, project = NULL, match_manifest = TRUE, verbose = TRUE )julia_ready( packages, github = character(), state_env = new.env(parent = emptyenv()), install = TRUE, project = NULL, match_manifest = TRUE, verbose = TRUE )
packages |
Character vector of Julia package names to ensure are
loaded (e.g. |
github |
Named character vector of GitHub URLs for packages not in
the General registry. Names must match entries in |
state_env |
An environment used to track initialisation state. The caller (typically a wrapping R package) supplies its own environment. Several consuming packages then keep separate state. |
install |
If |
project |
Optional path to a Julia project directory containing a
|
match_manifest |
If |
verbose |
If |
Locates the Julia binary (see julia_bin()).
Without project, checks in a Julia subprocess that each required
package loads. If a package is missing and install = TRUE, installs
it (from a GitHub URL if listed in github, otherwise from the
General registry). With project, instantiates that project instead.
Both run in a subprocess, apart from the JuliaConnectoR server.
Starts (or attaches to) the JuliaConnectoR server.
Loads each package with juliaEval("using <pkg>"). Dotted
constructor names such as EpiBranch.NegBin then resolve.
Idempotent: if state_env$ready is already TRUE, returns immediately.
Invisibly TRUE.
## Not run: .my_pkg_env <- new.env(parent = emptyenv()) julia_ready( packages = c("EpiBranch", "Distributions", "Random"), github = c(EpiBranch = "epiforecasts/EpiBranch.jl"), state_env = .my_pkg_env ) ## End(Not run)## Not run: .my_pkg_env <- new.env(parent = emptyenv()) julia_ready( packages = c("EpiBranch", "Distributions", "Random"), github = c(EpiBranch = "epiforecasts/EpiBranch.jl"), state_env = .my_pkg_env ) ## End(Not run)
Because a finaliser can run partway through another Julia call, it only adds the handle to a queue. This function flushes the queue. Call it immediately before a Julia call of your own.
julia_release_pending(state_env, release)julia_release_pending(state_env, release)
state_env |
The environment given to |
release |
Name of the Julia function releasing a handle, taking the handle and the session token. |
Only handles from the current setup of state_env are released. The
queue is dropped when state_env is not set up. Handles from an earlier
setup are dropped too, because their objects went with the old Julia
server. A release that fails stays queued for the next call, up to
three attempts, after which it is dropped: a release that keeps failing
usually means the server was replaced without julia_alive() noticing.
Invisibly the number of handles released.
## Not run: julia_release_pending(.my_pkg_env, "MyBridge.release!") call_julia("MyBridge.forecast", handle, horizon) ## End(Not run)## Not run: julia_release_pending(.my_pkg_env, "MyBridge.release!") call_julia("MyBridge.forecast", handle, horizon) ## End(Not run)
Records a call to a Julia constructor as an R object, rendered to Julia source only when it is run. A package wrapping a Julia library builds its own constructors on top of this, and exposes it directly so that users can reach anything it has not wrapped.
julia_spec(.fn, ..., .role = NULL)julia_spec(.fn, ..., .role = NULL)
.fn |
Character string. Name of the Julia constructor, which may be
qualified by its module and may hold a parameter list, as in
|
... |
Arguments to the constructor. Unnamed arguments are positional
and named arguments become keyword arguments. A keyword name is refused
if it begins with a digit, holds a space or comma, holds a character
that would take it out of the call, or repeats another; beyond that,
which names Julia accepts is left to Julia, and a name may hold
non-ASCII characters. Every keyword name is checked, including one whose
value is |
.role |
Optional character string naming what the spec is, in
whatever vocabulary the calling package uses (for example |
Arguments are rendered to Julia as follows: specs and julia()
expressions are inserted as code, numeric vectors of length one become
scalars and longer ones become vectors, unnamed lists become vectors,
NA becomes missing, and character strings become Julia strings.
Integers (e.g. 2L) render as Julia integers and doubles as floats.
A NULL keyword argument is dropped, so the Julia default applies; a
NULL positional argument is an error, since dropping it would renumber
the arguments that follow. A matrix or array keeps its shape, arriving in
Julia as a Matrix or Array of the same dimensions. A named vector, a
named list and an empty value are all refused, since Julia would read them
as something the R value did not say.
An object of class julia_spec, holding the constructor name
in $fn, the positional arguments in $args and the keyword arguments
in $kwargs. A package may read those to inspect or rewrite a call.
# Any Julia constructor, by name. R types map across: `10L` is a Julia # integer, `1e-8` a float and `"BFGS"` a Julia string. julia_spec("Solver", 10L, tol = 1e-8, method = "BFGS") # A name may be qualified by its module, and a spec may nest in another julia_spec("MyModule.Problem", julia_spec("Solver", 10L), verbose = TRUE) # A matrix keeps its shape, reaching Julia as a Matrix as_julia(julia_spec("Weights", matrix(1:4, nrow = 2))) # A role lets the calling package check how specs are composed julia_spec("Normal", 0, 1, .role = "prior")# Any Julia constructor, by name. R types map across: `10L` is a Julia # integer, `1e-8` a float and `"BFGS"` a Julia string. julia_spec("Solver", 10L, tol = 1e-8, method = "BFGS") # A name may be qualified by its module, and a spec may nest in another julia_spec("MyModule.Problem", julia_spec("Solver", 10L), verbose = TRUE) # A matrix keeps its shape, reaching Julia as a Matrix as_julia(julia_spec("Weights", matrix(1:4, nrow = 2))) # A role lets the calling package check how specs are composed julia_spec("Normal", 0, 1, .role = "prior")
The user's default channel stays as it is. The caller uses the returned
binary, typically through JULIACONNECTOR_JULIABIN.
juliaup_julia(version, verbose = TRUE)juliaup_julia(version, verbose = TRUE)
version |
Julia version to install, e.g. |
verbose |
If |
Path to the Julia executable, or NULL when juliaup is absent
or the installation failed.
## Not run: juliaup_julia("1.12") ## End(Not run)## Not run: juliaup_julia("1.12") ## End(Not run)
A Manifest.toml records the Julia version that resolved it and pins
standard-library versions that only exist on that version. A
package shipping a pinned project needs that Julia version to
instantiate cleanly.
manifest_julia_version(project)manifest_julia_version(project)
project |
Path to a Julia project directory containing a
|
A versioned manifest (Manifest-v1.12.toml, supported from Julia 1.11)
takes precedence over Manifest.toml on the Julia version it names.
When any exist, the highest-versioned one is read. If it records no
Julia version, its file name supplies one.
The major and minor version as a string (e.g. "1.12"), or
NULL when there is no manifest or it records no version.
## Not run: manifest_julia_version(system.file("julia", package = "MyPkg")) ## End(Not run)## Not run: manifest_julia_version(system.file("julia", package = "MyPkg")) ## End(Not run)
These thin wrappers let consumer packages call into Julia through
juliabridge alone. If juliabridge ever switches backend, consumer code
keeps working.