Using WebAssembly on iOS

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

Here at Shareup, we are a small team building the easiest and fastest way to securely share anything with anyone. In order to achieve our ambitious goal with our small team, we need to be efficient. One of the ways we are trying to save time and effort is by compiling our mission-critical code to WebAssembly and shipping that code to all the platforms we support. With this goal in mind, it was critical for us to find a way to execute WebAssembly on iOS. We tried a few different approaches, but the one we’re happiest with right now is Wasm3, a high-performance WebAssembly interpreter written in C.

Adding Wasm3 to an iOS Xcode project the easy way

The Wasm3 project is built using CMake. However, being a C project, it would be trivial to add Wasm3 to an iOS or macOS project without having to mess around with CMake. Simply adding the C source and header files contained in the project’s source directory to an Xcode project would be enough. After adding the Wasm3 source files to your Xcode project, you would need to add #include "wasm3.h" to your project’s bridging header (making sure to reference the bridging header in your project’s SWIFT_OBJC_BRIDGING_HEADER build setting). Following that, you would be able to call Wasm3’s C functions natively using Swift, Objective-C, or C.

// ProjectName-Bridging-Header.h

#ifndef ProjectName_Bridging_Header_h
#define ProjectName_Bridging_Header_h

#include "wasm3.h"

#endif // ProjectName_Bridging_Header_h

Adding Wasm3 to an iOS Xcode project the Swift way

Although directly adding C source files to an iOS Xcode project is easy, at Shareup, we prefer to manage our dependencies using Swift Package Manager because it makes it easy to write, test, and reuse small and focused libraries. Creating a Swift package comprised solely of C source files isn’t documented very well by Apple. The Swift community has done a admirable job of explaining how to wrap C system libraries in Swift packages (here, here, and here). However, there’s a paucity of information showing how to wrap non-system C code in Swift packages. Thankfully, the process is very simple.

The first step is to create a Swift library package. When wrapping C code in a Swift package, the convention is to prefix the library name with “C” to indicate it’s a C library. Given that, a good name for this package would be “CWasm3”.

mkdir cwasm3
cd cwasm3
swift package init --name CWasm3 --type library

After creating the Swift package, it’s necessary to add the C source and header files to it. First, we delete Sources/CWasm3/CWasm3.swift, which was created by swift package init. We copy the C source files from Wasm3’s source directory into the Sources/CWasm3 directory. Then, we create the Sources/CWasm3/include directory to hold Wasm3’s header files, which is a C convention the Swift Package Manager follows. Consumers of the CWasm Swift package would only be able to call its functions if the header files were located inside of the include directory. So, we copy the C header files from Wasm3’s source directory into the newly-created include directory.

At this point, the library is ready. In order to ensure everything is working properly, we replace the testExample() in Tests/CWasm3Tests/CWasm3Tests.swift with the following:

func testCanCreateEnvironmentAndRuntime() throws {
	let environment = m3_NewEnvironment()
	defer { m3_FreeEnvironment(environment) }
	XCTAssertNotNil(environment)

	let runtime = m3_NewRuntime(environment, 512, nil)
	defer { m3_FreeRuntime(runtime) }
	XCTAssertNotNil(runtime)
}

Running the test using swift test passes, which means we were able to use the Wasm3 C library from Swift! However, we aren’t finished yet.

Accessing return values from WebAssembly functions

One of the biggest limitations of Wasm3 is the inability to retrieve return values from Wasm functions. Instead of returning values from Wasm functions, Wasm3 currently prints them to stderr—a design decision that clearly exposes Wasm3’s origin as a research project. The solution is straightforward. We need to replace the m3_CallWithArgs(IM3Function i_function, uint32_t i_argc, const char * const * i_argv) function with one that writes the return value to an “out parameter”. Instead of replacing the existing function, we write a separate one we call instead.

wasm3_CallWithArgs(
    IM3Function i_function,
    uint32_t i_argc,
    const char * const * i_argv,
    size_t *o_size,
    void *o_ret
)

o_size and o_ret are pointers to memory the caller allocates. wasm3_CallWithArgs() will save the size of the return value and the return value of the Wasm function itself to the provided memory addresses. The WebAssembly functions we write all have well-defined return types, which means the caller will always know how much memory to allocate for a given return value. In practice, we use the value of o_size to verify the size of the return value matches our expectations. If it doesn’t match, we throw an error.

After implementing this function, we are able to load WebAssembly modules and call their functions relatively easily, which we can verify by adding some tests to our test suite.

func testCanCallAndReceiveReturnValueFromAdd() throws {
	let environment = m3_NewEnvironment()
	defer { m3_FreeEnvironment(environment) }

	let runtime = m3_NewRuntime(environment, 512, nil)
	defer { m3_FreeRuntime(runtime) }

	var addBytes = try addWasm()
	defer { addBytes.removeAll() }

	var module: IM3Module?
	XCTAssertNil(m3_ParseModule(environment, &module, addBytes, UInt32(addBytes.count)))
	XCTAssertNil(m3_LoadModule(runtime, module))

	var addFunction: IM3Function?
	XCTAssertNil(m3_FindFunction(&addFunction, runtime, "add"))

	let result = ["3", "12345"].withCStrings { (args) -> Int32 in
		let size = UnsafeMutablePointer<Int>.allocate(capacity: 1)
		let output = UnsafeMutablePointer<Int32>.allocate(capacity: 1)
		XCTAssertNil(wasm3_CallWithArgs(addFunction, UInt32(2), args, size, output))
		XCTAssertEqual(MemoryLayout<Int32>.size, size.pointee)
		return output.pointee
	}

	XCTAssertEqual(12348, result)
}

CWasm3 has been open-sourced under the MIT license. If you want to add it to your own Swift project, you can add it as dependency to your Package.swift file:

let package = Package(
  dependencies: [
    .package(name: "CWasm3", url: "https://github.com/shareup/cwasm3.git", .upToNextMinor(from: "0.4.7"))
  ]
)

Next steps

It’s great to be able to use Wasm3 from Swift, but, as you can tell from the example above, calling even a simple addition function requires writing a lot of ugly, error-prone, boilerplate code. As it currently exists, it’s not suitable for any but the simplest use cases. The next step will be to wrap CWasm3 in a fully-native Swift library, hiding the ugly C-ness safely away behind a beautiful Swift interface. We’ll write about that challenge later…😄