xturbo
A pure-Go X11 client runtime with generated bindings. The runtime speaks the
X11 wire protocol over a non-blocking Unix socket; the bindings come from
xcb-proto 1.17.0. It depends only on the standard library and
golang.org/x/sys, builds with CGO_ENABLED=0, and keeps the steady-state
request, reply, event and error paths free of allocations.
Module: bnema.dev/xturbo. Source: https://git.bnema.dev/xturbo.
Packages
| Package | Contents |
|---|---|
bnema.dev/xturbo |
Conn, Dial, Buf, Dispatch, Expect, Sync, InternAtoms, XID allocation |
xproto |
core protocol |
bigreq, ge, xcmisc |
BIG-REQUESTS, Generic Event, XC-MISC |
composite, xfixes, render, shape, res |
Composite, XFIXES, RENDER, SHAPE, X-Resource |
The root package never imports the generated packages.
Install
go get bnema.dev/xturbo
Example
Dial (or NewConn(fd) for an already-connected socket) performs the
handshake and negotiates the supported extensions. Requests are encoded into
the connection's Buf and return a Seq (0 on failure). Flush never
blocks, and Dispatch reads whatever the server sent and calls the Handler.
package xturbo_test
import (
"errors"
"log"
"golang.org/x/sys/unix"
"bnema.dev/xturbo"
"bnema.dev/xturbo/xproto"
)
// Replies registered with Expect carry this kind (16 and above are free).
const kindGeometry uint16 = 16
func Example() {
c, err := xturbo.Dial("") // "" means $DISPLAY
if err != nil {
log.Fatal(err)
}
defer c.Close()
// Startup: blocking calls, bounded by a 5 s deadline.
var atoms [1]uint32
if err := c.InternAtoms([]string{"WM_DELETE_WINDOW"}, false, atoms[:]); err != nil {
log.Fatal(err)
}
// Queue requests; they return a Seq (0 on failure, see c.Buf().Err()).
b := c.Buf()
win, err := c.NewXID()
if err != nil {
log.Fatal(err)
}
var v xproto.CreateWindowValues
v.SetEventMask(xproto.EventMaskExposure | xproto.EventMaskStructureNotify)
xproto.CreateWindow(b, 0, xproto.Window(win), xproto.Window(c.Root()),
0, 0, 640, 480, 0, xproto.WindowClassInputOutput, 0, &v)
xproto.MapWindow(b, xproto.Window(win))
if seq := xproto.GetGeometry(b, xproto.Drawable(win)); seq != 0 {
if err := c.Expect(seq, kindGeometry, win, 0, 0, 0); err != nil {
log.Fatal(err)
}
}
// Callbacks run inside Dispatch. Events, replies and errors are borrowed:
// copy what you keep.
h := &xturbo.Handler{
OnEvent: func(ev *xturbo.Event) {
switch ev.Kind {
case xproto.KindExpose:
e := xproto.ExposeOf(ev)
log.Printf("expose %dx%d", e.Width(), e.Height())
}
},
OnReply: func(r *xturbo.Reply) {
if r.Kind == kindGeometry {
if g, ok := xproto.ParseGetGeometryReply(r.Raw); ok {
log.Printf("window %#x is %dx%d", r.Args[0], g.Width(), g.Height())
}
}
},
OnError: func(e *xturbo.XError) { log.Print(e) },
}
for {
// Flush never blocks. On ErrWouldBlock, also wait for POLLOUT.
events := int16(unix.POLLIN)
if err := c.Flush(); errors.Is(err, xturbo.ErrWouldBlock) {
events |= unix.POLLOUT
} else if err != nil {
log.Fatal(err)
}
fds := []unix.PollFd{{Fd: int32(c.Fd()), Events: events}}
if _, err := unix.Poll(fds, -1); err != nil && err != unix.EINTR {
log.Fatal(err)
}
if err := c.Dispatch(h); err != nil { // io.EOF: server closed
log.Fatal(err)
}
// Keep the XID pool topped up without blocking later.
if c.XIDsLow() {
_ = c.PrefetchXIDs()
}
}
}
Rules
- Lifetimes. A request's body slice is valid only until the next request is
encoded. Reply views (
Reply.Rawand the parsed views) and eventTailare valid only during the callback;Event.Rawis a copy. Copy what you keep. - Callbacks. They may queue requests, call
ExpectandPrefetchXIDs. They must not callDispatch,Sync,InternAtoms, orNewXIDwhen it needs a refill. - Blocking.
DialandNewConnblock for at most 10 s during the handshake. OnlySync,InternAtomsand aNewXIDrefill block after that, for at most 5 s, and are meant for startup. UseXIDsLowandPrefetchXIDsto keep the event loop non-blocking. - Errors. A failed
Bufis sticky: later requests return 0 andFlushreturns the error.
Performance
Measured with make bench on an AMD Ryzen 9 7900X3D (Go 1.27, CGO_ENABLED=0).
Every benchmark reports 0 allocations per operation.
| Benchmark | Result |
|---|---|
Encode a request (MapWindow) |
6.2 ns |
| Encode and flush 64 requests over a socketpair | 1.55 µs (about 24 ns per request) |
| Decode 10k events | 124 µs (about 12 ns per event) |
| Dispatch 10k mixed packets | 251 µs (about 25 ns per packet) |
Parse a GetProperty reply view |
1.9 ns |
Parse a QueryTree reply view |
3.2 ns |
Regenerating bindings
Generated code lives in the extension packages and xproto; do not edit it.
The inputs are the pinned xcb-proto 1.17.0 files in spec/, listed with
checksums in spec/manifest.json.
make gen # go generate ./... (cmd/xtgen)
make gen-check # regenerate and fail on any difference
go run ./cmd/xtsync # refresh spec/ from upstream and update the manifest
Tests
make check # vet, staticcheck, tests, race, allocation, poison, gen-check
make xserver # tests against a real Xvfb (needs xvfb and xtrace)
make fuzz # root targets for FUZZTIME (60s), generated ones for GENFUZZTIME (10s)
make bench # benchmarks
License
MIT, see LICENSE. The xcb-proto XML and its license are under LICENSES/.