Skip to content

Conversation

@anforowicz
Copy link

@anforowicz anforowicz commented Jun 16, 2025

This RFC proposes to add #[export_visibility = …] attribute, which seems like a reasonable way to address the following issues:

This RFC complements the -Zdefault-visibility=... command-line flag, which is tracked in rust-lang/rust#131090

This PR replaces the Major Change Proposal (MCP) at rust-lang/compiler-team#881
(/cc @bjorn3, @ChrisDenton, @chorman0773, @joshtriplett, @mati865, @workingjubilee, and @Urgau who have kindly provided feedback in the Zulip thread associated with that MCP)

/cc @tmandry from rust-lang/rust-project-goals#253, because one area where this RFC seems needed is FFI tooling

Rendered

@anforowicz anforowicz force-pushed the export-visibility branch 2 times, most recently from a79823e to ab37907 Compare June 16, 2025 19:36

## Benefit: Smaller binaries

One undesirable consequence of unnecessary public exports is binary size bloat.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should only be the case for libraries. For binaries all functions are already made not-exported.

Copy link
Member

@joshtriplett joshtriplett Oct 1, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

True, though on (weird/stunt) occasions there might be reasons to have public symbols in a binary.

(That case is not common and not important, just mentioning it in case someone figured it was categorically impossible and never happened.)

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think @joshtriplett's comment implies that there actually might exist a use case for #[export_visibility = "public"] (or interposable) on binary crates. This should be noted in the RFC. We can still choose not to support it initially.

(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
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would have expected all of Chromium to use a single rust allocator rather than use a different one for each DSO. Why is that not the case?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would have expected all of Chromium to use a single rust allocator rather than use a different one for each DSO. Why is that not the case?

Is that really a requirement if foo.so doesn't export any functions that return pointers to Rust-related objects? I would expect in such a case that which Rust allocator / standard library / etc is used would be an internal implementation detail of foo.so. IIUC this detail leaks out only because of an unintentional export of a cxx-generated, internal symbol.

But to try to answer the question - the same allocator is statically linked into Chromium binaries. This means that an executable and an .so may end up with a separate copy of the same global data structures of the allocator.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is that really a requirement if foo.so doesn't export any functions that return pointers to Rust-related objects?

It is not a requirement. I'm just surprised that Chromium copies the entire rust standard library between the dylib and executable rather than using the copy from the dylib in the executable to save space.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is that really a requirement if foo.so doesn't export any functions that return pointers to Rust-related objects?

It is not a requirement. I'm just surprised that Chromium copies the entire rust standard library between the dylib and executable rather than using the copy from the dylib in the executable to save space.

That is indeed a bit unfortunate. I think this is to some extent based on the following:

  • Chromium requirement to use an external linker
  • Assumption that only rlibs / static_libs can be linked by an external linker, and that an external linker wouldn't be able to handle dylibs

But thank you for bringing this up - maybe this should indeed be treated as an alternative fix for https://crbug.com/418073233. I am not sure what the next steps should be for this aspect:

@bjorn3
Copy link
Member

bjorn3 commented Jun 16, 2025

For most use cases rather than specifying the exact symbol visibility (which may not even be supported by the object file format, like interposable on pe/coff or (with the default two-level namespaces) mach-o) I think having just a way to force SymbolExportLevel::Rust rather than the default SymbolExportLevel::C would be a better idea. This causes it to still be exported from rust dylibs (as necessary to avoid linker errors depending on the exactly when rustc decides to codegen functions), but prevents it from being exported from cdylibs. It doesn't work for staticlibs currently, but for those if you want to limit symbol visibility you have to specify your own version script during linking anyway to prevent exporting all rust mangled symbols too.

@anforowicz
Copy link
Author

For most use cases rather than specifying the exact symbol visibility (which may not even be supported by the object file format, like interposable on pe/coff or (with the default two-level namespaces) mach-o) I think having just a way to force SymbolExportLevel::Rust rather than the default SymbolExportLevel::C would be a better idea. This causes it to still be exported from rust dylibs (as necessary to avoid linker errors depending on the exactly when rustc decides to codegen functions), but prevents it from being exported from cdylibs.

The fact that you are distinguishing between dylibs and cdylibs makes me think that you assume that linking is driven by rustc. If so, then this may not apply to Chromium, which uses an external linker.

It doesn't work for staticlibs currently, but for those if you want to limit symbol visibility you have to specify your own version script during linking anyway to prevent exporting all rust mangled symbols too.

That's not 100% correct - instead of using a version script, one may also use -Zdefault-visibility=hidden.

@bjorn3
Copy link
Member

bjorn3 commented Jun 16, 2025

The fact that you are distinguishing between dylibs and cdylibs makes me think that you assume that linking is driven by rustc. If so, then this may not apply to Chromium, which uses an external linker.

The Chromium case is effectively equivalent to using staticlibs, not to using rust dylibs/cdylibs.

That's not 100% correct - instead of using a version script, one may also use -Zdefault-visibility=hidden.

That doesn't apply to the standard library unless you go out of your way using unstable features to recompile the standard library.

@anforowicz
Copy link
Author

anforowicz commented Jun 16, 2025

The fact that you are distinguishing between dylibs and cdylibs makes me think that you assume that linking is driven by rustc. If so, then this may not apply to Chromium, which uses an external linker.

The Chromium case is effectively equivalent to using staticlibs, not to using rust dylibs/cdylibs.

Ack / agreed.

That's not 100% correct - instead of using a version script, one may also use -Zdefault-visibility=hidden.

That doesn't apply to the standard library unless you go out of your way using unstable features to recompile the standard library.

Thank you for bringing up this point. This probably should be explicitly addressed by the RFC (*), but I am not sure if I agree with your conclusions so far. This is because:

  • Chromium does in fact compile the standard library within Chromium's build system (e.g. see build/rust/std/rules/BUILD.gn auto-generated from standard library's Cargo.toml files)
  • But using a pre-built standard library doesn't necessarily make #[export_visibility = ...] less useful, because there may be other actions that can be taken to change the behavior of the standard library. For example, the RFC discusses changing the behavior of #[export_name = ...] and/or #[no_mangle] (in a future Rust language edition) so that in the future these attributes imply #[export_visibility = "inherit"] rather than #[export_visibility = "interposable"]. So maybe a similar change can/should be applied to #[rustc_std_internal_symbol]? And while users of #[no_mangle] and/or #[export_name = ...] may actually want the public-export behavior of these attributes, I think this is not the case for standard library symbols (so maybe the change to inherit behavior could even be done within the current language edition; or maybe trigerred by -Zdefault-visibility although this then would get quite close to the -Zdefault-visibility-for-c-exports=... alternative from the RFC).

(*) I am not sure what the right process is here. Should I add commits to the RFC as we keep discussing here? Should I first give people an opportunity to review the first draft?

@ehuss ehuss added the T-lang Relevant to the language team, which will review and decide on the RFC. label Jun 16, 2025
@petrochenkov
Copy link
Contributor

petrochenkov commented Jun 17, 2025

I think this is a good opportunity to expand the design space (and documentation) of "various levels of exportendess" a bit, even if the resulting proposal for export_visibility specifically stays more or less the same.

There are multiple attributes that targets this similar space (export_visibility, linkage, used, rustc_std_internal_symbol), so it would be good to somehow target them together properly.

What I'd like to see is a table of "levels of exportedness" combined with the kinds of end artifacts, and how we can users can express all those levels with the attributes listed above.

  • a symbol only visible inside an object file
  • a symbol visible outside of an object file but not outside of a cdylib/dylib/executable
  • a symbol visible outside of a cdylib
  • a symbol visible outside of a rust dylib
  • a symbol visible outside of an executable
  • a symbol visible outside of a cdylib/dylib/executable but not some other crate type
  • a symbol visible outside of an object file inside a rlib/staticlib, but not outside of it
  • a symbol visible outside of an X but only for LTO, after that it is only visible outside of Y (I think I've seen some issue about this in the tracker)
  • where is the information about the exportedness level stored? in which case it can be stored in the target's object format (e.g. ELF, COFF or even archive metadata) and in which cases it needs to be stored in rmeta and require rustc for interpreting it (e.g. rustc must be used for linking).
  • if a symbol is visible outside of X, does it mean that it is used in some sense? who can optimize that used symbol away in each case?
  • if a symbol is used(X), does it also mean that it is visible outside of Y?
  • which of the case combinations above make sense?

In particular, one of my requirements is that #[rustc_std_internal_symbol] should be expressible as an alias to several more fine-grained and single-purpose attributes available to users. IIRC, it had some LTO-related visibility requirement in particular.
Also, all symbols hard-coded in the compiler by name (there were such symbols in the past, not sure about now, some were migrated to rustc_std_internal_symbol) should be expressible by the same fine-grained attributes as well.

@bjorn3
Copy link
Member

bjorn3 commented Jun 17, 2025

a symbol only visible inside an object file

This is something only rustc must be allowed to do (other than for symbols defined in inline asm called from within the same inline asm block). Only rustc knows if all callers will end up in the same object file as the definition and it doesn't provide any guarantees around when this happens. So exposing this to the user is a stability hazard.

where is the information about the exportedness level stored? in which case it can be stored in the target's object format (e.g. ELF, COFF or even archive metadata) and in which cases it needs to be stored in rmeta and require rustc for interpreting it (e.g. rustc must be used for linking).

For regular functions and #[rustc_std_internal_symbol] we have to store it in the rmeta and use a version script as at compile time we don't yet know if the object file ends up in a rust dylib or cdylib.

a symbol visible outside of an object file inside a rlib/staticlib, but not outside of it

For rlib this doesn't make sense. There is no way to make rlibs a symbol export boundary without introducing an expensive link/object file rewrite step for each individual rlib. For staticlib it would be nice to have a symbol export boundary, but unfortunately we don't have one right now even for SymbolExportLevel::Rust (which really shouldn't be exported from staticlibs and for which we already support not exporting them from cdylibs) except I believe when we do (fat?) LTO as in that case all object files in the staticlib get optimized together allowing them to be internalized in the output object.

a symbol visible outside of a cdylib

This makes sense to me. See the end of my comment.

a symbol visible outside of a rust dylib

This has to always be the case if it is visible outside of the object file. The very point of rust dylibs is that rust code in a separate DSO can call any public function, which thanks to cross-crate inlining can call effectively every function that rustc wouldn't make private to the current object file. And again, rustc doesn't provide any guarantees when this happens, so allowing you to not export symbols from a rust dylib is a stability hazard.

if a symbol is visible outside of X, does it mean that it is used in some sense? who can optimize that used symbol away in each case?

Yes.

if a symbol is used(X), does it also mean that it is visible outside of Y?

No

Also, all symbols hard-coded in the compiler by name (there were such symbols in the past, not sure about now, some were migrated to rustc_std_internal_symbol) should be expressible by the same fine-grained attributes as well.

rust_eh_personality should be the only remaining symbol with a hard coded name once rust-lang/rust#141061 lands (which removes the unmangled __rust_no_alloc_shim_is_unstable in favor of a mangled __rust_no_alloc_shim_is_unstable_v2). We unfortunately can't mangle it's name as LLVM hard codes it.

IIRC, it had some LTO-related visibility requirement in particular.

Not really aside from the visibility information we already tell the linker (export from rust dylib, don't export from cdylib).

Currently rustc internally works with three different symbol export levels:

  • Not exported from the object file. This is done using internal symbol linkage.
  • SymbolExportLevel::Rust. This exports from a rust dylib, but not a cdylib. This is for #[rustc_std_internal_symbol] and regular rust functions that are not #[no_mangle] that aren't made private to the object file either
  • SymbolExportLevel::C. This exports from all crate types (for bin only when -Zexecutable-export-symbols is passed). This is enabled using #[no_mangle].

It makes sense to me to allow SymbolExportLevel::Rust for #[no_mangle] symbols for C/C++ code that ends up getting linked into the same cdylib.

@anforowicz
Copy link
Author

It makes sense to me to allow SymbolExportLevel::Rust for #[no_mangle] symbols for C/C++ code that ends up getting linked into the same cdylib.

I think this probably should be captured somehow as one of the alternatives in the RFC. Is there a specific syntax that you have in mind here? I guess one option would be to have a #[symbol_export_level = "rust"] or maybe #[no_c_level_symbol_export] (or #[no_cdylib_symbol_export]?), although maybe the names could be improved somehow.

@bjorn3
Copy link
Member

bjorn3 commented Jun 17, 2025

I think this probably should be captured somehow as one of the alternatives in the RFC.

👍

#[symbol_export_level = "rust"] or maybe #[no_c_level_symbol_export]

I don't think this is a good name as it is still meant to be usable from C, just not outside of the linked DSO.

#[no_cdylib_symbol_export]

This would be an option, although ideally if we manage to stop exporting all symbols from staticlibs, I would like the same attribute to be usable to prevent export from both cdylib and staticlib, so it should probably not mention cdylib in the name. I don't have suggestions for a better name though.

@chorman0773
Copy link

Frankly, if we have #[no_cdylib_symbol_export], I'd like the inverse for imports at least, so that it's possible to export symbols defined in C (or another language) from a cdylib.

@brjsp
Copy link

brjsp commented Jul 4, 2025

In Rust 1.87 and before on GNU/Linux, this code:

#![feature(rustc_attrs)]


#[rustc_std_internal_symbol]
#[no_mangle]
pub extern "C" fn blah(i: u32) -> u32 {
	i+1
}

produced an object with the blah symbol marked with internal linkage (t in nm output)
This is required when linking a Rust shared library with C dependencies which call back to the Rust code, when the blah function is an internal glue function that should not be exported from the library.

What this issue is not applicable to:

  • linking most executables as opposed to DLLs (executables have exports removed by default during linking)
  • linking Rust code in a staticlib into a C library (we can use the likes of -Wl,--exclude-libs to hide any offending exports)
  • linking anything in pure Rust (there are no FFI requirements, any instances of #[no_mangle] can be removed from code)

Omitting the #[rustc_std_internal_symbol] produces a correctly-named symbol, but with incorrect external linkage (T in nm output).
Such symbol can be called from outside the DLL, which we don't want to.

In C++, mangling (controlled by extern "C") and linkage (controlled by the visibility attribute) are completely orthogonal concepts. One should not control the other.

The analog to what #[rustc_std_internal_symbol] did in C/C++ is __attribute__((visibility("hidden"))) and we are trying to reproduce its effect on the ELF object.

This errors now in 1.88, the only way i'm aware of being injecting an assembly declaration core::arch::global_asm!(".hidden blah");. This obviously does not work when the binary is compiled with LTO.

@anforowicz
Copy link
Author

@bjorn3 - thank you for proposing a narrower fix, focusing on setting SymbolExportLevel::Rust for the exported symbols. I have:

  1. Prototyped this approach: anforowicz/rust@9dd4d3f
  2. Verified that this also addresses https://crbug.com/418073233: patchset 3 here: https://crrev.com/c/6580611/3
  3. Edited the RFC to list this approach as an alternative solution

From my perspective both #[export_visibility = ...] and #[rust_symbol_export_level] work equally well for addressing my problems. OTOH it seems that #[rust_symbol_export_level] may avoid some open questions and IIUC also avoids undesirable dylib interactions. So that probably makes the new approach preferable over #[export_visibility = ...]. I am not sure what the next steps are:

  • Continue discussing whether #[export_visibility = ...] may be more desirable than #[rust_symbol_export_level] for some other folks / other requirements / other scenarios?
  • Continue brainstorming other names for the #[rust_symbol_export_level] attribute? So far we had:
    • #[rust_symbol_export_level] - I've used this in the prototype because it directly maps to the compiler code... :-/ If the name is good enough for the compiler code, then maybe it is okay in a user-facing attribute?
    • no_cdylib_symbol_export
    • #[symbol_export_level = "rust"]
  • Later (not sure when?) consider switching to using #[rust_symbol_export_level] as the main approach. Not sure if I should open a new PR/RFC for this (preserving earlier commits I guess)? Or just edit the current one?

@tmandry
Copy link
Member

tmandry commented Sep 11, 2025

I think the current RFC does a good job of creating a conceptual framework for symbol visibility that can abstract over platform differences, and the remaining issues seem like they can be addressed. Perhaps the issue with dylibs can be solved by linting on calls to non-inlined, hidden-linkage functions from inlined or generic functions.

My feeling is that having two export levels, "Rust" and "C", isn't quite the right abstraction – or if it is, we should be able to spell it as extern "Rust" and extern "C". There are more fine grained distinctions that users want to capture when it comes to symbol visibility, and they don't map directly onto Rust's existing concepts.

I love the idea @petrochenkov raised in #3834 (comment) of having a table of all the things you might want to accomplish and how you would spell them. I wouldn't put the onus on this RFC to completely fill out that table, but it would be helpful to know which gaps exist and which are getting filled.

It's also important to me that the users who really know what they're doing when it comes to symbol visibility should be able to exercise direct control. While those users are a small percentage of Rust users, I do see this as a significant benefit to them and to unlocking Rust usage in more arcane contexts. That control can be mediated by a table that maps from "how it's spelled in C compilers and linker scripts" to "how to spell it in Rust"; it doesn't require us to adopt the existing models or terminology wholesale.

There are places we might reasonably want to place limits on this (as brought up by @bjorn3 above, setting visibility to an individual codegen unit is almost certainly a bad idea). Otherwise I think we should have some path that favors flexibility and transparency, and avoid forcing outside experts who come to Rust to guess at complex logic the compiler might be doing and why.

@tmandry
Copy link
Member

tmandry commented Sep 30, 2025

@rfcbot fcp merge

@rust-rfcbot
Copy link
Collaborator

rust-rfcbot commented Sep 30, 2025

Team member @tmandry has proposed to merge this. The next step is review by the rest of the tagged team members:

Concerns:

Once a majority of reviewers approve (and at most 2 approvals are outstanding), this will enter its final comment period. If you spot a major issue that hasn't been raised at any point in this process, please speak up!

cc @rust-lang/lang-advisors: FCP proposed for lang, please feel free to register concerns.
See this document for info about what commands tagged team members can give me.

@rust-rfcbot rust-rfcbot added proposed-final-comment-period Currently awaiting signoff of all team members in order to enter the final comment period. disposition-merge This RFC is in PFCP or FCP with a disposition to merge it. labels Sep 30, 2025
@traviscross traviscross added the I-lang-radar Items that are on lang's radar and will need eventual work or consideration. label Sep 30, 2025
@traviscross traviscross changed the title RFC: #[export_visibility = ...] attribute. RFC: #[export_visibility = ...] attribute Sep 30, 2025
@anforowicz
Copy link
Author

@tmandry, can you please clarify the concern below?

#3834 (comment)

@rfcbot concern due diligence on tier 1 platforms

The RFC should demonstrate that the parameters it gives for each option can be implemented for at least our Tier 1 platforms, or specify where there are exceptions or particular unknowns.

IIUC the RFC does not add any new kinds of visibility to rustc - the RFC simply reuses existing visibility kinds, which rustc maps to LLVM visibility kinds. I assume that if LLVM supports/exposes a given visibility kind, then this visibility makes (some) sense for all LLVM-supported platforms. At the very least, if a visibility is problematic for a specific platform, then it probably should be seen as an LLVM problem, not a problem with Rust language or rustc, right? See https://llvm.org/docs/LangRef.html#visibility-styles for LLVM definitions.

If you think that delegating the responsibility to LLVM is not okay, then can you please help me understand what kind of test results or what kind of data you'd like to see added to the RFC?

@programmerjake
Copy link
Member

generics can't be marked as extern "C"

that's incorrect https://play.rust-lang.org/?version=stable&mode=debug&edition=2024&gist=acebbccd2aeb3384a353161e9be5f801

@chorman0773
Copy link

I would note that generics being extern "C" are a common case in FFI libraries for callbacks that use userdata, as a way to "convert" a Rust trait type, to a simple function. You are correct, however, that they cannot be #[no_mangle] or #[export_name].

Also, the point about generics is that a generic function may call an #[export_visibility] function, and the generic function may be called from another crate (which may cross a dylib boundary), which requires the function to be codegened in the callee crate because there may be not other definition of the particular instantiation. The issue isn't applying this to a generic function - the issue is calling such a function (or referencing the static symbol) from a generic function, which can create a cross-crate reference even if the function is declared in the same crate.
Unlike #[inline] (where the function can be marked as not cross-crate inlineable, and force codegen) this is not easily solved with a generic function.

Comment on lines 709 to 710
OTOH, ideally we would somehow check what happens on some representative subset
of target platforms (maybe: Posix, Windows, Wasm?):
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As far as I'm aware, there is no Windows equivalent of the "protected" visibility class.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Doesn't COFF have protected as normal visibility class, with no equivalent for ELF default visibility? And same for Mach-O. ELF default visibility allows another dylib to overwrite exported functions of the local dylib for calls from within the local dylib.

@tmandry
Copy link
Member

tmandry commented Oct 7, 2025

IIUC the RFC does not add any new kinds of visibility to rustc - the RFC simply reuses existing visibility kinds, which rustc maps to LLVM visibility kinds. I assume that if LLVM supports/exposes a given visibility kind, then this visibility makes (some) sense for all LLVM-supported platforms. At the very least, if a visibility is problematic for a specific platform, then it probably should be seen as an LLVM problem, not a problem with Rust language or rustc, right? See https://llvm.org/docs/LangRef.html#visibility-styles for LLVM definitions.

The Rust language does not assume LLVM, and rustc supports multiple backends. Saying that these map to LLVM definitions is not enough on its own. Those definitions might have meaning or not on various platforms and in various combinations.

The way I see it, there are two directions this RFC could go:

  1. Present a model that is maximally cross-platform and maximally intuitive. Minimize "leaky abstractions" as much as possible.
  2. Present a list of platform-specific options that would need to be used in combination with cfg_attr.

1 is my preferred outcome. The goals of cross-platform and intuitive are in tension, however, and not necessarily related to the way that LLVM surfaces its options. Looking over the LLVM and GCC docs, some of the visibility options strike me as leaky abstractions with a list of platform-specific caveats.

We can simplify the problem by restricting scope. As you suggest, it's valid to cut out "hidden" and focus on a single "protected" option, with more options as future possibilities. I would like to know we have a plausible path to stabilizing future options, i.e. we'll need to discuss some tradeoffs but there's at least one option that looks usable. With my current understanding I think "hidden" meets this bar. Another future possibility is that we stabilize both "abstract" and "platform-specific" options under the same attribute.

To clarify something, am I correct in understanding that your particular use case is solved with either "hidden" or "protected"?

@anforowicz
Copy link
Author

To clarify something, am I correct in understanding that your particular use case is solved with either "hidden" or "protected"?

My usecase (i.e. fixing https://crbug.com/418073233) can be addressed by either:

  • #[export_visibility = "hidden"]`
  • or #[export_visibility = "inherit"](because Chromium [builds](https://source.chromium.org/chromium/chromium/src/+/main:build/config/gcc/BUILD.gn;l=35;drc=ee3900fd57b3c580aefff15c64052904d81b7760) with-Zdefault-visibility=hidden, so in this environment "inherit"="hidden"`)
    • I also note that #[rust_symbol_export_level] is equivalent to #[export_visibility = "inherit"]` (the end result is the same, although the mechanism for doing this is a bit different - explicitly inheriting VS using Rust-level-export which implies inheriting)

I think that my usecase would not be addressed by "protected".

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 rust-lang#3834
@anforowicz
Copy link
Author

#3834 (comment)

@rfcbot concern due diligence on tier 1 platforms

The RFC should demonstrate that the parameters it gives for each option can be implemented for at least our Tier 1 platforms, or specify where there are exceptions or particular unknowns.

@tmandry, I have taken the following steps to address the concern above:

  • I have edited the RFC to initially only support #[export_visibility = "inherit"].
    • This means that #[export = "inherited"] defers the choice of an actual visibility level to
      1. Session-wide default of SymbolVisibility::Interposable
      2. Unless overridden by target platform’s default visibility specified in rustc_target::spec::TargetOptions,
      3. Or overridden by -Zdefault-visibility=... command-line flag.
    • This means that this RFC doesn't necessarily need to define the exact semantics and behavior of supported visibility levels.
  • Even though the exact definitions are deferred to future work, we probably want to have a reasonable confidence level that those kinds of definitions are actually possible in the future. To address this, I share below results of end-to-end tests for effects of the #[export_visibility = "inherit"] on an ELF binary.
  • I am sadly unable to run the tests on an Mach-O binary (I don't have an easy access to a Mac machine)
  • So far my attempts to run x.ps1 build on a Windows machine have failed. I'll keep investigating, but so far I am not able to share test results for a PE binary.

Would you be able to please:

  • Clarify if Mach-O and PE test results are a hard blocker/requirement for proceeding with the RFC (even though the RFC itself doesn't anymore mention any specific visibility levels)
  • Help with running end-to-end tests against an Mach-O binary

REPRO STEPS (Linux host and target):

  1. Checkout https://github.com/anforowicz/rust/tree/export-visibility => rust-lang/rust@091cc7f. For example:

    $ git clone https://github.com/anforowicz/rust.git
    $ cd rust
    $ git checkout 091cc7fa4e835de0a83772db028769f541aef973
    $ cat compiler/rustc_attr_parsing/src/attributes/codegen_attrs.rs | grep ExportVisibility # (just to verify we have the right code/branch/commit)
    …
    
  2. Build rustc - e.g. ./[x.py](http://x.py/) build

  3. Prepare a Rust source file that exercises the proposed #[export_visibility = …] attribute:

    $ cat ~/scratch/export_visibility_end_to_end_test.rs
    #![feature(export_visibility)]
    
    #[unsafe(export_name = "test_fn_no_attr")]
    unsafe extern "C" fn test_fn_no_attr() -> u32 {
        line!()
    }
    
    #[unsafe(export_name = "test_fn_inherit")]
    #[export_visibility = "inherit"]
    unsafe extern "C" fn test_fn_asks_to_inherit() -> u32 {
        line!()
    }
    
  4. Build a DSO (using --crate-type=cdylib) using the locally-build rustc. Do this twice:

    • Once with no extra command-line arguments - for example:
      build/x86_64-unknown-linux-gnu/stage1/bin/rustc ~/scratch/export_visibility_end_to_end_test.rs --crate-type=cdylib -o ~/scratch/export_visibility_end_to_end_test_with_no_cmdline_args.so
    • Once with -Zdefault-visibility=hidden - for example:
      build/x86_64-unknown-linux-gnu/stage1/bin/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
  5. Inspect the DSOs built in the previous step. For example:

    $ readelf --dyn-syms --demangle ~/scratch/export_visibility_end_to_end_test_with_no_cmdline_args.so | grep test_fn
        55: 0000000000035950     6 FUNC    GLOBAL DEFAULT   15 test_fn_inherit
        56: 0000000000035960     6 FUNC    GLOBAL DEFAULT   15 test_fn_no_attr
    
    $ readelf --dyn-syms --demangle ~/scratch/export_visibility_end_to_end_test_with_hidden_default_visibility.so | grep test_fn
        55: 00000000000358f0     6 FUNC    GLOBAL DEFAULT   15 test_fn_no_attr
    

@traviscross traviscross removed I-lang-nominated Indicates that an issue has been nominated for prioritizing at the next lang team meeting. P-lang-drag-2 Lang team prioritization drag level 2. labels Oct 15, 2025
@traviscross
Copy link
Contributor

We're talking about this in the lang meeting. Question -- and maybe it's in here -- is there ever a case where one wants to use this without no_mangle? If there is, it'd be worth discussing that in the RFC. If there's not, I could see that affecting how we might want to express the syntax for this.

@tmandry
Copy link
Member

tmandry commented Oct 15, 2025

@rfcbot resolve due diligence on tier 1 platforms

With only "target_default" option in this RFC now, there's no question of how how that gets expressed on multiple platforms.

Clarify if Mach-O and PE test results are a hard blocker/requirement for proceeding with the RFC (even though the RFC itself doesn't anymore mention any specific visibility levels)

Testing is not required for the RFC; the question was whether the RFC defines semantics that make sense on all platforms. Including only "target_default" sidesteps the question.

And since "hidden" and "interposable" have been removed from the RFC, these concerns are no longer relevant:

@rfcbot resolve specify how inlining interacts with hidden symbols
@rfcbot resolve UB potential of interposable

@anforowicz
Copy link
Author

We're talking about this in the lang meeting. Question -- and maybe it's in here -- is there ever a case where one wants to use this without no_mangle? If there is, it'd be worth discussing that in the RFC. If there's not, I could see that affecting how we might want to express the syntax for this.

Not really - #[export_visibility = ...] should only ever be used with either #[no_mangle] or #[export_name = ...].

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).
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do you mean that -Zdefault-visibility=hidden + #[export_visibility = "target_default"] on ELF would result in default visibility? If I understand that correctly, that makes this attribute useless for projects that let rustc do the linking and don't use -Zdefault-visibility as in that case there is still no way to restrict the visibility of #[no_mangle] functions.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I mean that -Zdefault-visibility=hidden + #[export_visibility = "target_default"] on ELF will result in hidden visibility. I've added some examples to the RFC - hopefully that helps?

I think the confusion stems from the fact that "the default visibility of the target platform" can be interpreted as either one of:

Does the example help here? Or do you think we should change the names proposed by the RFC? e.g. maybe go back to "inherit"? Or come up with some other synonym for "default" like maybe "baseline"?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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:

I would have expected -Zdefault-visiblity=hidden to mark all functions as hidden unless explicitly overwritten. But that is indeed not the current behavior: https://github.com/rust-lang/rust/blob/779e19d8baa3e3625bd4fc5c85cbb2ad47b43155/compiler/rustc_monomorphize/src/partitioning.rs#L931-L933 I guess that explains my confusion about #[export_visibility = "target_default"].

@traviscross
Copy link
Contributor

traviscross commented Oct 15, 2025

Not really - #[export_visibility = ...] should only ever be used with either #[no_mangle] or #[export_name = ...].

Might it make sense syntactically, then, to be coupled with those? E.g., #[no_mangle(visibility = "..")]. I suppose for export_name it'd be less clear how to combine it since it uses the MetaNameValueStr syntax.

@anforowicz
Copy link
Author

I think I would lean toward keeping #[export_visibility = ...] as a separate, new attribute (rather than making it an extra option for the existing #[no_mangle] and #[export_name = ...] attributes).

Not really - #[export_visibility = ...] should only ever be used with either #[no_mangle] or #[export_name = ...].

Might it make sense syntactically, then, to be coupled with those? E.g., #[no_mangle(visibility = "..")], #[export_name = "..", visibility = ".."].

I am not sure if #[export_name = "..", visibility = ".."] can be supported by the current grammar from https://doc.rust-lang.org/reference/attributes.html#r-attributes.meta.builtin.syntax. We could in theory use https://doc.rust-lang.org/reference/attributes.html#grammar-MetaListNameValueStr which would result in #[export_name(name = "..", visibility = "..")] (although having to same "name" twice doesn't seem that great).

If we could imagine any reason to want this with unmangled symbols, then I could see this being handled in a syntactically orthogonal manner, but if not, then I start to see some virtue in putting these together, especially if we think we might want it to be an error to use export_visibility without no_mangle or export_name.

I note that having a separate attribute may be desirable for extra flexibility. For example, it allows for separate, independent control of visibility by cfg_attr. In theory cfg_attr could also be used with export_name(name = .., visibility = ..) but it risks combinatorial explosion (some configurations may control the export name, some may control the visibility). This may be especially important initially, when #[export_visibility = ...] will be gated behind an unstable feature flag and therefore only available in some rustc versions.

FWIW the initial prototype for this RFC only allows #[export_visibility = ...] on fn and static, but having a separate attribute would theoretically allow supporting/considering in the future a use case like this:

```
#[export_visibility = ...]  // Specified once
mod foo {
    #[unsafe(export_name = "...")]  // Specified multiple times - once for each function.
    extern "C" fn foo() {}

    #[unsafe(export_name = "...")]
    extern "C" fn bar() {}
} 
```

PS. I am not sure if there are other similar attributes to take inspiration from. In particular, I am not sure if the linkage attribute or other attributes (#[used]?) only make sense for #[no_mangle] / #[export_name = ...] items.

@bjorn3
Copy link
Member

bjorn3 commented Oct 16, 2025

PS. I am not sure if there are other similar attributes to take inspiration from. In particular, I am not sure if the linkage attribute or other attributes (#[used]?) only make sense for #[no_mangle] / #[export_name = ...] items.

#[used] is useful independently of #[no_mangle]/#[export_name]. It is generally used in combination with #[link_section]. #[linkage] is not really useful without #[no_mangle]/#[export_name], but given that it is unstable there is no reason we couldn't merge it into #[no_mangle]/#[export_name] for consistency if we do the same for #[export_visibility].

@traviscross
Copy link
Contributor

traviscross commented Oct 16, 2025

The interaction with cfg_attr is a good argument. I find that persuasive.

Setting aside the syntax, would we specify export_visibility to be accepted without no_mangle or export_name, or would you expect this to be an error?

(It'd be good for the RFC to speak to this if it doesn't already.)

@anforowicz
Copy link
Author

Setting aside the syntax, would we specify export_visibility to be accepted without no_mangle or export_name, or would you expect this to be an error?

I would expect this to be an error. Rationale: it's easier to relax this restriction in the future (if needed) than doing things the other way round (starting by allowing it everywhere and then risk breaking changes to add restrictions).

And the prototype does treat this as an error:

FWIW the current prototype checks codegen_fn_attrs.contains_extern_indicator() as a precondition for allowing #[export_visibility = ...] attribute. This is a proxy for having #[no_mangle] or #[export_name = ...] or other similar current or future attribute, but this is probably too broad, because elsewhere in the RFC we say that using #[export_visibility = ...] with #[rustc_std_internal_symbol] should be an error - I just didn't implement that yet in the prototype.

(It'd be good for the RFC to speak to this if it doesn't already.)

Done - see 68a5cef

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

disposition-merge This RFC is in PFCP or FCP with a disposition to merge it. I-lang-radar Items that are on lang's radar and will need eventual work or consideration. proposed-final-comment-period Currently awaiting signoff of all team members in order to enter the final comment period. T-lang Relevant to the language team, which will review and decide on the RFC.

Projects

None yet

Development

Successfully merging this pull request may close these issues.