I'm trying to use Swift for a side-project, since it kinda fits for it:
I want a UI on macOS, but some CLI for Linux, ideally using same codebase.
Kinda interested "Swift-on-Server" since I like that approach since it's handy to use same language on front-end and back-end & my background in mostly server-side things... Thus I'm actually not very good at Swift - since I avoid UIs generally β but want to learn it better since I like the closures and pattern matching. Plus I think Musl support running on Alpine Linux is pretty nifty concept, since that's a common backend these days.
One need for my little experiment-in-swift project is to listen for BSD sockets for a router's UDP broadcast message (i.e. unfortunately not everything use nDNS for device discovery). I got socket stuff working for this, or at least enough Swift compiled and can process/print parsed packets, using Static Linux SDK on X86 Alpine VM in UTM (in Virtualization Framework mode for fun).
I did run into some minor roadblocks, so I'll share since there really wasn't a lot of info on using Swift on Linux, in case it helps others:
- I use macOS, and perhaps if you're a pro at the xc* commands, it might be possible to compile Musl Swift. I'm not, and gave up with using Xcode and macOS Terminal for it. Instead, used a clean Ubuntu Desktop in VM with VSCode, and followed the swift.org's guide. Everything just worked for dealing with Swift on Linux, without involving Xcode.
- While I wrote the BSD code mainly in Xcode, I'll offer the sourcekit-LSP seem to do it's thing close enough to Xcode (and since I'm more familiar with VSCode, handy), and was one par win my tests (i.e. me thinking: maybe SourceKit-LSP is wrong, try in Xcode, to get the same error - and my problem ;) )
- And Sourcekit-LSP is the only way to look the generated "C" headers used by Swift, that was important trick to get the socket code to compile on Linux and Musl. This was critical for figuring out what a in_addr and in6_addr look like to Linux Swift. To see Musl, I needed to add this to
.sourcekit-lsp/config.json
:
{
"swiftPM": {
"swiftSDK": "x86_64-swift-linux-musl"
}
}
- I had to use a more complex #ifdef to deal with Musl than swift.org shows, since Musl is Linux...:
#if os(macOS) || os(iOS)
import Darwin
import Network
#else
// otherwise Musl ... even if Linux
#if canImport(Musl)
import Musl
// now try Glibc
#elseif os(Linux)
import Glibc
#elseif os(Windows)
import ucrt
#endif
// could error, but let build fail to know where
#endif
- The biggest struggle with the code was deal with IP addresses. IMO, the Networking's IPv6Address and IPv4Address structs lack support for even taking Darwin socket things. And, worse in my case, the UDP contains an IP address, so part of the packet's Data() is some [UInt8] with either IPv4 or IPv6 - the Swift IPAddress don't take [UInt8]. Basically the Networking Framework's IPAddress protocol fails to deal with either in[6]_addr or bigEndian arrays. And this one that leads to some questions...
Docs on Linux support are few. Maybe someone knows these things.
- It wasn't 100% clear, but I believe the Networking Framework is not supported on Linux and Static/Musl Linux Swift. But this is why I choose using sockets... So if I'm wrong β please let me know.
- Xcode and sourcekit-LSP both report that String(cString:) is deprecated, but my reading of Apple docs (init(cString:) | Apple Developer Documentation) is that it's only String.init(copyNoByte:) that's deprecated β but both Xcode and sourcekit-LSP disagree . This one is handy for deal with socket data... specifically from inet_ntop(), sockaddr_in6, etc. I'm dealing with warning since using Encoder isn't just one call to do that conversion, unless I'm missing something but Google turned up this, which seem inelegant to replace String(cString:):
let validbuffer = buffer.prefix { $0 != 0 }.map {
UInt8(bitPattern: $0)
}
return String(decoding: validbuffer, as: UTF8.self)
- On these same IPAddress things, basic Swift mem mgmt question, does this need to be wrapped in some withUnsafe*() thing, from a C/C++ POV everything is on stack, but I don't know ARC/etc specifics:
static func getIPv4StringFromBytes(_ data: Data) -> String {
var bytes = [UInt8](data)
var buffer = [CChar](repeating: 0, count: Int(INET_ADDRSTRLEN))
guard
inet_ntop(AF_INET, &bytes, &buffer, socklen_t(INET_ADDRSTRLEN))
!= nil
else { return "" }
return String(cString: buffer)
}
- For IPv6, I really couldn't come up with better than #ifdef's to get the bytes into the need in6_addr type. But I have no clue why such a complex β and different one β is generated. Maybe I'm missing something, but there has to be easier way to get Data() into in[6]_addr types...
static func getIPv6StringFromBytes(_ data: Data) -> String {
var bytes = [UInt8](data)
if (data.count != 16) {
print("IPV6","wrong len",data.count,"bytes",bytes)
return ""
}
var buffer = [CChar](repeating: 0, count: Int(INET6_ADDRSTRLEN))
var addr: in6_addr
#if os(macOS) || os(iOS) || os(tvOS) || os(watchOS)
addr = in6_addr(
__u6_addr: in6_addr.__Unnamed_union___u6_addr(
__u6_addr8: (
bytes[0], bytes[1], bytes[2], bytes[3],
bytes[4], bytes[5], bytes[6], bytes[7],
bytes[8], bytes[9], bytes[10], bytes[11],
bytes[12], bytes[13], bytes[14], bytes[15]
)))
#elseif canImport(Musl)
addr = in6_addr(
__in6_union: in6_addr.__Unnamed_union___in6_union(__s6_addr: (
bytes[0], bytes[1], bytes[2], bytes[3],
bytes[4], bytes[5], bytes[6], bytes[7],
bytes[8], bytes[9], bytes[10], bytes[11],
bytes[12], bytes[13], bytes[14], bytes[15]
)))
#else // Linux and other platforms
addr = in6_addr(
__in6_u: in6_addr.__Unnamed_union___in6_u(__u6_addr8: (
bytes[0], bytes[1], bytes[2], bytes[3],
bytes[4], bytes[5], bytes[6], bytes[7],
bytes[8], bytes[9], bytes[10], bytes[11],
bytes[12], bytes[13], bytes[14], bytes[15]
)))
#endif
guard
inet_ntop(AF_INET6, &addr, &buffer, socklen_t(INET6_ADDRSTRLEN))
!= nil
else {
return ""
}
// TODO: cannot believe this is right way
// but using return String(cString: buffer) gives warning
let validbuffer = buffer.prefix { $0 != 0 }.map {
UInt8(bitPattern: $0)
}
return String(decoding: validbuffer, as: UTF8.self)
}
- I was trying to do this without any package - to better learn Swift - but if there is some good cross-platform wrapper for IPAddress things. Or more general cross-platform nuances, LMK β I'm sure in_addr won't be the first...
I'll note all of the above works on macOS, Linux, and using Static Linux SDK --sdk option. It just kinda ugly. And took the most time in all the socket stuff to figure out.
If anyone had better approach than above, I'd be curious to hear.