HomeiOS DevelopmentSwift Cross Platform | Cocoanetics

Swift Cross Platform | Cocoanetics


My ardour for cross platform software program growth began across the 12 months 2000 after I noticed how a contractor at Austria’s Join Austria – which finally turned the mobile community supplier DREI – would create C++ utilities on PC with Visible Studio that then could possibly be compiled and run in manufacturing on Compaq Tru64 Unix. I’ve fond reminiscences of writing a number of small CLI utilities the identical strategy to resolve information points we encountered processing name file information for billing.

I all the time hated the strategy of emulating a digital machine to get code operating of various platforms. This feels to me like watching the trojans rejoice their massive wood horse and no person keen to hearken to me shouting warnings of what ugliness is perhaps inside.

Apple platforms and Linux share a typical Unix ancestry and so it was a straightforward addition to compile Swift for Linux as properly. Swift on Android and Home windows solely matured properly sufficient fairly lately in order that – with the assistance of coding brokers – we might get severe of concentrating on these platforms as properly.

Recently I’ve found a brand new ambition: I need all open supply Swift code of mine to run on the most range of platforms it could actually. That is probably as a result of GitHub grants uns OSS builders limitless CI runs and along with my Claude that may babysit a PR all by evaluation feedback and CI failures till all platforms go inexperienced. I saved getting the identical sorts of errors on these non-Apple platforms and each time my brokers constructed yet one more workaround, some hyper full, others very sloppy.

Apple’s Basis is beneficiant. It ships a pile of comfort API that the open-source swift-corelibs-foundation — the Basis you get on Linux, Home windows, and Android — merely doesn’t have. The second your code reaches for a kind of conveniences, it stops compiling the moment it leaves Cupertino:

// Effective on Apple. On Linux: "worth of sort 'URLSession' has no member 'bytes'"
let (bytes, response) = strive await URLSession.shared.bytes(for: request)
for strive await line in bytes.strains { … }

// Effective on Apple. On Linux: "can't discover sort 'UTType' in scope"
let mime = UTType(filenameExtension: "png")?.preferredMIMEType

The usual treatment is to scatter #if canImport(FoundationNetworking) by the file till the compiler stops complaining. In order that’s what I did. Then I did it once more within the subsequent undertaking. Someplace across the third copy of the identical AsyncBytes workaround, sitting in a 3rd unrelated repo, I had the sensation each programmer is aware of: that is dumb, why is that this not in a single place?

Pulling the shims into one place

So I lifted all of them out of the person tasks and into one package deal. I known as it SwiftCross — as in cross-platform — and gave it precisely two guidelines: it has no dependencies, and it should let the identical Swift supply compile and run on each platform the toolchain targets.

Utilizing it’s virtually aggressively boring, which is the entire level:

import SwiftCross   // as a substitute of: import Basis

import SwiftCross is a drop-in alternative for import Basis. It re-exports Basis (plus FoundationNetworking the place that’s a separate module, plus UniformTypeIdentifiers the place it exists) and layers the lacking items on prime. On a platform that already has the actual API, SwiftCross steps apart and palms you the native implementation. On one which doesn’t, you get the shim — and crucially, it’s the identical name web site both means. The #if dances don’t vanish; they simply all transfer inside SwiftCross, as soon as, as a substitute of being smeared throughout your individual code the place you need to learn previous them on daily basis.

A phrase on the identify

I’ve to confess the identify made me grin. Swift Cross Platform Utilities, briefly SwiftCross — say it out loud — sounds an terrible lot like Swiss Cross. And actually that matches higher than I’ve any proper to count on. Switzerland is the proverbial impartial floor, and that’s exactly what this package deal is supposed to be: impartial floor between Apple’s world and everybody else’s, a spot the place one physique of code is welcome on each platform.

So the brand turned the Swiss flag — the white cross on pink — besides the cross isn’t strong. It’s manufactured from Swift’s little swallow, a complete flock of them in formation. Swiss swallows. I’m far too happy with it.

The one I really care about: URLSession.bytes, for actual this time

The shim that set all of this in movement — and nonetheless the one I’d save first if the constructing had been on hearth — is async byte streaming on URLSession.

On Apple platforms you write this and also you’re achieved:

let (bytes, response) = strive await URLSession.shared.bytes(for: request)
for strive await line in bytes.strains {
    print(line)
}

What you get is a real incremental stream of the response, line by line, because it arrives over the wire. That’s the entire sport for one thing like Server-Despatched Occasions, the place the connection stays open and the server retains pushing tokens at you for so long as it likes. SwiftMCP speaks precisely this type of long-lived streaming HTTP.

swift-corelibs-foundation has none of it. No URLSession.bytes, no AsyncBytes, nothing. And right here’s the half I’m not happy with: prior to now, after I wanted it on Linux, I faked it. I’d obtain the complete physique, then flip round and hand it again chunk by chunk as if it had streamed. For a small JSON reply, no person can inform the distinction. For an SSE stream that’s supposed to remain open and ship occasions for minutes — probably eternally — “buffer the entire physique first, then begin” isn’t a shim. It’s a dangle with good manners.

SwiftCross does it correctly. It spins up a one-shot URLSession with an information delegate, forwards every didReceive(information:) chunk straight into an AsyncThrowingStream, and resolves the response object the second the headers land — not when the physique finishes. The session and process are owned by the stream’s termination handler, so the entire thing tears itself down the moment you cease studying, whether or not you ran to the pure finish or breaked out of the loop early. The .strains helper treats each n and rn as breaks and doesn’t conjure a phantom empty line out of a trailing newline — the identical fiddly little semantics Apple’s model has. It’s a actual stream, and SSE now behaves identically on Linux, Home windows, and Android because it does on my Mac.

The bugs you may solely meet on the platform

Right here’s the factor no person warns you about whenever you determine to help platforms you don’t run: the compiler errors are the simple half. The compiler errors are well mannered. They let you know precisely what’s lacking and also you go fill it in. The genuinely fascinating failures solely present up when the code really executes over there — and you discover these by studying a stack hint from a machine you’ll by no means log into.

That is the outer loop I’ve written about earlier than, simply pointed at portability as a substitute of options: a pink leg on some platform I can’t reproduce regionally, a speculation, a repair, one other push, and a sluggish hill-climb till each checkmark goes inexperienced. A couple of that left a mark:

The timeout that traps with SIGILL. swift-corelibs-foundation palms your URLSession timeout to libcurl by computing Int(timeout) * 1000. Completely innocent — till you do not forget that Int(.infinity) in Swift doesn’t return some giant quantity, it traps the method with SIGILL. So does Int(.nan). So does any finite timeout large enough to overflow Int as soon as it’s been multiplied by a thousand. A caller who’d set an effectively-infinite timeout — utterly high quality on Apple — would deliver the entire thing down on Linux. The repair clamps solely the values that may really lure, and lets each real looking length by untouched:

static func swiftCrossSafeTimeout(_ timeout: TimeInterval) -> TimeInterval {
    let maxSafe = TimeInterval(1 

A minute, per week, a 12 months — all preserved precisely, as a result of silently shortening somebody’s configured timeout would simply be a quieter, nastier bug. Solely the genuinely harmful values get pinned to the biggest interval that also survives the × 1000. There’s a bit check suite holding each halves of that promise down.

Android isn’t simply “Linux with a unique identify.” SwiftCross exposes ProcessInfo.localIPAddress — the machine’s personal routable IP, one thing Basis has no transportable API for in any respect. On Apple, Linux, and Android it walks the interface record with getifaddrs. Besides the Android leg fought me three separate occasions:

  • It’s a must to import Android — the actual platform overlay — not attain for the Bionic libc subset, or half the declarations you want merely aren’t there.
  • On that overlay, ifa_name arrives as a strict non-obligatory (it’s implicitly-unwrapped on Darwin and glibc), so the code that compiled in every single place else instantly wants an specific unwrap.
  • And getnameinfo desires its buffer size as size_t on Android however socklen_t on Darwin/glibc, so the size has to launder itself by numericCast to maintain each compilers joyful.

Home windows doesn’t have getifaddrs in any respect. So on Home windows the identical localIPAddress opens a UDP socket and “connects” it to a documentation handle — no packet is ever despatched; the join on a datagram socket exists purely to make the OS decide to a supply handle — after which reads that handle straight again with getsockname. It looks like a parlor trick. It’s the cleanest transportable means I discovered to ask the OS “in case you had been going to speak to the surface world, which of my addresses would you employ?”

Not a kind of was discoverable from a person web page on my Mac. Each certainly one of them got here out of a failing CI run.

The remainder of the gathering

A handful of different shims rode alongside, each certainly one of them extracted from a undertaking the place it had been quietly incomes its hold:

UTType, lifted from SwiftMail. A stand-in for UniformTypeIdentifiers’ UTType, masking the filename-extension ↔ MIME-type mapping that transportable code really reaches for. The massive lookup desk got here straight out of SwiftMail’s guts, with a curated layer of recent, most well-liked values sitting on prime of the broad one (so png resolves to picture/png and never some vintage picture/x-… spelling). On Apple the actual UTType is re-exported untouched, so its full UTI hierarchy — conformances, supertypes, the lot — stays proper the place you left it; the shim intentionally solely fashions the extension/MIME floor.

UTType(filenameExtension: "png")?.preferredMIMEType            // "picture/png"
UTType(mimeType: "utility/json")?.preferredFilenameExtension  // "json"

String.Encoding(ianaCharsetName:). Flip an IANA charset label — "utf-8", "ISO-8859-1", "windows-1252", "shift_jis" — right into a String.Encoding, with the normalization and alias-folding the actual world calls for. CoreFoundation ships a whole desk for this on Apple; in every single place else SwiftCross carries its personal. You cease noticing it’s there proper up till you’re decoding an e mail physique or an HTTP response whose charset header somebody typed by hand.

The half I can’t repair by myself

SwiftCross cleans up the Basis-shaped half of the issue, and that half I might resolve on my own. There’s one other half I can’t. SwiftMCP nonetheless can’t attain Home windows, and the blocker has nothing to do with Basis — it’s SwiftNIO. SwiftMCP leans on NIO for its networking, and NIO’s Home windows help has been a very long time coming. The day that lastly lands is the day the street to SwiftMCP-on-Home windows really opens. SwiftCross was the half inside my attain; NIO is the half I’m ready on, watching the tracker like everybody else.

Within the meantime although, an agent is refactoring the SwiftMCP package deal construction so that you simply don’t have to incorporate the MCP server portion – which is the one requiring SwiftNIO – in case you are solely doing client-side work. The concept is to make use of package deal traits that default to every thing, however you may restrict it to the consumer half in case you don’t want the server.

If that works out then it unlocks SwiftAgents for Home windows, as this new brokers package deal solely wants MCP consumer code and JSONValue for representing JSON schemas for LLM instrument calls. I’ve gotten fairly shut to really additionally pull out JSONValue right into a separate repo which might have solved this instantly, however there are another MCP components I’m utilizing in there so it felt not value the additional trouble at the moment.

Write as soon as for Apple, run in every single place Swift runs

That’s the complete philosophy, and it’s a small one: write the code you’d already write for Apple, change one import, and have it run in every single place Swift runs — with no #if dances cluttering the decision web site. SwiftCross is MIT-licensed, dependency-free, and each platform is constructed and examined on each push, as a result of the entire motive it exists is that I can’t verify these platforms some other means.

Now comes the satisfying half. I get to return by SwiftMCP, SwiftBash, SwiftScript, SwiftPorts and the remaining, delete each copy of that twenty-line shim I’ve been carrying round, and substitute the lot with a single import SwiftCross. 5 copies of a factor lastly collapsing into one — there isn’t a lot on this job that feels higher than that.

Talking of SwiftBash, SwiftPorts and ShellKit… there are fairly just a few extra system-level or shell-adjacent issues that in all probability may even find yourself in SwiftCross. Typically there are Posix-functions of various names doing the identical factor so a number of potential for additional additions.

This was simply step one to get the preliminary bulk unified. As I uncover extra holes in non-Apple frameworks, SwiftCross would be the place for the canonical gap filler. Go to the repo at github.com/Cocoanetics/SwiftCross


Classes: Updates

RELATED ARTICLES

LEAVE A REPLY

Please enter your comment!
Please enter your name here

- Advertisment -
Google search engine

Most Popular

Recent Comments