Tutorial: Type-safe APIs with Swift gRPC

Sergio Campamá
10 min readJul 29, 2018

--

In this tutorial we’ll learn how to build a Swift gRPC server and client from scratch, configuring an interactive development environment using Xcode and a simple deployment mechanism using Docker. We will build a basic Echo service, where the server will reply with the same content as sent by the client.

We’ll also go over some shortcomings of the Swift ecosystem encountered while developing this tutorial, and “hackish” solutions to these issues.

Technologies we’ll use:

Before we start, we’ll need the following dependencies already installed in our machines:

Project Scaffolding

To start off, we’ll create an empty Swift project called Echo, and we’ll add some files and folders where we will add code during the tutorial:

$ mkdir Echo
$ cd Echo
$ swift package init --type empty
$ mkdir -p Sources/EchoServer
$ mkdir -p Sources/EchoClient
$ mkdir -p Sources/Protos
$ touch Sources/EchoServer/main.swift
$ touch Sources/EchoClient/main.swift
$ find .
./.gitignore
./Package.swift
./README.md
./Sources
./Sources/EchoClient
./Sources/EchoServer
./Sources/Protos
./Tests

This will create a basic and empty project structure where we’ll set up our code. I chose to use the Swift Package Manager as it provides a simple interface for building command line applications without having to rely on Xcode, while still allowing an interactive development experience by generating an Xcode project.

Protocol Buffers and gRPC

As Google states:

Protocol buffers are a language-neutral, platform-neutral extensible mechanism for serializing structured data.

In practice, Protocol Buffers offer a shareable interface for exchanging structured data between different services. It is generally composed of two parts: a compiler that generates language bindings from.proto files, and a language support library to support Protocol Buffers entities at runtime.

gRPC, on the other hand, is an RPC framework used to connect services across the Internet. We’ll use gRPC as the transport mechanism for our Protocol Buffer messages between the server and the client. As Protocol Buffers, it also has a compiler part which generates gRPC endpoint stubs for different languages from .proto service definitions, and a language support library.

We’ll then add a service definition for our Echo service using the Protocol Buffers language under Echo/Sources/Protos/echo.proto:

// Echo/Sources/Protos/echo.proto
syntax = "proto3";
service EchoService {
rpc echo(EchoRequest) returns (EchoResponse);
}
message EchoRequest {
string contents = 1;
}
message EchoResponse {
string contents = 1;
}

In this file, we’re defining an EchoService RPC service, which has 1 endpoint named echo. This endpoint expects a message of type EchoRequest and it replies with a message of type EchoResponse. The definitions of EchoRequest and EchoResponse are below, and each define a string field where the request and response values will be populated, respectively.

To generate Swift Protocol Buffer and gRPC sources, we’ll use the Swift gRPC Docker image. The benefit of using Docker to generate the sources is that we don’t have to install any of the dependencies on our machines. We just take advantage of an image that is already configured for us, and it’s also easier to clean once we’re done with it. To generate the Swift code generators we’ll run¹:

$ cd Sources/Protos
$ docker run -i -t -v `pwd`:/working_dir -w /working_dir \
sergiocampama/swift_grpc \
protoc --swift_out=. --swiftgrpc_out=. \
echo.proto

That’s one scary command! Let’s explain it. docker run tells Docker that we want to run something using an image, in this case, one called sergiocampama/swift_grpc. One of the advantages of Docker is that it will download the image from Docker Cloud upon the first reference to it. The -i flag tells Docker to run in interactive mode by displaying the output of the command back to your terminal.

The -v `pwd`:/working_dir flag tells Docker that when running that image, we want to mount the current directory where we’re running the command (as returned by pwd) into a directory named /working_dir inside the Docker image. The -w /working_dir flag tells Docker that we want it to run inside the /working_dir directory. These two flags have the effect of making the files inside the current folder available to the Docker image under the /working_dir directory.

Finally we get to the actual Protocol Buffers generation command, which will be run inside the Docker image. protoc is the code generator engine. The --swift_out=. flag tells protoc that we want to generate Swift bindings in the current directory for the Protocol Buffers messages given, while --swiftgrpc_out=. requests Swift gRPC code to be generated in the current directory.

After running this command, two files will appear in Echo/Sources/Protos, echo.pb.swift and echo.grpc.swift. These will be the Protocol Buffers and gRPC bindings for the Echo service, respectively. We’ll compile both of these files into the server and client, so we’ll copy them into the Echo/Sources/EchoServer and Echo/Sources/EchoClient folders².

$ cp *.swift ../EchoServer
$ cp *.swift ../EchoClient
$ cd ../..

Implementing the business logic

Now that we have the Protocol Buffers and gRPC bindings generated, we can start implementing the server and the client. We’ll start by defining the EchoServer and EchoClient products in the package by modifying the Package.swift file to the following:

// Echo/Package.swift
// swift-tools-version:4.1
import PackageDescription
let package = Package(
name: "Echo",
products: [
.executable(
name: "EchoServer",
targets: ["EchoServer"]
),
.executable(
name: "EchoClient",
targets: ["EchoClient"]
),
],
dependencies: [
.package(
url: "https://github.com/grpc/grpc-swift.git",
from: "0.4.1"
),
],
targets: [
.target(
name: "EchoServer",
dependencies: [
"SwiftGRPC",
]
),
.target(
name: "EchoClient",
dependencies: [
"SwiftGRPC",
]
),
]
)

At this point, we can already start compiling our code. For each target, Swift Package Manager will look for sources under the Sources/<TargetName> directory, which for both EchoServer and EchoClient should already have the echo.pb.swift and echo.grpc.swift files:

$ swift build # First time takes a while as it compiles dependencies

Implementing EchoServer

But we haven’t done anything useful for now. Let’s implement the EchoServer logic by creating Echo/Sources/EchoServer/main.swift file with the following contents:

import Dispatch
import SwiftGRPC
class EchoProvider: EchoServiceProvider {
func echo(request: EchoRequest,
session: EchoServiceechoSession)
throws -> EchoResponse {
var response = EchoResponse()
response.contents = "You sent: \(request.contents)"
return response
}
}
let address = "0.0.0.0:9000"
print("Starting server in \(address)")
let server = ServiceServer(
address: address, serviceProviders: [EchoProvider()]
)
server.start()dispatchMain()

Let’s step through what the code is doing. Skipping over the imports, we first see an EchoProvider class, which implements the EchoServiceProvider protocol. This EchoServiceProvider protocol is defined in Echo/Sources/EchoServer/echo.grpc.swift file. The purpose of the EchoProvider class is to implement the logic behind the service EchoService definition in Echo/Sources/Protos/echo.proto. You can see that it only has one method, named echo (just like the rpc definition), and that it takes an EchoRequest message as input and returns an EchoResponse message, both defined in Echo/Sources/EchoServer/echo.pb.swift. Because this is an echo service, the response is equal to the request prepended with You sent:.

Then we define the address, which will be hardcoded to the local machine under port 9000, and create an instance of the ServiceServer class provided by SwiftGRPC with the address and an instance of the EchoProvider class. With this we’re configuring the server to respond to the EchoService.echo commands using the EchoProvider implementation.

Finally, we use a DispatchSemaphore to keep the server running. This has the effect of keeping the server alive while yielding the execution to the ServiceServer class whenever it gets a new connection.

We can run this server by running swift run EchoServer and you’ll see the Starting server in 0.0.0.0:9000 message. You can close the server with Ctrl + C.

Implementing EchoClient

Let’s implement a simple client that makes use of the EchoServer we just implemented. Add the following contents into Echo/Sources/EchoClient/main.swift:

let client = EchoServiceServiceClient(
address: "0.0.0.0:9000", secure: false
)
var request = EchoRequest()
request.contents = "Hello, world!"
let response = try client.echo(request)
print(response.contents)

First, we make an instance of the EchoServiceServiceClient class defined in Echo/Sources/EchoClient/echo.grpc.swift, then we create an EchoRequest, defined in Echo/Sources/EchoClient/echo.pb.swift and set Hello, world! as its contents. We finally invoke the echo endpoint in EchoService with the request, which returns an EchoResponse, and we print its contents.

Now, make sure the EchoServer is running on a terminal as described above, and on a different terminal navigate to the Echo project and run swift run EchoClient. After compiling EchoClient, it will run and if everything went ok, it should print You sent: Hello, world!. We’ve successfully ran a gRPC server and client connection!

Interactive Development

With what we’ve accomplished at this point we could start building more complex servers and clients, but with more logic comes more potential issues that we’d have to debug. Wouldn’t it be nice to use a rich development IDE with debugging support for implementing the client and the server?

Luckily for us, the Swift Package Manager has support for generating an Xcode project based on the Package.swift project definition³. We can generate and open this project in Xcode with:

$ swift package generate-xcodeproj
$ open Echo.xcodeproj

We’re now presented with something that should look like this:

Let’s add a breakpoint in the echo endpoint by clicking on the number 8 in the editor gutter, and then let’s run the EchoServer target by clicking on the Play button on the top left. Make sure that to the right of that button the EchoServer target is selected. Then, while the server is running, select the EchoClient target in the dropdown list, and click the Play button again. Xcode will then run the client code in parallel with the server. This should make the client generate a connection to the server, and hit the breakpoint:

With the code paused on the breakpoint, we can now inspect the contents of the request and further debug our code.

Deploying to the cloud

During the tutorial, we’ve been compiling and running the server and client exclusively on a macOS machine. But deployment doesn’t usually involve a macOS server but rather a Linux based one, like Ubuntu. So let’s compile the server into a Linux binary that we can deploy on the cloud⁴:

$ docker run -i -t -v `pwd`:/working_dir -w /working_dir \
swiftdocker/swift:4.1 \
swift build --product EchoServer \
--build-path .release \
--configuration release

Once again, we’ll use Docker to do this. The -i -t -v -w flags we’re already explained in the Protocol Buffers and gRPC section, but now we’re using the swiftdocker/swift:4.1 Docker image. This is an Ubuntu based image with the Swift toolchain already installed.

Then we run the swift build command with release configuration flags, and specify the output to be in .release. This will generate the EchoServer Linux binary in Echo/.release/x86_64-unknown-linux/release/EchoServer.

We can then package this binary into a Docker image by creating the Echo/Dockerfile file⁵:

FROM swift:4.1ADD Echo/.release/x86_64-unknown-linux/release/EchoServer /app/EchoServerCMD /app/EchoServer

Finally we’ll build this image using docker build -t $USER/echoserver. With this Docker image, you can now upload it to your preferred container registry and then use it from your cloud infrastructure.

Conclusions

At this point in the tutorial we’ve managed to:

  • create a server and client system that communicates via gRPC and ProtocolBuffers,
  • tested their functionality from the command line,
  • added breakpoint and debugging support for both client and server using Xcode,
  • compiled the server for Linux in the release configuration, ready to be deployed to the cloud.

All of this with no infrastructure dependencies other than Xcode and Docker.

It is important to note that the gRPC server presents a uniform interface to any gRPC connection. This means that the client code doesn’t necessarily need to be developed using Swift, and the server we’ve built can support any client language and platform also supported by gRPC (e.g. ObjC, Python, Java, Javascript, Android, iOS, Web).

Future work

One idea I’d like to explore more is statically linking the server binary to avoid needing the Swift runtime libraries installed in the host machine. We avoid this now by using the swift:4.1 Docker image as a default, but this limits us to where we can deploy our binary. Sadly, static linking of binaries for Linux is not quite ready for prime time yet, as there are some issues with the Swift toolchain for Linux.

Final words

This is my first time writing a public tutorial on building software. As such, my writing style or content could definitely be improved. Any feedback on the writing, content, or methodology described in this article would be very helpful for me to improve. Thanks.

Notes

  1. The Swift gRPC project does not offer an official Docker image configured for generating Swift Protocol Buffers and gRPC bindings, so I built and distributed my own image using their Dockerfile. I created a feature request on the grpc-swift project asking for one.
  2. Copying these files into each of the EchoServer and EchoClient source directories is a workaround to Swift Package Manager not supporting sharing the same source files code between different targets. For these cases SwiftPM forces developers to create an intermediate target and then depend on that target. Sadly, the Swift generated code for Protocol Buffers and gRPC doesn’t set the public modifiers on the classes, so they are not visible when linking against the intermediate target.
  3. I developed this tutorial using Xcode 10b3. The swift version included in this Xcode has a bug that won’t add the SWIFT_PACKAGE config in SWIFT_ACTIVE_COMPILATION_CONDITIONS ‘s debug setting. Because of this, building on the debug configuration will be broken if you’re using Xcode 10b3. This bug has already been fixed in upstream, so it’s likely that the final version of Xcode 10 will have this fix included.
  4. We use the swiftdocker/swift:4.1 image explicitly as the latest tag hasn’t been updated and uses an older version.
  5. I chose to build and package the EchoServer binary already compiled, as opposed to copying the sources in the Dockerfile and building the server inside of it, as I didn’t find appealing the idea of releasing a Docker image that contains the sources. By only packaging the output binaries, we avoid any errors that may lead to releasing your source code inside a Docker image.

--

--