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:
__attribute__(( export_name("blah-blah") ))
does not work__attribute__(( visibility("default") ))
with or without --export-dynamic
does not workwasm-ld
directly or via clang
wrapper results in the same behavior.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.