clang offers the ability to mark a symbol as exported or non-exported from a dynamic shared object (DSO) using __attribute__((visibility ("default"))) and __attribute__ ((visibility ("hidden"))). This issue requests the same in Rust, though it may be a bit more complex in the Rust case.
Another way of looking at it is to cleave the dual purpose of #[no_mangle] where it both sets visibility and alters the exported name.
The rest of this issue explains why this would be useful.
I am working in a pre-existing complex C++ codebase. It produces multiple DSOs, let’s say:
libbase.so
libservices.so
We want to build Rust code into each of those DSOs. Internally, the C++ and Rust code is intermixed freely (for example, the C++ JSON APIs within libbase.so call into a serde-based parser, which calls back into C++ to instantiate objects, etc.) There is no possibility of splitting these DSOs into separate DSOs for Rust and C++ code.
We build Rust code into these DSOs in the approved way, which is to aggregate a bunch of Rust libraries (rlibs) into a separate Rust staticlib for each of the DSOs. (For example, libbase_rust_deps.a and libservices_rust_deps.a). The final C++ linker links exactly one of these staticlibs together with the C++ .a and .o in the final construction of the DSO. That should be fine, since the whole point of a staticlib Rust target is to contain the Rust code and all its dependencies.
libservices.so depends on libbase.so. In C++, libservices.so uses symbols exposed from libbase.so. For example, there may be a CPP::Log function used by C++ code within both libbase.so and libservices.so. That’ll be exported from libbase.so using an __attribute__((visibility ("default"))) annotation in the source code for CPP::Log.
So far so good.
But now we want to put a Rust wrapper around that CPP::Log function, to make, say, rs_log. Ideally, there would be just a single implementation of rs_log in libbase.so. We want to be able to call that Rust wrapper from within the Rust parts of libbase.so or libservices.so.
This seems to be impossible.
Our options are:
- Build a
dylib using rustc, which exposes all public Rust APIs from the DSO. We can’t do that for the reasons cited above; we need to build a staticlib as the Rust code is just a small component of existing DSOs. Rustc can’t be in control of the final linking.
- Mark
rs_log as #[no_mangle] which successfully exports it from libbase.so, but using a C ABI which prevents Rust discovering and using this symbol even if downstream crates are built using --extern rust_base=./libbase.so
(this latter case gives
error[E0463]: can't find crate for `rust_base`
--> ./rust_static_libs/libservices_rust_deps/mod.rs:20:1
|
20 | extern crate rust_base;
| ^^^^^^^^^^^^^^^^^^^^^^^ can't find crate
)
Solution?
It’s a problem that #[no_mangle] has twin effects: specify a particular name for C linkage purposes, and mark the symbol for DSO export. We want to do the latter but not the former. #54135 (comment) wants to do the former but not the latter. Can we separate those?
For our needs, ideally we’d mark rs_log with something like an #[dso_export] annotation. This would:
- Mark that symbol with the equivalent of
__attribute__((visibility ("default"))), such that as it moves through the rlib, then staticlib, then the DSO, it is successfully exported from the DSO.
- But unlike
#[no_mangle] it wouldn’t change the symbol name and it would remain callable by Rust.
- (The likely trickier bit) When building the final
staticlib, determine if there are any such exported Rust APIs. If so, include whatever information is required by Rust to consider libbase.so to be a valid crate equivalent to a dylib. (I don’t know how Rust metadata is structured; whether this is just an extra few exported symbols or if there are entire linker sections within a dylib that would somehow need to be replicated. I strongly suspect the latter so I don’t even know if this is possible without changing the final C++ linker command).
Related work
#54135 requests something similar, but using #[used] on static variables - so I think it’s not quite the same thing.
dtolnay/cxx#219 proposes the workaround which we might need to do meanwhile (but isn’t ideal because, in the above example, RS::Log would be duplicated between two DSOs)
clang offers the ability to mark a symbol as exported or non-exported from a dynamic shared object (DSO) using
__attribute__((visibility ("default")))and__attribute__ ((visibility ("hidden"))). This issue requests the same in Rust, though it may be a bit more complex in the Rust case.Another way of looking at it is to cleave the dual purpose of
#[no_mangle]where it both sets visibility and alters the exported name.The rest of this issue explains why this would be useful.
I am working in a pre-existing complex C++ codebase. It produces multiple DSOs, let’s say:
libbase.solibservices.soWe want to build Rust code into each of those DSOs. Internally, the C++ and Rust code is intermixed freely (for example, the C++ JSON APIs within
libbase.socall into a serde-based parser, which calls back into C++ to instantiate objects, etc.) There is no possibility of splitting these DSOs into separate DSOs for Rust and C++ code.We build Rust code into these DSOs in the approved way, which is to aggregate a bunch of Rust libraries (rlibs) into a separate Rust
staticlibfor each of the DSOs. (For example,libbase_rust_deps.aandlibservices_rust_deps.a). The final C++ linker links exactly one of these staticlibs together with the C++ .a and .o in the final construction of the DSO. That should be fine, since the whole point of astaticlibRust target is to contain the Rust code and all its dependencies.libservices.sodepends onlibbase.so. In C++,libservices.souses symbols exposed fromlibbase.so. For example, there may be aCPP::Logfunction used by C++ code within bothlibbase.soandlibservices.so. That’ll be exported fromlibbase.sousing an__attribute__((visibility ("default")))annotation in the source code forCPP::Log.So far so good.
But now we want to put a Rust wrapper around that
CPP::Logfunction, to make, say,rs_log. Ideally, there would be just a single implementation ofrs_loginlibbase.so. We want to be able to call that Rust wrapper from within the Rust parts oflibbase.soorlibservices.so.This seems to be impossible.
Our options are:
dylibusing rustc, which exposes all public Rust APIs from the DSO. We can’t do that for the reasons cited above; we need to build astaticlibas the Rust code is just a small component of existing DSOs. Rustc can’t be in control of the final linking.rs_logas#[no_mangle]which successfully exports it fromlibbase.so, but using a C ABI which prevents Rust discovering and using this symbol even if downstream crates are built using--extern rust_base=./libbase.so(this latter case gives
)
Solution?
It’s a problem that
#[no_mangle]has twin effects: specify a particular name for C linkage purposes, and mark the symbol for DSO export. We want to do the latter but not the former. #54135 (comment) wants to do the former but not the latter. Can we separate those?For our needs, ideally we’d mark
rs_logwith something like an#[dso_export]annotation. This would:__attribute__((visibility ("default"))), such that as it moves through the rlib, then staticlib, then the DSO, it is successfully exported from the DSO.#[no_mangle]it wouldn’t change the symbol name and it would remain callable by Rust.staticlib, determine if there are any such exported Rust APIs. If so, include whatever information is required by Rust to considerlibbase.soto be a valid crate equivalent to adylib. (I don’t know how Rust metadata is structured; whether this is just an extra few exported symbols or if there are entire linker sections within adylibthat would somehow need to be replicated. I strongly suspect the latter so I don’t even know if this is possible without changing the final C++ linker command).Related work
#54135 requests something similar, but using
#[used]on static variables - so I think it’s not quite the same thing.dtolnay/cxx#219 proposes the workaround which we might need to do meanwhile (but isn’t ideal because, in the above example,
RS::Logwould be duplicated between two DSOs)