Embedding a Rust binary in another Rust binary
For reasons1 I need to embed a Rust binary in another Rust binary, however out of the box Cargo doesn’t support this easily. RFC 3028 will fix this but as of Rust 1.52
it is not implemented. This blog post will go though an example, show where Cargo does not work today and how to overcome this with a custom script.
An Example
A basic example would be having two crates, inner
and outer
in the same Cargo workspace. Both are binary crates, but we want to embed the entire inner
binary into the outer
binary. An example for this would be that when outer
starts up, it needs to extract the binary in a special location and ask another application to launch inner
.
A naive approach
A Cargo workspace could look like:
At the root we have our Cargo.toml
which declares the two crates:
In our inner
crates’ Cargo.toml
we have:
And the entire code for the crate is:
Our outer
crate could look like
In the above approach, we declare a dependency from outer
to inner
and hard code the location of the debug binary.
From a clean workspace a build doesn’t work
However a subsequent build does work:
And we can run outer
with:
There are three problems here:
- Cargo does not respect the dependency from
outer
toinner
and proceeds to build them in parallel. - Subsequent builds ‘work’ but you might be getting a stale binary embedded which can lead to a lot of confusion.
- There is no easy way for
outer
to know were the desiredinner
binary is. The above code assumes a debug build for the current architecture but the path would be different if it were a release build or a different architecture.
A custom build script
The only way I have found to do this is to have a custom build.rs
build script for outer
which expects the desired binary in a specific place, and another script to coordinate the builds of inner
and outer
.
One benefit of this approach is that we can get rid of the dependency between inner
and outer
which will prevent us from accidentally referencing code in another binary.
The build.rs
for outer
looks like:
We have to use environment variables because that’s the only way a build script can get an argument.
Once this is done we can change ./outer/src/main.rs
to read from OUT_DIR
After this is done we just need a script to drive cargo and set the appropriate environment variables:
With this script, we explicitly build inner
first, grab the output location from cargo and persist it in the environment variable that the outer/build.rs
consumes.
Invoking the script allows us to properly build inner
and then embed it in outer
. The design of the shell script allows us to also pass arguments to cargo build
like --release
or a target architecture. This script also works from a clean build and will always ensure we are embedding the latest build of inner
.
Consider the case of trying to bundle a complex application into a single binary. This application could extract it’s own sub applications on startup to simplify distribution.↩︎