Write WebAssembly in Swift and use it in Swift App
Background
I've been developing a new app for a while, one of the coolest ideas is to let the user write their own script to extend the app's ability.
But what kind of scripting language should I support? Why not support them all? So the decision is to adapt WebAssembly which grants the user's flavor.
What's more, We will also use Swift to write wasm thanks to the great SwiftWasm project.
Communication
WebAssembly was designed to be a 32-bit sandbox VM, it's safe and isolated from our 64-bit Swift host app.
The only way to communicate with each other is to copy memory from the host app into the VM and read the processed memory from VM back later.
This's a big challenge, but we will overcome it step by step.
Reference
If you are not quite familiar with Swift pointer & memory layout, check out these talks.
Exploring Swift Memory Layout Size, Stride, Alignment Unsafe Swift: Using Pointers and Interacting With C
Setup SwiftWasm
1. Install swiftenv
We will use swiftenv to manage the toolchain, So please install it first.
2. Install swiftwasm
Since swiftwasm has not been merged into the repo, we gonna install it on our own.
Here is the Github release page of the toolchain.
3. Set swift env in project
Use swiftenv to check the version of your swiftwasm
swiftenv versions
Mine is wasm-5.6.0
at the time, so at the root of your project folder, run swiftenv local wasm-5.6.0
to set the project level swift env.
Basic WebAssembly App
Finished project can be found here https://github.com/kevinzhow/write-wasm-in-swift-demo
As we know before, the Host app can only communicate with WebAssembly VM through memory copy, with the help of protobuf, we can transport data between 32-bit wasm VM and 64-bit host app easily.
But we also need to implement a few functions to handle these.
- allocate memory with size and return memory pointer
- deallocate memory at the pointer
- function to do the real work with memory address and size.
Quick look
import Foundation
@_cdecl("allocate")
func allocate(size: Int) -> UnsafeMutableRawPointer {
return UnsafeMutableRawPointer.allocate(byteCount: size, alignment: MemoryLayout<UInt8>.alignment)
}
@_cdecl("deallocate")
func deallocate(pointer: UnsafeMutableRawPointer) {
pointer.deallocate()
}
@_cdecl("change_article_proto")
func changeBookProto(protoData: UnsafeMutableRawPointer, size: Int, newAuthor: UnsafeRawPointer, authorSize: Int, newSize: UnsafeMutablePointer<Int>) -> UnsafeRawPointer {
// Decode proto binary data
let data = Data(bytes: protoData, count: size)
var book = try! BookInfo(serializedData: data)
// Change author
book.author = String(data: Data(bytes: newAuthor, count: authorSize), encoding: .utf8)!
let newData = try! book.serializedData()
newSize.pointee = newData.count
// get the data pointer of the new book proto data
let pointer = newData.withUnsafeBytes{ (bufferRawBufferPointer) -> UnsafeRawPointer in
let bufferPointer: UnsafePointer<UInt8> = bufferRawBufferPointer.baseAddress!.assumingMemoryBound(to: UInt8.self)
return UnsafeRawPointer(bufferPointer)
}
return pointer
}
Now we can build our wasm with command
swift build --triple wasm32-unknown-wasi -c release -Xlinker --allow-undefined
We pass --allow-undefined
to make sure all @_cdecl
functions will be exported.
Then copy it out
cp .build/release/swiftwasm.wasm ./swiftwasm.wasm
Swift Host App
Finished Project can be found here https://github.com/kevinzhow/swiftwasm-host-app-demo
First, we implement a Wasm Module to handle the memory exchange and method call.
import Foundation
import WasmInterpreter
import SwiftProtobuf
public struct WasmModule {
private let _vm: WasmInterpreter
init() throws {
_vm = try WasmInterpreter(module: Bundle.module.url(forResource: "swiftwasm", withExtension: "wasm")!)
}
/// Allocate memory on heap
/// It returns byteoffset
func allocate(size: Int) throws -> Int {
return Int(try _vm.call("allocate", Int32(size)) as Int32)
}
func deallocate(byteOffset: Int) throws {
try _vm.call("deallocate", Int32(byteOffset))
}
/// Allocate size on heap
/// It returns byteoffset
func allocateSize() throws -> Int {
let length = MemoryLayout<Int32>.size
let newSizePointer = try! allocate(size: length)
return newSizePointer
}
/// Write string to heap
/// It returns byteoffset
func writeString(string: String) throws -> (Int, Int) {
let length = Data(string.utf8).count
let pointer = try! allocate(size: length)
try _vm.writeToHeap(string: string, byteOffset: pointer)
return (pointer, length)
}
/// Write Data to heap
/// It returns byteoffset
func writeData(data: Data) throws -> Int {
let length = data.count
let pointer = try! allocate(size: length)
try _vm.writeToHeap(data: data, byteOffset: pointer)
return pointer
}
/// Send Protobuf binary into
func changeBook(_ book: BookInfo, author: String) throws -> BookInfo {
let data = try! book.serializedData()
let (newAuthorPtr, newAuthorSize) = try! writeString(string: author)
let newSizePointer = try! allocateSize()
let dataPointer = try writeData(data: data)
let newArticlePointer = Int(try _vm.call("change_article_proto",
Int32(dataPointer),
Int32(data.count),
Int32(newAuthorPtr),
Int32(newAuthorSize),
Int32(newSizePointer)) as Int32)
let newSizeValue = Int(try _vm.valueFromHeap(byteOffset: newSizePointer) as Int32)
let newData = try _vm.dataFromHeap(byteOffset: newArticlePointer, length: newSizeValue)
let newBook = try! BookInfo(serializedData: newData)
try! deallocate(byteOffset: newAuthorPtr)
try! deallocate(byteOffset: newSizePointer)
try! deallocate(byteOffset: dataPointer)
try! deallocate(byteOffset: newArticlePointer)
return newBook
}
}
Finally, we can use it.
main.swift
import WasmInterpreter
print("Hello, world!")
let module = try! WasmModule()
var book = BookInfo()
book.id = 1
book.author = "Apple"
book.title = "Swift Programming"
let newBook = try! module.changeBook(book, author: "Apple Stuff")
print(newBook.author)