Strings and Lists
Now we know how to write a WebAssembly library and add two numbers, let's work with something slightly more interesting - strings and lists!
First, we need to create a new project to hold our code. We'll also remove the existing code, so we start from blank slate.
$ cargo new --lib tutorial-02
$ cd tutorial-02 && rm src/lib.rs
The WIT File
Just like last time, our first step is to define a WIT file for our interface.
This file has two functions, the first function will create a string that greets a person by name (i.e. "Hello, Michael!")...
// strings-and-lists.wai
/// Greet a person by name.
greet: func(name: string) -> string
... and the other function will take a list of people's names, greeting them all at the same time.
/// Say hello to multiple people.
greet-many: func(people: list<string>) -> string
Writing Some Rust
Now we've defined our strings-and-lists.wai
file, let's implement the crate.
The first thing we need to do is add wai-bindgen
as a dependency.
$ cargo add wai-bindgen-rust
We also need to tell wai-bindgen
that we're implementing
strings-and-lists.wai
.
#![allow(unused)] fn main() { // src/lib.rs wai_bindgen_rust::export!("strings-and-lists.wai"); }
Next, we need to define a StringsAndLists
type and implement the
strings_and_lists::StringsAndLists
on it.
#![allow(unused)] fn main() { struct StringsAndLists; impl strings_and_lists::StringsAndLists for StringsAndLists { fn greet(name: String) -> String { format!("Hello, {name}!") } fn greet_many(people: Vec<String>) -> String { match people.as_slice() { [] => "Oh, nobody's there...".to_string(), [person] => format!("Hello, {person}!"), [people @ .., last] => { let people = people.join(", "); format!("Hello, {people}, and {last}!") } } } } }
The implementation of these functions is fairly straightforward, so we don't
need to go into too much detain about it other than pointing out
greet_many()
's use of Slice Patterns.
A Note on Ownership
While our code wasn't overly complex, there is something that needs to be pointed out,
Both functions use owned values for their arguments and results
This may seem odd, because it's idiomatic in normal Rust to pass strings and
lists around by reference (i.e. &str
and &[String]
) so the caller can
maintain ownership of the original value and doesn't need to make unnecessary
copies.
This comes back to one of the design decisions for WebAssembly, namely that a
guest (our tutorial-02
crate) is completely sandboxed and unable to access
memory on the host.
That means when the host calls our WebAssembly function, arguments will be passed in by
- Allocate some memory inside the guest's linear memory
- Copy the value into this newly allocated buffer
- Hand ownership of the buffer to the guest function (i.e. our
greet()
method)
Luckily wai-bindgen
will generate all the code we need for this, but it's
something to be aware of.
Another thing to keep in mind is that all return values must be owned, too.
WebAssembly doesn't have any concept of ownership and borrowing, so it'd be easy
for the host to run into use-after-free issues and dangling pointers if we were
allowed to return non-'static
values.
Publishing
Similar to last time, if we want to publish our package to WAPM, we'll need to
update our Cargo.toml
file.
# Cargo.toml
[package]
...
description = "Greet one or more people"
[lib]
crate-type = ["cdylib", "rlib"]
[package.metadata.wapm]
namespace = "wasmer"
abi = "none"
bindings = { wai-bindgen = "0.1.0", exports = "strings-and-lists.wai" }
Now, we can publish it to WAPM.
$ cargo wapm
Successfully published package `wasmer/tutorial-02@0.1.0`
Using The Bindings From TypeScript
For a change, let's use our bindings from TypeScript. First, we need to create
a basic package.json
file.
$ yarn init --yes
success Saved package.json
We'll need to install TypeScript and ts-node
.
$ yarn add --dev ts-node typescript @types/node
The TypeScript compiler will need a basic tsconfig.json
file.
// tsconfig.json
{
"compilerOptions": {
"target": "es2016",
"module": "ESNext",
"moduleResolution": "node",
"strict": true,
"skipLibCheck": true
}
}
Now, we can use wapm install
to add our tutorial-02
package as a dependency.
$ wapm install --yarn wasmer/tutorial-02
Finally, we're able to start writing some code.
// index.ts
import { bindings } from "@wasmer/tutorial-02";
async function main() {
const strings = await bindings.strings_and_lists();
console.log(strings.greet("World"));
console.log(strings.greetMany(["a", "b", "c"]));
}
main();
If we run it using the ts-node
loader, we'll see exactly the output we expect.
$ node --loader ts-node/esm index.ts
Hello, World!
Hello, a, b, and c!
Conclusion
Strings and lists are the building blocks of all meaningful applications, so it's important to know how to use them.
Our first foray into non-primitive types has also introduced us to the repercussions of running your code inside a fully sandboxed virtual machine - any data received from the outside world must be copied into linear memory.