• Stars
    star
    588
  • Rank 73,377 (Top 2 %)
  • Language
    Rust
  • License
    Apache License 2.0
  • Created about 6 years ago
  • Updated 10 months ago

Reviews

There are no reviews yet. Be the first to send feedback to the community and the maintainers!

Repository Details

JIT compiler and runtime for a toy language, using Cranelift

Hello!

This is a simple demo that JIT-compiles a toy language, using Cranelift.

It uses the new JIT interface in development here. JIT takes care of managing a symbol table, allocating memory, and performing relocations, offering a relatively simple API.

This is inspired in part by Ulysse Carion's llvm-rust-getting-started and JT's rustyjit.

A quick introduction to Cranelift: Cranelift is a compiler backend. It's light-weight, supports no_std mode, doesn't use floating-point itself, and it makes efficient use of memory.

And Cranelift is being architected to allow flexibility in how one uses it. Sometimes that flexibility can be a burden, which we've recently started to address in a new set of crates, cranelift-module, cranelift-jit, and cranelift-faerie, which put the pieces together in some easy-to-use configurations for working with multiple functions at once. cranelift-module is a common interface for working with multiple functions and data interfaces at once. This interface can sit on top of cranelift-jit, which writes code and data to memory where they can be executed and accessed. And, it can sit on top of cranelift-faerie, which writes code and data to native .o files which can be linked into native executables.

This post introduces Cranelift by walking through a JIT demo, using the cranelift-jit crate. Currently this demo works on Linux x86-64 platforms. It may also work on Mac x86-64 platforms, though I haven't specifically tested that yet. And Cranelift is being designed to support many other kinds of platforms in the future.

A walkthrough

First, let's take a quick look at the toy language in use. It's a very simple language, in which all variables have type isize. (Cranelift does have full support for other integer and floating-point types, so this is just to keep the toy language simple).

For a quick flavor, here's our first example in the toy language:

        fn foo(a, b) -> (c) {
            c = if a {
                if b {
                    30
                } else {
                    40
                }
            } else {
                50
            }
            c = c + 2
        }

The grammar for this toy language is defined here, and this demo uses the peg parser generator library to generate actual parser code for it.

The output of parsing is a custom AST type:

pub enum Expr {
    Literal(String),
    Identifier(String),
    Assign(String, Box<Expr>),
    Eq(Box<Expr>, Box<Expr>),
    Ne(Box<Expr>, Box<Expr>),
    Lt(Box<Expr>, Box<Expr>),
    Le(Box<Expr>, Box<Expr>),
    Gt(Box<Expr>, Box<Expr>),
    Ge(Box<Expr>, Box<Expr>),
    Add(Box<Expr>, Box<Expr>),
    Sub(Box<Expr>, Box<Expr>),
    Mul(Box<Expr>, Box<Expr>),
    Div(Box<Expr>, Box<Expr>),
    IfElse(Box<Expr>, Vec<Expr>, Vec<Expr>),
    WhileLoop(Box<Expr>, Vec<Expr>),
    Call(String, Vec<Expr>),
    GlobalDataAddr(String),
}

It's pretty minimal and straightforward. The IfElse can return a value, to show how that's done in Cranelift (see below).

The first thing we do is create an instance of our JIT:

let mut jit = jit::JIT::new();

The JIT class is defined here and contains several fields:

  • builder_context - Cranelift uses this to reuse dynamic allocations between compiling multiple functions.
  • ctx - This is the main Context object for compiling functions.
  • data_ctx - Similar to ctx, but for "compiling" data sections.
  • module - The Module which holds information about all functions and data objects defined in the current JIT.

Before we go any further, let's talk about the underlying model here. The Module class divides the world into two kinds of things: functions, and data objects. Both functions and data objects have names, and can be imported into a module, defined and only referenced locally, or defined and exported for use in outside code. Functions are immutable, while data objects can be declared either readonly or writable.

Both functions and data objects can contain references to other functions and data objects. Cranelift is designed to allow the low-level parts operate on each function and data object independently, so each function and data object maintains its own individual namespace of imported names. The Module struct takes care of maintaining a set of declarations for use across multiple functions and data objects.

These concepts are sufficiently general that they're applicable to JITing as well as native object files (more discussion below!), and Module provides an interface which abstracts over both.

Once we've initialized the JIT data structures, we then use our JIT to compile some functions.

The JIT's compile function takes a string containing a function in the toy language. It parses the string into an AST, and then translates the AST into Cranelift IR.

Our toy language only supports one type, so we start by declaring that type for convenience.

We then start translating the function by adding the function parameters and return types to the Cranelift function signature.

Then we create a FunctionBuilder which is a utility for building up the contents of a Cranelift IR function. As we'll see below, FunctionBuilder includes functionality for constructing SSA form automatically so that users don't have to worry about it.

Next, we start an initial basic block (block), which is the entry block of the function, and the place where we'll insert some code.

  • A basic block is a sequence of IR instructions which have a single entry point, and no branches until the very end, so execution always starts at the top and proceeds straight through to the end.

Cranelift's basic blocks can have parameters. These take the place of PHI functions in other IRs.

Here's an example of a block, showing branches (brif and jump) that are at the end of the block, and demonstrating some block parameters.

block0(v0: i32, v1: i32, v2: i32, v507: i64):
    v508 = iconst.i32 0
    v509 = iconst.i64 0
    v404 = ifcmp_imm v2, 0
    v10 = iadd_imm v2, -7
    v405 = ifcmp_imm v2, 7
    brif ugt v405, block29(v10)
    jump block29(v508)

The FunctionBuilder library will take care of inserting block parameters automatically, so frontends that don't need to use them directly generally don't need to worry about them, though one place they do come up is that incoming arguments to a function are represented as block parameters to the entry block. We must tell Cranelift to add the parameters, using append_block_params_for_function_params like so.

The FunctionBuilder keeps track of a "current" block that new instructions are to be inserted into; we next inform it of our new block, using switch_to_block, so that we can start inserting instructions into it.

The one major concept about blocks is that the FunctionBuilder wants to know when all branches which could branch to a block have been seen, at which point it can seal the block, which allows it to perform SSA construction. All blocks must be sealed by the end of the function. We seal a block with seal_block.

Next, our toy language doesn't have explicit variable declarations, so we walk the AST to discover all the variables, so that we can declare then to the FunctionBuilder. These variables need not be in SSA form; the FunctionBuilder will take care of constructing SSA form internally.

For convenience when walking the function body, the demo here uses a FunctionTranslator object, which holds the FunctionBuilder, the current Module, as well as the symbol table for looking up variables. Now we can start walking the function body.

AST translation utilizes the instruction-building features of FunctionBuilder. Let's start with a simple example translating integer literals:

    Expr::Literal(literal) => {
        let imm: i32 = literal.parse().unwrap();
        self.builder.ins().iconst(self.int, i64::from(imm))
    }

The first part is just extracting the integer value from the AST. The next line is the builder line:

  • The .ins() returns an "insertion object", which allows inserting an instruction at the end of the currently active block.
  • iconst is the name of the builder routine for creating integer constants in Cranelift. Every instruction in the IR can be created directly through such a function call.

Translation of Add nodes and other arithmetic operations is similarly straightforward.

Translation of variable references is mostly handled by FunctionBuilder's use_var function:

    Expr::Identifier(name) => {
        // `use_var` is used to read the value of a variable.
        let variable = self.variables.get(&name).expect("variable not defined");
        self.builder.use_var(*variable)
    }

use_var is for reading the value of a (non-SSA) variable. (Internally, FunctionBuilder constructs SSA form to satisfy all uses).

Its companion is def_var, which is used to write the value of a (non-SSA) variable, which we use to implement assignment:

    fn translate_assign(&mut self, name: String, expr: Expr) -> Value {
        // `def_var` is used to write the value of a variable. Note that
        // variables can have multiple definitions. Cranelift will
        // convert them into SSA form for itself automatically.
        let new_value = self.translate_expr(*expr);
        let variable = self.variables.get(&name).unwrap();
        self.builder.def_var(*variable, new_value);
        new_value
    }

Next, let's dive into if-else expressions. In order to demonstrate explicit SSA construction, this demo gives if-else expressions return values. The way this looks in Cranelift is that the true and false arms of the if-else both have branches to a common merge point, and they each pass their "return value" as a block parameter to the merge point.

Note that we seal the blocks we create once we know we'll have no more predecessors, which is something that a typical AST makes it easy to know.

Putting it all together, here's the Cranelift IR for the function named foo in the demo program, which contains multiple ifs:

function u0:0(i64, i64) -> i64 system_v {
block0(v0: i64, v1: i64):
    v2 = iconst.i64 0
    brz v0, block2
    jump block1

block1:
    v4 = iconst.i64 0
    brz.i64 v1, block5
    jump block4

block4:
    v6 = iconst.i64 0
    v7 = iconst.i64 30
    jump block6(v7)

block5:
    v8 = iconst.i64 0
    v9 = iconst.i64 40
    jump block6(v9)

block6(v5: i64):
    jump block3(v5)

block2:
    v10 = iconst.i64 0
    v11 = iconst.i64 50
    jump block3(v11)

block3(v3: i64):
    v12 = iconst.i64 2
    v13 = iadd v3, v12
    return v13
}

The while loop translation is also straightforward.

Here's the Cranelift IR for the function named iterative_fib in the demo program, which contains a while loop:

function u0:0(i64) -> i64 system_v {
block0(v0: i64):
    v1 = iconst.i64 0
    v2 = iconst.i64 0
    v3 = icmp eq v0, v2
    v4 = bint.i64 v3
    brz v4, block2
    jump block1

block1:
    v6 = iconst.i64 0
    v7 = iconst.i64 0
    jump block3(v7, v7)

block2:
    v8 = iconst.i64 0
    v9 = iconst.i64 1
    v10 = isub.i64 v0, v9
    v11 = iconst.i64 0
    v12 = iconst.i64 1
    jump block4(v10, v12, v11)

block4(v13: i64, v17: i64, v18: i64):
    v14 = iconst.i64 0
    v15 = icmp ne v13, v14
    v16 = bint.i64 v15
    brz v16, block6
    jump block5

block5:
    v19 = iadd.i64 v17, v18
    v20 = iconst.i64 1
    v21 = isub.i64 v13, v20
    jump block4(v21, v19, v17)

block6:
    v22 = iconst.i64 0
    jump block3(v22, v17)

block3(v5: i64, v23: i64):
    return v23
}

For calls, the basic steps are to determine the call signature, declare the function to be called, put the values to be passed in an array, and then call the call function.

The translation for global data symbols, is similar; first declare the symbol to the module, then declare it to the current function, and then use the symbol_value instruction to produce the value.

And with that, we can return to our main toy.rs file and run some more examples. There are examples of recursive and iterative fibonacci, which demonstrate more use of calls and control flow.

And there's a hello world example which demonstrates several other features.

This program needs to allocate some data to hold the string data. Inside jit.rs, create_data we initialize a DataContext with the contents of the hello string, and also declare a data object. Then we use the DataContext object to define the object. At that point, we're done with the DataContext object and can clear it. We then call finalize_data to perform linking (although our simple hello string doesn't make any references so there isn't anything to do) and to obtain the final runtime address of the data, which we then convert back into a Rust slice for convenience.

And to show off a handy feature of the jit backend, it can look up symbols with libc::dlsym, so you can call libc functions such as puts (being careful to NUL-terminate your strings!). Unfortunately, printf requires varargs, which Cranelift does not yet support.

And with all that, we can say "hello world!".

Native object files

Because of the Module abstraction, this demo can be adapted to write out an ELF .o file rather than JITing the code to memory with only minor changes, and I've done so in a branch here. This writes a test.o file, which on an x86-64 ELF platform you can link with cc test.o and it produces an executable that calls the generated functions, including printing "hello world!".

Another branch here shows how to write Mach-O object files.

Object files are written using the faerie library.

Have fun!

Cranelift is still evolving, so if there are things here which are confusing or awkward, please let us know, via github issues or just stop by the gitter chat. Very few things in Cranelift's design are set in stone at this time, and we're really interested to hear from people about what makes sense what doesn't.

More Repositories

1

wasmtime

A fast and secure runtime for WebAssembly
Rust
14,541
star
2

wasm-micro-runtime

WebAssembly Micro Runtime (WAMR)
C
4,528
star
3

lucet

Lucet, the Sandboxing WebAssembly Compiler.
Rust
4,074
star
4

cranelift

Cranelift code generator
2,482
star
5

javy

JS to WebAssembly toolchain
Rust
1,958
star
6

rustix

Safe Rust bindings to POSIX-ish APIs
Rust
1,286
star
7

wasm-tools

CLI and Rust libraries for low-level manipulation of WebAssembly modules
Rust
1,105
star
8

wit-bindgen

A language binding generator for WebAssembly interface types
Rust
887
star
9

wizer

The WebAssembly Pre-Initializer
Rust
859
star
10

wasmtime-go

Go WebAssembly runtime powered by Wasmtime
Go
729
star
11

cap-std

Capability-oriented version of the Rust standard library
Rust
614
star
12

jco

JavaScript tooling for working with WebAssembly Components
Rust
468
star
13

cargo-wasi

A lightweight Cargo subcommand to build Rust code for the `wasm32-wasi` target
Rust
435
star
14

cargo-component

A Cargo subcommand for creating WebAssembly components based on the component model proposal.
Rust
405
star
15

wasmtime-dotnet

.NET embedding of Wasmtime https://bytecodealliance.github.io/wasmtime-dotnet/
C#
391
star
16

wasmtime-py

Python WebAssembly runtime powered by Wasmtime
Python
352
star
17

wasi

Experimental WASI API bindings for Rust
Rust
210
star
18

wasmparser

A simple event-driven library for parsing WebAssembly binary files
178
star
19

regalloc2

A new register allocator
Rust
178
star
20

wasi.dev

HTML
170
star
21

ComponentizeJS

JS -> WebAssembly Component
Rust
163
star
22

registry

WebAssembly Registry (Warg)
Rust
153
star
23

wasmtime-demos

Historical and dated demos for Wasmtime usage and WASI content
C#
152
star
24

wat

Rust WAT and WAST parser (WebAssembly Text Format)
113
star
25

regalloc.rs

Modular register allocator algorithms
Rust
107
star
26

WASI-Virt

Virtual implementations of WASI APIs
Rust
98
star
27

componentize-py

Rust
89
star
28

spidermonkey-wasm-rs

Rust
86
star
29

preview2-prototyping

Polyfill adapter for preview1-using wasm modules to call preview2 functions.
Rust
78
star
30

wasmtime-rb

Ruby WebAssembly runtime powered by Wasmtime
Rust
73
star
31

wasmtime-cpp

C++
70
star
32

wasm-interface-types

Raw Rust toolchain support for Wasm Interface Types
Rust
70
star
33

wac

WebAssembly Composition (WAC) tooling
Rust
69
star
34

sightglass

A benchmark suite and tool to compare different implementations of the same primitives.
C
64
star
35

rfcs

RFC process for Bytecode Alliance projects
57
star
36

component-docs

Documentation around creating and using WebAssembly Components
Rust
46
star
37

target-lexicon

Target "triple" support
Rust
44
star
38

userfaultfd-rs

Rust bindings for the Linux userfaultfd functionality
Rust
42
star
39

system-interface

Extensions to the Rust standard library
Rust
40
star
40

wasi-nn

High-level bindings for wasi-nn system calls
CSS
36
star
41

wasmprinter

Rust library to print a WebAssembly binary to its textual format
32
star
42

spidermonkey-wasm-build

Utilities to compile SpiderMonkey to wasm32-wasi
JavaScript
22
star
43

wit-deps

WIT dependency manager
Rust
20
star
44

meetings

Python
20
star
45

filecheck

Library for writing tests for utilities that read text files and produce text output
Rust
20
star
46

vscode-wit

Visual Studio Code extension to recognize and highlight the WebAssembly Interface Type (WIT) IDL.
TypeScript
19
star
47

wamr-rust-sdk

Rust
15
star
48

wasm-score

A benchmark for standalone WebAssembly
C
15
star
49

cranelift.vim

Vim editor configuration for working with cranelift IR (clif) files
Vim Script
14
star
50

arf-strings

Encoding and decoding for ARF strings
C
12
star
51

SIG-Registries

11
star
52

bytecodealliance.org

CSS
10
star
53

subscribe-to-label-action

A GitHub action that allows users to subscribe to a label and automatically get @'d when the label is applied
JavaScript
10
star
54

SIG-Guest-Languages

Special Interest Group (SIG) whose goal is to investigate how best to integrate Wasm and components into dynamic programming language ecosystems in a way that feels native to those ecosystems.
10
star
55

wasm-spec-interpreter

Rust bindings for the Wasm spec interpreter.
Rust
8
star
56

governance

7
star
57

wasm-parallel-gzip

Some example scripts for building a parallel compression/decompression tool for WebAssembly
Makefile
6
star
58

wamr-python

Python
6
star
59

fs-set-times

Set filesystem timestamps
Rust
5
star
60

arena-btree

Rust
5
star
61

wasmtime-libfuzzer-corpus

libFuzzer corpus for our wasmtime fuzz targets
Shell
5
star
62

wamr.dev

The WAMR homepage
HTML
5
star
63

wasmtime.dev

The Wasmtime homepage
CSS
4
star
64

cm-go

4
star
65

libc-test

Mirror of git://nsz.repo.hu:49100/repo/libc-test (see https://wiki.musl-libc.org/libc-test.html for more information)
C
3
star
66

wamr-app-framework

WebAssembly Micro Runtime Application Framework
C
2
star
67

wasmtime-wasi-nn

2
star
68

actions

GitHub actions to setup wasm-tools and wasmtime
TypeScript
1
star
69

label-messager-action

Automatically leave a message when an issue or pull request has a certain label
JavaScript
1
star
70

wasm-ml-meetings

Informal working group for machine learning and WebAssembly, especially wasi-nn
1
star