Skip to content

--lib archives are compiled without per-function sections, so ELF consumers cannot section-GC them #287

Description

@mmamedel

Summary

--lib archives are compiled without -ffunction-sections -fdata-sections. On
ELF targets that makes --gc-sections at the consumer's link only able to
discard whole objects, so a library-mode consumer cannot recover the size win
that 0.0.36 gives the executable lane. Adding the two flags to the library
archive's cflags takes our Android .so from 852,720 to 674,616 bytes
(-21%) with no other change.

Why this isn't a bug report about the executable lane

0.0.36's section elimination is deliberately scoped, and the note beside it is
clear — backend/native-toolchain.js:

Archives and compile-only object/library recipes deliberately do not use the
link half. In particular, --lib preserves its established object and
archive contract; section GC is an executable-link optimization only.

We agree with that scoping: the link half belongs to whoever links. Our ask is
only about the compile half, which is not an optimization the consumer can
apply retroactively — by the time we get entry.lib.a, the section granularity
is already fixed.

The asymmetry this creates is platform-shaped, and matches the matrix in
executableSectionEliminationFlags:

Target Consumer can strip a --lib archive?
Apple / Mach-O yes — ld64 dead-strips per symbol subsection, no compile flag needed
ELF, COFF no — needs -ffunction-sections at compile time, which only scriptc can pass

So on iOS we fixed this ourselves with -Wl,-dead_strip (409,232 -> 232,208
bytes, -43%). On Android we can only get partway.

Repro

scriptc 0.0.36, NDK r27 (aarch64-linux-android26-clang++), arm64-v8a,
consumer link is a -shared JNI library, all sizes after llvm-strip.

scriptc --lib --profile … -o entry.lib.a
aarch64-linux-android26-clang++ shell.cc -shared -fPIC -O2 \
    -static-libstdc++ entry.lib.a -llog -o libapp.so <flags>
llvm-strip libapp.so
Archive built by Consumer flags libapp.so
stock 0.0.36 none 1,406,488
stock 0.0.36 --gc-sections 1,354,832
stock 0.0.36 --gc-sections --exclude-libs,ALL 852,720
+ -ffunction-sections -fdata-sections --gc-sections --exclude-libs,ALL 674,616

(--exclude-libs,ALL is needed on our side regardless: a shared library
exports every default-visibility symbol, which roots the whole archive and
leaves GC nothing to collect. That part is our bug, and it is fixed.)

The last row was produced by adding the two flags to the cflags array in
compileLibArchive and nothing else. It yields 1,544 .text.* sections in the
archive, and the resulting .so loads under dlopen(RTLD_NOW) on an
arm64-v8a emulator with all JNI entry points resolvable.

Suggested change

In compileLibArchive, alongside the existing -DSCR_LIB:

"-ffunction-sections", "-fdata-sections",

This is compile-side only and does not touch the archive's link contract.
A consumer who does not pass --gc-sections gets an output that is equivalent
but not bit-for-bit reproducible against the old one — worth stating precisely,
since "no change" would be too strong:

stock archive + function-sections
libapp.so size 1,406,488 1,406,488
sections 26 26
.text size 0xb5af8 0xb5af8
dynamic exports 2,751 2,751 (identical symbol names)
undefined symbols 0 0
DT_NEEDED 4 4

Same size, same section count, same .text size, same exported symbol set.
The files do differ — 158,124 bytes — but the difference is the order of
functions within .text, which is what per-function sections change. Nothing
is added or discarded unless the consumer asks for --gc-sections.

If you would rather not perturb existing output at all, an opt-in flag works
equally well for us.

Environment

scriptc 0.0.36 · NDK r27.0.12077973 · arm64-v8a, minSdk 26 · macOS host ·
consumer is janela, which builds mobile
apps by linking a --lib archive into a UIKit / JNI shell.

Activity

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

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions