linkerclangstatic-librarieswebassembly

wasm-ld --export-all does not work with static libraries


While playing with building WASM modules using clang toolchain I hit a behavior that I'm struggling to understand. Basically, I have a very simple C++ code (in test.cc):

extern "C" int foo() { return 42; }

I want to build out of this a WASM module that just exports the function defined in it. The twist is that I added an intermediate step of creating a static library before linking the final binary.

After reading through https://lld.llvm.org/WebAssembly.html#exports, this is how I went about doing it:

/usr/bin/clang --target=wasm32 -nostdlib -c test.cc -o test.o
/usr/bin/llvm-ar rcsD libtest.a test.o
/usr/bin/wasm-ld -o test.wasm libtest.a --no-entry --no-gc-sections --export-all

However looking at the final result, I see no foo function defined there:

llvm-objdump -d test.wasm

test.wasm:      file format wasm

Disassembly of section CODE:

00000000 <CODE>:
        # 1 functions in section.

00000001 <__wasm_call_ctors>:

       3: 0b            end

At the same time, if I used --export=foo instead of --export-all it works as I expect:

llvm-objdump -d test.wasm

test.wasm:      file format wasm

Disassembly of section CODE:

00000000 <CODE>:
        # 2 functions in section.

00000001 <__wasm_call_ctors>:

       3: 0b            end

00000004 <foo>:
        .local i32
       8: 41 2a         i32.const       42
       a: 21 00         local.set       0
       c: 20 00         local.get       0
       e: 0f            return
       f: 0b            end

Finally, if I use object file directly instead of packaging it into a static library first, it also works as expected. Which is where I suspect the problem actually lies.

I might speculate that maybe linker has some logic that treats static libraries differently and only exports the symbols from static libraries that are explicitly requested and --export-all applies only to kept symbols, while, at the same time, plain --export implies that the symbol must be kept. However, I'm failing to find a confirmation to the above theory.

Can somebody help me understand where the difference between static libraries and regular object files comes from? And consequently, why --export-all does not export symbols from the linked static libraries?

Is it maybe a generally assumed behavior for linkers and that's why I'm struggling to find reference to that in the docs?

Thank you.

FWIW, if it helps, I tried multiple different combinations as well:

  1. Using __attribute__(( export_name("blah-blah") )) does not work
  2. Using __attribute__(( visibility("default") )) with or without --export-dynamic does not work
  3. Using wasm-ld directly or via clang wrapper results in the same behavior.

Solution

  • I might speculate that maybe linker has some logic that treats static libraries differently.

    That's right. wasm-ld is similar to GNU ld, the stock GNU linux linker, and has the same fundamental rules as to when it considers an object file to be needed for the linkage.

    Any object file explicitly input in the commandline is needed unconditionally and statically linked into the output image.

    An object file that is an archive member of a static library is considered needed, by default, if and only if it provides at least one definition for an unresolved symbol reference that has accrued earlier in the linkage. See the Stackoverflow static-libraries tag wiki. The function of a static library is to furnish the linker with a choice of related object files, some of which it might need, so the linker must have prior needs.

    The default behaviour can be overridden by:

    --whole-archive libtest.a --no-whole-archive
    

    The effect of:

    --whole-archive liba.a [libb.a]... --no-whole-archive
    

    is to make the linker suspend its default policy for the sequence of static libraries liba.a [libb.a]... and consider that all the object files in these archives are needed whether they resolve any references or not. See wasm-ld --help.

    If --whole-archive is not in effect, the linkage must consume at least one explicit object file before before any object files in static libraries will be considered needed, because no unresolved references can accrue until an object file containing some unresolved reference is explicitly input.

    Your linkage:

    /usr/bin/wasm-ld -o test.wasm libtest.a --no-entry --no-gc-sections --export-all
    

    does not change that. The effect of --export-all is simply to migrate all symbols in the global symbol table of the output image into its dynamic symbol table, making them accessible to the dynamic linker. But since libtest.a(test.o) is not even linked into the image, foo does not get into the global symbol table in the first place.

    On the other hand your linkage:

    /usr/bin/wasm-ld -o test.wasm libtest.a --no-entry --no-gc-sections --export=foo
    

    does work because:

    $ wasm-ld --help | grep '\--export=<value>'
      --export=<value>        Force a symbol to be exported
      
    

    The operative word is force. The effect of --export=foo is to coerce the linker to assume that foo is an unresolved reference before the linkage consumes any files and at the end to enter foo in the dynamic symbol table if a definition is found. That assumption will induce the linker to consider that libtest.a(test.o) is needed, and in your case that linkage is equivalent to:

    $ /usr/bin/wasm-ld -o test.wasm libtest.a --no-entry --no-gc-sections --undefined=foo --export-all
    

    Your attempts to use:

    __attribute__(( export_name("blah-blah") ))
    

    and:

    __attribute__(( visibility("default") ))
    

    were unsuccessful because the first only causes the symbol to which is applied to be represented by an alias in the dynamic symbol table of the image, and the second only applies default dynamic visibility to a symbol in the image. Neither of them can make a symbol be defined in the image if no definition of it is linked.