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

Write user documentation for Rust contracts #129

Open
wants to merge 4 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -90,7 +90,7 @@ One of the [design goals](https://github.com/ewasm/design/blob/master/rationale.
At present, we've developed support for the following languages and toolchains:

- [C/C++ (LLVM) WebAssembly tutorial](./clang.md)
- Rust: documentation pending
- [Rust](./rust.md)
- [AssemblyScript](https://github.com/AssemblyScript/assemblyscript), a subset of TypeScript, which uses the JavaScript toolchain: see the [etherts org](https://github.com/etherts/docs) for more information on writing contracts in AssemblyScript.

If you're interested in adding support for another language, framework, or toolset, see the Contributing section above and reach out.
Expand Down
146 changes: 146 additions & 0 deletions rust.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,146 @@
# Writing Ewasm contracts in Rust

Rust is a modern systems programming language.
Its type system and memory model allow for strong assumptions about memory safety at little or no runtime cost.
Furthermore, its sophisticated compiler optimizations minimize the overhead of high-level language features.

Such a combination of performance and safety lends itself to use in developing Ethereum smart contracts.
This document serves as a comprehensive guide to using Rust in your Ewasm project.

### Assumptions

This document assumes:
* A Unix-like OS (although hopefully some of this document is applicable to Windows as well)
* The presence of a working Rust and `cargo` installation via `rustup`
* Basic familiarity with the command line, Rust, and Ethereum.

## 1. Toolchain Setup

In order to compile to WebAssembly, Rust needs the WebAssembly backend installed.

`rustup` makes this extremely simple. Just run:
```console
$ rustup target add wasm32-unknown-unknown
```
Make sure you are installing the target `wasm32-unknown-unknown` and not `wasm32-unknown-emscripten`.

## 2. Configuration and Build

Now that the WebAssembly backend is installed, it can be used to compile almost any Rust project to a Wasm binary.
However, Ewasm also specifies a set of imports and exports for contracts. These are roughly analogous to symbols in a native binary.

In order to properly export and import said symbols, the project must be compiled as a "shared library."
While this has no meaning in WebAssembly (all binaries are modules), the flag ensures that the compiler will export and import the correct symbols.

To do this, ensure that the project uses `lib.rs` as the crate root (not `main.rs`), and add the following lines to `Cargo.toml`:
```toml
[lib]
crate-type = ["cdylib"]
```
Compile the project using the `--target` flag:
```console
$ cargo build --target=wasm32-unknown-unknown
```
Alternatively, to set WebAssembly as a default target, one can specify it in `.cargo/config`.
Create a `.cargo` directory in the project root, if it does not exist already:
```console
$ mkdir .cargo
```
Proceed to create or modify `.cargo/config` and add the following:
```toml
[build]
target = "wasm32-unknown-unknown"
```
Once this is done, `cargo build` will compile the project to WebAssembly by default.

### Optimizing Ewasm binaries

By default, the Rust compiler can generate huge WebAssembly binaries, even in release mode.
These include lots of unnecessary cruft and are not suitable for use as Ewasm contracts.

The following steps will show how to optimize for size and remove unnecessary code segments from the resulting Wasm binary.

#### Compiler optimizations

The simplest way to slim down Rust-generated binaries is by enabling certain compiler optimizations:

* Enabling `lto`, or link-time optimizations, allows the compiler to prune or inline parts of the code at link-time.
* Setting `opt-level` to `'s'` tells the compiler to optimize for binary size rather than speed. `opt-level = 'z'` will optimize more aggressively for size, although at the cost of performance.
Copy link
Member

Choose a reason for hiding this comment

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

Note:

error: the optimizations s or z are only accepted on the nightly compiler


Enable these options in the `release` profile by adding the following to `Cargo.toml`:
```toml
[profile.release]
lto = true
opt-level = 's'
```

#### Using [wasm-snip](https://github.com/rustwasm/wasm-snip)

The Rust compiler, by default, includes panicking and formatting code in generated binaries. For the purpose of Ewasm contracts, this is useless.
Using the `wasm-snip` tool, it is possible to replace the function bodies of such code with a single `unreachable` opcode, further shrinking the binary size.
Copy link
Member

Choose a reason for hiding this comment

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

wasm-snip requires a run of wasm-gc afterwards, iirc.


`wasm-snip` can be easily installed using `cargo`:
```console
$ cargo install wasm-snip
```
`wasm-snip` depends on the name section in the Wasm binary being present. Therefore, we must build with `debug` enabled. Add the following to the `[profile.release]` section in `Cargo.toml`:
```toml
debug = true
```
After build, run `wasm-snip` on the resulting binary. It should be located in `target/wasm32-unknown-unknown/release`:
```console
$ wasm-snip --snip-rust-panicking-code --snip-rust-fmt-code input.wasm -o output.wasm
```

## 3. Writing Ewasm contracts

In addition to the normal constraints of the blockchain, the Ewasm [specification](https://github.com/ewasm/design) makes writing code for Ewasm contracts a little different from writing plain Rust.

The following is a list of rules and best practices for writing Ewasm contracts.

### `lib.rs`, not `main.rs`

As stated earlier, using `main.rs` as a crate root will produce an executable rather than a library.

### Export `main`

Every Ewasm contract is required to export a `main` function as the main entry point for execution. In Rust, simply add the `pub` and `extern "C"` qualifiers to do this:
```rust
#[no_mangle]
pub extern "C" fn main() {
// function body goes here
}
```
The `no_mangle` directive should be included as well, to ensure that the compiler will not mangle the `main` identifier and invalidate it.

### Use `finish` and `revert` to end execution

When writing code for a native platform, ending execution properly is usually handled behind the scenes. In Ewasm, there are two EEI functions that signal to end execution:

* [`finish`](https://github.com/ewasm/design/blob/master/eth_interface.md#finish) behaves like EVM's `return`. Simply put, it sets the output data and halts the VM.
* [`revert`](https://github.com/ewasm/design/blob/master/eth_interface.md#revert) is the exact same thing as EVM's `revert`. It halts the VM and refunds any gas.

### Minimize the use of the standard library

Many constructs in Rust's standard library are not optimized for usage in Ewasm, or are outright unavailable due to dependence on a native syscall interface.
Copy link
Member

Choose a reason for hiding this comment

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

Could you give a specific example of something that is unavailable or not optimized for use?

Copy link
Member Author

Choose a reason for hiding this comment

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

Examples are below. As for something that is unavailable due to syscalls, most collections can simply use a custom allocator. So I might drop this line since nobody actually needs to read about that for this purpose.

Copy link
Member

Choose a reason for hiding this comment

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

I might drop this line since nobody actually needs to read about that for this purpose

Agree--where in doubt, keep it simple


Minimizing the use of non-primitive standard library constructs will improve the size, performance, and gas efficiency of resulting Wasm binaries. Where possible, follow these rules of thumb:
* Minimize the usage of the heap, in general. Don't use `Box<T>` or `Rc<T>`, or any other smart pointers.
* Avoid using types with implicitly heap-allocated data. Use arrays instead of `Vec`, and `str` instead of `String`.
* Borrowing is your friend. Stop using `clone()` and passing non-trivial structs by value, use lifetime parameters and references instead.

### Use the [ewasm-rust-api](https://github.com/ewasm/ewasm-rust-api)

The `ewasm-rust-api` is a convenient library providing safe wrappers around the native EEI functions, in addition to a set of useful EVM-style types.

To use it in your contract, simply add `ewasm_api` as a dependency to `Cargo.toml` and include it in your project.

## 4. Deploying Ewasm contracts

There is one final step that must be done before contract creation: post-processing.

The Rust compiler, at the moment, does not have fine-grained control over the imports and exports generated other than by their identifiers in the source. As a result, unnecessary exports must be removed and malformed imports corrected.

This can be done using an automated tool such as [wasm-chisel](https://github.com/wasmx/wasm-chisel), or by hand in the `.wast` file produced by disassembling the Wasm binary.
Copy link
Member

Choose a reason for hiding this comment

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

Could you demonstrate how to run wasm-chisel here so these instructions are complete?


If done by hand, the most common errors will be import names being prefixed with `ethereum_` and globals or tables being unnecessarily exported.