Spinning with Swift

Swift is a great language for creating Spin applications. This tutorial walks through the process of installing SwiftWasm, building a simple Wagi app, and then running it in Spin. The Swift community has been working on a WebAssembly compiler with full WASI support. And here at Fermyon, we’re big fans of this community-led project. We’ll show you how easy it is to work with Swift and Spin.

Setting Up Swift for WebAssembly

The SwiftWasm project is developing the compiler and related toolchain and libraries for generating WebAssembly binaries for Swift applications. To use Swift with Spin, you’ll need to install and configure SwiftWasm.

If you have set up SwiftWasm correctly, then running the swift --version command should print out the appropriate SwiftWasm compiler name:

$ swift --version
SwiftWasm Swift version 5.6 (swiftlang-5.6.0)

SwiftWasm is evolving rapidly, and each new version supports more features and libraries. We’ve been impressed with the rapid pace of improvements.

This article assumes that you have already downloaded and installed Spin.

Creating a New Swift Program

With SwiftWasm installed, the next step is to write a simple Swift program. In the interest of full disclosure, while I’m a Swift fan, I am by no means a Swift expert. There are probably more elegant ways to write some of this Swift code.

Spin has a built-in template to generate Swift applications. We can run spin new to generate a new Swift HTTP application:

$ spin new http-swift hello-there
Project description: A simple Hello example in Swift
HTTP base: /
HTTP path: /...

If you haven’t already installed the Spin templates, you might need to run this command before running spin new: spin templates install --git https://github.com/fermyon/spin. For more details, check out the Spin quickstart.

This creates a directory named hello-there with some files pre-configured for us:

$ tree hello-there 
hello-there
├── main.swift
└── spin.toml

0 directories, 2 files

Changing into that directory and opening an editor for main.swift, we’ll see code that looks something like this:

import WASILibc

// Until all of ProcessInfo makes its way into SwiftWasm
func getEnvVar(key: String) -> Optional<String> {
    guard let rawValue = getenv(key) else {return Optional.none}
    return String(validatingUTF8: rawValue)
}

let server = getEnvVar(key: "SERVER_SOFTWARE") ?? "Unknown Server"
let message = """
content-type: text/plain

Hello from \(server)!
"""

print(message)

This code does a few things. Let’s take a look at the getEnvVar function and then look at the top-level code.

The getEnvVar function

This is a nice utility function generated for us. It wraps the processes of getting environment variables.

This Spin app will run our application in Wagi mode. Wagi defines a CGI-based method for invoking WebAssembly binaries in a cloud environment. It’s very easy to use. Anything written to STDOUT (e.g. a print function) will go to the browser. And information about the client request (such as HTTP header info) is stored in environment variables. Spin’s Wagi driver defines over a dozen environment variables. In a moment, we’ll use one. And later on in this article we will use another one.

The getEnvVar function takes a String value for a key (the name of the environment variable) and returns an Optional<String>. This covers the case where sometimes an environment variable cannot be fetched or properly decoded.

There is one thing to keep in mind with this function, though: Sometimes an environment variable will be defined, but will have an empty value. That is, the environment variable might look like this: KEY="". This function does not do anything special if the value is an empty string.

The Top-level Code

In addition to the getEnvVar function, the generated Swift program has some top-level code:

let server = getEnvVar(key: "SERVER_SOFTWARE") ?? "Unknown Server"
let message = """
content-type: text/plain

Hello from \(server)!
"""

print(message)

First, it sets server to be the value of the environment variable SERVER_SOFTWARE. If no such variable exists, server will be Unknown Server.

Next, the generated code constructs a simple message and then sends this message back to the client.

The message constant is a simple string template. The named parameter server is injected into that string. And then all we do is use print to send the body back to the client. (IN a Wagi application, using print will send the message back to the client.)

One thing to note about the message is that the first two lines do something special. content-type tells Spin what type of content is being returned. This, in turn, helps Spin construct the HTTP headers to send to the client. Since we are sending back data we want to be interpreted as plain text, we set content-type: text/plain.

Then there is a mandatory empty line, signaling to Spin that the header is done and everything that follows is content destined for the client.

Running the Code

We haven’t changed a line of code since running spin new, and we can already compile and test it. From the directory where the spin.toml and main.swift file are, we can run a command that will build the app and then start an HTTP service running the app:

$ spin build --up
Executing the build command for component hello-there: swiftc -target wasm32-unknown-wasi main.swift -o main.wasm
Successfully ran the build command for the Spin components.
Preparing Wasm modules is taking a few seconds...

Serving http://127.0.0.1:3000
Available Routes:
  hello-there: http://127.0.0.1:3000 (wildcard)

Now if we go to http://127.0.0.1:3000, we should see the following:

Browser showing “Hello from WAGI/1!”

The SERVER_SOFTWARE environment variable was set to WAGI/1 by Spin.

Now let’s change the application to a personal greeting. The greeting can be customized using HTTP query parameters, but it has a sensible default as well. And we’ll also switch the output from plain text to HTML.

Here’s the new code, still in main.swift:

import WASILibc

// Until all of ProcessInfo makes its way into SwiftWasm
func getEnvVar(key: String) -> Optional<String> {
    guard let rawValue = getenv(key) else {return Optional.none}
    return String(validatingUTF8: rawValue)
}

// Get the name from the QUERY_STRING environment variable.
// Use a default if the query string is empty.
func getName() -> String {
    let defaultName = "there"
    let rawName = getEnvVar(key: "QUERY_STRING") ?? defaultName
    return rawName == "" ? defaultName : rawName
}

// Print an HTML page with the greeting
func printHTML(name: String) {
    let message = """
        content-type: text/html

        <html>
        <body>
        <h1>Hello, \(name)!</h1>
        <p>It's nice to see you.</p>
        </body>
        </html>
        """

    print(message)
}

// Get the name, then send a response back to the client.
let name = getName()
printHTML(name: name)

There are two new functions in this program: getName and printHTML.

The getName function

The getName function tries to determine the name to insert into the greeting. If a name is set in the query parameters, it will use that name. Otherwise, it will use the default string there. We’ll see this in action in a bit.

When we want to find out what the query string is (the part of the URL that comes after ?), we can read the environment variable QUERY_STRING, which is defined by Wagi. Once more, we use the pre-generated function getEnvVar to read QUERY_STRING for us. And if that string comes back empty, we fall back to the default value. At the end of this function, we will either have the default value ("there") or the name the client provided.

Most often, query strings are composed of name /value pairs separated by ampersand (& characters). For example, a QUERY_STRING might be param1=val1&param2=val2. In our simple example, we’re just using a the entire query string to pass a name.

As an interesting aside, the query string parameters are also passed into a Spin Wagi app using the arguments array. So we could use the CommandLine object to get query info as well. In that case, the getName function would look like this:

func getName() -> String {
    let defaultName = "there"
    let rawName = CommandLine.arguments[1]
    return rawName == "" ? defaultName : rawName
}

Either route is fine. Since we have the handy getEnvVar function, we’ll stick to this method.

The printHTML function

The second function, printHTML, takes a name, generates a basic HTML page, and sends that page back to the browser.

// Print an HTML page with the greeting
func printHTML(name: String) {
    let message = """
        content-type: text/html

        <html>
        <body>
        <h1>Hello, \(name)!</h1>
        <p>It's nice to see you.</p>
        </body>
        </html>
        """

    print(message)
}

Most of this function body is spent declaring an HTML template. The named parameter name is injected into that string. And then all we do is use print to send the body back to the client.

In the generated code, the first line of the message set the content-type to text/plain. Since we are sending back data we want to be interpreted as HTML, we change this to content-type: text/html.

Calling our functions

Finally, at the bottom of the example.swift file, we have the top-level entry code for our program:

// Get the name, then send a response back to the client.
let name = getName()
printHTML(name: name)

This merely calls getName, then calls printPage with the name returned from getName.

Compiling and Running Our Code

We’ve finished writing the code. Now it’s time to recompile. Running spin build will compile the code for us. Behind the scenes, spin build is using the SwiftWasm version of swiftc to compile. If we peek at the spin.toml we can see the exact command it is running:

[component.build]
command = "swiftc -target wasm32-unknown-wasi main.swift -o main.wasm"

The —target wasm32-unknown-wasi tells the compiler to build a WebAssembly binary. When we run spin build, we’ll see the command echoed back in the output.

$ spin build     
Executing the build command for component hello-there: swiftc -target wasm32-unknown-wasi main.swift -o main.wasm
Successfully ran the build command for the Spin components.

Once we’ve run spin build, we’ll have a file named main.wasm.

At this point, we can debug our program on the command line using Wasmtime: wasmtime --env QUERY_STRING=Swift example.wasm . A Wagi application is just a regular program that can be run by any WASI-capable WebAssembly runner.

Now we can start up a server using spin up.

$ spin up   
Preparing Wasm modules is taking a few seconds...

Serving http://127.0.0.1:3000
Available Routes:
  hello-there: http://127.0.0.1:3000 (wildcard)

The above tells us that our hello-there component is now running at the specified URL. Pasting that URL into a web browser returns a page that looks like this:

Browser says “Hello, there!”

Note that this uses the default name (Hello, there!). But if we specify a name in the query string, we can see the results. For example, if we use the URL http://localhost:3000/?Swift, then we will see this:

Browser says “Hello, Swift!”

So there we have it! We’ve created a Swift Spin application.

Optimizing the Binary Size

One final thing is worth mentioning. When we compile the program above, small as it is, it is still 9.1M. That’s rather large.

The SwiftWasm runtime is bigger than, say, Rust’s or TinyGo’s. But we can definitely optimize. The Binaryen project includes a byte code optimizer called wasm-opt. We can use that to shrink down our binary:

$ ls -lh example.wasm
-rwxr-xr-x  1 technosophos  staff   9.1M Jul 12 10:28 main.wasm
$ wasm-opt -O main.wasm -o main.wasm        
$ ls -lh main.wasm                     
-rwxr-xr-x  1 technosophos  staff   4.0M Jul 12 10:28 main.wasm

Above, we’ve used the wasm-opt command to trim 4.1M of unused code from our Swift binary.

Conclusion

Thanks to the hard work of the SwiftWasm community, Swift is turning out to be an excellent language for WebAssembly development. In this article, we’ve created a simple Spin application in Swift, compiled it to WebAssembly with WASI, and then executed it as a Swift Wagi application.

Spin’s support for Swift is in its earliest stages, and we anticipate that it will improve over time.

Interested in learning more?

Get Updates