Trying out WASM Support

Hey y’all. :wave: … new to Crystal but heard about the merge of WASM/WASI support and wanted to take it for a spin. I compiled from master and attempted a hello world…

fun _start()
  puts "Hello World"
end

Build works fine:

⚡  ~/src/crystal/bin/crystal build hello_world.cr --cross-compile --target wasm32-unknown-wasi
Using compiled compiler at /Users/nickwesselman/src/crystal/.build/crystal
wasm-ld hello_world.wasm -o hello_world  -lc -L/opt/homebrew/lib -lpcre

But using the output wasm-ld command doesn’t work for me on macOS 12.3:

⚡  wasm-ld hello_world.wasm -o hello_world  -lc -L/opt/homebrew/lib -lpcre
wasm-ld: error: unable to find library -lc

I can get it to run successfully by adding wasi-sysroot to the library path:

⚡  wasm-ld hello_world.wasm -o hello_world_final.wasm  -lc -L/Users/nickwesselman/src/hello-world-crystal/wasi-sysroot/lib/wasm32-wasi/ -L/opt/homebrew/lib -lpcre

But running with wasmtime fails:

⚡  wasmtime hello_world_final.wasm                                        
Error: failed to run main module `hello_world_final.wasm`

Caused by:
    0: failed to invoke command default
    1: wasm trap: out of bounds memory access
       wasm backtrace:
           0: 0xeb8d - []
                           at /Users/nickwesselman/src/crystal/src/pointer.cr:117:6

Any ideas? Thanks!

2 Likes

Just if you had not read Crystal in (possible) WASM everywhere future? - #20 by maxfierke

That thread seems a bit dated given this PR was merged. :slight_smile:

@lbguilherme Sorry for the tag but maybe you can provide some guidance here? Amazing work btw!

Crystal supports targeting WASM, but doesn’t include any of the necessary libs for linking yet. You will need libc, librt and libpcre at least.

Grab the libc sysroot from here: https://github.com/WebAssembly/wasi-sdk/releases/download/wasi-sdk-14/wasi-sysroot-14.0.tar.gz
Grab the compiler_rt from here: https://github.com/WebAssembly/wasi-sdk/releases/download/wasi-sdk-14/libclang_rt.builtins-wasm32-wasi-14.0.tar.gz
Grab a libpcre prebuilt from here: https://github.com/lbguilherme/crystal/files/7791111/libpcre-8.45.tar.gz

Take all the .a files and put them together in a single folder, let’s call it “wasm32-wasi”.

You shouldn’t redefine the _start function, instead just write normal Crystal:

puts "Hello World"

Then you can write a crystal program, cross compile it to wasm32-unknown-wasi and link it:

crystal/bin/crystal build hello.cr --cross-compile --target wasm32-unknown-wasi

For linking you have to include wasm32-wasi/crt1.o from the sysroot:

wasm-ld hello.wasm wasm32-wasi/crt1.o -o hello-final.wasm -L wasm32-wasi -lc -lpcre -lclang_rt.builtins-wasm32

And finally run it:

$ wasmtime hello-final.wasm 
Hello World
8 Likes

Don’t worry, this will definitely get simpler in the future.

6 Likes
⚡  wasmtime hello-final.wasm 
Hello World

:raised_hands: :raised_hands: :raised_hands:

Got it working! Thanks!!

2 Likes

FYI you can optimize the binary size significantly by using wasm-opt:

mkdir -p build
~/src/crystal/bin/crystal build src/main.cr --cross-compile --target wasm32-unknown-wasi -o build/unlinked
wasm-ld build/unlinked.wasm wasm32-wasi/crt1.o -o build/linked.wasm -L wasm32-wasi -lc -lpcre -lclang_rt.builtins-wasm32
wasm-opt build/linked.wasm -o build/index.wasm -Oz --strip-dwarf --strip-producers --zero-filled-memory

Output size*:

index.wasm     377K
linked.wasm    958K
unlinked.wasm  953K

(* For my actual app, not hello world :smile:)

3 Likes

Great!

You can try adding --strip-all --compress-relocations to wasm-ld and --converge to wasm-opt, and --release to crystal build to shave a few bytes.

2 Likes

I got this error:

$ git clone https://github.com/lbguilherme/crystal.git && cd crystal && make && cd ..

$ crystal/.build/crystal build hello.cr --cross-compile --target wasm32-unknown-wasi
Error: Unsupported architecture for target triple: wasm32-unknown-wasi

$ crystal/.build/crystal build hello.cr
Showing last frame. Use --error-trace for full trace.

In hello.cr:1:1

 1 | puts "hello world"
     ^
Error: can't find file 'prelude'

If you're trying to require a shard:
- Did you remember to run `shards install`?
- Did you make sure you're running the compiler in the same directory as your shard.yml?

$ crystal/.build/crystal -v
Crystal 1.3.0-dev [ee9996728] (2021-12-22)

LLVM: 12.0.0
Default target: x86_64-pc-linux-gnu

$ cat /etc/os-release |head
NAME="Ubuntu"
VERSION="18.04.6 LTS (Bionic Beaver)"
ID=ubuntu
ID_LIKE=debian
PRETTY_NAME="Ubuntu 18.04.6 LTS"
VERSION_ID="18.04"
HOME_URL="https://www.ubuntu.com/"
SUPPORT_URL="https://help.ubuntu.com/"
BUG_REPORT_URL="https://bugs.launchpad.net/ubuntu/"
PRIVACY_POLICY_URL="https://www.ubuntu.com/legal/terms-and-policies/privacy-policy"


Hi @orangeSi!

Clone from https://github.com/crystal-lang/crystal.git, the feature is already merged upstream.

2 Likes

@orangeSi Try using crystal/bin/crystal and not crystal/.build/crystal.

2 Likes

@lbguilherme @Blacksmoke16 After use https://github.com/crystal-lang/crystal.git and crystal/bin/crystal I got this error now:

$ crystal/bin/crystal build hello.cr --cross-compile --target wasm32-unknown-wasi
$ /usr/lib/llvm-10/bin/wasm-ld  hello.wasm  wasi-sysroot/lib/wasm32-wasi/crt1.o  -o hello_final.wasm -L wasi-sysroot/lib/wasm32-wasi -lc -lpcre -lclang_rt.builtins-wasm32 
wasm-ld: error: hello.wasm: Bad relocation type:

$ crystal/bin/crystal -v
Using compiled compiler at crystal/.build/crystal
Crystal 1.4.0-dev [7e9b09f1c] (2022-03-28)

LLVM: 12.0.0
Default target: x86_64-pc-linux-gnu


$ ls /usr/lib/llvm-12/bin/wasm-ld
ls: cannot access '/usr/lib/llvm-12/bin/wasm-ld': No such file or directory

# had copy libpcre and libclang_rt to wasi-sysroot/lib/wasm32-wasi/ as below
$ ls wasi-sysroot/lib/wasm32-wasi/ -ltr
total 4856
-rw-r--r-- 1 ubuntu ubuntu       8 Nov 19 10:15 libxnet.a
-rw-r--r-- 1 ubuntu ubuntu    4484 Nov 19 10:15 libwasi-emulated-signal.a
-rw-r--r-- 1 ubuntu ubuntu    1752 Nov 19 10:15 libwasi-emulated-process-clocks.a
-rw-r--r-- 1 ubuntu ubuntu     982 Nov 19 10:15 libwasi-emulated-mman.a
-rw-r--r-- 1 ubuntu ubuntu     370 Nov 19 10:15 libwasi-emulated-getpid.a
-rw-r--r-- 1 ubuntu ubuntu       8 Nov 19 10:15 libutil.a
-rw-r--r-- 1 ubuntu ubuntu       8 Nov 19 10:15 librt.a
-rw-r--r-- 1 ubuntu ubuntu       8 Nov 19 10:15 libresolv.a
-rw-r--r-- 1 ubuntu ubuntu       8 Nov 19 10:15 libpthread.a
-rw-r--r-- 1 ubuntu ubuntu       8 Nov 19 10:15 libm.a
-rw-r--r-- 1 ubuntu ubuntu       8 Nov 19 10:15 libdl.a
-rw-r--r-- 1 ubuntu ubuntu       8 Nov 19 10:15 libcrypt.a
-rw-r--r-- 1 ubuntu ubuntu   30938 Nov 19 10:15 libc-printscan-no-floating-point.a
-rw-r--r-- 1 ubuntu ubuntu   37982 Nov 19 10:15 libc-printscan-long-double.a
-rw-r--r-- 1 ubuntu ubuntu    2140 Nov 19 10:15 libc.imports
-rw-r--r-- 1 ubuntu ubuntu  702508 Nov 19 10:15 libc++abi.a
-rw-r--r-- 1 ubuntu ubuntu 1957878 Nov 19 10:15 libc++.a
-rw-r--r-- 1 ubuntu ubuntu  882238 Nov 19 10:15 libc.a
-rw-r--r-- 1 ubuntu ubuntu     338 Nov 19 10:15 crt1-reactor.o
-rw-r--r-- 1 ubuntu ubuntu     456 Nov 19 10:15 crt1.o
-rw-r--r-- 1 ubuntu ubuntu     370 Nov 19 10:15 crt1-command.o
-rw-r--r-- 1 ubuntu ubuntu  864018 Dec 26 22:39 libpcre.a
-rw-r--r-- 1 ubuntu ubuntu    6896 Dec 26 22:39 libpcreposix.a
-rw-r--r-- 1 ubuntu ubuntu  409160 Mar 29 13:09 libclang_rt.builtins-wasm32.a

wasm-ld from LLVM 10 is probably too old and the linking format changed since then. Try to install a more recent version, from LLVM 12 for example. You compiled Crystal with LLVM 12, so it should be available for your system. Alternatively you my try compiling Crystal with LLVM 10, but I’m not sure that will work.

2 Likes

It works now, thanks! I install lld-12 by apt install then get file /usr/lib/llvm-12/bin/wasm-ld then works as below command.

I wrote some simple toolkits with crystal, now I try to compile them to wasm, it also works in ```wasmer``!

2 Likes

can export function to use in wasmer now? I get error as below:

$ cat simple.cr 
def sum(a : Int32, b : Int32)
  return a + b
end

$ cat simple_cr_to_wasm.sh 
set -ve
prefix=simple
base=~/webassembly_crystal/
$base/crystal/bin/crystal build $prefix.cr --cross-compile --target wasm32-unknown-wasi --release
/usr/lib/llvm-12/bin/wasm-ld  $prefix.wasm  $base/wasi-sysroot/lib/wasm32-wasi/crt1.o  -o ${prefix}_final.wasm -L $base/wasi-sysroot/lib/wasm32-wasi -lc -lpcre -lclang_rt.builtins-wasm32  
echo done

$ sh simple_cr_to_wasm.sh 
prefix=simple
base=~/webassembly_crystal/
$base/crystal/bin/crystal build $prefix.cr --cross-compile --target wasm32-unknown-wasi --release
Using compiled compiler at /home/ubuntu/webassembly_crystal/crystal/.build/crystal
wasm-ld simple.wasm -o simple  -lc -L/usr/local/bin/../lib/crystal -lpcre
/usr/lib/llvm-12/bin/wasm-ld  $prefix.wasm  $base/wasi-sysroot/lib/wasm32-wasi/crt1.o  -o ${prefix}_final.wasm -L $base/wasi-sysroot/lib/wasm32-wasi -lc -lpcre -lclang_rt.builtins-wasm32  
echo done
done

$ wasmer simple_final.wasm -i sum 1 2
error: failed to run `simple_final.wasm`
╰─▶ 1: Error while importing "wasi_snapshot_preview1"."args_get": unknown import. Expected Function(FunctionType { params: [I32, I32], results: [I32] })

and in wasmer-crystal(GitHub - naqvis/wasmer-crystal: WebAssembly runtime for Crystal):

$ cat use_simple_in_wasmer.cr
require "../src/wasmer"

class AssertionError < RuntimeError
end

def assert
  raise AssertionError.new unless yield
end

# Let's define the engine, that holds the compiler.
engine = Wasmer::Engine.new

# Let's define the store, that holds the engine, that holds the compiler.
store = Wasmer::Store.new(engine)

# Above two lines are same as invoking below helper method
# store = Wasmer.default_engine.new_store

# Let's compile the module to be able to execute it!
module_ = Wasmer::Module.new(store, File.read("#{__DIR__}/simple_final.wasm"))

# Now the module is compiled, we can instantiate it.
instance = Wasmer::Instance.new(module_)

# get the exported `sum` function
# function methods returns nil if it can't find the requested function. we know its there, so let's add `not_nil!`
sum = instance.function("sum").not_nil!

# Call the exported `sum` function with Crystal standard values. The WebAssembly
# types are inferred and values are casted automatically.
result = sum.call(5, 37)

puts result # => 42

$ crystal build use_simple_in_wasmer.cr

$ ./use_simple_in_wasmer
Unhandled exception: Missing hash key: "wasi_snapshot_preview1" (KeyError)
  from /usr/local/share/crystal/src/hash.cr:1031:11 in '[]'
  from /home/ubuntu/wasmer-crystal/src/wasmer/import.cr:25:86 in 'into_inner'
  from /home/ubuntu/wasmer-crystal/src/wasmer/instance.cr:23:17 in 'initialize'
  from /home/ubuntu/wasmer-crystal/src/wasmer/instance.cr:22:5 in 'new'
  from /home/ubuntu/wasmer-crystal/src/wasmer/instance.cr:32:7 in 'new'
  from use_simple.cr:16:1 in '__crystal_main'
  from /usr/local/share/crystal/src/crystal/main.cr:115:5 in 'main_user_code'
  from /usr/local/share/crystal/src/crystal/main.cr:101:7 in 'main'
  from /usr/local/share/crystal/src/crystal/main.cr:127:3 in 'main'
  from /lib/x86_64-linux-gnu/libc.so.6 in '__libc_start_main'
  from ./use_simple in '_start'
  from ???

See Importing and Exporting in WebAssembly · Issue #11941 · crystal-lang/crystal · GitHub and Add 'wasm_import_module' option to the @[Link] annotation by lbguilherme · Pull Request #11935 · crystal-lang/crystal · GitHub.

Until that PR is accepted you can do the following:

# Exported functions need to use `fun`. They must be fully typed. 
fun sum(a : Int32, b : Int32) : Int32
  return a + b
end

And then add --export sum to wasm-ld.

Also, the module depends on WASI and requires you to provide it as imports. There is no wasm32-unknown-unknown yet.

1 Like

@lbguilherme , It’s seem like not work any more with 1.5.0 on linux?

 ╰─ $ crystal build main.cr --cross-compile --target wasm32-unknown-wasi
wasm-ld main.wasm -o main  -lc -L/home/zw963/Crystal/bin/../lib/crystal -lpcre

 ╰─ $ wasm-ld main.wasm -o main  -lc -L"$PWD/wasm32-wasi-libs" -lpcre
wasm-ld: error: main.wasm: undefined symbol: __original_main

 ╰─ $ \cat main.cr
puts "Hello World"

 ╰─ $ cr version
Crystal 1.5.0 [994c70b10] (2022-07-06)

LLVM: 14.0.6
Default target: x86_64-pc-linux-gnu

 ╰─ $ 1  pacman -Q |grep 'llvm\|lld'
lib32-llvm-libs 14.0.6-2
lld 14.0.6-1
llvm 14.0.6-3
llvm-libs 14.0.6-3

@zw963, Hi there!

Are you using version 0.0.2 from here?

If that’s the case, please try with 0.0.1, it should work.

Crystal isn’t yet ready for wasi-sdk 16. I’ll open a PR for that.

4 Likes