README with usage example and rules, compile-checked example_test.go, make fuzz target and scheduled fuzz workflow.
#7 chore(release): prepare v0.1.0 (README, example, fuzz CI)
A .github/workflows/fuzz.yml +17 -0
| @@ -0,0 +1,17 @@ | |||
| 1 | + | name: fuzz | |
| 2 | + | on: | |
| 3 | + | push: | |
| 4 | + | branches: [main] | |
| 5 | + | workflow_dispatch: | |
| 6 | + | permissions: | |
| 7 | + | contents: read | |
| 8 | + | jobs: | |
| 9 | + | fuzz: | |
| 10 | + | runs-on: ubuntu-latest | |
| 11 | + | timeout-minutes: 60 | |
| 12 | + | steps: | |
| 13 | + | - uses: actions/checkout@v4 | |
| 14 | + | - uses: actions/setup-go@v5 | |
| 15 | + | with: | |
| 16 | + | go-version-file: go.mod | |
| 17 | + | - run: make fuzz FUZZTIME=60s GENFUZZTIME=20s | |
| @@ -0,0 +1,17 @@ | |||||
| 1 | + | name: fuzz | |||
| 2 | + | on: | |||
| 3 | + | push: | |||
| 4 | + | branches: [main] | |||
| 5 | + | workflow_dispatch: | |||
| 6 | + | permissions: | |||
| 7 | + | contents: read | |||
| 8 | + | jobs: | |||
| 9 | + | fuzz: | |||
| 10 | + | runs-on: ubuntu-latest | |||
| 11 | + | timeout-minutes: 60 | |||
| 12 | + | steps: | |||
| 13 | + | - uses: actions/checkout@v4 | |||
| 14 | + | - uses: actions/setup-go@v5 | |||
| 15 | + | with: | |||
| 16 | + | go-version-file: go.mod | |||
| 17 | + | - run: make fuzz FUZZTIME=60s GENFUZZTIME=20s | |||
M Makefile +13 -1
| @@ -1,7 +1,9 @@ | |||
| 1 | 1 | export CGO_ENABLED := 0 | |
| 2 | 2 | STATICCHECK ?= staticcheck | |
| 3 | + | FUZZTIME ?= 60s | |
| 4 | + | GENFUZZTIME ?= 10s | |
| 3 | 5 | ||
| 4 | - | .PHONY: gen gen-check test race alloc poison bench xserver staticcheck vet check | |
| 6 | + | .PHONY: gen gen-check test race alloc poison bench fuzz xserver staticcheck vet check | |
| 5 | 7 | ||
| 6 | 8 | gen: | |
| 7 | 9 | go generate ./... | |
| @@ -18,6 +20,16 @@ poison: | |||
| 18 | 20 | go test -tags xturbo_poison ./... | |
| 19 | 21 | bench: | |
| 20 | 22 | go test -bench . -benchmem ./... | |
| 23 | + | # Root targets (dispatch, event decode, reply ring) get FUZZTIME; generated | |
| 24 | + | # packages and the generator get GENFUZZTIME. | |
| 25 | + | fuzz: | |
| 26 | + | @set -e; root=$$(go list .); for pkg in $$(go list ./...); do \ | |
| 27 | + | time=$(GENFUZZTIME); [ "$$pkg" = "$$root" ] && time=$(FUZZTIME); \ | |
| 28 | + | for t in $$(go test -list 'Fuzz.*' $$pkg | grep '^Fuzz'); do \ | |
| 29 | + | echo "== $$pkg $$t ($$time)"; \ | |
| 30 | + | go test -run '^$$' -fuzz "^$$t$$" -fuzztime $$time $$pkg; \ | |
| 31 | + | done; \ | |
| 32 | + | done | |
| 21 | 33 | xserver: | |
| 22 | 34 | PATH="$$PATH:/usr/sbin" go test -tags xserver ./... | |
| 23 | 35 | staticcheck: | |
| @@ -1,7 +1,9 @@ | |||||
| 1 | export CGO_ENABLED := 0 | 1 | export CGO_ENABLED := 0 | ||
| 2 | STATICCHECK ?= staticcheck | 2 | STATICCHECK ?= staticcheck | ||
| 3 | + | FUZZTIME ?= 60s | |||
| 4 | + | GENFUZZTIME ?= 10s | |||
| 3 | 5 | ||||
| 4 | - | .PHONY: gen gen-check test race alloc poison bench xserver staticcheck vet check | 6 | + | .PHONY: gen gen-check test race alloc poison bench fuzz xserver staticcheck vet check |
| 5 | 7 | ||||
| 6 | gen: | 8 | gen: | ||
| 7 | go generate ./... | 9 | go generate ./... | ||
| @@ -18,6 +20,16 @@ poison: | |||||
| 18 | go test -tags xturbo_poison ./... | 20 | go test -tags xturbo_poison ./... | ||
| 19 | bench: | 21 | bench: | ||
| 20 | go test -bench . -benchmem ./... | 22 | go test -bench . -benchmem ./... | ||
| 23 | + | # Root targets (dispatch, event decode, reply ring) get FUZZTIME; generated | |||
| 24 | + | # packages and the generator get GENFUZZTIME. | |||
| 25 | + | fuzz: | |||
| 26 | + | @set -e; root=$$(go list .); for pkg in $$(go list ./...); do \ | |||
| 27 | + | time=$(GENFUZZTIME); [ "$$pkg" = "$$root" ] && time=$(FUZZTIME); \ | |||
| 28 | + | for t in $$(go test -list 'Fuzz.*' $$pkg | grep '^Fuzz'); do \ | |||
| 29 | + | echo "== $$pkg $$t ($$time)"; \ | |||
| 30 | + | go test -run '^$$' -fuzz "^$$t$$" -fuzztime $$time $$pkg; \ | |||
| 31 | + | done; \ | |||
| 32 | + | done | |||
| 21 | xserver: | 33 | xserver: | ||
| 22 | PATH="$$PATH:/usr/sbin" go test -tags xserver ./... | 34 | PATH="$$PATH:/usr/sbin" go test -tags xserver ./... | ||
| 23 | staticcheck: | 35 | staticcheck: | ||
A README.md +176 -0
| @@ -0,0 +1,176 @@ | |||
| 1 | + | # xturbo | |
| 2 | + | ||
| 3 | + | A pure-Go X11 client runtime with generated bindings. The runtime speaks the | |
| 4 | + | X11 wire protocol over a non-blocking Unix socket; the bindings come from | |
| 5 | + | xcb-proto 1.17.0. It depends only on the standard library and | |
| 6 | + | `golang.org/x/sys`, builds with `CGO_ENABLED=0`, and keeps the steady-state | |
| 7 | + | request, reply, event and error paths free of allocations. | |
| 8 | + | ||
| 9 | + | Module: `bnema.dev/xturbo`. Source: <https://git.bnema.dev/xturbo>. | |
| 10 | + | ||
| 11 | + | ## Packages | |
| 12 | + | ||
| 13 | + | | Package | Contents | | |
| 14 | + | | --- | --- | | |
| 15 | + | | `bnema.dev/xturbo` | `Conn`, `Dial`, `Buf`, `Dispatch`, `Expect`, `Sync`, `InternAtoms`, XID allocation | | |
| 16 | + | | `xproto` | core protocol | | |
| 17 | + | | `bigreq`, `ge`, `xcmisc` | BIG-REQUESTS, Generic Event, XC-MISC | | |
| 18 | + | | `composite`, `xfixes`, `render`, `shape`, `res` | Composite, XFIXES, RENDER, SHAPE, X-Resource | | |
| 19 | + | ||
| 20 | + | The root package never imports the generated packages. | |
| 21 | + | ||
| 22 | + | ## Install | |
| 23 | + | ||
| 24 | + | ```sh | |
| 25 | + | go get bnema.dev/xturbo | |
| 26 | + | ``` | |
| 27 | + | ||
| 28 | + | ## Example | |
| 29 | + | ||
| 30 | + | `Dial` (or `NewConn(fd)` for an already-connected socket) performs the | |
| 31 | + | handshake and negotiates the supported extensions. Requests are encoded into | |
| 32 | + | the connection's `Buf` and return a `Seq` (0 on failure). `Flush` never | |
| 33 | + | blocks, and `Dispatch` reads whatever the server sent and calls the `Handler`. | |
| 34 | + | ||
| 35 | + | ```go | |
| 36 | + | package xturbo_test | |
| 37 | + | ||
| 38 | + | import ( | |
| 39 | + | "errors" | |
| 40 | + | "log" | |
| 41 | + | ||
| 42 | + | "golang.org/x/sys/unix" | |
| 43 | + | ||
| 44 | + | "bnema.dev/xturbo" | |
| 45 | + | "bnema.dev/xturbo/xproto" | |
| 46 | + | ) | |
| 47 | + | ||
| 48 | + | // Replies registered with Expect carry this kind (16 and above are free). | |
| 49 | + | const kindGeometry uint16 = 16 | |
| 50 | + | ||
| 51 | + | func Example() { | |
| 52 | + | c, err := xturbo.Dial("") // "" means $DISPLAY | |
| 53 | + | if err != nil { | |
| 54 | + | log.Fatal(err) | |
| 55 | + | } | |
| 56 | + | defer c.Close() | |
| 57 | + | ||
| 58 | + | // Startup: blocking calls, bounded by a 5 s deadline. | |
| 59 | + | var atoms [1]uint32 | |
| 60 | + | if err := c.InternAtoms([]string{"WM_DELETE_WINDOW"}, false, atoms[:]); err != nil { | |
| 61 | + | log.Fatal(err) | |
| 62 | + | } | |
| 63 | + | ||
| 64 | + | // Queue requests; they return a Seq (0 on failure, see c.Buf().Err()). | |
| 65 | + | b := c.Buf() | |
| 66 | + | win, err := c.NewXID() | |
| 67 | + | if err != nil { | |
| 68 | + | log.Fatal(err) | |
| 69 | + | } | |
| 70 | + | var v xproto.CreateWindowValues | |
| 71 | + | v.SetEventMask(xproto.EventMaskExposure | xproto.EventMaskStructureNotify) | |
| 72 | + | xproto.CreateWindow(b, 0, xproto.Window(win), xproto.Window(c.Root()), | |
| 73 | + | 0, 0, 640, 480, 0, xproto.WindowClassInputOutput, 0, &v) | |
| 74 | + | xproto.MapWindow(b, xproto.Window(win)) | |
| 75 | + | if seq := xproto.GetGeometry(b, xproto.Drawable(win)); seq != 0 { | |
| 76 | + | if err := c.Expect(seq, kindGeometry, win, 0, 0, 0); err != nil { | |
| 77 | + | log.Fatal(err) | |
| 78 | + | } | |
| 79 | + | } | |
| 80 | + | ||
| 81 | + | // Callbacks run inside Dispatch. Events, replies and errors are borrowed: | |
| 82 | + | // copy what you keep. | |
| 83 | + | h := &xturbo.Handler{ | |
| 84 | + | OnEvent: func(ev *xturbo.Event) { | |
| 85 | + | switch ev.Kind { | |
| 86 | + | case xproto.KindExpose: | |
| 87 | + | e := xproto.ExposeOf(ev) | |
| 88 | + | log.Printf("expose %dx%d", e.Width(), e.Height()) | |
| 89 | + | } | |
| 90 | + | }, | |
| 91 | + | OnReply: func(r *xturbo.Reply) { | |
| 92 | + | if r.Kind == kindGeometry { | |
| 93 | + | if g, ok := xproto.ParseGetGeometryReply(r.Raw); ok { | |
| 94 | + | log.Printf("window %#x is %dx%d", r.Args[0], g.Width(), g.Height()) | |
| 95 | + | } | |
| 96 | + | } | |
| 97 | + | }, | |
| 98 | + | OnError: func(e *xturbo.XError) { log.Print(e) }, | |
| 99 | + | } | |
| 100 | + | ||
| 101 | + | for { | |
| 102 | + | // Flush never blocks. On ErrWouldBlock, also wait for POLLOUT. | |
| 103 | + | events := int16(unix.POLLIN) | |
| 104 | + | if err := c.Flush(); errors.Is(err, xturbo.ErrWouldBlock) { | |
| 105 | + | events |= unix.POLLOUT | |
| 106 | + | } else if err != nil { | |
| 107 | + | log.Fatal(err) | |
| 108 | + | } | |
| 109 | + | fds := []unix.PollFd{{Fd: int32(c.Fd()), Events: events}} | |
| 110 | + | if _, err := unix.Poll(fds, -1); err != nil && err != unix.EINTR { | |
| 111 | + | log.Fatal(err) | |
| 112 | + | } | |
| 113 | + | if err := c.Dispatch(h); err != nil { // io.EOF: server closed | |
| 114 | + | log.Fatal(err) | |
| 115 | + | } | |
| 116 | + | // Keep the XID pool topped up without blocking later. | |
| 117 | + | if c.XIDsLow() { | |
| 118 | + | _ = c.PrefetchXIDs() | |
| 119 | + | } | |
| 120 | + | } | |
| 121 | + | } | |
| 122 | + | ``` | |
| 123 | + | ||
| 124 | + | ## Rules | |
| 125 | + | ||
| 126 | + | - **Lifetimes.** A request's body slice is valid only until the next request is | |
| 127 | + | encoded. Reply views (`Reply.Raw` and the parsed views) and event `Tail` are | |
| 128 | + | valid only during the callback; `Event.Raw` is a copy. Copy what you keep. | |
| 129 | + | - **Callbacks.** They may queue requests, call `Expect` and `PrefetchXIDs`. | |
| 130 | + | They must not call `Dispatch`, `Sync`, `InternAtoms`, or `NewXID` when it | |
| 131 | + | needs a refill. | |
| 132 | + | - **Blocking.** `Dial` and `NewConn` block for at most 10 s during the | |
| 133 | + | handshake. Only `Sync`, `InternAtoms` and a `NewXID` refill block after | |
| 134 | + | that, for at most 5 s, and are meant for startup. Use `XIDsLow` and `PrefetchXIDs` to | |
| 135 | + | keep the event loop non-blocking. | |
| 136 | + | - **Errors.** A failed `Buf` is sticky: later requests return 0 and `Flush` | |
| 137 | + | returns the error. | |
| 138 | + | ||
| 139 | + | ## Performance | |
| 140 | + | ||
| 141 | + | Measured with `make bench` on an AMD Ryzen 9 7900X3D (Go 1.27, `CGO_ENABLED=0`). | |
| 142 | + | Every benchmark reports 0 allocations per operation. | |
| 143 | + | ||
| 144 | + | | Benchmark | Result | | |
| 145 | + | | --- | --- | | |
| 146 | + | | Encode a request (`MapWindow`) | 6.2 ns | | |
| 147 | + | | Encode and flush 64 requests over a socketpair | 1.55 µs (about 24 ns per request) | | |
| 148 | + | | Decode 10k events | 124 µs (about 12 ns per event) | | |
| 149 | + | | Dispatch 10k mixed packets | 251 µs (about 25 ns per packet) | | |
| 150 | + | | Parse a `GetProperty` reply view | 1.9 ns | | |
| 151 | + | | Parse a `QueryTree` reply view | 3.2 ns | | |
| 152 | + | ||
| 153 | + | ## Regenerating bindings | |
| 154 | + | ||
| 155 | + | Generated code lives in the extension packages and `xproto`; do not edit it. | |
| 156 | + | The inputs are the pinned xcb-proto 1.17.0 files in `spec/`, listed with | |
| 157 | + | checksums in `spec/manifest.json`. | |
| 158 | + | ||
| 159 | + | ```sh | |
| 160 | + | make gen # go generate ./... (cmd/xtgen) | |
| 161 | + | make gen-check # regenerate and fail on any difference | |
| 162 | + | go run ./cmd/xtsync # refresh spec/ from upstream and update the manifest | |
| 163 | + | ``` | |
| 164 | + | ||
| 165 | + | ## Tests | |
| 166 | + | ||
| 167 | + | ```sh | |
| 168 | + | make check # vet, staticcheck, tests, race, allocation, poison, gen-check | |
| 169 | + | make xserver # tests against a real Xvfb (needs xvfb and xtrace) | |
| 170 | + | make fuzz # root targets for FUZZTIME (60s), generated ones for GENFUZZTIME (10s) | |
| 171 | + | make bench # benchmarks | |
| 172 | + | ``` | |
| 173 | + | ||
| 174 | + | ## License | |
| 175 | + | ||
| 176 | + | MIT, see `LICENSE`. The xcb-proto XML and its license are under `LICENSES/`. | |
| @@ -0,0 +1,176 @@ | |||||
| 1 | + | # xturbo | |||
| 2 | + | ||||
| 3 | + | A pure-Go X11 client runtime with generated bindings. The runtime speaks the | |||
| 4 | + | X11 wire protocol over a non-blocking Unix socket; the bindings come from | |||
| 5 | + | xcb-proto 1.17.0. It depends only on the standard library and | |||
| 6 | + | `golang.org/x/sys`, builds with `CGO_ENABLED=0`, and keeps the steady-state | |||
| 7 | + | request, reply, event and error paths free of allocations. | |||
| 8 | + | ||||
| 9 | + | Module: `bnema.dev/xturbo`. Source: <https://git.bnema.dev/xturbo>. | |||
| 10 | + | ||||
| 11 | + | ## Packages | |||
| 12 | + | ||||
| 13 | + | | Package | Contents | | |||
| 14 | + | | --- | --- | | |||
| 15 | + | | `bnema.dev/xturbo` | `Conn`, `Dial`, `Buf`, `Dispatch`, `Expect`, `Sync`, `InternAtoms`, XID allocation | | |||
| 16 | + | | `xproto` | core protocol | | |||
| 17 | + | | `bigreq`, `ge`, `xcmisc` | BIG-REQUESTS, Generic Event, XC-MISC | | |||
| 18 | + | | `composite`, `xfixes`, `render`, `shape`, `res` | Composite, XFIXES, RENDER, SHAPE, X-Resource | | |||
| 19 | + | ||||
| 20 | + | The root package never imports the generated packages. | |||
| 21 | + | ||||
| 22 | + | ## Install | |||
| 23 | + | ||||
| 24 | + | ```sh | |||
| 25 | + | go get bnema.dev/xturbo | |||
| 26 | + | ``` | |||
| 27 | + | ||||
| 28 | + | ## Example | |||
| 29 | + | ||||
| 30 | + | `Dial` (or `NewConn(fd)` for an already-connected socket) performs the | |||
| 31 | + | handshake and negotiates the supported extensions. Requests are encoded into | |||
| 32 | + | the connection's `Buf` and return a `Seq` (0 on failure). `Flush` never | |||
| 33 | + | blocks, and `Dispatch` reads whatever the server sent and calls the `Handler`. | |||
| 34 | + | ||||
| 35 | + | ```go | |||
| 36 | + | package xturbo_test | |||
| 37 | + | ||||
| 38 | + | import ( | |||
| 39 | + | "errors" | |||
| 40 | + | "log" | |||
| 41 | + | ||||
| 42 | + | "golang.org/x/sys/unix" | |||
| 43 | + | ||||
| 44 | + | "bnema.dev/xturbo" | |||
| 45 | + | "bnema.dev/xturbo/xproto" | |||
| 46 | + | ) | |||
| 47 | + | ||||
| 48 | + | // Replies registered with Expect carry this kind (16 and above are free). | |||
| 49 | + | const kindGeometry uint16 = 16 | |||
| 50 | + | ||||
| 51 | + | func Example() { | |||
| 52 | + | c, err := xturbo.Dial("") // "" means $DISPLAY | |||
| 53 | + | if err != nil { | |||
| 54 | + | log.Fatal(err) | |||
| 55 | + | } | |||
| 56 | + | defer c.Close() | |||
| 57 | + | ||||
| 58 | + | // Startup: blocking calls, bounded by a 5 s deadline. | |||
| 59 | + | var atoms [1]uint32 | |||
| 60 | + | if err := c.InternAtoms([]string{"WM_DELETE_WINDOW"}, false, atoms[:]); err != nil { | |||
| 61 | + | log.Fatal(err) | |||
| 62 | + | } | |||
| 63 | + | ||||
| 64 | + | // Queue requests; they return a Seq (0 on failure, see c.Buf().Err()). | |||
| 65 | + | b := c.Buf() | |||
| 66 | + | win, err := c.NewXID() | |||
| 67 | + | if err != nil { | |||
| 68 | + | log.Fatal(err) | |||
| 69 | + | } | |||
| 70 | + | var v xproto.CreateWindowValues | |||
| 71 | + | v.SetEventMask(xproto.EventMaskExposure | xproto.EventMaskStructureNotify) | |||
| 72 | + | xproto.CreateWindow(b, 0, xproto.Window(win), xproto.Window(c.Root()), | |||
| 73 | + | 0, 0, 640, 480, 0, xproto.WindowClassInputOutput, 0, &v) | |||
| 74 | + | xproto.MapWindow(b, xproto.Window(win)) | |||
| 75 | + | if seq := xproto.GetGeometry(b, xproto.Drawable(win)); seq != 0 { | |||
| 76 | + | if err := c.Expect(seq, kindGeometry, win, 0, 0, 0); err != nil { | |||
| 77 | + | log.Fatal(err) | |||
| 78 | + | } | |||
| 79 | + | } | |||
| 80 | + | ||||
| 81 | + | // Callbacks run inside Dispatch. Events, replies and errors are borrowed: | |||
| 82 | + | // copy what you keep. | |||
| 83 | + | h := &xturbo.Handler{ | |||
| 84 | + | OnEvent: func(ev *xturbo.Event) { | |||
| 85 | + | switch ev.Kind { | |||
| 86 | + | case xproto.KindExpose: | |||
| 87 | + | e := xproto.ExposeOf(ev) | |||
| 88 | + | log.Printf("expose %dx%d", e.Width(), e.Height()) | |||
| 89 | + | } | |||
| 90 | + | }, | |||
| 91 | + | OnReply: func(r *xturbo.Reply) { | |||
| 92 | + | if r.Kind == kindGeometry { | |||
| 93 | + | if g, ok := xproto.ParseGetGeometryReply(r.Raw); ok { | |||
| 94 | + | log.Printf("window %#x is %dx%d", r.Args[0], g.Width(), g.Height()) | |||
| 95 | + | } | |||
| 96 | + | } | |||
| 97 | + | }, | |||
| 98 | + | OnError: func(e *xturbo.XError) { log.Print(e) }, | |||
| 99 | + | } | |||
| 100 | + | ||||
| 101 | + | for { | |||
| 102 | + | // Flush never blocks. On ErrWouldBlock, also wait for POLLOUT. | |||
| 103 | + | events := int16(unix.POLLIN) | |||
| 104 | + | if err := c.Flush(); errors.Is(err, xturbo.ErrWouldBlock) { | |||
| 105 | + | events |= unix.POLLOUT | |||
| 106 | + | } else if err != nil { | |||
| 107 | + | log.Fatal(err) | |||
| 108 | + | } | |||
| 109 | + | fds := []unix.PollFd{{Fd: int32(c.Fd()), Events: events}} | |||
| 110 | + | if _, err := unix.Poll(fds, -1); err != nil && err != unix.EINTR { | |||
| 111 | + | log.Fatal(err) | |||
| 112 | + | } | |||
| 113 | + | if err := c.Dispatch(h); err != nil { // io.EOF: server closed | |||
| 114 | + | log.Fatal(err) | |||
| 115 | + | } | |||
| 116 | + | // Keep the XID pool topped up without blocking later. | |||
| 117 | + | if c.XIDsLow() { | |||
| 118 | + | _ = c.PrefetchXIDs() | |||
| 119 | + | } | |||
| 120 | + | } | |||
| 121 | + | } | |||
| 122 | + | ``` | |||
| 123 | + | ||||
| 124 | + | ## Rules | |||
| 125 | + | ||||
| 126 | + | - **Lifetimes.** A request's body slice is valid only until the next request is | |||
| 127 | + | encoded. Reply views (`Reply.Raw` and the parsed views) and event `Tail` are | |||
| 128 | + | valid only during the callback; `Event.Raw` is a copy. Copy what you keep. | |||
| 129 | + | - **Callbacks.** They may queue requests, call `Expect` and `PrefetchXIDs`. | |||
| 130 | + | They must not call `Dispatch`, `Sync`, `InternAtoms`, or `NewXID` when it | |||
| 131 | + | needs a refill. | |||
| 132 | + | - **Blocking.** `Dial` and `NewConn` block for at most 10 s during the | |||
| 133 | + | handshake. Only `Sync`, `InternAtoms` and a `NewXID` refill block after | |||
| 134 | + | that, for at most 5 s, and are meant for startup. Use `XIDsLow` and `PrefetchXIDs` to | |||
| 135 | + | keep the event loop non-blocking. | |||
| 136 | + | - **Errors.** A failed `Buf` is sticky: later requests return 0 and `Flush` | |||
| 137 | + | returns the error. | |||
| 138 | + | ||||
| 139 | + | ## Performance | |||
| 140 | + | ||||
| 141 | + | Measured with `make bench` on an AMD Ryzen 9 7900X3D (Go 1.27, `CGO_ENABLED=0`). | |||
| 142 | + | Every benchmark reports 0 allocations per operation. | |||
| 143 | + | ||||
| 144 | + | | Benchmark | Result | | |||
| 145 | + | | --- | --- | | |||
| 146 | + | | Encode a request (`MapWindow`) | 6.2 ns | | |||
| 147 | + | | Encode and flush 64 requests over a socketpair | 1.55 µs (about 24 ns per request) | | |||
| 148 | + | | Decode 10k events | 124 µs (about 12 ns per event) | | |||
| 149 | + | | Dispatch 10k mixed packets | 251 µs (about 25 ns per packet) | | |||
| 150 | + | | Parse a `GetProperty` reply view | 1.9 ns | | |||
| 151 | + | | Parse a `QueryTree` reply view | 3.2 ns | | |||
| 152 | + | ||||
| 153 | + | ## Regenerating bindings | |||
| 154 | + | ||||
| 155 | + | Generated code lives in the extension packages and `xproto`; do not edit it. | |||
| 156 | + | The inputs are the pinned xcb-proto 1.17.0 files in `spec/`, listed with | |||
| 157 | + | checksums in `spec/manifest.json`. | |||
| 158 | + | ||||
| 159 | + | ```sh | |||
| 160 | + | make gen # go generate ./... (cmd/xtgen) | |||
| 161 | + | make gen-check # regenerate and fail on any difference | |||
| 162 | + | go run ./cmd/xtsync # refresh spec/ from upstream and update the manifest | |||
| 163 | + | ``` | |||
| 164 | + | ||||
| 165 | + | ## Tests | |||
| 166 | + | ||||
| 167 | + | ```sh | |||
| 168 | + | make check # vet, staticcheck, tests, race, allocation, poison, gen-check | |||
| 169 | + | make xserver # tests against a real Xvfb (needs xvfb and xtrace) | |||
| 170 | + | make fuzz # root targets for FUZZTIME (60s), generated ones for GENFUZZTIME (10s) | |||
| 171 | + | make bench # benchmarks | |||
| 172 | + | ``` | |||
| 173 | + | ||||
| 174 | + | ## License | |||
| 175 | + | ||||
| 176 | + | MIT, see `LICENSE`. The xcb-proto XML and its license are under `LICENSES/`. | |||
A example_test.go +86 -0
| @@ -0,0 +1,86 @@ | |||
| 1 | + | package xturbo_test | |
| 2 | + | ||
| 3 | + | import ( | |
| 4 | + | "errors" | |
| 5 | + | "log" | |
| 6 | + | ||
| 7 | + | "golang.org/x/sys/unix" | |
| 8 | + | ||
| 9 | + | "bnema.dev/xturbo" | |
| 10 | + | "bnema.dev/xturbo/xproto" | |
| 11 | + | ) | |
| 12 | + | ||
| 13 | + | // Replies registered with Expect carry this kind (16 and above are free). | |
| 14 | + | const kindGeometry uint16 = 16 | |
| 15 | + | ||
| 16 | + | func Example() { | |
| 17 | + | c, err := xturbo.Dial("") // "" means $DISPLAY | |
| 18 | + | if err != nil { | |
| 19 | + | log.Fatal(err) | |
| 20 | + | } | |
| 21 | + | defer c.Close() | |
| 22 | + | ||
| 23 | + | // Startup: blocking calls, bounded by a 5 s deadline. | |
| 24 | + | var atoms [1]uint32 | |
| 25 | + | if err := c.InternAtoms([]string{"WM_DELETE_WINDOW"}, false, atoms[:]); err != nil { | |
| 26 | + | log.Fatal(err) | |
| 27 | + | } | |
| 28 | + | ||
| 29 | + | // Queue requests; they return a Seq (0 on failure, see c.Buf().Err()). | |
| 30 | + | b := c.Buf() | |
| 31 | + | win, err := c.NewXID() | |
| 32 | + | if err != nil { | |
| 33 | + | log.Fatal(err) | |
| 34 | + | } | |
| 35 | + | var v xproto.CreateWindowValues | |
| 36 | + | v.SetEventMask(xproto.EventMaskExposure | xproto.EventMaskStructureNotify) | |
| 37 | + | xproto.CreateWindow(b, 0, xproto.Window(win), xproto.Window(c.Root()), | |
| 38 | + | 0, 0, 640, 480, 0, xproto.WindowClassInputOutput, 0, &v) | |
| 39 | + | xproto.MapWindow(b, xproto.Window(win)) | |
| 40 | + | if seq := xproto.GetGeometry(b, xproto.Drawable(win)); seq != 0 { | |
| 41 | + | if err := c.Expect(seq, kindGeometry, win, 0, 0, 0); err != nil { | |
| 42 | + | log.Fatal(err) | |
| 43 | + | } | |
| 44 | + | } | |
| 45 | + | ||
| 46 | + | // Callbacks run inside Dispatch. Events, replies and errors are borrowed: | |
| 47 | + | // copy what you keep. | |
| 48 | + | h := &xturbo.Handler{ | |
| 49 | + | OnEvent: func(ev *xturbo.Event) { | |
| 50 | + | switch ev.Kind { | |
| 51 | + | case xproto.KindExpose: | |
| 52 | + | e := xproto.ExposeOf(ev) | |
| 53 | + | log.Printf("expose %dx%d", e.Width(), e.Height()) | |
| 54 | + | } | |
| 55 | + | }, | |
| 56 | + | OnReply: func(r *xturbo.Reply) { | |
| 57 | + | if r.Kind == kindGeometry { | |
| 58 | + | if g, ok := xproto.ParseGetGeometryReply(r.Raw); ok { | |
| 59 | + | log.Printf("window %#x is %dx%d", r.Args[0], g.Width(), g.Height()) | |
| 60 | + | } | |
| 61 | + | } | |
| 62 | + | }, | |
| 63 | + | OnError: func(e *xturbo.XError) { log.Print(e) }, | |
| 64 | + | } | |
| 65 | + | ||
| 66 | + | for { | |
| 67 | + | // Flush never blocks. On ErrWouldBlock, also wait for POLLOUT. | |
| 68 | + | events := int16(unix.POLLIN) | |
| 69 | + | if err := c.Flush(); errors.Is(err, xturbo.ErrWouldBlock) { | |
| 70 | + | events |= unix.POLLOUT | |
| 71 | + | } else if err != nil { | |
| 72 | + | log.Fatal(err) | |
| 73 | + | } | |
| 74 | + | fds := []unix.PollFd{{Fd: int32(c.Fd()), Events: events}} | |
| 75 | + | if _, err := unix.Poll(fds, -1); err != nil && err != unix.EINTR { | |
| 76 | + | log.Fatal(err) | |
| 77 | + | } | |
| 78 | + | if err := c.Dispatch(h); err != nil { // io.EOF: server closed | |
| 79 | + | log.Fatal(err) | |
| 80 | + | } | |
| 81 | + | // Keep the XID pool topped up without blocking later. | |
| 82 | + | if c.XIDsLow() { | |
| 83 | + | _ = c.PrefetchXIDs() | |
| 84 | + | } | |
| 85 | + | } | |
| 86 | + | } | |
| @@ -0,0 +1,86 @@ | |||||
| 1 | + | package xturbo_test | |||
| 2 | + | ||||
| 3 | + | import ( | |||
| 4 | + | "errors" | |||
| 5 | + | "log" | |||
| 6 | + | ||||
| 7 | + | "golang.org/x/sys/unix" | |||
| 8 | + | ||||
| 9 | + | "bnema.dev/xturbo" | |||
| 10 | + | "bnema.dev/xturbo/xproto" | |||
| 11 | + | ) | |||
| 12 | + | ||||
| 13 | + | // Replies registered with Expect carry this kind (16 and above are free). | |||
| 14 | + | const kindGeometry uint16 = 16 | |||
| 15 | + | ||||
| 16 | + | func Example() { | |||
| 17 | + | c, err := xturbo.Dial("") // "" means $DISPLAY | |||
| 18 | + | if err != nil { | |||
| 19 | + | log.Fatal(err) | |||
| 20 | + | } | |||
| 21 | + | defer c.Close() | |||
| 22 | + | ||||
| 23 | + | // Startup: blocking calls, bounded by a 5 s deadline. | |||
| 24 | + | var atoms [1]uint32 | |||
| 25 | + | if err := c.InternAtoms([]string{"WM_DELETE_WINDOW"}, false, atoms[:]); err != nil { | |||
| 26 | + | log.Fatal(err) | |||
| 27 | + | } | |||
| 28 | + | ||||
| 29 | + | // Queue requests; they return a Seq (0 on failure, see c.Buf().Err()). | |||
| 30 | + | b := c.Buf() | |||
| 31 | + | win, err := c.NewXID() | |||
| 32 | + | if err != nil { | |||
| 33 | + | log.Fatal(err) | |||
| 34 | + | } | |||
| 35 | + | var v xproto.CreateWindowValues | |||
| 36 | + | v.SetEventMask(xproto.EventMaskExposure | xproto.EventMaskStructureNotify) | |||
| 37 | + | xproto.CreateWindow(b, 0, xproto.Window(win), xproto.Window(c.Root()), | |||
| 38 | + | 0, 0, 640, 480, 0, xproto.WindowClassInputOutput, 0, &v) | |||
| 39 | + | xproto.MapWindow(b, xproto.Window(win)) | |||
| 40 | + | if seq := xproto.GetGeometry(b, xproto.Drawable(win)); seq != 0 { | |||
| 41 | + | if err := c.Expect(seq, kindGeometry, win, 0, 0, 0); err != nil { | |||
| 42 | + | log.Fatal(err) | |||
| 43 | + | } | |||
| 44 | + | } | |||
| 45 | + | ||||
| 46 | + | // Callbacks run inside Dispatch. Events, replies and errors are borrowed: | |||
| 47 | + | // copy what you keep. | |||
| 48 | + | h := &xturbo.Handler{ | |||
| 49 | + | OnEvent: func(ev *xturbo.Event) { | |||
| 50 | + | switch ev.Kind { | |||
| 51 | + | case xproto.KindExpose: | |||
| 52 | + | e := xproto.ExposeOf(ev) | |||
| 53 | + | log.Printf("expose %dx%d", e.Width(), e.Height()) | |||
| 54 | + | } | |||
| 55 | + | }, | |||
| 56 | + | OnReply: func(r *xturbo.Reply) { | |||
| 57 | + | if r.Kind == kindGeometry { | |||
| 58 | + | if g, ok := xproto.ParseGetGeometryReply(r.Raw); ok { | |||
| 59 | + | log.Printf("window %#x is %dx%d", r.Args[0], g.Width(), g.Height()) | |||
| 60 | + | } | |||
| 61 | + | } | |||
| 62 | + | }, | |||
| 63 | + | OnError: func(e *xturbo.XError) { log.Print(e) }, | |||
| 64 | + | } | |||
| 65 | + | ||||
| 66 | + | for { | |||
| 67 | + | // Flush never blocks. On ErrWouldBlock, also wait for POLLOUT. | |||
| 68 | + | events := int16(unix.POLLIN) | |||
| 69 | + | if err := c.Flush(); errors.Is(err, xturbo.ErrWouldBlock) { | |||
| 70 | + | events |= unix.POLLOUT | |||
| 71 | + | } else if err != nil { | |||
| 72 | + | log.Fatal(err) | |||
| 73 | + | } | |||
| 74 | + | fds := []unix.PollFd{{Fd: int32(c.Fd()), Events: events}} | |||
| 75 | + | if _, err := unix.Poll(fds, -1); err != nil && err != unix.EINTR { | |||
| 76 | + | log.Fatal(err) | |||
| 77 | + | } | |||
| 78 | + | if err := c.Dispatch(h); err != nil { // io.EOF: server closed | |||
| 79 | + | log.Fatal(err) | |||
| 80 | + | } | |||
| 81 | + | // Keep the XID pool topped up without blocking later. | |||
| 82 | + | if c.XIDsLow() { | |||
| 83 | + | _ = c.PrefetchXIDs() | |||
| 84 | + | } | |||
| 85 | + | } | |||
| 86 | + | } | |||
M hardening_test.go +2 -1
| @@ -91,7 +91,8 @@ func TestSyncDeadlineFlood(t *testing.T) { | |||
| 91 | 91 | } | |
| 92 | 92 | } | |
| 93 | 93 | }() | |
| 94 | - | defer func() { close(stop); <-done }() | |
| 94 | + | // Closing the client unblocks a writer stuck on a full socket buffer. | |
| 95 | + | defer func() { close(stop); _ = c.Close(); <-done }() | |
| 95 | 96 | within(t, shortDeadline, func() error { return c.Sync(nil) }) | |
| 96 | 97 | } | |
| 97 | 98 | ||
| @@ -91,7 +91,8 @@ func TestSyncDeadlineFlood(t *testing.T) { | |||||
| 91 | } | 91 | } | ||
| 92 | } | 92 | } | ||
| 93 | }() | 93 | }() | ||
| 94 | - | defer func() { close(stop); <-done }() | 94 | + | // Closing the client unblocks a writer stuck on a full socket buffer. |
| 95 | + | defer func() { close(stop); _ = c.Close(); <-done }() | |||
| 95 | within(t, shortDeadline, func() error { return c.Sync(nil) }) | 96 | within(t, shortDeadline, func() error { return c.Sync(nil) }) | ||
| 96 | } | 97 | } | ||
| 97 | 98 | ||||
Comments0 open / 0
no comments yet.