-
Notifications
You must be signed in to change notification settings - Fork 159
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Ensure the WASM output is compatible with wasmtime + WASI #1461
Comments
The goal here would be to do: $ lfortran --backend=wasm examples/expr2.f90 -o expr2.wasm
$ wasmtime expr2.wasm
Error: failed to run main module `expr2.wasm`
Caused by:
0: failed to instantiate "expr2.wasm"
1: unknown import: `js::print_i32` has not been defined We should standardize the WASM output that we generate, and it seems the change is minor, in this example the WAT looks like: $ lfortran --show-wat examples/expr2.f90
(module
(type (;0;) (func (param i32) (result)))
(type (;1;) (func (param i64) (result)))
(type (;2;) (func (param f32) (result)))
(type (;3;) (func (param f64) (result)))
(type (;4;) (func (param i32 i32) (result)))
(type (;5;) (func (param) (result)))
(type (;6;) (func (param i32) (result)))
(type (;7;) (func (param) (result)))
(import "js" "print_i32" (func (;0;) (type 0)))
(import "js" "print_i64" (func (;1;) (type 1)))
(import "js" "print_f32" (func (;2;) (type 2)))
(import "js" "print_f64" (func (;3;) (type 3)))
(import "js" "print_str" (func (;4;) (type 4)))
(import "js" "flush_buf" (func (;5;) (type 5)))
(import "js" "set_exit_code" (func (;6;) (type 6)))
(import "js" "memory" (memory (;0;) 100 100))
(func $7 (type 7) (param) (result)
(local i32)
i32.const 25
local.set 0
local.get 0
call 0
call 5
i32.const 0
call 6
return
)
(export "_lcompilers_main" (func $7))
) And we just need to change the imports: (import "js" "print_i32" (func (;0;) (type 0)))
(import "js" "print_i64" (func (;1;) (type 1)))
(import "js" "print_f32" (func (;2;) (type 2)))
(import "js" "print_f64" (func (;3;) (type 3)))
(import "js" "print_str" (func (;4;) (type 4)))
(import "js" "flush_buf" (func (;5;) (type 5)))
(import "js" "set_exit_code" (func (;6;) (type 6)))
(import "js" "memory" (memory (;0;) 100 100)) To be compatible with WASI/wasmtime. Details to be figured out. Then in JavaScript we change the API of our implementation to be also compatible with WASI for the subset that we generate, and so the generated wasm will continue working at https://dev.lfortran.org/. |
For printing, we can see how Clang does it using this tutorial: https://github.com/bytecodealliance/wasmtime/blob/main/docs/WASI-tutorial.md |
As an example, instead of https://github.com/WebAssembly/WASI/blob/main/phases/snapshot/docs.md#-proc_exitrval-exitcode |
Would it be fine if we use
I guess there might be a fix for the above. I am concerned if the fix might break/affect other development environment/tools like |
Yes, I think emscripten is fine. As long as it runs in wasmtime. The goal is to just figure out how they do it, what API they use. |
Got it. |
It seems |
Here is how to experiment with WASI. First install https://github.com/WebAssembly/wasi-sdk, you can go to releases and unpack a tarball. Let's create a simple program: #include <stdio.h>
int main() {
int x;
x = (2+3)*5;
printf("%d\n", x);
return 0;
} Compile to WASM + WASI: $ /path/to/wasi-sdk-19.0/bin/clang expr2.c -o expr2.wasm
$ file expr2.wasm
expr2.wasm: WebAssembly (wasm) binary module version 0x1 (MVP)
$ ll -h expr2.wasm
-rwxr-xr-x 1 ondrej staff 18K Feb 3 20:07 expr2.wasm You can run it with $ wasmtime expr2.wasm
25
$ wasmer expr2.wasm
25 You can compile to a binary using: $ wasmer create-exe expr2.wasm -o expr2
Compiler: cranelift
Target: aarch64-apple-darwin
Format: Symbols
Using path `/Users/ondrej/.wasmer/lib/libwasmer.a` as libwasmer path.
✔ Native executable compiled successfully to `expr2`.
$ file expr2
expr2: Mach-O 64-bit executable arm64
$ ll -h expr2
-rwxr-xr-x 1 ondrej staff 20M Feb 3 20:09 expr2 And you can run the binary: $ ./expr2
25 The You can then disassemble using: $ /path/to/wabt/build/wasm2wat expr2.wasm -o expr2.wat
$ head -n 20 expr2.wat
(module
(type (;0;) (func (param i32 i32 i32) (result i32)))
(type (;1;) (func (param i32 i64 i32) (result i64)))
(type (;2;) (func (param i32) (result i32)))
(type (;3;) (func (param i32 i32) (result i32)))
(type (;4;) (func (param i32 i64 i32 i32) (result i32)))
(type (;5;) (func (param i32 i32 i32 i32) (result i32)))
(type (;6;) (func (param i32)))
(type (;7;) (func))
(type (;8;) (func (result i32)))
(type (;9;) (func (param i32 i32 i32 i32 i32) (result i32)))
(type (;10;) (func (param i32 i32 i32)))
(type (;11;) (func (param i32 i32 i32 i32 i32)))
(type (;12;) (func (param f64 i32) (result f64)))
(import "wasi_snapshot_preview1" "fd_close" (func $__imported_wasi_snapshot_preview1_fd_close (type 2)))
(import "wasi_snapshot_preview1" "fd_fdstat_get" (func $__imported_wasi_snapshot_preview1_fd_fdstat_get (type 3)))
(import "wasi_snapshot_preview1" "fd_seek" (func $__imported_wasi_snapshot_preview1_fd_seek (type 4)))
(import "wasi_snapshot_preview1" "fd_write" (func $__imported_wasi_snapshot_preview1_fd_write (type 5)))
(import "wasi_snapshot_preview1" "proc_exit" (func $__imported_wasi_snapshot_preview1_proc_exit (type 6)))
(func $__wasm_call_ctors (type 7)) So the WASM binary is using exactly 5 imports. The WASM that we generate thus should use exactly these imports and things should work. Regarding how to print floats: we have to implement it one way or the other.
We can, although it would not be used by the frontend (which would continue using the current approach with existing ASR nodes), only by the WASM backend.
Yes, the backend has to implement it. Here are some ideas how to do it:
The wasm_x64 backend will thus not print any integers directly anymore, it just implements Another way to look at it: currently we implement the int to str conversion in the wasm_x64 backend in assembly. The first step is to "lift" it up into the WASM backend to implement in WASM, and the wasm_x64 only implements printing of strings. The next step is to lift it up from the WASM backend into ASR, so WASM backend only prints strings, and ASR implements the conversion. The frontend typically generates Print(int), so it would probably be an ASR pass that does it. That way backends that don't want to implement can just call the pass. The mechanism to do that is similar to the IntrinsicFunction mechanism that I am working on in: lfortran/lfortran#1045. Until then, just implement the int to str in Python, compile to WASM using your backend, and then you can "semi-manually" call it from the backend, or just implement in WASM by hand, it's probably not that hard, if you did it already in assembly before. That is, take this: lpython/src/libasr/codegen/x86_assembler.cpp Line 139 in acd593e
|
Here is another related issue: I realized today that wasmtime cannot create a standalone binary executable, it does compile WASM to native code, but it relies on various hooks into the wasmtime runtime: bytecodealliance/wasmtime#4563 (comment). Wasmer can create an executable and it works for the simplest examples, as shown above, but the executable is large (bloated). Also the generated binary doesn't seem to have the capability to read files: wasmerio/wasmer#3574, which is a problem. To read files it seems one must run it via wasmtime or wasmer. The goal of our wasm_x64 backend should be to create a very small lean x64 standalone binary, and if allowed, the binary should be able to read files. |
Here is an example how to get lean WASM output using Clang: apparently we cannot use the libc library which is bloated (18K): int main() {
int x;
x = (2+3)*5;
return x;
} this gives: $ /path/to/wasi-sdk-19.0/bin/clang -Os expr2.c -o expr2.wasm
$ /path/to/wabt/build/wasm2wat expr2.wasm -o expr2.wat
$ ll expr2.wasm
-rwxr-xr-x 1 ondrej staff 507 Feb 3 20:57 expr2.wasm
$ wasmtime expr2.wasm
$ echo $?
25 The (module
(type (;0;) (func (param i32)))
(type (;1;) (func))
(type (;2;) (func (result i32)))
(import "wasi_snapshot_preview1" "proc_exit" (func $__imported_wasi_snapshot_preview1_proc_exit (type 0)))
(func $__wasm_call_ctors (type 1))
(func $_start (type 1)
(local i32)
block ;; label = @1
block ;; label = @2
i32.const 0
i32.load offset=1024
br_if 0 (;@2;)
i32.const 0
i32.const 1
i32.store offset=1024
call $__wasm_call_ctors
call $__original_main
local.set 0
call $__wasm_call_dtors
local.get 0
br_if 1 (;@1;)
return
end
unreachable
unreachable
end
local.get 0
call $__wasi_proc_exit
unreachable)
(func $__original_main (type 2) (result i32)
i32.const 25)
(func $__wasi_proc_exit (type 0) (param i32)
local.get 0
call $__imported_wasi_snapshot_preview1_proc_exit
unreachable)
(func $dummy (type 1))
(func $__wasm_call_dtors (type 1)
call $dummy
call $dummy)
(table (;0;) 1 1 funcref)
(memory (;0;) 2)
(global $__stack_pointer (mut i32) (i32.const 66576))
(export "memory" (memory 0))
(export "_start" (func $_start))) Unfortunately the wasmer created binary does not work: $ wasmer create-exe expr2.wasm -o expr2
Compiler: cranelift
Target: aarch64-apple-darwin
Format: Symbols
Using path `~/.wasmer/lib/libwasmer.a` as libwasmer path.
✔ Native executable compiled successfully to `expr2`.
$ ll -h expr2
-rwxr-xr-x 1 ondrej staff 20M Feb 3 20:59 expr2
$ ./expr2
Trap is not NULL: TODO:
$ echo $?
255 The 507 bytes of WASM still generates a 20M binary, I think they are linking the wasmer runtime in it:
which makes it bloated. Our backend must generate truly native executable, no runtime like this. I think part of the reason the wasmer generated binary is so large is because they assume the webassembly code cannot be trusted, so they implement WASM in a "safe" way, handling WASM traps correctly, etc. While we assume the code can be trusted and we care more about the size of the binary and speed of compilation, so this should allow us to create small binaries. |
@Shaikh-Ubaid given that neither wasmtime nor wasmer can create binaries that robustly work yet (and if they work, they are still about 1,000x larger than they should be), I would not worry about creating binaries using them, but let's use wasmtime and optionally wasmer to test that the WASM that LPython generates runs via their runtime ( |
Got it.
What do we mean when we say "semi-manually"? Yes, we can implement it similar to |
This #1461 (comment) outputs a small-sized binary because I think it does have any print statements. When a print statement is added, the binary size is (base) $ cat expr2.c
#include <stdio.h>
int main() {
int x;
x = (2+3)*5;
printf("%d\n", x);
return 0;
}
(base) $ clang -Os expr2.c -o expr2.wasm
(base) $ wasmtime expr2.wasm
25
(base) $ |
#1461 (comment), |
Is it possible to include a function in the def print_int(x: i32):
#print digits of x similar to wasm_x64 backend's print_i32() This function could then be parsed/processed upto the |
Yes, that's the first bullet point approach in #1461 (comment). |
What's missing:
|
This issue is now fixed. The GUI supports WASI in a Draft PR that we will merge in a week: lfortran/lcompilers_frontend#64, so I'll close this issue. |
We should ensure that the subset of WASM that we generate in the ASR->WASM is compatible with wasmtime + WASI, so that one can use wasmtime and other tools like (wasm2c) out of the box.
The text was updated successfully, but these errors were encountered: