flak rss random

an aborted experiment with server swift

I wanted to write a fun little experimental ActivityPub server. I have a solid idea how to handle this in go, or at least I think I do, so that’d be pretty boring. Instead, let’s try a new technology.

I settled on swift, a general-purpose programming language built using a modern approach to safety, performance, and software design patterns, to create the best available language for uses ranging from systems programming, to mobile and desktop apps, scaling up to cloud services.

This worked until it didn’t. Here are some incomplete notes on the experience, and some extrapolated thoughts on how things may have gone.

intro

Swift seems like a nice language to program in. I don’t think many people think of it as their first choice for a project running on linux, but that’s what made it interesting. I do like that it’s compiled, supposedly runs efficiently, and takes a modern approach to modularity.

ActivityPub requires a web server that parses incoming json requests, and sends out its own requests. There are some libraries available for swift to make all this possible, so I should just plug it all together, and then write some logic for request handling.

To get started, I used vapor, the future of web development.

first build

I’ve got the hello world web app created with vapor new. Time to run swift build. This goes on forever. Then it stops.

I’ve lost the original error message, but it was because the c++ <memory> header couldn’t be found compiling some BoringSSL code. This was my fault, for installing the g++ package, but not the g++-dev package or whatever, but it raises the bigger question of why are we compiling BoringSSL in the first place?

Swift was sold to me as a safe language. But I think it would be more accurate to describe it as a thin veneer of safety over a deep pit of peril. The linux version of swift replicates a large number of native mac frameworks using whatever mix of C and C++ gets the job done. (On mac, these frameworks are also written in c++ or obj-c or something, by Apple, who may be slowly rewriting them in swift, but certainly isn’t done.)

This is troubling because I have no idea what version of BoringSSL is included here, or what its update policy is. I can imagine that vapor also includes a middleware layer that automagically transcodes images using a broken libwebp. I don’t think it does, but it’s the future of web development, so who even knows. Regardless of specifics, there’s a large iceberg of code here, that could be outdated or vulnerable.

Spot check: vapor updated async-http-client on 2023-07-28 from 1.10.0 to 1.18.0. A review of async-http-client releases shows 1.10.0 was released 2022-04-27 and 1.18.0 on 2023-05-22, and there’s a few concerning issues in the intervening releases.

Back to building hello, I installed the necessary headers, and finish. A complete build takes about five minutes on my laptop. This is not one time, but actually recurs, but we’ll get to that. Incremental builds of small changes are pretty fast, a few seconds.

real    4m44.354s
user    22m35.414s
sys     0m55.632s

package.swift

The weirdest thing about swift is it records dependencies in a file called Package.swift. And then swift build interprets this file somehow? Cool, but also janky? I’ve seen people do this with lua (which looks a lot like json), but swift is kinda syntax heavy for this purpose.

    targets: [
        .executableTarget(
            name: "App",
            dependencies: [
                .product(name: "Leaf", package: "leaf"),
                .product(name: "Vapor", package: "vapor")
            ]
        ),

Another issue I did not quite sort out, simply due to unfamiliarity, is how this file relates to import statements in my code. Like there’s an equivalence to CFLAGS and LDFLAGS, but it’s not what I expected, and I was quite surprised by a few things.

sometimes super reliable

I’ve got my hello world app built, run it a few times, make some changes to have it say hello tedu, the usual kicking the tires. Seems quite reliable, but I notice that sometimes it’s super reliable. As in, hitting ^C doesn’t kill the process about one time in ten. The server keeps running, keeps serving requests. It’s an immortal web juggernaut. The only way to stop it is kill -9.

A hard to kill web server sounds like a feature, but sometimes we can have too much of a good thing. During development, I’d actually like the server to be easy to kill and restart.

The fact that this occurs only one time in ten makes me think it’s a bug, and not by design. (Unless it’s buggy 90% of the time.) I did not investigate further because signal races are not my idea of a good time. Not a good omen, either.

blast from the past

I started with vapor, but the project I had in mind needed some lower level access to http requests, so I went searching for an example server that used SwiftNIO. Start integrating some code and immediately get compiler errors.

     .childChannelInitializer { channel in
-        channel.pipeline.configureHTTPServerPipeline().then {
+        channel.pipeline.configureHTTPServerPipeline().flatMap { () in
             channel.pipeline.addHandler(myHandler)
         }
     }

Why? I think it’s weird that swift programmers consider this an improvement in legibility, and even more that this is such an improvement it’s worth making a breaking change for.

There are dozens of such little changes required, and I guess if you’ve been following swift from day one, you’re on top of such things, but as somebody new to swift, having to speedrun the whole experience represents an unfriendly learning curve. The NIO library is complicated enough I don’t think I would have figured out how to wire it up based solely on reading the documentation, hence the reliance on found samples.

Whenever a new language or framework comes out, people rush to try it, and github and stackoverflow and everywhere else is immediately filled with code samples that work with 1.0. But none of that info gets garbage collected when it becomes outdated, and people write fewer examples for new code, and the new samples have less link juice, with the result that the answers you seek are not the answers you find.

everything everywhere

Here’s another example that was pretty simple but taught me another quirk about swift.

     func handlerAdded(ctx: ChannelHandlerContext) {
         self.buffer = ctx.channel.allocator.buffer(capacity: 128)
-        self.buffer.write(staticString: "it works!")
+        self.buffer.writeStaticString("it works!")
     }

Yeah, sure, explicit method names are better than one method with many parameters. But now look at the compiler error.

error: incorrect argument label in call (have 'staticstring:', expected 'http2ErrorCode:')
        self.buffer.write(staticstring: "it works!")
                         ^~~~~~~~~~~~~
                          http2ErrorCode

http2 what? Nowhere in my code was I doing anything with http/2. Not intentionally.

Turns out that swift allows adding extension methods to classes in other modules. Okay, cool feature if you ask for it, but confusing if you haven’t. Unlike dynamic languages such as ruby, where monkey patched methods are of course globally visible, swift is statically compiled. The compiler should know which extensions I’ve asked for and which I haven’t. Long ago, I worked on a complicated c++ code base with tons of operator overloads, and you can get some pretty surprising compiler errors if you have operator, in scope. The solution is you put the damn thing in a separate header, and then it doesn’t show up except in the code that asks for it.

You can see the implementation of this extension in the source for HTTP2ErrorCode. But it appears in neither the documentation for HTTP2ErrorCode nor the documentation for ByteBuffer. I’m not sure how I would discover this method in the event that I did want to use it. I’ve been told the documentation simply needs to be rebuilt with the new version of DocC, but at the time of writing, that has not happened.

I do find it amusing that the http/1 team felt the need to clean up these write methods, but the http/2 team didn’t get the memo or felt it wasn’t worth the bother. I’m not sure how this scales in a larger project. You use a component, they remove or rename a method, so then you just add it back? And bizarrely, due to the way extensions become globally imported, it may be some invisible dependency you’re using, leaving you unaware you’re using an obsolete method. This seems very likely to lead to chaos.

builds

After building your hello world app, you get a build directory full of artifacts. This is for a web app that prints hello on port 8080.

176M    .build/repositories
355M    .build/x86_64-unknown-linux-gnu/debug
250M    .build/x86_64-unknown-linux-gnu/release
924M    total

These are not portable, however. As in, you cannot relocate them. Or copy them. Should you dare to do so, the compiler will vomit.

error: PCH was compiled with module cache path '/home/tedu/proj/swello/.build/x86_64-unknown-linux-gnu/debug/ModuleCache/2U5BFZYJRSGKH', but the path is currently '/home/tedu/proj/swift/swello/.build/x86_64-unknown-linux-gnu/debug/ModuleCache/2U5BFZYJRSGKH'

I am a bad developer who likes to hold things wrong, so the way I develop features is usually to create proj-feature1 and proj-feature2 directories. This is easier for my tiny brain to reason about than smashing all the features into branches in a single directory. And in the situation where I’m developing a server that talks to other servers, it even lets me see what happens when new code talks to old code. But this requires a full rebuild, which takes long enough rebuilding all that BoringSSL code that it would be a big help to also copy the build directory. Alas, that is forbidden.

I once heard a rumor that there is a secret flag to let swift share build directories, but I was unable to find existence of such, so it may just be an urban legend.

illegal instruction

You know what else is illegal? The instructions generated by the swift compiler.

I made a fairly small change, to change a get route to a post route, and upon hitting that route with curl was greeted with a special treat.

Illegal instruction (core dumped)

This is where my swift journey ends. I’m not debugging this. I’m not working around it. I’m giving up.

I cannot rule out the possibility that this is somehow my fault, since I don’t know swift that well, or at all really, so maybe this is just what happens when you forget to call await or something like that. But if that’s the case, this is an unfriendly failure mode for a supposedly modern safe language.

language

Anachronistic ecosystem aside, working with swift was fairly pleasant. I wrote some code in a fairly obvious way, the compiler compiled it, and then it ran as expected (usually). Once I figured out the correct names of the methods I wanted, the compiler was not overly fussy. I think I would have been happy to advance to the level of competent swift developer.

I got a lot done without spending all my time worrying about error handling, although I have the sinking suspicion that’s because the code doesn’t handle errors, not that the magic error elves are doing it for me. Memory management seemed easy and complete, as advertised. I did not make it far enough to assess how terrifying it is to debug an asynchronous concurrency bug.

I’m pretty sure one of the swift designers was scarred by working on a huge java swing application in a past life, because the language puts a lot of emphasis on making it very easy to call little anonymous functions. I too remember the dark days of new ActionListener. Mixed thoughts on this. It helps reduce clutter, and I think it’s a great way to add a little bit of logic to constructors, etc. without making them super complicated. Except we have all this async code in the mix. I can’t tell what’s a for loop and what’s a callback, and what’s going to run now and what’s going to run later. In your own code, I’m sure you learn what’s what, but reading a new code base will require a lot of documentation consultation.

thoughts

I certainly can’t recommend swift based on my experience. I don’t know if I can recommend against it based on such a short trial. Several of my frustrations were definitely newb pains, and may not have even been worthy of mention given more time.

My concern using it for any other project would be the big iceberg of code that’s not swift. That doesn’t appear to be going away anytime soon.

The combination of the signal handling and illegal instruction is also more than a little problematic. If you wanted to make a serious try at swift, you’d want a plan to deal with that. The vapor website is very pretty, so I’m sure somebody has used swift to do something fun at some point. That somebody just wasn’t me.

I wonder if Dan Luu has ever tried swift.

Posted 28 Sep 2023 06:23 by tedu Updated: 28 Sep 2023 06:23
Tagged: programming