Hi all!
According to @elcritch in another thread I posted, it should be possible to compile nim to WASM, but I cannot get it done :(
It looks like there is no built-in support for emcc and if I use config nim to use clang and compile nim to C sources, then use emcc to compile these C sources, I'll get a static assert error, saying that the size of "long" is incorrect (can't remember exactly. I'm on another computer without nim). And I tried to "cheat" by setting clang.path and clang.exe to point to emcc.bat, and hoping nim can call emcc directly, but also failed.
So... what is the preferred way to compile nim to WebAssembly? I found two other possible solutions:
https://github.com/arnetheduck/nlvm
It claims wasm32 support is still very bare-bones, so you will need to do a bit of tinkering to get it to work.. Also I found this:
https://github.com/stisa/nwasm
And I still think It's not a bad idea to compile nim to C sources first, then use emcc just like what I did in my C++ -> WASM projects (I already have several such projects deployed in production). Maybe I just need a few preprocessor defines to satisfy nim's static asserts?
BTW: I tried both x64 and x86 versions of nim 1.4.2 (I once suspected wasm32 can only be supported by x86 nim), on windows, but I prefer to use x64 version.
BTW2: I need basic WASI support (my WASM programs will be run in server and embedded in C++, not in browser), which is automatically supported by emcc, that's why I prefer to do nim -> c, then use emcc, if possible.
Thank you! I don't completely understand it, but it certainly gave me some important info. I tried this:
nim c -d:danger -d:emscripten --cpu:wasm32 --compileOnly test.nim
Then go into the nimcache directory, rename @mtest.nim.c to mtest.nim.c, copy files to a testnim directory, and then:
emcc mtest.nim.c stdlib_io.nim.c stdlib_system.nim.c -ID:\dev\nim-1.4.2_x64\nim-1.4.2\lib -ID:\dev\emscripten\testnim
However, I'm getting this:
stdlib_io.nim.c:10:10: fatal error: 'io.h' file not found
#include <io.h>
^~~~~~
1 error generated.
emcc: error: 'D:/dev/emscripten/emsdk/upstream/bin\clang.exe -DEMSCRIPTEN -fignore-exceptions -mllvm -combiner-global-alias-analysis=false -mllvm -enable-emscripten-sjlj -mllvm -disable-lsr -Xclang -isystemD:\dev\emscripten\emsdk\upstream\emscripten\system\include\SDL -target wasm32-unknown-emscripten -D__EMSCRIPTEN_major__=2 -D__EMSCRIPTEN_minor__=0 -D__EMSCRIPTEN_tiny__=12 -D_LIBCPP_ABI_VERSION=2 -Dunix -D__unix -D__unix__ -Werror=implicit-function-declaration --sysroot=D:\dev\emscripten\emsdk\upstream\emscripten\system -Xclang -isystemD:\dev\emscripten\emsdk\upstream\emscripten\cache\include -Xclang -iwithsysroot/include\compat -Xclang -iwithsysroot/include\libc -Xclang -iwithsysroot/lib\libc\musl\arch\emscripten -Xclang -iwithsysroot/local\include -Xclang -iwithsysroot/include\SSE -Xclang -iwithsysroot/include\neon -Xclang -iwithsysroot/lib\compiler-rt\include -Xclang -iwithsysroot/lib\libunwind\include -ID:\dev\nim-1.4.2_x64\nim-1.4.2\lib -ID:\dev\emscripten\testnim stdlib_io.nim.c -c -o C:\Users\eryiju\AppData\Local\Temp\emscripten_temp_64ejlqi6\stdlib_io.nim_1.o' failed (1)
I have two io.h files:
Directory of D:\dev\emscripten\emsdk\upstream\emscripten\system\include\libc\sys
2021/01/10 17:55 219 io.h
1 File(s) 219 bytes
Directory of D:\dev\emscripten\emsdk\upstream\emscripten\system\lib\libc\musl\arch\emscripten\bits
2021/01/10 17:56 0 io.h
1 File(s) 0 bytes
But it looks like it's not in the include path. However, as far as I know, io.h is not ANSI C, so does that mean I somehow failed to tell nim compiler that io.h is not available?
Good progress: I added --os:linux despite the fact that I'm running windows.
nim c -d:danger -d:emscripten --cpu:wasm32 --os:linux --compileOnly test.nim
Then it compiles. The flag is already in ng-public but I thought I should not add it because I'm running windows, but it turns out omitting it (or adding --os:windows) caused the io.h thing and also a few other issue like using _setmode, _fileno and _O_BINARY. All those issues are gone once I set --os:linux.
However, I got an error when running wasm3:
Error: [Fatal] m3_LinkWASI: function signature mismatch
Error: function signature mismatch ()
BTW: test.nim is just
echo "hello"
Ouch, I think wasm did not like the env parameter:
int main(int argc, char** args, char** env) {
}
After manually removed env and re-run emcc, I got out of memory from wasm3 and this error from wasmtime 0.22:
Error: failed to run main module `a.out.wasm`
Caused by:
0: failed to instantiate "a.out.wasm"
1: unknown import: `env::invoke_vi` has not been defined
wasmer inspect testnim.wasm tells me that we have a bunch of imported functions:
"env"."invoke_vi": [I32, I32] -> []
"env"."emscripten_longjmp": [I32, I32] -> []
"env"."setTempRet0": [I32] -> []
"env"."getTempRet0": [] -> [I32]
"env"."invoke_ii": [I32, I32] -> [I32]
"env"."invoke_v": [I32] -> []
"env"."signal": [I32, I32] -> [I32]
I don' think ng_public need to deal with it because it's running in the browser, not WASI environment. Currently I don't know where invoke_* comes from, but emscripten_longjmp is a nono, because WASM does not support longjmp. Emscripten can workaround this when running in browser, but I'm not aware of any solution when running outside a browser :(
Is it possible to disable features that needs longjmp (like exceptions)?
Or rather, is there some kind of "minimal mode" in nim that calls as few system C functions as possible, at the cost of disabling some optional language features? If there is, I can directly work on that mode, if it's still not WASI-compatible, then it should impossible (or very hard).
Some nim options to experiment with
gc:arc
noMain:on
panics:on
define:release
define:noSignalHandler
define:useMalloc
These took me from:
"env"."exit": [I32] -> []
"env"."getTempRet0": [] -> [I32]
"env"."invoke_ii": [I32, I32] -> [I32]
"env"."emscripten_longjmp": [I32, I32] -> []
"env"."invoke_vii": [I32, I32, I32] -> []
"env"."setTempRet0": [I32] -> []
"env"."invoke_v": [I32] -> []
"env"."invoke_iiii": [I32, I32, I32, I32] -> [I32]
"env"."invoke_vi": [I32, I32] -> []
"env"."invoke_iiiii": [I32, I32, I32, I32, I32] -> [I32]
"env"."__sys_munmap": [I32, I32] -> [I32]
"env"."__sys_mmap2": [I32, I32, I32, I32, I32, I32] -> [I32]
"wasi_snapshot_preview1"."fd_close": [I32] -> [I32]
"wasi_snapshot_preview1"."fd_write": [I32, I32, I32, I32] -> [I32]
"env"."emscripten_resize_heap": [I32] -> [I32]
"env"."emscripten_memcpy_big": [I32, I32, I32] -> [I32]
"wasi_snapshot_preview1"."fd_seek": [I32, I32, I32, I32, I32] -> [I32]
to:
"env"."exit": [I32] -> []
"wasi_snapshot_preview1"."fd_close": [I32] -> [I32]
"wasi_snapshot_preview1"."fd_write": [I32, I32, I32, I32] -> [I32]
"env"."emscripten_resize_heap": [I32] -> [I32]
"env"."emscripten_memcpy_big": [I32, I32, I32] -> [I32]
"env"."setTempRet0": [I32] -> []
"wasi_snapshot_preview1"."fd_seek": [I32, I32, I32, I32, I32] -> [I32]
There are lots of emscripten options to experiment with. "-s MINIMAL_RUNTIME=1" may be relevant to your interests. I believe invoking emscripten-compiled wasm via wasmer or similar can be tricky but I haven't looked into it much. Just know that there is an emscripten runtime component. Here's just some tips; you'll probably get it to work eventually.
Thank you! I finally got to to working. Your nim options are the key of success. My final nim to c command line is:
nim c --compileOnly --cpu:wasm32 --os:linux --gc:arc --panics:on -d:danger -d:emscripten -d:useMalloc -d:noSignalHandler test.nim
Then manually change main() by removing env parameter (using noMain option also needs some manual tweaking, but either is trivial), then compile with emscripten:
emcc mtest.nim.c stdlib_formatfloat.nim.c stdlib_io.nim.c stdlib_system.nim.c -w -Os -o test.wasm -ID:\dev\nim-1.4.2-0102\nim-1.4.2\lib
Then wasmer inspect reports the following imported functions:
Functions:
"env"."__wait": [I32, I32, I32, I32] -> []
"wasi_snapshot_preview1"."fd_close": [I32] -> [I32]
"wasi_snapshot_preview1"."fd_seek": [I32, I64, I32, I32] -> [I32]
"wasi_snapshot_preview1"."fd_write": [I32, I32, I32, I32] -> [I32]
"wasi_snapshot_preview1"."proc_exit": [I32] -> []
"wasi_snapshot_preview1"."args_sizes_get": [I32, I32] -> [I32]
"wasi_snapshot_preview1"."args_get": [I32, I32] -> [I32]
However, wasmtime (I use it on server side) and wasm3 (I use it in mobile apps where JIT is not available) both provide wasi_snapshot_preview1 functions so the only trouble is env.__wait. Luckily, but inspecting the generated glue javascript (with -o test.js -s MINIMAL_RUNTIME=1), I found the env.__wait is actually empty, so I manually added it to wasm3's source code, then it works! I'm able to get messages printed to stdout with wasm3 test.wasm (though wasm3 is my modified version)
Note that this test.nim is not the trivial one I posted above. It uses objects, float to string conversions etc.
I have a step by step Emscripten Nim tutorial: https://github.com/treeform/nim_emscripten_tutorial
Maybe it can help you out?
Even if they get a little stale, that's not entirely a bad thing as little things like that are an easy contribution for people new to Nim. That fix they make is the initial seed of volunteering and becoming an active community member.
Anyhow, maybe Nim by Example as a starting point and it can find a better home if that's not the right place later? Anyone interested in WASM and Nim want to try a hand at a PR?
Thank you! I've solved my problem. It's slightly different from the scenario in your tutorial because I'm not running the wasm in a browser, but in a WASI environment, which has its own challenge. I'm glad it's working now.
BTW: Your jsony is very sweet, I like it :)