Skip to content

Instantly share code, notes, and snippets.

@LukasKalbertodt
Last active September 22, 2020 12:18
Show Gist options
  • Save LukasKalbertodt/821ab8b85a25f4c54544cc43bed2c39f to your computer and use it in GitHub Desktop.
Save LukasKalbertodt/821ab8b85a25f4c54544cc43bed2c39f to your computer and use it in GitHub Desktop.
Several Rust & WebAssembly notes

Compiling Rust to Wasm manually with the LLVM wasm-backend (without Emscripten)

EDIT November 2017: recently the target wasm32-unknown-unknown was added to rustc which uses the LLVM WASM backend and works without Emscripten. This is now the recommended way of generating WASM code from Rust (as it is much easier). Thus, this gist document is pretty much useless now. A great resource on getting started with WASM and Rust is hellorust.com: Setup and Minimal Example.





Preparation

  1. Download the latest llvm version, preferably directly from their svn repo or this git mirror. Compile LLVM with enabled wasm-backend (this will take around one hour):
mkdir build
cd build
cmake -DCMAKE_BUILD_TYPE=Release -DLLVM_EXPERIMENTAL_TARGETS_TO_BUILD=WebAssembly ..
make -j8
  1. Download and compile Binaryen. Just clone the git repo, cd into it and follow the build instructions.

  2. Install a recent version of rustc (stable channel is fine, though!).

  3. Make sure all of the above software is installed and correctly in your $PATH. We need several executables usually hidden in the bin/ folder.

Compile

  1. Write your awesome Rust library (add.rs):
#[no_mangle]
pub fn add_twenty_seven(n: i32) -> i32 {
    n + 27
}

We add the #[no_mangle] attribute to preserve the exact function name instead of mangling it (this is useful for usage from other Rust code, therefore it's on by default).

  1. Compile the library into llvm bitcode (resulting in add.bc):
rustc --crate-type=lib --emit=llvm-bc add.rs
  1. Convert llvm-bc to wasm assembly in a linear assembly format (resulting in add.s):
llc -march=wasm32 add.bc
  1. Convert the .s assembly into an s-expression assembly format (resulting in add.wast):
s2wasm -o add.wast add.s

This should result in something like this:

(module
 (table 0 anyfunc)
 (memory $0 1)
 (export "memory" (memory $0))
 (export "add_twenty_seven" (func $add_twenty_seven))
 (func $add_twenty_seven (param $0 i32) (result i32)
  (i32.add
   (get_local $0)
   (i32.const 27)
  )
 )
)
  1. Finally, assembly the wast file into the binary add.wasm file:
wasm-as add.wast

Execute it

You can embed it into a website like so:

index.html:

<html>
<head>
<script src="main.js"></script>
</head>
<body>Hi</body>
</html>

main.js:

function loadWasm() {
    fetch('add.wasm').then(response => 
        response.arrayBuffer()
    ).then(buffer =>
        WebAssembly.instantiate(buffer)
    ).then(({module, instance}) => {
        console.log(instance.exports.add_twenty_seven);
        console.log(instance.exports.add_twenty_seven(4));
    });
}

loadWasm()

Next, start a local webserver. This is necessary, file server does not work!

python3 -m http.server 8000

Visit localhost:8000 in your browser, open the JS console and you should see:

function 0() { [native code] }
31

Using functions from JavaScript in Rust

print.rs:

extern {
    fn print(n: i32);
}

#[no_mangle]
pub fn print_twenty_seven_more(n: i32) {
    unsafe {
        print(n + 27);
    }
}

main.js:

function loadWasm() {
    var importObj = {env: {
        print: console.log,
    }};

    fetch('print.wasm').then(response => 
        response.arrayBuffer()
    ).then(buffer =>
        WebAssembly.instantiate(buffer, importObj)
    ).then(({module, instance}) => {
        console.log(instance.exports.print_twenty_seven_more(2));
    });
}

loadWasm()

Compile the Rust file exactly as explained above. Also use the index.html from above. The JS console should show 29.

#!/bin/bash
# Super simple script to compile Rust to wasm. Usage:
# ./compile.sh foo.rs
if [ -z ${1+x} ]; then
echo "missing argument: rust source file"
exit 1
fi
stem="${1%.*}"
rustc -O --crate-type=lib --emit=llvm-bc "${stem}.rs" && \
llc -march=wasm32 "${stem}.bc" && \
s2wasm -o "${stem}.wast" "${stem}.s" && \
wasm-as "${stem}.wast"
@OpenGG
Copy link

OpenGG commented Nov 17, 2017

I follow these instructions and get the following rust.wast content:

(module
 (type $FUNCSIG$vi (func (param i32)))
 (import "env" "_ZN4core9panicking5panic17hc8058a23bb7ed97dE" (func $_ZN4core9panicking5panic17hc8058a23bb7ed97dE (param i32)))
 (table 0 anyfunc)
 (memory $0 1)
 (data (i32.const 16) "add.rs")
 (data (i32.const 32) "attempt to add with overflow")
 (data (i32.const 64) " \00\00\00\00\00\00\00\1c\00\00\00\00\00\00\00\10\00\00\00\00\00\00\00\06\00\00\00\00\00\00\00\05\00\00\00\05\00\00\00")
 (export "memory" (memory $0))
 (export "add_twenty_seven" (func $add_twenty_seven))
 (func $add_twenty_seven (; 1 ;) (param $0 i32) (result i32)
  (local $1 i32)
  (block $label$0
   (br_if $label$0
    (i32.and
     (tee_local $1
      (i32.gt_s
       (get_local $0)
       (i32.const -1)
      )
     )
     (i32.ne
      (get_local $1)
      (i32.gt_s
       (tee_local $0
        (i32.add
         (get_local $0)
         (i32.const 27)
        )
       )
       (i32.const -1)
      )
     )
    )
   )
   (return
    (get_local $0)
   )
  )
  (call $_ZN4core9panicking5panic17hc8058a23bb7ed97dE
   (i32.const 64)
  )
  (unreachable)
 )
)

And I have to modify main.js accordingly

// main.js
function loadWasm() {
    fetch('add.wasm').then(response =>
        response.arrayBuffer()
    ).then(buffer =>
        WebAssembly.instantiate(buffer, {
            env: {
                _ZN4core9panicking5panic17hc8058a23bb7ed97dE: console.log
            }
        })
    ).then(({
        module,
        instance
    }) => {
        console.log(instance.exports.add_twenty_seven);
        console.log(instance.exports.add_twenty_seven(4));
    });
}

loadWasm()

What's with the _ZN4core9panicking5panic17hc8058a23bb7ed97dE import?

My rustc and llvm version:

root@1c77e03e1f13:/work# rustc --version
rustc 1.21.0 (3b72af97e 2017-10-09)
root@1c77e03e1f13:/work# llc --version
LLVM (http://llvm.org/):
  LLVM version 6.0.0svn
  Optimized build.
  Default target: x86_64-unknown-linux-gnu
  Host CPU: cannonlake

  Registered Targets:
    aarch64    - AArch64 (little endian)
    aarch64_be - AArch64 (big endian)
    amdgcn     - AMD GCN GPUs
    arm        - ARM
    arm64      - ARM64 (little endian)
    armeb      - ARM (big endian)
    bpf        - BPF (host endian)
    bpfeb      - BPF (big endian)
    bpfel      - BPF (little endian)
    hexagon    - Hexagon
    lanai      - Lanai
    mips       - Mips
    mips64     - Mips64 [experimental]
    mips64el   - Mips64el [experimental]
    mipsel     - Mipsel
    msp430     - MSP430 [experimental]
    nvptx      - NVIDIA PTX 32-bit
    nvptx64    - NVIDIA PTX 64-bit
    ppc32      - PowerPC 32
    ppc64      - PowerPC 64
    ppc64le    - PowerPC 64 LE
    r600       - AMD GPUs HD2XXX-HD6XXX
    sparc      - Sparc
    sparcel    - Sparc LE
    sparcv9    - Sparc V9
    systemz    - SystemZ
    thumb      - Thumb
    thumbeb    - Thumb (big endian)
    wasm32     - WebAssembly 32-bit
    wasm64     - WebAssembly 64-bit
    x86        - 32-bit X86: Pentium-Pro and above
    x86-64     - 64-bit X86: EM64T and AMD64
    xcore      - XCore

@OpenGG
Copy link

OpenGG commented Nov 17, 2017

I try rustc --crate-type=lib --emit=llvm-bc -C opt-level=3 add.rs, and the output comes without the _ZN4core9panicking5panic* import.

@LukasKalbertodt
Copy link
Author

@OpenGG Sorry, apparently I don't receive notifications for gist documents. Unfortunately, I don't really know why you have this panic reference in your output. But please see the edit at the very top of this gist: compiling to wasm is much easier now.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment