Expand description
Logo by MisiasArt
rubicon enables a dangerous form of dynamic linking in Rust through cdylib crates and carefully-enforced invariants.
This crate provides macros to handle global state (thread-locals and process-locals/statics) in a way that’s compatible with the “xgraph” dynamic linking model, where multiple copies of the same crate can coexist in the same address space.
The main macros provided are:
thread_local!
: A drop-in replacement forstd::thread_local!
process_local!
: Used to declare statics (includingstatic mut
)
These macros behave differently depending on which feature is enabled:
export-globals
: symbols are exported for use by other shared objectsimport-globals
: symbols are imported from “the dynamic loader namespace”- neither: the macros act as pass-through to standard Rust constructs
Additionally, the compatibility_check!
macro is provided to help ensure that
common dependencies used by various shared objects are ABI-compatible.
§Explain like I’m five
Let’s assume you’re a very precocious five-year old: say you’re making a static site generator. Part of its job is to compile LaTeX markup into HTML: this uses KaTeX, which requires a JavaScript runtime, that takes a long time to compile.
You decide you want to put this functionality in a shared object, so that you can iterate on the rest of the static site generator without the whole JS runtime being recompiled every time, or even taken into account by cargo when doing check/clippy/build/test/etc.
However, both your app and your “latex module” use the tracing crate for structured logging. tracing uses “globals” (thread-locals and process-locals) to keep track of the current span, and where to log events (ie. the “subscriber”).
If you do tracing_subscriber::fmt::init()
from the app, any use of tracing
in the app
will work fine, but if you do the same from the module, the log events will go nowhere:
as far as it’s concerned (because it has a copy of the entire code of tracing
), there
is no subscriber.
This is where rubicon
comes in: by patching tracing
to use rubicon’s macros, like
thread_local!
and process_local!
, we can have the app export the globals, and
the module import them, so that there’s only one “global subscriber” for all shared
objects.
§That’s it?
Not quite — it’s actually annoyingly hard to export symbols from an executable. So really
what you have instead is a rubicon-exports
shared object that both the app and the module
link against, and import all globals from.
§Why isn’t this built into rustc/cargo?
Because of the “Safety” section below. However, I believe if we work together,
we can make this crate redundant. A global -C globals-linkage=[import,export]
rustc flag would singlehandedly solve the problem.
Someone just has to do it. In the meantime, this crate (and source-patching crates like
tokio
, tracing
, parking_lot
, eyre
, see the compatibility tracker)
act as a “polyfill” — proving that it’s a reasonable and useful approach, which should
make “hey can we let rustc dangerously allow dynamic linking” simpler to defend when it
comes time to submit a PR.
§Safety
By using this crate, you agree to:
- Use the exact same rustc version for all shared objects
- Not use
-Z randomize-layout
(duh) - Enable the exact same cargo features for all common dependencies (e.g.
tokio
)
In short: don’t do anything that would cause crates to have a different ABI from one shared
object to the next. 1 and 2 are trivial, as for 3, the compatibility_check!
macro is here
to help.
For more details on the motivation and implementation of the “xgraph” model, refer to the crate’s README and documentation.
Macros§
- compatibility_
check - Performs a compatibility check for the crate when using Rubicon’s dynamic linking features.
- process_
local - Imports or exports a
static
, depending on the enabled cargo features. - thread_
local - A drop-in replacement for
std::thread_local
that imports/exports the thread-local, depending on the enabled cargo features.