Solod v0.2: Networking, new targets, friendlier interop
Anton Zhiyanov<br>projects<br>books<br>blog<br>about
Solod (So ) is a system-level language with Go syntax, zero runtime, and a familiar standard library. It's designed for two main audiences:<br>Go developers who want low-level control and zero-cost C interop without having to learn Zig or Odin.<br>C developers who like Go's style.<br>The previous version (v0.1) focused on porting core Go stdlib packages and providing convenient C interop. At the end of that post, I said the next release would focus on networking, concurrency, or both. Now, networking is here — the v0.2 release I'm sharing today includes support for TCP, UDP, and Unix domain sockets. Concurrency is still planned for the future, so for now, servers handle one connection at a time.<br>This release also lets you compile So to more targets, like 32-bit platforms, WebAssembly, and bare metal. And C interop even smoother!<br>Networking •<br>TCP server •<br>TCP client •<br>Deadlines •<br>IP addresses •<br>Targets •<br>Interop •<br>Stdlib •<br>Wrapping up<br>Networking<br>The main feature in v0.2 is the net package. It's a simplified version of Go's net package which supports the three most commonly used transports:<br>TCP (networks tcp, tcp4, tcp6) via ResolveTCPAddr, DialTCP, and ListenTCP, with the TCPConn and TCPListener types.<br>UDP (networks udp, udp4, udp6) via ResolveUDPAddr, DialUDP (a connected socket), and ListenUDP (an unconnected socket with ReadFrom/WriteTo).<br>Unix domain sockets (unix for streams, unixgram for datagrams) via ResolveUnixAddr, DialUnix, ListenUnix, and ListenUnixgram.<br>The API mirrors Go closely, so most of it will feel familiar. The big difference is that So has no goroutines, so there's no concurrent server support — you accept and serve connections sequentially. More on that in a moment.<br>TCP server<br>Let's build a classic: an echo server that accepts a connection, reads a message, and sends it back.<br>package main
import "solod.dev/so/net"
func main() {<br>// Resolve the local address to listen on.<br>laddr, err := net.ResolveTCPAddr("tcp", "127.0.0.1:8080")<br>if err != nil {<br>panic(err)
// Start listening on the local address.<br>ln, err := net.ListenTCP("tcp", &laddr)<br>if err != nil {<br>panic(err)<br>defer ln.Close()<br>println("listening on", "127.0.0.1:8080")
// Accept connections and serve them in a loop.<br>for {<br>conn, err := ln.Accept()<br>if err != nil {<br>panic(err)<br>serve(&conn)
// serve reads one message from the connection, echoes it back,<br>// and closes the connection.<br>func serve(conn *net.TCPConn) {<br>defer conn.Close()
var buf [256]byte<br>n, err := conn.Read(buf[:])<br>if err != nil {<br>return<br>conn.Write(buf[:n])
listening on 127.0.0.1:8080
If you've written a TCP server in Go, this should look familiar — ListenTCP, an Accept loop, and Read/Write on the connection. The only thing missing is a go serve(conn): without goroutines, each connection is handled to completion before moving on to the next Accept.<br>TCP client<br>The client starts the connection using DialTCP, then uses Write to send a request and Read to get the reply:<br>package main
import "solod.dev/so/net"
func main() {<br>// Resolve the server address.<br>raddr, err := net.ResolveTCPAddr("tcp", "127.0.0.1:8080")<br>if err != nil {<br>panic(err)
// A nil laddr lets the system choose the local address.<br>conn, err := net.DialTCP("tcp", nil, &raddr)<br>if err != nil {<br>panic(err)<br>defer conn.Close()
// Send a request and read the reply.<br>conn.Write([]byte("hello"))
var buf [256]byte<br>n, err := conn.Read(buf[:])<br>if err != nil {<br>panic(err)<br>println(string(buf[:n]))
hello
UDP and Unix domain sockets work in a similar way. For UDP, an unconnected ListenUDP socket uses ReadFrom to get data and the sender's address, and WriteTo to send a reply. For Unix sockets, there are ListenUnix (stream) and ListenUnixgram (datagram).<br>Deadlines<br>By default, Accept, Read, and Write are blocking. In Go, you'd typically use goroutines and contexts to prevent getting stuck forever. Since that's not available in So (yet), every connection and listener supports deadlines instead:<br>// Give the client 5 seconds to send something.<br>conn.SetReadDeadline(time.Now().Add(5 * time.Second))
n, err := conn.Read(buf[:])<br>if err == net.ErrTimeout {<br>// The client went quiet; drop the connection.<br>return
SetDeadline, SetReadDeadline, and SetWriteDeadline are available on TCPConn, UDPConn, UnixConn, and listener types. When the deadline passes, any pending call fails with net.ErrTimeout. If you don't set a deadline, a blocked call will wait forever. This isn't concurrency, but it's enough to keep a single-threaded server responsive.<br>IP addresses<br>Along with net, v0.2 ports Go's net/netip package, which provides small, allocation-free value types for IP addresses. Addr represents an IP address, AddrPort combines an IP address with a port, and Prefix is an IP with a prefix length (a CIDR block):<br>addr, err := netip.ParseAddr("192.168.1.10")<br>if err != nil {<br>panic(err)<br>println(addr.Is4()) // true
ap := netip.AddrPortFrom(addr, 8080)<br>println(ap.Port())...