Uh oh!
There was an error while loading. Please reload this page.
Staticlib hide internal symbols - #155338
Conversation
rustbot
commented
Apr 15, 2026
rustbot has assigned @petrochenkov. Use Why was this reviewer chosen?The reviewer was selected based on:
|
This comment has been minimized.
This comment has been minimized.
bjorn3
commented
Apr 15, 2026
This would also need to rename symbols to avoid conflicts between two rust staticlibs ending up getting linked together, right? |
bjorn3
commented
Apr 15, 2026
Why exactly is that the case? |
ff707ad to
7ac49d1Compare
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
cezarbbb
commented
Apr 15, 2026
My primary goal right now is to reduce binary size, so I don't have immediate plans to implement symbol renaming. This means that linking multiple Rust staticlibs together can still result in multiple definition errors. Would you like me to address that in this PR as well? It seems feasible to implement — for example, by rehashing symbols and updating their references accordingly. |
cezarbbb
commented
Apr 15, 2026
I previously assumed this symbol needed to remain externally visible to support scenarios requiring cross-language exception propagation. Do you think we should also set rust_eh_personality as hidden? |
bjorn3
commented
Apr 15, 2026
If it isn't too hard it would be nice to do symbol renaming too. I think doing in-place modification isn't going to work for that though. Adding a unique suffix would require growing the size of the string table. |
bjorn3
commented
Apr 15, 2026
|
cezarbbb
commented
Apr 15, 2026
Got it. I will first fix the |
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
5e1c3a1 to
c7d4e98CompareSparrowLii
commented
Apr 16, 2026
@bors delegate=try |
✌️ @cezarbbb, you can now perform try builds on this pull request! You can now post |
cezarbbb
commented
Apr 16, 2026
@bors try |
This comment has been minimized.
This comment has been minimized.
`-Zstaticlib-hide-internal-symbols`: Hide non-exported internal symbols from staticlibs
cezarbbb
commented
Apr 16, 2026
@bors try jobs=x86_64-* |
This comment has been minimized.
This comment has been minimized.
This pull request was unapproved. This PR was contained in a rollup (#157464), which was unapproved. |
bc6e8ed to
e5d8cdfComparecezarbbb
commented
Jun 5, 2026
It appears that |
cezarbbb
commented
Jun 5, 2026
@bors try jobs=i686-* |
This comment has been minimized.
This comment has been minimized.
Staticlib hide internal symbols try-job: i686-*
petrochenkov
commented
Jun 5, 2026
Yeah, it's not good to rely on specific sizes for C types. |
petrochenkov
commented
Jun 5, 2026
@bors r+ |
… r=petrochenkov Staticlib hide internal symbols According to issue rust-lang#104707, when building a staticlib, all Rust internal symbols — mangled symbols, `#[rustc_std_internal_symbol]` items, allocator shims, etc. — leak out of the static archive. In contrast, cdylib correctly exports only `#[no_mangle]` symbols via a linker version script. `-Zstaticlib-hide-internal-symbols` directly post-processes ELF object files in the archive: parsing the `SHT_SYMTAB` sections and setting `STV_HIDDEN` visibility on any `GLOBAL/WEAK` defined symbol that is not in the exported symbol set, without changing the binding. This is an in-place modification (only writing the st_other byte per matching entry), with zero overhead. Supported on ELF targets (Linux, BSD, etc.) and Apple targets (macOS, iOS, etc.). On unsupported targets (Windows), a warning is emitted and the flag has no effect. **Update**: The rename counterpart (`-Zstaticlib-rename-internal-symbols`) is in rust-lang#156950. The test code are as follows: 1.a std rust staticlib: ```rust use std::collections::HashMap; use std::panic::{catch_unwind, AssertUnwindSafe}; #[no_mangle] pub extern "C" fn my_add(a: i32, b: i32) -> i32 { a + b } #[no_mangle] pub extern "C" fn my_hash_lookup(key: u64) -> u64 { let mut map = HashMap::new(); for i in 0..100u64 { map.insert(i, i.wrapping_mul(2654435761)); } *map.get(&key).unwrap_or(&0) } pub fn internal_reverse(s: &str) -> String { s.chars().rev().collect() } #[no_mangle] pub extern "C" fn my_format_number(n: i32) -> i32 { let s = format!("number: {}", n); s.len() as i32 } #[no_mangle] pub extern "C" fn my_safe_div(a: i32, b: i32) -> i32 { match catch_unwind(AssertUnwindSafe(|| { if b == 0 { panic!("division by zero!"); } a / b })) { Ok(result) => result, Err(_) => -1, } } #[no_mangle] pub extern "C" fn my_uncaught_panic() { panic!("uncaught panic across FFI"); } ``` 1.b downstream c program: ```c extern int my_add(int a, int b); extern unsigned long my_hash_lookup(unsigned long key); extern int my_format_number(int n); extern int my_safe_div(int a, int b); extern void my_uncaught_panic(void); int main() { int failures = 0; if (my_add(10, 20) != 30) failures++; if (my_hash_lookup(5) != 5UL * 2654435761UL) failures++; if (my_format_number(42) != 10) failures++; if (my_safe_div(100, 5) != 20) failures++; if (my_safe_div(100, 0) != -1) failures++; pid_t pid = fork(); if (pid == 0) { alarm(5); my_uncaught_panic(); _exit(0); } else { waitpid(pid, &status, 0); } return failures; } ``` The test results with different compiler flags(which might cause binary size reduction) are as follows: 1.c result with `-Zstaticlib-hide-internal-symbols` ``` settings OFF ON -Zsave ALL OFF.dynsym ON.dynsym ------------------------------------------------------------------------ default 1.7M 1.5M 204K (12%) 1735 5 1730 lto_thin 616K 584K 33K (5%) 246 5 241 lto_fat 525K 525K 0 (0%) 6 5 1 opt_s 1.7M 1.5M 204K (12%) 1735 5 1730 opt_z 1.7M 1.5M 204K (12%) 1735 5 1730 lto_thin_z 602K 570K 32K (5%) 246 5 241 lto_fat_z 514K 514K 0 (0%) 6 5 1 full 514K 514K 0 (0%) 6 5 1 ``` 1.d result with `-Zstaticlib-hide-internal-symbols + -Zstaticlib-rename-internal-symbols` ``` settings OFF ON -Zsave ALL OFF.dynsym ON.dynsym ------------------------------------------------------------------------ default 1.7M 1.5M 162K (9%) 1735 5 1730 lto_thin 616K 599K 18K (2%) 246 5 241 lto_fat 525K 535K -1% (-1%) 6 5 1 opt_s 1.7M 1.5M 162K (9%) 1735 5 1730 opt_z 1.7M 1.5M 162K (9%) 1735 5 1730 lto_thin_z 602K 585K 18K (2%) 246 5 241 lto_fat_z 514K 524K -1% (-1%) 6 5 1 full 514K 523K -1% (-1%) 6 5 1 ``` 2.a no_std rust staticlib ```rust #![no_std] #![feature(core_intrinsics)] use core::panic::PanicInfo; #[panic_handler] fn panic(_info: &PanicInfo) -> ! { loop {} } #[no_mangle] pub extern "C" fn embedded_add(a: i32, b: i32) -> i32 { a.wrapping_add(b) } #[no_mangle] pub extern "C" fn embedded_checksum(data: *const u8, len: usize) -> u8 { if data.is_null() { return 0; } let slice = unsafe { core::slice::from_raw_parts(data, len) }; let mut sum: u8 = 0; for &byte in slice { sum = sum.wrapping_add(byte); } sum } fn internal_helper() -> i32 { 42 } #[no_mangle] pub extern "C" fn call_internal() -> i32 { internal_helper() } #[no_mangle] pub extern "C" fn embedded_trigger_abort() { core::intrinsics::abort(); } ``` 2.b downstream c program ```c extern int embedded_add(int a, int b); extern unsigned char embedded_checksum(const unsigned char *data, unsigned long len); extern int call_internal(void); extern void embedded_trigger_abort(void); int main() { int failures = 0; if (embedded_add(10, 20) != 30) failures++; unsigned char data[] = {1, 2, 3}; if (embedded_checksum(data, 3) != 6) failures++; if (call_internal() != 42) failures++; pid_t pid = fork(); if (pid == 0) { embedded_trigger_abort(); _exit(0); } else { waitpid(pid, &status, 0); } return failures; } ``` The test results with different compiler flags(which might cause binary size reduction) are as follows: 2.c result with `-Zstaticlib-hide-internal-symbols` ``` settings OFF ON -Zsave ALL OFF.dynsym ON.dynsym ------------------------------------------------------------------------ default 485K 429K 56K (11%) 490 4 486 lto_thin 180K 180K 0 (0%) 4 4 0 lto_fat 179K 179K 0 (0%) 4 4 0 opt_s 485K 429K 56K (11%) 490 4 486 opt_z 485K 429K 56K (11%) 490 4 486 lto_thin_z 180K 180K 0 (0%) 4 4 0 lto_fat_z 179K 179K 0 (0%) 4 4 0 full 179K 179K 0 (0%) 4 4 0 ``` 2.d result with `-Zstaticlib-hide-internal-symbols + -Zstaticlib-rename-internal-symbols` ``` settings OFF ON -Zsave ALL OFF.dynsym ON.dynsym ------------------------------------------------------------------------ default 485K 447K 39K (7%) 490 4 486 lto_thin 180K 189K -5% (-5%) 4 4 0 lto_fat 179K 189K -5% (-5%) 4 4 0 opt_s 485K 448K 38K (7%) 490 4 486 opt_z 485K 448K 38K (7%) 490 4 486 lto_thin_z 180K 189K -5% (-5%) 4 4 0 lto_fat_z 179K 189K -5% (-5%) 4 4 0 full 179K 189K -5% (-5%) 4 4 0 ``` Test results show that this compiler option is beneficial for scenarios where LTO cannot be enabled. r? @bjorn3@petrochenkov
JonathanBrouwer
commented
Jun 5, 2026
💔 I suspect this PR failed tests as part of a rollup After fixing the problem, consider running a try job for the failed job before re-approving. Link to failure: #157484 (comment) |
This pull request was unapproved. This PR was contained in a rollup (#157484), which was unapproved. |
cezarbbb
commented
Jun 5, 2026
Weird. I'll check and fix it on Monday. |
cezarbbb
commented
Jun 8, 2026
It seems that the logic for handling prefixe |
This comment has been minimized.
This comment has been minimized.
cezarbbb
commented
Jun 8, 2026
@rustbot ready |
petrochenkov
commented
Jun 8, 2026
@bors r+ |
JonathanBrouwer
commented
Jun 9, 2026
@rust-timer build 7f76357 |
This comment has been minimized.
This comment has been minimized.
rust-timer
commented
Jun 9, 2026
Finished benchmarking commit (7f76357): comparison URL. Overall result: ✅ improvements - no action neededBenchmarking means the PR may be perf-sensitive. It's automatically marked not fit for rolling up. Overriding is possible but disadvised: it risks changing compiler perf. @bors rollup=never Instruction countOur most reliable metric. Used to determine the overall result above. However, even this metric can be noisy.
Max RSS (memory usage)Results (primary 1.7%)A less reliable metric. May be of interest, but not used to determine the overall result above.
CyclesResults (secondary -3.5%)A less reliable metric. May be of interest, but not used to determine the overall result above.
Binary sizeThis perf run didn't have relevant results for this metric. Bootstrap: 516.268s -> 514.837s (-0.28%) |
View all comments
According to issue #104707, when building a staticlib, all Rust internal symbols — mangled symbols,
#[rustc_std_internal_symbol]items, allocator shims, etc. — leak out of the static archive. In contrast, cdylib correctly exports only#[no_mangle]symbols via a linker version script.-Zstaticlib-hide-internal-symbolsdirectly post-processes ELF object files in the archive: parsing theSHT_SYMTABsections and settingSTV_HIDDENvisibility on anyGLOBAL/WEAKdefined symbol that is not in the exported symbol set, without changing the binding. This is an in-place modification (only writing the st_other byte per matching entry), with zero overhead.Supported on ELF targets (Linux, BSD, etc.) and Apple targets (macOS, iOS, etc.). On unsupported targets (Windows), a warning is emitted and the flag has no effect.
Update: The rename counterpart (
-Zstaticlib-rename-internal-symbols) is in #156950.The test code are as follows:
1.a std rust staticlib:
1.b downstream c program:
The test results with different compiler flags(which might cause binary size reduction) are as follows:
1.c result with
-Zstaticlib-hide-internal-symbols1.d result with
-Zstaticlib-hide-internal-symbols + -Zstaticlib-rename-internal-symbols2.a no_std rust staticlib
2.b downstream c program
The test results with different compiler flags(which might cause binary size reduction) are as follows:
2.c result with
-Zstaticlib-hide-internal-symbols2.d result with
-Zstaticlib-hide-internal-symbols + -Zstaticlib-rename-internal-symbolsTest results show that this compiler option is beneficial for scenarios where LTO cannot be enabled.
r? @bjorn3@petrochenkov