# 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: . ## 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 ```sh 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`. ```go 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.Raw` and the parsed views) and event `Tail` are valid only during the callback; `Event.Raw` is a copy. Copy what you keep. - **Callbacks.** They may queue requests, call `Expect` and `PrefetchXIDs`. They must not call `Dispatch`, `Sync`, `InternAtoms`, or `NewXID` when it needs a refill. - **Blocking.** `Dial` and `NewConn` block for at most 10 s during the handshake. Only `Sync`, `InternAtoms` and a `NewXID` refill block after that, for at most 5 s, and are meant for startup. Use `XIDsLow` and `PrefetchXIDs` to keep the event loop non-blocking. - **Errors.** A failed `Buf` is sticky: later requests return 0 and `Flush` returns 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`. ```sh 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 ```sh 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/`.