From 6b9c107825bee7a9d39903f1f060313b1f13f7c9 Mon Sep 17 00:00:00 2001 From: Lukasz Anforowicz Date: Thu, 12 Jun 2025 14:57:13 +0000 Subject: [PATCH 01/24] Initial draft of export-visibility RFC. --- text/0000-export-visibility.md | 582 +++++++++++++++++++++++++++++++++ 1 file changed, 582 insertions(+) create mode 100644 text/0000-export-visibility.md diff --git a/text/0000-export-visibility.md b/text/0000-export-visibility.md new file mode 100644 index 00000000000..c843bb3538f --- /dev/null +++ b/text/0000-export-visibility.md @@ -0,0 +1,582 @@ +- Feature Name: `export_visibility` +- Start Date: 2025-06-12 +- RFC PR: [rust-lang/rfcs#0000](https://github.com/rust-lang/rfcs/pull/0000) +- Rust Issue: [rust-lang/rust#0000](https://github.com/rust-lang/rust/issues/0000) + +# Summary +[summary]: #summary + +Documentation of +[`#[no_mangle]`](https://doc.rust-lang.org/reference/abi.html#the-no_mangle-attribute) +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 = "inherit"]`, +then it will instead inherit the default visibility of the target platform +(or the default visibility specified with the +[`-Zdefault-visibility=...`](https://doc.rust-lang.org/beta/unstable-book/compiler-flags/default-visibility.html) +command-line flag). + +# Motivation +[motivation]: #motivation + +## Context: Enabling non-mangled, non-exported symbols + +Rust items (functions or `static`s) decorated with +[`#[no_mangle]`](https://doc.rust-lang.org/reference/abi.html#the-no_mangle-attribute) +or +[`#[export_name = ...]`](https://doc.rust-lang.org/reference/abi.html#the-export_name-attribute) +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](https://gcc.gnu.org/wiki/Visibility) +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](https://clang.llvm.org/docs/ClangCommandLineReference.html#cmdoption-clang-fvisibility) + or + [GCC](https://gcc.gnu.org/onlinedocs/gcc/Code-Gen-Options.html#index-fvisibility) +* Per-item `__attribute__ ((visibility ("default")))` is recognized by + [Clang](https://clang.llvm.org/docs/AttributeReference.html#visibility) + and + [GCC](https://gcc.gnu.org/onlinedocs/gcc/Common-Function-Attributes.html#index-visibility-function-attribute) + +Rust has an equivalent command-line flag ( +[`-Zdefault-visibility=...`](https://doc.rust-lang.org/beta/unstable-book/compiler-flags/default-visibility.html), +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. + +## Benefit: Avoiding undefined behavior + +Documentation of +[`#[export_name = ...]`](https://doc.rust-lang.org/reference/abi.html#the-export_name-attribute) +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 unsafety risk is present for the +[`#[no_mangle]`](https://doc.rust-lang.org/reference/abi.html#the-no_mangle-attribute) +attribute. Unnecessary public exports increase the scope of this risk. + +A more concrete example of this problem is related to a memory allocator. +A memory allocator may use some global or per-thread data structures to +manage active allocations. Each +[Dynamic Shared Object (DSO)](https://en.wikipedia.org/wiki/Dynamic_shared_object) +can use a different allocator library (and even if using the same allocator, +it can use separate, DSO-specific global data structures). +Therefore taking an allocation made in one DSO +and freeing it in another DSO can lead to memory unsafety +(when the freeing allocator expects that the pointer it got was earlier +allocated by the same allocator instance). + +This is what happened in https://crbug.com/418073233. In the smaller repro +from https://crrev.com/c/6580611/1 we see the following: + +* Without this RFC the [`cxx`](https://cxx.rs) library cannot avoid publicly + exporting symbols that are called from C++. In particular, the following + two symbols are publicly exported from a static library called `rust_lib`: + - `rust_lib$cxxbridge1$get_string` (generated by `#[cxx::bridge]` proc macro + to generate an FFI/C-ABI-friendly thunk for + [`rust_lib::get_string`](https://chromium-review.googlesource.com/c/chromium/src/+/6580611/1/build/rust/tests/test_unexpected_so_hop_418073233/src/lib.rs) + - `cxxbridge1$string$drop` exported from + [`cxx/src/symbols/rust_string.rs`](https://github.com/dtolnay/cxx/blob/07d2bca38b7bfbbe366a9e844d3d66b80820d339/src/symbols/rust_string.rs#L83C18-L86) +* In the repro case, `rust_lib` is statically linked into the main test + executable, and into an `.so`. + - The `.so` statically links `rust_lib`, but doesn't actually use it. + (In the original repro the `.so` used a small part of a bigger statically linked + "base" library and also didn't actually use Rust's `cxx`-related symbols. See + https://crrev.com/c/6504932 which removes the `.so`'s dependency on the + "base" library as a workaround for this problem.) + - The test executable calls `rust_lib::get_string` and then + `cxxbridge1$string$drop`. +* This scenario leads to memory unsafety when: + - The call from test executable to `rust_lib::get_string` ends up calling + `dso!rust_lib::get_string` rather than `exe!rust_lib::get_string`. + - The call from test executable to `cxxbridge1$string$drop` ends up + calling `exe!cxxbridge1$string$drop`. + - This means that the `exe`'s allocator tries to free an allocation made + by the allocator from the `dso`. In debug builds this is caught by an + assertion. In release builds this would lead to memory unsafety. + +# Guide-level explanation +[guide-level-explanation]: #guide-level-explanation + +## The `export_visibility` attribute + +[`#[no_mangle]`](https://doc.rust-lang.org/reference/abi.html#the-no_mangle-attribute) +or +[`#[export_name = ...]`](https://doc.rust-lang.org/reference/abi.html#the-export_name-attribute) +attribute may be used to export +a Rust +[function](https://doc.rust-lang.org/reference/items/functions.html) +or +[static](https://doc.rust-lang.org/reference/items/static-items.html). +The `#[export_visibility = ...]` attribute overrides visibility +of such an exported symbol. + +The `#[export_visibility = ...]` attribute uses the +[MetaNameValueStr](https://doc.rust-lang.org/reference/attributes.html#meta-item-attribute-syntax) +syntax to specify the desired visibility. The following sections describe +string values that may be used. + +### Interposable visibility + +`#[export_visibility = "interposable"]` will cause symbols to be emitted with +"default" visibility. On platforms that support it, this makes it so that +symbols can be interposed, which means that they can be overridden by symbols +with the same name from the executable or by other shared objects earlier in the +load order. + +> **Note**: +> See [interposable-vs-llvm] section below for discussion about an open +> question that asks about interactions between `interposable` visibility +> and LLVM optimization passes. + +> **Note**: +> See [interposable-vs-dllexport] section below for discussion whether +> this visibility should also inject `dllexport` when targeting Windows +> platform. + +> **TODO**: This section (as well as `protected` and `hidden` sections below) is based on +> https://doc.rust-lang.org/beta/unstable-book/compiler-flags/default-visibility.html#interposable +> In the long-term we should deduplicated these docs/definitions (for example +> description of `hidden` in this RFC is a bit expanded and brings up additional +> benefits of hiding symbols). "long-term" probably means: 1) once this or the +> other feature have been stabilized and/or 2) once we are confident with names, +> behavior, etc of all the visibility levels. + +### Protected visibility + + + +`#[export_visibility = "protected"]` signals to the compiler, the linker, and +the runtime linker that the symbol cannot be overridden by the executable or by +other shared objects earlier in the load order. + +This allows the compiler to emit direct references to symbols, which may improve +performance. It also removes the need for these symbols to be resolved when a +shared object built with this option is loaded. + +Using protected visibility when linking with GNU `ld` prior to 2.40 will result +in linker errors when building for Linux. Other linkers such as LLD are not +affected. + +### Hidden visibility + + + +`#[export_visibility = "hidden"]` marks the symbol as hidden. +Hidden symbols will not be exported from the created shared object, so cannot be +referenced from other shared objects or from executables. + + +Similarly to protected visibility, hidden visibility may allow the compiler +to improve performance of the generated code by +emitting direct references to symbols. +And it may remove the runtime overhead of linking these symbols at runtime. + + +Unlike protected visibility, hidden visibility may also enable additional inlining +during Link Time Optimization (LTO), which may be especially important for small +functions (thunks) used for cross-language calls. It may also limit the scope +of the safety risk of having 2 symbols with the same name. + + +`hidden` visibility should *not* be used when it is not possible to control +whether the symbol may be referenced from another shared object. For example, +`hidden` visibility should be avoided when building `dylib`s, because +cross-`dylib` inlining may lead to linking errors. + +> **Note**: +> See [hidden-vs-dylibs] section below for more discussion on what to do +> about the interaction between `dylibs` and this RFC. + +### Inherited visibility + +`#[export_visibility = "inherit"]` uses +the standard visibility of the target platform. + +Note: the nightly version of the `rustc` compiler +supports overriding the target platform's visibility with the +[`-Zdefault-visibility=...`](https://doc.rust-lang.org/beta/unstable-book/compiler-flags/default-visibility.html) +command-line flag. + +### Choosing the right visibility + +If you control the linking process (i.e. you control how your symbols are linked +into an executable, or into a `cdylib`, `so` or `dll`), then you should use the +lowest possible visibility. If a public export is not needed, then use the +`hidden` visibility. Otherwise consider using `protected` or `interposable` +visibility. + +If you are an author of a reusable crate, then you don't know how users of your +crate will link it into executables, `cdylib`s, `dylib`s, etc. In this case it +is best to give control over visibility of your symbols to your clients by using +`#[export_visibility = "inherit"]`. Alternatively (e.g. if you provide a proc +macro to generate the exported symbols) you can consider parametrizing the +behavior of your crate to let your clients specify the exact visibility that +your library will declare through the `#[export_visibility = ...]` attribute. + +# Reference-level explanation +[reference-level-explanation]: #reference-level-explanation + +It seems that the guide-level explanation above may also work as a +reference-level explanation. (At least, the reference documentation of +[`#[no_mangle]`](https://doc.rust-lang.org/reference/abi.html#the-no_mangle-attribute) +and +[`#[export_name = ...]`](https://doc.rust-lang.org/reference/abi.html#the-export_name-attribute) +attributes provides a similar level of details.) + +A few additional notes attempt to clarify the intended behavior of the proposed +behavior beyond what is described in the guide-level explanation above: + +* The `#[export_visibility = ...]` attribute may only be applied to item + definitions with an "extern" indicator as checked by + [`fn contains_extern_indicator`](https://github.com/rust-lang/rust/blob/3bc767e1a215c4bf8f099b32e84edb85780591b1/compiler/rustc_middle/src/middle/codegen_fn_attrs.rs#L174-L184). + Therefore it may only be applied to items to which + `#[no_mangle]`, `#[export_name = ...]`, and similar already-existing + attributes may be already applied. +* The proposal in this RFC has been prototyped in + https://github.com/anforowicz/rust/tree/export-visibility + +# Drawbacks +[drawbacks]: #drawbacks + +See "Open questions" section. + +# Rationale and alternatives +[rationale-and-alternatives]: #rationale-and-alternatives + +## Alternative: version scripts + +Using +[linker version scripts](https://sourceware.org/binutils/docs/ld/VERSION.html) +has been proposed as a way to control visibility of Rust-defined symbols +(e.g. this is a workaround pointed out in +https://github.com/rust-lang/rust/issues/18541). +In particular, using version scripts is indeed a feasible way of avoiding +undefined behavior from https://crbug.com/418073233. + +Using a version script has the following downsides compared to +the `#[export_visibility = ...]`-based approach proposed in this RFC: + +* Using the attribute allows the compiler to optimize the code a bit more than + when using a version script + (based on + [this Stack Overflow answer](https://stackoverflow.com/a/58527480/24042981)) +* Using a version script means that visibility of a symbol is defined in a + centralized location, far away from the source code of the symbol. + - Copying symbol definitions from `.rs` files into a new library is not + sufficient for preserving symbol visibility (for that the version script + has to be replicated as well). + - There is a risk that version script and the symbol definition may diverge + (e.g. after renaming symbol name in an `.rs` file, one has to remember + to check if a version script also needs to be updated). +* Version scripts don't work on all target platforms. In particular, + they work in GNU `ld` and LLVM `lld`, but the native Microsoft Visual C++ + linker (`link.exe`) does not directly support GNU-style version scripts. + Instead, MSVC uses `.def` (module-definition) files to control symbol export and + other aspects of DLL creation. Having to use + [a `.def` file](https://learn.microsoft.com/en-us/cpp/build/reference/exports?view=msvc-170) + has a few extra downsides compared to a version script: + - Having to support both formats + - Lack of support for wildcards means that it is impossible to hide + all symbols matching a pattern like `*cxxbridge*` used by `cxx` in + auto-generated FFI thunks. +* Using a version script is one way of fixing https://crbug.com/418073233. + This fix approach requires that authors of each future shared library know + about the problem and use a version script. This is in contrast to using + `-Zdefault-visibility=hidden` and `#[export_visibility = "inherit"]` for `cxx` + symbols, which has to be done only once to centrally, automatically avoid the + problem for all `cxx`-dependent libraries in a given build environment. + (In fairness, using the command-line flag also requires awareness and opt-in, + but it seems easier to append `-Zdefault-visibility=hidden` to default + `rustflags` in globally-applicable build settings than it is to modify build + tools to require a linker script for all shared libraries. In fact, Chromium + [already builds with the `-Zdefault-visibility=...` flag](https://source.chromium.org/chromium/chromium/src/+/main:build/config/gcc/BUILD.gn;l=34-35;drc=ee3900fd57b3c580aefff15c64052904d81b7760).) + +## Alternative: introduce `-Zdefault-visibility-for-c-exports=...` + +Introducing and using `-Zdefault-visibility-for-c-export=hidden` +can realize most benefits outlined in the "Motivation" section +(except C/C++ parity). +In particular this is a feasible way of avoiding undefined behavior from +https://crbug.com/418073233. + +The main downside, is that it doesn't support making a subset of Rust-defined +symbols public, while hiding another subset. This may still be achievable, +but would require reaching out for C/C++ to export some symbols +(i.e. defining `foo_hidden` in Rust, and then calling it from a publicly +exported `foo` defined in C/C++). + +## Alternative: change behavior of `#[no_mangle]` in future language edition + +Some past proposals suggested changing the behavior of `#[no_mangle]` +(and `#[export_name = ...]`) attribute in a future Rust language edition. +For example, this is what has been proposed in +https://github.com/rust-lang/rust/issues/73958#issuecomment-2889126604 +(although it seems that this proposal wouldn't help with +https://crbug.com/418073233, because it seems to only affect scenarios where +linking is driven by `rustc`). +Other edition-boundary changes may also be considered - for example +just changing the default effect of `#[no_mangle]` from +`#[export_visibility = "interposable"]` to +`#[export_visibility = "inherit"]` +(which combined with `-Zdefault-visibility=hidden` should address +https://crbug.com/418073233). + +It seems that the new edition behaviors proposed above would still benefit from +having a way to diverge from the default visibility behavior of `#[no_mangle]` +symbols. And therefore it seems that the `#[export_visibility = ...]` attribute +proposed in this RFC would be useful not only in the current Rust edition, +but also in the hypothetical future Rust edition. + +# Prior art +[prior-art]: #prior-art + +## Other languages + +This RFC is quite directly based on how C/C++ supports +per-item `__attribute__ ((visibility ("default")))` (at least in +[Clang](https://clang.llvm.org/docs/AttributeReference.html#visibility) +and +[GCC](https://gcc.gnu.org/onlinedocs/gcc/Common-Function-Attributes.html#index-visibility-function-attribute)). +Using an assembly language, one can also use the `.hidden` directive +(e.g. see +[here](https://docs.oracle.com/cd/E26502_01/html/E28388/eoiyg.html#:~:text=.hidden%20symbol1%2C%20symbol2%2C%20...%2C%20symbolN)). + +It seems that so far a similar feature hasn't yet been introduced to other +languages that compile to native binary code: + +* It is unclear if GoLang has a way to explicitly specify visibility. + Using `#pragma GCC visibility push(hidden)` has been proposed as a workaround + (see [here](https://github.com/golang/go/issues/28340#issuecomment-466645246)). +* Haskell libraries can say `foreign export ccall some_function_name :: Int -> Int` + to export a function (see + [the Haskell wiki](https://wiki.haskell.org/Foreign_Function_Interface)). + Presumably such functions are publicly exported + (just as with Rust's `#[no_mangle]`). +* There is + [a proposal](https://forums.swift.org/t/current-status-of-swift-symbol-visibility/66949) + for Swift language to leverage + [the `package` access modifier](https://github.com/swiftlang/swift-evolution/blob/main/proposals/0386-package-access-modifier.md) as a way to specify public visibility. +* There is an open issue that tracks adding a similar mechanism to Zig: + https://github.com/ziglang/zig/issues/9762 + +## Rust language + +[`#[linkage...]` attribute](https://github.com/rust-lang/rust/issues/29603) +has been proposed in the past for specifying +[linkage type](https://llvm.org/docs/LangRef.html#linkage-types) of a symbol +(e.g. `weak`, `linkonce_odr`, etc.). +Linkage type is related to, but nevertheless different from +[linkage visibility](https://llvm.org/docs/LangRef.html#visibility-styles) +that this RFC focuses on. + +The `#[export_visibility = ...]` attribute has been earlier covered by a +Major Change Proposal (MCP) at +https://github.com/rust-lang/compiler-team/issues/881, but it was pointed +out that +"a compiler MCP isn't quite the right avenue here, +as attributes are part of the language." + +# Unresolved questions +[unresolved-questions]: #unresolved-questions + +## Interaction between `#[export_visibility = "hidden"]` vs `dylib`s +[hidden-vs-dylibs]: #interaction-between-export_visibility--hidden-vs-dylibs + +### Problem description + +https://github.com/rust-lang/rust/issues/73958#issuecomment-2635015556 +points out that using `#[export_visibility = "hidden"]` may break some `dylib` +scenarios. + +For example, let's say that a crate named `rlib` is compiled into an `rlib` with +the following functions: + +```rust +/// Let's say that this is an internal helper that is only intended to be called +/// from code within this library. To facilitate this, this function is *not* +/// `pub`. +/// +/// To also enable calling the helper from a friendly (also internal-only), +/// supporting C/C++ library we may use `#[no_mangle]`. To keep this function +/// internal and only enable directly calling this helper from statically-linked +/// C/C++ libraries we may /// use `#[export_visibility = "hidden"]`. We will +/// see below how the hidden visibility may have some undesirable +/// interactions with `dylib`s. +/// +/// TODO: Do we need `rustc` command-line examples that would show how such +/// static linking would be done when building a `staticlib`, `bin`, `cdylib`, +/// or a `dylib` (I haven't checked how/if this would work in all of those +/// cases; i.e. I haven't checked how to ask `rustc` to link with static +/// libraries produced by a C/C++ compiler). +#[no_mangle] +#[export_visibility = "hidden"] +fn internal_helper_called_from_rust_or_cpp() { todo!() } + +/// This is a public (`pub`) Rust function - it may be called from other Rust +/// crates. +/// +/// This function may internally (say, as an implementation detail) call +/// `fn internal_helper_called_from_rust_or_cpp` above. If this public function +/// gets inlined into another `dylib` then the call to the internal helper +/// will cross `dylib` boundaries - this will **not** work if the internal +/// helper is hidden from dynamic linking. +#[inline] +pub fn public_function() { + internal_helper_called_from_rust_or_cpp() +} +``` + +### Potential answers + +The following options have been identified so far as a potential way for +answering the `dylib`-vs-`hidden`-visibility problem: + +* Don't stabilize (or don't support at all) `#[export_visibility = "hidden"]` + but still support other visibilities +* Support `#[export_visibility = "hidden"]`, but + - Document that `hidden` visibility may break linking of `dylib`s + (see the "Hidden visibility" section in the guide-level explanation above) + - Document a recommendation that reusable crates shouldn't use a hardcoded + visibility (see the "Choosing the right visibility" section in the + guide-level explanation above) +* Investigate if cross-`dylib`-inlining can (should?) be avoided if the inlined + code ends up calling a hidden symbol from the other crate. + +## Cross-platform behavior + +We don't really know +whether the `hidden` / `protected` / `interposable` visibilities +make sense across different target platforms and/or map to distinct entities +(see +[a Zulip question here](https://rust-lang.zulipchat.com/#narrow/channel/233931-t-compiler.2Fmajor-changes/topic/.60.23.5Bexport_visibility.20.3D.20.2E.2E.2E.5D.60.20attribute.20compiler-team.23881/near/522491140)). + +One weak argument is that these visibilities are supported by LLVM and Clang, so hopefully +they would also make sense for Rust: + +* **LLVM**: Those visibilities are ultimately mapped from +[`rustc_target`'s `SymbolVisibility`](https://github.com/rust-lang/rust/blob/81a964c23ea4fe9ab52b4449bb166bf280035797/compiler/rustc_target/src/spec/mod.rs#L839-L843), +through +[`rustc_middle`'s `Visibility`](https://github.com/rust-lang/rust/blob/81a964c23ea4fe9ab52b4449bb166bf280035797/compiler/rustc_middle/src/mir/mono.rs#L396-L407), +and into +[`rustc_codegen_llvm`'s `Visibility`](https://github.com/rust-lang/rust/blob/81a964c23ea4fe9ab52b4449bb166bf280035797/compiler/rustc_codegen_llvm/src/llvm/ffi.rs#L153-L160). +So all the values make some sense at +[the LLVM level](https://llvm.org/docs/LangRef.html#visibility-styles). +* **Clang** and **GCC** support those 3 visibilities + (see the "Parity with C++" subsection in the "Motivation" section above). + +OTOH, ideally we would somehow check what happens on some representative subset +of target platforms (maybe: Posix, Windows, Wasm?): + +* TODO: what exactly do we want to verify on these target platforms? + +## Windows and `__declspec(dllexport)` +[interposable-vs-dllexport]: #windows-and-__declspecdllexport + +We need to decide whether `#[export_visibility = "interposable"]` should also +result in `__declspec((dllexport))` being added to a symbol. See for example +[this Stack Overflow question and answer](https://stackoverflow.com/a/25746044/24042981). + +Potential answers: + +* Don't stabilize for now (or don't support at all) + `#[export_visibility = "interposable"]` but still support other visibilities +* `#[export_visibility = "interposable"]` should only control visibility +* `#[export_visibility = "interposable"]` should control visibility + and also use `__declspec(dllexport)` + +## Interposability vs LLVM optimization passes +[interposable-vs-llvm]: #interposability-vs-llvm-optimization-passes + +This RFC proposes to use `interposable` to map to +[`SymbolVisibility::Interposable`](https://github.com/rust-lang/rust/blob/81a964c23ea4fe9ab52b4449bb166bf280035797/compiler/rustc_target/src/spec/mod.rs#L842) +which is then mapped to +[`llvm::Visibility::Default`](https://github.com/rust-lang/rust/blob/81a964c23ea4fe9ab52b4449bb166bf280035797/compiler/rustc_codegen_llvm/src/llvm/ffi.rs#L167). This mimics how `interposable` is implemented and supported +in +[`-Zdefault-visibility=...`](https://doc.rust-lang.org/beta/unstable-book/compiler-flags/default-visibility.html). + +One problem here is that `llvm::Visibility::Default` is not sufficient to +achieve actual interposability. https://crbug.com/418073233 has one example of +undefined behavior, but even if DSO-local global data structures were not an +issue, then LLVM-level assumptions could still lead to undefined behavior. +This is because the LLVM optimization passes assume that a symbol with normal +external linkage (not weak, odr, etc) the definition it can see is the +definition that will be actually used. To avoid these LLVM assumptions `rustc` +would have to enable +[the SemanticInterposition feature](https://clang.llvm.org/docs/ClangCommandLineReference.html#cmdoption-clang-fsemantic-interposition). + +Special thanks to @jyknight for pointing out this concern. + +Potential answers: + +* Don't stabilize for now (or don't support at all) + `#[export_visibility = "interposable"]` but still support other visibilities +* Rename `interposable` to `public` or `default`. + (It is quite unfortunate that `default` is an overloaded term and + may be potentially confused with the `inherit` behavior.) + +# Future possibilities +[future-possibilities]: #future-possibilities + +Couldn't think of anything so far. From f21a5271e64b0e093b04b9d7035fb63c5c81085f Mon Sep 17 00:00:00 2001 From: Lukasz Anforowicz Date: Fri, 20 Jun 2025 18:04:33 +0000 Subject: [PATCH 02/24] New unresolved question section: Rust standard library --- text/0000-export-visibility.md | 89 ++++++++++++++++++++++++++++++++++ 1 file changed, 89 insertions(+) diff --git a/text/0000-export-visibility.md b/text/0000-export-visibility.md index c843bb3538f..d2d56656d78 100644 --- a/text/0000-export-visibility.md +++ b/text/0000-export-visibility.md @@ -531,6 +531,95 @@ of target platforms (maybe: Posix, Windows, Wasm?): * TODO: what exactly do we want to verify on these target platforms? +## Rust standard library + +### Problem description + +The scope of this RFC is currently limited to just introducing the +`#[export_visibility = ...]` attribute. This should help realize the +benefits described by this RFC wherever the new attribute is used +(even if there remain places where the new attribute is not used). +OTOH this means that this RFC treats +visibility of Rust standard library symbols +as out of scope. + +Currently Rust standard library may end up exporting two kinds of symbols. +One kind is symbols using `#[rustc_std_internal_symbol]` attribute +(similar to `#[no_mangle]` so in theory `#[export_visibility = ...]` +attribute could be applied to such symbols). +An example can be found below: + +``` +$ git clone git@github.com:guidance-ai/llguidance.git +$ cd llguidance/parser +$ cargo rustc -- --crate-type=staticlib +... +$ nm --demangle --defined-only ../target/debug/libllguidance.a 2>/dev/null | grep __rustc:: +0000000000000000 T __rustc::__rust_alloc +0000000000000000 T __rustc::__rust_dealloc +0000000000000000 T __rustc::__rust_realloc +0000000000000000 T __rustc::__rust_alloc_zeroed +0000000000000000 T __rustc::__rust_alloc_error_handler +0000000000000000 B __rustc::__rust_alloc_error_handler_should_panic +00000000 T __rustc::__rust_probestack +``` + +But non-`#[rustc_std_internal_symbol]` symbols (e.g. +[`String::new`](https://github.com/rust-lang/rust/blob/9c4ff566babe632af5e30281a822d1ae9972873b/library/alloc/src/string.rs#L439-L446)) +can also end up publicly exported: + +``` +$ nm --demangle --defined-only ../target/debug/libllguidance.a 2>/dev/null \ + | grep alloc::string::String::new +0000000000000000 T alloc::string::String::new +0000000000000000 T alloc::string::String::new +0000000000000000 T alloc::string::String::new +0000000000000000 t alloc::string::String::new +0000000000000000 T alloc::string::String::new +0000000000000000 T alloc::string::String::new +0000000000000000 T alloc::string::String::new +``` + +> **Disclaimer**: The example above could be illustrated with other crates. +> It uses `llguidance` because: +> +> 1. it exposes C API +> (and therefore it is potentially useful to build it as a `staticlib`) +> 2. it happens to be used by Chromium so the RFC author is somewhat familiar +> with the crate +> 3. the RFC author had trouble building `rustc-demangle-capi` in this way +> (hitting `#[panic_handler]`-related errors). + +### Potential answers + +The following options have been identified so far as a potential way for +hiding symbols coming from Rust standard library: + +* Do nothing. + - Hiding symbols would require rebuilding Rust standard library with + `-Zdefault-visibility=hidden`. + - Note that there are other valid reasons + for rebuilding the standard library when building a given project. + For example this is a way to use globally consistent `-C` options + like `-Cpanic=abort`, + [`-Clto=no`](https://source.chromium.org/chromium/chromium/src/+/main:build/config/compiler/BUILD.gn;l=1115-1118;drc=26d51346374a0d16b0ba2243ef83c015a944d975), + etc. + - Rebuilding the standard library is possible, + although it is currently supported as an **unstable** + [`-Zbuild-std`](https://doc.rust-lang.org/cargo/reference/unstable.html#build-std) + command-line flag of `cargo`. + FWIW Chromium currently does rebuild the standard library + (using automated + [tooling](https://source.chromium.org/chromium/chromium/src/+/main:tools/rust/gnrt_stdlib.py;drc=628c608971bc01c96193055bb0848149cccde645) + to translate standard library's `Cargo.toml` files into + [equivalent `BUILD.gn` rules](https://source.chromium.org/chromium/chromium/src/+/main:build/rust/std/rules/BUILD.gn;drc=35fb76c686b55acc25b53f7e5c9b58e56dca7f4a)), + which is one reason why this RFC is a viable UB fix for + https://crbug.com/418073233. +* Alternative: change the semantics of `#[rustc_std_internal_symbol]` + - Drawback: On its own this wouldn't affect + visibility of non-`#[rustc_std_internal_symbol]` symbols + like `String::new`. + ## Windows and `__declspec(dllexport)` [interposable-vs-dllexport]: #windows-and-__declspecdllexport From 58f6c1ed5f220a6b072f8a4e2ee01bd4d5ad2ded Mon Sep 17 00:00:00 2001 From: Lukasz Anforowicz Date: Mon, 23 Jun 2025 20:29:27 +0000 Subject: [PATCH 03/24] New alternative answer for Rust std lib: Partial linking. --- text/0000-export-visibility.md | 18 ++++++++++++++---- 1 file changed, 14 insertions(+), 4 deletions(-) diff --git a/text/0000-export-visibility.md b/text/0000-export-visibility.md index d2d56656d78..757c13b2be0 100644 --- a/text/0000-export-visibility.md +++ b/text/0000-export-visibility.md @@ -615,10 +615,20 @@ hiding symbols coming from Rust standard library: [equivalent `BUILD.gn` rules](https://source.chromium.org/chromium/chromium/src/+/main:build/rust/std/rules/BUILD.gn;drc=35fb76c686b55acc25b53f7e5c9b58e56dca7f4a)), which is one reason why this RFC is a viable UB fix for https://crbug.com/418073233. -* Alternative: change the semantics of `#[rustc_std_internal_symbol]` - - Drawback: On its own this wouldn't affect - visibility of non-`#[rustc_std_internal_symbol]` symbols - like `String::new`. +* Alternative: support in `rustc` emitting a `staticlib` with symbol visibility + that matches `cdylib` behavior. + - Initially this behavior would be gated behind a new unstable `-Z` flag. + - Either a new flag: `-Zhide-rust-level-exports` + - Or by ensuring that `-Zdefault-visibility=hidden` also applies + to Rust-level exports transitively linked into a `staticlib` + - Open question: should this behavior become the default in the future? + - Implementation-wise, this behavior can be seen as partial linking or + as object file rewriting. + - It seems that this behavior makes most sense for `staticlib`s, because + `rlib`s don't include transitive dependencies. So when linking `rlib`s + (using an external, non-`rustc` linker) hiding standard library symbols + would still require rebuilding it with `-Zdefault-visibility=hidden` + as described in the "do nothing" alternative above. ## Windows and `__declspec(dllexport)` [interposable-vs-dllexport]: #windows-and-__declspecdllexport From 5a003a403005d07b3a00e56125b8e696ee34e501 Mon Sep 17 00:00:00 2001 From: Lukasz Anforowicz Date: Wed, 30 Jul 2025 23:51:34 +0000 Subject: [PATCH 04/24] New alternative: `#[rust_symbol_export_level]`. --- text/0000-export-visibility.md | 43 ++++++++++++++++++++++++++++++++++ 1 file changed, 43 insertions(+) diff --git a/text/0000-export-visibility.md b/text/0000-export-visibility.md index 757c13b2be0..615d8d90804 100644 --- a/text/0000-export-visibility.md +++ b/text/0000-export-visibility.md @@ -305,6 +305,48 @@ See "Open questions" section. # Rationale and alternatives [rationale-and-alternatives]: #rationale-and-alternatives +## Alternative: `#[rust_symbol_export_level]` + +The `#[export_visibility = ...]` proposed in this RFC supports directly +controlling an exact visibility level of a symbol. One alternative is +to control the visibility indirectly, levereging the fact that `#[no_mangle]` +and `#[export_name = ...]` symbols are currently public only because: + +* Such symbols are treated as `SymbolExportLevel::C`: + https://github.com/rust-lang/rust/blob/3048886e59c94470e726ecaaf2add7242510ac11/compiler/rustc_codegen_ssa/src/back/symbol_export.rs#L593-L605 +* `SymbolExportLevel::C` translates into public visibility, but + visibility of `SymbolExportLevel::Rust` may be controlled via + `-Zdefault-visibility=...`: + https://github.com/rust-lang/rust/blob/3048886e59c94470e726ecaaf2add7242510ac11/compiler/rustc_monomorphize/src/partitioning.rs#L941-L948 + +Special thanks to @bjorn3 for proposing this alternative in +https://github.com/rust-lang/rfcs/pull/3834#issuecomment-2978073435 + +This alternative has been prototyped in +https://github.com/rust-lang/rust/commit/9dd4d3f6b2beecb85ff4220502a8c7f61edca839 +and tested to verify that it also addresses https://crbug.com/418073233 +(with similar test/repro steps as in #comment12 of that bug, but using +https://crrev.com/c/6580611/3). + +Other notes: + +* Pros: + - It is simpler + (`#[rust_symbol_export_level]` vs `#[export_visibility = ""]`) + both for users and for implementation. + - It avoids some problems and open questions associated with + `#[export_visibility = ...]`: + - No `dylib` trouble (see [hidden-vs-dylibs]) + - No need to define behavior of specific visibilities - this question + is punted to `-Zdefault-visibility=...`. + See also [cross-platform-behavior], [interposable-vs-dllexport] + and [interposable-vs-llvm]. +* Cons: + - Doesn't give the same level of control as C++ attributes +* Open questions: + - The name of the attribute proposed in this alternative is subject to + change if a better name is proposed. + ## Alternative: version scripts Using @@ -505,6 +547,7 @@ answering the `dylib`-vs-`hidden`-visibility problem: code ends up calling a hidden symbol from the other crate. ## Cross-platform behavior +[cross-platform-behavior]: #cross-platform-behavior We don't really know whether the `hidden` / `protected` / `interposable` visibilities From 3b92c0b633bb06e451fdaac1becf90397dec46fb Mon Sep 17 00:00:00 2001 From: Lukasz Anforowicz Date: Thu, 31 Jul 2025 00:09:51 +0000 Subject: [PATCH 05/24] Mention the inverse of `#[rust_symbol_export_level]` --- text/0000-export-visibility.md | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/text/0000-export-visibility.md b/text/0000-export-visibility.md index 615d8d90804..888e12f0d96 100644 --- a/text/0000-export-visibility.md +++ b/text/0000-export-visibility.md @@ -346,6 +346,11 @@ Other notes: * Open questions: - The name of the attribute proposed in this alternative is subject to change if a better name is proposed. +* Possible follow-ups (but probably out-of-scope for this RFC): + - @chorman0773 pointed out in + https://github.com/rust-lang/rfcs/pull/3834#issuecomment-2981459636 + that an inverse attribute may also be desirable in some scenarios + (e.g. `c_symbol_export_level`). ## Alternative: version scripts From 2d5eea827d64382f5b632a908e8f03d75f61dce3 Mon Sep 17 00:00:00 2001 From: Lukasz Anforowicz Date: Mon, 6 Oct 2025 18:38:40 +0000 Subject: [PATCH 06/24] Remove support for "interposable" visibility - it would be a no-op. --- text/0000-export-visibility.md | 122 ++++++++++----------------------- 1 file changed, 38 insertions(+), 84 deletions(-) diff --git a/text/0000-export-visibility.md b/text/0000-export-visibility.md index 888e12f0d96..a45d48a88f9 100644 --- a/text/0000-export-visibility.md +++ b/text/0000-export-visibility.md @@ -162,32 +162,6 @@ The `#[export_visibility = ...]` attribute uses the syntax to specify the desired visibility. The following sections describe string values that may be used. -### Interposable visibility - -`#[export_visibility = "interposable"]` will cause symbols to be emitted with -"default" visibility. On platforms that support it, this makes it so that -symbols can be interposed, which means that they can be overridden by symbols -with the same name from the executable or by other shared objects earlier in the -load order. - -> **Note**: -> See [interposable-vs-llvm] section below for discussion about an open -> question that asks about interactions between `interposable` visibility -> and LLVM optimization passes. - -> **Note**: -> See [interposable-vs-dllexport] section below for discussion whether -> this visibility should also inject `dllexport` when targeting Windows -> platform. - -> **TODO**: This section (as well as `protected` and `hidden` sections below) is based on -> https://doc.rust-lang.org/beta/unstable-book/compiler-flags/default-visibility.html#interposable -> In the long-term we should deduplicated these docs/definitions (for example -> description of `hidden` in this RFC is a bit expanded and brings up additional -> benefits of hiding symbols). "long-term" probably means: 1) once this or the -> other feature have been stabilized and/or 2) once we are confident with names, -> behavior, etc of all the visibility levels. - ### Protected visibility - -`#[export_visibility = "protected"]` signals to the compiler, the linker, and -the runtime linker that the symbol cannot be overridden by the executable or by -other shared objects earlier in the load order. - -This allows the compiler to emit direct references to symbols, which may improve -performance. It also removes the need for these symbols to be resolved when a -shared object built with this option is loaded. - -Using protected visibility when linking with GNU `ld` prior to 2.40 will result -in linker errors when building for Linux. Other linkers such as LLD are not -affected. - -### Hidden visibility - - - -`#[export_visibility = "hidden"]` marks the symbol as hidden. -Hidden symbols will not be exported from the created shared object, so cannot be -referenced from other shared objects or from executables. - - -Similarly to protected visibility, hidden visibility may allow the compiler -to improve performance of the generated code by -emitting direct references to symbols. -And it may remove the runtime overhead of linking these symbols at runtime. - - -Unlike protected visibility, hidden visibility may also enable additional inlining -during Link Time Optimization (LTO), which may be especially important for small -functions (thunks) used for cross-language calls. It may also limit the scope -of the safety risk of having 2 symbols with the same name. - - -`hidden` visibility should *not* be used when it is not possible to control -whether the symbol may be referenced from another shared object. For example, -`hidden` visibility should be avoided when building `dylib`s, because -cross-`dylib` inlining may lead to linking errors. - -> **Note**: -> See [hidden-vs-dylibs] section below for more discussion on what to do -> about the interaction between `dylibs` and this RFC. - ### Inherited visibility `#[export_visibility = "inherit"]` uses @@ -299,21 +238,6 @@ supports overriding the target platform's visibility with the [`-Zdefault-visibility=...`](https://doc.rust-lang.org/beta/unstable-book/compiler-flags/default-visibility.html) command-line flag. -### Choosing the right visibility - -If you control the linking process (i.e. you control how your symbols are linked -into an executable, or into a `cdylib`, `so` or `dll`), then you should use the -lowest possible visibility. If a public export is not needed, then use the -`hidden` visibility. - -If you are an author of a reusable crate, then you don't know how users of your -crate will link it into executables, `cdylib`s, `dylib`s, etc. In this case it -is best to give control over visibility of your symbols to your clients by using -`#[export_visibility = "inherit"]`. Alternatively (e.g. if you provide a proc -macro to generate the exported symbols) you can consider parametrizing the -behavior of your crate to let your clients specify the exact visibility that -your library will declare through the `#[export_visibility = ...]` attribute. - # Reference-level explanation [reference-level-explanation]: #reference-level-explanation @@ -582,107 +506,6 @@ as attributes are part of the language." # Unresolved questions [unresolved-questions]: #unresolved-questions -## Open question: `#[export_visibility = "hidden"]` vs `dylib`s -[hidden-vs-dylibs]: #interaction-between-export_visibility--hidden-vs-dylibs - -### Problem description - -https://github.com/rust-lang/rust/issues/73958#issuecomment-2635015556 -points out that using `#[export_visibility = "hidden"]` may break some `dylib` -scenarios. - -For example, let's say that a crate named `rlib` is compiled into an `rlib` with -the following functions: - -```rust -/// Let's say that this is an internal helper that is only intended to be called -/// from code within this library. To facilitate this, this function is *not* -/// `pub`. -/// -/// To also enable calling the helper from a friendly (also internal-only), -/// supporting C/C++ library we may use `#[no_mangle]`. To keep this function -/// internal and only enable directly calling this helper from statically-linked -/// C/C++ libraries we may /// use `#[export_visibility = "hidden"]`. We will -/// see below how the hidden visibility may have some undesirable -/// interactions with `dylib`s. -/// -/// TODO: Do we need `rustc` command-line examples that would show how such -/// static linking would be done when building a `staticlib`, `bin`, `cdylib`, -/// or a `dylib` (I haven't checked how/if this would work in all of those -/// cases; i.e. I haven't checked how to ask `rustc` to link with static -/// libraries produced by a C/C++ compiler). -#[no_mangle] -#[export_visibility = "hidden"] -fn internal_helper_called_from_rust_or_cpp() { todo!() } - -/// This is a public (`pub`) Rust function - it may be called from other Rust -/// crates. -/// -/// This function may internally (say, as an implementation detail) call -/// `fn internal_helper_called_from_rust_or_cpp` above. If this public function -/// gets inlined into another `dylib` then the call to the internal helper -/// will cross `dylib` boundaries - this will **not** work if the internal -/// helper is hidden from dynamic linking. -#[inline] -pub fn public_function() { - internal_helper_called_from_rust_or_cpp() -} -``` - -### Potential answers - -The following options have been identified so far as a potential way for -answering the `dylib`-vs-`hidden`-visibility problem: - -* Don't stabilize (or don't support at all) `#[export_visibility = "hidden"]` - but still support other visibilities -* Support `#[export_visibility = "hidden"]`, but - - Document that `hidden` visibility may break linking of `dylib`s - (see the "Hidden visibility" section in the guide-level explanation above) - - Document a recommendation that reusable crates shouldn't use a hardcoded - visibility (see the "Choosing the right visibility" section in the - guide-level explanation above) -* Avoid inlining if the inlined code ends up calling a hidden symbol from - another `dylib` - - Currently preventing inlining is problematic, because `#[inline]` will - stop the function from being codegened in the original crate unless used - (hattip - [@chorman0773](https://github.com/rust-lang/rfcs/pull/3834#issuecomment-3352655525)). - OTOH, this doesn't necessarily seem like a hard blocker (i.e. maybe this - behavior can change). - - Generics also cannot have code generated in the original crate, because - codegen requires knowing the generic parameters. But generics seem - irrelevant here, because `#[export_visibility = ...]` does _not_ apply to - generics. In particular, `#[no_mangle]` - ([Rust - playground](https://play.rust-lang.org/?version=stable&mode=debug&edition=2024&gist=ac8f26f9b05471c2480b3185388c05e8)) - and `#[export_name = ...]` cannot be used with generics, because the names - of the symbols (ones generated during monomorphization) need to differ - based on the generic parameters. - - One major problem with avoiding inlining is that during codegen it is not yet - known if two crates will end up getting linked into the same or different - dylib. This means that inlining would need to be inhibited for any - cross-crate calls into hidden symbols. And this would suppress many - legitimate optimizations. (hattip - [@bjorn3](https://github.com/rust-lang/rfcs/pull/3834#issuecomment-3352658642)) -* Add a lint/warning that detects when `#[export_visibility = ...]` is used - inappropriately - - Sub-idea 1: when a hidden function is called from a caller that may be - inlined into another crate. (hattip - [@tmandry](https://github.com/rust-lang/rfcs/pull/3834#issuecomment-3282373591)) - - This idea is problematic, because using inlineability for restricting - how source programs are written means committing to implementation - details of rustc’s codegen strategy. For example, `rustc` currently - has some logic to treat small functions as-if they were `#[inline]` - for codegen purposes even if they weren’t declared as such in the - source code. (hattip - [@hanna-kruppe](https://github.com/rust-lang/rfcs/pull/3834#discussion_r2395437679)) - - Sub-idea 2: when a hidden function is called _at all_ from another Rust - function - - This seems very drastic, but in practice `#[no_mangle]` are oftentimes - called only from _another, non-Rust_ language. This is definitely the - case for FFI thunks used as one of motivating examples in this RFC. - ## Open question: Cross-platform behavior [cross-platform-behavior]: #cross-platform-behavior @@ -813,4 +636,183 @@ hiding symbols coming from Rust standard library: # Future possibilities [future-possibilities]: #future-possibilities -Couldn't think of anything so far. +## Provide reference-level definitions of supported visibility levels + +`#[export = "inherited"]` defers the choice of an actual visibility level to + +1. Session-wide default of + [`SymbolVisibility::Interposable`](https://github.com/rust-lang/rust/blob/910617d84d611e9ba508fd57a058c59b8a767697/compiler/rustc_session/src/session.rs#L551-L557) +2. Unless overridden by target platform’s default visibility specified in + [`rustc_target::spec::TargetOptions`](https://github.com/rust-lang/rust/blob/910617d84d611e9ba508fd57a058c59b8a767697/compiler/rustc_target/src/spec/mod.rs#L2225-L2230), +3. Or overridden by + [`-Zdefault-visibility=...`](https://doc.rust-lang.org/beta/unstable-book/compiler-flags/default-visibility.html) command-line flag. + +This means that _this_ RFC doesn't necessarily need to +define the exact semantics and behavior of supported visibility levels. +OTOH, such definitions may be desirable in the future: + +* If/when stabilizing `-Zdefault-visibility=...` +* If/when extending `#[export_visibility = ...]` to support specific visibility + levels (i.e. if the attribute would support not only the `"inherit"` + visibility value, but also `"hidden"`, `"protected"`, and/or + `"interposable"`). + +One way to provide such definitions would be to map different visibility levels +into specific behavior on the supported Tier 1 platforms. This can be limited +to documenting the impact for ELF, Mach-O, and PE binaries, because all of +[Tier 1 target triples](https://doc.rust-lang.org/beta/rustc/platform-support.html#tier-1-with-host-tools) +use one of those three binary formats: + +* `aarch64-apple-darwin`: MachO (documented + [here](https://doc.rust-lang.org/beta/rustc/platform-support/apple-darwin.html#binary-format)) +* `aarch64-pc-windows-msvc`: PE/COFF (documented + [here](https://doc.rust-lang.org/beta/rustc/platform-support/windows-msvc.html#platform-details)) +* `aarch64-unknown-linux-gnu`: ELF +* `i686-pc-windows-msvc`: PE/COFF (same documentation as above) +* `i686-unknown-linux-gnu`: ELF +* `x86_64-pc-windows-gnu`: PE (documented + [here](https://doc.rust-lang.org/beta/rustc/platform-support/windows-gnu.html#requirements)) +* `x86_64-unknown-linux-gnu`: ELF + +Ad-hoc, manual tests (TODO: link to a GitHub comment) +of `#[export_visibility = "inherit"]` provide some +reassurance that such definitions should be possible in the future. +OTOH, when future RFCs or PRs consider implementing specific visibility levels, +they should ideally come with: + +* Codegen tests that verify how `#[export_visibility = …]` is translated into + LLVM syntax +* End-to-end tests for 3 platforms that cover ELF, Mach-O, and PE binaries. + Verification in such tests would most likely have to depend on arbitrary + developer tools (e.g. + [`readelf`](https://man7.org/linux/man-pages/man1/readelf.1.html) or + [`dumpbin`](https://learn.microsoft.com/en-us/cpp/build/reference/dumpbin-reference?view=msvc-170)) + and therefore such tests would most likely have to be + `make`-based. + +## Support hidden visibility + +In the future, we may consider supporting `#[export_visibility = “hidden”]`. +In terms of the internal `rustc` APIs this would map to +[`rustc_target::spec::SymbolVisibility::Hidden`](https://github.com/rust-lang/rust/blob/910617d84d611e9ba508fd57a058c59b8a767697/compiler/rustc_target/src/spec/mod.rs#L884). +The hidden visibility would have the following impact on Tier 1 binaries: + +* ELF binaries: The symbol is marked + [`STV_HIDDEN`](https://man7.org/linux/man-pages/man5/elf.5.html#:~:text=specific%20hidden%20class.-,STV_HIDDEN,-Symbol%20is%20unavailable) +* PE binaries: The symbol is non-exported (i.e. the symbol is not listed in + [the `.edata` section](https://learn.microsoft.com/en-us/windows/win32/debug/pe-format#the-edata-section-image-only)) +* MachO binaries: The symbol is non-exported (i.e. the symbol is not listed in + [the export trie](https://github.com/apple-oss-distributions/xnu/blob/8d741a5de7ff4191bf97d57b9f54c2f6d4a15585/EXTERNAL_HEADERS/mach-o/loader.h#L1369)) + +### Open question: `#[export_visibility = "hidden"]` vs `dylib`s +[hidden-vs-dylibs]: #interaction-between-export_visibility--hidden-vs-dylibs + +#### Problem description + +https://github.com/rust-lang/rust/issues/73958#issuecomment-2635015556 +points out that using `#[export_visibility = "hidden"]` may break some `dylib` +scenarios. + +For example, let's say that a crate named `rlib` is compiled into an `rlib` with +the following functions: + +```rust +/// Let's say that this is an internal helper that is only intended to be called +/// from code within this library. To facilitate this, this function is *not* +/// `pub`. +/// +/// To also enable calling the helper from a friendly (also internal-only), +/// supporting C/C++ library we may use `#[no_mangle]`. To keep this function +/// internal and only enable directly calling this helper from statically-linked +/// C/C++ libraries we may /// use `#[export_visibility = "hidden"]`. We will +/// see below how the hidden visibility may have some undesirable +/// interactions with `dylib`s. +/// +/// TODO: Do we need `rustc` command-line examples that would show how such +/// static linking would be done when building a `staticlib`, `bin`, `cdylib`, +/// or a `dylib` (I haven't checked how/if this would work in all of those +/// cases; i.e. I haven't checked how to ask `rustc` to link with static +/// libraries produced by a C/C++ compiler). +#[no_mangle] +#[export_visibility = "hidden"] +fn internal_helper_called_from_rust_or_cpp() { todo!() } + +/// This is a public (`pub`) Rust function - it may be called from other Rust +/// crates. +/// +/// This function may internally (say, as an implementation detail) call +/// `fn internal_helper_called_from_rust_or_cpp` above. If this public function +/// gets inlined into another `dylib` then the call to the internal helper +/// will cross `dylib` boundaries - this will **not** work if the internal +/// helper is hidden from dynamic linking. +#[inline] +pub fn public_function() { + internal_helper_called_from_rust_or_cpp() +} +``` + +#### Potential answers + +The following options have been identified so far as a potential way for +answering the `dylib`-vs-`hidden`-visibility problem: + +* Don't stabilize (or don't support at all) `#[export_visibility = "hidden"]` + but still support other visibilities +* Support `#[export_visibility = "hidden"]`, but + - Document that `hidden` visibility may break linking of `dylib`s + (see the "Hidden visibility" section in the guide-level explanation above) + - Document a recommendation that reusable crates shouldn't use a hardcoded + visibility (see the "Choosing the right visibility" section in the + guide-level explanation above) +* Avoid inlining if the inlined code ends up calling a hidden symbol from + another `dylib` + - Currently preventing inlining is problematic, because `#[inline]` will + stop the function from being codegened in the original crate unless used + (hattip + [@chorman0773](https://github.com/rust-lang/rfcs/pull/3834#issuecomment-3352655525)). + OTOH, this doesn't necessarily seem like a hard blocker (i.e. maybe this + behavior can change). + - Generics also cannot have code generated in the original crate, because + codegen requires knowing the generic parameters. But generics seem + irrelevant here, because `#[export_visibility = ...]` does _not_ apply to + generics. In particular, `#[no_mangle]` + ([Rust + playground](https://play.rust-lang.org/?version=stable&mode=debug&edition=2024&gist=ac8f26f9b05471c2480b3185388c05e8)) + and `#[export_name = ...]` cannot be used with generics, because the names + of the symbols (ones generated during monomorphization) need to differ + based on the generic parameters. + - One major problem with avoiding inlining is that during codegen it is not yet + known if two crates will end up getting linked into the same or different + dylib. This means that inlining would need to be inhibited for any + cross-crate calls into hidden symbols. And this would suppress many + legitimate optimizations. (hattip + [@bjorn3](https://github.com/rust-lang/rfcs/pull/3834#issuecomment-3352658642)) +* Add a lint/warning that detects when `#[export_visibility = ...]` is used + inappropriately + - Sub-idea 1: when a hidden function is called from a caller that may be + inlined into another crate. (hattip + [@tmandry](https://github.com/rust-lang/rfcs/pull/3834#issuecomment-3282373591)) + - This idea is problematic, because using inlineability for restricting + how source programs are written means committing to implementation + details of rustc’s codegen strategy. For example, `rustc` currently + has some logic to treat small functions as-if they were `#[inline]` + for codegen purposes even if they weren’t declared as such in the + source code. (hattip + [@hanna-kruppe](https://github.com/rust-lang/rfcs/pull/3834#discussion_r2395437679)) + - Sub-idea 2: when a hidden function is called _at all_ from another Rust + function + - This seems very drastic, but in practice `#[no_mangle]` are oftentimes + called only from _another, non-Rust_ language. This is definitely the + case for FFI thunks used as one of motivating examples in this RFC. + +## Support protected visibility + +In the future, we may consider supporting `#[export_visibility = “protected”]`. + +Open question: + +* Need to clarify how `protected` vs `interposable` visibilities would work for + Tier 1 platforms. In particular, it seems that PE and Mach-O binary formats + may not be able to distinguish between `protected` and `interposable` + visibilities (the latter is the default when a `#[no_mangle]` symbol is not + accompanied by `#[export_visibility = ...]`). From 05019fe925fb1e7c7289b3a7a4f2489ade72402a Mon Sep 17 00:00:00 2001 From: Lukasz Anforowicz Date: Tue, 14 Oct 2025 17:34:00 +0000 Subject: [PATCH 15/24] Remove an open question about cross-platform behavior. Rationale: * This question is no longer applicable after only supporting the `"inherit"` visibility. * Additionally, this future question will be proactively explored for `"hidden"` visibility - results will be shared through a comment on the RFC PR at https://github.com/rust-lang/rfcs/pull/3834 --- text/0000-export-visibility.md | 28 ---------------------------- 1 file changed, 28 deletions(-) diff --git a/text/0000-export-visibility.md b/text/0000-export-visibility.md index 1630c56a05e..229b56a1522 100644 --- a/text/0000-export-visibility.md +++ b/text/0000-export-visibility.md @@ -506,34 +506,6 @@ as attributes are part of the language." # Unresolved questions [unresolved-questions]: #unresolved-questions -## Open question: Cross-platform behavior -[cross-platform-behavior]: #cross-platform-behavior - -We don't really know -whether the `hidden` / `protected` visibilities -make sense across different target platforms and/or map to distinct entities -(see -[a Zulip question here](https://rust-lang.zulipchat.com/#narrow/channel/233931-t-compiler.2Fmajor-changes/topic/.60.23.5Bexport_visibility.20.3D.20.2E.2E.2E.5D.60.20attribute.20compiler-team.23881/near/522491140)). - -One weak argument is that these visibilities are supported by LLVM and Clang, so hopefully -they would also make sense for Rust: - -* **LLVM**: Those visibilities are ultimately mapped from -[`rustc_target`'s `SymbolVisibility`](https://github.com/rust-lang/rust/blob/81a964c23ea4fe9ab52b4449bb166bf280035797/compiler/rustc_target/src/spec/mod.rs#L839-L843), -through -[`rustc_middle`'s `Visibility`](https://github.com/rust-lang/rust/blob/81a964c23ea4fe9ab52b4449bb166bf280035797/compiler/rustc_middle/src/mir/mono.rs#L396-L407), -and into -[`rustc_codegen_llvm`'s `Visibility`](https://github.com/rust-lang/rust/blob/81a964c23ea4fe9ab52b4449bb166bf280035797/compiler/rustc_codegen_llvm/src/llvm/ffi.rs#L153-L160). -So all the values make some sense at -[the LLVM level](https://llvm.org/docs/LangRef.html#visibility-styles). -* **Clang** and **GCC** support those 3 visibilities - (see the "Parity with C++" subsection in the "Motivation" section above). - -OTOH, ideally we would somehow check what happens on some representative subset -of target platforms (maybe: Posix, Windows, Wasm?): - -* TODO: what exactly do we want to verify on these target platforms? - ## Open question: Rust standard library ### Problem description From 10ec93b1a144a45caf8b866af90b40c5ffca67f0 Mon Sep 17 00:00:00 2001 From: Lukasz Anforowicz Date: Tue, 14 Oct 2025 17:45:59 +0000 Subject: [PATCH 16/24] Move Rust std library concerns in a resolved section --- text/0000-export-visibility.md | 185 ++++++++++++++++----------------- 1 file changed, 88 insertions(+), 97 deletions(-) diff --git a/text/0000-export-visibility.md b/text/0000-export-visibility.md index 229b56a1522..5adbcbf866a 100644 --- a/text/0000-export-visibility.md +++ b/text/0000-export-visibility.md @@ -454,69 +454,20 @@ symbols. And therefore it seems that the `#[export_visibility = ...]` attribute proposed in this RFC would be useful not only in the current Rust edition, but also in the hypothetical future Rust edition. -# Prior art -[prior-art]: #prior-art +## Rationale: Okay to have no impact on Rust standard library -## Other languages +This RFC treats visibility of Rust standard library symbols as out of scope. +`-Zdefault-visibility=...` remains the only way to control symbol visibility +of the Rust standard library (assumming that it can be rebuilt with this +command-line flag). This is ok - the RFC is beneficial even with this limited +scope. -This RFC is quite directly based on how C/C++ supports -per-item `__attribute__ ((visibility ("default")))` (at least in -[Clang](https://clang.llvm.org/docs/AttributeReference.html#visibility) -and -[GCC](https://gcc.gnu.org/onlinedocs/gcc/Common-Function-Attributes.html#index-visibility-function-attribute)). -Using an assembly language, one can also use the `.hidden` directive -(e.g. see -[here](https://docs.oracle.com/cd/E26502_01/html/E28388/eoiyg.html#:~:text=.hidden%20symbol1%2C%20symbol2%2C%20...%2C%20symbolN)). +More details about symbols exported from the Rust standard library can be +found in the folded details section below: -It seems that so far a similar feature hasn't yet been introduced to other -languages that compile to native binary code: +
-* It is unclear if GoLang has a way to explicitly specify visibility. - Using `#pragma GCC visibility push(hidden)` has been proposed as a workaround - (see [here](https://github.com/golang/go/issues/28340#issuecomment-466645246)). -* Haskell libraries can say `foreign export ccall some_function_name :: Int -> Int` - to export a function (see - [the Haskell wiki](https://wiki.haskell.org/Foreign_Function_Interface)). - Presumably such functions are publicly exported - (just as with Rust's `#[no_mangle]`). -* There is - [a proposal](https://forums.swift.org/t/current-status-of-swift-symbol-visibility/66949) - for Swift language to leverage - [the `package` access modifier](https://github.com/swiftlang/swift-evolution/blob/main/proposals/0386-package-access-modifier.md) as a way to specify public visibility. -* There is an open issue that tracks adding a similar mechanism to Zig: - https://github.com/ziglang/zig/issues/9762 - -## Rust language - -[`#[linkage...]` attribute](https://github.com/rust-lang/rust/issues/29603) -has been proposed in the past for specifying -[linkage type](https://llvm.org/docs/LangRef.html#linkage-types) of a symbol -(e.g. `weak`, `linkonce_odr`, etc.). -Linkage type is related to, but nevertheless different from -[linkage visibility](https://llvm.org/docs/LangRef.html#visibility-styles) -that this RFC focuses on. - -The `#[export_visibility = ...]` attribute has been earlier covered by a -Major Change Proposal (MCP) at -https://github.com/rust-lang/compiler-team/issues/881, but it was pointed -out that -"a compiler MCP isn't quite the right avenue here, -as attributes are part of the language." - -# Unresolved questions -[unresolved-questions]: #unresolved-questions - -## Open question: Rust standard library - -### Problem description - -The scope of this RFC is currently limited to just introducing the -`#[export_visibility = ...]` attribute. This should help realize the -benefits described by this RFC wherever the new attribute is used -(even if there remain places where the new attribute is not used). -OTOH this means that this RFC treats -visibility of Rust standard library symbols -as out of scope. +### Symbols exported from Rust standard library Currently Rust standard library may end up exporting two kinds of symbols. One kind is symbols using `#[rustc_std_internal_symbol]` attribute @@ -565,45 +516,85 @@ $ nm --demangle --defined-only ../target/debug/libllguidance.a 2>/dev/null \ > 3. the RFC author had trouble building `rustc-demangle-capi` in this way > (hitting `#[panic_handler]`-related errors). -### Potential answers +### Justification for relying on `-Zdefault-visibility=...` + +Symbols can be hidden by rebuilding Rust standard library with +`-Zdefault-visibility=hidden`. + +There are other valid reasons +for rebuilding the standard library when building a given project. +For example this is a way to use globally consistent `-C` options +like `-Cpanic=abort`, +[`-Clto=no`](https://source.chromium.org/chromium/chromium/src/+/main:build/config/compiler/BUILD.gn;l=1115-1118;drc=26d51346374a0d16b0ba2243ef83c015a944d975), +etc. + +Rebuilding the standard library is possible, +although it is currently supported as an **unstable** +[`-Zbuild-std`](https://doc.rust-lang.org/cargo/reference/unstable.html#build-std) +command-line flag of `cargo`. +FWIW Chromium currently does rebuild the standard library +(using automated +[tooling](https://source.chromium.org/chromium/chromium/src/+/main:tools/rust/gnrt_stdlib.py;drc=628c608971bc01c96193055bb0848149cccde645) +to translate standard library's `Cargo.toml` files into +[equivalent `BUILD.gn` rules](https://source.chromium.org/chromium/chromium/src/+/main:build/rust/std/rules/BUILD.gn;drc=35fb76c686b55acc25b53f7e5c9b58e56dca7f4a)), +which is one reason why this RFC is a viable UB fix for +https://crbug.com/418073233. -The following options have been identified so far as a potential way for -hiding symbols coming from Rust standard library: - -* Do nothing. - - Hiding symbols would require rebuilding Rust standard library with - `-Zdefault-visibility=hidden`. - - Note that there are other valid reasons - for rebuilding the standard library when building a given project. - For example this is a way to use globally consistent `-C` options - like `-Cpanic=abort`, - [`-Clto=no`](https://source.chromium.org/chromium/chromium/src/+/main:build/config/compiler/BUILD.gn;l=1115-1118;drc=26d51346374a0d16b0ba2243ef83c015a944d975), - etc. - - Rebuilding the standard library is possible, - although it is currently supported as an **unstable** - [`-Zbuild-std`](https://doc.rust-lang.org/cargo/reference/unstable.html#build-std) - command-line flag of `cargo`. - FWIW Chromium currently does rebuild the standard library - (using automated - [tooling](https://source.chromium.org/chromium/chromium/src/+/main:tools/rust/gnrt_stdlib.py;drc=628c608971bc01c96193055bb0848149cccde645) - to translate standard library's `Cargo.toml` files into - [equivalent `BUILD.gn` rules](https://source.chromium.org/chromium/chromium/src/+/main:build/rust/std/rules/BUILD.gn;drc=35fb76c686b55acc25b53f7e5c9b58e56dca7f4a)), - which is one reason why this RFC is a viable UB fix for - https://crbug.com/418073233. -* Alternative: support in `rustc` emitting a `staticlib` with symbol visibility - that matches `cdylib` behavior. - - Initially this behavior would be gated behind a new unstable `-Z` flag. - - Either a new flag: `-Zhide-rust-level-exports` - - Or by ensuring that `-Zdefault-visibility=hidden` also applies - to Rust-level exports transitively linked into a `staticlib` - - Open question: should this behavior become the default in the future? - - Implementation-wise, this behavior can be seen as partial linking or - as object file rewriting. - - It seems that this behavior makes most sense for `staticlib`s, because - `rlib`s don't include transitive dependencies. So when linking `rlib`s - (using an external, non-`rustc` linker) hiding standard library symbols - would still require rebuilding it with `-Zdefault-visibility=hidden` - as described in the "do nothing" alternative above. +
+ +# Prior art +[prior-art]: #prior-art + +## Other languages + +This RFC is quite directly based on how C/C++ supports +per-item `__attribute__ ((visibility ("default")))` (at least in +[Clang](https://clang.llvm.org/docs/AttributeReference.html#visibility) +and +[GCC](https://gcc.gnu.org/onlinedocs/gcc/Common-Function-Attributes.html#index-visibility-function-attribute)). +Using an assembly language, one can also use the `.hidden` directive +(e.g. see +[here](https://docs.oracle.com/cd/E26502_01/html/E28388/eoiyg.html#:~:text=.hidden%20symbol1%2C%20symbol2%2C%20...%2C%20symbolN)). + +It seems that so far a similar feature hasn't yet been introduced to other +languages that compile to native binary code: + +* It is unclear if GoLang has a way to explicitly specify visibility. + Using `#pragma GCC visibility push(hidden)` has been proposed as a workaround + (see [here](https://github.com/golang/go/issues/28340#issuecomment-466645246)). +* Haskell libraries can say `foreign export ccall some_function_name :: Int -> Int` + to export a function (see + [the Haskell wiki](https://wiki.haskell.org/Foreign_Function_Interface)). + Presumably such functions are publicly exported + (just as with Rust's `#[no_mangle]`). +* There is + [a proposal](https://forums.swift.org/t/current-status-of-swift-symbol-visibility/66949) + for Swift language to leverage + [the `package` access modifier](https://github.com/swiftlang/swift-evolution/blob/main/proposals/0386-package-access-modifier.md) as a way to specify public visibility. +* There is an open issue that tracks adding a similar mechanism to Zig: + https://github.com/ziglang/zig/issues/9762 + +## Rust language + +[`#[linkage...]` attribute](https://github.com/rust-lang/rust/issues/29603) +has been proposed in the past for specifying +[linkage type](https://llvm.org/docs/LangRef.html#linkage-types) of a symbol +(e.g. `weak`, `linkonce_odr`, etc.). +Linkage type is related to, but nevertheless different from +[linkage visibility](https://llvm.org/docs/LangRef.html#visibility-styles) +that this RFC focuses on. + +The `#[export_visibility = ...]` attribute has been earlier covered by a +Major Change Proposal (MCP) at +https://github.com/rust-lang/compiler-team/issues/881, but it was pointed +out that +"a compiler MCP isn't quite the right avenue here, +as attributes are part of the language." + +# Unresolved questions +[unresolved-questions]: #unresolved-questions + +There are no unresolved questions at this point. # Future possibilities [future-possibilities]: #future-possibilities From 0fba008e8b1969e1a2bad6ae1e0e24f110bbc8d2 Mon Sep 17 00:00:00 2001 From: Lukasz Anforowicz Date: Tue, 14 Oct 2025 17:50:44 +0000 Subject: [PATCH 17/24] More concrete reference documentation proposal --- text/0000-export-visibility.md | 55 ++++++++++++++++++++++++++++++---- 1 file changed, 49 insertions(+), 6 deletions(-) diff --git a/text/0000-export-visibility.md b/text/0000-export-visibility.md index 5adbcbf866a..24c7a6714a4 100644 --- a/text/0000-export-visibility.md +++ b/text/0000-export-visibility.md @@ -241,16 +241,59 @@ command-line flag. # Reference-level explanation [reference-level-explanation]: #reference-level-explanation -It seems that the guide-level explanation above may also work as a -reference-level explanation. (At least, the reference documentation of -[`#[no_mangle]`](https://doc.rust-lang.org/reference/abi.html#the-no_mangle-attribute) -and -[`#[export_name = ...]`](https://doc.rust-lang.org/reference/abi.html#the-export_name-attribute) -attributes provides a similar level of details.) +## Prototype The proposal in this RFC has been prototyped in https://github.com/anforowicz/rust/tree/export-visibility +## 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. + > + > Syntax-tag: The export_name attribute uses the MetaNameValueStr syntax to + > specify the symbol name. + > + > Inherited-tag: Currently only `#[export_visibility = “inherited”]` 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. + # Drawbacks [drawbacks]: #drawbacks From f9c4ef420b007be2293e6d84e3a6d6024315b486 Mon Sep 17 00:00:00 2001 From: Lukasz Anforowicz Date: Tue, 14 Oct 2025 18:26:14 +0000 Subject: [PATCH 18/24] Point out that no drawbacks have been identified at this point. --- text/0000-export-visibility.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/text/0000-export-visibility.md b/text/0000-export-visibility.md index 24c7a6714a4..a261b36ddf0 100644 --- a/text/0000-export-visibility.md +++ b/text/0000-export-visibility.md @@ -297,7 +297,7 @@ https://doc.rust-lang.org/reference/abi.html should get a new section: # Drawbacks [drawbacks]: #drawbacks -See "Open questions" section. +No drawbacks have been identified at this point. # Rationale and alternatives [rationale-and-alternatives]: #rationale-and-alternatives From 359a93125d185906cee4df90a10e219b9d3f863b Mon Sep 17 00:00:00 2001 From: Lukasz Anforowicz Date: Tue, 14 Oct 2025 19:02:23 +0000 Subject: [PATCH 19/24] Hide detailed UB text underneath a
tag --- text/0000-export-visibility.md | 16 ++++++++++++++-- 1 file changed, 14 insertions(+), 2 deletions(-) diff --git a/text/0000-export-visibility.md b/text/0000-export-visibility.md index a261b36ddf0..724287b547d 100644 --- a/text/0000-export-visibility.md +++ b/text/0000-export-visibility.md @@ -123,7 +123,10 @@ The risk of name collisions is caused by two separate behaviors of 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. +benefit from outlining at a high-level how the UB may happen, so this topic +is explored underneath the folded details section below. + +
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. @@ -156,6 +159,8 @@ The author speculates that: (and it seems fair to treat this order as non-deterministic, or at least outside the immediate control of the code author). +
+ ## Benefit: Avoiding undefined behavior Using `#[export_visibility = ...]` to reduce symbol visibility can be used to @@ -164,7 +169,12 @@ reduce or eliminate the risk of undefined behavior (UB) described in the previou 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. In the smaller repro from +https://crbug.com/418073233. More information about this bug can be found in +the folded details section below. + +
+ +In the smaller repro from https://crrev.com/c/6580611/1 we see the following: * Without this RFC the [`cxx`](https://cxx.rs) library cannot avoid publicly @@ -207,6 +217,8 @@ https://crrev.com/c/6580611/1 we see the following: caught by an assertion. In release builds this would lead to memory unsafety. +
+ # Guide-level explanation [guide-level-explanation]: #guide-level-explanation From 1ce35f245dec7b83db70c3edf90fc1d501d8ee41 Mon Sep 17 00:00:00 2001 From: Lukasz Anforowicz Date: Tue, 14 Oct 2025 19:04:21 +0000 Subject: [PATCH 20/24] Rename "inherit" to "target_default" --- text/0000-export-visibility.md | 31 ++++++++++++++++--------------- 1 file changed, 16 insertions(+), 15 deletions(-) diff --git a/text/0000-export-visibility.md b/text/0000-export-visibility.md index 724287b547d..89b544fe30f 100644 --- a/text/0000-export-visibility.md +++ b/text/0000-export-visibility.md @@ -13,9 +13,9 @@ 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 = "inherit"]`, -then it will instead inherit the default visibility of the target platform -(or the default visibility specified with the +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=...`](https://doc.rust-lang.org/beta/unstable-book/compiler-flags/default-visibility.html) command-line flag). @@ -240,10 +240,10 @@ The `#[export_visibility = ...]` attribute uses the syntax to specify the desired visibility. The following sections describe string values that may be used. -### Inherited visibility +### Default target platform visibility -`#[export_visibility = "inherit"]` uses -the standard visibility of the target platform. +`#[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 @@ -301,10 +301,11 @@ https://doc.rust-lang.org/reference/abi.html should get a new section: > Syntax-tag: The export_name attribute uses the MetaNameValueStr syntax to > specify the symbol name. > - > Inherited-tag: Currently only `#[export_visibility = “inherited”]` 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. + > 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. # Drawbacks [drawbacks]: #drawbacks @@ -464,7 +465,7 @@ the `#[export_visibility = ...]`-based approach proposed in this RFC: * Using a version script is one way of fixing https://crbug.com/418073233. This fix approach requires that authors of each future shared library know about the problem and use a version script. This is in contrast to using - `-Zdefault-visibility=hidden` and `#[export_visibility = "inherit"]` for `cxx` + `-Zdefault-visibility=hidden` and `#[export_visibility = "target_default"]` for `cxx` symbols, which has to be done only once to centrally, automatically avoid the problem for all `cxx`-dependent libraries in a given build environment. (In fairness, using the command-line flag also requires awareness and opt-in, @@ -499,7 +500,7 @@ linking is driven by `rustc`). Other edition-boundary changes may also be considered - for example just changing the default effect of `#[no_mangle]` from (pseudo-code) `#[export_visibility = "interposable"]` to -`#[export_visibility = "inherit"]` +`#[export_visibility = "target_default"]` (which combined with `-Zdefault-visibility=hidden` should address https://crbug.com/418073233). @@ -656,7 +657,7 @@ There are no unresolved questions at this point. ## Provide reference-level definitions of supported visibility levels -`#[export = "inherited"]` defers the choice of an actual visibility level to +`#[export = "target_default"]` defers the choice of an actual visibility level to 1. Session-wide default of [`SymbolVisibility::Interposable`](https://github.com/rust-lang/rust/blob/910617d84d611e9ba508fd57a058c59b8a767697/compiler/rustc_session/src/session.rs#L551-L557) @@ -671,7 +672,7 @@ OTOH, such definitions may be desirable in the future: * If/when stabilizing `-Zdefault-visibility=...` * If/when extending `#[export_visibility = ...]` to support specific visibility - levels (i.e. if the attribute would support not only the `"inherit"` + levels (i.e. if the attribute would support not only the `"target_default"` visibility value, but also `"hidden"`, `"protected"`, and/or `"interposable"`). @@ -693,7 +694,7 @@ use one of those three binary formats: * `x86_64-unknown-linux-gnu`: ELF Ad-hoc, manual tests (TODO: link to a GitHub comment) -of `#[export_visibility = "inherit"]` provide some +of `#[export_visibility = "target_default"]` provide some reassurance that such definitions should be possible in the future. OTOH, when future RFCs or PRs consider implementing specific visibility levels, they should ideally come with: From 6a7c5e9f3fba97c18d54d2ba5cb1e76988524937 Mon Sep 17 00:00:00 2001 From: Lukasz Anforowicz Date: Thu, 16 Oct 2025 16:56:58 +0000 Subject: [PATCH 21/24] Clean up stale references in the hidden visibility section --- text/0000-export-visibility.md | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/text/0000-export-visibility.md b/text/0000-export-visibility.md index 89b544fe30f..dff91d1d890 100644 --- a/text/0000-export-visibility.md +++ b/text/0000-export-visibility.md @@ -775,14 +775,12 @@ pub fn public_function() { The following options have been identified so far as a potential way for answering the `dylib`-vs-`hidden`-visibility problem: -* Don't stabilize (or don't support at all) `#[export_visibility = "hidden"]` - but still support other visibilities +* Don't stabilize `#[export_visibility = "hidden"]` (initially? forever?) * Support `#[export_visibility = "hidden"]`, but - Document that `hidden` visibility may break linking of `dylib`s (see the "Hidden visibility" section in the guide-level explanation above) - Document a recommendation that reusable crates shouldn't use a hardcoded - visibility (see the "Choosing the right visibility" section in the - guide-level explanation above) + visibility * Avoid inlining if the inlined code ends up calling a hidden symbol from another `dylib` - Currently preventing inlining is problematic, because `#[inline]` will From 3f8de4f5dd68d58bb8c7620cb6ba1f89dfca95d2 Mon Sep 17 00:00:00 2001 From: Lukasz Anforowicz Date: Thu, 16 Oct 2025 17:29:51 +0000 Subject: [PATCH 22/24] Add end-to-end, and LLVM-level examples --- text/0000-export-visibility.md | 51 ++++++++++++++++++++++++++++++++++ 1 file changed, 51 insertions(+) diff --git a/text/0000-export-visibility.md b/text/0000-export-visibility.md index dff91d1d890..af1755a2fa8 100644 --- a/text/0000-export-visibility.md +++ b/text/0000-export-visibility.md @@ -250,6 +250,57 @@ supports overriding the target platform's visibility with the [`-Zdefault-visibility=...`](https://doc.rust-lang.org/beta/unstable-book/compiler-flags/default-visibility.html) 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](https://github.com/rust-lang/rust/commit/1e1924bdac60b3b522ecffefbedfef94e4aa79d5#diff-25436b0328a03fca2c8be8a36152e30d58272315d690d9b3b6b5f0b5ebf35269) +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 [reference-level-explanation]: #reference-level-explanation From 68a5cef6447666e60ed637cc93627ce964f92e47 Mon Sep 17 00:00:00 2001 From: Lukasz Anforowicz Date: Sat, 18 Oct 2025 02:41:50 +0000 Subject: [PATCH 23/24] Say that new attribute can only be used with no_mangle or export_name. --- text/0000-export-visibility.md | 18 +++++++++++++----- 1 file changed, 13 insertions(+), 5 deletions(-) diff --git a/text/0000-export-visibility.md b/text/0000-export-visibility.md index af1755a2fa8..292121ae3b2 100644 --- a/text/0000-export-visibility.md +++ b/text/0000-export-visibility.md @@ -304,11 +304,6 @@ previous section (with `-Zdefault-visibility=hidden`): # Reference-level explanation [reference-level-explanation]: #reference-level-explanation -## Prototype - -The proposal in this RFC has been prototyped in -https://github.com/anforowicz/rust/tree/export-visibility - ## Edits to reference documentation for `#[no_mangle]` If this RFC is adopted (and stabilized) then @@ -358,6 +353,19 @@ https://doc.rust-lang.org/reference/abi.html should get a new section: > be overridden by the unstable `-Zdefault-visibility=...` command-line > flag. +## Other details + +Other details (probably not important enough to include in the official +reference documentation for Rust): + +* It is an error to use `#[export_visibility = ...]` on an item + _without_ either `#[no_mangle]` or `#[export_name = ...]` + attribute. +* The proposal in this RFC has been prototyped in + https://github.com/anforowicz/rust/tree/export-visibility + + + # Drawbacks [drawbacks]: #drawbacks From e772d50fce28ddf32c9a01682ccc2ea5454edbe6 Mon Sep 17 00:00:00 2001 From: Lukasz Anforowicz Date: Sat, 18 Oct 2025 02:46:00 +0000 Subject: [PATCH 24/24] Wrap some long lines to 80 columns --- text/0000-export-visibility.md | 72 +++++++++++++++++++--------------- 1 file changed, 40 insertions(+), 32 deletions(-) diff --git a/text/0000-export-visibility.md b/text/0000-export-visibility.md index 292121ae3b2..de2e7be63c4 100644 --- a/text/0000-export-visibility.md +++ b/text/0000-export-visibility.md @@ -1,7 +1,7 @@ - Feature Name: `export_visibility` - Start Date: 2025-06-12 - RFC PR: [rust-lang/rfcs#0000](https://github.com/rust-lang/rfcs/pull/0000) -- Rust Issue: [rust-lang/rust#0000](https://github.com/rust-lang/rust/issues/0000) +- Rust Issue:[rust-lang/rust#0000](https://github.com/rust-lang/rust/issues/0000) # Summary [summary]: #summary @@ -117,7 +117,9 @@ The risk of name collisions is caused by two separate behaviors of * Turning-off mangling (e.g. see [here](https://github.com/rust-lang/rust/blob/3d8c1c1fc077d04658de63261d8ce2903546db13/compiler/rustc_symbol_mangling/src/lib.rs#L240-L243)) introduces the _possibility_ of naming collisions. -* Exporting the symbol with public visibility (e.g. see [here](https://github.com/rust-lang/rust/blob/8111a2d6da405e9684a8a83c2c9d69036bf23f12/compiler/rustc_monomorphize/src/partitioning.rs#L930-L937)) increases the _scope_ of possible naming collisions (covering all DSOs). +* Exporting the symbol with public visibility (e.g. see + [here](https://github.com/rust-lang/rust/blob/8111a2d6da405e9684a8a83c2c9d69036bf23f12/compiler/rustc_monomorphize/src/partitioning.rs#L930-L937)) + increases the _scope_ of possible naming collisions (covering all DSOs). ### Origins of UB @@ -164,8 +166,8 @@ The author speculates that: ## 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. +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 @@ -204,18 +206,18 @@ https://crrev.com/c/6580611/1 we see the following: UB exists for calls to `cxxbridge1$string$drop`. * The UB from the previous item leads to memory unsafety when: - The call from test executable to `rust_lib$cxxbridge1$get_string` - ends up calling the definition in the `.so`, rather than in the executable. - This means that allocations made by `get_string` use **one** set of the - allocator's global symbols - the copy within the `.so`. + ends up calling the definition in the `.so`, rather than in the + executable. This means that allocations made by `get_string` use **one** + set of the allocator's global symbols - the copy within the `.so`. - The call from test executable to `cxxbridge1$string$drop` ends 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. + - 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.
@@ -341,8 +343,8 @@ 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. + > Intro-tag: The _`export_visibility` attribute_ overrides if or how the + > item is exported from the produced library or object file. > > Syntax-tag: The export_name attribute uses the MetaNameValueStr syntax to > specify the symbol name. @@ -513,8 +515,8 @@ the `#[export_visibility = ...]`-based approach proposed in this RFC: * Version scripts don't work on all target platforms. In particular, they work in GNU `ld` and LLVM `lld`, but the native Microsoft Visual C++ linker (`link.exe`) does not directly support GNU-style version scripts. - Instead, MSVC uses `.def` (module-definition) files to control symbol export and - other aspects of DLL creation. Having to use + Instead, MSVC uses `.def` (module-definition) files to control symbol export + and other aspects of DLL creation. Having to use [a `.def` file](https://learn.microsoft.com/en-us/cpp/build/reference/exports?view=msvc-170) has a few extra downsides compared to a version script: - Having to support both formats @@ -524,13 +526,14 @@ the `#[export_visibility = ...]`-based approach proposed in this RFC: * Using a version script is one way of fixing https://crbug.com/418073233. This fix approach requires that authors of each future shared library know about the problem and use a version script. This is in contrast to using - `-Zdefault-visibility=hidden` and `#[export_visibility = "target_default"]` for `cxx` - symbols, which has to be done only once to centrally, automatically avoid the - problem for all `cxx`-dependent libraries in a given build environment. - (In fairness, using the command-line flag also requires awareness and opt-in, - but it seems easier to append `-Zdefault-visibility=hidden` to default - `rustflags` in globally-applicable build settings than it is to modify build - tools to require a linker script for all shared libraries. In fact, Chromium + `-Zdefault-visibility=hidden` and `#[export_visibility = "target_default"]` + for `cxx` symbols, which has to be done only once to centrally, automatically + avoid the problem for all `cxx`-dependent libraries in a given build + environment. (In fairness, using the command-line flag also requires + awareness and opt-in, but it seems easier to append + `-Zdefault-visibility=hidden` to default `rustflags` in globally-applicable + build settings than it is to modify build tools to require a linker script for + all shared libraries. In fact, Chromium [already builds with the `-Zdefault-visibility=...` flag](https://source.chromium.org/chromium/chromium/src/+/main:build/config/gcc/BUILD.gn;l=34-35;drc=ee3900fd57b3c580aefff15c64052904d81b7760).) ## Alternative: introduce `-Zdefault-visibility-for-c-exports=...` @@ -676,8 +679,10 @@ languages that compile to native binary code: * It is unclear if GoLang has a way to explicitly specify visibility. Using `#pragma GCC visibility push(hidden)` has been proposed as a workaround - (see [here](https://github.com/golang/go/issues/28340#issuecomment-466645246)). -* Haskell libraries can say `foreign export ccall some_function_name :: Int -> Int` + (see + [here](https://github.com/golang/go/issues/28340#issuecomment-466645246)). +* Haskell libraries can say + `foreign export ccall some_function_name :: Int -> Int` to export a function (see [the Haskell wiki](https://wiki.haskell.org/Foreign_Function_Interface)). Presumably such functions are publicly exported @@ -685,7 +690,8 @@ languages that compile to native binary code: * There is [a proposal](https://forums.swift.org/t/current-status-of-swift-symbol-visibility/66949) for Swift language to leverage - [the `package` access modifier](https://github.com/swiftlang/swift-evolution/blob/main/proposals/0386-package-access-modifier.md) as a way to specify public visibility. + [the `package` access modifier](https://github.com/swiftlang/swift-evolution/blob/main/proposals/0386-package-access-modifier.md) + as a way to specify public visibility. * There is an open issue that tracks adding a similar mechanism to Zig: https://github.com/ziglang/zig/issues/9762 @@ -716,14 +722,16 @@ There are no unresolved questions at this point. ## Provide reference-level definitions of supported visibility levels -`#[export = "target_default"]` defers the choice of an actual visibility level to +`#[export = "target_default"]` defers the choice of an actual visibility level +to: 1. Session-wide default of [`SymbolVisibility::Interposable`](https://github.com/rust-lang/rust/blob/910617d84d611e9ba508fd57a058c59b8a767697/compiler/rustc_session/src/session.rs#L551-L557) 2. Unless overridden by target platform’s default visibility specified in [`rustc_target::spec::TargetOptions`](https://github.com/rust-lang/rust/blob/910617d84d611e9ba508fd57a058c59b8a767697/compiler/rustc_target/src/spec/mod.rs#L2225-L2230), 3. Or overridden by - [`-Zdefault-visibility=...`](https://doc.rust-lang.org/beta/unstable-book/compiler-flags/default-visibility.html) command-line flag. + [`-Zdefault-visibility=...`](https://doc.rust-lang.org/beta/unstable-book/compiler-flags/default-visibility.html) + command-line flag. This means that _this_ RFC doesn't necessarily need to define the exact semantics and behavior of supported visibility levels. @@ -857,10 +865,10 @@ answering the `dylib`-vs-`hidden`-visibility problem: and `#[export_name = ...]` cannot be used with generics, because the names of the symbols (ones generated during monomorphization) need to differ based on the generic parameters. - - One major problem with avoiding inlining is that during codegen it is not yet - known if two crates will end up getting linked into the same or different - dylib. This means that inlining would need to be inhibited for any - cross-crate calls into hidden symbols. And this would suppress many + - One major problem with avoiding inlining is that during codegen it is not + yet known if two crates will end up getting linked into the same or + different dylib. This means that inlining would need to be inhibited for + any cross-crate calls into hidden symbols. And this would suppress many legitimate optimizations. (hattip [@bjorn3](https://github.com/rust-lang/rfcs/pull/3834#issuecomment-3352658642)) * Add a lint/warning that detects when `#[export_visibility = ...]` is used