recorded decision · public · no signup
Summary
What was chosen
What was ruled out· 1
- Supporting `#[export_visibility = "interposable"]` was rejected because it would be a no-op, as `#[no_mangle]` already forces maximum visibility.adr ↗
Constraints
Consequences
- The new attribute enables defining un-mangled and un-exported symbols, improving cross-language interoperability.adr ↗
- `#[export_visibility = ...]` will reduce or eliminate the risk of undefined behavior caused by high symbol visibility.adr ↗
- Reducing unnecessary public exports will result in smaller binaries and faster loading times for dynamic shared objects.adr ↗
- The attribute allows hiding internal library functions, preventing external code from depending on implementation details.adr ↗
The recorded why
- Feature Name:
export_visibility - Start Date: 2025-06-12
- RFC PR: rust-lang/rfcs#3834
- Rust Issue:rust-lang/rust#151425
Summary
Documentation of
#[no_mangle]
points out that by default a #[no_mangle] function (or static)
will be "publicly exported from the produced library or object file".
This RFC proposes a new #[export_visibility = ...] attribute
to override this behavior.
This means that if the same #[no_mangle] function is also
decorated with #[export_visibility = "target_default"],
then it will instead use the default visibility of the target platform
(which can be overriden with the
-Zdefault-visibility=...
command-line flag).
Motivation
§ Context: Enabling non-mangled, non-exported symbols
Rust items (functions or statics) decorated with
#[no_mangle]
or
#[export_name = ...]
are by default publicly exported.
https://github.com/rust-lang/rust/issues/98449 points out that this means that
"it is not possible to define an un-mangled and un-exported symbol in Rust".
The new attribute proposed by this RFC would make this possible - this in turn
may realize the benefits described in the subsections below.
§ Context: Impact on FFI tooling
#[no_mangle] and #[export_name = ...] are the only way to specify
an exact symbol name that can be used outside of Rust (e.g. from C/C++)
to refer to an item (a function or a static) defined in Rust.
This means that FFI libraries and tools can't really avoid problems
caused by unintentional public exports.
This ties this RFC with one of rust-project-goals:
https://github.com/rust-lang/rust-project-goals/issues/253.
Adopting this RFC should improve this aspect of cross-language interop.
§ Benefit: Smaller binaries
One undesirable consequence of unnecessary public exports is binary size bloat. In particular, https://github.com/rust-lang/rust/issues/73958 points out that:
[...] cross-language LTO is supposed to inline the FFI functions into their callers. However, having them exported means also keeping those copies around. Also, unused FFI functions can't be eliminated as dead code.
§ Benefit: Faster loading
Unnecessarily big tables of dynamically-loaded symbols have negative impact on runtime performance. For example, GCC wiki points out that hiding unnecessary exports "very substantially improves load times of your DSO (Dynamic Shared Object)".
§ Benefit: Prevent misuses of internal functions
A shared library implemented in a mix of Rust and some other languages may use
#[export_name = ...] or #[no_mangle] to enable calling Rust functions from
those other languages. Some of those functions will be internal implementation
details of the library. Using #[export_visibility = ...] to hide those
functions will prevent other code from depending on those internal details.
§ Benefit: Parity with C++
C++ developers can control visibility of their symbols with:
-fvisibility=...command-line flag can be used in Clang or GCC- Per-item
__attribute__ ((visibility ("default")))is recognized by Clang and GCC
Rust has an equivalent command-line flag (
-Zdefault-visibility=...,
tracked in https://github.com/rust-lang/rust/issues/131090).
OTOH, Rust doesn't have an equivalent attribute.
Adopting this RFC would be a step toward closing this gap.
§ Context: Undefined behavior caused by naming collisions
The subsections below attempt to provide details about the risk of undefined behavior (UB) caused by duplicate symbol definitions.
Presence of UB risk
The fact that naming collisions may cause UB is documented in the documentation
of #[export_name = ...]
which points out that "this attribute is unsafe as a symbol with a custom name
may collide with another symbol with the same name (or with a well-known
symbol), leading to undefined behavior". Similar UB risk is documented for the
#[no_mangle]
attribute.
Scope of UB risk
The risk of name collisions is caused by two separate behaviors of
#[export_name = ...] and #[no_mangle]:
- Turning-off mangling (e.g. see here) introduces the possibility of naming collisions.
- Exporting the symbol with public visibility (e.g. see here) increases the scope of possible naming collisions (covering all DSOs).
Origins of UB
It is out of scope for this RFC to identify and/or explain the exact origin and/or mechanisms of the UB. Nevertheless, discussions related to this RFC may benefit from outlining at a high-level how the UB may happen, so this topic is explored underneath the folded details section below.
<details>The author of this RFC is not aware of a more authoratitative source that would explain the mechanisms that can lead to the UB in presence of naming collisions. The author speculates that:
- Compilers may assume that each symbol is defined only once (and that breaking
this assumption can lead to UB). Examples of such assumption:
- C++ documents One Definition Rule
(ODR).
This rule necessarily extends to binaries that link C++ compilation
artifacts with
rustcartifacts (even if official Rust documentation doesn't AFAIK talk about this rule). - LLVM optimization passes assume that if they see a definition of a symbol,
then this is the definition that will be actually used (for symbols with
normal linkage - not weak, odr, etc.). LLVM supports suppressing this
assumption with the SemanticInterposition
feature,
but
rustcdoesn't use this LLVM feature (e.g. see here). Special thanks to @jyknight for pointing this out.
- C++ documents One Definition Rule
(ODR).
This rule necessarily extends to binaries that link C++ compilation
artifacts with
- Linkers don't define which exact definition will be used when multiple
definitions are present
- LLVM explicitly
documents this for
linkonce_odrandweak_odrsaying that "one of the definitions is non-deterministically chosen to run" (emphasis mine). - It seems likely that dynamic linking may also be non-deterministic when multiple definitions are present. For example, it seems that the choice of a definition may depend on the order in which DSOs are linked (and it seems fair to treat this order as non-deterministic, or at least outside the immediate control of the code author).
- LLVM explicitly
documents this for
§ Benefit: Avoiding undefined behavior
Using #[export_visibility = ...] to reduce symbol visibility can be used to
reduce or eliminate the risk of undefined behavior (UB) described in the
previous ub-intro section.
UB caused by high symbol visibility is not just a hypothetical risk - this risk has actually caused difficult to diagnose symptoms that are captured in https://crbug.com/418073233. More information about this bug can be found in the folded details section below.
<details>In the smaller repro from https://crrev.com/c/6580611/1 we see the following:
- Without this RFC the
cxxlibrary cannot avoid publicly exporting symbols that are called from C++. In particular, the following two symbols are publicly exported from a static library calledrust_lib:rust_lib$cxxbridge1$get_string(generated by#[cxx::bridge]proc macro to generate an FFI/C-ABI-friendly thunk forrust_lib::get_stringcxxbridge1$string$dropexported fromcxx/src/symbols/rust_string.rs
- In the repro case,
rust_libis statically linked into the main test executable and into an.so. This results in the naming collision, because nowrust_lib$cxxbridge1$get_stringandcxxbridge1$string$dropboth have two definitions - one definition in the test executable and one in the.so. - The test executable calls
rust_lib$cxxbridge1$get_stringand thencxxbridge1$string$drop.- Side-note: The
.sostatically linksrust_lib, but doesn't actually use it. (In the original repro the.soused a small part of a bigger statically linked "base" library and also didn't actually use Rust'scxx-related symbols. See https://crrev.com/c/6504932 which removes the.so's dependency on the "base" library as a workaround for this problem.)
- Side-note: The
- Because naming collisions lead to UB (see the ub-intro section above),
it is non-deterministic whether calling
rust_lib$cxxbridge1$get_stringwill end up calling the definition in the test executable VS in the.so. Similar UB exists for calls tocxxbridge1$string$drop. - The UB from the previous item leads to memory unsafety when:
- The call from test executable to
rust_lib$cxxbridge1$get_stringends up calling the definition in the.so, rather than in the executable. This means that allocations made byget_stringuse one set of the allocator's global symbols - the copy within the.so. - The call from test executable to
cxxbridge1$string$dropends up calling the definition in the executable, rather than in the.so. This means that freeing the previous allocation uses other set of allocator's global symbols - the ones in the executable. - Using wrong global symbols means that the executable's allocator tries to
free an allocation that it doesn't know anything about (because this
allocation has been make by the allocator from the
.so). In debug builds this is caught by an assertion. In release builds this would lead to memory unsafety.
- The call from test executable to
Guide-level explanation
§ The export_visibility attribute
#[no_mangle]
or
#[export_name = ...]
attribute may be used to export
a Rust
function
or
static.
The #[export_visibility = ...] attribute overrides visibility
of such an exported symbol.
The #[export_visibility = ...] attribute uses the
MetaNameValueStr
syntax to specify the desired visibility. The following sections describe
string values that may be used.
Default target platform visibility
#[export_visibility = "target_default"] uses
the default visibility of the target platform.
Note: the nightly version of the rustc compiler
supports overriding the target platform's visibility with the
-Zdefault-visibility=...
command-line flag.
End-to-end example
Consider the following example code:
```
#![feature(export_visibility)]
#[unsafe(export_name = "test_fn_no_attr")]
unsafe extern "C" fn test_fn_with_no_attr() -> u32 {
line!() // `line!()` avoids identical code folding (ICF)
}
#[unsafe(export_name = "test_fn_target_default")]
#[export_visibility = "target_default"]
unsafe extern "C" fn test_fn_asks_for_target_default() -> u32 {
line!() // `line!()` avoids identical code folding (ICF)
}
```
When the code above is built into a DSO,
then -Zdefault-visiblity=hidden will affect visibility of the 2nd function
and prevent it from getting exported from the DSO.
See below for an example of how this may be observed on a Linux system:
```
$ rustc ~/scratch/export_visibility_end_to_end_test.rs \
--crate-type=cdylib \
-o ~/scratch/export_visibility_end_to_end_test_with_hidden_default_visibility.so \
-Zdefault-visibility=hidden
$ readelf \
--dyn-syms \
--demangle \
~/scratch/export_visibility_end_to_end_test_with_hidden_default_visibility.so \
| grep test_fn
55: 0000000000035920 6 FUNC GLOBAL DEFAULT 15 test_fn_no_attr
```
LLVM-level example
tests/codegen-llvm/export-visibility.rs proposed in
a prototype associated with this RFC
has the following expectations for the test functions from the example in the
previous section (with -Zdefault-visibility=hidden):
// HIDDEN: define noundef i32 @test_fn_no_attr
// HIDDEN: define hidden noundef i32 @test_fn_target_default
Reference-level explanation
§ Edits to reference documentation for #[no_mangle]
If this RFC is adopted (and stabilized) then https://doc.rust-lang.org/reference/abi.html#r-abi.no_mangle.publicly-exported should be edited.
Old text:
> Additionally, the item will be publicly exported from the produced library
> or object file, similar to the used attribute.
New text:
> Unless overridden by `#[export_visibility = …]`, the item will be publicly
> exported from the produced library or object file, similar to the used
> attribute.
§ Edits to reference documentation for #[export_name]
If this RFC is adopted (and stabilized) then https://doc.rust-lang.org/reference/abi.html#r-abi.export_name should be edited.
Old text doesn’t mention symbol visibility, or exporting.
New text / paragraph:
> Unless overridden by `#[export_visibility = …]`, the item will be publicly
> exported from the produced library or object file, similar to the used
> attribute.
§ New section for #[export_visibility = …]
If this RFC is adopted (and stabilized) then https://doc.rust-lang.org/reference/abi.html should get a new section:
> # The `export_visibility` attribute
>
> Intro-tag: The _`export_visibility` attribute_ overrides if or how the
> item is exported from the produced library or object file.
> The `export_visibility` attribute can only be applied to
> items with `#[no_mangle]` or `#[export_name = ...]` attributes.
>
> Syntax-tag: The export_visibility attribute uses the MetaNameValueStr
> syntax to specify the symbol name.
>
> Target-default-tag: Currently only `#[export_visibility =
> “target_default”]` is supported. When used, it means that the item will
> be exported with the default visibility of the target platform (which may
> be overridden by the unstable `-Zdefault-visibility=...` command-line
> flag.
Note that the applicability wording proposed above is based on the following factors:
- Desire to only apply the
#[export_visibility = ...]attribute to items for whichcontains_extern_indicatoristrue. Today this covers:- All items that use the
#[no_mangle]attribute - All items that use the
#[export_name = ...]attribute - All items that use the
#[rustc_std_internal_symbol]attribute - Some items that use
#[linkage = ...](note that this attribute has not yet been stabilized and this is why it is not yet mentioned in the proposed reference text above)
- All items that use the
- Desire to forbid applying the
#[export_visibility = ...]attribute in cases where doing so may increase an item visibility.- This is why
#[rustc_std_internal_symbol]is intentionally omitted and why the RFC proposes that using#[export_visibility = ...]for#[rustc_std_internal_symbol]items should be an error. See also the why-new-attr-cant-increase-visibility section below.
- This is why
§ Other details
Other details (probably not important enough to include in the official reference documentation for Rust):
- The proposal in this RFC has been prototyped in https://github.com/anforowicz/rust/tree/export-visibility
Drawbacks
No drawbacks have been identified at this point.
Rationale and alternatives
§ Context: why the new attribute cannot increase visibility
The #[export_visibility = ...] attribute may only be applied to item
definitions with an "extern" indicator as checked by fn contains_extern_indicator.
Based on the above, the #[export_visibility = ...] attribute may never
increase visibility of a symbol. This is because:
#[no_mangle]and#[export_name = ...]force the maximum possible visiblity. See here- It seems that
#[linkage = ...]should have no impact on symbol visibility - One known exception is
#[rustc_std_internal_symbol]- see here. The RFC avoids this exception by disallowing using#[export_visibility = ...]with#[rustc_std_internal_symbol].
§ Rationale for not supporting interposable visibility
The why-new-attr-cant-increase-visibility section above means that
#[export_visibility = "interposable"] would be a no-op. Because of this, the
"interposable" visibility value is not supported by the
#[export_visibility = ...] attribute.
Side-note: The "interposable" visibility is sometimes called "default" [linker] visibility (see the LLVM documentation here), or "public" or "exported" visibility.
Lack of support for the "interposable" visibility means that this RFC avoids
p
Excerpt — the full document is at the cited source: text/3834-export-visibility.md ↗