Calling WebAssembly functions using Swift

This is part of a series we’re writing on WebAssembly on iOS. Be sure to check out all the articles in this series:

  1. Using Wasm on iOS
  2. Loading Wasm modules in Swift
  3. Calling Wasm functions using Swift
  4. Accessing memory inside Wasm modules using Swift

When designing and building WasmInterpreter, our next goal after initializing and loading a WebAssembly (Wasm) module was to be able to call a Wasm function and receive its return value. Although adding this to WasmInterpreter wasn’t particularly difficult, it required some research to learn about Wasm functions and how Wasm3 (and, by extension, our Swift wrapper CWasm3) calls them.

Functions in WebAssembly

Like in most other programming languages, functions in WebAssembly have a signature, local variables, and a body. The signature declares what parameters the function takes and what it returns. The local variables are variables used inside of the function. The body is the list of low-level instructions called in the function.

Function signatures are simpler in WebAssembly than in many other programming languages, because WebAssembly only supports numeric types: 32- and 64-bit integers and floats. Additionally, although version 1.1 of the WebAssembly specification supports multiple return values, CWasm3 and WasmInterpreter follow the 1.0 specification, which only allowed for a single return value.

Writing a WebAssembly function by hand

Before writing any Swift code, we wanted to be able to unit test it, which meant we needed to write a Wasm function to test against. The easiest way to do this was to use the WebAssembly text format, which is a Lisp.

We wrote a simple WebAssembly module containing a single function called “add”. This function took two 32-bit integer arguments and returned a 32-bit integer. Inside of the body of the function, we simply called the add instruction and returned the value.

(module
  (type (;0;) (func (param i32 i32) (result i32)))
  (func (;0;) (type 0) (param i32 i32) (result i32)
    local.get 0
    local.get 1
    i32.add)
  (export "add" (func 0)))

By and large, the WebAssembly text format is straightforward and comprehensible, but the MDN web docs do a great job explaining the format in-depth, if you want to learn more about it.

WebAssembly text format files are saved with the wat extension. We saved our module as add.wat. However, wat files are not parsable directly by Wasm3. So, we had to first translate our file to the WebAssembly binary format. The best tool for this job is a command-line program called wat2wasm.

$ wat2wasm add.wat -o add.wasm

Calling WebAssembly functions using CWasm3

Calling Wasm functions using CWasm3 is a three-step process. First, retrieve the function from the parsed Wasm module. Second, pass the Wasm function, its arguments, and an in-out variable to hold the return value to wasm3_CallWithArgs(). Importantly, the arguments must be passed as C-style character arrays (the arguments will be converted to the correct type by CWasm3 based on the Wasm function signature). Third, read the return value from the in-out variable passed to wasm3_CallWithArgs() in #2.

// 1. Retrieve the Wasm function
var addFunction: IM3Function?
m3_FindFunction(&addFunction, runtime, "add")


// 2a. Format the arguments as C-style strings
["3", "12345"].withCStrings { (args) -> Int32 in
  let size = UnsafeMutablePointer<Int>.allocate(capacity: 1)
  let output = UnsafeMutablePointer<Int32>.allocate(capacity: 1)
  // 2b. Pass the arguments and in-out return value variable to the Wasm function
  wasm3_CallWithArgs(addFunction, UInt32(2), args, size, output))
  // 3. Read the return value
  print(output.pointee) // 12348
}

This code was adapted from this test case in CWasm3.

Creating a native Swift wrapper

As we said in our last post about Wasm on iOS, using CWasm3 to call WebAssembly functions required a lot of boilerplate code and awkward conversions between safe and unsafe Swift types. Our WasmInterpreter wrapper should abstract all of that ugliness away behind a native Swift interface.

When designing the interface for calling our add() WebAssembly function, we wanted the call site to look as similar to this as possible:

let bytes = /* load Add module from disk */
let addModule = try WasmInterpreter(module: bytes)
let ret = try addModule.call("add", 1, 2)

Loading the WebAssembly function by name

The first thing we needed to do was retrieve the function from the parsed module. This was straightforward because CWasm3's API for this was already quite simple.

func function(named name: String) throws -> IM3Function {
  var f: IM3Function?
  try WasmInterpreter.check(m3_FindFunction(&f, _runtime, name))
  guard let function = f else { throw WasmInterpreterError.couldNotFindFunction(name) }
  return function
}

We simply needed to call m3_FindFunction(), passing in the runtime, function name, and an in-out variable to hold the function.

Passing in arguments

As we mentioned above, function arguments need to be passed to CWasm3 as C-style character arrays. Asking the consumer of WasmInterpreter to convert all their function arguments to String was a non-starter for us. We wanted the consumers of this library to be able to use native, primitive Swift types to call WebAssembly functions. So, we needed to do the conversion ourselves. However, before writing that code, we decided to write a private function that accepted String arguments, converted them to C-style character arrays, and called wasm3_CallWithArgs().

func _call(_ function: IM3Function, args: [String]) throws {
  try args.withCStrings { (cStrings) throws -> Void in
    var mutableCStrings = cStrings
    let r = wasm3_CallWithArgs(function, UInt32(args.count), &mutableCStrings, nil, nil)
    if let result = r {
      throw WasmInterpreterError.onCallFunction(String(cString: result))
    } else {
      return ()
    }
  }
}

Array<String>.withCStrings() was a method we added to Array where Element == String. Its implementation is a bit beyond the scope of this article, but it handled converting the Swift String arguments into C-style character arrays. You can see its implementation here.

Retrieving the return value

After doing this, we were able to successfully pass arguments into our Wasm function, but we were still unable to retrieve the return value from the function. The fourth and fifth parameters to wasm3_CallWithArgs() are in-out arguments that hold, respectively, the size in bytes of the return value and the return value itself. Since we knew our WebAssembly functions could only ever return one of four values (Int32, Int64, Float32, or Float64), we could have written four separate functions—one for each of those return values. However, that would have resulted in a lot of duplicated code, which would have been error-prone and wasteful. Instead, we decided to use Swift’s generics.

In order to limit the generic return value to the correct types, we first created the WasmTypeProtocol and conformed our valid return value types to this protocol.

public protocol WasmTypeProtocol {}

extension Int32: WasmTypeProtocol {}
extension Int64: WasmTypeProtocol {}
extension Float32: WasmTypeProtocol {}
extension Float64: WasmTypeProtocol {}

The protocol didn’t need to include any required functions or properties because we only needed it to identify the correct return value types for the library consumer and compiler. By including a generic type in our _call() method and restricting it to types conforming to WasmTypeProtocol, we were able to easily add support for retrieving return values from WebAssembly functions.

func _call<T: WasmTypeProtocol>(_ function: IM3Function, args: [String]) throws -> T {
  try args.withCStrings { (cStrings) throws -> T in
    var mutableCStrings = cStrings
    let size = UnsafeMutablePointer<Int>.allocate(capacity: 1)
    let output = UnsafeMutablePointer<T>.allocate(capacity: 1)
    let r = wasm3_CallWithArgs(function, UInt32(args.count), &mutableCStrings, size, output)
    if let result = r {
      throw WasmInterpreterError.onCallFunction(String(cString: result))
    } else if MemoryLayout<T>.size != size.pointee {
      throw WasmInterpreterError.invalidFunctionReturnType
    } else {
      return output.pointee
    }
  }
}

Instead of blindly casting the return value from wasm3_CallWithArgs() to the type expected by _call(), we first tested its size in bytes to make sure it matched the size of the expected return value type.

Following this, we were able to call our add() WebAssembly function like this:

let bytes = /* load Add module from disk */
let addModule = try WasmInterpreter(module: bytes)
let f = try function(named: "add")
let ret: Int32 = try addModule._call(f, args: ["1", "2"])

Passing arguments via generic parameters

The combination of generic functions and type inference made it simple to return native Swift types from Wasm. We decided to use the same approach for passing native Swift types as arguments. Since our add() Wasm function accepted two arguments, we wrote a public call() method supporting two arguments and a return value.

public func call<Arg1, Arg2, Ret>(
  _ name: String, _ arg1: Arg1, _ arg2: Arg2
) throws -> Ret where Arg1: WasmTypeProtocol, Arg2: WasmTypeProtocol, Ret: WasmTypeProtocol {
  return try _call(
    try function(named: name),
    args: [try String(wasmType: arg1), try String(wasmType: arg2)]
  )
}

This function was short because it called through to our internal function(named:) and _call() methods. Its only purpose was to convert the generic arguments into Swift Strings. String(wasmType:) simply ensured the wasmType was one of our supported types and then wrapped it in a String.

After adding call(), we were finally able to call our add() WebAssembly function like this:

let bytes = /* load Add module from disk */
let addModule = try WasmInterpreter(module: bytes)
let ret: Int32 = try addModule("add", Int32(1), Int32(2))

Of course, not every Wasm function could be expected to accept exactly two arguments and return a value like our add(). We wanted to be able to support the vast majority of all WebAssembly functions people would want to call. The approach we chose to solve this problem was to write a series of generic methods taking from zero to six arguments. For each of these, we also wrote a version that returned a value and one that didn’t. You can see these methods here. This could have been a good opportunity to use a code generation tool like Swift GYB, but, alas, we simply wrote them by hand.

Next…

In the next article, I’ll write about how to read from the WebAssembly runtime’s heap. As always, if you don’t want to wait, you can look at all of the code now. Clone WasmInterpreter and start playing around. A good place to start is AddModule, which is a wrapper around the simple Wasm add() function we wrote today. Be sure to let me know if you have any thoughts or use WasmInterpreter to build something cool.