Crate rubicon

Source
Expand description

The rubicon logo: a shallow river in northeastern Italy famously crossed by Julius Caesar in 49 BC

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:

These macros behave differently depending on which feature is enabled:

  • export-globals: symbols are exported for use by other shared objects
  • import-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:

  1. Use the exact same rustc version for all shared objects
  2. Not use -Z randomize-layout (duh)
  3. 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.