WebAssembly / WASI

WebAssembly System Interface
Other
4.85k stars 251 forks source link

__wasm_call_ctors, __wasm_call_dtors #471

Closed realuptime closed 2 years ago

realuptime commented 2 years ago

Hey,

Why in the 14 release wasm_call_ctors and wasm_call_ctors is called after each export function call? In the 12 release everything worked fine and all the variables and state was preserved, but 14 release broke this.

realuptime commented 2 years ago

Hey, I found a workaround after reading this: https://reviews.llvm.org/D81689

This new behavior is disabled when the input has an explicit call to
__wasm_call_ctors, indicating code not expecting new-style command
support.

So in case anyone has this issue that static constructors and destructors are being called for each export function, make a call to __wasm_call_ctors in _start/main function:

In my case, C++: // declare external C function extern "C" { // mandatory! extern void __wasm_call_ctors(); }

WA_EXPORT("main") int main(void) { __wasm_call_ctors(); // this call is needed! }

And not the linker generates code where: exported functions are no longer wrapped in wasm_call_ctors / wasm_call_dtors calls!!!!

realuptime commented 2 years ago

I also suggest to avoid static destructors for unexpected llvm changes that will break correct functionality!

To detect them use the -Wexit-time-destructors flag. I also enable -Werror to be sure.

It is also worth noting that clang has support for not generating static destructors: -fno-c++-static-destructors https://clang.llvm.org/docs/AttributeReference.html#no-destroy

Calling by default C++ static constructors and functions (static int x = somefunc()) for each "export" entry point is a very very bad idea!!! I had to compile with -fno-c++-static-destructors first and then refactor my code. Or use no_destroy flag (https://clang.llvm.org/docs/AttributeReference.html#no-destroy). For example, on the project that I am working on I am calling a function(export.NeedsDisplay()) for each rendered frame, which could be 1000 times per second without VSYNC!

The project: http://myapp.eu/wasi/

I also wrote about this issue to llvm guys: https://reviews.llvm.org/D81689#3333973

sbc100 commented 2 years ago

The idea here is that there are two different types of wasm modules that one might build: -mexec-model=command and -mexec-model=reactor. In the default "command" model your application entry points act line "main" in that they run a single function that the lifetime of the module is bound to that single entry point. In this mode the idea is that state does not persist between calls the module (just like the main in /bin/ls.. each time you call it your can a fresh process).

What you are building sound more like a "reactor" which is more like shared library that maintains state between calls into it from the outside.

You can read more about this change here: https://github.com/WebAssembly/WASI/issues/13

realuptime commented 2 years ago

Thank you Sam!

PiotrSikora commented 2 years ago

The issue is that this change broke previously working WASI reactors.

Note that Rust supports building proper WASI reactors only in nightly using -Z wasi-exec-model=reactor, and the only(?) way to create WASI reactors using stable is to build cdylib library (.wasm module without a start function) with exported entrypoints.

This worked fine until Rust v1.56.0 (which updated LLVM to v13.0), which started adding command_export wrappers to the existing entrypoints. This unexpected behavior leads to either degraded performance (WASI constructors and destructors called on each entrypoint, which is a source of significant overhead for shallow entrypoints) or pretty unusable modules (in restricted environments that don't always allow hostcalls).

Thanks for the workaround, but I don't understand why this was added as an opt-out instead of an opt-in feature.

sunfishcode commented 2 years ago

The C execution model is that there's a main function, when it's called, the program runs, and when it exits, the program completes. wasi-libc, though its use of musl, inherits this assumption. Calling functions before libc's startup code has run, or after its shutdown code has run, isn't how it was anticipated to work. In practice, it appears that doing so kind of worked, but only if you didn't call into certain parts of libc.

"-mexec-model=reactor" is the LLVM wasm backend's way to support this use case, with a design and implementation that actually anticipates being used this way. It supports calling exports without rerunning the constructor on each entrypoint.

PiotrSikora commented 2 years ago

Yes, I understand that.

What I'm saying is that the LLVM change (and the lack of support for -mexec-model=reactor in Rust stable) changed previously working "handmade" WASI reactors (running constructors only once) into new-style multi-entry commands (running constructors on each entry).

sunfishcode commented 2 years ago

Ultimately, I made a judgement call. It appears I misjudged how many people were using "handmade" WASI reactors here.

PiotrSikora commented 2 years ago

Yeah, no worries. The landscape was very different 2 years ago when you originally authored that change... and it wouldn't even be an issue if wasi-exec-model=reactor stabilized in Rust in the meantime.

Anyway, I mostly wanted to bring more attention to this issue and provide more context for other people to find, since I would definitely spend way more time tracking this down if @realuptime didn't open this issue.