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

[WIP] Add ASM.JS/Emscripten Target #3634

Closed
wants to merge 21 commits into from
Closed

Conversation

keplersj
Copy link
Contributor

@keplersj keplersj commented Dec 4, 2016

Much like #3582 this is very initial.

Fixes #535

Using a few tricks basic Crystal programs (like single lines containing numbers basic) are being compiled to JavaScript.
* master: (69 commits)
  Fixed: splat restriction and double splat restriction didn't match empty tuple/named-tuple
  Fixed crystal-lang#1203: allow using free var to match the size of a static array
  Updated Changelog
  Compiler: don't remove old directories when compiling macro run programs. Related to crystal-lang#2625
  Fixed crystal-lang#2624: Running multiple macro runs doesn't work
  Docker: Preinstall git in released docker image
  Update and fix TypeNode macro method docs
  Fixed wrong datatype in 0.17 branch
  Docs: use `*` and `**` before type argument for Tuple and NamedTuple
  Docs: use `*` and `**` before type argument for Tuple and NamedTuple
  Compiler: allow a double splat restriction on a double splat argument
  Compiler: allow a splat restriction on a splat argument
  Compiler: allow restriction in double splat
  Compiler: guess type from captured block without restriction
  Fixed crystal-lang#731: initialize's default arguments are incorrectly evaluated at the class scope
  Fixed crystal-lang#2619: wrong codegen for global variable assignment in type declaration
  Compiler: allow a double splat restriction on a double splat argument
  Compiler: allow a splat restriction on a splat argument
  Compiler: allow restriction in double splat
  Compiler: guess type from captured block without restriction
  ...
Cuts down on a dependency, and I believe the JS engine will do GC.
Inspiration taken heavily from Rust.
This can be used with emscripten by setting LLVM_CONFIG to emscripten’s embedded llvm-config
I tried generating libc using posix but ran into issues
@keplersj
Copy link
Contributor Author

keplersj commented Dec 4, 2016

I'm not sure how to handle garbage collection at this point. Everything seems to compile, although it is a bit convoluted and needs some work.

First you must compile the compiler with LLVM_CONFIG set to Emscripten's embedded llvm-config. On my machine (macOS 10.12.1, Emscripten installed through Homebrew) the command run is LLVM_CONFIG=/usr/local/opt/emscripten/libexec/llvm/bin/llvm-config make producing:

Using /usr/local/opt/emscripten/libexec/llvm/bin/llvm-config [version=3.9.0]
c++ -c  -o src/llvm/ext/llvm_ext.o src/llvm/ext/llvm_ext.cc `/usr/local/opt/emscripten/libexec/llvm/bin/llvm-config --cxxflags`
cc -fPIC    -c -o src/ext/sigfault.o src/ext/sigfault.c
ar -rcs src/ext/libcrystal.a src/ext/sigfault.o
CRYSTAL_CONFIG_PATH=`pwd`/src ./bin/crystal build  -o .build/crystal src/compiler/crystal.cr -D without_openssl -D without_zlib

From here you can begin compiling Crystal files to JavaScript, by setting CC to emcc and passing a few parameters to the Crystal compiler.

With a basic number.cr file:

def number
  1
end

number

Running CC=emcc ./bin/crystal build --target asmjs-unknown-emscripten --verbose --single-module --emit llvm-ir --link-flags -v number.cr produces:

Using compiled compiler at .build/crystal
emcc -o "/Users/kepler/Developer/crystal-lang/crystal/number" "${@}" -v -rdynamic  -lpcre -lgc -lpthread /Users/kepler/Developer/crystal-lang/crystal/src/ext/libcrystal.a -levent -L/usr/lib -L/usr/local/lib _main.o
INFO:root:(Emscripten: Running sanity checks)
WARNING:root:_main.o is not valid LLVM bitcode
WARNING:root:ignoring dynamic library libpthread.dylib because not compiling to JS or HTML, remember to link it when compiling to JS or HTML at the end
llvm-link: Not enough positional command line arguments specified!
Must specify at least 1 positional arguments: See: /usr/local/opt/emscripten/libexec/llvm/bin/llvm-link -help
Traceback (most recent call last):
  File "/usr/local/bin/emcc", line 13, in <module>
    emcc.run()
  File "/usr/local/Cellar/emscripten/1.36.14/libexec/emcc.py", line 1410, in run
    shared.Building.link(linker_inputs, specified_target)
  File "/usr/local/Cellar/emscripten/1.36.14/libexec/tools/shared.py", line 1580, in link
    assert os.path.exists(target) and (output is None or 'Could not open input file' not in output), 'Linking error: ' + output
AssertionError: Linking error: 
Error: execution of command failed with code: 1: `emcc -o "/Users/kepler/Developer/crystal-lang/crystal/number" "${@}" -v -rdynamic  -lpcre -lgc -lpthread /Users/kepler/Developer/crystal-lang/crystal/src/ext/libcrystal.a -levent -L/usr/lib -L/usr/local/lib`

From here we have to compile the emitted LLVM IR code to emcc by running emcc number.ll -o number.js which produces:

warning: unexpected number of arguments 7 in call to '__mmap', should be 6
warning: unresolved symbol: GC_init
warning: unresolved symbol: GC_push_all_eager
warning: unresolved symbol: event_base_loop
warning: unresolved symbol: _Unwind_GetRegionStart
warning: unresolved symbol: GC_pthread_detach
warning: unresolved symbol: i64Subtract_1
warning: unresolved symbol: GC_register_finalizer_ignore_self
warning: unresolved symbol: GC_free
warning: unresolved symbol: GC_set_handle_fork
warning: unresolved symbol: event_free
warning: unresolved symbol: event_add
warning: unresolved symbol: GC_get_push_other_roots
warning: unresolved symbol: _Unwind_SetIP
warning: unresolved symbol: GC_realloc
warning: unresolved symbol: __uremdi3_5
warning: unresolved symbol: event_base_new
warning: unresolved symbol: GC_malloc
warning: unresolved symbol: __udivdi3_4
warning: unresolved symbol: _Unwind_GetLanguageSpecificData
warning: unresolved symbol: event_new
warning: unresolved symbol: GC_malloc_atomic
warning: unresolved symbol: bitshift64Lshr_6
warning: unresolved symbol: bitshift64Shl_7
warning: unresolved symbol: _Unwind_SetGR
warning: unresolved symbol: GC_set_push_other_roots
warning: unresolved symbol: setup_sigfault_handler
warning: unresolved symbol: _Unwind_GetIP
warning: unresolved symbol: GC_stackbottom
warning: unresolved symbol: pcre_free
warning: unresolved symbol: pcre_malloc

This produces a 37,000+ line JavaScript which runs in node and errors out:

node number.js:

missing function: GC_set_handle_fork
-1
-1

/Users/kepler/Developer/crystal-lang/crystal/number.js:110
      throw ex;
      ^
abort(-1) at Error
    at jsStackTrace (/Users/kepler/Developer/crystal-lang/crystal/number.js:1092:13)
    at stackTrace (/Users/kepler/Developer/crystal-lang/crystal/number.js:1109:12)
    at abort (/Users/kepler/Developer/crystal-lang/crystal/number.js:36975:44)
    at _GC_set_handle_fork (/Users/kepler/Developer/crystal-lang/crystal/number.js:4773:63)
    at __GC__init_Nil (/Users/kepler/Developer/crystal-lang/crystal/number.js:27659:2)
    at _main (/Users/kepler/Developer/crystal-lang/crystal/number.js:26910:2)
    at Object.asm._main (/Users/kepler/Developer/crystal-lang/crystal/number.js:36680:19)
    at Object.callMain (/Users/kepler/Developer/crystal-lang/crystal/number.js:36850:30)
    at doRun (/Users/kepler/Developer/crystal-lang/crystal/number.js:36910:60)
    at run (/Users/kepler/Developer/crystal-lang/crystal/number.js:36924:5)

From here anyone should be able to help out and hopefully get this merged into master. I don't have a lot of time to dedicate to open source projects at the moment so it would be really appreciated if someone(s) interested in this picked up it from here.

@keplersj
Copy link
Contributor Author

keplersj commented Dec 4, 2016

/cc @catmando

@keplersj
Copy link
Contributor Author

keplersj commented Dec 4, 2016

@kripken any advice on how Crystal should integrate Emscripten?

@kripken
Copy link

kripken commented Dec 4, 2016

I'd love to help with advice if I can. I'm not that familiar with how Crystal is architected, though. Anything specific I can answer?

In general, it looks like you're generating LLVM IR using one LLVM (Crystal's? Or plain LLVM?), and then loading the IR in text form in emscripten's LLVM? That can sometimes work. But the triple difference can cause problems. You should use the wasm triple for this, as it is the closest to the asm.js triple (and we are working to make them as close as possible), instead of your native system's triple.

@catmando
Copy link

catmando commented Dec 4, 2016

Why does the compiler need to be compiled by emscriptem? I'm figuring you are cross compiling using emscriptem as the back end only

@keplersj
Copy link
Contributor Author

keplersj commented Dec 4, 2016

@catmando The compiler isn't being compiled by emscripten. It's being told the location of emscripten's llvm-config utility program which Crystal uses to determine the configuration of the LLVM environment it's using. Crystal uses llvm-config to determine what backends LLVM has available to it.

@keplersj
Copy link
Contributor Author

keplersj commented Dec 4, 2016

@kripken Crystal uses a plain LLVM installed on the system to generate LLVM IR which it then passes to Clang to produce a binary. This PR allows Crystal to use the LLVM in Emscripten to generate LLVM IR and then pass it to emcc to produce JavaScript. With this method can Crystal use the asmjs-unknown-emscripten triple to target JavaScript.

Currently it appears that a Crystal program and the Crystal standard library can be compiled to JavaScript, but the programs are missing their runtime dependencies. Most Crystal programs are dependent on libgc, libpcre, libevent, and libpcl. I'm not quite sure how we should account for these libraries when using Emscripten. Usually the user installs them on their system an Crystal links against the libraries, but that doesn't seem to an option with Emscripten.

@kripken
Copy link

kripken commented Dec 4, 2016

@keplersj: oh ok, then if you are using the same triple everywhere that should be fine.

Regarding libraries, yes, they need to be ported to emscripten to be usable - a JS program can't link with local system libraries that contain native code. Generally you would port them to emscripten, generating a bitcode file for each, then link that with the program's bitcode, then compile all of that into JS.

One possible issue with libgc in that list is that it needs to scan the stack for roots. That's not possible in JS. One solution is to take advantage of the fact that JS code needs to run in short-lived events anyhow, due to how the web works, and so you unwind the stack completely from time to time, and it is safe to GC then. (The only downside to that is if in a single such frame of execution you need to GC in the middle in order to not run out of memory.) In wasm this might improve in the future.

@RX14
Copy link
Contributor

RX14 commented Dec 4, 2016

The best place to start here is using a js-specific prelude. Start with --prelude=empty to make sure that you can get something that runs first, then worry about porting the stdlib.

@asterite
Copy link
Member

asterite commented Dec 4, 2016

I'm a total newbie regarding emscripten and asm.js, so with this question I just want to learn/understand.

What would be a use case for compiling Crystal to JS?

@RX14
Copy link
Contributor

RX14 commented Dec 4, 2016

@asterite sharing code between a crystal backend and frontend would be the obvious answer.

@pannous
Copy link

pannous commented Dec 9, 2016

@asterite You could run crystal code natively in the browser, (almost) without JS

DEMO

@keplersj
Copy link
Contributor Author

keplersj commented Dec 10, 2016

@asterite JavaScript runs in more places than Crystal does at the moment. Compiling to JS would allow Crystal code to be run on Windows, iOS, Android, Pebble, really any platform with a JavaScript Engine.

@drhuffman12
Copy link

Maybe that could lead to:

  • an Opal-like version of Crystal
  • Crystal Play could be server-less.

@samueleaton
Copy link
Contributor

@drhuffman12 although it would be cool to write crystal on the client, the playground could never be serverless because there are some things that crystal can do that can not be replicated in a browser (e.g. spawning a process)

@pannous
Copy link

pannous commented Dec 10, 2016

One should distinguish between these different goals:

  1. transpiling crystal code to JS (meh)
  2. compiling crystal code to ASM.JS (deprecated!)
  3. compiling crystal code 'directly' to native Webassembly (doable+nice!)
  4. compiling the crystal compiler to Webassembly(wasm) (hard but super awesome)

3 could make crystal a nice new standard language to do fast stuff in the browser that JS can't do
4 would allow super awesome <script type="crystal">puts "hi"</script> tags in html!!

I think there is precedence for custom script tags with embedded mruby/python.
They really 'just' compiled the whole engine to wasm (not JS)!

IMHO we should drop JS from the discussion, just look at the wasm DEMO and be amazed.

Exciting times.

@ysbaddaden
Copy link
Contributor

Despite being fun to achieve, I'm afraid it has the same problem than trying to target AVR (Arduino) or an ARM cortex M0: it's easy to target them with an empty prelude, but if you can't have BOEHM-GC running (at least) then you can't use anything from the corelib and stdlib. I'm not even talking of POSIX requirements, but you can barely use pointers or strings.

Note that compiling to JS (emscripten, wasm, whatever) may only make sense when targeting the browser. Compiling Crystal to JS to run on Node to run on Android... Nonsense. Let's target the platforms natively instead :-)

@refi64
Copy link
Contributor

refi64 commented Dec 10, 2016

I think you can actually use the Boehm GC under emscripten:

https://groups.google.com/forum/m/#!topic/emscripten-discuss/SGwr56gXCZo

@kripken
Copy link

kripken commented Dec 11, 2016

@kirbyfan64: yes, Boehm works, the only limitation is stack scanning as mentioned in this comment. The workaround mentioned there is good enough for it to be used in production, but you do need to be aware of it.

@kbrock
Copy link

kbrock commented May 1, 2017

I would think a PR like this would take some time to merge.

Would it make sense to pull changes that are not asm.js specific (e.g. compiler.cr and type.cr) into a separate PR and get those merged quickly?

@faustinoaq
Copy link
Contributor

Q: Don't you need features like garbage collection and threading added to WebAssembly to make this work?

No, WebAssembly in its current state is sufficient. The .NET runtime handles its own garbage collection and threading concerns.

^ Interesting 😅

^ https://github.com/aspnet/Blazor/wiki/FAQ#q-dont-you-need-features-like-garbage-collection-and-threading-added-to-webassembly-to-make-this-work

@asterite
Copy link
Member

@keplersj Thank you for this! But since this is WIP and not finished, there really isn't much we can do here. Instead of leaving this PR open, we'd like to close it to avoid clutter.

Please reopen or send another PR if it's feature-complete. Thank you!

@asterite asterite closed this Apr 27, 2018
maxfierke added a commit to maxfierke/crystal that referenced this pull request Apr 6, 2020
Both emscripten and wasi, for now. Though will likely only keep one.

Based on crystal-lang#3634
maxfierke added a commit to maxfierke/crystal that referenced this pull request Apr 6, 2020
Both emscripten and wasi, for now. Though will likely only keep one.

Based on crystal-lang#3634
maxfierke added a commit to maxfierke/crystal that referenced this pull request Apr 23, 2020
Both emscripten and wasi, for now. Though will likely only keep one.

Based on crystal-lang#3634
maxfierke added a commit to maxfierke/crystal that referenced this pull request Sep 27, 2020
Both emscripten and wasi, for now. Though will likely only keep one.

Based on crystal-lang#3634
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.