Skip to content
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

Hello WebAssembly! (MVP implementation) #10870

Merged
merged 48 commits into from
Mar 21, 2022

Conversation

lbguilherme
Copy link
Contributor

@lbguilherme lbguilherme commented Jul 1, 2021

See #829.

This PR adds minimal support for compiling a Crystal program into WebAssembly and then linking it with a WASI-based LibC. Browsers are still not supported.

What does not work:

  • GC
  • Fiber and spawn
  • Mutex
  • Thread
  • Signal
  • Exception
  • CallStack
  • Dwarf
  • Event Loop (needs building LibEvent. It should work fine, but I didn't try)

How to use:

  1. Write a simple Crystal program, let's say: puts "Hello WebAssembly!" and put it to main.cr.
  2. Build a compiler from this branch (simply run make).
  3. Build the WebAssembly module: bin/crystal build main.cr --cross-compile --target wasm32-unknown-wasi. This will generate a main.o file that is actually a binary wasm file.
  4. For this to work you will need a LibC based on WASI. You can download a precompiled one from https://github.com/WebAssembly/wasi-sdk/releases/download/wasi-sdk-14/wasi-sysroot-14.0.tar.gz.
  5. Link your program and this LibC into a final WebAssembly module: wasm-ld main.o -o main.wasm wasi-sysroot-14.0/wasi-sysroot/lib/wasm32-wasi/libc.a wasi-sysroot-14.0/wasi-sysroot/lib/wasm32-wasi/crt1.o.
  6. Now you have a main.wasm file. It is already a final module that can run on any WASI-capable runtime, like Wasmer.
$ wasmer main.wasm 
Hello WebAssembly!

This PR is the bare minimum necessary to get it working. Unfortunately, I had to add flag checks on more places than I would like, but these can go away once other features get ported.

@lbguilherme
Copy link
Contributor Author

Would it be better to implement more WebAssembly-related features here before this gets merged or to focus on merging this minimally as is (hardly useful by itself) and bring improvements later? The included smoke test should at least avoid regressions.

@maxfierke
Copy link
Contributor

IMO I Would strongly caution against trying to get more features in and maintaining a long-running branch for a new platform (it'll be harder to keep momentum if you have to play catch-up with upstream)

Each of the pending things is easily enough extra time and work for their own PR (and likely multiples)

@straight-shoota
Copy link
Member

The scope of this is perfect, thank you. It just seems that nobody has gotten around reviewing it, due to the release cycle, conference and other more urgent tasks.

@Fryguy
Copy link
Contributor

Fryguy commented Jul 21, 2021

Would it be better to implement more WebAssembly-related features here before this gets merged or to focus on merging this minimally as is (hardly useful by itself) and bring improvements later? The included smoke test should at least avoid regressions.

My only concern is there are a lot of stub methods, which is fine, but there is no indication that they need to be completed later, which might get confusing as this is iteratively worked on. I'm wondering if those stub methods should at least have a # TODO: comment or something, if possible with a description of what is required to complete them.

@straight-shoota
Copy link
Member

Yes, a couple of TODO or NotImplementedError would be nice. Ideally, any stub method should have at least one of those.

Co-authored-by: Sijawusz Pur Rahnama <sija@sija.pl>
Copy link
Contributor

@maxfierke maxfierke left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Overall looks really good! Very organized! I found a couple of small things, most of which can probably be deferred. All pretty minor concerns, save for maybe the ABI stuff.

src/lib_c/wasm32-wasi Outdated Show resolved Hide resolved
src/crystal/system/random.cr Outdated Show resolved Hide resolved
Comment on lines 27 to 30
{% unless flag?(:wasm32) %}
LibPCRE.pcre_malloc = ->GC.malloc(LibC::SizeT)
LibPCRE.pcre_free = ->GC.free(Void*)
{% end %}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

with gc/none, these will just point to regular C malloc/free, so you can probably leave this untouched for wasm32

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I left it here because I didn't build libpcre and didn't test it. I can remove this check before merging.

src/compiler/crystal/codegen/target.cr Outdated Show resolved Hide resolved
src/llvm/abi/wasm32.cr Outdated Show resolved Hide resolved
src/signal.cr Outdated Show resolved Hide resolved
@tjpalmer
Copy link

Now I think I'm just having fun with it. This produces just 22kb of gzipped wasm and runs on any modern browser just fine:

Do you have the build instructions for this? And how big uncompressed?

@lbguilherme
Copy link
Contributor Author

lbguilherme commented Dec 30, 2021

22kb gzipped / 54kb before compression.

Here is my code and instructions on running it: https://github.com/lbguilherme/crystal-web

@lbguilherme
Copy link
Contributor Author

Current status:

  • This PR is ready for review, I don't plan to add anything more here.
  • Fibers: stack switching can be implemented on top of Binaryen Asyncify. It allows unwinding and rewinding stacks. The main function must be wrapped in a loop that will first begin normal execution. The swapcontext will then set a global variable with the target Fiber and begin unwinding until it reaches the main function. It will check this global for the target Fiber and start rewinding into it. The loop repeats until no Fiber wants to execute. Thus the Scheduler must be started from main, wrapping everything inside it. LLVM produces a shadow stack on memory for the local variables that we grab a pointerof. The address of this stack has to be switched too, but this is much simpler, just swapping a pointer at the right time.
  • Garbage Collection: LibGC (bdwgc) can be compiled into WebAssembly, but it requires inspecting the stack to work properly. In WebAssembly the normal stack can't be inspected. But we can take advantage of Asyncify again: If we unwind the current Fiber then, at this point, every variable is stored in memory. This is the only time the GC can run. We must then rewind into the same Fiber again.
  • Exceptions: There is an exceptions extension to WebAssembly that is already implemented by Chrome and is in the process of being implemented by Firefox. LLVM already supports emitting it behind a flag. So I guess we just wait for it to be ready and use it. No need to be too clever here.
  • WASI: There are currently two usual targets for WebAssembly: WASI is a set of standard functions (think of it as syscalls or a special libc) that most embedders already implement natively, and there is Emscripten that emits a companion JavaScript file that will work on browsers too, following a very specific format of this tool. There are currently more libs that work on Emscripten than that work on WASI, but I opted for using a standard format that produces a single WebAssembly file and has no relation with JavaScript. It limits the things we might do, but I think it's ok. Shards can provide the missing functionality by emitting JavaScript (see lbguilherme/crystal-web for example)
  • Threads: WebAssembly has atomics and can be run in multiple threads. But WASI has no way of creating new threads, so we can't do much. I guess this can remain NotImplemented until either WASI supports threads or we decide to emit a JavaScript support file for it. Either option should take a while given multi-threading is still experimental in native Crystal.
  • EventLoop: WASI supports async IO and wasi-libc implements poll on top of it, so I think compiling libevent is viable. If it isn't, we can always implement the event loop on top of WASI directly.
  • Signals and Process: Those don't make sense in WebAssembly since there is no concept of process. So it will remain not implemented.
  • CallStack: We might be able to raise and rescue from exceptions, but we are still unable to obtain a stack trace from it as the stack can't be inspected. There are two options as far as I can tell: unwinding the current Fiber with Binaryen Asyncify and then analyzing the generated format together with debug info stored in some format (highly complex!) or calling into a JavaScript runtime where producing a stack trace is trivial, but this again means the output won't be a pure wasm file. The debugability of WebAssembly itself right now is far from ideal.
  • Web: The WASI target can be used for the Web by mocking some WASI functions. I'm not sure if this should be always provided by shards or if the compiler should support a second WebAssembly target (--target wasm32-unknown-web?) where it will always emit a js file (and thus enable some of the features we just discussed). Anyway this can be decided later.
  • Libraries: Building a final wasm file requires libc, libpcre, libgc, libevent, etc. All of those should be built for wasm-wasi and grabbing those isn't trivial. Ideally, the compiler should ship with prebuilt versions of these so it can target WebAssembly without any extra work from the user. Is there a precedent for this on other platforms? Also wasm-ld from LLVM and wasm-opt from Binaryen would be required to produce a working final binary. Could those be shipped together as well? Or the user will be instructed to install them?

@@ -43,7 +43,7 @@ module SystemError
end

# The original system error wrapped by this exception
getter os_error : Errno | WinError | Nil
getter os_error : Errno | WinError | WasiError | Nil
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💭 Maybe at this point we should create an alias for this union (optional in this PR!)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't like much the fact that we have this union, since a program will only ever ever have one of these kinds of errors. It doesn't make sense to have all of those defined, they should be guarded by flags or moved to the Crystal::System, I don't know.

Copy link
Member

@straight-shoota straight-shoota left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I believe these should also be wasi?

There are more flag?(:wasm32) left. They should probably all be wasi instead, except for those directly related to codegen.

spec/compiler/ffi/ffi_spec.cr Show resolved Hide resolved
spec/compiler/loader/unix_spec.cr Show resolved Hide resolved
lbguilherme and others added 5 commits February 8, 2022 16:10
Co-authored-by: Johannes Müller <straightshoota@gmail.com>
Co-authored-by: Johannes Müller <straightshoota@gmail.com>
@lbguilherme
Copy link
Contributor Author

Actually, those were correct.

We can think about three wasm targets:

  • wasm32-unknown-wasi: Currently implemented. Uses the WASI layer together with the libc from wasi-sdk.
  • wasm32-unknown-unknown: Imports nothing and should restrain itself to only pure wasm features. This is useful for embedding wasm somewhere. It has no IO of any kind, but it is still possible to include LibGC. Does not work currently.
  • wasm32-unknown-js: For running wasm inside a JavaScript environment, I'm not sure if we will have this functionality short-term.

The first two targets are quite important to have. I'm using flag?(:wasi) for things that involve only the first target and flag?(:wasm32) for the guards about both targets.

Wasm doesn't have a way of implementing FFI with or without wasi. It could be done with js, but I'm leaving this to the future.

Copy link
Member

@straight-shoota straight-shoota left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🚀 🦾

Copy link
Member

@asterite asterite left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is amazing, thank you!

Copy link
Member

@bcardiff bcardiff left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💯 After this get merged maybe we should move more stuff to Crystal::System regarding sockets, right? It seems that there where many tweaks of flags on networking that might be good to encapsulate so a next platform could start with all the NotImplemented as this PR.

We meet again my dear 32 bits!

@straight-shoota straight-shoota added this to the 1.4.0 milestone Mar 20, 2022
@straight-shoota straight-shoota merged commit 67c32ec into crystal-lang:master Mar 21, 2022
beta-ziliani pushed a commit to crystal-lang/crystal-book that referenced this pull request May 27, 2022
Basic WebAssembly support has been added in crystal-lang/crystal#10870 and was released in Crystal 1.4

The second commit applies uniform format for the libc description.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

10 participants