Skip to content

Instantly share code, notes, and snippets.

@marler8997
Last active April 16, 2026 03:40
Show Gist options
  • Select an option

  • Save marler8997/a156993d68fb6597cd8247fc19a51547 to your computer and use it in GitHub Desktop.

Select an option

Save marler8997/a156993d68fb6597cd8247fc19a51547 to your computer and use it in GitHub Desktop.
zigx v libxcb
const std = @import("std");
const x11 = @import("x11");
const window_width = 400;
const window_height = 400;
const Ids = struct {
range: x11.IdRange,
pub fn window(self: Ids) x11.Window {
return self.range.addAssumeCapacity(0).window();
}
pub fn bg_gc(self: Ids) x11.GraphicsContext {
return self.range.addAssumeCapacity(1).graphicsContext();
}
pub fn fg_gc(self: Ids) x11.GraphicsContext {
return self.range.addAssumeCapacity(2).graphicsContext();
}
const needed_capacity = 3;
};
const Root = struct {
window: x11.Window,
visual: x11.Visual,
depth: x11.Depth,
};
pub fn main() !void {
try x11.wsaStartup();
const stream: std.net.Stream, const ids: Ids, const root: Root = blk: {
var read_buffer: [1000]u8 = undefined;
var socket_reader, const used_auth = try x11.draft.connect(&read_buffer);
errdefer x11.disconnect(socket_reader.getStream());
_ = used_auth;
const setup = x11.readSetupSuccess(socket_reader.interface()) catch |err| switch (err) {
error.ReadFailed => return socket_reader.getError().?,
error.EndOfStream, error.Protocol => |e| return e,
};
std.log.info("setup reply {f}", .{setup});
var source: x11.Source = .initFinishSetup(socket_reader.interface(), &setup);
const screen = (x11.draft.readSetupDynamic(&source, &setup, .{}) catch |err| switch (err) {
error.ReadFailed => return socket_reader.getError().?,
error.EndOfStream, error.Protocol => |e| return e,
}) orelse {
std.log.err("no screen?", .{});
std.process.exit(0xff);
};
const id_range = try x11.IdRange.init(setup.resource_id_base, setup.resource_id_mask);
if (id_range.capacity() < Ids.needed_capacity) {
std.log.err("X server id range capacity {} is less than needed {}", .{ id_range.capacity(), Ids.needed_capacity });
std.process.exit(0xff);
}
break :blk .{
socket_reader.getStream(), .{ .range = id_range }, .{
.window = screen.root,
.visual = screen.root_visual,
.depth = x11.Depth.init(screen.root_depth) orelse std.debug.panic(
"unsupported depth {}",
.{screen.root_depth},
),
},
};
};
defer x11.disconnect(stream);
var write_buffer: [1000]u8 = undefined;
var read_buffer: [1000]u8 = undefined;
var socket_writer = x11.socketWriter(stream, &write_buffer);
var socket_reader = x11.socketReader(stream, &read_buffer);
var sink: x11.RequestSink = .{ .writer = &socket_writer.interface };
var source: x11.Source = .initAfterSetup(socket_reader.interface());
run(ids, &root, &sink, &source) catch |err| switch (err) {
error.WriteFailed => |e| return x11.onWriteError(e, socket_writer.err.?),
error.ReadFailed, error.EndOfStream, error.Protocol => |e| return source.onReadError(e, socket_reader.getError()),
error.UnexpectedMessage => |e| return e,
};
}
fn run(
ids: Ids,
root: *const Root,
sink: *x11.RequestSink,
source: *x11.Source,
) error{ WriteFailed, ReadFailed, EndOfStream, Protocol, UnexpectedMessage }!void {
try sink.CreateWindow(
.{
.window_id = ids.window(),
.parent_window_id = root.window,
.depth = 0,
.x = 0,
.y = 0,
.width = window_width,
.height = window_height,
.border_width = 0,
.class = .input_output,
.visual_id = root.visual,
},
.{
.bg_pixel = root.depth.rgbFrom24(0xbbccdd),
.event_mask = .{ .Exposure = 1 },
},
);
try sink.CreateGc(
ids.bg_gc(),
ids.window().drawable(),
.{ .foreground = root.depth.rgbFrom24(0) },
);
try sink.CreateGc(
ids.fg_gc(),
ids.window().drawable(),
.{
.background = root.depth.rgbFrom24(0),
.foreground = root.depth.rgbFrom24(0xffaadd),
},
);
const font_dims: FontDims = blk: {
try sink.QueryTextExtents(ids.fg_gc().fontable(), .initComptime(&[_]u16{'m'}));
try sink.writer.flush();
const extents, _ = try source.readSynchronousReplyFull(sink.sequence, .QueryTextExtents);
std.log.info("text extents: {}", .{extents});
break :blk .{
.width = @intCast(extents.overall_width),
.height = @intCast(extents.font_ascent + extents.font_descent),
.font_left = @intCast(extents.overall_left),
.font_ascent = extents.font_ascent,
};
};
try sink.MapWindow(ids.window());
const bench = std.posix.getenv("BENCH") != null;
while (true) {
try sink.writer.flush();
const msg_kind = try source.readKind();
switch (msg_kind) {
.Expose => {
const expose = try source.read2(.Expose);
std.log.info("X11 {}", .{expose});
try render(sink, ids.window(), ids.bg_gc(), ids.fg_gc(), font_dims);
if (bench) {
try sink.writer.flush();
return;
}
},
.MappingNotify => {
const notify = try source.read2(.MappingNotify);
std.log.info("ignoring {}", .{notify});
},
else => std.debug.panic("unexpected X11 {f}", .{source.readFmtDropError()}),
}
}
}
const FontDims = struct {
width: u8,
height: u8,
font_left: i16, // pixels to the left of the text basepoint
font_ascent: i16, // pixels up from the text basepoint to the top of the text
};
fn render(
sink: *x11.RequestSink,
window_id: x11.Window,
bg_gc_id: x11.GraphicsContext,
fg_gc_id: x11.GraphicsContext,
font_dims: FontDims,
) !void {
try sink.PolyFillRectangle(
window_id.drawable(),
bg_gc_id,
.initComptime(&[_]x11.Rectangle{
.{ .x = 100, .y = 100, .width = 200, .height = 200 },
}),
);
try sink.ClearArea(
window_id,
.{
.x = 150,
.y = 150,
.width = 100,
.height = 100,
},
.{ .exposures = false },
);
const text = "Hello X!";
const text_width = font_dims.width * text.len;
try sink.ImageText8(
window_id.drawable(),
fg_gc_id,
.{
.x = @divTrunc((window_width - @as(i16, @intCast(text_width))), 2) + font_dims.font_left,
.y = @divTrunc((window_height - @as(i16, @intCast(font_dims.height))), 2) + font_dims.font_ascent,
},
.initComptime(text),
);
}
const std = @import("std");
const c = @cImport({
@cInclude("xcb/xcb.h");
});
const window_width = 400;
const window_height = 400;
pub fn main() !void {
const conn = c.xcb_connect(null, null) orelse {
std.log.err("xcb_connect returned null", .{});
std.process.exit(0xff);
};
if (c.xcb_connection_has_error(conn) != 0) {
std.log.err("failed to connect to X server", .{});
std.process.exit(0xff);
}
defer c.xcb_disconnect(conn);
const setup = c.xcb_get_setup(conn);
const iter = c.xcb_setup_roots_iterator(setup);
const screen: *c.xcb_screen_t = iter.data orelse {
std.log.err("no screen?", .{});
std.process.exit(0xff);
};
const window: c.xcb_window_t = c.xcb_generate_id(conn);
const bg_gc: c.xcb_gcontext_t = c.xcb_generate_id(conn);
const fg_gc: c.xcb_gcontext_t = c.xcb_generate_id(conn);
{
const value_mask: u32 = c.XCB_CW_BACK_PIXEL | c.XCB_CW_EVENT_MASK;
const value_list = [_]u32{ 0xbbccdd, c.XCB_EVENT_MASK_EXPOSURE };
_ = c.xcb_create_window(
conn,
c.XCB_COPY_FROM_PARENT,
window,
screen.root,
0,
0,
window_width,
window_height,
0,
c.XCB_WINDOW_CLASS_INPUT_OUTPUT,
screen.root_visual,
value_mask,
&value_list,
);
}
{
const value_mask: u32 = c.XCB_GC_FOREGROUND;
const value_list = [_]u32{0};
_ = c.xcb_create_gc(conn, bg_gc, window, value_mask, &value_list);
}
{
const value_mask: u32 = c.XCB_GC_BACKGROUND | c.XCB_GC_FOREGROUND;
const value_list = [_]u32{ 0, 0xffaadd };
_ = c.xcb_create_gc(conn, fg_gc, window, value_mask, &value_list);
}
const font_dims: FontDims = blk: {
const char = c.xcb_char2b_t{ .byte1 = 0, .byte2 = 'm' };
const cookie = c.xcb_query_text_extents(conn, fg_gc, 1, &char);
var err: ?*c.xcb_generic_error_t = null;
const reply = c.xcb_query_text_extents_reply(conn, cookie, &err) orelse {
std.log.err("QueryTextExtents failed", .{});
std.process.exit(0xff);
};
defer std.c.free(reply);
std.log.info("text extents: ascent={} descent={} overall_width={} overall_left={}", .{
reply.*.font_ascent, reply.*.font_descent, reply.*.overall_width, reply.*.overall_left,
});
break :blk .{
.width = @intCast(reply.*.overall_width),
.height = @intCast(reply.*.font_ascent + reply.*.font_descent),
.font_left = @intCast(reply.*.overall_left),
.font_ascent = reply.*.font_ascent,
};
};
_ = c.xcb_map_window(conn, window);
_ = c.xcb_flush(conn);
const bench = std.posix.getenv("BENCH") != null;
while (true) {
const event = c.xcb_wait_for_event(conn) orelse {
std.log.err("I/O error waiting for event", .{});
std.process.exit(0xff);
};
defer std.c.free(event);
switch (event.*.response_type & 0x7f) {
c.XCB_EXPOSE => {
const expose: *c.xcb_expose_event_t = @ptrCast(event);
std.log.info("Expose x={} y={} w={} h={}", .{
expose.x, expose.y, expose.width, expose.height,
});
render(conn, window, bg_gc, fg_gc, font_dims);
_ = c.xcb_flush(conn);
if (bench) return;
},
c.XCB_MAPPING_NOTIFY => {
std.log.info("ignoring MappingNotify", .{});
},
else => std.debug.panic("unexpected event type {}", .{event.*.response_type}),
}
}
}
const FontDims = struct {
width: u8,
height: u8,
font_left: i16,
font_ascent: i16,
};
fn render(
conn: *c.xcb_connection_t,
window: c.xcb_window_t,
bg_gc: c.xcb_gcontext_t,
fg_gc: c.xcb_gcontext_t,
font_dims: FontDims,
) void {
const rects = [_]c.xcb_rectangle_t{
.{ .x = 100, .y = 100, .width = 200, .height = 200 },
};
_ = c.xcb_poly_fill_rectangle(conn, window, bg_gc, rects.len, &rects);
_ = c.xcb_clear_area(conn, 0, window, 150, 150, 100, 100);
const text = "Hello X!";
const text_width: i16 = @as(i16, font_dims.width) * @as(i16, @intCast(text.len));
const x = @divTrunc(window_width - text_width, 2) + font_dims.font_left;
const y = @divTrunc(window_height - @as(i16, font_dims.height), 2) + font_dims.font_ascent;
_ = c.xcb_image_text_8(conn, text.len, window, fg_gc, x, y, text);
}

zigx vs libxcb: Full Comparison

Both programs do the identical workload: connect to X server, create a 400×400 window, create 2 graphics contexts, query text extents for 'm', map the window, and on Expose draw a filled rectangle + clear an area + draw "Hello X!".

  • examples/hello.zig — zigx version (pure Zig, no C deps)
  • examples/hello_xcb.zig — libxcb version (@cImport("xcb/xcb.h"), links libc + libxcb)

Both built with Zig 0.15.2 on Linux x86_64. Benchmark mode triggered via BENCH=1 env var (exits after first render).


1. Binary Size (disk)

Mode hello (zigx, static) hello_xcb (exe only) hello_xcb + shared lib deps
Debug 13.8 MB 7.1 MB ~10.0 MB
ReleaseSafe 3.4 MB 2.2 MB ~4.8 MB
ReleaseFast 4.1 MB 1.3 MB ~3.9 MB
ReleaseSmall 154 KB 18 KB ~2.6 MB

Shared lib deps for hello_xcb: libxcb (162 KB) + libc (2.1 MB) + ld-linux (237 KB) + libXau (19 KB) + libXdmcp (27 KB) ≈ 2.57 MB.

zigx is a fully static ELF with zero dynamic dependencies. libxcb requires 6 shared libraries at runtime.


2. Runtime Memory (via /proc/<pid>/status, ReleaseSmall)

metric hello (zigx) hello_xcb overhead
VmRSS (physical) 196 KB 1,920 KB 9.8×
RssAnon (heap+stack) 40 KB 180 KB 4.5×
RssFile (mapped files) 156 KB 1,740 KB 11.2×
VmLib (shared-lib code) 8 KB (vDSO only) 1,924 KB 240×
VmData (data+heap virt) 28 KB 260 KB 9.3×

3. Performance (ReleaseFast, poop, 200+ iterations)

metric hello (zigx) hello_xcb delta
wall_time 21.6 ms 24.2 ms +12%
cpu_cycles 65.5K 451K +589%
instructions 47.6K 392K +724%
cache_references 5.14K 34.5K +571%
cache_misses 2.56K 11.8K +361%
branch_misses 831 6,290 +656%
peak_rss 291 KB 1.90 MB +552%

zigx does ~8× less CPU work. Wall-clock gap is only 12% because both are latency-bound on X server round-trips.


4. Syscalls

hello (zigx) hello_xcb
total syscalls 65 118
distinct syscalls 14 30

zigx only issues X-protocol syscalls (socket, connect, sendmsg, writev, readv, shutdown, close) plus execve boilerplate.

hello_xcb adds ~53 extra syscalls before main() runs, all from ld.so loading shared libraries:

syscall count why
mmap 30 map each .so into memory
openat 8 open each .so file
fstat 8 inspect each .so
mprotect 8 set page permissions
pread64 2 read ELF headers
brk 3 glibc malloc arena setup
plus ~10 getrandom, rseq, set_tid_address, set_robust_list, fcntl, uname, getsockname, getpeername, access, etc.

5. Source Code

  • Both files are ~130 lines of Zig.
  • hello.zig (zigx): pure-Zig protocol implementation, no C interop.
  • hello_xcb.zig: @cImport("xcb/xcb.h") + links libc + libxcb.

TL;DR

dimension winner margin
Exe size on disk (just the binary) libxcb 1.5–8× smaller
Total deployed bytes (exe + required libs) zigx 3–17× smaller (esp. ReleaseSmall)
Startup CPU cost zigx ~7× (instructions), ~7× (cycles)
RSS / memory footprint zigx ~10×
Syscall count zigx ~2×
Wall-clock (this workload) zigx only ~12% (latency-bound)
Deployment simplicity zigx single static binary, no .so deps

For short-lived X11 utilities that draw minimal UI, zigx is dramatically cheaper across every dimension except "just the executable file size on disk" — and even that flips the moment you count the shared libraries libxcb requires at runtime.

The performance & memory gap shrinks for long-running programs, since most of libxcb's cost is one-time dynamic-linker + libc setup. But the 154 KB fully-static ReleaseSmall binary is a capability libxcb simply cannot match.


Methodology

Measurements done on:

  • Linux 6.17.0-19-generic, x86_64
  • Zig 0.15.2
  • libxcb 1.15+ (Ubuntu system package)
  • Xorg server (/usr/lib/xorg/Xorg)

Build commands:

zig build build-hello build-hello_xcb -Doptimize=ReleaseSmall
zig build build-hello build-hello_xcb -Doptimize=ReleaseFast
# etc.

Memory measured via awk '/^Vm|^Rss/' /proc/<pid>/status after window appears.

Performance via poop, 5-second duration, requires sudo -E or sysctl kernel.perf_event_paranoid=1:

BENCH=1 sudo -E poop -d 5000 ./zig-out/bin/hello ./zig-out/bin/hello_xcb

Syscalls via:

BENCH=1 strace -c ./zig-out/bin/hello
BENCH=1 strace -c ./zig-out/bin/hello_xcb
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment