diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index d624e243..7231ef77 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -245,7 +245,9 @@ jobs: UI_REPLAY_XVFB: --xvfb UI_REPLAY_SCREENSHOT_COMMAND: import UI_REPLAY_DISPLAY: 121 - run: make check-unit + # XCB=1: the XCB shim is off by default (mk/config.mk), so ask for it + # explicitly or its libraries, tests and ABI gate never build here. + run: make XCB=1 check-unit - name: Build bundled examples run: make examples -j"$(nproc)" @@ -323,7 +325,7 @@ jobs: UI_REPLAY_XVFB: --xvfb UI_REPLAY_SCREENSHOT_COMMAND: import UI_REPLAY_DISPLAY: 121 - run: make OPTFLAGS=-O0 CFLAGS_EXTRA=-DDEBUG_LIBX11_COMPAT check-unit + run: make XCB=1 OPTFLAGS=-O0 CFLAGS_EXTRA=-DDEBUG_LIBX11_COMPAT check-unit - name: ccache stats run: ccache --show-stats @@ -413,7 +415,7 @@ jobs: # autoconf which probes for Xutf8TextExtents via a link test # that fails when SAN_FLAGS leak into the upstream configure # ("Motif requires a UTF-8 capable libX11"). - make CFLAGS_EXTRA="$SAN_FLAGS" LDFLAGS="$SAN_FLAGS" check-unit + make XCB=1 CFLAGS_EXTRA="$SAN_FLAGS" LDFLAGS="$SAN_FLAGS" check-unit - name: ccache stats run: ccache --show-stats @@ -2138,7 +2140,7 @@ jobs: # dummy, software renderer) plus symbol coverage; it does not need # Xvfb. The make test runner also folds SDL_RUNTIME_LIBDIR (the sdl3 # pkg-config libdir) onto LD_LIBRARY_PATH so libSDL3 resolves. - run: make SDL_BACKEND=sdl3 check-unit + run: make XCB=1 SDL_BACKEND=sdl3 check-unit - name: Build bundled examples (SDL_BACKEND=sdl3) run: make SDL_BACKEND=sdl3 examples -j"$(nproc)" diff --git a/Makefile b/Makefile index 2fc51caa..a057d380 100644 --- a/Makefile +++ b/Makefile @@ -25,6 +25,7 @@ include mk/common.mk include mk/font-data.mk include mk/sdl-wrapper.mk include mk/library.mk +include mk/libxcb.mk include mk/libxt.mk include mk/libxpm.mk include mk/xcompat-libs.mk diff --git a/README.md b/README.md index 7a0f2419..495a99d4 100644 --- a/README.md +++ b/README.md @@ -64,14 +64,20 @@ Clients link against it the same way they would link against the system `libX11. ## Examples -`examples/` bundles real Xlib clients built against the local `libX11-compat.so`: +`examples/` bundles real Xlib and XCB clients built against the local +compatibility libraries: ```sh make examples build/examples/2048 +make XCB=1 examples +build/examples/xcb-kaleidoscope ``` The bundle covers a 2048 game, a paint demo, Conway's Game of Life, an analog clock, an interactive Mandelbrot viewer, a single-runner Processing-style showcase, an SDL-backed clipboard probe, and the upstream X.Org `x11perf` benchmark. +The XCB shim is off by default, so `XCB=1` adds the two native XCB clients: a +geometric image/primitives demo and a CPU-rendered Mandelbrot client; see +[`examples/README-xcb.md`](examples/README-xcb.md). See [`docs/EXAMPLES.md`](docs/EXAMPLES.md) for the API each example exercises. The screenshot above is from the larger ViolaWWW port described in [Larger Workloads Under Investigation](#larger-workloads-under-investigation). diff --git a/compat/xcb-compat-private.h b/compat/xcb-compat-private.h new file mode 100644 index 00000000..d0af8337 --- /dev/null +++ b/compat/xcb-compat-private.h @@ -0,0 +1,34 @@ +#ifndef LIBX11_COMPAT_XCB_PRIVATE_H +#define LIBX11_COMPAT_XCB_PRIVATE_H +#include +#include +typedef struct _XDisplay Display; +typedef union _XEvent XEvent; +Display *xcbCompatDisplay(xcb_connection_t *connection); +xcb_connection_t *xcbCompatConnectionForDisplay(Display *display); +void xcbCompatSetQueueOwner(xcb_connection_t *connection, int owner); +unsigned int xcbCompatEventWaiters(xcb_connection_t *connection); +void xcbCompatSetConnectionError(xcb_connection_t *connection, int error); +int xcbCompatRequestReady(xcb_connection_t *connection); +void xcbCompatReleaseRequestResources(xcb_connection_t *connection); +uint64_t xcbCompatNextSequence(xcb_connection_t *connection); +void xcbCompatSetNextSequence(xcb_connection_t *connection, uint64_t sequence); +void xcbCompatStorePending(xcb_connection_t *connection, + uint64_t sequence, + void *reply, + xcb_generic_error_t *error); +void xcbCompatStoreProtocolError(xcb_connection_t *connection, + uint64_t sequence, + uint8_t code, + uint32_t resource, + uint8_t opcode, + int checked); +void *xcbCompatTakeReply(xcb_connection_t *connection, + uint64_t sequence, + xcb_generic_error_t **error); +xcb_void_cookie_t xcbCompatVoidCookie(xcb_connection_t *connection, + uint8_t errorCode, + uint32_t resource, + uint8_t opcode, + int checked); +#endif diff --git a/compat/xcb-compat.c b/compat/xcb-compat.c new file mode 100644 index 00000000..5930ae88 --- /dev/null +++ b/compat/xcb-compat.c @@ -0,0 +1,1536 @@ +#include +#include +#include +#include +#include +#include +#include +#include "xcb-compat-private.h" +#include "../src/display.h" +#include "../src/events.h" +#include "../src/resource-types.h" + + + +typedef struct PendingReply { + uint64_t sequence; + void *reply; + xcb_generic_error_t *error; + struct PendingReply *next; +} PendingReply; + +typedef struct QueuedError { + xcb_generic_error_t *error; + struct QueuedError *next; +} QueuedError; + +typedef struct { + xcb_setup_t setup; + char vendor[16]; + xcb_format_t formats[4]; + xcb_screen_t screen; + xcb_depth_t depth; + xcb_visualtype_t visual; +} SetupWire; + +_Static_assert(offsetof(SetupWire, formats) == 56, "XCB format wire offset"); +_Static_assert(offsetof(SetupWire, screen) == 88, "XCB screen wire offset"); +_Static_assert(offsetof(SetupWire, depth) == 128, "XCB depth wire offset"); +_Static_assert(offsetof(SetupWire, visual) == 136, "XCB visual wire offset"); + +struct xcb_connection_t { + Display *display; + int error; + pthread_mutex_t mutex; + pthread_cond_t eventCond; + unsigned int eventUsers; + unsigned int closeCallbacks; + int closing; + uint64_t nextSequence; + PendingReply **pendingBuckets; + size_t pendingBucketCount; + size_t pendingCount; + QueuedError *errorHead; + QueuedError *errorTail; + SetupWire wire; + int ownsDisplay; + int queueOwner; + struct xcb_connection_t *next; +}; + +static pthread_mutex_t connectionsMutex = PTHREAD_MUTEX_INITIALIZER; +static xcb_connection_t *connections; + +__attribute__((weak)) void xcbCompatReleaseRequestResources( + xcb_connection_t *connection) +{ + (void) connection; +} + +/* Callers hold connectionsMutex. A display maps to at most one connection. */ +static xcb_connection_t *connectionForDisplayLocked(Display *display) +{ + for (xcb_connection_t *c = connections; c; c = c->next) + if (c->display == display) + return c; + return NULL; +} + +static int eventQueueOwner(Display *display) +{ + int owner = 0; + pthread_mutex_lock(&connectionsMutex); + xcb_connection_t *c = connectionForDisplayLocked(display); + if (c) { + pthread_mutex_lock(&c->mutex); + owner = c->queueOwner; + pthread_mutex_unlock(&c->mutex); + } + pthread_mutex_unlock(&connectionsMutex); + return owner; +} + +static int eventWaitCancelled(Display *display) +{ + int cancelled = 0; + pthread_mutex_lock(&connectionsMutex); + xcb_connection_t *c = connectionForDisplayLocked(display); + if (c) { + pthread_mutex_lock(&c->mutex); + cancelled = c->closing || c->errorHead != NULL; + pthread_mutex_unlock(&c->mutex); + } + pthread_mutex_unlock(&connectionsMutex); + return cancelled; +} + +/* Release everything a dead connection still holds. Caller holds c->mutex. + * Nothing can reap these once the connection is in error: xcb_request_check and + * the reply accessors both refuse a failed connection. + */ +static void drainPendingLocked(xcb_connection_t *c) +{ + for (size_t i = 0; i < c->pendingBucketCount; i++) { + PendingReply *pending = c->pendingBuckets[i]; + while (pending) { + PendingReply *next = pending->next; + free(pending->reply); + free(pending->error); + free(pending); + pending = next; + } + c->pendingBuckets[i] = NULL; + } + c->pendingCount = 0; + while (c->errorHead) { + QueuedError *queued = c->errorHead; + c->errorHead = queued->next; + free(queued->error); + free(queued); + } + c->errorTail = NULL; +} + +static void displayClosed(Display *display) +{ + pthread_mutex_lock(&connectionsMutex); + xcb_connection_t *connection = connectionForDisplayLocked(display); + if (connection) { + pthread_mutex_lock(&connection->mutex); + connection->closeCallbacks++; + pthread_mutex_unlock(&connection->mutex); + } + pthread_mutex_unlock(&connectionsMutex); + if (!connection) + return; + + pthread_mutex_lock(&connection->mutex); + connection->closing = 1; + connection->error = XCB_CONN_ERROR; + while (connection->eventUsers) + pthread_cond_wait(&connection->eventCond, &connection->mutex); + connection->display = NULL; + connection->ownsDisplay = 0; + + /* The wrapper itself stays registered and errored, because a client that + * kept the XGetXCBConnection pointer must get a failed connection rather + * than a dangling one. What it can no longer reach goes now. + */ + drainPendingLocked(connection); + pthread_mutex_unlock(&connection->mutex); + + xcbCompatReleaseRequestResources(connection); + pthread_mutex_lock(&connection->mutex); + connection->closeCallbacks--; + pthread_cond_broadcast(&connection->eventCond); + pthread_mutex_unlock(&connection->mutex); +} + +static void installDisplayHooks(void) +{ + libx11CompatSetDisplayCloseHook(displayClosed); + libx11CompatSetEventQueueOwnerHook(eventQueueOwner); + libx11CompatSetEventWaitCancelledHook(eventWaitCancelled); +} + +static void registerConnection(xcb_connection_t *c) +{ + installDisplayHooks(); + pthread_mutex_lock(&connectionsMutex); + c->next = connections; + connections = c; + pthread_mutex_unlock(&connectionsMutex); +} + +/* Publish c for its display unless another thread got there first, in which + * case the winner is returned and c is left for the caller to discard. The + * lookup and the insertion happen under one acquisition, so a display can never + * end up with two connection wrappers. + */ +static xcb_connection_t *registerConnectionForDisplay(xcb_connection_t *c) +{ + installDisplayHooks(); + pthread_mutex_lock(&connectionsMutex); + xcb_connection_t *existing = connectionForDisplayLocked(c->display); + if (!existing) { + c->next = connections; + connections = c; + } + pthread_mutex_unlock(&connectionsMutex); + return existing; +} + +static void unregisterConnection(xcb_connection_t *c) +{ + pthread_mutex_lock(&connectionsMutex); + xcb_connection_t **link = &connections; + while (*link && *link != c) + link = &(*link)->next; + if (*link) + *link = c->next; + pthread_mutex_unlock(&connectionsMutex); +} + +static void initializeSetup(xcb_connection_t *connection) +{ + Display *display = connection->display; + SetupWire *wire = &connection->wire; + xcb_setup_t *setup = &wire->setup; + xcb_screen_t *screen = &wire->screen; + Visual *visual = DefaultVisual(display, DefaultScreen(display)); + int depth = DefaultDepth(display, DefaultScreen(display)); + memset(wire, 0, sizeof(*wire)); + setup->status = 1; + setup->protocol_major_version = 11; + setup->length = (sizeof(*wire) - 8) / 4; + setup->release_number = 1; + setup->resource_id_base = XID_RESOURCE_BASE; + setup->resource_id_mask = XID_RESOURCE_MASK; + setup->vendor_len = sizeof("libx11-compat") - 1; + setup->maximum_request_length = 65535; + setup->roots_len = 1; + setup->pixmap_formats_len = 4; + setup->image_byte_order = ImageByteOrder(display); + setup->bitmap_format_bit_order = BitmapBitOrder(display); + setup->bitmap_format_scanline_unit = BitmapUnit(display); + setup->bitmap_format_scanline_pad = BitmapPad(display); + int minimumKeycode, maximumKeycode; + XDisplayKeycodes(display, &minimumKeycode, &maximumKeycode); + setup->min_keycode = minimumKeycode; + setup->max_keycode = maximumKeycode; + memcpy(wire->vendor, "libx11-compat", setup->vendor_len); + static const xcb_format_t formats[] = { + {.depth = 1, .bits_per_pixel = 1, .scanline_pad = 32}, + {.depth = 16, .bits_per_pixel = 16, .scanline_pad = 32}, + {.depth = 24, .bits_per_pixel = 32, .scanline_pad = 32}, + {.depth = 32, .bits_per_pixel = 32, .scanline_pad = 32}, + }; + memcpy(wire->formats, formats, sizeof(formats)); + screen->root = DefaultRootWindow(display); + screen->default_colormap = DefaultColormap(display, DefaultScreen(display)); + screen->white_pixel = WhitePixel(display, DefaultScreen(display)); + screen->black_pixel = BlackPixel(display, DefaultScreen(display)); + screen->width_in_pixels = DisplayWidth(display, DefaultScreen(display)); + screen->height_in_pixels = DisplayHeight(display, DefaultScreen(display)); + screen->width_in_millimeters = + DisplayWidthMM(display, DefaultScreen(display)); + screen->height_in_millimeters = + DisplayHeightMM(display, DefaultScreen(display)); + screen->min_installed_maps = screen->max_installed_maps = 1; + screen->root_visual = XVisualIDFromVisual(visual); + screen->backing_stores = DoesBackingStore(DefaultScreenOfDisplay(display)); + screen->save_unders = DoesSaveUnders(DefaultScreenOfDisplay(display)); + screen->root_depth = depth; + screen->allowed_depths_len = 1; + wire->depth.depth = depth; + wire->depth.visuals_len = 1; + wire->visual.visual_id = XVisualIDFromVisual(visual); + wire->visual._class = visual->class; + wire->visual.bits_per_rgb_value = visual->bits_per_rgb; + wire->visual.colormap_entries = visual->map_entries; + wire->visual.red_mask = visual->red_mask; + wire->visual.green_mask = visual->green_mask; + wire->visual.blue_mask = visual->blue_mask; +} + +static xcb_connection_t *allocConnection(void) +{ + xcb_connection_t *c = calloc(1, sizeof(*c)); + if (!c) + return NULL; + if (pthread_mutex_init(&c->mutex, NULL) != 0) { + free(c); + return NULL; + } + if (pthread_cond_init(&c->eventCond, NULL) != 0) { + pthread_mutex_destroy(&c->mutex); + free(c); + return NULL; + } + c->nextSequence = 1; + return c; +} + +static void freeConnection(xcb_connection_t *c) +{ + pthread_cond_destroy(&c->eventCond); + pthread_mutex_destroy(&c->mutex); + free(c); +} + +xcb_connection_t *xcb_connect(const char *name, int *screen) +{ + xcb_connection_t *c = allocConnection(); + if (!c) + return NULL; + if (screen) + *screen = 0; + c->display = XOpenDisplay(name); + if (!c->display) { + c->error = XCB_CONN_ERROR; + return c; + } + c->ownsDisplay = 1; + initializeSetup(c); + registerConnection(c); + return c; +} + +xcb_connection_t *xcbCompatConnectionForDisplay(Display *display) +{ + if (!display) + return NULL; + pthread_mutex_lock(&connectionsMutex); + xcb_connection_t *existing = connectionForDisplayLocked(display); + pthread_mutex_unlock(&connectionsMutex); + if (existing) + return existing; + xcb_connection_t *c = allocConnection(); + if (!c) + return NULL; + c->display = display; + initializeSetup(c); + + /* Another thread may have bound this display while the setup record was + * being built, so publish and re-check as one step and keep the winner. + */ + existing = registerConnectionForDisplay(c); + if (existing) { + freeConnection(c); + return existing; + } + return c; +} + +/* Number of threads currently blocked inside an event wait on this connection. + * Exposed for tests, which otherwise have no way to tell a thread that is about + * to block from one that already is. + */ +unsigned int xcbCompatEventWaiters(xcb_connection_t *c) +{ + if (!c) + return 0; + pthread_mutex_lock(&c->mutex); + unsigned int waiters = c->eventUsers; + pthread_mutex_unlock(&c->mutex); + return waiters; +} + +void xcbCompatSetQueueOwner(xcb_connection_t *c, int owner) +{ + if (c) { + pthread_mutex_lock(&c->mutex); + c->queueOwner = owner != 0; + pthread_mutex_unlock(&c->mutex); + } +} + +xcb_connection_t *xcb_connect_to_display_with_auth_info(const char *display, + xcb_auth_info_t *auth, + int *screen) +{ + (void) auth; + return xcb_connect(display, screen); +} + +int xcb_connection_has_error(xcb_connection_t *c) +{ + if (!c) + return XCB_CONN_ERROR; + pthread_mutex_lock(&c->mutex); + int error = c->error; + pthread_mutex_unlock(&c->mutex); + return error; +} + +void xcbCompatSetConnectionError(xcb_connection_t *c, int error) +{ + if (!c || !error) + return; + pthread_mutex_lock(&c->mutex); + if (!c->error) + c->error = error; + pthread_mutex_unlock(&c->mutex); +} +int xcbCompatRequestReady(xcb_connection_t *c) +{ + if (!c) + return 0; + pthread_mutex_lock(&c->mutex); + int ready = c->display && !c->error && !c->closing; + pthread_mutex_unlock(&c->mutex); + return ready; +} +int xcb_get_file_descriptor(xcb_connection_t *c) +{ + (void) c; + return -1; +} +const xcb_setup_t *xcb_get_setup(xcb_connection_t *c) +{ + return (!c || c->error) ? NULL : &c->wire.setup; +} +uint32_t xcb_get_maximum_request_length(xcb_connection_t *c) +{ + const xcb_setup_t *s = xcb_get_setup(c); + return s ? s->maximum_request_length : 0; +} +uint32_t xcb_generate_id(xcb_connection_t *c) +{ + return (!c || c->error) ? UINT32_MAX : allocXidResource(); +} + +typedef struct { + int type; + Display *display; + uint64_t serial; + uint8_t synthetic, detail, mode, format, sameScreen, focus, + overrideRedirect; + uint32_t window, event, root, child, sibling, parent, owner, requestor, + selection, target, property, colormap, atom, messageType, time, state; + int16_t x, y, rootX, rootY; + uint16_t unsignedX, unsignedY, width, height, borderWidth, count, + minorOpcode; + uint8_t fromConfigure, newColormap, majorOpcode, firstKeycode, keys[32], + data[20]; +} NormalizedEvent; + +static void normalizeEvent(const XEvent *x, NormalizedEvent *n) +{ + memset(n, 0, sizeof(*n)); + n->type = x->type; + n->display = x->xany.display; + n->serial = x->xany.serial; + n->synthetic = x->xany.send_event; + switch (x->type) { + case KeyPress: + case KeyRelease: + n->detail = x->xkey.keycode; + n->time = x->xkey.time; + n->root = x->xkey.root; + n->window = x->xkey.window; + n->child = x->xkey.subwindow; + n->rootX = x->xkey.x_root; + n->rootY = x->xkey.y_root; + n->x = x->xkey.x; + n->y = x->xkey.y; + n->state = x->xkey.state; + n->sameScreen = x->xkey.same_screen; + break; + case ButtonPress: + case ButtonRelease: + n->detail = x->xbutton.button; + n->time = x->xbutton.time; + n->root = x->xbutton.root; + n->window = x->xbutton.window; + n->child = x->xbutton.subwindow; + n->rootX = x->xbutton.x_root; + n->rootY = x->xbutton.y_root; + n->x = x->xbutton.x; + n->y = x->xbutton.y; + n->state = x->xbutton.state; + n->sameScreen = x->xbutton.same_screen; + break; + case MotionNotify: + n->detail = x->xmotion.is_hint; + n->time = x->xmotion.time; + n->root = x->xmotion.root; + n->window = x->xmotion.window; + n->child = x->xmotion.subwindow; + n->rootX = x->xmotion.x_root; + n->rootY = x->xmotion.y_root; + n->x = x->xmotion.x; + n->y = x->xmotion.y; + n->state = x->xmotion.state; + n->sameScreen = x->xmotion.same_screen; + break; + case EnterNotify: + case LeaveNotify: + n->detail = x->xcrossing.detail; + n->time = x->xcrossing.time; + n->root = x->xcrossing.root; + n->window = x->xcrossing.window; + n->child = x->xcrossing.subwindow; + n->rootX = x->xcrossing.x_root; + n->rootY = x->xcrossing.y_root; + n->x = x->xcrossing.x; + n->y = x->xcrossing.y; + n->state = x->xcrossing.state; + n->mode = x->xcrossing.mode; + n->sameScreen = x->xcrossing.same_screen; + n->focus = x->xcrossing.focus; + break; + case FocusIn: + case FocusOut: + n->detail = x->xfocus.detail; + n->window = x->xfocus.window; + n->mode = x->xfocus.mode; + break; + case KeymapNotify: + n->window = x->xkeymap.window; + memcpy(n->keys, x->xkeymap.key_vector, sizeof(n->keys)); + break; + case Expose: + n->window = x->xexpose.window; + n->x = x->xexpose.x; + n->y = x->xexpose.y; + n->width = x->xexpose.width; + n->height = x->xexpose.height; + n->count = x->xexpose.count; + break; + case GraphicsExpose: + n->window = x->xgraphicsexpose.drawable; + n->unsignedX = x->xgraphicsexpose.x; + n->unsignedY = x->xgraphicsexpose.y; + n->width = x->xgraphicsexpose.width; + n->height = x->xgraphicsexpose.height; + n->count = x->xgraphicsexpose.count; + n->majorOpcode = x->xgraphicsexpose.major_code; + n->minorOpcode = x->xgraphicsexpose.minor_code; + break; + case NoExpose: + n->window = x->xnoexpose.drawable; + n->majorOpcode = x->xnoexpose.major_code; + n->minorOpcode = x->xnoexpose.minor_code; + break; + case VisibilityNotify: + n->window = x->xvisibility.window; + n->state = x->xvisibility.state; + break; + case ConfigureNotify: + n->event = x->xconfigure.event; + n->window = x->xconfigure.window; + n->sibling = x->xconfigure.above; + n->x = x->xconfigure.x; + n->y = x->xconfigure.y; + n->width = x->xconfigure.width; + n->height = x->xconfigure.height; + n->borderWidth = x->xconfigure.border_width; + n->overrideRedirect = x->xconfigure.override_redirect; + break; + case CreateNotify: + n->parent = x->xcreatewindow.parent; + n->window = x->xcreatewindow.window; + n->x = x->xcreatewindow.x; + n->y = x->xcreatewindow.y; + n->width = x->xcreatewindow.width; + n->height = x->xcreatewindow.height; + n->borderWidth = x->xcreatewindow.border_width; + n->overrideRedirect = x->xcreatewindow.override_redirect; + break; + case MapNotify: + n->event = x->xmap.event; + n->window = x->xmap.window; + n->overrideRedirect = x->xmap.override_redirect; + break; + case UnmapNotify: + n->event = x->xunmap.event; + n->window = x->xunmap.window; + n->fromConfigure = x->xunmap.from_configure; + break; + case ReparentNotify: + n->event = x->xreparent.event; + n->window = x->xreparent.window; + n->parent = x->xreparent.parent; + n->x = x->xreparent.x; + n->y = x->xreparent.y; + n->overrideRedirect = x->xreparent.override_redirect; + break; + case GravityNotify: + n->event = x->xgravity.event; + n->window = x->xgravity.window; + n->x = x->xgravity.x; + n->y = x->xgravity.y; + break; + case MapRequest: + n->parent = x->xmaprequest.parent; + n->window = x->xmaprequest.window; + break; + case ConfigureRequest: + n->parent = x->xconfigurerequest.parent; + n->window = x->xconfigurerequest.window; + n->sibling = x->xconfigurerequest.above; + n->x = x->xconfigurerequest.x; + n->y = x->xconfigurerequest.y; + n->width = x->xconfigurerequest.width; + n->height = x->xconfigurerequest.height; + n->borderWidth = x->xconfigurerequest.border_width; + n->detail = x->xconfigurerequest.detail; + n->state = x->xconfigurerequest.value_mask; + break; + case ResizeRequest: + n->window = x->xresizerequest.window; + n->width = x->xresizerequest.width; + n->height = x->xresizerequest.height; + break; + case CirculateNotify: + n->event = x->xcirculate.event; + n->window = x->xcirculate.window; + n->detail = x->xcirculate.place; + break; + case CirculateRequest: + n->event = x->xcirculaterequest.parent; + n->window = x->xcirculaterequest.window; + n->detail = x->xcirculaterequest.place; + break; + case PropertyNotify: + n->window = x->xproperty.window; + n->atom = x->xproperty.atom; + n->time = x->xproperty.time; + n->state = x->xproperty.state; + break; + case SelectionClear: + n->time = x->xselectionclear.time; + n->owner = x->xselectionclear.window; + n->selection = x->xselectionclear.selection; + break; + case SelectionRequest: + n->time = x->xselectionrequest.time; + n->owner = x->xselectionrequest.owner; + n->requestor = x->xselectionrequest.requestor; + n->selection = x->xselectionrequest.selection; + n->target = x->xselectionrequest.target; + n->property = x->xselectionrequest.property; + break; + case SelectionNotify: + n->time = x->xselection.time; + n->requestor = x->xselection.requestor; + n->selection = x->xselection.selection; + n->target = x->xselection.target; + n->property = x->xselection.property; + break; + case ColormapNotify: + n->window = x->xcolormap.window; + n->colormap = x->xcolormap.colormap; + n->newColormap = x->xcolormap.new; + n->state = x->xcolormap.state; + break; + case MappingNotify: + n->window = x->xmapping.window; + n->detail = x->xmapping.request; + n->firstKeycode = x->xmapping.first_keycode; + n->count = x->xmapping.count; + break; + case DestroyNotify: + n->event = x->xdestroywindow.event; + n->window = x->xdestroywindow.window; + break; + case ClientMessage: + n->format = x->xclient.format; + n->window = x->xclient.window; + n->messageType = x->xclient.message_type; + memcpy(n->data, x->xclient.data.b, sizeof(n->data)); + break; + } +} + +static xcb_generic_event_t *convertEvent(xcb_connection_t *c, const XEvent *x) +{ + _Static_assert(sizeof(xcb_raw_generic_event_t) == 32, + "XCB core event wire size"); + _Static_assert(offsetof(xcb_generic_event_t, full_sequence) == 32, + "XCB full sequence trailer offset"); + xcb_generic_event_t *generic = calloc(1, sizeof(*generic)); + if (!generic) + return NULL; + NormalizedEvent n; + normalizeEvent(x, &n); + generic->response_type = (uint8_t) n.type | (n.synthetic ? 0x80 : 0); + generic->sequence = (uint16_t) n.serial; + generic->full_sequence = (uint32_t) n.serial; + switch (n.type) { + case KeyPress: + case KeyRelease: + case ButtonPress: + case ButtonRelease: + case MotionNotify: { + xcb_key_press_event_t *e = (xcb_key_press_event_t *) generic; + e->detail = n.detail; + e->time = n.time; + e->root = n.root; + e->event = n.window; + e->child = n.child; + e->root_x = n.rootX; + e->root_y = n.rootY; + e->event_x = n.x; + e->event_y = n.y; + e->state = n.state; + e->same_screen = n.sameScreen; + break; + } + case EnterNotify: + case LeaveNotify: { + xcb_enter_notify_event_t *e = (xcb_enter_notify_event_t *) generic; + e->detail = n.detail; + e->time = n.time; + e->root = n.root; + e->event = n.window; + e->child = n.child; + e->root_x = n.rootX; + e->root_y = n.rootY; + e->event_x = n.x; + e->event_y = n.y; + e->state = n.state; + e->mode = n.mode; + e->same_screen_focus = (n.sameScreen ? 1u : 0u) | (n.focus ? 2u : 0u); + break; + } + case FocusIn: + case FocusOut: { + xcb_focus_in_event_t *e = (xcb_focus_in_event_t *) generic; + e->detail = n.detail; + e->event = n.window; + e->mode = n.mode; + break; + } + case KeymapNotify: { + xcb_keymap_notify_event_t *e = (xcb_keymap_notify_event_t *) generic; + memcpy(e->keys, n.keys + 1, sizeof(e->keys)); + break; + } + case Expose: { + xcb_expose_event_t *e = (xcb_expose_event_t *) generic; + e->window = n.window; + e->x = n.x; + e->y = n.y; + e->width = n.width; + e->height = n.height; + e->count = n.count; + break; + } + case GraphicsExpose: { + xcb_graphics_exposure_event_t *e = + (xcb_graphics_exposure_event_t *) generic; + e->drawable = n.window; + e->x = n.unsignedX; + e->y = n.unsignedY; + e->width = n.width; + e->height = n.height; + e->minor_opcode = n.minorOpcode; + e->count = n.count; + e->major_opcode = n.majorOpcode; + break; + } + case NoExpose: { + xcb_no_exposure_event_t *e = (xcb_no_exposure_event_t *) generic; + e->drawable = n.window; + e->minor_opcode = n.minorOpcode; + e->major_opcode = n.majorOpcode; + break; + } + case VisibilityNotify: { + xcb_visibility_notify_event_t *e = + (xcb_visibility_notify_event_t *) generic; + e->window = n.window; + e->state = n.state; + break; + } + case ConfigureNotify: { + xcb_configure_notify_event_t *e = + (xcb_configure_notify_event_t *) generic; + e->event = n.event; + e->window = n.window; + e->above_sibling = n.sibling; + e->x = n.x; + e->y = n.y; + e->width = n.width; + e->height = n.height; + e->border_width = n.borderWidth; + e->override_redirect = n.overrideRedirect; + break; + } + case CreateNotify: { + xcb_create_notify_event_t *e = (xcb_create_notify_event_t *) generic; + e->parent = n.parent; + e->window = n.window; + e->x = n.x; + e->y = n.y; + e->width = n.width; + e->height = n.height; + e->border_width = n.borderWidth; + e->override_redirect = n.overrideRedirect; + break; + } + case MapNotify: { + xcb_map_notify_event_t *e = (xcb_map_notify_event_t *) generic; + e->event = n.event; + e->window = n.window; + e->override_redirect = n.overrideRedirect; + break; + } + case UnmapNotify: { + xcb_unmap_notify_event_t *e = (xcb_unmap_notify_event_t *) generic; + e->event = n.event; + e->window = n.window; + e->from_configure = n.fromConfigure; + break; + } + case ReparentNotify: { + xcb_reparent_notify_event_t *e = + (xcb_reparent_notify_event_t *) generic; + e->event = n.event; + e->window = n.window; + e->parent = n.parent; + e->x = n.x; + e->y = n.y; + e->override_redirect = n.overrideRedirect; + break; + } + case GravityNotify: { + xcb_gravity_notify_event_t *e = (xcb_gravity_notify_event_t *) generic; + e->event = n.event; + e->window = n.window; + e->x = n.x; + e->y = n.y; + break; + } + case MapRequest: { + xcb_map_request_event_t *e = (xcb_map_request_event_t *) generic; + e->parent = n.parent; + e->window = n.window; + break; + } + case ConfigureRequest: { + xcb_configure_request_event_t *e = + (xcb_configure_request_event_t *) generic; + e->stack_mode = n.detail; + e->parent = n.parent; + e->window = n.window; + e->sibling = n.sibling; + e->x = n.x; + e->y = n.y; + e->width = n.width; + e->height = n.height; + e->border_width = n.borderWidth; + e->value_mask = n.state; + break; + } + case ResizeRequest: { + xcb_resize_request_event_t *e = (xcb_resize_request_event_t *) generic; + e->window = n.window; + e->width = n.width; + e->height = n.height; + break; + } + case CirculateNotify: + case CirculateRequest: { + xcb_circulate_notify_event_t *e = + (xcb_circulate_notify_event_t *) generic; + e->event = n.event; + e->window = n.window; + e->place = n.detail; + break; + } + case PropertyNotify: { + xcb_property_notify_event_t *e = + (xcb_property_notify_event_t *) generic; + e->window = n.window; + e->atom = n.atom; + e->time = n.time; + e->state = n.state; + break; + } + case SelectionClear: { + xcb_selection_clear_event_t *e = + (xcb_selection_clear_event_t *) generic; + e->time = n.time; + e->owner = n.owner; + e->selection = n.selection; + break; + } + case SelectionRequest: { + xcb_selection_request_event_t *e = + (xcb_selection_request_event_t *) generic; + e->time = n.time; + e->owner = n.owner; + e->requestor = n.requestor; + e->selection = n.selection; + e->target = n.target; + e->property = n.property; + break; + } + case SelectionNotify: { + xcb_selection_notify_event_t *e = + (xcb_selection_notify_event_t *) generic; + e->time = n.time; + e->requestor = n.requestor; + e->selection = n.selection; + e->target = n.target; + e->property = n.property; + break; + } + case ColormapNotify: { + xcb_colormap_notify_event_t *e = + (xcb_colormap_notify_event_t *) generic; + e->window = n.window; + e->colormap = n.colormap; + e->_new = n.newColormap; + e->state = n.state; + break; + } + case MappingNotify: { + xcb_mapping_notify_event_t *e = (xcb_mapping_notify_event_t *) generic; + e->request = n.detail; + e->first_keycode = n.firstKeycode; + e->count = n.count; + break; + } + case DestroyNotify: { + xcb_destroy_notify_event_t *e = (xcb_destroy_notify_event_t *) generic; + e->event = n.event; + e->window = n.window; + break; + } + case ClientMessage: { + xcb_client_message_event_t *e = (xcb_client_message_event_t *) generic; + e->format = n.format; + e->window = n.window; + e->type = n.messageType; + memcpy(e->data.data8, n.data, sizeof(e->data.data8)); + break; + } + default: + break; + } + (void) c; + return generic; +} + +static xcb_generic_event_t *takeQueuedErrorLocked(xcb_connection_t *c) +{ + QueuedError *queued = c->errorHead; + if (!queued) + return NULL; + c->errorHead = queued->next; + if (!c->errorHead) + c->errorTail = NULL; + xcb_generic_event_t *error = (xcb_generic_event_t *) queued->error; + free(queued); + return error; +} + +static xcb_generic_event_t *takeEvent(xcb_connection_t *c, int mode) +{ + if (!c) + return NULL; + pthread_mutex_lock(&c->mutex); + xcb_generic_event_t *queuedError = takeQueuedErrorLocked(c); + if (queuedError) { + pthread_mutex_unlock(&c->mutex); + return queuedError; + } + if (!c->display || c->error || c->closing) { + pthread_mutex_unlock(&c->mutex); + return NULL; + } + Display *display = c->display; + c->eventUsers++; + pthread_mutex_unlock(&c->mutex); + + libx11CompatBeginXcbEventAccess(); + XEvent event = {0}; + int haveEvent = mode < 0 || XEventsQueued(display, mode) != 0; + while (haveEvent) { + XNextEvent(display, &event); + pthread_mutex_lock(&c->mutex); + int interrupted = c->closing; + if (event.type == 0 && !interrupted) + queuedError = takeQueuedErrorLocked(c); + pthread_mutex_unlock(&c->mutex); + if (queuedError || interrupted || event.type != 0) + break; + } + libx11CompatEndXcbEventAccess(); + + pthread_mutex_lock(&c->mutex); + int closing = c->closing; + pthread_mutex_unlock(&c->mutex); + xcb_generic_event_t *converted = + haveEvent && !closing && !queuedError ? convertEvent(c, &event) : NULL; + + pthread_mutex_lock(&c->mutex); + if (haveEvent && !closing && !queuedError && !converted) + c->error = XCB_CONN_CLOSED_MEM_INSUFFICIENT; + c->eventUsers--; + pthread_cond_broadcast(&c->eventCond); + pthread_mutex_unlock(&c->mutex); + return queuedError ? queuedError : converted; +} + +xcb_generic_event_t *xcb_poll_for_event(xcb_connection_t *c) +{ + return takeEvent(c, QueuedAfterReading); +} + +xcb_generic_event_t *xcb_poll_for_queued_event(xcb_connection_t *c) +{ + return takeEvent(c, QueuedAlready); +} + +xcb_generic_event_t *xcb_wait_for_event(xcb_connection_t *c) +{ + return takeEvent(c, -1); +} + +xcb_generic_event_t *xcb_poll_for_special_event(xcb_connection_t *c, + xcb_special_event_t *special) +{ + (void) c; + (void) special; + return NULL; +} + +char *xcb_setup_vendor(const xcb_setup_t *s) +{ + return s ? (char *) (s + 1) : NULL; +} + +int xcb_setup_vendor_length(const xcb_setup_t *s) +{ + return s ? s->vendor_len : 0; +} + +xcb_generic_iterator_t xcb_setup_vendor_end(const xcb_setup_t *s) +{ + xcb_generic_iterator_t end = {0}; + if (s) { + end.data = xcb_setup_vendor(s) + s->vendor_len; + end.index = sizeof(*s) + s->vendor_len; + } + return end; +} + +xcb_format_t *xcb_setup_pixmap_formats(const xcb_setup_t *s) +{ + if (!s) + return NULL; + size_t vendor = (s->vendor_len + 3u) & ~3u; + return (xcb_format_t *) ((char *) (s + 1) + vendor); +} + +int xcb_setup_pixmap_formats_length(const xcb_setup_t *s) +{ + return s ? s->pixmap_formats_len : 0; +} + +xcb_format_iterator_t xcb_setup_pixmap_formats_iterator(const xcb_setup_t *s) +{ + xcb_format_iterator_t i = {0}; + if (s) { + i.data = xcb_setup_pixmap_formats(s); + i.rem = s->pixmap_formats_len; + i.index = (char *) i.data - (char *) s; + } + return i; +} + +void xcb_format_next(xcb_format_iterator_t *i) +{ + if (!i || i->rem <= 0) + return; + i->data++; + i->index += sizeof(*i->data); + i->rem--; +} + +xcb_generic_iterator_t xcb_format_end(xcb_format_iterator_t i) +{ + while (i.rem > 0) + xcb_format_next(&i); + xcb_generic_iterator_t end = { + .data = i.data, + .rem = 0, + .index = i.index, + }; + return end; +} + +xcb_screen_iterator_t xcb_setup_roots_iterator(const xcb_setup_t *s) +{ + xcb_screen_iterator_t i = {0}; + if (!s) + return i; + size_t vendor = (s->vendor_len + 3u) & ~3u; + i.data = (xcb_screen_t *) ((char *) (s + 1) + vendor + + s->pixmap_formats_len * sizeof(xcb_format_t)); + i.rem = s->roots_len; + i.index = (char *) i.data - (char *) s; + return i; +} +int xcb_setup_roots_length(const xcb_setup_t *s) +{ + return s ? s->roots_len : 0; +} +xcb_depth_iterator_t xcb_screen_allowed_depths_iterator(const xcb_screen_t *s) +{ + xcb_depth_iterator_t i = {0}; + if (s) { + i.data = (xcb_depth_t *) (s + 1); + i.rem = s->allowed_depths_len; + i.index = sizeof(*s); + } + return i; +} +int xcb_screen_allowed_depths_length(const xcb_screen_t *s) +{ + return s ? s->allowed_depths_len : 0; +} +xcb_visualtype_iterator_t xcb_depth_visuals_iterator(const xcb_depth_t *d) +{ + xcb_visualtype_iterator_t i = {0}; + if (d) { + i.data = (xcb_visualtype_t *) (d + 1); + i.rem = d->visuals_len; + i.index = sizeof(*d); + } + return i; +} +xcb_visualtype_t *xcb_depth_visuals(const xcb_depth_t *d) +{ + return d ? (xcb_visualtype_t *) (d + 1) : NULL; +} +int xcb_depth_sizeof(const void *buffer) +{ + if (!buffer) + return 0; + const xcb_depth_t *depth = buffer; + uint64_t size = sizeof(*depth) + + (uint64_t) depth->visuals_len * sizeof(xcb_visualtype_t); + return size <= INT_MAX ? (int) size : 0; +} +int xcb_depth_visuals_length(const xcb_depth_t *d) +{ + return d ? d->visuals_len : 0; +} +void xcb_depth_next(xcb_depth_iterator_t *i) +{ + if (!i || i->rem <= 0) + return; + size_t bytes = + sizeof(*i->data) + i->data->visuals_len * sizeof(xcb_visualtype_t); + i->data = (xcb_depth_t *) ((char *) i->data + bytes); + i->index += bytes; + i->rem--; +} +xcb_generic_iterator_t xcb_depth_end(xcb_depth_iterator_t i) +{ + while (i.rem > 0) + xcb_depth_next(&i); + xcb_generic_iterator_t end = { + .data = i.data, + .rem = 0, + .index = i.index, + }; + return end; +} +void xcb_visualtype_next(xcb_visualtype_iterator_t *i) +{ + if (!i || i->rem <= 0) + return; + i->data++; + i->index += sizeof(*i->data); + i->rem--; +} +xcb_generic_iterator_t xcb_visualtype_end(xcb_visualtype_iterator_t i) +{ + while (i.rem > 0) + xcb_visualtype_next(&i); + xcb_generic_iterator_t end = { + .data = i.data, + .rem = 0, + .index = i.index, + }; + return end; +} +int xcb_screen_sizeof(const void *buffer) +{ + if (!buffer) + return 0; + const xcb_screen_t *screen = buffer; + const xcb_depth_t *depth = (const xcb_depth_t *) (screen + 1); + uint64_t size = sizeof(*screen); + for (uint8_t i = 0; i < screen->allowed_depths_len; i++) { + int depthSize = xcb_depth_sizeof(depth); + if (!depthSize || size + (uint64_t) depthSize > INT_MAX) + return 0; + size += (uint64_t) depthSize; + depth = (const xcb_depth_t *) ((const char *) depth + depthSize); + } + return (int) size; +} +void xcb_screen_next(xcb_screen_iterator_t *i) +{ + if (!i || i->rem <= 0) + return; + xcb_depth_iterator_t depths = xcb_screen_allowed_depths_iterator(i->data); + xcb_generic_iterator_t end = xcb_depth_end(depths); + size_t bytes = (char *) end.data - (char *) i->data; + i->data = (xcb_screen_t *) ((char *) i->data + bytes); + i->index += bytes; + i->rem--; +} +xcb_generic_iterator_t xcb_screen_end(xcb_screen_iterator_t i) +{ + while (i.rem > 0) + xcb_screen_next(&i); + xcb_generic_iterator_t end = { + .data = i.data, + .rem = 0, + .index = i.index, + }; + return end; +} +int xcb_setup_sizeof(const void *buffer) +{ + if (!buffer) + return 0; + const xcb_setup_t *setup = buffer; + uint64_t vendorSize = ((uint64_t) setup->vendor_len + 3u) & ~UINT64_C(3); + uint64_t size = sizeof(*setup) + vendorSize + + (uint64_t) setup->pixmap_formats_len * sizeof(xcb_format_t); + if (size > INT_MAX) + return 0; + const xcb_screen_t *screen = + (const xcb_screen_t *) ((const char *) setup + (size_t) size); + for (uint8_t i = 0; i < setup->roots_len; i++) { + int screenSize = xcb_screen_sizeof(screen); + if (!screenSize || size + (uint64_t) screenSize > INT_MAX) + return 0; + size += (uint64_t) screenSize; + screen = (const xcb_screen_t *) ((const char *) screen + screenSize); + } + return (int) size; +} +int xcb_flush(xcb_connection_t *c) +{ + if (!c || c->error) + return 0; + XFlush(c->display); + return 1; +} + +Display *xcbCompatDisplay(xcb_connection_t *c) +{ + return c ? c->display : NULL; +} + +xcb_void_cookie_t xcbCompatVoidCookie(xcb_connection_t *c, + uint8_t errorCode, + uint32_t resource, + uint8_t opcode, + int checked) +{ + if (!xcbCompatRequestReady(c)) + return (xcb_void_cookie_t) {.sequence = 0}; + uint64_t sequence = xcbCompatNextSequence(c); + xcb_void_cookie_t cookie = {.sequence = (unsigned int) sequence}; + if (errorCode) + xcbCompatStoreProtocolError(c, sequence, errorCode, resource, opcode, + checked); + else if (checked) + xcbCompatStorePending(c, sequence, NULL, NULL); + return cookie; +} + +void xcbCompatStoreProtocolError(xcb_connection_t *c, + uint64_t sequence, + uint8_t code, + uint32_t resource, + uint8_t opcode, + int checked) +{ + xcb_generic_error_t *error = calloc(1, sizeof(*error)); + if (!error) { + xcbCompatSetConnectionError(c, XCB_CONN_CLOSED_MEM_INSUFFICIENT); + return; + } + error->error_code = code; + error->sequence = sequence; + error->full_sequence = sequence; + error->resource_id = resource; + error->major_code = opcode; + if (checked) { + xcbCompatStorePending(c, sequence, NULL, error); + } else { + QueuedError *queued = calloc(1, sizeof(*queued)); + if (!queued) { + free(error); + xcbCompatSetConnectionError(c, XCB_CONN_CLOSED_MEM_INSUFFICIENT); + } else { + queued->error = error; + pthread_mutex_lock(&c->mutex); + if (c->errorTail) + c->errorTail->next = queued; + else + c->errorHead = queued; + c->errorTail = queued; + pthread_cond_broadcast(&c->eventCond); + pthread_mutex_unlock(&c->mutex); + } + } +} + +uint64_t xcbCompatNextSequence(xcb_connection_t *c) +{ + pthread_mutex_lock(&c->mutex); + uint64_t sequence = c->nextSequence++; + pthread_mutex_unlock(&c->mutex); + return sequence; +} +void xcbCompatSetNextSequence(xcb_connection_t *c, uint64_t sequence) +{ + pthread_mutex_lock(&c->mutex); + c->nextSequence = sequence; + pthread_mutex_unlock(&c->mutex); +} +static size_t pendingBucket(uint64_t sequence, size_t bucketCount) +{ + return (size_t) (sequence ^ (sequence >> 32)) & (bucketCount - 1); +} + +static PendingReply **findPending(xcb_connection_t *c, uint64_t sequence) +{ + PendingReply **link = + &c->pendingBuckets[pendingBucket(sequence, c->pendingBucketCount)]; + while (*link && (*link)->sequence != sequence) + link = &(*link)->next; + return link; +} + +static int growPendingTable(xcb_connection_t *c) +{ + size_t bucketCount = c->pendingBucketCount ? c->pendingBucketCount * 2 : 64; + if (bucketCount < c->pendingBucketCount || + bucketCount > SIZE_MAX / sizeof(*c->pendingBuckets)) + return 0; + PendingReply **buckets = calloc(bucketCount, sizeof(*buckets)); + if (!buckets) + return 0; + for (size_t i = 0; i < c->pendingBucketCount; i++) { + PendingReply *pending = c->pendingBuckets[i]; + while (pending) { + PendingReply *next = pending->next; + size_t bucket = pendingBucket(pending->sequence, bucketCount); + pending->next = buckets[bucket]; + buckets[bucket] = pending; + pending = next; + } + } + free(c->pendingBuckets); + c->pendingBuckets = buckets; + c->pendingBucketCount = bucketCount; + return 1; +} + +static uint64_t widenSequence(xcb_connection_t *c, uint32_t sequence) +{ + uint64_t last = c->nextSequence - 1; + uint64_t widened = (last & ~((uint64_t) UINT32_MAX)) | sequence; + if (widened > last && widened >= ((uint64_t) 1 << 32)) + widened -= (uint64_t) 1 << 32; + return widened; +} +void xcbCompatStorePending(xcb_connection_t *c, + uint64_t sequence, + void *reply, + xcb_generic_error_t *error) +{ + PendingReply *pending = calloc(1, sizeof(*pending)); + if (!pending) { + free(reply); + free(error); + pthread_mutex_lock(&c->mutex); + c->error = XCB_CONN_CLOSED_MEM_INSUFFICIENT; + pthread_mutex_unlock(&c->mutex); + return; + } + pending->sequence = sequence; + pending->reply = reply; + pending->error = error; + pthread_mutex_lock(&c->mutex); + if (!c->pendingBucketCount && !growPendingTable(c)) { + c->error = XCB_CONN_CLOSED_MEM_INSUFFICIENT; + pthread_mutex_unlock(&c->mutex); + free(pending->reply); + free(pending->error); + free(pending); + return; + } + if (c->pendingCount >= c->pendingBucketCount - c->pendingBucketCount / 4) + growPendingTable(c); + PendingReply **link = findPending(c, sequence); + if (*link) { + PendingReply *old = *link; + pending->next = old->next; + *link = pending; + free(old->reply); + free(old->error); + free(old); + } else { + *link = pending; + c->pendingCount++; + } + pthread_mutex_unlock(&c->mutex); +} +xcb_generic_error_t *xcb_request_check(xcb_connection_t *c, + xcb_void_cookie_t cookie) +{ + if (!c) + return NULL; + pthread_mutex_lock(&c->mutex); + if (!c->pendingBucketCount) { + pthread_mutex_unlock(&c->mutex); + return NULL; + } + PendingReply **link = findPending(c, widenSequence(c, cookie.sequence)); + if (!*link) { + pthread_mutex_unlock(&c->mutex); + return NULL; + } + PendingReply *pending = *link; + *link = pending->next; + c->pendingCount--; + pthread_mutex_unlock(&c->mutex); + xcb_generic_error_t *error = pending->error; + free(pending->reply); + free(pending); + return error; +} + +void *xcbCompatTakeReply(xcb_connection_t *c, + uint64_t sequence, + xcb_generic_error_t **error) +{ + if (error) + *error = NULL; + if (!c) + return NULL; + pthread_mutex_lock(&c->mutex); + if (!c->pendingBucketCount) { + pthread_mutex_unlock(&c->mutex); + return NULL; + } + PendingReply **link = findPending(c, sequence); + PendingReply *pending = *link; + if (pending) { + *link = pending->next; + c->pendingCount--; + } + pthread_mutex_unlock(&c->mutex); + if (!pending) + return NULL; + void *reply = pending->reply; + if (error) + *error = pending->error; + else + free(pending->error); + free(pending); + return reply; +} +static void discardReply(xcb_connection_t *c, uint64_t sequence) +{ + if (!c) + return; + pthread_mutex_lock(&c->mutex); + if (!c->pendingBucketCount) { + pthread_mutex_unlock(&c->mutex); + return; + } + PendingReply **link = findPending(c, sequence); + PendingReply *pending = *link; + if (pending) { + *link = pending->next; + c->pendingCount--; + } + pthread_mutex_unlock(&c->mutex); + if (pending) { + free(pending->reply); + free(pending->error); + free(pending); + } +} +void xcb_discard_reply(xcb_connection_t *c, unsigned int sequence) +{ + if (!c) + return; + pthread_mutex_lock(&c->mutex); + uint64_t widened = widenSequence(c, sequence); + pthread_mutex_unlock(&c->mutex); + discardReply(c, widened); +} +void xcb_discard_reply64(xcb_connection_t *c, uint64_t sequence) +{ + discardReply(c, sequence); +} +void xcb_disconnect(xcb_connection_t *c) +{ + if (!c) + return; + pthread_mutex_lock(&c->mutex); + c->closing = 1; + c->error = XCB_CONN_ERROR; + while (c->eventUsers) + pthread_cond_wait(&c->eventCond, &c->mutex); + pthread_mutex_unlock(&c->mutex); + + unregisterConnection(c); + pthread_mutex_lock(&c->mutex); + while (c->closeCallbacks) + pthread_cond_wait(&c->eventCond, &c->mutex); + Display *display = c->display; + int ownsDisplay = c->ownsDisplay; + c->display = NULL; + c->ownsDisplay = 0; + pthread_mutex_unlock(&c->mutex); + + xcbCompatReleaseRequestResources(c); + if (display && ownsDisplay) + XCloseDisplay(display); + pthread_mutex_lock(&c->mutex); + drainPendingLocked(c); + pthread_mutex_unlock(&c->mutex); + free(c->pendingBuckets); + pthread_cond_destroy(&c->eventCond); + pthread_mutex_destroy(&c->mutex); + free(c); +} diff --git a/compat/xcb-requests.c b/compat/xcb-requests.c new file mode 100644 index 00000000..92cff0fd --- /dev/null +++ b/compat/xcb-requests.c @@ -0,0 +1,2313 @@ +#include +#include +#include +#include +#include +#include +#include +#include +#include "xcb-compat-private.h" +#include "../src/atoms.h" +#include "../src/drawing.h" +#include "../src/gc.h" +#include "../src/resource-types.h" +#include "../src/window.h" + +typedef struct GcEntry { + xcb_gcontext_t id; + GC gc; + Display *display; + xcb_connection_t *owner; + unsigned int depth; + struct GcEntry *next; +} GcEntry; +static GcEntry *gcEntries; +static pthread_mutex_t gcMutex = PTHREAD_MUTEX_INITIALIZER; +static _Atomic int failNextReplyAllocation; + +void xcbCompatFailNextReplyAllocationForTest(void) +{ + atomic_store_explicit(&failNextReplyAllocation, 1, memory_order_release); +} + +static GcEntry *findGcLocked(xcb_gcontext_t id) +{ + for (GcEntry *entry = gcEntries; entry; entry = entry->next) + if (entry->id == id) + return entry; + return NULL; +} + +void xcbCompatReleaseRequestResources(xcb_connection_t *connection) +{ + pthread_mutex_lock(&gcMutex); + GcEntry **link = &gcEntries; + while (*link) { + GcEntry *entry = *link; + if (entry->owner != connection) { + link = &entry->next; + continue; + } + *link = entry->next; + XFreeGC(entry->display, entry->gc); + free(entry); + } + pthread_mutex_unlock(&gcMutex); +} + +static int isDrawable(xcb_drawable_t drawable) +{ + return IS_TYPE(drawable, DRAWABLE); +} + +/* InputOnly windows are drawables the protocol refuses to render into, read + * from or copy between: they carry no pixels. Xlib reports that as BadMatch, + * and so must every request that reaches a drawing path. + */ +static int isInputOnlyDrawable(xcb_drawable_t drawable) +{ + return IS_TYPE(drawable, WINDOW) && IS_INPUT_ONLY(drawable); +} + +static unsigned int drawableDepth(xcb_connection_t *c, xcb_drawable_t drawable) +{ + if (IS_TYPE(drawable, PIXMAP)) + return GET_PIXMAP_STRUCT(drawable)->depth; + WindowStruct *window = GET_WINDOW_STRUCT(drawable); + return window->depth == XCB_COPY_FROM_PARENT + ? (unsigned int) DefaultDepth(xcbCompatDisplay(c), + DefaultScreen(xcbCompatDisplay(c))) + : (unsigned int) window->depth; +} + +static unsigned int nextSequence(xcb_connection_t *c) +{ + return (unsigned int) xcbCompatNextSequence(c); +} + +#define REQUIRE_REQUEST(connection, cookieType) \ + do { \ + if (!xcbCompatRequestReady(connection)) \ + return (cookieType) {.sequence = 0}; \ + } while (0) + +static void storeReply(xcb_connection_t *c, unsigned int sequence, void *reply) +{ + if (!reply) { + xcbCompatSetConnectionError(c, XCB_CONN_CLOSED_MEM_INSUFFICIENT); + return; + } + ((xcb_generic_reply_t *) reply)->sequence = sequence; + xcbCompatStorePending(c, sequence, reply, NULL); +} + +static void *allocateReplyPayload(xcb_connection_t *c, + size_t headerSize, + uint64_t count, + size_t itemSize, + size_t *payloadSize) +{ + if (atomic_exchange_explicit(&failNextReplyAllocation, 0, + memory_order_acq_rel)) { + xcbCompatSetConnectionError(c, XCB_CONN_CLOSED_MEM_INSUFFICIENT); + return NULL; + } + if (itemSize && count > (SIZE_MAX - headerSize) / itemSize) { + xcbCompatSetConnectionError(c, XCB_CONN_CLOSED_MEM_INSUFFICIENT); + return NULL; + } + size_t payload = (size_t) count * itemSize; + void *reply = calloc(1, headerSize + payload); + if (!reply) + xcbCompatSetConnectionError(c, XCB_CONN_CLOSED_MEM_INSUFFICIENT); + if (payloadSize) + *payloadSize = payload; + return reply; +} + +static int requestPayloadFits(xcb_connection_t *c, + size_t headerSize, + uint64_t count, + size_t itemSize) +{ + uint64_t payload = count * itemSize; + if (itemSize && payload / itemSize != count) + return 0; + if (payload > UINT64_MAX - 3u) + return 0; + uint64_t paddedPayload = (payload + 3u) & ~UINT64_C(3); + uint64_t maximumBytes = (uint64_t) xcb_get_maximum_request_length(c) * 4u; + return headerSize <= maximumBytes && + paddedPayload <= maximumBytes - headerSize; +} + +static int isWindow(xcb_window_t window) +{ + return IS_TYPE(window, WINDOW); +} + +#define XCB_CW_VALID_MASK \ + (XCB_CW_BACK_PIXMAP | XCB_CW_BACK_PIXEL | XCB_CW_BORDER_PIXMAP | \ + XCB_CW_BORDER_PIXEL | XCB_CW_BIT_GRAVITY | XCB_CW_WIN_GRAVITY | \ + XCB_CW_BACKING_STORE | XCB_CW_BACKING_PLANES | XCB_CW_BACKING_PIXEL | \ + XCB_CW_OVERRIDE_REDIRECT | XCB_CW_SAVE_UNDER | XCB_CW_EVENT_MASK | \ + XCB_CW_DONT_PROPAGATE | XCB_CW_COLORMAP | XCB_CW_CURSOR) + +#define XCB_CONFIG_WINDOW_VALID_MASK \ + (XCB_CONFIG_WINDOW_X | XCB_CONFIG_WINDOW_Y | XCB_CONFIG_WINDOW_WIDTH | \ + XCB_CONFIG_WINDOW_HEIGHT | XCB_CONFIG_WINDOW_BORDER_WIDTH | \ + XCB_CONFIG_WINDOW_SIBLING | XCB_CONFIG_WINDOW_STACK_MODE) + +#define XCB_GC_VALID_MASK \ + (XCB_GC_FUNCTION | XCB_GC_PLANE_MASK | XCB_GC_FOREGROUND | \ + XCB_GC_BACKGROUND | XCB_GC_LINE_WIDTH | XCB_GC_LINE_STYLE | \ + XCB_GC_CAP_STYLE | XCB_GC_JOIN_STYLE | XCB_GC_FILL_STYLE | \ + XCB_GC_FILL_RULE | XCB_GC_TILE | XCB_GC_STIPPLE | \ + XCB_GC_TILE_STIPPLE_ORIGIN_X | XCB_GC_TILE_STIPPLE_ORIGIN_Y | \ + XCB_GC_FONT | XCB_GC_SUBWINDOW_MODE | XCB_GC_GRAPHICS_EXPOSURES | \ + XCB_GC_CLIP_ORIGIN_X | XCB_GC_CLIP_ORIGIN_Y | XCB_GC_CLIP_MASK | \ + XCB_GC_DASH_OFFSET | XCB_GC_DASH_LIST | XCB_GC_ARC_MODE) + +xcb_intern_atom_cookie_t xcb_intern_atom(xcb_connection_t *c, + uint8_t onlyIfExists, + uint16_t nameLength, + const char *name) +{ + REQUIRE_REQUEST(c, xcb_intern_atom_cookie_t); + xcb_intern_atom_cookie_t cookie = {nextSequence(c)}; + char *copy = malloc((size_t) nameLength + 1); + xcb_intern_atom_reply_t *reply = calloc(1, sizeof(*reply)); + if (copy && reply) { + memcpy(copy, name, nameLength); + copy[nameLength] = '\0'; + reply->response_type = 1; + reply->atom = XInternAtom(xcbCompatDisplay(c), copy, onlyIfExists); + storeReply(c, cookie.sequence, reply); + } else { + free(reply); + xcbCompatSetConnectionError(c, XCB_CONN_CLOSED_MEM_INSUFFICIENT); + } + free(copy); + return cookie; +} + +xcb_intern_atom_cookie_t xcb_intern_atom_unchecked(xcb_connection_t *c, + uint8_t onlyIfExists, + uint16_t nameLength, + const char *name) +{ + return xcb_intern_atom(c, onlyIfExists, nameLength, name); +} + +xcb_intern_atom_reply_t *xcb_intern_atom_reply(xcb_connection_t *c, + xcb_intern_atom_cookie_t cookie, + xcb_generic_error_t **error) +{ + return xcbCompatTakeReply(c, cookie.sequence, error); +} + +static xcb_get_atom_name_cookie_t xcbGetAtomName(xcb_connection_t *c, + xcb_atom_t atom, + int checked) +{ + REQUIRE_REQUEST(c, xcb_get_atom_name_cookie_t); + xcb_get_atom_name_cookie_t cookie = {nextSequence(c)}; + if (!isValidAtom(atom)) { + xcbCompatStoreProtocolError(c, cookie.sequence, XCB_ATOM, atom, + XCB_GET_ATOM_NAME, checked); + return cookie; + } + char *name = XGetAtomName(xcbCompatDisplay(c), atom); + size_t length = name ? strlen(name) : 0; + size_t paddedLength = length <= SIZE_MAX - 3 ? (length + 3u) & ~3u : 0; + xcb_get_atom_name_reply_t *reply = + name && paddedLength >= length + ? allocateReplyPayload(c, sizeof(*reply), paddedLength, 1, NULL) + : NULL; + if (reply) { + reply->response_type = 1; + reply->name_len = length; + reply->length = (length + 3) / 4; + memcpy(reply + 1, name, length); + } + free(name); + storeReply(c, cookie.sequence, reply); + return cookie; +} + +xcb_get_atom_name_cookie_t xcb_get_atom_name(xcb_connection_t *c, + xcb_atom_t atom) +{ + return xcbGetAtomName(c, atom, 1); +} + +xcb_get_atom_name_cookie_t xcb_get_atom_name_unchecked(xcb_connection_t *c, + xcb_atom_t atom) +{ + return xcbGetAtomName(c, atom, 0); +} + +xcb_get_atom_name_reply_t *xcb_get_atom_name_reply( + xcb_connection_t *c, + xcb_get_atom_name_cookie_t cookie, + xcb_generic_error_t **error) +{ + return xcbCompatTakeReply(c, cookie.sequence, error); +} +int xcb_get_atom_name_sizeof(const void *buffer) +{ + const xcb_get_atom_name_reply_t *reply = buffer; + return reply ? (int) (sizeof(*reply) + reply->name_len) : 0; +} +char *xcb_get_atom_name_name(const xcb_get_atom_name_reply_t *reply) +{ + return reply ? (char *) (reply + 1) : NULL; +} +int xcb_get_atom_name_name_length(const xcb_get_atom_name_reply_t *reply) +{ + return reply ? reply->name_len : 0; +} +xcb_generic_iterator_t xcb_get_atom_name_name_end( + const xcb_get_atom_name_reply_t *reply) +{ + xcb_generic_iterator_t end = {0}; + if (reply) { + end.data = xcb_get_atom_name_name(reply) + reply->name_len; + end.index = sizeof(*reply) + reply->name_len; + } + return end; +} + +static void decodeAttributes(uint32_t mask, + const uint32_t *values, + XSetWindowAttributes *a) +{ + memset(a, 0, sizeof(*a)); + unsigned int n = 0; +#define TAKE(bit, field) \ + do { \ + if (mask & (bit)) \ + a->field = values[n++]; \ + } while (0) + TAKE(XCB_CW_BACK_PIXMAP, background_pixmap); + TAKE(XCB_CW_BACK_PIXEL, background_pixel); + TAKE(XCB_CW_BORDER_PIXMAP, border_pixmap); + TAKE(XCB_CW_BORDER_PIXEL, border_pixel); + TAKE(XCB_CW_BIT_GRAVITY, bit_gravity); + TAKE(XCB_CW_WIN_GRAVITY, win_gravity); + TAKE(XCB_CW_BACKING_STORE, backing_store); + TAKE(XCB_CW_BACKING_PLANES, backing_planes); + TAKE(XCB_CW_BACKING_PIXEL, backing_pixel); + TAKE(XCB_CW_OVERRIDE_REDIRECT, override_redirect); + TAKE(XCB_CW_SAVE_UNDER, save_under); + TAKE(XCB_CW_EVENT_MASK, event_mask); + TAKE(XCB_CW_DONT_PROPAGATE, do_not_propagate_mask); + TAKE(XCB_CW_COLORMAP, colormap); + TAKE(XCB_CW_CURSOR, cursor); +#undef TAKE +} + +static uint8_t validateAttributes(uint32_t mask, + const XSetWindowAttributes *attributes, + unsigned int windowDepth, + int inputOnly, + uint32_t *errorValue) +{ + const uint32_t inputOnlyMask = XCB_CW_WIN_GRAVITY | XCB_CW_EVENT_MASK | + XCB_CW_DONT_PROPAGATE | + XCB_CW_OVERRIDE_REDIRECT | XCB_CW_CURSOR; + if (inputOnly && (mask & ~inputOnlyMask)) { + *errorValue = mask & ~inputOnlyMask; + return XCB_MATCH; + } + if ((mask & XCB_CW_BIT_GRAVITY) && + attributes->bit_gravity > XCB_GRAVITY_STATIC) { + *errorValue = attributes->bit_gravity; + return XCB_VALUE; + } + if ((mask & XCB_CW_WIN_GRAVITY) && + attributes->win_gravity > XCB_GRAVITY_STATIC) { + *errorValue = attributes->win_gravity; + return XCB_VALUE; + } + if ((mask & XCB_CW_BACKING_STORE) && + attributes->backing_store > XCB_BACKING_STORE_ALWAYS) { + *errorValue = attributes->backing_store; + return XCB_VALUE; + } + if ((mask & XCB_CW_OVERRIDE_REDIRECT) && + attributes->override_redirect != False && + attributes->override_redirect != True) { + *errorValue = attributes->override_redirect; + return XCB_VALUE; + } + if ((mask & XCB_CW_SAVE_UNDER) && attributes->save_under != False && + attributes->save_under != True) { + *errorValue = attributes->save_under; + return XCB_VALUE; + } + const uint32_t validEventMask = (XCB_EVENT_MASK_OWNER_GRAB_BUTTON << 1) - 1; + if ((mask & XCB_CW_EVENT_MASK) && + ((uint32_t) attributes->event_mask & ~validEventMask)) { + *errorValue = attributes->event_mask; + return XCB_VALUE; + } + const uint32_t validDoNotPropagate = + XCB_EVENT_MASK_KEY_PRESS | XCB_EVENT_MASK_KEY_RELEASE | + XCB_EVENT_MASK_BUTTON_PRESS | XCB_EVENT_MASK_BUTTON_RELEASE | + XCB_EVENT_MASK_POINTER_MOTION | XCB_EVENT_MASK_BUTTON_1_MOTION | + XCB_EVENT_MASK_BUTTON_2_MOTION | XCB_EVENT_MASK_BUTTON_3_MOTION | + XCB_EVENT_MASK_BUTTON_4_MOTION | XCB_EVENT_MASK_BUTTON_5_MOTION | + XCB_EVENT_MASK_BUTTON_MOTION; + if ((mask & XCB_CW_DONT_PROPAGATE) && + ((uint32_t) attributes->do_not_propagate_mask & ~validDoNotPropagate)) { + *errorValue = attributes->do_not_propagate_mask; + return XCB_VALUE; + } + const struct { + uint32_t bit; + Pixmap pixmap; + int parentRelativeAllowed; + } pixmaps[] = { + {XCB_CW_BACK_PIXMAP, attributes->background_pixmap, 1}, + {XCB_CW_BORDER_PIXMAP, attributes->border_pixmap, 0}, + }; + for (size_t i = 0; i < sizeof(pixmaps) / sizeof(pixmaps[0]); i++) { + if (!(mask & pixmaps[i].bit) || pixmaps[i].pixmap == XCB_NONE || + (pixmaps[i].parentRelativeAllowed && + pixmaps[i].pixmap == XCB_BACK_PIXMAP_PARENT_RELATIVE)) + continue; + *errorValue = pixmaps[i].pixmap; + if (!IS_TYPE(pixmaps[i].pixmap, PIXMAP)) + return XCB_PIXMAP; + if (GET_PIXMAP_STRUCT(pixmaps[i].pixmap)->depth != windowDepth) + return XCB_MATCH; + } + if ((mask & XCB_CW_COLORMAP) && attributes->colormap != XCB_NONE && + !IS_TYPE(attributes->colormap, COLORMAP)) { + *errorValue = attributes->colormap; + return XCB_COLORMAP; + } + if ((mask & XCB_CW_CURSOR) && attributes->cursor != XCB_NONE && + !IS_TYPE(attributes->cursor, CURSOR)) { + *errorValue = attributes->cursor; + return XCB_CURSOR; + } + return 0; +} + +static xcb_void_cookie_t createWindow(xcb_connection_t *c, + uint8_t depth, + xcb_window_t wid, + xcb_window_t parent, + int16_t x, + int16_t y, + uint16_t width, + uint16_t height, + uint16_t borderWidth, + uint16_t clazz, + xcb_visualid_t visual, + uint32_t mask, + const uint32_t *values, + int checked) +{ + REQUIRE_REQUEST(c, xcb_void_cookie_t); + uint8_t error = 0; + uint32_t errorValue = wid; + if (!isWindow(parent)) { + error = XCB_WINDOW; + errorValue = parent; + } + if (!error && getXidStruct(wid)->type != 0) + error = XCB_ID_CHOICE; + if (!error && (mask & ~XCB_CW_VALID_MASK)) { + error = XCB_VALUE; + errorValue = mask; + } + if (!error && mask && !values) { + error = XCB_VALUE; + errorValue = mask; + } + if (!error && clazz > XCB_WINDOW_CLASS_INPUT_ONLY) { + error = XCB_VALUE; + errorValue = clazz; + } + if (!error && !width) { + error = XCB_VALUE; + errorValue = width; + } + if (!error && !height) { + error = XCB_VALUE; + errorValue = height; + } + int parentInputOnly = !error && IS_INPUT_ONLY(parent); + int inputOnly = + clazz == XCB_WINDOW_CLASS_INPUT_ONLY || + (clazz == XCB_WINDOW_CLASS_COPY_FROM_PARENT && parentInputOnly); + if (!error && clazz == XCB_WINDOW_CLASS_INPUT_OUTPUT && parentInputOnly) { + error = XCB_MATCH; + errorValue = parent; + } + if (!error && inputOnly && borderWidth != 0) { + error = XCB_MATCH; + errorValue = borderWidth; + } + unsigned int defaultDepth = 0; + xcb_visualid_t defaultVisual = XCB_NONE; + if (!error) { + defaultDepth = (unsigned int) DefaultDepth( + xcbCompatDisplay(c), DefaultScreen(xcbCompatDisplay(c))); + defaultVisual = XVisualIDFromVisual(DefaultVisual( + xcbCompatDisplay(c), DefaultScreen(xcbCompatDisplay(c)))); + } + if (!error && inputOnly && depth != XCB_COPY_FROM_PARENT) { + error = XCB_MATCH; + errorValue = depth; + } + if (!error && inputOnly && visual != XCB_COPY_FROM_PARENT) { + error = XCB_MATCH; + errorValue = visual; + } + if (!error && !inputOnly && depth != XCB_COPY_FROM_PARENT && + depth != defaultDepth) { + error = XCB_MATCH; + errorValue = depth; + } + if (!error && !inputOnly && visual != XCB_COPY_FROM_PARENT && + visual != defaultVisual) { + error = XCB_MATCH; + errorValue = visual; + } + if (!error) { + XSetWindowAttributes attributes; + decodeAttributes(mask, values, &attributes); + unsigned int windowDepth = + depth == XCB_COPY_FROM_PARENT ? drawableDepth(c, parent) : depth; + error = validateAttributes(mask, &attributes, windowDepth, inputOnly, + &errorValue); + if (error) + goto done; + Visual *xvisual = visual == XCB_COPY_FROM_PARENT + ? CopyFromParent + : DefaultVisual(xcbCompatDisplay(c), 0); + + /* Claim a client-chosen id last: every check above can still fail, and + * an id reserved before them would have to be handed back. From here + * the creator owns it and releases it itself if it fails. + */ + if (!isXidAllocated(wid) && !reserveXidResource(wid)) + error = XCB_ID_CHOICE; + else if (libx11CompatCreateWindowWithId( + xcbCompatDisplay(c), wid, parent, x, y, width, height, + borderWidth, depth, clazz, xvisual, mask, + &attributes) != wid) + error = XCB_VALUE; + } +done: + return xcbCompatVoidCookie(c, error, errorValue, XCB_CREATE_WINDOW, + checked); +} + +xcb_void_cookie_t xcb_create_window_checked(xcb_connection_t *c, + uint8_t depth, + xcb_window_t wid, + xcb_window_t parent, + int16_t x, + int16_t y, + uint16_t width, + uint16_t height, + uint16_t borderWidth, + uint16_t clazz, + xcb_visualid_t visual, + uint32_t mask, + const void *values) +{ + return createWindow(c, depth, wid, parent, x, y, width, height, borderWidth, + clazz, visual, mask, values, 1); +} +xcb_void_cookie_t xcb_create_window(xcb_connection_t *c, + uint8_t depth, + xcb_window_t wid, + xcb_window_t parent, + int16_t x, + int16_t y, + uint16_t width, + uint16_t height, + uint16_t borderWidth, + uint16_t clazz, + xcb_visualid_t visual, + uint32_t mask, + const void *values) +{ + return createWindow(c, depth, wid, parent, x, y, width, height, borderWidth, + clazz, visual, mask, values, 0); +} + +#define WINDOW_VOID(name, checked_name, xfunc, opcode) \ + static xcb_void_cookie_t name##Impl(xcb_connection_t *c, xcb_window_t w, \ + int checked) \ + { \ + REQUIRE_REQUEST(c, xcb_void_cookie_t); \ + uint8_t e = isWindow(w) ? 0 : XCB_WINDOW; \ + if (!e) \ + xfunc(xcbCompatDisplay(c), w); \ + return xcbCompatVoidCookie(c, e, w, opcode, checked); \ + } \ + xcb_void_cookie_t name(xcb_connection_t *c, xcb_window_t w) \ + { \ + return name##Impl(c, w, 0); \ + } \ + xcb_void_cookie_t checked_name(xcb_connection_t *c, xcb_window_t w) \ + { \ + return name##Impl(c, w, 1); \ + } +WINDOW_VOID(xcb_map_window, xcb_map_window_checked, XMapWindow, XCB_MAP_WINDOW) +WINDOW_VOID(xcb_unmap_window, + xcb_unmap_window_checked, + XUnmapWindow, + XCB_UNMAP_WINDOW) +WINDOW_VOID(xcb_destroy_window, + xcb_destroy_window_checked, + XDestroyWindow, + XCB_DESTROY_WINDOW) + +static xcb_get_window_attributes_cookie_t +getWindowAttributes(xcb_connection_t *c, xcb_window_t w, int checked) +{ + REQUIRE_REQUEST(c, xcb_get_window_attributes_cookie_t); + xcb_get_window_attributes_cookie_t cookie = {nextSequence(c)}; + if (!isWindow(w)) { + xcbCompatStoreProtocolError(c, cookie.sequence, XCB_WINDOW, w, + XCB_GET_WINDOW_ATTRIBUTES, checked); + return cookie; + } + xcb_get_window_attributes_reply_t *reply = calloc(1, sizeof(*reply)); + XWindowAttributes a; + if (reply && XGetWindowAttributes(xcbCompatDisplay(c), w, &a)) { + reply->response_type = 1; + reply->visual = XVisualIDFromVisual(a.visual); + reply->_class = a.class; + reply->bit_gravity = a.bit_gravity; + reply->win_gravity = a.win_gravity; + reply->backing_store = a.backing_store; + reply->backing_planes = a.backing_planes; + reply->backing_pixel = a.backing_pixel; + reply->save_under = a.save_under; + reply->map_is_installed = a.map_installed; + reply->map_state = a.map_state; + reply->override_redirect = a.override_redirect; + reply->colormap = a.colormap; + reply->all_event_masks = a.all_event_masks; + reply->your_event_mask = a.your_event_mask; + reply->do_not_propagate_mask = a.do_not_propagate_mask; + } else { + free(reply); + reply = NULL; + } + storeReply(c, cookie.sequence, reply); + return cookie; +} +xcb_get_window_attributes_cookie_t xcb_get_window_attributes( + xcb_connection_t *c, + xcb_window_t w) +{ + return getWindowAttributes(c, w, 1); +} +xcb_get_window_attributes_cookie_t xcb_get_window_attributes_unchecked( + xcb_connection_t *c, + xcb_window_t w) +{ + return getWindowAttributes(c, w, 0); +} +xcb_get_window_attributes_reply_t *xcb_get_window_attributes_reply( + xcb_connection_t *c, + xcb_get_window_attributes_cookie_t cookie, + xcb_generic_error_t **e) +{ + return xcbCompatTakeReply(c, cookie.sequence, e); +} + +static xcb_get_geometry_cookie_t getGeometry(xcb_connection_t *c, + xcb_drawable_t d, + int checked) +{ + REQUIRE_REQUEST(c, xcb_get_geometry_cookie_t); + xcb_get_geometry_cookie_t cookie = {nextSequence(c)}; + if (!isDrawable(d)) { + xcbCompatStoreProtocolError(c, cookie.sequence, XCB_DRAWABLE, d, + XCB_GET_GEOMETRY, checked); + return cookie; + } + Window root; + int x, y; + unsigned int width, height, border, depth; + xcb_get_geometry_reply_t *reply = calloc(1, sizeof(*reply)); + if (reply && XGetGeometry(xcbCompatDisplay(c), d, &root, &x, &y, &width, + &height, &border, &depth)) { + reply->response_type = 1; + reply->root = root; + reply->x = x; + reply->y = y; + reply->width = width; + reply->height = height; + reply->border_width = border; + reply->depth = depth; + } else { + free(reply); + reply = NULL; + } + storeReply(c, cookie.sequence, reply); + return cookie; +} +xcb_get_geometry_cookie_t xcb_get_geometry(xcb_connection_t *c, + xcb_drawable_t d) +{ + return getGeometry(c, d, 1); +} +xcb_get_geometry_cookie_t xcb_get_geometry_unchecked(xcb_connection_t *c, + xcb_drawable_t d) +{ + return getGeometry(c, d, 0); +} +xcb_get_geometry_reply_t *xcb_get_geometry_reply( + xcb_connection_t *c, + xcb_get_geometry_cookie_t cookie, + xcb_generic_error_t **e) +{ + return xcbCompatTakeReply(c, cookie.sequence, e); +} + +static xcb_query_tree_cookie_t queryTree(xcb_connection_t *c, + xcb_window_t w, + int checked) +{ + REQUIRE_REQUEST(c, xcb_query_tree_cookie_t); + xcb_query_tree_cookie_t cookie = {nextSequence(c)}; + if (!isWindow(w)) { + xcbCompatStoreProtocolError(c, cookie.sequence, XCB_WINDOW, w, + XCB_QUERY_TREE, checked); + return cookie; + } + Window root, parent, *children = NULL; + unsigned int count = 0; + int ok = + XQueryTree(xcbCompatDisplay(c), w, &root, &parent, &children, &count); + xcb_query_tree_reply_t *reply = + ok ? allocateReplyPayload(c, sizeof(*reply), count, + sizeof(xcb_window_t), NULL) + : NULL; + if (reply) { + reply->response_type = 1; + reply->root = root; + reply->parent = parent; + reply->children_len = count; + reply->length = count; + for (unsigned int i = 0; i < count; i++) + ((xcb_window_t *) (reply + 1))[i] = children[i]; + } + free(children); + storeReply(c, cookie.sequence, reply); + return cookie; +} +xcb_query_tree_cookie_t xcb_query_tree(xcb_connection_t *c, xcb_window_t w) +{ + return queryTree(c, w, 1); +} +xcb_query_tree_cookie_t xcb_query_tree_unchecked(xcb_connection_t *c, + xcb_window_t w) +{ + return queryTree(c, w, 0); +} +xcb_query_tree_reply_t *xcb_query_tree_reply(xcb_connection_t *c, + xcb_query_tree_cookie_t cookie, + xcb_generic_error_t **e) +{ + return xcbCompatTakeReply(c, cookie.sequence, e); +} +int xcb_query_tree_sizeof(const void *buffer) +{ + const xcb_query_tree_reply_t *reply = buffer; + return reply ? (int) (sizeof(*reply) + + (size_t) reply->children_len * sizeof(xcb_window_t)) + : 0; +} +xcb_window_t *xcb_query_tree_children(const xcb_query_tree_reply_t *r) +{ + return r ? (xcb_window_t *) (r + 1) : NULL; +} +int xcb_query_tree_children_length(const xcb_query_tree_reply_t *r) +{ + return r ? r->children_len : 0; +} +xcb_generic_iterator_t xcb_query_tree_children_end( + const xcb_query_tree_reply_t *reply) +{ + xcb_generic_iterator_t end = {0}; + if (reply) { + end.data = xcb_query_tree_children(reply) + reply->children_len; + end.index = xcb_query_tree_sizeof(reply); + } + return end; +} + +static xcb_void_cookie_t changeProperty(xcb_connection_t *c, + uint8_t mode, + xcb_window_t window, + xcb_atom_t property, + xcb_atom_t type, + uint8_t format, + uint32_t length, + const void *data, + int checked) +{ + REQUIRE_REQUEST(c, xcb_void_cookie_t); + uint8_t error = 0; + uint32_t errorValue = window; + if (!isWindow(window)) + error = XCB_WINDOW; + if (!error && !isValidAtom(property)) { + error = XCB_ATOM; + errorValue = property; + } + if (!error && !isValidAtom(type)) { + error = XCB_ATOM; + errorValue = type; + } + if (!error && mode > XCB_PROP_MODE_APPEND) { + error = XCB_VALUE; + errorValue = mode; + } + if (!error && format != 8 && format != 16 && format != 32) { + error = XCB_VALUE; + errorValue = format; + } + if (!error && !requestPayloadFits(c, sizeof(xcb_change_property_request_t), + length, format / 8u)) { + error = XCB_LENGTH; + errorValue = length; + } + if (!error && length && !data) { + error = XCB_VALUE; + errorValue = length; + } + unsigned long *wide = NULL; + const unsigned char *xdata = data; + if (!error && format == 32 && length) { + wide = calloc(length, sizeof(*wide)); + if (!wide) + error = XCB_ALLOC; + for (uint32_t i = 0; wide && i < length; i++) + wide[i] = ((const uint32_t *) data)[i]; + xdata = (const unsigned char *) wide; + } + if (!error && !XChangeProperty(xcbCompatDisplay(c), window, property, type, + format, mode, xdata, length)) + error = XCB_MATCH; + free(wide); + return xcbCompatVoidCookie(c, error, errorValue, XCB_CHANGE_PROPERTY, + checked); +} + +xcb_void_cookie_t xcb_change_property(xcb_connection_t *c, + uint8_t mode, + xcb_window_t window, + xcb_atom_t property, + xcb_atom_t type, + uint8_t format, + uint32_t length, + const void *data) +{ + return changeProperty(c, mode, window, property, type, format, length, data, + 0); +} +xcb_void_cookie_t xcb_change_property_checked(xcb_connection_t *c, + uint8_t mode, + xcb_window_t window, + xcb_atom_t property, + xcb_atom_t type, + uint8_t format, + uint32_t length, + const void *data) +{ + return changeProperty(c, mode, window, property, type, format, length, data, + 1); +} + +static xcb_get_property_cookie_t getProperty(xcb_connection_t *c, + uint8_t delete, + xcb_window_t window, + xcb_atom_t property, + xcb_atom_t type, + uint32_t offset, + uint32_t length, + int checked) +{ + REQUIRE_REQUEST(c, xcb_get_property_cookie_t); + xcb_get_property_cookie_t cookie = {nextSequence(c)}; + if (!isWindow(window)) { + xcbCompatStoreProtocolError(c, cookie.sequence, XCB_WINDOW, window, + XCB_GET_PROPERTY, checked); + return cookie; + } + if (!isValidAtom(property) || + (type != XCB_GET_PROPERTY_TYPE_ANY && !isValidAtom(type))) { + xcbCompatStoreProtocolError(c, cookie.sequence, XCB_ATOM, + !isValidAtom(property) ? property : type, + XCB_GET_PROPERTY, checked); + return cookie; + } + Atom actualType; + int format; + unsigned long count, after; + unsigned char *value = NULL; + int status = XGetWindowProperty(xcbCompatDisplay(c), window, property, + offset, length, delete, type, &actualType, + &format, &count, &after, &value); + + /* The wire pads a reply payload to a 4-byte boundary and length counts + * those words, so an 8- or 16-bit property whose bytes do not divide by + * four needs the padding allocated too. A client that trusts length and + * reads length * 4 bytes would otherwise run off the end of the block. + */ + size_t itemSize = format ? (unsigned int) format / 8 : 0; + uint64_t valueBytes = (uint64_t) count * itemSize; + uint64_t paddedBytes = + valueBytes <= UINT64_MAX - 3 ? (valueBytes + 3u) & ~UINT64_C(3) : 0; + size_t bytes = (size_t) valueBytes; + xcb_get_property_reply_t *reply = + status == Success && paddedBytes >= valueBytes + ? allocateReplyPayload(c, sizeof(*reply), paddedBytes, 1, NULL) + : NULL; + if (reply) { + reply->response_type = 1; + reply->format = format; + reply->type = actualType; + reply->bytes_after = after; + reply->value_len = count; + reply->length = (uint32_t) (paddedBytes / 4); + if (format == 32) { + for (unsigned long i = 0; i < count; i++) + ((uint32_t *) (reply + 1))[i] = ((unsigned long *) value)[i]; + } else if (bytes) { + memcpy(reply + 1, value, bytes); + } + } + free(value); + if (status == Success) + storeReply(c, cookie.sequence, reply); + else + xcbCompatStoreProtocolError(c, cookie.sequence, (uint8_t) status, + window, XCB_GET_PROPERTY, checked); + return cookie; +} +xcb_get_property_cookie_t xcb_get_property(xcb_connection_t *c, + uint8_t delete, + xcb_window_t window, + xcb_atom_t property, + xcb_atom_t type, + uint32_t offset, + uint32_t length) +{ + return getProperty(c, delete, window, property, type, offset, length, 1); +} +xcb_get_property_cookie_t xcb_get_property_unchecked(xcb_connection_t *c, + uint8_t delete, + xcb_window_t window, + xcb_atom_t property, + xcb_atom_t type, + uint32_t offset, + uint32_t length) +{ + return getProperty(c, delete, window, property, type, offset, length, 0); +} +xcb_get_property_reply_t *xcb_get_property_reply( + xcb_connection_t *c, + xcb_get_property_cookie_t cookie, + xcb_generic_error_t **error) +{ + return xcbCompatTakeReply(c, cookie.sequence, error); +} +static int propertyValueBytes(const xcb_get_property_reply_t *reply) +{ + if (!reply) + return 0; + uint64_t bytes = (uint64_t) reply->value_len * (reply->format / 8u); + return bytes <= INT_MAX ? (int) bytes : -1; +} +int xcb_get_property_sizeof(const void *buffer) +{ + const xcb_get_property_reply_t *reply = buffer; + int bytes = propertyValueBytes(reply); + if (!reply || bytes < 0) + return 0; + uint64_t padded = ((uint64_t) bytes + 3u) & ~UINT64_C(3); + return padded <= INT_MAX - sizeof(*reply) + ? (int) sizeof(*reply) + (int) padded + : 0; +} +void *xcb_get_property_value(const xcb_get_property_reply_t *reply) +{ + return reply ? (void *) (reply + 1) : NULL; +} +int xcb_get_property_value_length(const xcb_get_property_reply_t *reply) +{ + int bytes = propertyValueBytes(reply); + return bytes >= 0 ? bytes : 0; +} +xcb_generic_iterator_t xcb_get_property_value_end( + const xcb_get_property_reply_t *reply) +{ + xcb_generic_iterator_t end = {0}; + int bytes = propertyValueBytes(reply); + if (reply && bytes >= 0) { + end.data = (char *) xcb_get_property_value(reply) + bytes; + end.index = sizeof(*reply) + bytes; + } + return end; +} + +static xcb_void_cookie_t deleteProperty(xcb_connection_t *c, + xcb_window_t window, + xcb_atom_t property, + int checked) +{ + REQUIRE_REQUEST(c, xcb_void_cookie_t); + uint8_t error = isWindow(window) ? 0 : XCB_WINDOW; + if (!error && !isValidAtom(property)) + error = XCB_ATOM; + if (!error) + XDeleteProperty(xcbCompatDisplay(c), window, property); + return xcbCompatVoidCookie(c, error, error == XCB_ATOM ? property : window, + XCB_DELETE_PROPERTY, checked); +} + +static xcb_list_properties_cookie_t listProperties(xcb_connection_t *c, + xcb_window_t window, + int checked) +{ + REQUIRE_REQUEST(c, xcb_list_properties_cookie_t); + xcb_list_properties_cookie_t cookie = {nextSequence(c)}; + if (!isWindow(window)) { + xcbCompatStoreProtocolError(c, cookie.sequence, XCB_WINDOW, window, + XCB_LIST_PROPERTIES, checked); + return cookie; + } + int count = 0; + Atom *atoms = XListProperties(xcbCompatDisplay(c), window, &count); + if (count < 0) { + free(atoms); + storeReply(c, cookie.sequence, NULL); + return cookie; + } + size_t bytes = 0; + xcb_list_properties_reply_t *reply = allocateReplyPayload( + c, sizeof(*reply), (uint64_t) count, sizeof(xcb_atom_t), &bytes); + if (reply) { + reply->response_type = 1; + reply->atoms_len = (uint16_t) count; + reply->length = (uint32_t) count; + for (int i = 0; i < count; i++) + ((xcb_atom_t *) (reply + 1))[i] = (xcb_atom_t) atoms[i]; + } + free(atoms); + storeReply(c, cookie.sequence, reply); + return cookie; +} + +xcb_list_properties_cookie_t xcb_list_properties(xcb_connection_t *c, + xcb_window_t window) +{ + return listProperties(c, window, 1); +} + +int xcb_list_properties_sizeof(const void *buffer) +{ + const xcb_list_properties_reply_t *reply = buffer; + return reply + ? (int) (sizeof(*reply) + reply->atoms_len * sizeof(xcb_atom_t)) + : 0; +} + +xcb_list_properties_cookie_t xcb_list_properties_unchecked(xcb_connection_t *c, + xcb_window_t window) +{ + return listProperties(c, window, 0); +} + +xcb_list_properties_reply_t *xcb_list_properties_reply( + xcb_connection_t *c, + xcb_list_properties_cookie_t cookie, + xcb_generic_error_t **error) +{ + return xcbCompatTakeReply(c, cookie.sequence, error); +} + +xcb_atom_t *xcb_list_properties_atoms(const xcb_list_properties_reply_t *reply) +{ + return reply ? (xcb_atom_t *) (reply + 1) : NULL; +} + +int xcb_list_properties_atoms_length(const xcb_list_properties_reply_t *reply) +{ + return reply ? reply->atoms_len : 0; +} + +xcb_generic_iterator_t xcb_list_properties_atoms_end( + const xcb_list_properties_reply_t *reply) +{ + xcb_generic_iterator_t iterator = {0}; + if (reply) { + iterator.data = xcb_list_properties_atoms(reply) + reply->atoms_len; + iterator.index = sizeof(*reply) + reply->atoms_len * sizeof(xcb_atom_t); + } + return iterator; +} + +static xcb_void_cookie_t setSelectionOwner(xcb_connection_t *c, + xcb_window_t owner, + xcb_atom_t selection, + xcb_timestamp_t time, + int checked) +{ + REQUIRE_REQUEST(c, xcb_void_cookie_t); + uint8_t error = owner != XCB_NONE && !isWindow(owner) ? XCB_WINDOW : 0; + if (!error && !isValidAtom(selection)) + error = XCB_ATOM; + if (!error) + XSetSelectionOwner(xcbCompatDisplay(c), selection, owner, time); + return xcbCompatVoidCookie(c, error, error == XCB_ATOM ? selection : owner, + XCB_SET_SELECTION_OWNER, checked); +} + +xcb_void_cookie_t xcb_set_selection_owner(xcb_connection_t *c, + xcb_window_t owner, + xcb_atom_t selection, + xcb_timestamp_t time) +{ + return setSelectionOwner(c, owner, selection, time, 0); +} + +xcb_void_cookie_t xcb_set_selection_owner_checked(xcb_connection_t *c, + xcb_window_t owner, + xcb_atom_t selection, + xcb_timestamp_t time) +{ + return setSelectionOwner(c, owner, selection, time, 1); +} + +static xcb_get_selection_owner_cookie_t getSelectionOwner(xcb_connection_t *c, + xcb_atom_t selection, + int checked) +{ + REQUIRE_REQUEST(c, xcb_get_selection_owner_cookie_t); + xcb_get_selection_owner_cookie_t cookie = {nextSequence(c)}; + if (!isValidAtom(selection)) { + xcbCompatStoreProtocolError(c, cookie.sequence, XCB_ATOM, selection, + XCB_GET_SELECTION_OWNER, checked); + return cookie; + } + xcb_get_selection_owner_reply_t *reply = calloc(1, sizeof(*reply)); + if (reply) { + reply->response_type = 1; + reply->owner = XGetSelectionOwner(xcbCompatDisplay(c), selection); + } + storeReply(c, cookie.sequence, reply); + return cookie; +} + +xcb_get_selection_owner_cookie_t xcb_get_selection_owner(xcb_connection_t *c, + xcb_atom_t selection) +{ + return getSelectionOwner(c, selection, 1); +} + +xcb_get_selection_owner_cookie_t xcb_get_selection_owner_unchecked( + xcb_connection_t *c, + xcb_atom_t selection) +{ + return getSelectionOwner(c, selection, 0); +} + +xcb_get_selection_owner_reply_t *xcb_get_selection_owner_reply( + xcb_connection_t *c, + xcb_get_selection_owner_cookie_t cookie, + xcb_generic_error_t **error) +{ + return xcbCompatTakeReply(c, cookie.sequence, error); +} + +static xcb_void_cookie_t convertSelection(xcb_connection_t *c, + xcb_window_t requestor, + xcb_atom_t selection, + xcb_atom_t target, + xcb_atom_t property, + xcb_timestamp_t time, + int checked) +{ + REQUIRE_REQUEST(c, xcb_void_cookie_t); + uint8_t error = isWindow(requestor) ? 0 : XCB_WINDOW; + uint32_t errorValue = requestor; + if (!error && !isValidAtom(selection)) { + error = XCB_ATOM; + errorValue = selection; + } + if (!error && !isValidAtom(target)) { + error = XCB_ATOM; + errorValue = target; + } + if (!error && property != XCB_NONE && !isValidAtom(property)) { + error = XCB_ATOM; + errorValue = property; + } + if (!error) + XConvertSelection(xcbCompatDisplay(c), selection, target, property, + requestor, time); + return xcbCompatVoidCookie(c, error, errorValue, XCB_CONVERT_SELECTION, + checked); +} + +xcb_void_cookie_t xcb_convert_selection(xcb_connection_t *c, + xcb_window_t requestor, + xcb_atom_t selection, + xcb_atom_t target, + xcb_atom_t property, + xcb_timestamp_t time) +{ + return convertSelection(c, requestor, selection, target, property, time, 0); +} + +xcb_void_cookie_t xcb_convert_selection_checked(xcb_connection_t *c, + xcb_window_t requestor, + xcb_atom_t selection, + xcb_atom_t target, + xcb_atom_t property, + xcb_timestamp_t time) +{ + return convertSelection(c, requestor, selection, target, property, time, 1); +} +xcb_void_cookie_t xcb_delete_property(xcb_connection_t *c, + xcb_window_t w, + xcb_atom_t p) +{ + return deleteProperty(c, w, p, 0); +} +xcb_void_cookie_t xcb_delete_property_checked(xcb_connection_t *c, + xcb_window_t w, + xcb_atom_t p) +{ + return deleteProperty(c, w, p, 1); +} + +static xcb_void_cookie_t changeAttributes(xcb_connection_t *c, + xcb_window_t window, + uint32_t mask, + const uint32_t *values, + int checked) +{ + REQUIRE_REQUEST(c, xcb_void_cookie_t); + uint8_t error = !isWindow(window) ? XCB_WINDOW : 0; + uint32_t errorValue = window; + if (!error && (mask & ~XCB_CW_VALID_MASK)) { + error = XCB_VALUE; + errorValue = mask; + } + if (!error && mask && !values) { + error = XCB_VALUE; + errorValue = mask; + } + if (!error) { + XSetWindowAttributes attributes; + decodeAttributes(mask, values, &attributes); + error = validateAttributes(mask, &attributes, drawableDepth(c, window), + IS_INPUT_ONLY(window), &errorValue); + if (!error && !XChangeWindowAttributes(xcbCompatDisplay(c), window, + mask, &attributes)) + error = XCB_VALUE; + } + return xcbCompatVoidCookie(c, error, errorValue, + XCB_CHANGE_WINDOW_ATTRIBUTES, checked); +} +xcb_void_cookie_t xcb_change_window_attributes(xcb_connection_t *c, + xcb_window_t window, + uint32_t mask, + const void *values) +{ + return changeAttributes(c, window, mask, values, 0); +} +xcb_void_cookie_t xcb_change_window_attributes_checked(xcb_connection_t *c, + xcb_window_t window, + uint32_t mask, + const void *values) +{ + return changeAttributes(c, window, mask, values, 1); +} + +static void decodeConfigure(uint16_t mask, + const uint32_t *values, + XWindowChanges *changes) +{ + memset(changes, 0, sizeof(*changes)); + unsigned int index = 0; +#define TAKE(bit, field, type) \ + do { \ + if (mask & (bit)) \ + changes->field = (type) values[index++]; \ + } while (0) + TAKE(XCB_CONFIG_WINDOW_X, x, int16_t); + TAKE(XCB_CONFIG_WINDOW_Y, y, int16_t); + TAKE(XCB_CONFIG_WINDOW_WIDTH, width, uint16_t); + TAKE(XCB_CONFIG_WINDOW_HEIGHT, height, uint16_t); + TAKE(XCB_CONFIG_WINDOW_BORDER_WIDTH, border_width, uint16_t); + TAKE(XCB_CONFIG_WINDOW_SIBLING, sibling, xcb_window_t); + TAKE(XCB_CONFIG_WINDOW_STACK_MODE, stack_mode, uint8_t); +#undef TAKE +} + +static xcb_void_cookie_t xcbConfigureWindow(xcb_connection_t *c, + xcb_window_t window, + uint16_t mask, + const uint32_t *values, + int checked) +{ + REQUIRE_REQUEST(c, xcb_void_cookie_t); + uint8_t error = !isWindow(window) ? XCB_WINDOW : 0; + uint32_t errorValue = window; + if (!error && (mask & ~XCB_CONFIG_WINDOW_VALID_MASK)) { + error = XCB_VALUE; + errorValue = mask; + } + if (!error && mask && !values) { + error = XCB_VALUE; + errorValue = mask; + } + XWindowChanges changes; + if (!error) { + decodeConfigure(mask, values, &changes); + if (mask & XCB_CONFIG_WINDOW_WIDTH && changes.width <= 0) { + error = XCB_VALUE; + errorValue = changes.width; + } else if (mask & XCB_CONFIG_WINDOW_HEIGHT && changes.height <= 0) { + error = XCB_VALUE; + errorValue = changes.height; + } else if (mask & XCB_CONFIG_WINDOW_STACK_MODE && + changes.stack_mode > XCB_STACK_MODE_OPPOSITE) { + error = XCB_VALUE; + errorValue = changes.stack_mode; + } else if ((mask & XCB_CONFIG_WINDOW_SIBLING) && + !isWindow(changes.sibling)) { + error = XCB_WINDOW; + errorValue = changes.sibling; + } else if ((mask & XCB_CONFIG_WINDOW_SIBLING) && + !(mask & XCB_CONFIG_WINDOW_STACK_MODE)) { + error = XCB_MATCH; + errorValue = window; + } else if ((mask & XCB_CONFIG_WINDOW_SIBLING) && + (changes.sibling == window || + GET_PARENT(changes.sibling) != GET_PARENT(window))) { + error = XCB_MATCH; + errorValue = window; + } + } + if (!error) { + if (!XConfigureWindow(xcbCompatDisplay(c), window, mask, &changes)) + error = XCB_MATCH; + } + return xcbCompatVoidCookie(c, error, errorValue, XCB_CONFIGURE_WINDOW, + checked); +} +xcb_void_cookie_t xcb_configure_window(xcb_connection_t *c, + xcb_window_t window, + uint16_t mask, + const void *values) +{ + return xcbConfigureWindow(c, window, mask, values, 0); +} +xcb_void_cookie_t xcb_configure_window_checked(xcb_connection_t *c, + xcb_window_t window, + uint16_t mask, + const void *values) +{ + return xcbConfigureWindow(c, window, mask, values, 1); +} + +static xcb_void_cookie_t reparentWindow(xcb_connection_t *c, + xcb_window_t window, + xcb_window_t parent, + int16_t x, + int16_t y, + int checked) +{ + REQUIRE_REQUEST(c, xcb_void_cookie_t); + int windowValid = isWindow(window); + int parentValid = isWindow(parent); + uint8_t error = !windowValid ? XCB_WINDOW : 0; + uint32_t errorResource = window; + if (!error && !parentValid) { + error = XCB_WINDOW; + errorResource = parent; + } + if (!error && !XReparentWindow(xcbCompatDisplay(c), window, parent, x, y)) + error = XCB_MATCH; + return xcbCompatVoidCookie(c, error, errorResource, XCB_REPARENT_WINDOW, + checked); +} +xcb_void_cookie_t xcb_reparent_window(xcb_connection_t *c, + xcb_window_t window, + xcb_window_t parent, + int16_t x, + int16_t y) +{ + return reparentWindow(c, window, parent, x, y, 0); +} +xcb_void_cookie_t xcb_reparent_window_checked(xcb_connection_t *c, + xcb_window_t window, + xcb_window_t parent, + int16_t x, + int16_t y) +{ + return reparentWindow(c, window, parent, x, y, 1); +} + +/* Accept exactly the depths the setup record advertises, rather than repeating + * the list src/pixmap.c keeps: a client picks its depth from those formats, so + * anything else is a BadValue and the two sets cannot drift apart. + */ +static int supportedPixmapDepth(xcb_connection_t *c, uint8_t depth) +{ + const xcb_setup_t *setup = xcb_get_setup(c); + if (!setup) + return 0; + const xcb_format_t *formats = xcb_setup_pixmap_formats(setup); + int count = xcb_setup_pixmap_formats_length(setup); + for (int i = 0; i < count; i++) + if (formats[i].depth == depth) + return 1; + return 0; +} + +static xcb_void_cookie_t createPixmap(xcb_connection_t *c, + uint8_t depth, + xcb_pixmap_t pid, + xcb_drawable_t drawable, + uint16_t width, + uint16_t height, + int checked) +{ + REQUIRE_REQUEST(c, xcb_void_cookie_t); + uint8_t error = !isDrawable(drawable) ? XCB_DRAWABLE : 0; + uint32_t errorValue = drawable; + if (!error && getXidStruct(pid)->type != 0) { + error = XCB_ID_CHOICE; + errorValue = pid; + } + if (!error && !width) { + error = XCB_VALUE; + errorValue = width; + } + if (!error && !height) { + error = XCB_VALUE; + errorValue = height; + } + if (!error && !supportedPixmapDepth(c, depth)) { + error = XCB_VALUE; + errorValue = depth; + } + + /* Past the argument checks the only way the shared path still fails is a + * failed allocation, which the protocol reports as BadAlloc rather than + * blaming one of the arguments. Claim a client-chosen id last, once nothing + * else can reject the request; the creator owns it from here and releases + * it itself if it fails. + */ + if (!error && !isXidAllocated(pid) && !reserveXidResource(pid)) { + error = XCB_ID_CHOICE; + errorValue = pid; + } + if (!error && + libx11CompatCreatePixmapWithId(xcbCompatDisplay(c), pid, drawable, + width, height, depth) == XCB_NONE) { + error = XCB_ALLOC; + errorValue = pid; + } + return xcbCompatVoidCookie(c, error, errorValue, XCB_CREATE_PIXMAP, + checked); +} + +xcb_void_cookie_t xcb_create_pixmap(xcb_connection_t *c, + uint8_t depth, + xcb_pixmap_t pid, + xcb_drawable_t drawable, + uint16_t width, + uint16_t height) +{ + return createPixmap(c, depth, pid, drawable, width, height, 0); +} +xcb_void_cookie_t xcb_create_pixmap_checked(xcb_connection_t *c, + uint8_t depth, + xcb_pixmap_t pid, + xcb_drawable_t drawable, + uint16_t width, + uint16_t height) +{ + return createPixmap(c, depth, pid, drawable, width, height, 1); +} + +static xcb_void_cookie_t freePixmap(xcb_connection_t *c, + xcb_pixmap_t pixmap, + int checked) +{ + REQUIRE_REQUEST(c, xcb_void_cookie_t); + uint8_t error = IS_TYPE(pixmap, PIXMAP) ? 0 : XCB_PIXMAP; + if (!error) + XFreePixmap(xcbCompatDisplay(c), pixmap); + return xcbCompatVoidCookie(c, error, pixmap, XCB_FREE_PIXMAP, checked); +} +xcb_void_cookie_t xcb_free_pixmap(xcb_connection_t *c, xcb_pixmap_t pixmap) +{ + return freePixmap(c, pixmap, 0); +} +xcb_void_cookie_t xcb_free_pixmap_checked(xcb_connection_t *c, + xcb_pixmap_t pixmap) +{ + return freePixmap(c, pixmap, 1); +} + +static void decodeGcValues(uint32_t mask, + const uint32_t *list, + XGCValues *values) +{ + memset(values, 0, sizeof(*values)); + unsigned int n = 0; +#define TAKE(bit, field, type) \ + do { \ + if (mask & (bit)) \ + values->field = (type) list[n++]; \ + } while (0) + TAKE(XCB_GC_FUNCTION, function, int); + TAKE(XCB_GC_PLANE_MASK, plane_mask, unsigned long); + TAKE(XCB_GC_FOREGROUND, foreground, unsigned long); + TAKE(XCB_GC_BACKGROUND, background, unsigned long); + TAKE(XCB_GC_LINE_WIDTH, line_width, int); + TAKE(XCB_GC_LINE_STYLE, line_style, int); + TAKE(XCB_GC_CAP_STYLE, cap_style, int); + TAKE(XCB_GC_JOIN_STYLE, join_style, int); + TAKE(XCB_GC_FILL_STYLE, fill_style, int); + TAKE(XCB_GC_FILL_RULE, fill_rule, int); + TAKE(XCB_GC_TILE, tile, Pixmap); + TAKE(XCB_GC_STIPPLE, stipple, Pixmap); + TAKE(XCB_GC_TILE_STIPPLE_ORIGIN_X, ts_x_origin, int); + TAKE(XCB_GC_TILE_STIPPLE_ORIGIN_Y, ts_y_origin, int); + TAKE(XCB_GC_FONT, font, Font); + TAKE(XCB_GC_SUBWINDOW_MODE, subwindow_mode, int); + TAKE(XCB_GC_GRAPHICS_EXPOSURES, graphics_exposures, Bool); + TAKE(XCB_GC_CLIP_ORIGIN_X, clip_x_origin, int); + TAKE(XCB_GC_CLIP_ORIGIN_Y, clip_y_origin, int); + TAKE(XCB_GC_CLIP_MASK, clip_mask, Pixmap); + TAKE(XCB_GC_DASH_OFFSET, dash_offset, int); + TAKE(XCB_GC_DASH_LIST, dashes, char); + TAKE(XCB_GC_ARC_MODE, arc_mode, int); +#undef TAKE +} + +static uint8_t validateGcValues(uint32_t mask, + const XGCValues *values, + unsigned int gcDepth, + uint32_t *errorValue) +{ +#define BAD_VALUE(bit, field, limit) \ + do { \ + if ((mask & (bit)) && \ + (values->field < 0 || values->field > (limit))) { \ + *errorValue = (uint32_t) values->field; \ + return XCB_VALUE; \ + } \ + } while (0) + BAD_VALUE(XCB_GC_FUNCTION, function, XCB_GX_SET); + BAD_VALUE(XCB_GC_LINE_STYLE, line_style, XCB_LINE_STYLE_DOUBLE_DASH); + BAD_VALUE(XCB_GC_CAP_STYLE, cap_style, XCB_CAP_STYLE_PROJECTING); + BAD_VALUE(XCB_GC_JOIN_STYLE, join_style, XCB_JOIN_STYLE_BEVEL); + BAD_VALUE(XCB_GC_FILL_STYLE, fill_style, XCB_FILL_STYLE_OPAQUE_STIPPLED); + BAD_VALUE(XCB_GC_FILL_RULE, fill_rule, XCB_FILL_RULE_WINDING); + BAD_VALUE(XCB_GC_SUBWINDOW_MODE, subwindow_mode, + XCB_SUBWINDOW_MODE_INCLUDE_INFERIORS); + BAD_VALUE(XCB_GC_ARC_MODE, arc_mode, XCB_ARC_MODE_PIE_SLICE); +#undef BAD_VALUE + if ((mask & XCB_GC_GRAPHICS_EXPOSURES) && + values->graphics_exposures != False && + values->graphics_exposures != True) { + *errorValue = (uint32_t) values->graphics_exposures; + return XCB_VALUE; + } + if ((mask & XCB_GC_DASH_LIST) && values->dashes == 0) { + *errorValue = 0; + return XCB_VALUE; + } + const struct { + uint32_t bit; + Pixmap pixmap; + unsigned int requiredDepth; + int noneAllowed; + } pixmaps[] = { + {XCB_GC_TILE, values->tile, gcDepth, 0}, + {XCB_GC_STIPPLE, values->stipple, 1, 0}, + {XCB_GC_CLIP_MASK, values->clip_mask, 1, 1}, + }; + for (size_t i = 0; i < sizeof(pixmaps) / sizeof(pixmaps[0]); i++) { + if (!(mask & pixmaps[i].bit) || + (pixmaps[i].noneAllowed && pixmaps[i].pixmap == XCB_NONE)) + continue; + *errorValue = pixmaps[i].pixmap; + if (!IS_TYPE(pixmaps[i].pixmap, PIXMAP)) + return XCB_PIXMAP; + if (GET_PIXMAP_STRUCT(pixmaps[i].pixmap)->depth != + pixmaps[i].requiredDepth) + return XCB_MATCH; + } + if (mask & XCB_GC_FONT) { + *errorValue = values->font; + if (!IS_TYPE(values->font, FONT)) + return XCB_FONT; + } + return 0; +} + +static xcb_void_cookie_t createGc(xcb_connection_t *c, + xcb_gcontext_t cid, + xcb_drawable_t drawable, + uint32_t mask, + const uint32_t *list, + int checked) +{ + REQUIRE_REQUEST(c, xcb_void_cookie_t); + uint8_t error = !isDrawable(drawable) ? XCB_DRAWABLE : 0; + uint32_t errorValue = drawable; + if (!error && isInputOnlyDrawable(drawable)) + error = XCB_MATCH; + if (!error && getXidStruct(cid)->type != 0) { + error = XCB_ID_CHOICE; + errorValue = cid; + } + if (!error && (mask & ~XCB_GC_VALID_MASK)) { + error = XCB_VALUE; + errorValue = mask; + } + if (!error && mask && !list) { + error = XCB_VALUE; + errorValue = mask; + } + XGCValues values; + unsigned int gcDepth = 0; + if (!error) { + gcDepth = drawableDepth(c, drawable); + decodeGcValues(mask, list, &values); + error = validateGcValues(mask, &values, gcDepth, &errorValue); + } + + /* Claim a client-chosen id last, once nothing else can reject the request; + * the creator owns it from here and releases it itself if it fails. + */ + if (!error && !isXidAllocated(cid) && !reserveXidResource(cid)) { + error = XCB_ID_CHOICE; + errorValue = cid; + } + if (!error) { + GC gc = libx11CompatCreateGCWithId(xcbCompatDisplay(c), cid, drawable, + mask, &values); + GcEntry *entry = gc ? calloc(1, sizeof(*entry)) : NULL; + if (!entry) { + if (gc) + XFreeGC(xcbCompatDisplay(c), gc); + error = XCB_ALLOC; + } else { + entry->id = cid; + entry->gc = gc; + entry->display = xcbCompatDisplay(c); + entry->owner = c; + entry->depth = gcDepth; + pthread_mutex_lock(&gcMutex); + entry->next = gcEntries; + gcEntries = entry; + pthread_mutex_unlock(&gcMutex); + } + } + return xcbCompatVoidCookie(c, error, errorValue, XCB_CREATE_GC, checked); +} + +#define GC_CREATE_WRAPPER(name, checkedValue) \ + xcb_void_cookie_t name(xcb_connection_t *c, xcb_gcontext_t cid, \ + xcb_drawable_t drawable, uint32_t mask, \ + const void *list) \ + { \ + return createGc(c, cid, drawable, mask, list, checkedValue); \ + } +GC_CREATE_WRAPPER(xcb_create_gc, 0) +GC_CREATE_WRAPPER(xcb_create_gc_checked, 1) + +static xcb_void_cookie_t changeGc(xcb_connection_t *c, + xcb_gcontext_t id, + uint32_t mask, + const uint32_t *list, + int checked) +{ + REQUIRE_REQUEST(c, xcb_void_cookie_t); + pthread_mutex_lock(&gcMutex); + GcEntry *entry = findGcLocked(id); + GC gc = entry ? entry->gc : NULL; + uint8_t error = entry ? 0 : XCB_G_CONTEXT; + uint32_t errorValue = id; + if (!error && (mask & ~XCB_GC_VALID_MASK)) { + error = XCB_VALUE; + errorValue = mask; + } + if (!error && mask && !list) { + error = XCB_VALUE; + errorValue = mask; + } + if (!error) { + XGCValues values; + decodeGcValues(mask, list, &values); + error = validateGcValues(mask, &values, entry->depth, &errorValue); + if (!error && !XChangeGC(xcbCompatDisplay(c), gc, mask, &values)) + error = XCB_VALUE; + } + pthread_mutex_unlock(&gcMutex); + return xcbCompatVoidCookie(c, error, errorValue, XCB_CHANGE_GC, checked); +} +#define GC_CHANGE_WRAPPER(name, checkedValue) \ + xcb_void_cookie_t name(xcb_connection_t *c, xcb_gcontext_t gc, \ + uint32_t mask, const void *list) \ + { \ + return changeGc(c, gc, mask, list, checkedValue); \ + } +GC_CHANGE_WRAPPER(xcb_change_gc, 0) +GC_CHANGE_WRAPPER(xcb_change_gc_checked, 1) + +static xcb_void_cookie_t copyGc(xcb_connection_t *c, + xcb_gcontext_t source, + xcb_gcontext_t destination, + uint32_t mask, + int checked) +{ + REQUIRE_REQUEST(c, xcb_void_cookie_t); + pthread_mutex_lock(&gcMutex); + GcEntry *srcEntry = findGcLocked(source); + GcEntry *dstEntry = findGcLocked(destination); + GC src = srcEntry ? srcEntry->gc : NULL; + GC dst = dstEntry ? dstEntry->gc : NULL; + uint8_t error = !srcEntry || !dstEntry ? XCB_G_CONTEXT : 0; + uint32_t errorValue = !srcEntry ? source : destination; + if (!error && srcEntry->depth != dstEntry->depth) + error = XCB_MATCH; + if (!error && (mask & ~XCB_GC_VALID_MASK)) { + error = XCB_VALUE; + errorValue = mask; + } + if (!error && !XCopyGC(xcbCompatDisplay(c), src, mask, dst)) + error = XCB_VALUE; + pthread_mutex_unlock(&gcMutex); + return xcbCompatVoidCookie(c, error, errorValue, XCB_COPY_GC, checked); +} +xcb_void_cookie_t xcb_copy_gc(xcb_connection_t *c, + xcb_gcontext_t src, + xcb_gcontext_t dst, + uint32_t mask) +{ + return copyGc(c, src, dst, mask, 0); +} +xcb_void_cookie_t xcb_copy_gc_checked(xcb_connection_t *c, + xcb_gcontext_t src, + xcb_gcontext_t dst, + uint32_t mask) +{ + return copyGc(c, src, dst, mask, 1); +} + +static xcb_void_cookie_t freeGc(xcb_connection_t *c, + xcb_gcontext_t id, + int checked) +{ + REQUIRE_REQUEST(c, xcb_void_cookie_t); + pthread_mutex_lock(&gcMutex); + GcEntry **link = &gcEntries; + while (*link && (*link)->id != id) + link = &(*link)->next; + uint8_t error = *link ? 0 : XCB_G_CONTEXT; + if (!error) { + GcEntry *entry = *link; + *link = entry->next; + XFreeGC(entry->display, entry->gc); + free(entry); + } + pthread_mutex_unlock(&gcMutex); + return xcbCompatVoidCookie(c, error, id, XCB_FREE_GC, checked); +} +xcb_void_cookie_t xcb_free_gc(xcb_connection_t *c, xcb_gcontext_t gc) +{ + return freeGc(c, gc, 0); +} +xcb_void_cookie_t xcb_free_gc_checked(xcb_connection_t *c, xcb_gcontext_t gc) +{ + return freeGc(c, gc, 1); +} + +static uint8_t lockDrawingResources(xcb_connection_t *c, + xcb_drawable_t drawable, + xcb_gcontext_t gc, + GC *xgc) +{ + *xgc = NULL; + if (!isDrawable(drawable)) + return XCB_DRAWABLE; + if (isInputOnlyDrawable(drawable)) + return XCB_MATCH; + pthread_mutex_lock(&gcMutex); + GcEntry *entry = findGcLocked(gc); + *xgc = entry ? entry->gc : NULL; + if (entry && entry->depth == drawableDepth(c, drawable)) + return 0; + if (entry) { + pthread_mutex_unlock(&gcMutex); + *xgc = NULL; + return XCB_MATCH; + } + pthread_mutex_unlock(&gcMutex); + return XCB_G_CONTEXT; +} + +static xcb_void_cookie_t clearArea(xcb_connection_t *c, + uint8_t exposures, + xcb_window_t window, + int16_t x, + int16_t y, + uint16_t width, + uint16_t height, + int checked) +{ + REQUIRE_REQUEST(c, xcb_void_cookie_t); + uint8_t error = isWindow(window) ? 0 : XCB_WINDOW; + if (!error) + XClearArea(xcbCompatDisplay(c), window, x, y, width, height, exposures); + return xcbCompatVoidCookie(c, error, window, XCB_CLEAR_AREA, checked); +} +#define CLEAR_WRAPPER(name, checkedValue) \ + xcb_void_cookie_t name(xcb_connection_t *c, uint8_t exposures, \ + xcb_window_t window, int16_t x, int16_t y, \ + uint16_t width, uint16_t height) \ + { \ + return clearArea(c, exposures, window, x, y, width, height, \ + checkedValue); \ + } +CLEAR_WRAPPER(xcb_clear_area, 0) +CLEAR_WRAPPER(xcb_clear_area_checked, 1) + +static xcb_void_cookie_t copyArea(xcb_connection_t *c, + xcb_drawable_t source, + xcb_drawable_t destination, + xcb_gcontext_t gc, + int16_t sourceX, + int16_t sourceY, + int16_t destinationX, + int16_t destinationY, + uint16_t width, + uint16_t height, + int checked, + int copyPlane, + uint32_t plane) +{ + REQUIRE_REQUEST(c, xcb_void_cookie_t); + GC xgc = NULL; + int sourceValid = isDrawable(source); + int destinationValid = isDrawable(destination); + uint8_t error = !sourceValid || !destinationValid + ? XCB_DRAWABLE + : lockDrawingResources(c, destination, gc, &xgc); + if (!error && isInputOnlyDrawable(source)) + error = XCB_MATCH; + uint32_t errorValue = !sourceValid ? source + : !destinationValid ? destination + : error == XCB_G_CONTEXT ? gc + : source; + if (!error && !copyPlane && + drawableDepth(c, source) != drawableDepth(c, destination)) { + error = XCB_MATCH; + errorValue = destination; + } + + /* CopyPlane names a single bit, and that bit has to exist in the source: a + * plane above the source depth selects nothing at all. A depth of 32 or + * more admits every bit, and shifting by the width of the type would be + * undefined, so that case skips the range test. + */ + if (!error && copyPlane) { + unsigned int sourceDepth = drawableDepth(c, source); + if (!plane || (plane & (plane - 1)) || + (sourceDepth < 32 && plane >= (UINT32_C(1) << sourceDepth))) { + error = XCB_VALUE; + errorValue = plane; + } + } + if (!error) { + if (copyPlane) + XCopyPlane(xcbCompatDisplay(c), source, destination, xgc, sourceX, + sourceY, width, height, destinationX, destinationY, + plane); + else + XCopyArea(xcbCompatDisplay(c), source, destination, xgc, sourceX, + sourceY, width, height, destinationX, destinationY); + } + if (xgc) + pthread_mutex_unlock(&gcMutex); + return xcbCompatVoidCookie(c, error, errorValue, + copyPlane ? XCB_COPY_PLANE : XCB_COPY_AREA, + checked); +} +#define COPY_AREA_WRAPPER(name, checkedValue) \ + xcb_void_cookie_t name(xcb_connection_t *c, xcb_drawable_t src, \ + xcb_drawable_t dst, xcb_gcontext_t gc, int16_t sx, \ + int16_t sy, int16_t dx, int16_t dy, uint16_t width, \ + uint16_t height) \ + { \ + return copyArea(c, src, dst, gc, sx, sy, dx, dy, width, height, \ + checkedValue, 0, 0); \ + } +COPY_AREA_WRAPPER(xcb_copy_area, 0) +COPY_AREA_WRAPPER(xcb_copy_area_checked, 1) + +#define COPY_PLANE_WRAPPER(name, checkedValue) \ + xcb_void_cookie_t name(xcb_connection_t *c, xcb_drawable_t src, \ + xcb_drawable_t dst, xcb_gcontext_t gc, int16_t sx, \ + int16_t sy, int16_t dx, int16_t dy, uint16_t width, \ + uint16_t height, uint32_t plane) \ + { \ + return copyArea(c, src, dst, gc, sx, sy, dx, dy, width, height, \ + checkedValue, 1, plane); \ + } +COPY_PLANE_WRAPPER(xcb_copy_plane, 0) +COPY_PLANE_WRAPPER(xcb_copy_plane_checked, 1) + +typedef enum { + DRAW_POINTS, + DRAW_LINES, + DRAW_SEGMENTS, + DRAW_RECTANGLES, + DRAW_ARCS +} DrawKind; + +static xcb_void_cookie_t drawPrimitives(xcb_connection_t *c, + xcb_drawable_t drawable, + xcb_gcontext_t gc, + uint8_t mode, + uint32_t count, + const void *items, + DrawKind kind, + uint8_t opcode, + int checked) +{ + REQUIRE_REQUEST(c, xcb_void_cookie_t); + _Static_assert(sizeof(xcb_point_t) == sizeof(XPoint), "point ABI"); + _Static_assert(sizeof(xcb_segment_t) == sizeof(XSegment), "segment ABI"); + _Static_assert(sizeof(xcb_rectangle_t) == sizeof(XRectangle), + "rectangle ABI"); + _Static_assert(sizeof(xcb_arc_t) == sizeof(XArc), "arc ABI"); + size_t itemSize = kind == DRAW_POINTS || kind == DRAW_LINES + ? sizeof(xcb_point_t) + : kind == DRAW_SEGMENTS ? sizeof(xcb_segment_t) + : kind == DRAW_RECTANGLES ? sizeof(xcb_rectangle_t) + : sizeof(xcb_arc_t); + GC xgc = NULL; + uint8_t error = + requestPayloadFits(c, sizeof(xcb_poly_point_request_t), count, itemSize) + ? lockDrawingResources(c, drawable, gc, &xgc) + : XCB_LENGTH; + uint32_t errorValue = error == XCB_G_CONTEXT ? gc + : error == XCB_LENGTH ? count + : drawable; + if (!error && count && !items) { + error = XCB_VALUE; + errorValue = count; + } + if (!error && (kind == DRAW_POINTS || kind == DRAW_LINES) && + mode > XCB_COORD_MODE_PREVIOUS) { + error = XCB_VALUE; + errorValue = mode; + } + if (!error) { + switch (kind) { + case DRAW_POINTS: + XDrawPoints(xcbCompatDisplay(c), drawable, xgc, (XPoint *) items, + count, mode); + break; + case DRAW_LINES: + XDrawLines(xcbCompatDisplay(c), drawable, xgc, (XPoint *) items, + count, mode); + break; + case DRAW_SEGMENTS: + XDrawSegments(xcbCompatDisplay(c), drawable, xgc, + (XSegment *) items, count); + break; + case DRAW_RECTANGLES: + XDrawRectangles(xcbCompatDisplay(c), drawable, xgc, + (XRectangle *) items, count); + break; + case DRAW_ARCS: + XDrawArcs(xcbCompatDisplay(c), drawable, xgc, (XArc *) items, + count); + break; + } + } + if (xgc) + pthread_mutex_unlock(&gcMutex); + return xcbCompatVoidCookie(c, error, errorValue, opcode, checked); +} + +#define POINTS_WRAPPER(name, kindValue, opcodeValue, checkedValue) \ + xcb_void_cookie_t name(xcb_connection_t *c, uint8_t mode, \ + xcb_drawable_t drawable, xcb_gcontext_t gc, \ + uint32_t count, const xcb_point_t *items) \ + { \ + return drawPrimitives(c, drawable, gc, mode, count, items, kindValue, \ + opcodeValue, checkedValue); \ + } +POINTS_WRAPPER(xcb_poly_point, DRAW_POINTS, XCB_POLY_POINT, 0) +POINTS_WRAPPER(xcb_poly_point_checked, DRAW_POINTS, XCB_POLY_POINT, 1) +POINTS_WRAPPER(xcb_poly_line, DRAW_LINES, XCB_POLY_LINE, 0) +POINTS_WRAPPER(xcb_poly_line_checked, DRAW_LINES, XCB_POLY_LINE, 1) + +#define FIXED_WRAPPER(name, itemType, kindValue, opcodeValue, checkedValue) \ + xcb_void_cookie_t name(xcb_connection_t *c, xcb_drawable_t drawable, \ + xcb_gcontext_t gc, uint32_t count, \ + const itemType *items) \ + { \ + return drawPrimitives(c, drawable, gc, 0, count, items, kindValue, \ + opcodeValue, checkedValue); \ + } +FIXED_WRAPPER(xcb_poly_segment, + xcb_segment_t, + DRAW_SEGMENTS, + XCB_POLY_SEGMENT, + 0) +FIXED_WRAPPER(xcb_poly_segment_checked, + xcb_segment_t, + DRAW_SEGMENTS, + XCB_POLY_SEGMENT, + 1) +FIXED_WRAPPER(xcb_poly_rectangle, + xcb_rectangle_t, + DRAW_RECTANGLES, + XCB_POLY_RECTANGLE, + 0) +FIXED_WRAPPER(xcb_poly_rectangle_checked, + xcb_rectangle_t, + DRAW_RECTANGLES, + XCB_POLY_RECTANGLE, + 1) +FIXED_WRAPPER(xcb_poly_arc, xcb_arc_t, DRAW_ARCS, XCB_POLY_ARC, 0) +FIXED_WRAPPER(xcb_poly_arc_checked, xcb_arc_t, DRAW_ARCS, XCB_POLY_ARC, 1) + +/* ImageText8 and ImageText16 differ only in the character width, so one body + * takes both: wide selects the Xlib entry point, the string type and the + * opcode, the same way copyArea covers CopyPlane. + */ +static xcb_void_cookie_t imageText(xcb_connection_t *c, + uint8_t length, + xcb_drawable_t drawable, + xcb_gcontext_t gc, + int16_t x, + int16_t y, + const void *string, + int wide, + int checked) +{ + REQUIRE_REQUEST(c, xcb_void_cookie_t); + _Static_assert(sizeof(xcb_char2b_t) == sizeof(XChar2b), "char2b ABI"); + GC xgc; + uint8_t error = lockDrawingResources(c, drawable, gc, &xgc); + if (!error && length && !string) + error = XCB_VALUE; + if (!error && wide) + XDrawImageString16(xcbCompatDisplay(c), drawable, xgc, x, y, + (XChar2b *) string, length); + else if (!error) + XDrawImageString(xcbCompatDisplay(c), drawable, xgc, x, y, string, + length); + if (xgc) + pthread_mutex_unlock(&gcMutex); + return xcbCompatVoidCookie(c, error, error == XCB_G_CONTEXT ? gc : drawable, + wide ? XCB_IMAGE_TEXT_16 : XCB_IMAGE_TEXT_8, + checked); +} + +#define TEXT_WRAPPER(name, stringType, wideValue, checkedValue) \ + xcb_void_cookie_t name(xcb_connection_t *c, uint8_t length, \ + xcb_drawable_t drawable, xcb_gcontext_t gc, \ + int16_t x, int16_t y, stringType *string) \ + { \ + return imageText(c, length, drawable, gc, x, y, string, wideValue, \ + checkedValue); \ + } +TEXT_WRAPPER(xcb_image_text_8, const char, 0, 0) +TEXT_WRAPPER(xcb_image_text_8_checked, const char, 0, 1) +TEXT_WRAPPER(xcb_image_text_16, const xcb_char2b_t, 1, 0) +TEXT_WRAPPER(xcb_image_text_16_checked, const xcb_char2b_t, 1, 1) + +static int imagePayloadSize(uint16_t width, + uint16_t height, + uint8_t depth, + size_t *rowBytes, + size_t *totalBytes) +{ + unsigned int bitsPerPixel = depth <= 1 ? 1 + : depth <= 8 ? 8 + : depth <= 16 ? 16 + : depth <= 32 ? 32 + : 0; + if (!bitsPerPixel) + return 0; + uint64_t rowBits = (uint64_t) width * bitsPerPixel; + uint64_t row = ((rowBits + 31) & ~UINT64_C(31)) / 8; + uint64_t total = row * height; + if (row > SIZE_MAX || total > SIZE_MAX || total > UINT32_MAX) + return 0; + *rowBytes = (size_t) row; + *totalBytes = (size_t) total; + return 1; +} + +static xcb_void_cookie_t putImage(xcb_connection_t *c, + uint8_t format, + xcb_drawable_t drawable, + xcb_gcontext_t gc, + uint16_t width, + uint16_t height, + int16_t destinationX, + int16_t destinationY, + uint8_t leftPad, + uint8_t depth, + uint32_t dataLength, + const uint8_t *data, + int checked) +{ + REQUIRE_REQUEST(c, xcb_void_cookie_t); + GC xgc; + uint8_t error = lockDrawingResources(c, drawable, gc, &xgc); + uint32_t errorValue = error == XCB_G_CONTEXT ? gc : drawable; + size_t rowBytes = 0, bytes = 0; + if (!error && format != XCB_IMAGE_FORMAT_Z_PIXMAP) { + error = XCB_VALUE; + errorValue = format; + } + if (!error && leftPad != 0) { + error = XCB_VALUE; + errorValue = leftPad; + } + if (!error && !width) { + error = XCB_VALUE; + errorValue = width; + } + if (!error && !height) { + error = XCB_VALUE; + errorValue = height; + } + if (!error && depth != drawableDepth(c, drawable)) { + error = XCB_MATCH; + errorValue = drawable; + } + if (!error && !imagePayloadSize(width, height, depth, &rowBytes, &bytes)) { + error = XCB_LENGTH; + errorValue = dataLength; + } + if (!error && !requestPayloadFits(c, sizeof(xcb_put_image_request_t), + dataLength, 1)) { + error = XCB_LENGTH; + errorValue = dataLength; + } + if (!error && dataLength < bytes) { + error = XCB_VALUE; + errorValue = dataLength; + } + if (!error && !data) { + error = XCB_VALUE; + errorValue = dataLength; + } + if (!error) { + char *copy = malloc(bytes); + if (!copy) + error = XCB_ALLOC; + else { + memcpy(copy, data, bytes); + XImage *image = XCreateImage( + xcbCompatDisplay(c), + DefaultVisual(xcbCompatDisplay(c), + DefaultScreen(xcbCompatDisplay(c))), + depth, ZPixmap, 0, copy, width, height, 32, (int) rowBytes); + if (!image) { + free(copy); + error = XCB_ALLOC; + } else { + XPutImage(xcbCompatDisplay(c), drawable, xgc, image, 0, 0, + destinationX, destinationY, width, height); + XDestroyImage(image); + } + } + } + if (xgc) + pthread_mutex_unlock(&gcMutex); + return xcbCompatVoidCookie(c, error, errorValue, XCB_PUT_IMAGE, checked); +} +#define PUT_IMAGE_WRAPPER(name, checkedValue) \ + xcb_void_cookie_t name(xcb_connection_t *c, uint8_t format, \ + xcb_drawable_t drawable, xcb_gcontext_t gc, \ + uint16_t width, uint16_t height, int16_t x, \ + int16_t y, uint8_t leftPad, uint8_t depth, \ + uint32_t dataLength, const uint8_t *data) \ + { \ + return putImage(c, format, drawable, gc, width, height, x, y, leftPad, \ + depth, dataLength, data, checkedValue); \ + } +PUT_IMAGE_WRAPPER(xcb_put_image, 0) +PUT_IMAGE_WRAPPER(xcb_put_image_checked, 1) + +static xcb_get_image_cookie_t getImage(xcb_connection_t *c, + uint8_t format, + xcb_drawable_t drawable, + int16_t x, + int16_t y, + uint16_t width, + uint16_t height, + uint32_t planeMask, + int checked) +{ + REQUIRE_REQUEST(c, xcb_get_image_cookie_t); + xcb_get_image_cookie_t cookie = {nextSequence(c)}; + if (!isDrawable(drawable)) { + xcbCompatStoreProtocolError(c, cookie.sequence, XCB_DRAWABLE, drawable, + XCB_GET_IMAGE, checked); + return cookie; + } + if (isInputOnlyDrawable(drawable)) { + xcbCompatStoreProtocolError(c, cookie.sequence, XCB_MATCH, drawable, + XCB_GET_IMAGE, checked); + return cookie; + } + if (format != XCB_IMAGE_FORMAT_Z_PIXMAP) { + xcbCompatStoreProtocolError(c, cookie.sequence, XCB_VALUE, format, + XCB_GET_IMAGE, checked); + return cookie; + } + if (!width || !height) { + xcbCompatStoreProtocolError(c, cookie.sequence, XCB_VALUE, + !width ? width : height, XCB_GET_IMAGE, + checked); + return cookie; + } + + /* Core GetImage requires the rectangle to lie wholly inside the drawable. + * XGetImage zero-fills whatever hangs over the edge, so without this the + * client would get a successful reply full of invented pixels. + */ + Window root; + int drawableX, drawableY; + unsigned int drawableWidth, drawableHeight, borderWidth, depth; + if (!XGetGeometry(xcbCompatDisplay(c), drawable, &root, &drawableX, + &drawableY, &drawableWidth, &drawableHeight, &borderWidth, + &depth) || + x < 0 || y < 0 || (unsigned int) x + width > drawableWidth || + (unsigned int) y + height > drawableHeight) { + xcbCompatStoreProtocolError(c, cookie.sequence, XCB_MATCH, drawable, + XCB_GET_IMAGE, checked); + return cookie; + } + XImage *image = XGetImage(xcbCompatDisplay(c), drawable, x, y, width, + height, planeMask, ZPixmap); + if (!image) { + xcbCompatStoreProtocolError(c, cookie.sequence, XCB_MATCH, drawable, + XCB_GET_IMAGE, checked); + return cookie; + } + if (image->bytes_per_line < 0) { + XDestroyImage(image); + xcbCompatSetConnectionError(c, XCB_CONN_CLOSED_MEM_INSUFFICIENT); + return cookie; + } + uint64_t imageBytes = + (uint64_t) (unsigned int) image->bytes_per_line * height; + if (imageBytes > SIZE_MAX || imageBytes / 4 > UINT32_MAX) { + XDestroyImage(image); + xcbCompatSetConnectionError(c, XCB_CONN_CLOSED_MEM_INSUFFICIENT); + return cookie; + } + size_t bytes = 0; + xcb_get_image_reply_t *reply = + allocateReplyPayload(c, sizeof(*reply), imageBytes, 1, &bytes); + if (reply) { + reply->response_type = 1; + reply->depth = image->depth; + reply->visual = + isWindow(drawable) + ? XVisualIDFromVisual(DefaultVisual( + xcbCompatDisplay(c), DefaultScreen(xcbCompatDisplay(c)))) + : XCB_NONE; + reply->length = bytes / 4; + if (bytes) + memcpy(reply + 1, image->data, bytes); + } + XDestroyImage(image); + storeReply(c, cookie.sequence, reply); + return cookie; +} +xcb_get_image_cookie_t xcb_get_image(xcb_connection_t *c, + uint8_t format, + xcb_drawable_t drawable, + int16_t x, + int16_t y, + uint16_t width, + uint16_t height, + uint32_t planeMask) +{ + return getImage(c, format, drawable, x, y, width, height, planeMask, 1); +} +xcb_get_image_cookie_t xcb_get_image_unchecked(xcb_connection_t *c, + uint8_t format, + xcb_drawable_t drawable, + int16_t x, + int16_t y, + uint16_t width, + uint16_t height, + uint32_t planeMask) +{ + return getImage(c, format, drawable, x, y, width, height, planeMask, 0); +} +xcb_get_image_reply_t *xcb_get_image_reply(xcb_connection_t *c, + xcb_get_image_cookie_t cookie, + xcb_generic_error_t **error) +{ + return xcbCompatTakeReply(c, cookie.sequence, error); +} +int xcb_get_image_sizeof(const void *buffer) +{ + const xcb_get_image_reply_t *reply = buffer; + uint64_t bytes = reply ? (uint64_t) reply->length * 4u : 0; + return reply && bytes <= INT_MAX - sizeof(*reply) + ? (int) sizeof(*reply) + (int) bytes + : 0; +} +uint8_t *xcb_get_image_data(const xcb_get_image_reply_t *reply) +{ + return reply ? (uint8_t *) (reply + 1) : NULL; +} +int xcb_get_image_data_length(const xcb_get_image_reply_t *reply) +{ + uint64_t bytes = reply ? (uint64_t) reply->length * 4u : 0; + return bytes <= INT_MAX ? (int) bytes : 0; +} +xcb_generic_iterator_t xcb_get_image_data_end( + const xcb_get_image_reply_t *reply) +{ + xcb_generic_iterator_t iterator = {0}; + uint64_t bytes = reply ? (uint64_t) reply->length * 4u : 0; + if (reply && bytes <= INT_MAX - sizeof(*reply)) { + iterator.data = xcb_get_image_data(reply) + (size_t) bytes; + iterator.index = sizeof(*reply) + (int) bytes; + } + return iterator; +} diff --git a/compat/xlib-xcb-compat.c b/compat/xlib-xcb-compat.c new file mode 100644 index 00000000..50b5ad4a --- /dev/null +++ b/compat/xlib-xcb-compat.c @@ -0,0 +1,12 @@ +#include +#include "xcb-compat-private.h" + +xcb_connection_t *XGetXCBConnection(Display *display) +{ + return xcbCompatConnectionForDisplay(display); +} + +void XSetEventQueueOwner(Display *display, enum XEventQueueOwner owner) +{ + xcbCompatSetQueueOwner(xcbCompatConnectionForDisplay(display), owner); +} diff --git a/docs/XCB-COVERAGE.md b/docs/XCB-COVERAGE.md new file mode 100644 index 00000000..d2e7a8e0 --- /dev/null +++ b/docs/XCB-COVERAGE.md @@ -0,0 +1,188 @@ +# XCB Compatibility Coverage + +## Building + +The layer is opt-in. `XCB=1` builds `libxcb-compat.so`, the +`libX11-xcb-compat.so` bridge and their tests; a default build has none of them. + +```sh +make XCB=1 +make XCB=1 check-unit +``` + +## Header staging + +The public `xcb/` headers are not tracked and are not copied from a +distribution package. `scripts/sync-upstream-headers.py` builds them into +`build/upstream/include/xcb/` alongside the Xorg headers, from two pinned +release tarballs: + +- `libxcb-1.15` ships `xcb.h` and `xcbext.h` in its `src/`. +- `xproto.h` does not exist in any release. libxcb generates it from + xcb-proto's XML with `src/c_client.py`, so the sync script runs that same + generator against pinned `xcb-proto-1.15.2` with the arguments libxcb's own + `src/Makefile.am` passes. + +Both tarball digests are verified before use, and the generator inputs are part +of the staging stamp, so a change to either release or to the invocation +restages. `make install` ships the built copies. + +## Implemented surface + +Requests 1-4, 7-8, 10, 12, 14-24, 53-57, 60-68, 72-73, and 76-77: window +lifecycle and configuration, geometry and tree queries, properties, selections, +pixmaps, graphics contexts, drawing primitives, text and image transfer. The +exact callable ABI is `tests/xcb-symbols.txt`, enforced against the built +library by `scripts/check-xcb-symbols.py` on every `make XCB=1 check-unit`. + +Every other core opcode is deferred and deliberately absent from the +export manifest, so a client fails at link time instead of receiving a cookie +that silently never completes. + +The staged headers are upstream's own and so declare the whole core protocol: +645 entry points against the 143 this layer exports. That gap is the deferral +made visible. A client that calls a deferred request fails to link, which is the +intended outcome. + +## Deliberate limits + +`xcb_get_file_descriptor()` returns -1: this backend has no transport socket, so +callers must use the wait and poll entry points rather than integrating a +connection FD into an external `poll(2)` set. + +PutImage and GetImage accept `ZPixmap` only. XYBitmap and XYPixmap have a +different payload geometry and left-pad meaning, and the backing store keeps +pixels in Z order, so the other two formats answer `BadValue` rather than +guessing at a conversion. + +RENDER, SHM, SHAPE, XFIXES and RANDR are separate extension libraries and are +not part of this layer. DRI, Present, FD passing and authorization transport +have no in-process equivalent at all. + +## Differential oracle + +`tests/probe-system-xcb.c` exercises the core surface and prints a +screen-independent digest of what it observed. `make check-xcb-reference` runs +it under an isolated Xvfb and compares against `tests/data/xcb-reference.txt`. + +<<<<<<< HEAD +The probe currently links system XCB on both sides, so it pins the oracle rather +than this layer. Pointing one of the two runs at `build/pkgconfig/xcb.pc` is +what would turn it into a real differential. +||||||| parent of d49ecf6 (Implement XCB pixmap, GC and drawing requests) +## Core opcode ledger + +The compatibility library currently implements requests 1-4, 7-8, 10, 12, +and 14-24. Every other core opcode is deferred and deliberately absent +from the export manifest; consumers therefore fail at link time instead of +receiving a false successful cookie. + +“Deferred” means no ABI is advertised. Once a checked entry point is exported, +an unsupported combination must return its protocol error (`BadWindow`, +`BadValue`, `BadMatch`, and so on) through `xcb_request_check`; it must never +return silent success. + +## Header provenance + +The public headers come from Ubuntu's `libxcb1-dev` 1.15-1ubuntu2 package, +published at `https://archive.ubuntu.com/ubuntu/pool/main/libx/libxcb/` from the +upstream libxcb 1.15 release. The pinned package SHA-256 is +`1bafe3432feafc9e57f858721da524dfb3ee1f6fc4ef6b0c73023e79eadb9c28`. + +`scripts/sync-xcb-headers.sh` verifies two digests per header. The upstream +digest authenticates the file as unpacked from that package: + +| Header | Upstream SHA-256 | +| --- | --- | +| `xcb.h` | `70218365dcfd8b2e9202b145e9bafbaba042872eae046888680bf58c38cf30e6` | +| `xcbext.h` | `b101d53f1bed75e659e7469f0b7a3eb213bf1cf228f821580c7020b8e70be647` | +| `xproto.h` | `6f45223c52dc24621e7b307b26d39e4d7c884dc08900be619361e076fcac40ec` | + +The vendored digest authenticates what lands in `include/xcb/`, which is the +same text with trailing whitespace removed because this repository's whitespace +gate rejects it. Only `xcb.h` is byte-identical to its upstream form: + +| Header | Vendored SHA-256 | +| --- | --- | +| `xcb.h` | `70218365dcfd8b2e9202b145e9bafbaba042872eae046888680bf58c38cf30e6` | +| `xcbext.h` | `432b474c8b74444c1ed91a529f6500a57ae826a3d7dceec1b351f09c4147fedb` | +| `xproto.h` | `3da9c1a330e53a8bd6e44b895d27627e65e0da51664c5b7d48414e84ffddd766` | + +Confirm the tree against the second table with `shasum -a 256 include/xcb/*.h`. +The headers retain the upstream MIT-style license notices. Regenerate them with: + +```sh +sh scripts/sync-xcb-headers.sh +``` +======= +## Core opcode ledger + +The compatibility library currently implements requests 1-4, 7-8, 10, 12, +14-24, 53-57, 60-68, 72-73, and 76-77. The Phase-0 probe does not issue any +input, focus, grab, pointer, GC, drawing, image, font, color, cursor, +extension-query, keyboard-control, or host control request. Every other core +opcode is deferred and deliberately absent from the export manifest; consumers +therefore fail at link time instead of receiving a false successful cookie. + +The deferred opcode groups below cover every core request in the vendored X11 +protocol header. A group may move to the required list only with its ABI symbols, +protocol-error behavior, and a focused differential test in the same commit. + +| Opcodes | Deferred requests | +| --- | --- | +| 5-6, 9, 11, 13 | DestroySubwindows, ChangeSaveSet, MapSubwindows, UnmapSubwindows, CirculateWindow | +| 25 | SendEvent | +| 26-30 | GrabPointer, UngrabPointer, GrabButton, UngrabButton, ChangeActivePointerGrab | +| 31-37 | GrabKeyboard, UngrabKeyboard, GrabKey, UngrabKey, AllowEvents, GrabServer, UngrabServer | +| 38-44 | QueryPointer, GetMotionEvents, TranslateCoordinates, WarpPointer, SetInputFocus, GetInputFocus, QueryKeymap | +| 45-52 | OpenFont through GetFontPath | +| 58-59 | SetDashes, SetClipRectangles | +| 69-71 | FillPoly, PolyFillRectangle, PolyFillArc | +| 74-75 | PolyText8, PolyText16 | +| 78-83 | CreateColormap through ListInstalledColormaps | +| 84-92 | AllocColor through LookupColor | +| 93-97 | CreateCursor through QueryBestSize | +| 98-99 | QueryExtension, ListExtensions | +| 100-108 | ChangeKeyboardMapping through GetScreenSaver | +| 109-115 | ChangeHosts through ForceScreenSaver | +| 116-119 | SetPointerMapping through GetModifierMapping | +| 127 | NoOperation | + +“Deferred” means no ABI is advertised. Once a checked entry point is exported, +an unsupported combination must return its protocol error (`BadWindow`, +`BadValue`, `BadMatch`, and so on) through `xcb_request_check`; it must never +return silent success. + +## Header provenance + +The public headers come from Ubuntu's `libxcb1-dev` 1.15-1ubuntu2 package, +published at `https://archive.ubuntu.com/ubuntu/pool/main/libx/libxcb/` from the +upstream libxcb 1.15 release. The pinned package SHA-256 is +`1bafe3432feafc9e57f858721da524dfb3ee1f6fc4ef6b0c73023e79eadb9c28`. + +`scripts/sync-xcb-headers.sh` verifies two digests per header. The upstream +digest authenticates the file as unpacked from that package: + +| Header | Upstream SHA-256 | +| --- | --- | +| `xcb.h` | `70218365dcfd8b2e9202b145e9bafbaba042872eae046888680bf58c38cf30e6` | +| `xcbext.h` | `b101d53f1bed75e659e7469f0b7a3eb213bf1cf228f821580c7020b8e70be647` | +| `xproto.h` | `6f45223c52dc24621e7b307b26d39e4d7c884dc08900be619361e076fcac40ec` | + +The vendored digest authenticates what lands in `include/xcb/`, which is the +same text with trailing whitespace removed because this repository's whitespace +gate rejects it. Only `xcb.h` is byte-identical to its upstream form: + +| Header | Vendored SHA-256 | +| --- | --- | +| `xcb.h` | `70218365dcfd8b2e9202b145e9bafbaba042872eae046888680bf58c38cf30e6` | +| `xcbext.h` | `432b474c8b74444c1ed91a529f6500a57ae826a3d7dceec1b351f09c4147fedb` | +| `xproto.h` | `3da9c1a330e53a8bd6e44b895d27627e65e0da51664c5b7d48414e84ffddd766` | + +Confirm the tree against the second table with `shasum -a 256 include/xcb/*.h`. +The headers retain the upstream MIT-style license notices. Regenerate them with: + +```sh +sh scripts/sync-xcb-headers.sh +``` +>>>>>>> d49ecf6 (Implement XCB pixmap, GC and drawing requests) diff --git a/examples/README-xcb.md b/examples/README-xcb.md new file mode 100644 index 00000000..d57760b7 --- /dev/null +++ b/examples/README-xcb.md @@ -0,0 +1,14 @@ +# XCB Showcases + +These native examples exercise the XCB compatibility library without using +Xlib client calls: + +- `xcb-kaleidoscope` combines image upload with XCB line and rectangle + primitives. +- `xcb-mandelbrot` renders a CPU-generated fractal through `xcb_put_image`. + +The XCB compatibility layer is off by default, so these two build with +`make XCB=1 examples` alongside the Xlib clients. Run +`build/examples/xcb-kaleidoscope` or `build/examples/xcb-mandelbrot` and press +any key to exit. Use `make XCB=1 check-xcb-showcases` for a headless, +non-interactive smoke test with SDL's dummy video driver. diff --git a/examples/xcb-kaleidoscope.c b/examples/xcb-kaleidoscope.c new file mode 100644 index 00000000..120a60bb --- /dev/null +++ b/examples/xcb-kaleidoscope.c @@ -0,0 +1,50 @@ +#include "xcb-shared.h" + +static int paint(xcb_connection_t *connection, + xcb_window_t window, + xcb_gcontext_t gc, + uint8_t depth) +{ + uint32_t *pixels = + malloc((size_t) SHOWCASE_WIDTH * SHOWCASE_HEIGHT * sizeof(*pixels)); + if (!pixels) + return 0; + for (unsigned int y = 0; y < SHOWCASE_HEIGHT; y++) { + for (unsigned int x = 0; x < SHOWCASE_WIDTH; x++) { + unsigned int dx = x > SHOWCASE_WIDTH / 2 ? x - SHOWCASE_WIDTH / 2 + : SHOWCASE_WIDTH / 2 - x; + unsigned int dy = y > SHOWCASE_HEIGHT / 2 ? y - SHOWCASE_HEIGHT / 2 + : SHOWCASE_HEIGHT / 2 - y; + unsigned int r = (dx * 3 + dy) & 0xff; + unsigned int g = (dx + dy * 3) & 0xff; + unsigned int b = ((dx ^ dy) * 2) & 0xff; + pixels[(size_t) y * SHOWCASE_WIDTH + x] = (r << 16) | (g << 8) | b; + } + } + showcasePutImage(connection, window, gc, depth, pixels); + free(pixels); + + const xcb_segment_t rays[] = { + {SHOWCASE_WIDTH / 2, SHOWCASE_HEIGHT / 2, 0, 0}, + {SHOWCASE_WIDTH / 2, SHOWCASE_HEIGHT / 2, SHOWCASE_WIDTH - 1, 0}, + {SHOWCASE_WIDTH / 2, SHOWCASE_HEIGHT / 2, SHOWCASE_WIDTH - 1, + SHOWCASE_HEIGHT - 1}, + {SHOWCASE_WIDTH / 2, SHOWCASE_HEIGHT / 2, 0, SHOWCASE_HEIGHT - 1}, + }; + const uint32_t white = 0x00ffffff; + xcb_change_gc(connection, gc, XCB_GC_FOREGROUND, &white); + xcb_poly_segment(connection, window, gc, sizeof(rays) / sizeof(rays[0]), + rays); + for (unsigned int inset = 24; inset < 220; inset += 24) { + xcb_rectangle_t rectangle = {inset, inset, SHOWCASE_WIDTH - 2 * inset, + SHOWCASE_HEIGHT - 2 * inset}; + xcb_poly_rectangle(connection, window, gc, 1, &rectangle); + } + xcb_flush(connection); + return 1; +} + +int main(int argc, char **argv) +{ + return showcaseRun(argc, argv, "xcb-kaleidoscope", paint); +} diff --git a/examples/xcb-mandelbrot.c b/examples/xcb-mandelbrot.c new file mode 100644 index 00000000..f04d51a5 --- /dev/null +++ b/examples/xcb-mandelbrot.c @@ -0,0 +1,47 @@ +#include "xcb-shared.h" + +enum { MAX_ITERATIONS = 96 }; + +static uint32_t color(unsigned int iteration) +{ + if (iteration == MAX_ITERATIONS) + return 0; + unsigned int shade = iteration * 255 / MAX_ITERATIONS; + return ((shade * 3 & 0xff) << 16) | ((shade * 7 & 0xff) << 8) | + (255 - shade); +} + +static int paint(xcb_connection_t *connection, + xcb_window_t window, + xcb_gcontext_t gc, + uint8_t depth) +{ + uint32_t *pixels = + malloc((size_t) SHOWCASE_WIDTH * SHOWCASE_HEIGHT * sizeof(*pixels)); + if (!pixels) + return 0; + for (unsigned int py = 0; py < SHOWCASE_HEIGHT; py++) { + double ci = (double) py / SHOWCASE_HEIGHT * 2.4 - 1.2; + for (unsigned int px = 0; px < SHOWCASE_WIDTH; px++) { + double cr = (double) px / SHOWCASE_WIDTH * 3.2 - 2.2; + double zr = 0.0, zi = 0.0; + unsigned int iteration = 0; + while (zr * zr + zi * zi <= 4.0 && iteration < MAX_ITERATIONS) { + double next = zr * zr - zi * zi + cr; + zi = 2.0 * zr * zi + ci; + zr = next; + iteration++; + } + pixels[(size_t) py * SHOWCASE_WIDTH + px] = color(iteration); + } + } + showcasePutImage(connection, window, gc, depth, pixels); + free(pixels); + xcb_flush(connection); + return 1; +} + +int main(int argc, char **argv) +{ + return showcaseRun(argc, argv, "xcb-mandelbrot", paint); +} diff --git a/examples/xcb-shared.h b/examples/xcb-shared.h new file mode 100644 index 00000000..123cf62f --- /dev/null +++ b/examples/xcb-shared.h @@ -0,0 +1,143 @@ +/* + * Scaffolding shared by the native XCB showcases: connection setup, a mapped + * window, a graphics context, the redraw loop and teardown. Each showcase then + * consists of its paint function alone. + * + * Header-only on purpose. The showcases are single-file programs built without + * a link step of their own, and the scaffolding is not a library anyone else + * consumes. + */ +#ifndef XCB_SHARED_H +#define XCB_SHARED_H + +#include +#include +#include +#include + +enum { SHOWCASE_WIDTH = 640, SHOWCASE_HEIGHT = 480 }; +enum { SHOWCASE_STRIPE_HEIGHT = 32 }; + +/* Returns nonzero on success. Called once before the loop and again on expose. + */ +typedef int (*ShowcasePaint)(xcb_connection_t *connection, + xcb_window_t window, + xcb_gcontext_t gc, + uint8_t depth); + +/* Upload a full frame of 32-bit pixels in horizontal stripes. */ +static inline void showcasePutImage(xcb_connection_t *connection, + xcb_window_t window, + xcb_gcontext_t gc, + uint8_t depth, + const uint32_t *pixels) +{ + for (unsigned int y = 0; y < SHOWCASE_HEIGHT; y += SHOWCASE_STRIPE_HEIGHT) { + unsigned int rows = SHOWCASE_HEIGHT - y < SHOWCASE_STRIPE_HEIGHT + ? SHOWCASE_HEIGHT - y + : SHOWCASE_STRIPE_HEIGHT; + xcb_put_image( + connection, XCB_IMAGE_FORMAT_Z_PIXMAP, window, gc, SHOWCASE_WIDTH, + rows, 0, y, 0, depth, + (uint32_t) ((size_t) SHOWCASE_WIDTH * rows * sizeof(*pixels)), + (const uint8_t *) (pixels + (size_t) y * SHOWCASE_WIDTH)); + } +} + +/* Drain the queue after a smoke run. The drawing requests are unchecked, so a + * run that failed every one of them would otherwise still exit successfully. + */ +static inline int showcaseDrainErrors(xcb_connection_t *connection, + const char *name) +{ + xcb_flush(connection); + xcb_generic_event_t *queued; + while ((queued = xcb_poll_for_event(connection))) { + int failed = (queued->response_type & 0x7f) == 0; + uint8_t code = ((xcb_generic_error_t *) queued)->error_code; + free(queued); + if (failed) { + fprintf(stderr, "%s: request failed (%u)\n", name, code); + return 1; + } + } + return 0; +} + +static inline int showcaseRun(int argc, + char **argv, + const char *name, + ShowcasePaint paint) +{ + int smoke = argc == 2 && strcmp(argv[1], "--smoke") == 0; + xcb_connection_t *connection = xcb_connect(NULL, NULL); + if (!connection || xcb_connection_has_error(connection)) { + fprintf(stderr, "%s: cannot connect\n", name); + xcb_disconnect(connection); + return 1; + } + xcb_screen_t *screen = + xcb_setup_roots_iterator(xcb_get_setup(connection)).data; + + /* The pixel buffers are packed 32 bits per pixel, so say so rather than + * uploading a mis-strided image on a screen that wants something else. + */ + if (screen->root_depth != 24 && screen->root_depth != 32) { + fprintf(stderr, "%s: needs a 24 or 32 bit screen, got %u\n", name, + screen->root_depth); + xcb_disconnect(connection); + return 1; + } + xcb_window_t window = xcb_generate_id(connection); + uint32_t windowValues[] = { + screen->black_pixel, + XCB_EVENT_MASK_EXPOSURE | XCB_EVENT_MASK_KEY_PRESS}; + xcb_generic_error_t *error = xcb_request_check( + connection, xcb_create_window_checked( + connection, screen->root_depth, window, screen->root, 0, + 0, SHOWCASE_WIDTH, SHOWCASE_HEIGHT, 0, + XCB_WINDOW_CLASS_INPUT_OUTPUT, screen->root_visual, + XCB_CW_BACK_PIXEL | XCB_CW_EVENT_MASK, windowValues)); + if (error) { + fprintf(stderr, "%s: create failed (%u)\n", name, error->error_code); + free(error); + xcb_disconnect(connection); + return 1; + } + xcb_gcontext_t gc = xcb_generate_id(connection); + xcb_create_gc(connection, gc, window, 0, NULL); + xcb_map_window(connection, window); + if (!paint(connection, window, gc, screen->root_depth)) { + fprintf(stderr, "%s: image allocation failed\n", name); + xcb_disconnect(connection); + return 1; + } + + int status = smoke ? showcaseDrainErrors(connection, name) : 0; + while (!smoke) { + xcb_generic_event_t *event = xcb_wait_for_event(connection); + if (!event) { + fprintf(stderr, "%s: connection closed while waiting\n", name); + status = 1; + break; + } + if ((event->response_type & 0x7f) == XCB_KEY_PRESS) { + free(event); + break; + } + if ((event->response_type & 0x7f) == XCB_EXPOSE && + !paint(connection, window, gc, screen->root_depth)) { + fprintf(stderr, "%s: repaint failed\n", name); + free(event); + status = 1; + break; + } + free(event); + } + xcb_free_gc(connection, gc); + xcb_destroy_window(connection, window); + xcb_disconnect(connection); + return status; +} + +#endif diff --git a/include/X11/Xlib-xcb.h b/include/X11/Xlib-xcb.h new file mode 100644 index 00000000..1f1093ca --- /dev/null +++ b/include/X11/Xlib-xcb.h @@ -0,0 +1,12 @@ +/* Minimal libX11-xcb compatibility API. */ +#ifndef _X11_XLIB_XCB_H_ +#define _X11_XLIB_XCB_H_ +#include +#include +#include +_XFUNCPROTOBEGIN +xcb_connection_t *XGetXCBConnection(Display *display); +enum XEventQueueOwner { XlibOwnsEventQueue = 0, XCBOwnsEventQueue }; +void XSetEventQueueOwner(Display *display, enum XEventQueueOwner owner); +_XFUNCPROTOEND +#endif diff --git a/mk/config.mk b/mk/config.mk index 28fb62a5..1eb6f94a 100644 --- a/mk/config.mk +++ b/mk/config.mk @@ -8,6 +8,14 @@ TARGET ?= $(OUT)/libX11-compat.so # either way, so GLX=0 is only for builds that want the smaller symbol surface. GLX ?= 1 +# XCB ?= 0 builds the core XCB shim (libxcb-compat.so) and the Xlib/XCB bridge +# (libX11-xcb-compat.so) alongside the Xlib library, together with their tests, +# example clients, pkg-config files and install entries. It is off by default +# because no in-tree workload needs it yet: an XCB client has to opt in with +# XCB=1, and a build that does not stays free of the extra libraries and of the +# xcb/x11-xcb .pc files a downstream configure might otherwise pick up. +XCB ?= 0 + # PYTHON is set in mk/toolchain.mk; do not redefine here. # SDL detection lives in mk/sdl.mk; this file consumes SDL_CPPFLAGS and # SDL_COMPAT_LIBS from it. diff --git a/mk/examples.mk b/mk/examples.mk index f05ea02d..4154b790 100644 --- a/mk/examples.mk +++ b/mk/examples.mk @@ -1,6 +1,11 @@ EXAMPLE_NAMES := 2048 paint life clock mandel processing clipboard catclock \ moire EXAMPLE_BINS := $(addprefix $(OUT)/examples/,$(EXAMPLE_NAMES)) +ifeq ($(XCB),1) + XCB_SHOWCASE_NAMES := xcb-kaleidoscope xcb-mandelbrot + XCB_SHOWCASE_BINS := $(addprefix $(OUT)/examples/,$(XCB_SHOWCASE_NAMES)) + EXAMPLE_BINS += $(XCB_SHOWCASE_BINS) +endif X11PERF_DIR := examples/x11perf X11PERF_SRCS := \ $(X11PERF_DIR)/bitmaps.c \ @@ -30,11 +35,20 @@ ifeq ($(UNAME_S),Darwin) EXAMPLE_LDFLAGS += -Wl,-rpath,$(abspath $(OUT)) endif -.PHONY: examples bench-x11perf +.PHONY: examples check-xcb-showcases bench-x11perf -## Build the bundled Xlib client examples (linked against libX11-compat.so) +## Build the bundled example clients (add XCB=1 for the native XCB ones) examples: $(EXAMPLE_BINS) $(X11PERF_BIN) +## Run the XCB showcases headless under SDL's dummy driver (needs XCB=1) +check-xcb-showcases: $(XCB_SHOWCASE_BINS) + @test "$(XCB)" = 1 || \ + { echo "Error: check-xcb-showcases needs XCB=1" >&2; exit 1; } + @set -e; for showcase in $(XCB_SHOWCASE_BINS); do \ + echo " RUN $$showcase --smoke"; \ + SDL_VIDEODRIVER=dummy $$showcase --smoke; \ + done + ## Run the bundled x11perf benchmark in short regression mode bench-x11perf: $(X11PERF_BIN) SDL_VIDEODRIVER=dummy $(X11PERF_BIN) $(X11PERF_BENCH_ARGS) @@ -45,6 +59,14 @@ $(OUT)/examples/%: examples/%.c $(TARGET) $(Q)$(CC) $(CPPFLAGS) $(FP_CFLAGS) $(CFLAGS_EXTRA) $< $(TARGET) \ $(LDLIBS) $(EXAMPLE_LDFLAGS) -o $@ +ifeq ($(XCB),1) +$(XCB_SHOWCASE_BINS): $(OUT)/examples/%: examples/%.c $(XCB_COMPAT_TARGET) $(TARGET) + @mkdir -p $(dir $@) + @echo " CC $<" + $(Q)$(CC) $(CPPFLAGS) $(FP_CFLAGS) $(CFLAGS_EXTRA) $< \ + $(XCB_COMPAT_TARGET) $(TARGET) $(LDLIBS) $(EXAMPLE_LDFLAGS) -o $@ +endif + $(X11PERF_BIN): $(X11PERF_SRCS) $(TARGET) @mkdir -p $(dir $@) @echo " CC examples/x11perf" diff --git a/mk/install.mk b/mk/install.mk index 7ab9f87e..7baf138d 100644 --- a/mk/install.mk +++ b/mk/install.mk @@ -29,6 +29,9 @@ CODESIGN_RESIGN := $(if $(filter Darwin,$(UNAME_S)),codesign --force --sign - ,: # Libraries a downstream links by their standard X11 SONAME (each gets a # libNAME.so -> libNAME-compat.so alias). XCOMPAT_INSTALL_ALIASED := X11 Xft Xext Xt Xmu Xaw Xpm Xinerama ICE SM +ifeq ($(XCB),1) +XCOMPAT_INSTALL_ALIASED += X11-xcb xcb +endif # Runtime-only wrappers libX11-compat dlopens; installed without an alias. Only # built for the SDL2 backend -- under SDL_BACKEND=sdl3 the stack links libSDL3 # directly (SDL_USE_WRAPPER=0), so there is nothing to install. @@ -44,9 +47,17 @@ XCOMPAT_INSTALL_LIB_FILES := \ $(foreach w,$(XCOMPAT_INSTALL_WRAPPERS),$(OUT)/lib$(w).so) .PHONY: install -install: $(XCOMPAT_INSTALL_LIB_FILES) $(UPSTREAM_HEADERS_STAMP) +# x11.pc describes libX11-compat, which every install ships, so it is not an +# XCB decision. It also has to travel with x11-xcb.pc, whose "Requires: x11" +# would otherwise resolve against whatever real libX11 the host has and quietly +# link the system library next to the shim. +XCOMPAT_INSTALL_PC := x11$(if $(filter 1,$(XCB)), xcb x11-xcb) + +install: $(XCOMPAT_INSTALL_LIB_FILES) $(UPSTREAM_HEADERS_STAMP) \ + $(addprefix $(PKGCONFIG_DIR)/,$(addsuffix .pc,$(XCOMPAT_INSTALL_PC))) @echo " INSTALL $(DESTDIR)$(PREFIX)" - $(Q)mkdir -p "$(DESTDIR)$(PREFIX)/lib" "$(DESTDIR)$(PREFIX)/include" + $(Q)mkdir -p "$(DESTDIR)$(PREFIX)/lib" "$(DESTDIR)$(PREFIX)/include" \ + $(if $(XCOMPAT_INSTALL_PC),"$(DESTDIR)$(PREFIX)/lib/pkgconfig") $(Q)for l in $(XCOMPAT_INSTALL_ALIASED); do \ cp "$(OUT)/lib$$l-compat.so" "$(DESTDIR)$(PREFIX)/lib/" && \ $(STRIP) -x "$(DESTDIR)$(PREFIX)/lib/lib$$l-compat.so" && \ @@ -65,3 +76,14 @@ install: $(XCOMPAT_INSTALL_LIB_FILES) $(UPSTREAM_HEADERS_STAMP) cp -R "include/$$d/." "$(DESTDIR)$(PREFIX)/include/$$d/"; \ fi; \ done + $(Q)prefix_escaped=$$(printf '%s' "$(PREFIX)" | sed -e 's/[&|\\]/\\&/g'); \ + for pc in $(XCOMPAT_INSTALL_PC); do \ + sed -e "s|^prefix=.*|prefix=$$prefix_escaped|" \ + -e 's|^exec_prefix=.*|exec_prefix=$${prefix}|' \ + -e 's|^libdir=.*|libdir=$${prefix}/lib|' \ + -e 's|^includedir=.*|includedir=$${prefix}/include|' \ + -e 's|^upstreamincludedir=.*|upstreamincludedir=$${includedir}|' \ + -e 's|^libxtbuildincludedir=.*|libxtbuildincludedir=$${includedir}|' \ + "$(PKGCONFIG_DIR)/$$pc.pc" \ + > "$(DESTDIR)$(PREFIX)/lib/pkgconfig/$$pc.pc" || exit 1; \ + done diff --git a/mk/libxcb.mk b/mk/libxcb.mk new file mode 100644 index 00000000..fedf3ed9 --- /dev/null +++ b/mk/libxcb.mk @@ -0,0 +1,80 @@ +# Core XCB ABI shim over libX11-compat. Built only for XCB=1 (mk/config.mk). +# +# The public xcb/ headers are staged into $(OUT)/upstream/include by +# mk/upstream-headers.mk, which is included after this file, so the stamp names +# are defined here the way mk/libxt.mk does it. Without them the order-only +# prerequisite below expands to nothing and a parallel clean build can compile +# these objects before the headers exist. +UPSTREAM_HEADERS_DIR ?= $(OUT)/upstream/include +UPSTREAM_HEADERS_STAMP ?= $(UPSTREAM_HEADERS_DIR)/.upstream-stamp + +ifeq ($(XCB),1) +XCB_COMPAT_TARGET := $(OUT)/libxcb-compat.so +XCB_COMPAT_OBJS := $(OUT)/xcb-compat.o $(OUT)/xcb-requests.o +XCB_DEFINED := $(OUT)/libxcb-compat.defined-syms +XCB_EXPORT_LIST := $(OUT)/libxcb-compat.$(if $(filter Darwin,$(UNAME_S)),exports,map) +XCB_EXPORT_FORMAT := $(if $(filter Darwin,$(UNAME_S)),macho,elf) +X11_XCB_COMPAT_TARGET := $(OUT)/libX11-xcb-compat.so +X11_XCB_COMPAT_OBJ := $(OUT)/xlib-xcb-compat.o +X11_XCB_DEFINED := $(OUT)/libX11-xcb-compat.defined-syms +X11_XCB_EXPORT_LIST := $(OUT)/libX11-xcb-compat.$(if $(filter Darwin,$(UNAME_S)),exports,map) +XCB_RPATH_FLAGS := $(call shared_lib_rpath_ldflags,$(notdir $(XCB_COMPAT_TARGET))) + +$(OUT)/xcb-compat.o: compat/xcb-compat.c $(UPSTREAM_HEADERS_STAMP) \ + $(SDL_BACKEND_STAMP) | $(OUT) + $(cc_object) + +$(OUT)/xcb-requests.o: compat/xcb-requests.c $(UPSTREAM_HEADERS_STAMP) \ + $(SDL_BACKEND_STAMP) | $(OUT) + $(cc_object) + +$(XCB_DEFINED): $(XCB_COMPAT_OBJS) | $(OUT) + @echo " GEN $@" + $(Q)nm -g $(XCB_COMPAT_OBJS) 2>/dev/null \ + | awk '$$1 ~ /^[0-9a-fA-F]+$$/ { print $$NF }' \ + | $(if $(filter Darwin,$(UNAME_S)),sed 's/^_//',cat) \ + | LC_ALL=C sort -u > $@ + $(Q)test -s $@ + +$(XCB_EXPORT_LIST): tests/xcb-symbols.txt $(XCB_DEFINED) scripts/gen-export-list.sh | $(OUT) + @echo " GEN $@" + $(Q)scripts/gen-export-list.sh "$(XCB_EXPORT_FORMAT)" 0 \ + $(XCB_DEFINED) tests/xcb-symbols.txt tests/xcb-private-symbols.txt > $@ + +# -Bsymbolic for the same reason mk/library.mk applies it to libX11-compat: a +# process that also loads the host libxcb.so would otherwise let its +# xcb_get_setup and friends interpose ours, and the host implementation would +# then be handed this shim's connection object, which is a different type. +XCB_EXPORT_FLAGS := $(if $(filter Darwin,$(UNAME_S)),-Wl$(comma)-exported_symbols_list$(comma)$(XCB_EXPORT_LIST),-Wl$(comma)--version-script=$(XCB_EXPORT_LIST) -Wl$(comma)-Bsymbolic) +$(XCB_COMPAT_TARGET): $(XCB_COMPAT_OBJS) $(TARGET) $(XCB_EXPORT_LIST) | $(OUT) + @echo " LD $@" + $(Q)$(CC) $(LDFLAGS) $(XCB_RPATH_FLAGS) $(XCB_EXPORT_FLAGS) -shared \ + -o $@ $(XCB_COMPAT_OBJS) -L$(OUT) -lX11-compat + +.PHONY: xcb +## Build the core XCB compatibility shared library (needs XCB=1) +xcb: $(XCB_COMPAT_TARGET) +all: $(XCB_COMPAT_TARGET) + +$(X11_XCB_COMPAT_OBJ): compat/xlib-xcb-compat.c $(UPSTREAM_HEADERS_STAMP) | $(OUT) + $(cc_object) + +$(X11_XCB_DEFINED): $(X11_XCB_COMPAT_OBJ) | $(OUT) + @echo " GEN $@" + $(Q)nm -g $< 2>/dev/null | awk '$$1 ~ /^[0-9a-fA-F]+$$/ { print $$NF }' \ + | $(if $(filter Darwin,$(UNAME_S)),sed 's/^_//',cat) | LC_ALL=C sort -u > $@ + $(Q)test -s $@ + +$(X11_XCB_EXPORT_LIST): tests/xlib-xcb-symbols.txt $(X11_XCB_DEFINED) scripts/gen-export-list.sh | $(OUT) + @echo " GEN $@" + $(Q)scripts/gen-export-list.sh "$(XCB_EXPORT_FORMAT)" 0 \ + $(X11_XCB_DEFINED) tests/xlib-xcb-symbols.txt > $@ + +$(X11_XCB_COMPAT_TARGET): $(X11_XCB_COMPAT_OBJ) $(XCB_COMPAT_TARGET) $(TARGET) $(X11_XCB_EXPORT_LIST) | $(OUT) + @echo " LD $@" + $(Q)$(CC) $(LDFLAGS) $(call shared_lib_rpath_ldflags,$(notdir $@)) \ + $(if $(filter Darwin,$(UNAME_S)),-Wl$(comma)-exported_symbols_list$(comma)$(X11_XCB_EXPORT_LIST),-Wl$(comma)--version-script=$(X11_XCB_EXPORT_LIST) -Wl$(comma)-Bsymbolic) \ + -shared -o $@ $(X11_XCB_COMPAT_OBJ) -L$(OUT) -lxcb-compat -lX11-compat + +all: $(X11_XCB_COMPAT_TARGET) +endif diff --git a/mk/pkgconfig.mk b/mk/pkgconfig.mk index d65bf5c3..3802b2c8 100644 --- a/mk/pkgconfig.mk +++ b/mk/pkgconfig.mk @@ -13,15 +13,22 @@ PKGCONFIG_FILES := \ $(PKGCONFIG_DIR)/fontconfig.pc \ $(PKGCONFIG_DIR)/xrender.pc \ $(PKGCONFIG_DIR)/xft.pc +ifeq ($(XCB),1) +PKGCONFIG_FILES += $(PKGCONFIG_DIR)/xcb.pc $(PKGCONFIG_DIR)/x11-xcb.pc +endif $(PKGCONFIG_DIR): @mkdir -p $@ +# Libs and Cflags refer to the variables above rather than repeating absolute +# paths, so `make install` can relocate a file by rewriting the variable lines +# alone (mk/install.mk). pkg-config expands them, so an in-tree consumer reading +# build/pkgconfig/ sees exactly the paths it saw before. define write_pc @{ \ echo "prefix=$(abspath $(OUT))"; \ - echo "exec_prefix=$(abspath $(OUT))"; \ - echo "libdir=$(abspath $(OUT))"; \ + echo "exec_prefix=\$${prefix}"; \ + echo "libdir=\$${prefix}"; \ echo "includedir=$$(pwd)/include"; \ echo "upstreamincludedir=$$(pwd)/$(OUT)/upstream/include"; \ echo "libxtbuildincludedir=$$(pwd)/include/libxt-build"; \ @@ -29,8 +36,9 @@ define write_pc echo "Name: $(1)"; \ echo "Description: libx11-compat $(1) shim"; \ echo "Version: $(2)"; \ - echo "Libs: -L$(abspath $(OUT)) $(3)"; \ - echo "Cflags: -I$$(pwd)/include -I$$(pwd)/$(OUT)/upstream/include $(4)"; \ + $(if $(5),echo "Requires: $(5)";) \ + echo "Libs: -L\$${libdir} $(3)"; \ + echo "Cflags: -I\$${includedir} -I\$${upstreamincludedir} $(4)"; \ } > $@ endef @@ -38,17 +46,25 @@ $(PKGCONFIG_DIR)/x11.pc: $(UPSTREAM_HEADERS_STAMP) mk/pkgconfig.mk | $(PKGCONFIG @echo " PC $@" $(call write_pc,x11,1.8.13,-lX11-compat,) +$(PKGCONFIG_DIR)/xcb.pc: mk/pkgconfig.mk | $(PKGCONFIG_DIR) + @echo " PC $@" + $(call write_pc,xcb,1.15,-lxcb-compat,) + +$(PKGCONFIG_DIR)/x11-xcb.pc: mk/pkgconfig.mk | $(PKGCONFIG_DIR) + @echo " PC $@" + $(call write_pc,x11-xcb,1.8.0,-lX11-xcb-compat,,x11 xcb) + $(PKGCONFIG_DIR)/xpm.pc: $(UPSTREAM_HEADERS_STAMP) mk/pkgconfig.mk | $(PKGCONFIG_DIR) @echo " PC $@" $(call write_pc,xpm,3.5.19,-lXpm-compat -lX11-compat,) $(PKGCONFIG_DIR)/xt.pc: $(UPSTREAM_HEADERS_STAMP) mk/pkgconfig.mk | $(PKGCONFIG_DIR) @echo " PC $@" - $(call write_pc,xt,1.3.1,-lXt-compat -lX11-compat,-I$$(pwd)/include/libxt-build) + $(call write_pc,xt,1.3.1,-lXt-compat -lX11-compat,-I\$${libxtbuildincludedir}) $(PKGCONFIG_DIR)/xmu.pc: $(UPSTREAM_HEADERS_STAMP) mk/pkgconfig.mk | $(PKGCONFIG_DIR) @echo " PC $@" - $(call write_pc,xmu,1.0,-lXmu-compat -lXt-compat -lX11-compat,-I$$(pwd)/include/libxt-build) + $(call write_pc,xmu,1.0,-lXmu-compat -lXt-compat -lX11-compat,-I\$${libxtbuildincludedir}) $(PKGCONFIG_DIR)/xext.pc: $(UPSTREAM_HEADERS_STAMP) mk/pkgconfig.mk | $(PKGCONFIG_DIR) @echo " PC $@" @@ -68,7 +84,7 @@ $(PKGCONFIG_DIR)/sm.pc: $(UPSTREAM_HEADERS_STAMP) mk/pkgconfig.mk | $(PKGCONFIG_ $(PKGCONFIG_DIR)/xaw7.pc: $(UPSTREAM_HEADERS_STAMP) mk/pkgconfig.mk | $(PKGCONFIG_DIR) @echo " PC $@" - $(call write_pc,xaw7,1.0.16,-lXaw-compat -lXt-compat -lXmu-compat -lXpm-compat -lX11-compat,-I$$(pwd)/include/libxt-build) + $(call write_pc,xaw7,1.0.16,-lXaw-compat -lXt-compat -lXmu-compat -lXpm-compat -lX11-compat,-I\$${libxtbuildincludedir}) # xproto.pc covers protocol-only headers. Apps like xclock pkg-config it # to get the X11/Xproto.h include path; the upstream-headers stamp already diff --git a/mk/tests.mk b/mk/tests.mk index 8f79b905..c90cc598 100644 --- a/mk/tests.mk +++ b/mk/tests.mk @@ -8,6 +8,14 @@ CHECK_BINS := $(OUT)/tests/check $(OUT)/tests/symbol-coverage \ $(OUT)/tests/test-xft-link \ $(OUT)/tests/test-xlibint-link \ $(OUT)/tests/test-xtest +ifeq ($(XCB),1) +CHECK_BINS += $(OUT)/tests/test-xcb-link +CHECK_BINS += $(OUT)/tests/test-xcb-setup +CHECK_BINS += $(OUT)/tests/test-xcb-window $(OUT)/tests/test-xcb-property +CHECK_BINS += $(OUT)/tests/test-xlib-xcb +CHECK_BINS += $(OUT)/tests/test-xcb-events +CHECK_BINS += $(OUT)/tests/test-xcb-drawing +endif # The GLX tests only exist when the optional GLX layer is built (GLX=1). # test-glx-link covers the no-provider degrade path; test-glx-provider drives the # full GLX->EGL translation against the in-tree fake EGL provider. @@ -48,7 +56,31 @@ else endif endif -.PHONY: check check-unit check-differential check-link-xaw symbol-coverage api-symbol-coverage bench bench-paths +.PHONY: check check-unit check-differential check-link-xaw symbol-coverage api-symbol-coverage bench bench-paths check-xcb-reference update-xcb-reference + +XCB_REFERENCE_BIN := $(OUT)/tests/probe-system-xcb +XCB_REFERENCE_OUT := tests/data/xcb-reference.txt + +$(XCB_REFERENCE_BIN): tests/probe-system-xcb.c | $(OUT) + @mkdir -p $(dir $@) + @echo " CC $< (system XCB)" + $(Q)$(CC) $(FP_CFLAGS) $(STRICT_CFLAGS) $(CFLAGS_EXTRA) $< \ + $$($(PKG_CONFIG) --cflags --libs xcb) -o $@ + +## Regenerate the deterministic system-XCB oracle under an isolated Xvfb. +update-xcb-reference: $(XCB_REFERENCE_BIN) + @command -v xvfb-run >/dev/null || { echo "Error: xvfb-run is required" >&2; exit 1; } + $(Q)xvfb-run -a -s '-screen 0 320x240x24 -nolisten tcp' \ + $(XCB_REFERENCE_BIN) > $(XCB_REFERENCE_OUT).tmp; \ + mv $(XCB_REFERENCE_OUT).tmp $(XCB_REFERENCE_OUT) + +## Compare the core-XCB probe with the checked-in system-XCB oracle. +check-xcb-reference: $(XCB_REFERENCE_BIN) $(XCB_REFERENCE_OUT) + @command -v xvfb-run >/dev/null || { echo "Error: xvfb-run is required" >&2; exit 1; } + $(Q)tmp=$$(mktemp -d); trap 'rm -rf "$$tmp"' EXIT; \ + xvfb-run -a -s '-screen 0 320x240x24 -nolisten tcp' \ + $(XCB_REFERENCE_BIN) >"$$tmp/actual"; \ + diff -u $(XCB_REFERENCE_OUT) "$$tmp/actual" DIFFERENTIAL_TARGETS := check-differential-motif check-differential-violawww \ check-differential-xmms @@ -71,7 +103,14 @@ check-unit: $(CHECK_BINS) @printf "$(BLUE)RUN$(RESET) tests/check-api-symbols.py\n" $(Q)LIBX11_COMPAT_GLX=$(GLX) $(PYTHON) tests/check-api-symbols.py $(TARGET) tests/api-symbols.txt @printf "$(BLUE)RUN$(RESET) tests/check-host-link-audit.py\n" - $(Q)$(PYTHON) tests/check-host-link-audit.py + $(Q)$(PYTHON) tests/check-host-link-audit.py $(wildcard $(OUT)/lib*-compat.so) +ifeq ($(XCB),1) + @printf "$(BLUE)RUN$(RESET) tests/test-check-xcb-symbols.py\n" + $(Q)$(PYTHON) tests/test-check-xcb-symbols.py + @printf "$(BLUE)RUN$(RESET) scripts/check-xcb-symbols.py\n" + $(Q)$(PYTHON) scripts/check-xcb-symbols.py $(XCB_COMPAT_TARGET) \ + tests/xcb-symbols.txt +endif ## Full local regression suite: unit tests, motif link/demos gates, the ## replay smoke tier, and the SSH-backed differential screenshots. The @@ -263,3 +302,29 @@ $(OUT)/tests/%: tests/%.c $(TARGET) @echo " CC $<" $(Q)$(CC) $(CPPFLAGS) $(FP_CFLAGS) $(CFLAGS_EXTRA) $< $(TARGET) \ $(LDLIBS) $(TEST_LDFLAGS) -o $@ + +ifeq ($(XCB),1) +$(OUT)/tests/test-xcb-setup: tests/test-xcb-setup.c $(XCB_COMPAT_OBJS) $(TARGET) + @mkdir -p $(dir $@) + @echo " CC $<" + $(Q)$(CC) $(CPPFLAGS) $(FP_CFLAGS) $(STRICT_CFLAGS) $(CFLAGS_EXTRA) $< \ + $(XCB_COMPAT_OBJS) $(TARGET) $(LDLIBS) $(TEST_LDFLAGS) -o $@ + +XCB_TESTS := test-xcb-link test-xcb-window test-xcb-property test-xcb-drawing \ + test-xlib-xcb test-xcb-events + +# One recipe; each test names the libraries it needs and nothing more, so the +# link test keeps proving that libxcb-compat resolves on its own. +$(OUT)/tests/test-xcb-link: XCB_TEST_LIBS := -lxcb-compat +$(OUT)/tests/test-xcb-window $(OUT)/tests/test-xcb-property \ +$(OUT)/tests/test-xcb-drawing: XCB_TEST_LIBS := -lxcb-compat -lX11-compat +$(OUT)/tests/test-xlib-xcb $(OUT)/tests/test-xcb-events: \ + XCB_TEST_LIBS := -lX11-xcb-compat -lxcb-compat -lX11-compat + +$(addprefix $(OUT)/tests/,$(XCB_TESTS)): $(OUT)/tests/%: tests/%.c \ + $(X11_XCB_COMPAT_TARGET) $(XCB_COMPAT_TARGET) $(TARGET) + @mkdir -p $(dir $@) + @echo " CC $<" + $(Q)$(CC) $(CPPFLAGS) $(FP_CFLAGS) $(STRICT_CFLAGS) $(CFLAGS_EXTRA) $< \ + -L$(OUT) $(XCB_TEST_LIBS) $(TEST_LDFLAGS) -o $@ +endif diff --git a/mk/upstream-headers.mk b/mk/upstream-headers.mk index 0ad05165..4c9079d8 100644 --- a/mk/upstream-headers.mk +++ b/mk/upstream-headers.mk @@ -6,10 +6,27 @@ UPSTREAM_HEADERS_DIR := $(OUT)/upstream/include UPSTREAM_SRC_DIR := $(OUT)/upstream/src UPSTREAM_HEADERS_STAMP := $(UPSTREAM_HEADERS_DIR)/.upstream-stamp UPSTREAM_SYNC := scripts/sync-upstream-headers.py - -$(UPSTREAM_HEADERS_STAMP): $(UPSTREAM_SYNC) | $(OUT) +# The public xcb/ headers are downloaded and generated, so only ask for them +# when something will compile against them. A default or wasm build then needs +# no network access for them at all. +XCB_STAGE_FLAG := $(if $(filter 1,$(XCB)),--with-xcb) + +# Toggling XCB changes what the sync stages, but nothing on disk that Make +# compares mtimes against, so record the flag in a file that only changes when +# the value does. mk/library.mk pins GLX the same way. +UPSTREAM_STAGE_CONFIG := $(OUT)/upstream.stage-config +.PHONY: FORCE +$(UPSTREAM_STAGE_CONFIG): FORCE | $(OUT) + $(Q)printf 'XCB=%s\n' '$(XCB)' > $@.tmp + $(Q)if test -r $@ && cmp -s $@.tmp $@; then \ + rm -f $@.tmp; \ + else \ + mv $@.tmp $@; \ + fi + +$(UPSTREAM_HEADERS_STAMP): $(UPSTREAM_SYNC) $(UPSTREAM_STAGE_CONFIG) | $(OUT) @echo " SYNC upstream tree -> $(OUT)/upstream" - $(Q)$(PYTHON) $(UPSTREAM_SYNC) fetch $(UPSTREAM_HEADERS_DIR) + $(Q)$(PYTHON) $(UPSTREAM_SYNC) fetch $(XCB_STAGE_FLAG) $(UPSTREAM_HEADERS_DIR) # The staged .c files are created as a side effect of the stamp recipe. # Declaring them here lets dependent .o rules use them as prerequisites @@ -30,7 +47,8 @@ $(EXAMPLE_BINS): | $(UPSTREAM_HEADERS_STAMP) ## Re-download and re-extract upstream headers upstream-sync: | $(OUT) - $(Q)$(PYTHON) $(UPSTREAM_SYNC) fetch --force $(UPSTREAM_HEADERS_DIR) + $(Q)$(PYTHON) $(UPSTREAM_SYNC) fetch --force $(XCB_STAGE_FLAG) \ + $(UPSTREAM_HEADERS_DIR) ## Compare local include/X11/ against the pinned upstream snapshot upstream-diff: diff --git a/mk/wasm-deps.mk b/mk/wasm-deps.mk index 27973362..abbc08fa 100644 --- a/mk/wasm-deps.mk +++ b/mk/wasm-deps.mk @@ -439,6 +439,27 @@ check-wasm: # the result is a symbol-complete wasm module. Separate from check-wasm so the # cheap example smoke does not pull in the libXt build. Skips without emcc. .PHONY: check-wasm-libxt + +# The four wasm app checks all end the same way: smoke the module under node, +# then drive it through scripts/wasm-paint-check.mjs. Keeping that in one place +# stops the four copies drifting; xnedit had already lost the +# WASM_PAINT_REQUIRED gate, so a CI host without node skipped its paint check +# silently while the others failed. +# $(1) app basename, $(2) non-empty to also run the node module smoke +# The body ends without a continuation so endef stays visible to make; call +# sites that continue a shell line add their own trailing backslash. +define wasm_paint_check + if command -v node >/dev/null 2>&1; then \ + $(if $(2),node scripts/wasm-node-smoke.mjs $(OUT)/$(1).wasm || exit 1; \ + ,)node scripts/wasm-paint-check.mjs $(OUT) $(1) || exit 1; \ + elif [ "$${WASM_PAINT_REQUIRED:-0}" = 1 ]; then \ + echo "check-wasm-$(1): node missing for required paint check" >&2; \ + exit 1; \ + else \ + echo " WARN check-wasm-$(1): node missing, skipped paint check" >&2; \ + fi; +endef + ## Link-smoke the wasm libXt archive (skips when emcc is unavailable) check-wasm-libxt: $(Q)if ! command -v emcc >/dev/null 2>&1; then \ diff --git a/mk/wasm-mosaic.mk b/mk/wasm-mosaic.mk index 478ef9d3..ee0c7819 100644 --- a/mk/wasm-mosaic.mk +++ b/mk/wasm-mosaic.mk @@ -182,12 +182,5 @@ check-wasm-mosaic: done; \ case `file -b $(OUT)/mosaic.wasm` in *WebAssembly*) ;; \ *) echo "check-wasm-mosaic: not a wasm module" >&2; exit 1;; esac; \ - if command -v node >/dev/null 2>&1; then \ - node scripts/wasm-node-smoke.mjs $(OUT)/mosaic.wasm || exit 1; \ - node scripts/wasm-paint-check.mjs $(OUT) mosaic || exit 1; \ - else \ - if [ "$${WASM_PAINT_REQUIRED:-0}" = 1 ]; then \ - echo "check-wasm-mosaic: node missing for required paint check" >&2; exit 1; fi; \ - echo " WARN check-wasm-mosaic: node missing, skipped module smoke" >&2; \ - fi; \ + $(call wasm_paint_check,mosaic,smoke) \ echo " OK check-wasm-mosaic (Mosaic cross-compiled to a wasm module)" diff --git a/mk/wasm-xcircuit.mk b/mk/wasm-xcircuit.mk index 1e6e0a5e..94ae8bd8 100644 --- a/mk/wasm-xcircuit.mk +++ b/mk/wasm-xcircuit.mk @@ -244,13 +244,5 @@ check-wasm-xcircuit: *WebAssembly*) ;; \ *) echo "check-wasm-xcircuit: not a wasm module" >&2; exit 1;; \ esac; \ - if command -v node >/dev/null 2>&1; then \ - node scripts/wasm-node-smoke.mjs $(OUT)/xcircuit.wasm || exit 1; \ - node scripts/wasm-paint-check.mjs $(OUT) xcircuit || exit 1; \ - else \ - if [ "$${WASM_PAINT_REQUIRED:-0}" = 1 ]; then \ - echo "check-wasm-xcircuit: node missing for required paint check" >&2; exit 1; \ - fi; \ - echo " WARN check-wasm-xcircuit: node missing, skipped module smoke" >&2; \ - fi; \ + $(call wasm_paint_check,xcircuit,smoke) \ echo " OK check-wasm-xcircuit (xcircuit cross-compiled to a wasm module)" diff --git a/mk/wasm-xnedit.mk b/mk/wasm-xnedit.mk index fd829fb2..743c2d51 100644 --- a/mk/wasm-xnedit.mk +++ b/mk/wasm-xnedit.mk @@ -174,7 +174,5 @@ check-wasm-xnedit: *WebAssembly*) ;; \ *) echo "check-wasm-xnedit: not a wasm module" >&2; exit 1;; \ esac; \ - if command -v node >/dev/null 2>&1; then \ - node scripts/wasm-paint-check.mjs $(OUT) xnedit || exit 1; \ - fi; \ + $(call wasm_paint_check,xnedit) \ echo " OK check-wasm-xnedit (xnedit cross-compiled to a wasm module)" diff --git a/mk/wasm-xwpe.mk b/mk/wasm-xwpe.mk index 6e6ca486..4c42b3ab 100644 --- a/mk/wasm-xwpe.mk +++ b/mk/wasm-xwpe.mk @@ -173,12 +173,5 @@ check-wasm-xwpe: done; \ case `file -b $(OUT)/xwpe.wasm` in *WebAssembly*) ;; \ *) echo "check-wasm-xwpe: not a wasm module" >&2; exit 1;; esac; \ - if command -v node >/dev/null 2>&1; then \ - node scripts/wasm-node-smoke.mjs $(OUT)/xwpe.wasm || exit 1; \ - node scripts/wasm-paint-check.mjs $(OUT) xwpe || exit 1; \ - else \ - if [ "$${WASM_PAINT_REQUIRED:-0}" = 1 ]; then \ - echo "check-wasm-xwpe: node missing for required paint check" >&2; exit 1; fi; \ - echo " WARN check-wasm-xwpe: node missing, skipped module smoke" >&2; \ - fi; \ + $(call wasm_paint_check,xwpe,smoke) \ echo " OK check-wasm-xwpe (xwpe cross-compiled to a wasm module)" diff --git a/mk/wasm.mk b/mk/wasm.mk index a11212cd..88043d26 100644 --- a/mk/wasm.mk +++ b/mk/wasm.mk @@ -35,6 +35,10 @@ HOST_CC ?= cc # missing EGL headers) back into the wasm build. override GLX := 0 +# The XCB shim links the native shared libraries the wasm build does not +# produce, so keep it out regardless of a command-line XCB=1. +override XCB := 0 + # Define OUT/TARGET here (config.mk uses ?= and defers to these) so the # archive path is a static library, not the native .so, from parse time on. OUT ?= build diff --git a/scripts/check-xcb-symbols.py b/scripts/check-xcb-symbols.py new file mode 100644 index 00000000..b20a344c --- /dev/null +++ b/scripts/check-xcb-symbols.py @@ -0,0 +1,75 @@ +#!/usr/bin/env python3 +"""Check libxcb-compat's exported ABI against an exact symbol manifest.""" + +from __future__ import annotations + +import re +import subprocess +import sys +from pathlib import Path + +XCB_API_RE = re.compile(r"^xcb_[a-z0-9_]+$") + + +def exported_symbols(library: Path) -> set[str]: + commands = (("nm", "-D", "-g", str(library)), ("nm", "-g", str(library))) + output = None + for command in commands: + try: + output = subprocess.check_output( + command, text=True, stderr=subprocess.DEVNULL + ) + break + except (OSError, subprocess.CalledProcessError): + continue + if output is None: + raise SystemExit(f"cannot inspect exported symbols in {library}") + + symbols = set() + for line in output.splitlines(): + fields = line.split() + if len(fields) < 2 or " U " in f" {line} ": + continue + symbol = ( + fields[-1].removeprefix("_") if sys.platform == "darwin" else fields[-1] + ) + if XCB_API_RE.fullmatch(symbol): + symbols.add(symbol) + return symbols + + +def manifest_symbols(path: Path) -> set[str]: + symbols = set() + for line_number, raw_line in enumerate(path.read_text().splitlines(), 1): + symbol = raw_line.strip() + if not symbol or symbol.startswith("#"): + continue + if not XCB_API_RE.fullmatch(symbol): + raise SystemExit(f"{path}:{line_number}: invalid XCB symbol: {symbol}") + if symbol in symbols: + raise SystemExit(f"{path}:{line_number}: duplicate symbol: {symbol}") + symbols.add(symbol) + return symbols + + +def main() -> int: + if len(sys.argv) != 3: + print("usage: check-xcb-symbols.py ", file=sys.stderr) + return 2 + exported = exported_symbols(Path(sys.argv[1])) + expected = manifest_symbols(Path(sys.argv[2])) + missing = expected - exported + unexpected = exported - expected + for label, symbols in (("missing", missing), ("unexpected", unexpected)): + if symbols: + print(f"{label} XCB symbols:") + for symbol in sorted(symbols): + print(f" {symbol}") + if missing or unexpected: + return 1 + print(f"XCB symbol coverage complete: {len(exported)} exports") + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/scripts/sync-upstream-headers.py b/scripts/sync-upstream-headers.py index e4b49c82..ef8110af 100644 --- a/scripts/sync-upstream-headers.py +++ b/scripts/sync-upstream-headers.py @@ -35,6 +35,7 @@ import hashlib import os import shutil +import subprocess import sys import tarfile import tempfile @@ -457,6 +458,33 @@ ) +# The XCB headers are built, not shipped: libxcb's release carries xcb.h and +# xcbext.h by hand but generates xproto.h from xcb-proto's XML through +# c_client.py. Both releases are pinned here and the generator runs at stage +# time, so nothing generated is tracked in the repository. +XCB_SOURCES = { + "libxcb": { + "version": "libxcb-1.15", + "url": ( + "https://xorg.freedesktop.org/archive/individual/lib/" "libxcb-1.15.tar.xz" + ), + "sha256": ("cc38744f817cf6814c847e2df37fcb8997357d72fa4bcbc228ae0fe47219a059"), + }, + "xcb-proto": { + "version": "xcb-proto-1.15.2", + "url": ( + "https://xorg.freedesktop.org/archive/individual/proto/" + "xcb-proto-1.15.2.tar.xz" + ), + "sha256": ("7072beb1f680a2fe3f9e535b797c146d22528990c72f63ddb49d2f350a3653ed"), + }, +} + +# Handed to c_client.py the way libxcb's own src/Makefile.am does. +XCB_MAN_PAGE = "X Version 11" +XCB_MAN_SUFFIX = "3" + + def sha256_of(path: Path) -> str: digest = hashlib.sha256() with path.open("rb") as handle: @@ -635,6 +663,71 @@ def relevant_util_member( return base +def _extract_tarball(tarball: Path, dest: Path) -> Path: + """Unpack tarball under dest and return its single top-level directory.""" + with tarfile.open(tarball, "r:*") as tar: + members = [m for m in tar.getmembers() if _is_safe(PurePosixPath(m.name).parts)] + roots = {PurePosixPath(m.name).parts[0] for m in members if m.name != "."} + if len(roots) != 1: + raise SystemExit(f"{tarball.name} has no single top-level directory") + for member in members: + tar.extract(member, dest, filter="data") + return dest / roots.pop() + + +def xcb_index() -> dict[str, tuple[str, bytes]]: + """Build the public XCB headers and return them keyed by staged path. + + xcb.h and xcbext.h ship in libxcb's source release; xproto.h does not + exist there and is generated from xcb-proto's XML by libxcb's c_client.py, + which is what libxcb itself does at build time. + """ + tarballs = { + name: download(source["url"], source["sha256"]) + for name, source in XCB_SOURCES.items() + } + index: dict[str, tuple[str, bytes]] = {} + with tempfile.TemporaryDirectory() as work_name: + work = Path(work_name) + libxcb = _extract_tarball(tarballs["libxcb"], work / "libxcb") + proto = _extract_tarball(tarballs["xcb-proto"], work / "xcb-proto") + for name in ("xcb.h", "xcbext.h"): + index[f"xcb/{name}"] = ( + XCB_SOURCES["libxcb"]["version"], + (libxcb / "src" / name).read_bytes(), + ) + generated = work / "generated" + generated.mkdir() + command = [ + sys.executable, + str(libxcb / "src" / "c_client.py"), + "-c", + XCB_SOURCES["libxcb"]["version"].replace("-", " "), + "-l", + XCB_MAN_PAGE, + "-s", + XCB_MAN_SUFFIX, + "-p", + str(proto), + str(proto / "src" / "xproto.xml"), + ] + result = subprocess.run( + command, cwd=generated, capture_output=True, text=True, check=False + ) + if result.returncode != 0: + raise SystemExit( + "c_client.py failed to generate xproto.h:\n" + result.stderr.strip() + ) + header = generated / "xproto.h" + if not header.exists(): + raise SystemExit("c_client.py produced no xproto.h") + index["xcb/xproto.h"] = ( + XCB_SOURCES["xcb-proto"]["version"], + header.read_bytes(), + ) + return index + + def upstream_index() -> dict[str, tuple[str, bytes]]: """Return ``{rel_path: (source_name, content)}`` merged across sources.""" index: dict[str, tuple[str, bytes]] = {} @@ -741,12 +834,19 @@ def _apply_patch(rel: str, content: bytes) -> bytes: return text.encode("utf-8") -def stamp_token() -> str: +def stamp_token(with_xcb: bool) -> str: lines = [ - "stamp-format=5", + "stamp-format=6", f"sync-script-sha256={sha256_of(Path(__file__).resolve())}", ] lines.extend(f"{src['name']}={src['version']}#{src['sha256']}" for src in SOURCES) + lines.append(f"xcb-staged={int(with_xcb)}") + if with_xcb: + lines.extend( + f"{name}={source['version']}#{source['sha256']}" + for name, source in sorted(XCB_SOURCES.items()) + ) + lines.append(f"xcb-c-client-args={XCB_MAN_PAGE}#{XCB_MAN_SUFFIX}") for src_name, basenames in sorted(SRC_WHITELIST.items()): lines.append(f"{src_name}-src={','.join(sorted(basenames))}") for src in SOURCES: @@ -877,7 +977,8 @@ def cmd_fetch(args: argparse.Namespace) -> int: if getattr(args, "subdir", None): return _restage_subdir(staging_root, args.subdir) stamp = dest / STAMP_NAME - token = stamp_token() + with_xcb = bool(getattr(args, "with_xcb", False)) + token = stamp_token(with_xcb) if ( not args.force and stamp.exists() @@ -888,9 +989,11 @@ def cmd_fetch(args: argparse.Namespace) -> int: # otherwise Make would re-run this recipe on every invocation. os.utime(stamp, None) return 0 - x11_root = dest / "X11" - if x11_root.exists(): - shutil.rmtree(x11_root) + for root_name in ("X11", "xcb"): + root = dest / root_name + if root.exists(): + shutil.rmtree(root) + for sub in _collect_staging_subdirs(): sub_dir = staging_root / sub if sub_dir.exists(): @@ -899,6 +1002,8 @@ def cmd_fetch(args: argparse.Namespace) -> int: stamp.unlink() dest.mkdir(parents=True, exist_ok=True) index = upstream_index() + if with_xcb: + index.update(xcb_index()) for rel, (_source, content) in index.items(): out_path = dest / rel # Defense in depth: even if relevant_member missed a traversal @@ -1015,6 +1120,11 @@ def main(argv: list[str]) -> int: p_fetch.add_argument( "--force", action="store_true", help="ignore the stamp and re-extract" ) + p_fetch.add_argument( + "--with-xcb", + action="store_true", + help="also build and stage the public xcb/ headers (XCB=1 builds only)", + ) p_fetch.add_argument( "--subdir", help="re-stage only this per-library src subdir (e.g. src-libXt), " diff --git a/scripts/wasm-paint-check.mjs b/scripts/wasm-paint-check.mjs index 70c671d8..aade63cc 100644 --- a/scripts/wasm-paint-check.mjs +++ b/scripts/wasm-paint-check.mjs @@ -23,7 +23,9 @@ // .js and, if present, .data must sit in build-dir. // Env: WASM_PAINT_TIMEOUT_MS (default 20000), WASM_PAINT_CALIBRATE=1 (report // only, never fail), WASM_PAINT_STRICT_INPUT=0 (downgrade xwpe input -// failure to a warning), WASM_PAINT_WEBDRIVER (driver binary path). +// failure to a warning), WASM_PAINT_WEBDRIVER (driver binary path), +// WASM_PAINT_REQUEST_TIMEOUT_MS (default 15000, per driver request), +// WASM_PAINT_ATTEMPTS (default 2, retries of a transient failure). import { createServer } from "node:http"; import { readFile, access } from "node:fs/promises"; @@ -38,7 +40,22 @@ if (!buildDir || appArgs.length === 0) { process.exit(2); } const apps = appArgs.map((a) => a.replace(/\.(html|js)$/, "")); -const TIMEOUT_MS = Number(process.env.WASM_PAINT_TIMEOUT_MS || 20000); +// A malformed budget must not silently disable a check or spin forever, so +// anything that is not a finite positive integer falls back to the documented +// default rather than propagating NaN or Infinity into a loop bound. +function positiveInt(value, fallback) { + const parsed = Number(value); + return Number.isInteger(parsed) && parsed > 0 ? parsed : fallback; +} +const TIMEOUT_MS = positiveInt(process.env.WASM_PAINT_TIMEOUT_MS, 20000); +// A probe or action that outlives this is treated as a stuck driver. The +// budget is generous because a loaded CI runner can stall a WebKit request for +// several seconds without anything being wrong. +const REQUEST_TIMEOUT_MS = + positiveInt(process.env.WASM_PAINT_REQUEST_TIMEOUT_MS, 15000); +// Retries cover only what a loaded runner can do before anything renders. Once +// a frame exists, every later outcome is a real result: see the verdict below. +const ATTEMPTS = positiveInt(process.env.WASM_PAINT_ATTEMPTS, 2); const CALIBRATE = process.env.WASM_PAINT_CALIBRATE === "1"; const KEEP_BROWSER = process.env.WASM_PAINT_KEEP_BROWSER === "1"; const STRICT_INPUT = process.env.WASM_PAINT_STRICT_INPUT !== "0"; @@ -316,7 +333,8 @@ function drvReq(method, path, body, timeoutMs) { // A driver that accepts the socket then hangs must not stall the whole run. // Navigation blocks until the page load event, so it gets the paint budget; // ordinary probe/action calls keep the short default. - req.setTimeout(timeoutMs || 5000, () => req.destroy(new Error("driver request timeout"))); + req.setTimeout(timeoutMs || REQUEST_TIMEOUT_MS, + () => req.destroy(new Error("driver request timeout"))); req.on("error", reject); req.end(data); }); @@ -376,6 +394,10 @@ for (const name of apps) { continue; } + let attempt = 0; + let transient = ""; + for (;;) { + attempt++; let sessionId = null; let best = { found: false, w: 0, h: 0, nonFirstPixels: -1 }; let launchErr = null; @@ -541,18 +563,38 @@ for (const name of apps) { const painted = best.w > 0 && best.h > 0 && best.nonFirstPixels > 16; const detail = `canvas=${best.w}x${best.h} nonFirstPixels=${best.nonFirstPixels}`; const inputOk = inputChanged && (!inputErr || !STRICT_INPUT); - if (painted && inputOk && !layoutErr && !visualErr) { - console.log(` OK ${name} (${detail}${inputDetail})`); + if (painted && inputOk && !layoutErr && !visualErr && !launchErr) { + console.log(` OK ${name} (${detail}${inputDetail}${ + attempt > 1 ? ` attempt=${attempt}` : ""})`); if (inputErr && !STRICT_INPUT) console.error(` WARN input: ${inputErr}`); - } else { - console.error(` FAIL ${name} (${detail})`); - if (best.err) console.error(` printErr: ${String(best.err).slice(0, 200)}`); - if (inputErr) console.error(` input: ${inputErr}`); - if (layoutErr) console.error(` layout: ${layoutErr}`); - if (visualErr) console.error(` visual: ${visualErr}`); - if (launchErr) console.error(` driver: ${String(launchErr).slice(0, 200)}`); - failed++; + break; + } + + /* Retry only a run that never produced a frame: that is what a loaded runner + * does to a healthy build. Once the canvas has painted, every later outcome + * is a real result and is reported on this attempt, including a driver + * request that hung during an input action. Keying this on the error alone + * would let a post-paint hang be retried away by a luckier attempt. + */ + transient = painted ? "" + : launchErr ? `driver: ${String(launchErr).slice(0, 120)}` + : `no frame (${detail})`; + if (transient && attempt < ATTEMPTS) { + console.error(` RETRY ${name} (${transient}; attempt ${attempt}/${ATTEMPTS})`); + await sleep(1000); + continue; + } + + console.error(` FAIL ${name} (${detail}${ + attempt > 1 ? ` after ${attempt} attempts` : ""})`); + if (best.err) console.error(` printErr: ${String(best.err).slice(0, 200)}`); + if (inputErr) console.error(` input: ${inputErr}`); + if (layoutErr) console.error(` layout: ${layoutErr}`); + if (visualErr) console.error(` visual: ${visualErr}`); + if (launchErr) console.error(` driver: ${String(launchErr).slice(0, 200)}`); + failed++; + break; } } diff --git a/src/display.c b/src/display.c index db34a85e..68a3a477 100644 --- a/src/display.c +++ b/src/display.c @@ -1,3 +1,4 @@ +#include #include #include #include @@ -408,6 +409,13 @@ static size_t pendingCloseCapacity = 0; #define PENDING_CLOSE_FALLBACK_SLOTS 8 static Display *pendingCloseFallback[PENDING_CLOSE_FALLBACK_SLOTS]; static size_t pendingCloseFallbackCount = 0; +typedef void (*DisplayCloseHook)(Display *display); +static _Atomic DisplayCloseHook displayCloseHook; + +void libx11CompatSetDisplayCloseHook(void (*hook)(Display *display)) +{ + atomic_store_explicit(&displayCloseHook, hook, memory_order_release); +} int XCloseDisplay(Display *display) { @@ -446,6 +454,10 @@ int XCloseDisplay(Display *display) } return 0; } + DisplayCloseHook closeHook = + atomic_load_explicit(&displayCloseHook, memory_order_acquire); + if (closeHook) + closeHook(display); freeExtensionStorage(display); freeSelectionStorage(display); int screenIndex; diff --git a/src/display.h b/src/display.h index 0f66a54d..5ce59cbf 100644 --- a/src/display.h +++ b/src/display.h @@ -97,4 +97,10 @@ Bool compatSdlHasWindowSizeInPixels(void); #define atomicLoadRequest(display) \ __atomic_load_n(&GET_DISPLAY(display)->request, __ATOMIC_RELAXED) + +/* Called when a Display is about to be torn down, so a sibling shim holding a + * handle on it (libxcb-compat) can invalidate that handle first. + */ +void libx11CompatSetDisplayCloseHook(void (*hook)(Display *display)); + #endif //_DISPLAY_H diff --git a/src/drawing.h b/src/drawing.h index b73da34c..fdd3091f 100644 --- a/src/drawing.h +++ b/src/drawing.h @@ -418,4 +418,15 @@ static inline void unionRect(const SDL_Rect *a, out->h = clampToInt(y2 > y1 ? y2 - y1 : 0); } + +/* Create a pixmap bound to a caller-chosen resource id, for the XCB shim. The + * id is released here if creation fails. + */ +Pixmap libx11CompatCreatePixmapWithId(Display *display, + Pixmap pixmap, + Drawable drawable, + unsigned int width, + unsigned int height, + unsigned int depth); + #endif /* _DRAWING_H_ */ diff --git a/src/events.c b/src/events.c index fd637d74..d3bc4926 100644 --- a/src/events.c +++ b/src/events.c @@ -1,3 +1,4 @@ +#include #include #include #include @@ -4922,11 +4923,64 @@ static Bool isInteractiveSdlEvent(const SDL_Event *event) } } +typedef int (*DisplayPredicateHook)(Display *display); +static _Atomic DisplayPredicateHook eventQueueOwnerHook; +static _Atomic DisplayPredicateHook eventWaitCancelledHook; +static _Thread_local unsigned int xcbEventAccessDepth; + +void libx11CompatSetEventQueueOwnerHook(int (*hook)(Display *display)) +{ + atomic_store_explicit(&eventQueueOwnerHook, hook, memory_order_release); +} + +void libx11CompatSetEventWaitCancelledHook(int (*hook)(Display *display)) +{ + atomic_store_explicit(&eventWaitCancelledHook, hook, memory_order_release); +} + +void libx11CompatBeginXcbEventAccess(void) +{ + xcbEventAccessDepth++; +} + +void libx11CompatEndXcbEventAccess(void) +{ + if (xcbEventAccessDepth) + xcbEventAccessDepth--; +} + +static Bool xlibMayConsumeEvents(Display *display) +{ + DisplayPredicateHook owner = + atomic_load_explicit(&eventQueueOwnerHook, memory_order_acquire); + return xcbEventAccessDepth || !owner || owner(display) == 0; +} + int XNextEvent(Display *display, XEvent *event_return) { // https://tronche.com/gui/x/xlib/event-handling/manipulating-event-queue/XNextEvent.html + if (!xlibMayConsumeEvents(display)) { + memset(event_return, 0, sizeof(*event_return)); + return 0; + } SDL_Event event; while (1) { + DisplayPredicateHook cancelled = + atomic_load_explicit(&eventWaitCancelledHook, memory_order_acquire); + if (cancelled && cancelled(display)) { + memset(event_return, 0, sizeof(*event_return)); + return 0; + } + + /* Ownership can change while this call is already waiting, so it is + * re-checked per iteration rather than only on entry. Otherwise a + * client that hands the queue to XCB mid-wait would still have this + * call consume the next event out from under it. + */ + if (!xlibMayConsumeEvents(display)) { + memset(event_return, 0, sizeof(*event_return)); + return 0; + } if (popPutBackEvent(display, event_return)) { printEventInfo(event_return); return 0; @@ -5169,6 +5223,8 @@ int XEventsQueued(Display *display, int mode) { // https://tronche.com/gui/x/xlib/event-handling/XEventsQueued.html // SET_X_SERVER_REQUEST(display, XCB_); + if (!xlibMayConsumeEvents(display)) + return 0; if (mode == QueuedAlready) return displayEventQueueLength(display); if (mode == QueuedAfterFlush) { diff --git a/src/events.h b/src/events.h index 95fb2981..431b2055 100644 --- a/src/events.h +++ b/src/events.h @@ -152,4 +152,14 @@ Bool libx11CompatWarpTargetGlobal(Window window, */ int libx11CompatSnapAxisToIncrement(int current, int inc, int base, int min); +/* Event-queue hooks for a sibling shim that shares this Display. The owner hook + * reports whether Xlib may still consume events; the cancel hook lets a pending + * wait be interrupted. The access counter marks the window in which the shim is + * itself draining the queue, so the owner hook does not lock it out. + */ +void libx11CompatSetEventQueueOwnerHook(int (*hook)(Display *display)); +void libx11CompatSetEventWaitCancelledHook(int (*hook)(Display *display)); +void libx11CompatBeginXcbEventAccess(void); +void libx11CompatEndXcbEventAccess(void); + #endif /* _EVENTS_H_ */ diff --git a/src/gc.c b/src/gc.c index e65ac55c..90d09fdc 100644 --- a/src/gc.c +++ b/src/gc.c @@ -33,21 +33,20 @@ int XFreeGC(Display *display, GC gc) return 1; } -GC XCreateGC(Display *display, - Drawable d, - unsigned long valuemask, - XGCValues *values) +GC libx11CompatCreateGCWithId(Display *display, + GContext contextId, + Drawable d, + unsigned long valuemask, + XGCValues *values) { - // https://tronche.com/gui/x/xlib/GC/XCreateGC.html - SET_X_SERVER_REQUEST(display, X_CreateGC); - TYPE_CHECK(d, DRAWABLE, display, NULL); - if (IS_TYPE(d, WINDOW) && IS_INPUT_ONLY(d)) { - handleError(0, display, d, 0, BadMatch, 0); + if (!IS_TYPE(d, DRAWABLE)) { + handleError(0, display, d, 0, BadDrawable, 0); + FREE_XID(contextId); return NULL; } - XID contextId = ALLOC_XID(); - if (contextId == None) { - handleOutOfMemory(0, display, 0, 0); + if (IS_TYPE(d, WINDOW) && IS_INPUT_ONLY(d)) { + handleError(0, display, d, 0, BadMatch, 0); + FREE_XID(contextId); return NULL; } GraphicContext *gc = malloc(sizeof(GraphicContext)); @@ -126,6 +125,21 @@ GC XCreateGC(Display *display, return graphicContextStruct; } +GC XCreateGC(Display *display, + Drawable d, + unsigned long valuemask, + XGCValues *values) +{ + // https://tronche.com/gui/x/xlib/GC/XCreateGC.html + SET_X_SERVER_REQUEST(display, X_CreateGC); + XID contextId = ALLOC_XID(); + if (contextId == None) { + handleOutOfMemory(0, display, 0, 0); + return NULL; + } + return libx11CompatCreateGCWithId(display, contextId, d, valuemask, values); +} + GContext XGContextFromGC(GC gc) { // https://tronche.com/gui/x/xlib/GC/XGContextFromGC.html diff --git a/src/gc.h b/src/gc.h index c4330bdb..219da5b8 100644 --- a/src/gc.h +++ b/src/gc.h @@ -25,6 +25,7 @@ typedef struct _GraphicContext { int clipOriginX; int clipOriginY; Pixmap clipMask; + /* Stored clip rectangles from XSetClipRectangles / XSetRegion. * clipRectanglesSet distinguishes an empty clip region from no rectangle * clip region. @@ -37,6 +38,7 @@ typedef struct _GraphicContext { // concatenated with itself. size_t numDashes; int arcMode; + /* Bumped on every mutation. Lets the draw path skip re-applying unchanged * SDL state by caching the (gc, generation) pair per drawable / renderer. */ @@ -68,4 +70,14 @@ typedef struct _GraphicContext { #define GET_GC(gc) GET_GC_FROM_XID(((struct _XGC *) (gc))->gid) #endif +/* Create a GC bound to a caller-chosen resource id. The XCB shim needs this + * because an XCB client names every resource itself; the id is released here if + * creation fails. + */ +GC libx11CompatCreateGCWithId(Display *display, + GContext contextId, + Drawable drawable, + unsigned long valueMask, + XGCValues *values); + #endif /* GC_H */ diff --git a/src/pixmap.c b/src/pixmap.c index 89103bf7..e80e94e3 100644 --- a/src/pixmap.c +++ b/src/pixmap.c @@ -98,30 +98,25 @@ static Pixmap createPixmapFromPixels(Display *display, return pixmap; } -Pixmap XCreatePixmap(Display *display, - Drawable drawable, - unsigned int width, - unsigned int height, - unsigned int depth) +Pixmap libx11CompatCreatePixmapWithId(Display *display, + Pixmap pixmap, + Drawable drawable, + unsigned int width, + unsigned int height, + unsigned int depth) { - // https://tronche.com/gui/x/xlib/pixmap-and-cursor/XCreatePixmap.html - SET_X_SERVER_REQUEST(display, X_CreatePixmap); (void) drawable; if (width == 0 || height == 0) { LOG("Width and/or height are 0 in XCreatePixmap: w = %u, h = %u\n", width, height); handleError(0, display, None, 0, BadValue, 0); + FREE_XID(pixmap); return None; } if (!isSupportedPixmapDepth(depth)) { LOG("Got unsupported depth (%u) in XCreatePixmap\n", depth); handleError(0, display, None, 0, BadValue, 0); - return None; - } - XID pixmap = ALLOC_XID(); - if (pixmap == None) { - LOG("Out of memory: Could not allocate XID in XCreatePixmap!\n"); - handleOutOfMemory(0, display, 0, 0); + FREE_XID(pixmap); return None; } PixmapStruct *pixmapStruct = malloc(sizeof(PixmapStruct)); @@ -162,6 +157,24 @@ Pixmap XCreatePixmap(Display *display, return pixmap; } +Pixmap XCreatePixmap(Display *display, + Drawable drawable, + unsigned int width, + unsigned int height, + unsigned int depth) +{ + // https://tronche.com/gui/x/xlib/pixmap-and-cursor/XCreatePixmap.html + SET_X_SERVER_REQUEST(display, X_CreatePixmap); + XID pixmap = ALLOC_XID(); + if (pixmap == None) { + LOG("Out of memory: Could not allocate XID in XCreatePixmap!\n"); + handleOutOfMemory(0, display, 0, 0); + return None; + } + return libx11CompatCreatePixmapWithId(display, pixmap, drawable, width, + height, depth); +} + int XFreePixmap(Display *display, Pixmap pixmap) { // https://tronche.com/gui/x/xlib/pixmap-and-cursor/XFreePixmap.html diff --git a/src/resource-types.c b/src/resource-types.c index a8957a68..52728fb2 100644 --- a/src/resource-types.c +++ b/src/resource-types.c @@ -1,84 +1,213 @@ #include "resource-types.h" +#include +#include +#include #include -#include -#define FIRST_RESOURCE_ID ((XID) 0x100000) +/* The X protocol has a client allocate resource ids as base | (n & mask), so + * the advertised base and mask must not share a bit and every id must fit the + * protocol's 29 bits. Spanning exactly [base, base | mask] means any id a + * client derives that way is one this table can hold. + */ +#define FIRST_RESOURCE_ID ((XID) XID_RESOURCE_BASE) +#define RESOURCE_ID_COUNT ((size_t) XID_RESOURCE_MASK + 1) +#define RESOURCE_CHUNK_SHIFT 10 +#define RESOURCE_CHUNK_SIZE ((size_t) 1 << RESOURCE_CHUNK_SHIFT) +#define RESOURCE_CHUNK_COUNT (RESOURCE_ID_COUNT / RESOURCE_CHUNK_SIZE) +#define RESOURCE_DIRECTORY_SHIFT 9 +#define RESOURCE_DIRECTORY_SIZE ((size_t) 1 << RESOURCE_DIRECTORY_SHIFT) +#define RESOURCE_DIRECTORY_COUNT \ + (RESOURCE_CHUNK_COUNT / RESOURCE_DIRECTORY_SIZE) typedef struct { - Bool allocated; + _Atomic Bool allocated; XID_Struct resource; } ResourceSlot; -static ResourceSlot *resourceSlots = NULL; -static size_t resourceSlotCapacity = 0; -static XID nextResourceId = FIRST_RESOURCE_ID; +typedef struct { + _Atomic(ResourceSlot *) chunks[RESOURCE_DIRECTORY_SIZE]; +} ResourceDirectory; +static _Atomic(ResourceDirectory *) + resourceDirectories[RESOURCE_DIRECTORY_COUNT]; +static pthread_mutex_t resourceMutex = PTHREAD_MUTEX_INITIALIZER; +static XID nextResourceId = FIRST_RESOURCE_ID; static XID_Struct invalidResource = {0, NULL}; -static Bool ensureResourceSlot(size_t index) +static ResourceSlot *resourceSlot(XID id) +{ + if (id < FIRST_RESOURCE_ID || id - FIRST_RESOURCE_ID >= RESOURCE_ID_COUNT) + return NULL; + size_t index = (size_t) (id - FIRST_RESOURCE_ID); + size_t chunkIndex = index >> RESOURCE_CHUNK_SHIFT; + ResourceDirectory *directory = atomic_load_explicit( + &resourceDirectories[chunkIndex >> RESOURCE_DIRECTORY_SHIFT], + memory_order_acquire); + if (!directory) + return NULL; + ResourceSlot *chunk = atomic_load_explicit( + &directory->chunks[chunkIndex & (RESOURCE_DIRECTORY_SIZE - 1)], + memory_order_acquire); + return chunk ? &chunk[index & (RESOURCE_CHUNK_SIZE - 1)] : NULL; +} + +static ResourceSlot *ensureResourceSlot(XID id) { - if (index < resourceSlotCapacity) - return True; - size_t newCapacity = resourceSlotCapacity ? resourceSlotCapacity : 1024; - while (index >= newCapacity) { - if (newCapacity > ((size_t) -1) / 2) - return False; - newCapacity *= 2; + size_t index = (size_t) (id - FIRST_RESOURCE_ID); + size_t chunkIndex = index >> RESOURCE_CHUNK_SHIFT; + size_t directoryIndex = chunkIndex >> RESOURCE_DIRECTORY_SHIFT; + ResourceDirectory *directory = atomic_load_explicit( + &resourceDirectories[directoryIndex], memory_order_relaxed); + if (!directory) { + directory = calloc(1, sizeof(*directory)); + if (!directory) + return NULL; + atomic_store_explicit(&resourceDirectories[directoryIndex], directory, + memory_order_release); } - ResourceSlot *newSlots = - realloc(resourceSlots, newCapacity * sizeof(*newSlots)); - if (!newSlots) - return False; - memset(newSlots + resourceSlotCapacity, 0, - (newCapacity - resourceSlotCapacity) * sizeof(*newSlots)); - resourceSlots = newSlots; - resourceSlotCapacity = newCapacity; - return True; + _Atomic(ResourceSlot *) *chunkSlot = + &directory->chunks[chunkIndex & (RESOURCE_DIRECTORY_SIZE - 1)]; + ResourceSlot *chunk = atomic_load_explicit(chunkSlot, memory_order_relaxed); + if (!chunk) { + chunk = calloc(RESOURCE_CHUNK_SIZE, sizeof(*chunk)); + if (!chunk) + return NULL; + atomic_store_explicit(chunkSlot, chunk, memory_order_release); + } + return &chunk[index & (RESOURCE_CHUNK_SIZE - 1)]; +} + +/* Caller holds resourceMutex. A freshly claimed slot carries no resource yet. + */ +static void claimSlotLocked(ResourceSlot *slot) +{ + slot->resource.type = 0; + slot->resource.dataPointer = NULL; + atomic_store_explicit(&slot->allocated, True, memory_order_release); } XID allocXidResource(void) { - if (nextResourceId < FIRST_RESOURCE_ID) - return None; - size_t index = (size_t) (nextResourceId - FIRST_RESOURCE_ID); - if (!ensureResourceSlot(index)) - return None; - XID id = nextResourceId++; - resourceSlots[index].allocated = True; - resourceSlots[index].resource.type = 0; - resourceSlots[index].resource.dataPointer = NULL; - return id; + pthread_mutex_lock(&resourceMutex); + + /* Step over ids a client reserved for itself: the cursor only moves + * forward, so a reserved id ahead of it must not be handed out twice. + */ + while (nextResourceId - FIRST_RESOURCE_ID < RESOURCE_ID_COUNT) { + XID id = nextResourceId++; + ResourceSlot *slot = ensureResourceSlot(id); + if (!slot) + break; + if (!atomic_load_explicit(&slot->allocated, memory_order_acquire)) { + claimSlotLocked(slot); + pthread_mutex_unlock(&resourceMutex); + return id; + } + } + pthread_mutex_unlock(&resourceMutex); + return None; +} + +Bool reserveXidResource(XID id) +{ + /* The range check needs no lock: both bounds are constants. */ + if (id < FIRST_RESOURCE_ID || id - FIRST_RESOURCE_ID >= RESOURCE_ID_COUNT) + return False; + + /* A client names its own resources, so the id can sit anywhere in the + * advertised range, including above ids handed out so far. + */ + pthread_mutex_lock(&resourceMutex); + ResourceSlot *slot = ensureResourceSlot(id); + Bool free = + slot && !atomic_load_explicit(&slot->allocated, memory_order_acquire); + if (free) + claimSlotLocked(slot); + pthread_mutex_unlock(&resourceMutex); + return free; } void freeXidResource(XID id) { - if (id < FIRST_RESOURCE_ID) - return; - size_t index = (size_t) (id - FIRST_RESOURCE_ID); - if (index >= resourceSlotCapacity) + pthread_mutex_lock(&resourceMutex); + ResourceSlot *slot = resourceSlot(id); + if (!slot) { + pthread_mutex_unlock(&resourceMutex); return; - resourceSlots[index].allocated = False; - resourceSlots[index].resource.type = 0; - resourceSlots[index].resource.dataPointer = NULL; + } + atomic_store_explicit(&slot->allocated, False, memory_order_release); + slot->resource.type = 0; + slot->resource.dataPointer = NULL; + pthread_mutex_unlock(&resourceMutex); } XID_Struct *getXidStruct(XID id) { - if (id < FIRST_RESOURCE_ID) - return &invalidResource; - size_t index = (size_t) (id - FIRST_RESOURCE_ID); - if (index >= resourceSlotCapacity || !resourceSlots[index].allocated) + ResourceSlot *slot = resourceSlot(id); + if (!slot || !atomic_load_explicit(&slot->allocated, memory_order_acquire)) return &invalidResource; - return &resourceSlots[index].resource; + return &slot->resource; +} + +Bool isXidAllocated(XID id) +{ + ResourceSlot *slot = resourceSlot(id); + return slot && atomic_load_explicit(&slot->allocated, memory_order_acquire); } void forEachXidResourceOfType(XResourceType type, void (*visit)(void *data, void *ctx), void *ctx) { - for (size_t i = 0; i < resourceSlotCapacity; i++) { - if (resourceSlots[i].allocated && - resourceSlots[i].resource.type == type) - visit(resourceSlots[i].resource.dataPointer, ctx); + /* Collect first, then visit with the lock dropped. Holding it across the + * callback would serialize correctly but makes every visitor a potential + * deadlock: they run at renderer teardown and are free to call back into + * anything, including this table. Snapshotting keeps the read of each slot + * atomic with respect to freeXidResource without that exposure. + * + * Walking the chunks that exist, rather than the id range they cover, keeps + * this proportional to live resources: a client-chosen id can sit anywhere + * in the advertised space. + */ + void **matches = NULL; + size_t count = 0, capacity = 0; + pthread_mutex_lock(&resourceMutex); + for (size_t d = 0; d < RESOURCE_DIRECTORY_COUNT; d++) { + ResourceDirectory *directory = + atomic_load_explicit(&resourceDirectories[d], memory_order_acquire); + if (!directory) + continue; + for (size_t c = 0; c < RESOURCE_DIRECTORY_SIZE; c++) { + ResourceSlot *chunk = atomic_load_explicit(&directory->chunks[c], + memory_order_acquire); + if (!chunk) + continue; + for (size_t i = 0; i < RESOURCE_CHUNK_SIZE; i++) { + ResourceSlot *slot = &chunk[i]; + if (!atomic_load_explicit(&slot->allocated, + memory_order_acquire) || + slot->resource.type != type) + continue; + if (count == capacity) { + size_t grown = capacity ? capacity * 2 : 64; + void **bigger = realloc(matches, grown * sizeof(*matches)); + if (!bigger) { + /* Out of memory mid-sweep: visit what was collected and + * drop the rest rather than lose the lock discipline or + * abort a teardown path. + */ + goto visit; + } + matches = bigger; + capacity = grown; + } + matches[count++] = slot->resource.dataPointer; + } + } } +visit: + pthread_mutex_unlock(&resourceMutex); + for (size_t i = 0; i < count; i++) + visit(matches[i], ctx); + free(matches); } diff --git a/src/resource-types.h b/src/resource-types.h index a3a58349..bcda5584 100644 --- a/src/resource-types.h +++ b/src/resource-types.h @@ -3,6 +3,14 @@ #include "X11/Xlib.h" +/* Resource ids handed to clients. The X protocol has a client derive its own + * ids as base | (n & mask), so the two must not share a bit, and base | mask + * has to stay inside the 29 bits the protocol gives a resource id. The XCB + * setup record advertises exactly these values. + */ +#define XID_RESOURCE_BASE UINT32_C(0x10000000) +#define XID_RESOURCE_MASK UINT32_C(0x0fffffff) + typedef enum { WINDOW = 1, DRAWABLE = 2, @@ -23,8 +31,10 @@ typedef struct { } XID_Struct; XID allocXidResource(void); +Bool reserveXidResource(XID id); void freeXidResource(XID id); XID_Struct *getXidStruct(XID id); +Bool isXidAllocated(XID id); /* Visit the data pointer of every live resource of a given type. Used to reach * per-resource caches (such as a pixmap's stipple stamp) that must be dropped diff --git a/src/window-internal.c b/src/window-internal.c index 13223acf..2dabf8b1 100644 --- a/src/window-internal.c +++ b/src/window-internal.c @@ -1924,15 +1924,27 @@ static Bool restackWindow(Display *display, if (siblingIndex < 0) return False; size_t target = (size_t) siblingIndex; - if (mode == Above) { + if (mode == Above) target++; - } else if (mode != Below) { - handleError(0, display, window, 0, BadValue, 0); - return False; + if (mode == Above || mode == Below) { + if ((size_t) index < target) + target--; + return moveChildToIndexAndExpose(display, window, target); } - if ((size_t) index < target) - target--; - return moveChildToIndexAndExpose(display, window, target); + Bool overlap = windowsOverlap(window, values->sibling); + Bool siblingAbove = siblingIndex > index; + if (mode == TopIf && overlap && siblingAbove) + return moveChildToIndexAndExpose(display, window, + children->length - 1); + if (mode == BottomIf && overlap && !siblingAbove) + return moveChildToIndexAndExpose(display, window, 0); + if (mode == Opposite && overlap) + return moveChildToIndexAndExpose( + display, window, siblingAbove ? children->length - 1 : 0); + if (mode == TopIf || mode == BottomIf || mode == Opposite) + return True; + handleError(0, display, window, 0, BadValue, 0); + return False; } size_t idx = (size_t) index; @@ -2449,6 +2461,11 @@ Bool configureWindow(Display *display, return False; if (HAS_VALUE(value_mask, CWStackMode)) hasChanged = True; + if (HAS_VALUE(value_mask, CWBorderWidth) && + windowStruct->borderWidth != (unsigned int) values->border_width) { + windowStruct->borderWidth = values->border_width; + hasChanged = True; + } if (HAS_VALUE(value_mask, CWX) || HAS_VALUE(value_mask, CWY)) { int x = oldX, y = oldY; if (HAS_VALUE(value_mask, CWX)) diff --git a/src/window.c b/src/window.c index 1c1855dd..2754c76d 100644 --- a/src/window.c +++ b/src/window.c @@ -1016,7 +1016,16 @@ Window XCreateSimpleWindow(Display *display, CWBackPixel | CWBorderPixel, &attributes); } +static void releaseReservedWindowId(Window requestedID) +{ + if (requestedID != None && isXidAllocated(requestedID) && + getXidStruct(requestedID)->type == 0 && + getXidStruct(requestedID)->dataPointer == NULL) + FREE_XID(requestedID); +} + static Window createWindowImpl(Display *display, + Window requestedID, Window parent, int x, int y, @@ -1032,9 +1041,14 @@ static Window createWindowImpl(Display *display, { // https://tronche.com/gui/x/xlib/window/XCreateWindow.html SET_X_SERVER_REQUEST(display, X_CreateWindow); - TYPE_CHECK(parent, WINDOW, display, None); + if (!isXidAllocated(parent) || !IS_TYPE(parent, WINDOW)) { + handleError(0, display, parent, 0, BadWindow, 0); + releaseReservedWindowId(requestedID); + return None; + } if (valueMask != 0 && !attributes) { handleError(0, display, None, 0, BadValue, 0); + releaseReservedWindowId(requestedID); return None; } @@ -1044,16 +1058,30 @@ static Window createWindowImpl(Display *display, LOG("Bad argument: Given class is InputOnly but border_with is not 0 " "in XCreateWindow!\n"); handleError(0, display, None, 0, BadMatch, 0); + releaseReservedWindowId(requestedID); + return None; + } + if (visual == CopyFromParent) + visual = getDefaultVisual(0); + if (!visual) { + handleError(0, display, None, 0, BadMatch, 0); + releaseReservedWindowId(requestedID); return None; } - Window windowID = ALLOC_XID(); + Window windowID = requestedID != None ? requestedID : ALLOC_XID(); if (windowID == None) { LOG("Out of memory: Could not allocate the window id in " "XCreateWindow!\n"); handleOutOfMemory(0, display, 0, 0); return None; } + if (requestedID != None && + (!isXidAllocated(requestedID) || getXidStruct(requestedID)->type != 0 || + getXidStruct(requestedID)->dataPointer != NULL)) { + handleError(0, display, requestedID, 0, BadIDChoice, 0); + return None; + } WindowStruct *windowStruct = malloc(sizeof(WindowStruct)); if (!windowStruct) { @@ -1084,16 +1112,6 @@ static Window createWindowImpl(Display *display, FREE_XID(windowID); return None; } - if (visual == CopyFromParent) - visual = getDefaultVisual(0); - if (!visual) { - handleError(0, display, None, 0, BadMatch, 0); - removeChildFromParent(windowID); - free(windowStruct); - FREE_XID(windowID); - return None; - } - int visualClass = visual->CLASS_ATTRIBUTE; windowStruct->colormap = (Colormap) XCreateColormap( display, windowID, visual, @@ -1137,8 +1155,28 @@ Window XCreateWindow(Display *display, unsigned long valueMask, XSetWindowAttributes *attributes) { - return createWindowImpl(display, parent, x, y, width, height, border_width, - depth, clazz, visual, valueMask, attributes, False); + return createWindowImpl(display, None, parent, x, y, width, height, + border_width, depth, clazz, visual, valueMask, + attributes, False); +} + +Window libx11CompatCreateWindowWithId(Display *display, + Window window, + Window parent, + int x, + int y, + unsigned int width, + unsigned int height, + unsigned int borderWidth, + int depth, + unsigned int clazz, + Visual *visual, + unsigned long valueMask, + XSetWindowAttributes *attributes) +{ + return createWindowImpl(display, window, parent, x, y, width, height, + borderWidth, depth, clazz, visual, valueMask, + attributes, False); } Window createInternalWindow(Display *display, @@ -1154,8 +1192,9 @@ Window createInternalWindow(Display *display, unsigned long valueMask, XSetWindowAttributes *attributes) { - return createWindowImpl(display, parent, x, y, width, height, border_width, - depth, clazz, visual, valueMask, attributes, True); + return createWindowImpl(display, None, parent, x, y, width, height, + border_width, depth, clazz, visual, valueMask, + attributes, True); } static int destroySubwindowsImpl(Display *display, Window window) @@ -2203,6 +2242,15 @@ Status XGetWindowAttributes(Display *display, window_attributes_return->y = windowStruct->y; window_attributes_return->width = windowStruct->w; window_attributes_return->height = windowStruct->h; + window_attributes_return->border_width = windowStruct->borderWidth; + window_attributes_return->class = + windowStruct->inputOnly ? InputOnly : InputOutput; + window_attributes_return->bit_gravity = windowStruct->bitGravity; + window_attributes_return->win_gravity = windowStruct->winGravity; + window_attributes_return->backing_store = NotUseful; + window_attributes_return->backing_planes = AllPlanes; + window_attributes_return->backing_pixel = 0; + window_attributes_return->save_under = False; if (IS_MAPPED_TOP_LEVEL_WINDOW(window)) { SDL_Window *sdlWindow = GET_WINDOW_STRUCT(window)->sdlWindow; Uint32 flags = SDL_GetWindowFlags(sdlWindow); @@ -2233,6 +2281,10 @@ Status XGetWindowAttributes(Display *display, GET_WINDOW_STRUCT(window)->overrideRedirect; window_attributes_return->your_event_mask = GET_WINDOW_STRUCT(window)->eventMask; + window_attributes_return->all_event_masks = windowStruct->eventMask; + window_attributes_return->do_not_propagate_mask = NoEventMask; + window_attributes_return->map_installed = True; + window_attributes_return->screen = DefaultScreenOfDisplay(display); return 1; } diff --git a/src/window.h b/src/window.h index bbb63288..d0f476c6 100644 --- a/src/window.h +++ b/src/window.h @@ -7,6 +7,20 @@ #include "resource-types.h" #include "util.h" +Window libx11CompatCreateWindowWithId(Display *display, + Window window, + Window parent, + int x, + int y, + unsigned int width, + unsigned int height, + unsigned int borderWidth, + int depth, + unsigned int clazz, + Visual *visual, + unsigned long valueMask, + XSetWindowAttributes *attributes); + typedef struct { Atom property; int dataFormat; diff --git a/tests/check-api-symbols.py b/tests/check-api-symbols.py index c345b551..e045003c 100644 --- a/tests/check-api-symbols.py +++ b/tests/check-api-symbols.py @@ -105,9 +105,10 @@ def main() -> int: failed = _diff_manifest(exported, covered, "API") - # The libx11Compat* shim hooks are only exported on builds that compile the - # live-resize/present layer (macOS). When none are present (e.g. a Linux CI - # build), the shim manifest is expected to be empty and the check is a no-op. + # The libx11Compat* shim hooks cover the live-resize/present layer, which + # only builds on macOS, and the XCB integration hooks, which build + # everywhere. When neither is present the manifest is expected to be empty + # and the check is a no-op. shim_exported = exported_symbols(library, SHIM_API_RE) shim_manifest_path = manifest.with_name("shim-symbols.txt") shim_covered: set[str] = set() diff --git a/tests/check-host-link-audit.py b/tests/check-host-link-audit.py index 965029a6..5fbf2da8 100644 --- a/tests/check-host-link-audit.py +++ b/tests/check-host-link-audit.py @@ -11,6 +11,7 @@ import importlib.util import os import platform +import sys import tempfile _HERE = os.path.dirname(os.path.abspath(__file__)) @@ -66,10 +67,14 @@ def test_alias_farm_exemption(audit): # Same host soname sitting elsewhere under --out (not the farm) -> host. stray = os.path.join(out, "libX11.so.6") open(stray, "w").close() - assert audit.is_host_x11(stray, out) is True, "a stray host lib under --out is not exempt" + assert ( + audit.is_host_x11(stray, out) is True + ), "a stray host lib under --out is not exempt" # The exemption must not depend on out_dir being passed: without it, the # farm path is classified purely on its basename -> host. - assert audit.is_host_x11(alias) is True, "no out_dir: farm path falls back to basename" + assert ( + audit.is_host_x11(alias) is True + ), "no out_dir: farm path falls back to basename" def test_uninspectable_is_reported_not_raised(audit): @@ -122,4 +127,16 @@ def test_otool_fails_closed_on_non_object(audit): test_alias_farm_exemption(audit) test_uninspectable_is_reported_not_raised(audit) test_otool_fails_closed_on_non_object(audit) + artifacts = sys.argv[1:] + if artifacts: + # The alias-farm exemption is keyed on the output directory, so derive + # it from all the artifacts rather than the first one: a caller passing + # paths from several directories, or a bare filename whose dirname is + # empty, would otherwise have the exemption silently disabled. + out_dir = os.path.commonpath( + [os.path.dirname(os.path.abspath(a)) for a in artifacts] + ) + bad = audit.audit_no_host_x11(artifacts, out_dir) + assert not bad, "host X11 dependencies found:\n" + "\n".join(bad) + print(f"OK: no host X11 dependency in {len(artifacts)} artifact(s)") print("OK: host-link audit classifier tests passed") diff --git a/tests/data/xcb-reference.txt b/tests/data/xcb-reference.txt new file mode 100644 index 00000000..add1b918 --- /dev/null +++ b/tests/data/xcb-reference.txt @@ -0,0 +1,9 @@ +setup status=1 byte_order=0 screens=1 roots=1 +screen root_depth=24 width=320 height=240 +atom name=LIBX11_COMPAT_XCB_PROBE length=23 +attributes class=1 gravity=1 map_state=0 override=0 +geometry x=10 y=20 width=160 height=90 border=0 depth=24 +tree parent_is_root=1 children=0 +property format=8 length=9 after=0 value=xcb-probe +event index=0 type=28 synthetic=0 +event index=1 type=19 synthetic=0 diff --git a/tests/private-symbols.txt b/tests/private-symbols.txt index a43ced0f..7e0cf5ff 100644 --- a/tests/private-symbols.txt +++ b/tests/private-symbols.txt @@ -3,23 +3,31 @@ # exposes them as a link contract. src/xft.c is compiled twice: once into the # core .so and once into the standalone libXft-compat.so. The standalone copy # calls these core-internal helpers, so hiding them would break the libXft -# link. Keep this list in sync with the core helpers src/xft.c reaches for. A +# link. The XCB compatibility libraries are the second such consumer: +# compat/xcb-compat.c and compat/xcb-requests.c call the XID table and atom +# helpers listed here across the same boundary. Keep this list in sync with the +# core helpers those two link contracts reach for. A # missing entry fails the libXft-compat link loudly on both platforms: macOS via # the two-level namespace, Linux via the -Wl,--no-undefined added to that link # (mk/xcompat-libs.mk). The libX11 underscore # ABI (_Xdebug, _XGetHostname, _XDefaultError, ...) is deliberately NOT listed # here: the export generator matches every core-defined _X* symbol by pattern, # so that surface is covered without hand-listing (see scripts/gen-export-list.sh). +SCREEN_WINDOW +allocXidResource applyShapeMaskOverDrawnRect captureShapeMaskBaseline clearRendererClip compatFontOpenFamilyFallback compatFontOpenFamilyFallbackForChar +freeXidResource getGcClipIterationCount getWindowRenderer getXidStruct +isValidAtom +isXidAllocated markPixmapReadbackDirty presentDrawableRectIfVisible -SCREEN_WINDOW +reserveXidResource setGcClipForIteration setRendererDrawableClip diff --git a/tests/probe-system-xcb.c b/tests/probe-system-xcb.c new file mode 100644 index 00000000..8863e783 --- /dev/null +++ b/tests/probe-system-xcb.c @@ -0,0 +1,130 @@ +/* Deterministic core-XCB reference probe. */ +#include +#include +#include +#include + +static void fail(const char *message) +{ + fprintf(stderr, "probe-system-xcb: %s\n", message); + exit(1); +} + +static xcb_screen_t *firstScreen(const xcb_setup_t *setup) +{ + xcb_screen_iterator_t iterator = xcb_setup_roots_iterator(setup); + return iterator.rem ? iterator.data : NULL; +} + +int main(void) +{ + int preferredScreen = 0; + xcb_connection_t *connection = xcb_connect(NULL, &preferredScreen); + if (!connection || xcb_connection_has_error(connection)) + fail("cannot connect"); + + const xcb_setup_t *setup = xcb_get_setup(connection); + if (!setup) + fail("connection has no setup"); + xcb_screen_t *screen = firstScreen(setup); + if (!screen) + fail("setup has no screen"); + + printf("setup status=%u byte_order=%u screens=%u roots=%d\n", setup->status, + setup->image_byte_order, setup->roots_len, + xcb_setup_roots_length(setup)); + printf("screen root_depth=%u width=%u height=%u\n", screen->root_depth, + screen->width_in_pixels, screen->height_in_pixels); + + const char atomName[] = "LIBX11_COMPAT_XCB_PROBE"; + xcb_intern_atom_cookie_t atomCookie = + xcb_intern_atom(connection, 0, sizeof(atomName) - 1, atomName); + xcb_intern_atom_reply_t *atomReply = + xcb_intern_atom_reply(connection, atomCookie, NULL); + if (!atomReply || atomReply->atom == XCB_ATOM_NONE) + fail("cannot intern atom"); + xcb_atom_t atom = atomReply->atom; + free(atomReply); + + xcb_get_atom_name_cookie_t nameCookie = xcb_get_atom_name(connection, atom); + xcb_get_atom_name_reply_t *nameReply = + xcb_get_atom_name_reply(connection, nameCookie, NULL); + if (!nameReply) + fail("cannot get atom name"); + printf("atom name=%.*s length=%d\n", + xcb_get_atom_name_name_length(nameReply), + xcb_get_atom_name_name(nameReply), + xcb_get_atom_name_name_length(nameReply)); + free(nameReply); + + xcb_window_t window = xcb_generate_id(connection); + uint32_t values[] = { + screen->white_pixel, + XCB_EVENT_MASK_STRUCTURE_NOTIFY | XCB_EVENT_MASK_PROPERTY_CHANGE}; + xcb_void_cookie_t createCookie = xcb_create_window_checked( + connection, XCB_COPY_FROM_PARENT, window, screen->root, 10, 20, 160, 90, + 0, XCB_WINDOW_CLASS_INPUT_OUTPUT, screen->root_visual, + XCB_CW_BACK_PIXEL | XCB_CW_EVENT_MASK, values); + xcb_generic_error_t *error = xcb_request_check(connection, createCookie); + if (error) + fail("create window failed"); + + const char payload[] = "xcb-probe"; + xcb_change_property(connection, XCB_PROP_MODE_REPLACE, window, atom, + XCB_ATOM_STRING, 8, sizeof(payload) - 1, payload); + + xcb_get_window_attributes_reply_t *attributes = + xcb_get_window_attributes_reply( + connection, xcb_get_window_attributes(connection, window), NULL); + if (!attributes) + fail("cannot get attributes"); + printf("attributes class=%u gravity=%u map_state=%u override=%u\n", + attributes->_class, attributes->win_gravity, attributes->map_state, + attributes->override_redirect); + free(attributes); + + xcb_get_geometry_reply_t *geometry = xcb_get_geometry_reply( + connection, xcb_get_geometry(connection, window), NULL); + if (!geometry) + fail("cannot get geometry"); + printf("geometry x=%d y=%d width=%u height=%u border=%u depth=%u\n", + geometry->x, geometry->y, geometry->width, geometry->height, + geometry->border_width, geometry->depth); + free(geometry); + + xcb_query_tree_reply_t *tree = xcb_query_tree_reply( + connection, xcb_query_tree(connection, window), NULL); + if (!tree) + fail("cannot query tree"); + printf("tree parent_is_root=%u children=%d\n", tree->parent == screen->root, + xcb_query_tree_children_length(tree)); + free(tree); + + xcb_get_property_reply_t *property = xcb_get_property_reply( + connection, + xcb_get_property(connection, 0, window, atom, XCB_GET_PROPERTY_TYPE_ANY, + 0, 64), + NULL); + if (!property) + fail("cannot get property"); + printf("property format=%u length=%d after=%u value=%.*s\n", + property->format, xcb_get_property_value_length(property), + property->bytes_after, xcb_get_property_value_length(property), + (const char *) xcb_get_property_value(property)); + free(property); + + xcb_map_window(connection, window); + xcb_flush(connection); + for (int index = 0; index < 2; index++) { + xcb_generic_event_t *event = xcb_wait_for_event(connection); + if (!event) + fail("property/map operation produced no event"); + printf("event index=%d type=%u synthetic=%u\n", index, + event->response_type & 0x7f, !!(event->response_type & 0x80)); + free(event); + } + + xcb_destroy_window(connection, window); + xcb_disconnect(connection); + return 0; +} diff --git a/tests/shim-symbols.txt b/tests/shim-symbols.txt index 68674ba7..7d4ac96a 100644 --- a/tests/shim-symbols.txt +++ b/tests/shim-symbols.txt @@ -1,14 +1,21 @@ # Non-Xlib shim hooks exported by libX11-compat (all prefixed libx11Compat). -# These are the live-resize / accelerated-present integration points, resolved -# at runtime by clients such as the xwpe live-resize harness. They are not part -# of the Xlib ABI but are still an exported surface, so this manifest is enforced -# by `make symbol-coverage` (via tests/check-api-symbols.py) to catch accidental -# additions or removals. Only exported on builds that compile the live-resize -# layer (macOS); on other platforms the exported set is empty and the check is a -# no-op. Keep this list sorted and in sync with intentional shim API changes. +# These are the live-resize / accelerated-present integration points and the +# hooks the XCB shim installs, resolved at runtime by clients such as the xwpe +# live-resize harness and by libxcb-compat. They are not part of the Xlib ABI +# but are still an exported surface, so this manifest is enforced by +# `make symbol-coverage` (via tests/check-api-symbols.py) to catch accidental +# additions or removals. The live-resize entries only exist on builds that +# compile that layer (macOS); the XCB hooks are unconditional, so the check is +# live on every platform. Keep this list sorted and in sync with intentional +# shim API changes. libx11CompatAcceleratedPresentForcedForTest libx11CompatAcceleratedPresentUsable +libx11CompatBeginXcbEventAccess libx11CompatConfigureLiveResizeLayer +libx11CompatCreateGCWithId +libx11CompatCreatePixmapWithId +libx11CompatCreateWindowWithId +libx11CompatEndXcbEventAccess libx11CompatFailAcceleratedPresentOnceForTest libx11CompatForceAcceleratedPresentForTest libx11CompatForceSoftwarePresent @@ -23,6 +30,9 @@ libx11CompatRegisterLiveResizeReflow libx11CompatResetLiveResizeCoalesce libx11CompatRunDeferredDisplayClose libx11CompatSdlPeepEventsIsXlibDrain +libx11CompatSetDisplayCloseHook +libx11CompatSetEventQueueOwnerHook +libx11CompatSetEventWaitCancelledHook libx11CompatSideQueueEventRemoved libx11CompatSnapAxisToIncrement libx11CompatSoftwareDemotionCountForTest diff --git a/tests/test-check-xcb-symbols.py b/tests/test-check-xcb-symbols.py new file mode 100644 index 00000000..941d8462 --- /dev/null +++ b/tests/test-check-xcb-symbols.py @@ -0,0 +1,49 @@ +#!/usr/bin/env python3 +"""Unit tests for scripts/check-xcb-symbols.py.""" + +from __future__ import annotations + +import importlib.util +import tempfile +from pathlib import Path + +ROOT = Path(__file__).resolve().parent.parent +SCRIPT = ROOT / "scripts" / "check-xcb-symbols.py" + + +def load_checker(): + spec = importlib.util.spec_from_file_location("check_xcb_symbols", SCRIPT) + module = importlib.util.module_from_spec(spec) + assert spec.loader + spec.loader.exec_module(module) + return module + + +def main() -> None: + checker = load_checker() + with tempfile.TemporaryDirectory() as temporary: + manifest = Path(temporary) / "symbols.txt" + manifest.write_text("# comment\nxcb_connect\nxcb_flush\n") + assert checker.manifest_symbols(manifest) == {"xcb_connect", "xcb_flush"} + + manifest.write_text("xcb_connect\nxcb_connect\n") + try: + checker.manifest_symbols(manifest) + except SystemExit as error: + assert "duplicate symbol" in str(error) + else: + raise AssertionError("duplicate manifest symbol was accepted") + + manifest.write_text("XOpenDisplay\n") + try: + checker.manifest_symbols(manifest) + except SystemExit as error: + assert "invalid XCB symbol" in str(error) + else: + raise AssertionError("non-XCB manifest symbol was accepted") + + print("test-check-xcb-symbols: ok") + + +if __name__ == "__main__": + main() diff --git a/tests/test-xcb-drawing.c b/tests/test-xcb-drawing.c new file mode 100644 index 00000000..429ef0e9 --- /dev/null +++ b/tests/test-xcb-drawing.c @@ -0,0 +1,446 @@ +#include +#include +#include +#include + +#define CHECK(c, m) \ + do { \ + if (!(c)) { \ + fprintf(stderr, "%s\n", m); \ + return 1; \ + } \ + } while (0) + +static uint64_t checksum(const xcb_get_image_reply_t *reply) +{ + const uint32_t *pixels = (const uint32_t *) xcb_get_image_data(reply); + size_t count = (size_t) xcb_get_image_data_length(reply) / 4; + uint64_t hash = UINT64_C(1469598103934665603); + for (size_t i = 0; i < count; i++) { + uint32_t pixel = pixels[i] & 0x00ffffff; + for (int byte = 0; byte < 3; byte++) { + hash ^= (pixel >> (byte * 8)) & 0xff; + hash *= UINT64_C(1099511628211); + } + } + return hash; +} + +int main(void) +{ + xcb_connection_t *c = xcb_connect(NULL, NULL); + CHECK(c && !xcb_connection_has_error(c), "connect"); + CHECK(!xcb_get_image_data(NULL) && !xcb_get_image_data_length(NULL) && + !xcb_get_image_data_end(NULL).data && !xcb_get_image_sizeof(NULL), + "NULL image accessor"); + xcb_screen_t *screen = xcb_setup_roots_iterator(xcb_get_setup(c)).data; + xcb_pixmap_t source = xcb_generate_id(c), target = xcb_generate_id(c); + CHECK(!xcb_request_check( + c, xcb_create_pixmap_checked(c, screen->root_depth, source, + screen->root, 16, 16)), + "create source"); + CHECK(!xcb_request_check( + c, xcb_create_pixmap_checked(c, screen->root_depth, target, + screen->root, 16, 16)), + "create target"); + xcb_gcontext_t gc = xcb_generate_id(c), copied = xcb_generate_id(c); + uint32_t values[] = {0x00ffffff, 0x00000000}; + uint32_t mask = XCB_GC_FOREGROUND | XCB_GC_BACKGROUND; + CHECK(!xcb_request_check( + c, xcb_create_gc_checked(c, gc, source, mask, values)), + "create gc"); + CHECK(!xcb_request_check( + c, xcb_create_gc_checked(c, copied, target, mask, values)), + "create copied gc"); + xcb_point_t onePoint = {0, 0}; + xcb_generic_error_t *error = xcb_request_check( + c, xcb_poly_point_checked(c, XCB_COORD_MODE_ORIGIN, source, gc, 65536, + &onePoint)); + CHECK(error && error->error_code == XCB_LENGTH && + error->major_code == XCB_POLY_POINT, + "oversized checked primitive request"); + free(error); + xcb_void_cookie_t uncheckedPrimitive = + xcb_poly_point(c, XCB_COORD_MODE_ORIGIN, source, gc, 65536, &onePoint); + error = (xcb_generic_error_t *) xcb_poll_for_queued_event(c); + CHECK(error && error->error_code == XCB_LENGTH && + error->major_code == XCB_POLY_POINT && + error->full_sequence == uncheckedPrimitive.sequence, + "oversized unchecked primitive request"); + free(error); + uint32_t onePixel = 0; + error = xcb_request_check( + c, xcb_put_image_checked(c, XCB_IMAGE_FORMAT_Z_PIXMAP, source, gc, + UINT16_MAX, UINT16_MAX, 0, 0, 0, + screen->root_depth, UINT32_MAX, + (const uint8_t *) &onePixel)); + CHECK(error && error->error_code == XCB_LENGTH && + error->major_code == XCB_PUT_IMAGE, + "put-image overflowing payload"); + free(error); + error = xcb_request_check( + c, xcb_put_image_checked(c, XCB_IMAGE_FORMAT_Z_PIXMAP, source, gc, 1, 1, + 0, 0, 0, screen->root_depth, UINT32_MAX, + (const uint8_t *) &onePixel)); + CHECK(error && error->error_code == XCB_LENGTH && + error->major_code == XCB_PUT_IMAGE, + "put-image oversized request"); + free(error); + error = xcb_request_check( + c, xcb_put_image_checked(c, XCB_IMAGE_FORMAT_Z_PIXMAP, source, gc, 0, 1, + 0, 0, 0, screen->root_depth, 0, NULL)); + CHECK(error && error->error_code == XCB_VALUE && + error->major_code == XCB_PUT_IMAGE, + "put-image zero width"); + free(error); + uint32_t pixels[16 * 16]; + for (unsigned int y = 0; y < 16; y++) + for (unsigned int x = 0; x < 16; x++) + pixels[y * 16 + x] = ((x * 13) << 16) | ((y * 11) << 8) | (x + y); + CHECK(!xcb_request_check( + c, xcb_put_image_checked(c, XCB_IMAGE_FORMAT_Z_PIXMAP, source, gc, + 16, 16, 0, 0, 0, screen->root_depth, + sizeof(pixels), (uint8_t *) pixels)), + "put image"); + xcb_copy_area(c, source, target, copied, 0, 0, 0, 0, 16, 16); + xcb_no_exposure_event_t *noExpose = + (xcb_no_exposure_event_t *) xcb_poll_for_queued_event(c); + CHECK(noExpose && noExpose->response_type == XCB_NO_EXPOSURE && + noExpose->drawable == target && + noExpose->major_opcode == XCB_COPY_AREA && + noExpose->minor_opcode == 0, + "copy-area no-exposure event"); + free(noExpose); + xcb_get_image_reply_t *image = + xcb_get_image_reply(c, + xcb_get_image(c, XCB_IMAGE_FORMAT_Z_PIXMAP, target, + 0, 0, 16, 16, UINT32_MAX), + NULL); + CHECK(image && xcb_get_image_data_length(image) == 16 * 16 * 4, + "get image"); + xcb_generic_iterator_t imageEnd = xcb_get_image_data_end(image); + CHECK(xcb_get_image_sizeof(image) == (int) (sizeof(*image) + 16 * 16 * 4) && + imageEnd.data == xcb_get_image_data(image) + + xcb_get_image_data_length(image) && + imageEnd.index == xcb_get_image_sizeof(image), + "image layout"); + CHECK(image->visual == XCB_NONE, "pixmap image visual"); + uint64_t hash = checksum(image); + CHECK(hash == UINT64_C(0x293005f544a302e3), "drawing checksum mismatch"); + printf("drawing-checksum=%016" PRIx64 "\n", hash); + free(image); + xcb_copy_area(c, source, target, copied, -2, 0, 0, 0, 4, 4); + xcb_graphics_exposure_event_t *graphicsExpose = + (xcb_graphics_exposure_event_t *) xcb_poll_for_queued_event(c); + CHECK(graphicsExpose && + graphicsExpose->response_type == XCB_GRAPHICS_EXPOSURE && + graphicsExpose->drawable == target && graphicsExpose->x == 0 && + graphicsExpose->y == 0 && graphicsExpose->width == 2 && + graphicsExpose->height == 4 && graphicsExpose->count == 0 && + graphicsExpose->major_opcode == XCB_COPY_AREA && + graphicsExpose->minor_opcode == 0, + "copy-area graphics-exposure event"); + free(graphicsExpose); + xcb_get_image_cookie_t uncheckedImage = + xcb_get_image_unchecked(c, XCB_IMAGE_FORMAT_Z_PIXMAP, + UINT32_C(0xdeadbeef), 0, 0, 1, 1, UINT32_MAX); + error = NULL; + image = xcb_get_image_reply(c, uncheckedImage, &error); + CHECK(!image && !error, "unchecked image error bypasses reply API"); + xcb_generic_error_t *queuedError = + (xcb_generic_error_t *) xcb_poll_for_queued_event(c); + CHECK(queuedError && queuedError->response_type == 0 && + queuedError->error_code == XCB_DRAWABLE && + queuedError->resource_id == UINT32_C(0xdeadbeef) && + queuedError->full_sequence == uncheckedImage.sequence, + "unchecked image error is queued"); + free(queuedError); + xcb_pixmap_t narrowPixmap = xcb_generate_id(c); + xcb_gcontext_t narrowGc = xcb_generate_id(c); + CHECK(!xcb_request_check(c, xcb_create_pixmap_checked(c, 16, narrowPixmap, + screen->root, 1, 1)), + "create 16-bit pixmap"); + CHECK(!xcb_request_check( + c, xcb_create_gc_checked(c, narrowGc, narrowPixmap, 0, NULL)), + "create 16-bit pixmap GC"); + const uint8_t narrowPixels[4] = {0}; + CHECK(!xcb_request_check( + c, xcb_put_image_checked(c, XCB_IMAGE_FORMAT_Z_PIXMAP, + narrowPixmap, narrowGc, 1, 1, 0, 0, 0, + 16, sizeof(narrowPixels), narrowPixels)), + "put padded 16-bit image"); + error = xcb_request_check( + c, xcb_put_image_checked(c, XCB_IMAGE_FORMAT_Z_PIXMAP, narrowPixmap, + narrowGc, 1, 1, 0, 0, 0, 8, + sizeof(narrowPixels), narrowPixels)); + CHECK(error && error->error_code == XCB_MATCH && + error->resource_id == narrowPixmap && + error->major_code == XCB_PUT_IMAGE, + "put image depth BadMatch"); + free(error); + image = + xcb_get_image_reply(c, + xcb_get_image(c, XCB_IMAGE_FORMAT_Z_PIXMAP, + narrowPixmap, 0, 0, 1, 1, UINT32_MAX), + NULL); + CHECK(image && image->depth == 16 && image->visual == XCB_NONE && + xcb_get_image_data_length(image) == sizeof(narrowPixels), + "16-bit image wire stride"); + free(image); + xcb_get_image_reply_t oversizedImage = {.length = UINT32_MAX}; + CHECK(!xcb_get_image_sizeof(&oversizedImage) && + !xcb_get_image_data_length(&oversizedImage) && + !xcb_get_image_data_end(&oversizedImage).data, + "oversized image layout"); + error = NULL; + image = + xcb_get_image_reply(c, + xcb_get_image(c, XCB_IMAGE_FORMAT_Z_PIXMAP, + narrowPixmap, 0, 0, 0, 1, UINT32_MAX), + &error); + CHECK(!image && error && error->error_code == XCB_VALUE && + error->resource_id == 0 && error->major_code == XCB_GET_IMAGE, + "get image zero-width BadValue"); + free(error); + xcb_point_t narrowPoint = {0, 0}; + error = xcb_request_check( + c, xcb_poly_point_checked(c, XCB_COORD_MODE_ORIGIN, narrowPixmap, gc, 1, + &narrowPoint)); + CHECK(error && error->error_code == XCB_MATCH && + error->resource_id == narrowPixmap && + error->major_code == XCB_POLY_POINT, + "cross-depth drawing BadMatch"); + free(error); + error = xcb_request_check( + c, xcb_copy_gc_checked(c, narrowGc, gc, XCB_GC_FOREGROUND)); + CHECK(error && error->error_code == XCB_MATCH && error->resource_id == gc && + error->major_code == XCB_COPY_GC, + "cross-depth copy-GC BadMatch"); + free(error); + uint32_t narrowTile = narrowPixmap; + error = xcb_request_check( + c, xcb_change_gc_checked(c, gc, XCB_GC_TILE, &narrowTile)); + CHECK(error && error->error_code == XCB_MATCH && + error->resource_id == narrowPixmap && + error->major_code == XCB_CHANGE_GC, + "cross-depth GC tile BadMatch"); + free(error); + error = + xcb_request_check(c, xcb_copy_area_checked(c, source, narrowPixmap, + narrowGc, 0, 0, 0, 0, 1, 1)); + CHECK(error && error->error_code == XCB_MATCH && + error->resource_id == narrowPixmap && + error->major_code == XCB_COPY_AREA, + "cross-depth copy-area BadMatch"); + free(error); + xcb_free_gc(c, narrowGc); + xcb_free_pixmap(c, narrowPixmap); + xcb_point_t points[] = {{1, 1}, {2, 3}, {4, 2}}; + xcb_poly_point(c, XCB_COORD_MODE_ORIGIN, source, gc, 3, points); + xcb_poly_line(c, XCB_COORD_MODE_ORIGIN, source, gc, 3, points); + xcb_segment_t segment[] = {{0, 15, 15, 0}}; + xcb_poly_segment(c, source, gc, 1, segment); + xcb_rectangle_t rectangle[] = {{3, 3, 8, 7}}; + xcb_poly_rectangle(c, source, gc, 1, rectangle); + xcb_arc_t arc[] = {{2, 2, 10, 10, 0, 180 * 64}}; + xcb_poly_arc(c, source, gc, 1, arc); + xcb_copy_plane(c, source, target, copied, 0, 0, 0, 0, 8, 8, 1); + uint32_t red = 0x00ff0000; + xcb_change_gc(c, copied, XCB_GC_FOREGROUND, &red); + xcb_copy_gc(c, copied, gc, XCB_GC_FOREGROUND); + xcb_point_t redPoint = {15, 15}; + xcb_poly_point(c, XCB_COORD_MODE_ORIGIN, target, gc, 1, &redPoint); + xcb_image_text_8(c, 1, target, gc, 0, 10, "A"); + xcb_char2b_t letter = {0, 'B'}; + xcb_image_text_16(c, 1, target, gc, 6, 10, &letter); + xcb_window_t window = xcb_generate_id(c); + CHECK( + !xcb_request_check(c, xcb_create_window_checked( + c, screen->root_depth, window, screen->root, + 0, 0, 8, 8, 0, XCB_WINDOW_CLASS_INPUT_OUTPUT, + screen->root_visual, 0, NULL)), + "create window"); + CHECK( + !xcb_request_check(c, xcb_clear_area_checked(c, 0, window, 0, 0, 0, 0)), + "clear area"); + error = + xcb_request_check(c, xcb_free_pixmap_checked(c, UINT32_C(0xdeadbeef))); + CHECK(error && error->error_code == XCB_PIXMAP, "BadPixmap"); + free(error); + xcb_pixmap_t invalidPixmap = xcb_generate_id(c); + error = xcb_request_check( + c, xcb_create_pixmap_checked(c, screen->root_depth, invalidPixmap, + screen->root, 0, 1)); + CHECK(error && error->error_code == XCB_VALUE && error->resource_id == 0 && + error->major_code == XCB_CREATE_PIXMAP, + "pixmap BadValue"); + free(error); + CHECK(!xcb_request_check( + c, xcb_create_pixmap_checked(c, screen->root_depth, invalidPixmap, + screen->root, 1, 1)), + "reuse pixmap ID after error"); + error = xcb_request_check( + c, xcb_create_pixmap_checked(c, screen->root_depth, invalidPixmap, + screen->root, 1, 1)); + CHECK(error && error->error_code == XCB_ID_CHOICE && + error->resource_id == invalidPixmap && + error->major_code == XCB_CREATE_PIXMAP, + "pixmap BadIDChoice metadata"); + free(error); + xcb_free_pixmap(c, invalidPixmap); + xcb_gcontext_t invalidGc = xcb_generate_id(c); + error = xcb_request_check( + c, xcb_create_gc_checked(c, invalidGc, UINT32_C(0xdeadbeef), 0, NULL)); + CHECK(error && error->error_code == XCB_DRAWABLE && + error->resource_id == UINT32_C(0xdeadbeef) && + error->major_code == XCB_CREATE_GC, + "GC BadDrawable"); + free(error); + CHECK(!xcb_request_check( + c, xcb_create_gc_checked(c, invalidGc, target, 0, NULL)), + "reuse GC ID after error"); + error = xcb_request_check( + c, xcb_create_gc_checked(c, invalidGc, target, 0, NULL)); + CHECK(error && error->error_code == XCB_ID_CHOICE && + error->resource_id == invalidGc && + error->major_code == XCB_CREATE_GC, + "GC BadIDChoice resource"); + free(error); + xcb_free_gc(c, invalidGc); + xcb_gcontext_t invalidMaskGc = xcb_generate_id(c); + uint32_t invalidMaskValue = 0; + error = xcb_request_check( + c, xcb_create_gc_checked(c, invalidMaskGc, target, UINT32_C(1) << 31, + &invalidMaskValue)); + CHECK(error && error->error_code == XCB_VALUE && + error->major_code == XCB_CREATE_GC, + "create GC unknown mask"); + free(error); + CHECK(!xcb_request_check( + c, xcb_create_gc_checked(c, invalidMaskGc, target, 0, NULL)), + "reuse GC ID after unknown mask"); + error = xcb_request_check( + c, xcb_change_gc_checked(c, invalidMaskGc, UINT32_C(1) << 31, + &invalidMaskValue)); + CHECK(error && error->error_code == XCB_VALUE && + error->resource_id == (UINT32_C(1) << 31) && + error->major_code == XCB_CHANGE_GC, + "change GC unknown mask"); + free(error); + uint32_t invalidFunction = 99; + error = xcb_request_check( + c, xcb_change_gc_checked(c, invalidMaskGc, XCB_GC_FUNCTION, + &invalidFunction)); + CHECK(error && error->error_code == XCB_VALUE && + error->resource_id == invalidFunction && + error->major_code == XCB_CHANGE_GC, + "change GC invalid function"); + free(error); + uint32_t invalidTile = screen->root; + error = xcb_request_check( + c, xcb_change_gc_checked(c, invalidMaskGc, XCB_GC_TILE, &invalidTile)); + CHECK(error && error->error_code == XCB_PIXMAP && + error->resource_id == screen->root && + error->major_code == XCB_CHANGE_GC, + "change GC tile BadPixmap"); + free(error); + uint32_t invalidFont = screen->root; + error = xcb_request_check( + c, xcb_change_gc_checked(c, invalidMaskGc, XCB_GC_FONT, &invalidFont)); + CHECK(error && error->error_code == XCB_FONT && + error->resource_id == screen->root && + error->major_code == XCB_CHANGE_GC, + "change GC BadFont"); + free(error); + error = xcb_request_check( + c, xcb_copy_area_checked(c, source, UINT32_C(0xdeadbeef), gc, 0, 0, 0, + 0, 1, 1)); + CHECK(error && error->error_code == XCB_DRAWABLE && + error->resource_id == UINT32_C(0xdeadbeef) && + error->major_code == XCB_COPY_AREA, + "copy-area destination BadDrawable metadata"); + free(error); + error = xcb_request_check( + c, xcb_copy_gc_checked(c, invalidMaskGc, gc, UINT32_C(1) << 31)); + CHECK(error && error->error_code == XCB_VALUE && + error->major_code == XCB_COPY_GC, + "copy GC unknown mask"); + free(error); + xcb_free_gc(c, invalidMaskGc); + error = xcb_request_check( + c, xcb_poly_point_checked(c, XCB_COORD_MODE_ORIGIN, target, + UINT32_C(0xdeadbeef), 1, &redPoint)); + CHECK(error && error->error_code == XCB_G_CONTEXT, "BadGC"); + free(error); + error = xcb_request_check( + c, xcb_poly_point_checked(c, XCB_COORD_MODE_ORIGIN, target, gc, + UINT32_MAX, &redPoint)); + CHECK(error && error->error_code == XCB_LENGTH && + error->major_code == XCB_POLY_POINT, + "drawing oversized item count"); + free(error); + error = xcb_request_check( + c, xcb_copy_plane_checked(c, source, target, gc, 0, 0, 0, 0, 1, 1, 3)); + CHECK(error && error->error_code == XCB_VALUE, "copy-plane BadValue"); + free(error); + + /* A depth-16 source makes the out-of-range plane deterministic whatever the + * screen depth is. + */ + xcb_pixmap_t shallowSource = xcb_generate_id(c); + CHECK(!xcb_request_check(c, xcb_create_pixmap_checked(c, 16, shallowSource, + screen->root, 4, 4)), + "create shallow copy-plane source"); + error = xcb_request_check( + c, xcb_copy_plane_checked(c, shallowSource, target, gc, 0, 0, 0, 0, 1, + 1, UINT32_C(1) << 20)); + CHECK(error && error->error_code == XCB_VALUE && + error->major_code == XCB_COPY_PLANE, + "copy-plane above source depth"); + free(error); + CHECK(!xcb_request_check( + c, xcb_copy_plane_checked(c, shallowSource, target, gc, 0, 0, 0, + 0, 1, 1, UINT32_C(1) << 15)), + "copy-plane within source depth"); + xcb_free_pixmap(c, shallowSource); + xcb_pixmap_t oddDepthPixmap = xcb_generate_id(c); + error = xcb_request_check( + c, xcb_create_pixmap_checked(c, 7, oddDepthPixmap, screen->root, 1, 1)); + CHECK(error && error->error_code == XCB_VALUE && + error->major_code == XCB_CREATE_PIXMAP, + "create pixmap unsupported depth"); + free(error); + xcb_get_image_cookie_t outsideImage = xcb_get_image( + c, XCB_IMAGE_FORMAT_Z_PIXMAP, source, 8, 8, 16, 16, UINT32_MAX); + error = NULL; + CHECK(!xcb_get_image_reply(c, outsideImage, &error) && error && + error->error_code == XCB_MATCH, + "image outside drawable"); + free(error); + xcb_free_gc(c, copied); + xcb_free_gc(c, gc); + xcb_free_pixmap(c, target); + xcb_free_pixmap(c, source); + xcb_destroy_window(c, window); + + xcb_gcontext_t orphanGc = xcb_generate_id(c); + CHECK(!xcb_request_check( + c, xcb_create_gc_checked(c, orphanGc, screen->root, 0, NULL)), + "create disconnect-owned GC"); + xcb_connection_t *survivor = xcb_connect(NULL, NULL); + CHECK(survivor && !xcb_connection_has_error(survivor), + "connect GC survivor"); + xcb_disconnect(c); + xcb_point_t survivorPoint = {0, 0}; + error = xcb_request_check( + survivor, + xcb_poly_point_checked( + survivor, XCB_COORD_MODE_ORIGIN, + xcb_setup_roots_iterator(xcb_get_setup(survivor)).data->root, + orphanGc, 1, &survivorPoint)); + CHECK(error && error->error_code == XCB_G_CONTEXT, + "disconnect removes owned GC entries"); + free(error); + xcb_disconnect(survivor); + return 0; +} diff --git a/tests/test-xcb-events.c b/tests/test-xcb-events.c new file mode 100644 index 00000000..5734fbad --- /dev/null +++ b/tests/test-xcb-events.c @@ -0,0 +1,622 @@ +#include +#include +#include +#include +#include +#include +#include +#include +#include "../compat/xcb-compat-private.h" + +#define CHECK(c, m) \ + do { \ + if (!(c)) { \ + fprintf(stderr, "%s\n", m); \ + return 1; \ + } \ + } while (0) + +typedef struct { + xcb_connection_t *connection; + xcb_generic_event_t *result; +} EventWaiter; + +static void *waitForEvent(void *opaque) +{ + EventWaiter *waiter = opaque; + waiter->result = xcb_wait_for_event(waiter->connection); + return NULL; +} + +/* Wait until the connection reports the expected number of blocked waiters. + * The started flag alone only proves a thread entered waitForEvent, not that it + * reached the blocking call, so a sleep here would be a timing guess. + */ +static void waitForWaiters(xcb_connection_t *connection, unsigned int expected) +{ + while (xcbCompatEventWaiters(connection) < expected) + sched_yield(); +} + +static xcb_generic_event_t *convert(Display *display, + xcb_connection_t *connection, + XEvent *event) +{ + XPutBackEvent(display, event); + return xcb_poll_for_queued_event(connection); +} + +static xcb_generic_event_t *pollForType(xcb_connection_t *connection, + uint8_t type) +{ + for (int i = 0; i < 16; i++) { + xcb_generic_event_t *event = xcb_poll_for_queued_event(connection); + if (!event || (event->response_type & 0x7f) == type) + return event; + free(event); + } + return NULL; +} + +int main(void) +{ + Display *display = XOpenDisplay(NULL); + CHECK(display, "open"); + xcb_connection_t *connection = XGetXCBConnection(display); + Window window = XCreateSimpleWindow(display, DefaultRootWindow(display), 1, + 2, 30, 40, 0, 0, 0); + XSetEventQueueOwner(display, XCBOwnsEventQueue); + XEvent x; + memset(&x, 0, sizeof(x)); + x.xexpose.type = Expose; + x.xexpose.display = display; + x.xexpose.window = window; + x.xexpose.send_event = True; + x.xexpose.serial = 0x12345; + x.xexpose.x = 3; + x.xexpose.y = 4; + x.xexpose.width = 5; + x.xexpose.height = 6; + XPutBackEvent(display, &x); + CHECK(XEventsQueued(display, QueuedAlready) == 0, + "Xlib cannot consume XCB-owned queue"); + xcb_expose_event_t *expose = + (xcb_expose_event_t *) xcb_poll_for_queued_event(connection); + CHECK(expose && expose->response_type == (XCB_EXPOSE | 0x80) && + expose->sequence == 0x2345 && expose->window == window && + expose->x == 3 && expose->height == 6, + "expose conversion"); + free(expose); + + memset(&x, 0, sizeof(x)); + x.xgraphicsexpose.type = GraphicsExpose; + x.xgraphicsexpose.drawable = window; + x.xgraphicsexpose.x = 7; + x.xgraphicsexpose.y = 8; + x.xgraphicsexpose.width = 9; + x.xgraphicsexpose.height = 10; + x.xgraphicsexpose.count = 2; + x.xgraphicsexpose.major_code = XCB_COPY_AREA; + x.xgraphicsexpose.minor_code = 11; + xcb_graphics_exposure_event_t *graphicsExpose = + (xcb_graphics_exposure_event_t *) convert(display, connection, &x); + CHECK(graphicsExpose && + graphicsExpose->response_type == XCB_GRAPHICS_EXPOSURE && + graphicsExpose->drawable == window && graphicsExpose->x == 7 && + graphicsExpose->y == 8 && graphicsExpose->width == 9 && + graphicsExpose->height == 10 && graphicsExpose->count == 2 && + graphicsExpose->major_opcode == XCB_COPY_AREA && + graphicsExpose->minor_opcode == 11, + "graphics-expose conversion"); + free(graphicsExpose); + + memset(&x, 0, sizeof(x)); + x.xnoexpose.type = NoExpose; + x.xnoexpose.drawable = window; + x.xnoexpose.major_code = XCB_COPY_PLANE; + x.xnoexpose.minor_code = 12; + xcb_no_exposure_event_t *noExpose = + (xcb_no_exposure_event_t *) convert(display, connection, &x); + CHECK(noExpose && noExpose->response_type == XCB_NO_EXPOSURE && + noExpose->drawable == window && + noExpose->major_opcode == XCB_COPY_PLANE && + noExpose->minor_opcode == 12, + "no-expose conversion"); + free(noExpose); + + memset(&x, 0, sizeof(x)); + x.xconfigure.type = ConfigureNotify; + x.xconfigure.window = window; + x.xconfigure.event = window; + x.xconfigure.width = 31; + x.xconfigure.height = 41; + xcb_configure_notify_event_t *configure = + (xcb_configure_notify_event_t *) convert(display, connection, &x); + CHECK(configure && configure->window == window && configure->width == 31 && + configure->height == 41, + "configure conversion"); + free(configure); + + Window root = DefaultRootWindow(display); + memset(&x, 0, sizeof(x)); + x.xcreatewindow.type = CreateNotify; + x.xcreatewindow.parent = root; + x.xcreatewindow.window = window; + x.xcreatewindow.x = -3; + x.xcreatewindow.y = 4; + x.xcreatewindow.width = 31; + x.xcreatewindow.height = 41; + x.xcreatewindow.border_width = 2; + x.xcreatewindow.override_redirect = True; + xcb_create_notify_event_t *create = + (xcb_create_notify_event_t *) convert(display, connection, &x); + CHECK(create && create->response_type == XCB_CREATE_NOTIFY && + create->parent == root && create->window == window && + create->x == -3 && create->height == 41 && + create->border_width == 2 && create->override_redirect, + "create conversion"); + free(create); + + memset(&x, 0, sizeof(x)); + x.xmap.type = MapNotify; + x.xmap.event = root; + x.xmap.window = window; + x.xmap.override_redirect = True; + xcb_map_notify_event_t *map = + (xcb_map_notify_event_t *) convert(display, connection, &x); + CHECK(map && map->event == root && map->window == window && + map->override_redirect, + "map conversion"); + free(map); + + memset(&x, 0, sizeof(x)); + x.xunmap.type = UnmapNotify; + x.xunmap.event = root; + x.xunmap.window = window; + x.xunmap.from_configure = True; + xcb_unmap_notify_event_t *unmap = + (xcb_unmap_notify_event_t *) convert(display, connection, &x); + CHECK(unmap && unmap->event == root && unmap->window == window && + unmap->from_configure, + "unmap conversion"); + free(unmap); + + memset(&x, 0, sizeof(x)); + x.xreparent.type = ReparentNotify; + x.xreparent.event = root; + x.xreparent.window = window; + x.xreparent.parent = root; + x.xreparent.x = 8; + x.xreparent.y = 9; + x.xreparent.override_redirect = True; + xcb_reparent_notify_event_t *reparent = + (xcb_reparent_notify_event_t *) convert(display, connection, &x); + CHECK(reparent && reparent->event == root && reparent->window == window && + reparent->parent == root && reparent->x == 8 && + reparent->y == 9 && reparent->override_redirect, + "reparent conversion"); + free(reparent); + + memset(&x, 0, sizeof(x)); + x.xgravity.type = GravityNotify; + x.xgravity.event = root; + x.xgravity.window = window; + x.xgravity.x = -7; + x.xgravity.y = 12; + xcb_gravity_notify_event_t *gravity = + (xcb_gravity_notify_event_t *) convert(display, connection, &x); + CHECK(gravity && gravity->event == root && gravity->window == window && + gravity->x == -7 && gravity->y == 12, + "gravity conversion"); + free(gravity); + + memset(&x, 0, sizeof(x)); + x.xmaprequest.type = MapRequest; + x.xmaprequest.parent = root; + x.xmaprequest.window = window; + xcb_map_request_event_t *mapRequest = + (xcb_map_request_event_t *) convert(display, connection, &x); + CHECK(mapRequest && mapRequest->parent == root && + mapRequest->window == window, + "map-request conversion"); + free(mapRequest); + + memset(&x, 0, sizeof(x)); + x.xconfigurerequest.type = ConfigureRequest; + x.xconfigurerequest.parent = root; + x.xconfigurerequest.window = window; + x.xconfigurerequest.above = root; + x.xconfigurerequest.x = -11; + x.xconfigurerequest.y = 12; + x.xconfigurerequest.width = 45; + x.xconfigurerequest.height = 46; + x.xconfigurerequest.border_width = 3; + x.xconfigurerequest.detail = Below; + x.xconfigurerequest.value_mask = CWX | CWHeight | CWStackMode; + xcb_configure_request_event_t *configureRequest = + (xcb_configure_request_event_t *) convert(display, connection, &x); + CHECK(configureRequest && configureRequest->parent == root && + configureRequest->window == window && + configureRequest->sibling == root && configureRequest->x == -11 && + configureRequest->height == 46 && + configureRequest->stack_mode == XCB_STACK_MODE_BELOW && + configureRequest->value_mask == + (XCB_CONFIG_WINDOW_X | XCB_CONFIG_WINDOW_HEIGHT | + XCB_CONFIG_WINDOW_STACK_MODE), + "configure-request conversion"); + free(configureRequest); + + memset(&x, 0, sizeof(x)); + x.xresizerequest.type = ResizeRequest; + x.xresizerequest.window = window; + x.xresizerequest.width = 47; + x.xresizerequest.height = 48; + xcb_resize_request_event_t *resize = + (xcb_resize_request_event_t *) convert(display, connection, &x); + CHECK(resize && resize->window == window && resize->width == 47 && + resize->height == 48, + "resize-request conversion"); + free(resize); + + memset(&x, 0, sizeof(x)); + x.xcirculate.type = CirculateNotify; + x.xcirculate.event = root; + x.xcirculate.window = window; + x.xcirculate.place = PlaceOnBottom; + xcb_circulate_notify_event_t *circulate = + (xcb_circulate_notify_event_t *) convert(display, connection, &x); + CHECK(circulate && circulate->event == root && + circulate->window == window && + circulate->place == XCB_PLACE_ON_BOTTOM, + "circulate conversion"); + free(circulate); + + memset(&x, 0, sizeof(x)); + x.xcirculaterequest.type = CirculateRequest; + x.xcirculaterequest.parent = root; + x.xcirculaterequest.window = window; + x.xcirculaterequest.place = PlaceOnTop; + xcb_circulate_request_event_t *circulateRequest = + (xcb_circulate_request_event_t *) convert(display, connection, &x); + CHECK(circulateRequest && circulateRequest->event == root && + circulateRequest->window == window && + circulateRequest->place == XCB_PLACE_ON_TOP, + "circulate-request conversion"); + free(circulateRequest); + + XSelectInput(display, window, StructureNotifyMask); + CHECK(!xcb_request_check(connection, + xcb_map_window_checked(connection, window)), + "checked map request"); + map = (xcb_map_notify_event_t *) pollForType(connection, XCB_MAP_NOTIFY); + CHECK(map && map->response_type == XCB_MAP_NOTIFY && map->event == window && + map->window == window, + "map request event"); + free(map); + CHECK(!xcb_request_check(connection, + xcb_unmap_window_checked(connection, window)), + "checked unmap request"); + unmap = + (xcb_unmap_notify_event_t *) pollForType(connection, XCB_UNMAP_NOTIFY); + CHECK(unmap && unmap->response_type == XCB_UNMAP_NOTIFY && + unmap->event == window && unmap->window == window && + !unmap->from_configure, + "unmap request event"); + free(unmap); + + memset(&x, 0, sizeof(x)); + x.xproperty.type = PropertyNotify; + x.xproperty.window = window; + x.xproperty.atom = XCB_ATOM_WM_NAME; + x.xproperty.state = PropertyDelete; + xcb_property_notify_event_t *property = + (xcb_property_notify_event_t *) convert(display, connection, &x); + CHECK(property && property->atom == XCB_ATOM_WM_NAME && + property->state == XCB_PROPERTY_DELETE, + "property conversion"); + free(property); + + memset(&x, 0, sizeof(x)); + x.xselectionclear.type = SelectionClear; + x.xselectionclear.window = window; + x.xselectionclear.selection = XCB_ATOM_PRIMARY; + x.xselectionclear.time = 2345; + xcb_selection_clear_event_t *selectionClear = + (xcb_selection_clear_event_t *) convert(display, connection, &x); + CHECK(selectionClear && + selectionClear->response_type == XCB_SELECTION_CLEAR && + selectionClear->owner == window && + selectionClear->selection == XCB_ATOM_PRIMARY && + selectionClear->time == 2345, + "selection-clear conversion"); + free(selectionClear); + + memset(&x, 0, sizeof(x)); + x.xselectionrequest.type = SelectionRequest; + x.xselectionrequest.owner = window; + x.xselectionrequest.requestor = root; + x.xselectionrequest.selection = XCB_ATOM_PRIMARY; + x.xselectionrequest.target = XCB_ATOM_STRING; + x.xselectionrequest.property = XCB_ATOM_WM_NAME; + x.xselectionrequest.time = 3456; + xcb_selection_request_event_t *selectionRequest = + (xcb_selection_request_event_t *) convert(display, connection, &x); + CHECK(selectionRequest && + selectionRequest->response_type == XCB_SELECTION_REQUEST && + selectionRequest->owner == window && + selectionRequest->requestor == root && + selectionRequest->selection == XCB_ATOM_PRIMARY && + selectionRequest->target == XCB_ATOM_STRING && + selectionRequest->property == XCB_ATOM_WM_NAME && + selectionRequest->time == 3456, + "selection-request conversion"); + free(selectionRequest); + + memset(&x, 0, sizeof(x)); + x.xselection.type = SelectionNotify; + x.xselection.requestor = window; + x.xselection.selection = XCB_ATOM_PRIMARY; + x.xselection.target = XCB_ATOM_STRING; + x.xselection.property = XCB_ATOM_NONE; + x.xselection.time = 4567; + xcb_selection_notify_event_t *selectionNotify = + (xcb_selection_notify_event_t *) convert(display, connection, &x); + CHECK(selectionNotify && + selectionNotify->response_type == XCB_SELECTION_NOTIFY && + selectionNotify->requestor == window && + selectionNotify->selection == XCB_ATOM_PRIMARY && + selectionNotify->target == XCB_ATOM_STRING && + selectionNotify->property == XCB_ATOM_NONE && + selectionNotify->time == 4567, + "selection-notify conversion"); + free(selectionNotify); + + memset(&x, 0, sizeof(x)); + x.xcolormap.type = ColormapNotify; + x.xcolormap.window = window; + x.xcolormap.colormap = DefaultColormap(display, DefaultScreen(display)); + x.xcolormap.new = True; + x.xcolormap.state = ColormapInstalled; + xcb_colormap_notify_event_t *colormap = + (xcb_colormap_notify_event_t *) convert(display, connection, &x); + CHECK(colormap && colormap->response_type == XCB_COLORMAP_NOTIFY && + colormap->window == window && + colormap->colormap == + DefaultColormap(display, DefaultScreen(display)) && + colormap->_new && colormap->state == XCB_COLORMAP_STATE_INSTALLED, + "colormap conversion"); + free(colormap); + + memset(&x, 0, sizeof(x)); + x.xmapping.type = MappingNotify; + x.xmapping.window = window; + x.xmapping.request = MappingKeyboard; + x.xmapping.first_keycode = 17; + x.xmapping.count = 23; + xcb_mapping_notify_event_t *mapping = + (xcb_mapping_notify_event_t *) convert(display, connection, &x); + CHECK(mapping && mapping->response_type == XCB_MAPPING_NOTIFY && + mapping->request == XCB_MAPPING_KEYBOARD && + mapping->first_keycode == 17 && mapping->count == 23, + "mapping conversion"); + free(mapping); + + memset(&x, 0, sizeof(x)); + x.xclient.type = ClientMessage; + x.xclient.window = window; + x.xclient.message_type = XCB_ATOM_WM_NAME; + x.xclient.format = 8; + memcpy(x.xclient.data.b, "client-message-data!", 20); + xcb_client_message_event_t *client = + (xcb_client_message_event_t *) convert(display, connection, &x); + CHECK(client && client->window == window && client->format == 8 && + !memcmp(client->data.data8, "client-message-data!", 20), + "client conversion"); + free(client); + + memset(&x, 0, sizeof(x)); + x.xfocus.type = FocusIn; + x.xfocus.window = window; + x.xfocus.mode = NotifyGrab; + x.xfocus.detail = NotifyPointer; + xcb_focus_in_event_t *focus = + (xcb_focus_in_event_t *) convert(display, connection, &x); + CHECK(focus && focus->event == window && + focus->mode == XCB_NOTIFY_MODE_GRAB && + focus->detail == XCB_NOTIFY_DETAIL_POINTER, + "focus conversion"); + free(focus); + memset(&x, 0, sizeof(x)); + x.xkeymap.type = KeymapNotify; + x.xkeymap.serial = 0x34567; + x.xkeymap.window = window; + for (size_t i = 0; i < sizeof(x.xkeymap.key_vector); i++) + x.xkeymap.key_vector[i] = (char) (i * 7u + 3u); + xcb_keymap_notify_event_t *keymap = + (xcb_keymap_notify_event_t *) convert(display, connection, &x); + CHECK(keymap && keymap->response_type == XCB_KEYMAP_NOTIFY && + !memcmp(keymap->keys, x.xkeymap.key_vector + 1, + sizeof(keymap->keys)) && + ((xcb_generic_event_t *) keymap)->full_sequence == 0x34567, + "keymap conversion"); + free(keymap); + memset(&x, 0, sizeof(x)); + x.xkey.type = KeyPress; + x.xkey.window = window; + x.xkey.root = DefaultRootWindow(display); + x.xkey.keycode = 42; + x.xkey.x = 7; + x.xkey.state = ShiftMask; + xcb_key_press_event_t *key = + (xcb_key_press_event_t *) convert(display, connection, &x); + CHECK(key && key->event == window && key->detail == 42 && + key->event_x == 7 && key->state == XCB_MOD_MASK_SHIFT, + "key conversion"); + free(key); + memset(&x, 0, sizeof(x)); + x.xbutton.type = ButtonPress; + x.xbutton.window = window; + x.xbutton.button = 3; + xcb_button_press_event_t *button = + (xcb_button_press_event_t *) convert(display, connection, &x); + CHECK(button && button->event == window && button->detail == 3, + "button conversion"); + free(button); + memset(&x, 0, sizeof(x)); + x.xmotion.type = MotionNotify; + x.xmotion.window = window; + x.xmotion.is_hint = NotifyHint; + x.xmotion.x_root = 19; + xcb_motion_notify_event_t *motion = + (xcb_motion_notify_event_t *) convert(display, connection, &x); + CHECK(motion && motion->event == window && + motion->detail == XCB_MOTION_HINT && motion->root_x == 19, + "motion conversion"); + free(motion); + memset(&x, 0, sizeof(x)); + x.xcrossing.type = EnterNotify; + x.xcrossing.window = window; + x.xcrossing.root = root; + x.xcrossing.subwindow = window; + x.xcrossing.time = 1234; + x.xcrossing.x = -2; + x.xcrossing.y = 3; + x.xcrossing.x_root = 18; + x.xcrossing.y_root = 19; + x.xcrossing.mode = NotifyGrab; + x.xcrossing.detail = NotifyInferior; + x.xcrossing.same_screen = True; + x.xcrossing.focus = True; + x.xcrossing.state = ControlMask; + xcb_enter_notify_event_t *enter = + (xcb_enter_notify_event_t *) convert(display, connection, &x); + CHECK(enter && enter->response_type == XCB_ENTER_NOTIFY && + enter->event == window && enter->root == root && + enter->child == window && enter->time == 1234 && + enter->event_x == -2 && enter->root_y == 19 && + enter->mode == XCB_NOTIFY_MODE_GRAB && + enter->detail == XCB_NOTIFY_DETAIL_INFERIOR && + enter->state == XCB_MOD_MASK_CONTROL && + enter->same_screen_focus == 3, + "enter conversion"); + free(enter); + x.xcrossing.type = LeaveNotify; + x.xcrossing.focus = False; + xcb_leave_notify_event_t *leave = + (xcb_leave_notify_event_t *) convert(display, connection, &x); + CHECK(leave && leave->response_type == XCB_LEAVE_NOTIFY && + leave->same_screen_focus == 1, + "leave conversion"); + free(leave); + memset(&x, 0, sizeof(x)); + x.xvisibility.type = VisibilityNotify; + x.xvisibility.window = window; + x.xvisibility.state = VisibilityPartiallyObscured; + xcb_visibility_notify_event_t *visibility = + (xcb_visibility_notify_event_t *) convert(display, connection, &x); + CHECK(visibility && visibility->response_type == XCB_VISIBILITY_NOTIFY && + visibility->window == window && + visibility->state == XCB_VISIBILITY_PARTIALLY_OBSCURED, + "visibility conversion"); + free(visibility); + memset(&x, 0, sizeof(x)); + x.xdestroywindow.type = DestroyNotify; + x.xdestroywindow.event = DefaultRootWindow(display); + x.xdestroywindow.window = window; + xcb_destroy_notify_event_t *destroy = + (xcb_destroy_notify_event_t *) convert(display, connection, &x); + CHECK(destroy && destroy->event == DefaultRootWindow(display) && + destroy->window == window, + "destroy conversion"); + free(destroy); + CHECK(!xcb_poll_for_event(connection), "empty poll"); + CHECK(!xcb_poll_for_special_event(connection, NULL), "special empty"); + memset(&x, 0, sizeof(x)); + x.xfocus.type = FocusOut; + x.xfocus.window = window; + XPutBackEvent(display, &x); + xcb_generic_event_t *waited = xcb_wait_for_event(connection); + CHECK(waited && waited->response_type == XCB_FOCUS_OUT, "blocking wait"); + free(waited); + XSetEventQueueOwner(display, XlibOwnsEventQueue); + memset(&x, 0, sizeof(x)); + x.xfocus.type = FocusIn; + x.xfocus.window = window; + XPutBackEvent(display, &x); + CHECK(XEventsQueued(display, QueuedAlready) == 1, + "Xlib sees Xlib-owned queue"); + XSetEventQueueOwner(display, XCBOwnsEventQueue); + waited = xcb_poll_for_queued_event(connection); + CHECK(waited && waited->response_type == XCB_FOCUS_IN, + "Xlib-to-XCB switch preserves event"); + free(waited); + memset(&x, 0, sizeof(x)); + x.xfocus.type = FocusOut; + x.xfocus.window = window; + XPutBackEvent(display, &x); + XSetEventQueueOwner(display, XlibOwnsEventQueue); + CHECK(XEventsQueued(display, QueuedAlready) == 1, + "XCB-to-Xlib switch preserves event"); + XEvent consumed; + XNextEvent(display, &consumed); + CHECK(consumed.type == FocusOut, "Xlib consumes after owner switch"); + XSelectInput(display, window, NoEventMask); + CHECK(!xcb_request_check(connection, + xcb_destroy_window_checked(connection, window)), + "checked destroy request"); + xcb_disconnect(connection); + XCloseDisplay(display); + + display = XOpenDisplay(NULL); + CHECK(display, "open display-close test"); + connection = XGetXCBConnection(display); + CHECK(connection, "display-close connection"); + EventWaiter waiter = {.connection = connection}; + pthread_t thread; + CHECK(!pthread_create(&thread, NULL, waitForEvent, &waiter), + "display-close waiter creation"); + waitForWaiters(connection, 1); + XCloseDisplay(display); + CHECK(!pthread_join(thread, NULL), "display-close waiter join"); + CHECK(!waiter.result && + xcb_connection_has_error(connection) == XCB_CONN_ERROR, + "display close wakes XCB waiter"); + xcb_disconnect(connection); + + connection = xcb_connect(NULL, NULL); + CHECK(connection && !xcb_connection_has_error(connection), + "open disconnect test"); + EventWaiter errorWaiter = {.connection = connection}; + pthread_t errorThread; + CHECK(!pthread_create(&errorThread, NULL, waitForEvent, &errorWaiter), + "unchecked-error waiter creation"); + waitForWaiters(connection, 1); + xcb_void_cookie_t unchecked = + xcb_destroy_window(connection, UINT32_C(0xdeadbeef)); + CHECK(!pthread_join(errorThread, NULL), "unchecked-error waiter join"); + xcb_generic_error_t *uncheckedError = + (xcb_generic_error_t *) errorWaiter.result; + CHECK(uncheckedError && uncheckedError->response_type == 0 && + uncheckedError->error_code == XCB_WINDOW && + uncheckedError->full_sequence == unchecked.sequence, + "unchecked error wakes XCB waiter"); + free(uncheckedError); + enum { DISCONNECT_WAITER_COUNT = 4 }; + EventWaiter disconnectWaiters[DISCONNECT_WAITER_COUNT] = {0}; + pthread_t disconnectThreads[DISCONNECT_WAITER_COUNT]; + for (size_t i = 0; i < DISCONNECT_WAITER_COUNT; i++) { + disconnectWaiters[i].connection = connection; + CHECK(!pthread_create(&disconnectThreads[i], NULL, waitForEvent, + &disconnectWaiters[i]), + "disconnect waiter creation"); + } + waitForWaiters(connection, DISCONNECT_WAITER_COUNT); + xcb_disconnect(connection); + for (size_t i = 0; i < DISCONNECT_WAITER_COUNT; i++) { + CHECK(!pthread_join(disconnectThreads[i], NULL), + "disconnect waiter join"); + CHECK(!disconnectWaiters[i].result, "disconnect wakes XCB waiters"); + } + puts("test-xcb-events: ok"); + return 0; +} diff --git a/tests/test-xcb-link.c b/tests/test-xcb-link.c new file mode 100644 index 00000000..cdcf3050 --- /dev/null +++ b/tests/test-xcb-link.c @@ -0,0 +1,33 @@ +#include +#include + +int main(void) +{ + int screenNumber = -1; + xcb_connection_t *connection = xcb_connect(NULL, &screenNumber); + if (!connection || xcb_connection_has_error(connection)) { + fprintf(stderr, "test-xcb-link: connection failed\n"); + return 1; + } + const xcb_setup_t *setup = xcb_get_setup(connection); + if (!setup) { + fprintf(stderr, "test-xcb-link: invalid setup\n"); + xcb_disconnect(connection); + return 1; + } + xcb_screen_iterator_t screens = xcb_setup_roots_iterator(setup); + if (setup->status != 1 || screenNumber != 0 || screens.rem != 1 || + !screens.data || screens.data->root == XCB_WINDOW_NONE) { + fprintf(stderr, "test-xcb-link: invalid setup\n"); + xcb_disconnect(connection); + return 1; + } + if (!xcb_flush(connection)) { + fprintf(stderr, "test-xcb-link: flush failed\n"); + xcb_disconnect(connection); + return 1; + } + xcb_disconnect(connection); + puts("test-xcb-link: ok"); + return 0; +} diff --git a/tests/test-xcb-property.c b/tests/test-xcb-property.c new file mode 100644 index 00000000..9e1195f2 --- /dev/null +++ b/tests/test-xcb-property.c @@ -0,0 +1,343 @@ +#include +#include +#include +#include +#include +#define CHECK(c, m) \ + do { \ + if (!(c)) { \ + fprintf(stderr, "%s\n", m); \ + return 1; \ + } \ + } while (0) +int main(void) +{ + xcb_connection_t *c = xcb_connect(NULL, NULL); + CHECK(c && !xcb_connection_has_error(c), "connect"); + CHECK( + !xcb_get_atom_name_name(NULL) && !xcb_get_atom_name_name_length(NULL) && + !xcb_get_atom_name_name_end(NULL).data && + !xcb_get_atom_name_sizeof(NULL) && !xcb_get_property_value(NULL) && + !xcb_get_property_value_length(NULL) && + !xcb_get_property_value_end(NULL).data && + !xcb_get_property_sizeof(NULL), + "NULL reply accessor"); + xcb_screen_t *s = xcb_setup_roots_iterator(xcb_get_setup(c)).data; + xcb_window_t w = xcb_generate_id(c); + CHECK(!xcb_request_check( + c, xcb_create_window_checked(c, 24, w, s->root, 0, 0, 20, 20, 0, + XCB_WINDOW_CLASS_INPUT_OUTPUT, + s->root_visual, 0, NULL)), + "create"); + const char name[] = "XCB_TEST_PROPERTY"; + xcb_intern_atom_reply_t *ia = xcb_intern_atom_reply( + c, xcb_intern_atom_unchecked(c, 0, sizeof(name) - 1, name), NULL); + CHECK(ia, "intern"); + xcb_atom_t atom = ia->atom; + free(ia); + xcb_get_atom_name_reply_t *an = + xcb_get_atom_name_reply(c, xcb_get_atom_name_unchecked(c, atom), NULL); + CHECK(an && xcb_get_atom_name_name_length(an) == (int) sizeof(name) - 1 && + !memcmp(xcb_get_atom_name_name(an), name, sizeof(name) - 1), + "atom name"); + xcb_generic_iterator_t atomNameEnd = xcb_get_atom_name_name_end(an); + CHECK( + xcb_get_atom_name_sizeof(an) == + (int) (sizeof(*an) + sizeof(name) - 1) && + atomNameEnd.data == xcb_get_atom_name_name(an) + sizeof(name) - 1 && + atomNameEnd.index == xcb_get_atom_name_sizeof(an), + "atom name layout"); + free(an); + xcb_generic_error_t *error = NULL; + an = xcb_get_atom_name_reply(c, xcb_get_atom_name(c, UINT32_MAX), &error); + CHECK(!an && error && error->error_code == XCB_ATOM && + error->resource_id == UINT32_MAX && + error->major_code == XCB_GET_ATOM_NAME, + "atom name BadAtom"); + free(error); + xcb_get_atom_name_cookie_t uncheckedAtomName = + xcb_get_atom_name_unchecked(c, UINT32_MAX); + error = (xcb_generic_error_t *) UINTPTR_MAX; + an = xcb_get_atom_name_reply(c, uncheckedAtomName, &error); + CHECK(!an && !error, "unchecked atom name bypasses reply error"); + error = (xcb_generic_error_t *) xcb_poll_for_queued_event(c); + CHECK(error && error->error_code == XCB_ATOM && + error->full_sequence == uncheckedAtomName.sequence, + "unchecked atom name queues error"); + free(error); + const char first[] = "abcd", second[] = "ef"; + xcb_change_property(c, XCB_PROP_MODE_REPLACE, w, atom, XCB_ATOM_STRING, 8, + 4, first); + xcb_change_property(c, XCB_PROP_MODE_APPEND, w, atom, XCB_ATOM_STRING, 8, 2, + second); + xcb_get_property_reply_t *p = xcb_get_property_reply( + c, + xcb_get_property_unchecked(c, 0, w, atom, XCB_GET_PROPERTY_TYPE_ANY, 0, + 1), + NULL); + CHECK(p && xcb_get_property_value_length(p) == 4 && p->bytes_after == 2 && + !memcmp(xcb_get_property_value(p), "abcd", 4), + "truncated property"); + xcb_generic_iterator_t propertyEnd = xcb_get_property_value_end(p); + CHECK(xcb_get_property_sizeof(p) == (int) (sizeof(*p) + 4) && + propertyEnd.data == (char *) xcb_get_property_value(p) + 4 && + propertyEnd.index == xcb_get_property_sizeof(p), + "property layout"); + free(p); + + /* Six bytes of an 8-bit property pad to eight on the wire. length counts + * those words, so a client that reads length * 4 bytes must stay inside the + * reply block. + */ + p = xcb_get_property_reply( + c, + xcb_get_property_unchecked(c, 0, w, atom, XCB_GET_PROPERTY_TYPE_ANY, 0, + 16), + NULL); + CHECK(p && xcb_get_property_value_length(p) == 6 && p->length == 2 && + xcb_get_property_sizeof(p) == (int) (sizeof(*p) + 8) && + !memcmp(xcb_get_property_value(p), "abcdef", 6), + "unaligned property payload is padded to its advertised length"); + free(p); + xcb_get_property_cookie_t uncheckedProperty = xcb_get_property_unchecked( + c, 0, w, UINT32_MAX, XCB_GET_PROPERTY_TYPE_ANY, 0, 1); + error = (xcb_generic_error_t *) UINTPTR_MAX; + p = xcb_get_property_reply(c, uncheckedProperty, &error); + CHECK(!p && !error, "unchecked property bypasses reply error"); + error = (xcb_generic_error_t *) xcb_poll_for_queued_event(c); + CHECK(error && error->error_code == XCB_ATOM && + error->full_sequence == uncheckedProperty.sequence, + "unchecked property queues error"); + free(error); + const char prefix[] = "01"; + CHECK(!xcb_request_check( + c, xcb_change_property_checked(c, XCB_PROP_MODE_PREPEND, w, atom, + XCB_ATOM_STRING, 8, 2, prefix)), + "prepend"); + p = xcb_get_property_reply( + c, xcb_get_property(c, 0, w, atom, XCB_GET_PROPERTY_TYPE_ANY, 0, 8), + NULL); + CHECK(p && xcb_get_property_value_length(p) == 8 && + !memcmp(xcb_get_property_value(p), "01abcdef", 8), + "prepend value"); + free(p); + error = xcb_request_check( + c, xcb_change_property_checked(c, XCB_PROP_MODE_APPEND, w, atom, + XCB_ATOM_INTEGER, 16, 1, prefix)); + CHECK(error && error->error_code == XCB_MATCH, "append type mismatch"); + free(error); + error = + xcb_request_check(c, xcb_change_property_checked( + c, XCB_PROP_MODE_REPLACE, w, atom, + XCB_ATOM_STRING, 8, UINT32_C(262144), prefix)); + CHECK(error && error->error_code == XCB_LENGTH && + error->major_code == XCB_CHANGE_PROPERTY, + "property oversized element count"); + free(error); + p = xcb_get_property_reply( + c, xcb_get_property(c, 0, w, atom, XCB_ATOM_INTEGER, 0, 8), NULL); + CHECK(p && p->type == XCB_ATOM_STRING && p->format == 8 && + p->value_len == 0 && p->bytes_after == 8, + "get type mismatch"); + free(p); + p = xcb_get_property_reply( + c, xcb_get_property(c, 0, w, atom, XCB_ATOM_STRING, 2, 8), &error); + CHECK(p && !error && p->value_len == 0 && p->bytes_after == 0, + "offset at end"); + free(p); + p = xcb_get_property_reply( + c, xcb_get_property(c, 0, w, atom, XCB_ATOM_STRING, 3, 8), &error); + CHECK(!p && error && error->error_code == XCB_VALUE, "offset beyond end"); + free(error); + error = NULL; + p = xcb_get_property_reply( + c, xcb_get_property(c, 0, w, atom, XCB_ATOM_STRING, 1, 8), &error); + CHECK(p && !error && xcb_get_property_value_length(p) == 4 && + !memcmp(xcb_get_property_value(p), "cdef", 4), + "offset at final unit"); + free(p); + p = xcb_get_property_reply( + c, xcb_get_property(c, 1, w, atom, XCB_ATOM_STRING, 0, 8), NULL); + CHECK(p && p->bytes_after == 0 && p->value_len == 8, + "delete-on-read reply"); + free(p); + p = xcb_get_property_reply( + c, xcb_get_property(c, 0, w, atom, XCB_GET_PROPERTY_TYPE_ANY, 0, 8), + NULL); + CHECK(p && p->type == XCB_ATOM_NONE, "delete-on-read removal"); + free(p); + uint16_t shorts[] = {0x1234, 0xabcd}; + xcb_change_property(c, XCB_PROP_MODE_REPLACE, w, atom, XCB_ATOM_INTEGER, 16, + 2, shorts); + p = xcb_get_property_reply( + c, xcb_get_property(c, 0, w, atom, XCB_ATOM_INTEGER, 0, 8), NULL); + CHECK(p && p->format == 16 && p->value_len == 2 && + !memcmp(xcb_get_property_value(p), shorts, sizeof(shorts)), + "16-bit property"); + free(p); + uint32_t words[] = {0x12345678, 0x90abcdef}; + xcb_change_property(c, XCB_PROP_MODE_REPLACE, w, atom, XCB_ATOM_CARDINAL, + 32, 2, words); + xcb_list_properties_reply_t *list = + xcb_list_properties_reply(c, xcb_list_properties(c, w), NULL); + CHECK(list && xcb_list_properties_atoms_length(list) == 1 && + xcb_list_properties_atoms(list)[0] == atom && + xcb_list_properties_sizeof(list) == + (int) (sizeof(*list) + sizeof(xcb_atom_t)) && + xcb_list_properties_atoms_end(list).index == + (int) (sizeof(*list) + sizeof(xcb_atom_t)), + "list properties"); + free(list); + xcb_get_selection_owner_reply_t *owner = xcb_get_selection_owner_reply( + c, xcb_get_selection_owner(c, atom), NULL); + CHECK(owner && owner->owner == XCB_NONE, "initial selection owner"); + free(owner); + CHECK(!xcb_request_check( + c, xcb_set_selection_owner_checked(c, w, atom, XCB_CURRENT_TIME)), + "set selection owner"); + owner = xcb_get_selection_owner_reply(c, xcb_get_selection_owner(c, atom), + NULL); + CHECK(owner && owner->owner == w, "get selection owner"); + free(owner); + CHECK(!xcb_request_check( + c, xcb_convert_selection_checked(c, w, atom, XCB_ATOM_STRING, + atom, XCB_CURRENT_TIME)), + "convert selection"); + xcb_selection_request_event_t *selectionRequest = + (xcb_selection_request_event_t *) xcb_poll_for_queued_event(c); + CHECK(selectionRequest && + selectionRequest->response_type == XCB_SELECTION_REQUEST && + selectionRequest->owner == w && + selectionRequest->requestor == w && + selectionRequest->selection == atom && + selectionRequest->target == XCB_ATOM_STRING && + selectionRequest->property == atom, + "selection request event"); + free(selectionRequest); + CHECK(!xcb_request_check(c, xcb_set_selection_owner_checked( + c, XCB_NONE, atom, XCB_CURRENT_TIME)), + "clear selection owner"); + xcb_selection_clear_event_t *selectionClear = + (xcb_selection_clear_event_t *) xcb_poll_for_queued_event(c); + CHECK(selectionClear && + selectionClear->response_type == XCB_SELECTION_CLEAR && + selectionClear->owner == w && selectionClear->selection == atom, + "selection clear event"); + free(selectionClear); + CHECK(!xcb_request_check( + c, xcb_convert_selection_checked(c, w, atom, XCB_ATOM_STRING, + atom, XCB_CURRENT_TIME)), + "convert unowned selection"); + xcb_selection_notify_event_t *selectionNotify = + (xcb_selection_notify_event_t *) xcb_poll_for_queued_event(c); + CHECK(selectionNotify && + selectionNotify->response_type == XCB_SELECTION_NOTIFY && + selectionNotify->requestor == w && + selectionNotify->selection == atom && + selectionNotify->target == XCB_ATOM_STRING && + selectionNotify->property == XCB_NONE, + "selection notify event"); + free(selectionNotify); + error = xcb_request_check(c, xcb_set_selection_owner_checked( + c, 0xdeadbeef, atom, XCB_CURRENT_TIME)); + CHECK(error && error->error_code == XCB_WINDOW, + "selection owner BadWindow"); + free(error); + error = xcb_request_check( + c, xcb_set_selection_owner_checked(c, w, 0xdeadbeef, XCB_CURRENT_TIME)); + CHECK(error && error->error_code == XCB_ATOM && + error->resource_id == UINT32_C(0xdeadbeef), + "selection owner BadAtom"); + free(error); + error = xcb_request_check( + c, xcb_delete_property_checked(c, w, UINT32_C(0xdeadbeef))); + CHECK(error && error->error_code == XCB_ATOM && + error->resource_id == UINT32_C(0xdeadbeef) && + error->major_code == XCB_DELETE_PROPERTY, + "delete property BadAtom metadata"); + free(error); + error = xcb_request_check( + c, xcb_change_property_checked(c, XCB_PROP_MODE_REPLACE, w, + UINT32_C(0xdeadbeef), XCB_ATOM_STRING, 8, + 0, NULL)); + CHECK(error && error->error_code == XCB_ATOM && + error->resource_id == UINT32_C(0xdeadbeef) && + error->major_code == XCB_CHANGE_PROPERTY, + "change property BadAtom metadata"); + free(error); + error = + xcb_request_check(c, xcb_change_property_checked( + c, 99, w, atom, XCB_ATOM_STRING, 8, 0, NULL)); + CHECK(error && error->error_code == XCB_VALUE && error->resource_id == 99 && + error->major_code == XCB_CHANGE_PROPERTY, + "change property mode metadata"); + free(error); + error = xcb_request_check( + c, xcb_change_property_checked(c, XCB_PROP_MODE_REPLACE, w, atom, + XCB_ATOM_STRING, 7, 0, NULL)); + CHECK(error && error->error_code == XCB_VALUE && error->resource_id == 7 && + error->major_code == XCB_CHANGE_PROPERTY, + "change property format metadata"); + free(error); + error = NULL; + owner = xcb_get_selection_owner_reply( + c, xcb_get_selection_owner(c, 0xdeadbeef), &error); + CHECK(!owner && error && error->error_code == XCB_ATOM, + "get selection owner BadAtom"); + free(error); + error = NULL; + list = xcb_list_properties_reply(c, xcb_list_properties(c, 0xdeadbeef), + &error); + CHECK(!list && error && error->error_code == XCB_WINDOW, + "list properties BadWindow"); + free(error); + xcb_list_properties_cookie_t uncheckedList = + xcb_list_properties_unchecked(c, UINT32_C(0xdeadbeef)); + error = (xcb_generic_error_t *) UINTPTR_MAX; + list = xcb_list_properties_reply(c, uncheckedList, &error); + CHECK(!list && !error, "unchecked list error bypasses reply API"); + xcb_generic_error_t *queuedError = + (xcb_generic_error_t *) xcb_poll_for_queued_event(c); + CHECK(queuedError && queuedError->response_type == 0 && + queuedError->error_code == XCB_WINDOW && + queuedError->resource_id == UINT32_C(0xdeadbeef) && + queuedError->full_sequence == uncheckedList.sequence, + "unchecked list error is queued"); + free(queuedError); + xcb_get_selection_owner_cookie_t uncheckedOwner = + xcb_get_selection_owner_unchecked(c, UINT32_C(0xdeadbeef)); + error = (xcb_generic_error_t *) UINTPTR_MAX; + owner = xcb_get_selection_owner_reply(c, uncheckedOwner, &error); + CHECK(!owner && !error, "unchecked selection error bypasses reply API"); + queuedError = (xcb_generic_error_t *) xcb_poll_for_queued_event(c); + CHECK(queuedError && queuedError->error_code == XCB_ATOM && + queuedError->full_sequence == uncheckedOwner.sequence, + "unchecked selection error is queued"); + free(queuedError); + xcb_change_property(c, XCB_PROP_MODE_REPLACE, w, atom, XCB_ATOM_CARDINAL, + 32, 2, words); + p = xcb_get_property_reply( + c, xcb_get_property(c, 0, w, atom, XCB_ATOM_CARDINAL, 0, 8), NULL); + CHECK(p && p->format == 32 && p->value_len == 2 && + !memcmp(xcb_get_property_value(p), words, sizeof(words)), + "32-bit property"); + free(p); + xcb_get_property_reply_t oversizedProperty = { + .format = 32, + .value_len = UINT32_MAX, + }; + CHECK(!xcb_get_property_sizeof(&oversizedProperty) && + !xcb_get_property_value_length(&oversizedProperty) && + !xcb_get_property_value_end(&oversizedProperty).data, + "oversized property layout"); + xcb_delete_property(c, w, atom); + p = xcb_get_property_reply( + c, xcb_get_property(c, 0, w, atom, XCB_GET_PROPERTY_TYPE_ANY, 0, 8), + NULL); + CHECK(p && p->type == XCB_ATOM_NONE, "delete"); + free(p); + xcb_destroy_window(c, w); + xcb_disconnect(c); + puts("test-xcb-property: ok"); + return 0; +} diff --git a/tests/test-xcb-setup.c b/tests/test-xcb-setup.c new file mode 100644 index 00000000..82a66ff3 --- /dev/null +++ b/tests/test-xcb-setup.c @@ -0,0 +1,363 @@ +#include +#include +#include +#include +#include +#include +#include +#include "../compat/xcb-compat-private.h" + +#define CHECK(c, m) \ + do { \ + if (!(c)) { \ + fprintf(stderr, "%s\n", (m)); \ + return 1; \ + } \ + } while (0) + +enum { ID_THREAD_COUNT = 8, IDS_PER_THREAD = 4096 }; + +typedef struct { + xcb_connection_t *connection; + uint32_t ids[IDS_PER_THREAD]; +} IdThread; + +static void *generateIds(void *opaque) +{ + IdThread *thread = opaque; + for (size_t i = 0; i < IDS_PER_THREAD; i++) + thread->ids[i] = xcb_generate_id(thread->connection); + return NULL; +} + +static int compareIds(const void *left, const void *right) +{ + uint32_t a = *(const uint32_t *) left; + uint32_t b = *(const uint32_t *) right; + return (a > b) - (a < b); +} + +static int failedConnectionChecks(void) +{ + CHECK(!setenv("SDL_VIDEODRIVER", "libx11-compat-invalid-driver", 1), + "failed to select invalid video driver"); + + xcb_connection_t *connection = xcb_connect(NULL, NULL); + CHECK(connection && xcb_connection_has_error(connection) == XCB_CONN_ERROR, + "failed connection was not reported"); + CHECK(xcb_generate_id(connection) == UINT32_MAX, + "failed connection generated an XID"); + CHECK(xcb_intern_atom(connection, 0, 1, "A").sequence == 0, + "failed atom request returned a sequence"); + xcb_get_geometry_cookie_t geometry = xcb_get_geometry(connection, XCB_NONE); + CHECK(geometry.sequence == 0, + "failed geometry request returned a sequence"); + xcb_generic_error_t *error = (xcb_generic_error_t *) (uintptr_t) 1; + CHECK(!xcb_get_geometry_reply(connection, geometry, &error) && !error, + "failed geometry request returned pending state"); + CHECK(xcb_create_window_checked( + connection, XCB_COPY_FROM_PARENT, 1, XCB_NONE, 0, 0, 1, 1, 0, + XCB_WINDOW_CLASS_COPY_FROM_PARENT, XCB_COPY_FROM_PARENT, 0, NULL) + .sequence == 0, + "failed window request returned a sequence"); + CHECK(xcb_change_property_checked(connection, XCB_PROP_MODE_REPLACE, 1, 1, + XCB_ATOM_STRING, 8, 0, NULL) + .sequence == 0, + "failed property request returned a sequence"); + CHECK( + xcb_create_pixmap_checked(connection, 1, 1, XCB_NONE, 1, 1).sequence == + 0, + "failed pixmap request returned a sequence"); + CHECK(xcb_create_gc_checked(connection, 1, XCB_NONE, 0, NULL).sequence == 0, + "failed GC request returned a sequence"); + CHECK(xcb_poly_point_checked(connection, XCB_COORD_MODE_ORIGIN, XCB_NONE, 1, + 0, NULL) + .sequence == 0, + "failed drawing request returned a sequence"); + CHECK(xcb_get_image(connection, XCB_IMAGE_FORMAT_Z_PIXMAP, XCB_NONE, 0, 0, + 1, 1, UINT32_MAX) + .sequence == 0, + "failed image request returned a sequence"); + CHECK(!xcb_request_check(connection, (xcb_void_cookie_t) {.sequence = 0}), + "failed connection stored a checked request"); + CHECK(xcb_connection_has_error(connection) == XCB_CONN_ERROR, + "failed connection error changed"); + xcb_disconnect(connection); + return 0; +} + +/* SDL caches the video-driver environment when it first initializes, so an + * invalid driver selected in this process would also break every connection + * attempted after it. Drive the failed-connection checks from a child so the + * poisoned environment dies with it. + */ +static int checkFailedConnection(void) +{ + pid_t child = fork(); + CHECK(child >= 0, "fork failed"); + if (!child) + _exit(failedConnectionChecks() ? 1 : 0); + int status = 0; + CHECK(waitpid(child, &status, 0) == child, "failed-connection wait failed"); + CHECK(WIFEXITED(status) && !WEXITSTATUS(status), + "failed-connection child failed"); + return 0; +} + +int main(void) +{ + CHECK(!checkFailedConnection(), "failed-connection checks failed"); + int screenNumber = -1; + xcb_connection_t *connection = + xcb_connect_to_display_with_auth_info(NULL, NULL, &screenNumber); + CHECK(connection && !xcb_connection_has_error(connection), + "connection failed"); + CHECK(screenNumber == 0 && xcb_get_file_descriptor(connection) == -1, + "connection metadata mismatch"); + CHECK(xcb_get_maximum_request_length(connection) == 65535, + "request limit mismatch"); + const xcb_setup_t *setup = xcb_get_setup(connection); + CHECK(setup && setup->status == 1 && setup->roots_len == 1, + "invalid setup"); + static const char expectedVendor[] = "libx11-compat"; + CHECK(xcb_setup_vendor_length(setup) == (int) sizeof(expectedVendor) - 1 && + !memcmp(xcb_setup_vendor(setup), expectedVendor, + sizeof(expectedVendor) - 1), + "invalid setup vendor"); + xcb_generic_iterator_t vendorEnd = xcb_setup_vendor_end(setup); + CHECK(vendorEnd.data == + xcb_setup_vendor(setup) + xcb_setup_vendor_length(setup) && + vendorEnd.index == + (int) (sizeof(*setup) + xcb_setup_vendor_length(setup)), + "invalid setup vendor end"); + CHECK(setup->resource_id_base && setup->resource_id_mask && + !(setup->resource_id_base & setup->resource_id_mask) && + (setup->resource_id_base | setup->resource_id_mask) <= 0x1fffffff, + "invalid XID range"); + static const xcb_format_t expectedFormats[] = { + {.depth = 1, .bits_per_pixel = 1, .scanline_pad = 32}, + {.depth = 16, .bits_per_pixel = 16, .scanline_pad = 32}, + {.depth = 24, .bits_per_pixel = 32, .scanline_pad = 32}, + {.depth = 32, .bits_per_pixel = 32, .scanline_pad = 32}, + }; + CHECK(xcb_setup_pixmap_formats_length(setup) == + (int) (sizeof(expectedFormats) / sizeof(expectedFormats[0])), + "invalid pixmap format count"); + xcb_format_iterator_t formats = xcb_setup_pixmap_formats_iterator(setup); + CHECK(formats.data == xcb_setup_pixmap_formats(setup), + "pixmap format accessor mismatch"); + for (size_t i = 0; i < sizeof(expectedFormats) / sizeof(expectedFormats[0]); + i++) { + CHECK(formats.rem > 0 && + formats.data->depth == expectedFormats[i].depth && + formats.data->bits_per_pixel == + expectedFormats[i].bits_per_pixel && + formats.data->scanline_pad == expectedFormats[i].scanline_pad, + "invalid pixmap format"); + xcb_format_next(&formats); + } + CHECK(!formats.rem && xcb_format_end(formats).data == formats.data, + "pixmap format iterator termination failed"); + /* The setup advertises the depths a client may ask for, so every one of + * them has to be creatable and anything else has to be refused. Without + * this the advertised set and the set src/pixmap.c accepts could drift. + */ + xcb_screen_t *formatScreen = xcb_setup_roots_iterator(setup).data; + for (xcb_format_iterator_t f = xcb_setup_pixmap_formats_iterator(setup); + f.rem > 0; xcb_format_next(&f)) { + xcb_pixmap_t pixmap = xcb_generate_id(connection); + CHECK(!xcb_request_check( + connection, + xcb_create_pixmap_checked(connection, f.data->depth, pixmap, + formatScreen->root, 2, 2)), + "advertised pixmap depth was refused"); + xcb_free_pixmap(connection, pixmap); + } + xcb_generic_error_t *depthError = xcb_request_check( + connection, + xcb_create_pixmap_checked(connection, 7, xcb_generate_id(connection), + formatScreen->root, 2, 2)); + CHECK(depthError && depthError->error_code == XCB_VALUE, + "unadvertised pixmap depth was accepted"); + free(depthError); + + xcb_screen_iterator_t screens = xcb_setup_roots_iterator(setup); + CHECK(screens.rem == 1 && screens.data && screens.data->root, + "invalid screen"); + CHECK(screens.data->width_in_pixels && screens.data->height_in_pixels, + "invalid dimensions"); + xcb_depth_iterator_t depths = + xcb_screen_allowed_depths_iterator(screens.data); + CHECK(depths.rem == 1 && depths.data->depth == screens.data->root_depth, + "invalid depth"); + xcb_visualtype_iterator_t visuals = xcb_depth_visuals_iterator(depths.data); + CHECK(visuals.data == xcb_depth_visuals(depths.data), + "visual accessor mismatch"); + CHECK(visuals.rem == 1 && + visuals.data->visual_id == screens.data->root_visual, + "invalid visual"); + xcb_generic_iterator_t visualEnd = xcb_visualtype_end(visuals); + xcb_generic_iterator_t depthEnd = xcb_depth_end(depths); + xcb_generic_iterator_t screenEnd = xcb_screen_end(screens); + CHECK( + !visualEnd.rem && visualEnd.data == xcb_depth_visuals(depths.data) + 1, + "visual end mismatch"); + CHECK(!depthEnd.rem && depthEnd.data == visualEnd.data, + "depth end mismatch"); + CHECK(!screenEnd.rem && screenEnd.data == depthEnd.data, + "screen end mismatch"); + CHECK(xcb_depth_sizeof(depths.data) == + (int) (sizeof(*depths.data) + sizeof(*visuals.data)) && + xcb_screen_sizeof(screens.data) == + (int) ((char *) screenEnd.data - (char *) screens.data) && + xcb_setup_sizeof(setup) == (int) (8 + setup->length * 4u), + "setup record size mismatch"); + xcb_visualtype_next(&visuals); + xcb_depth_next(&depths); + xcb_screen_next(&screens); + CHECK(!visuals.rem && !depths.rem && !screens.rem && + visuals.data == visualEnd.data && depths.data == depthEnd.data && + screens.data == screenEnd.data, + "iterator termination failed"); + struct { + xcb_screen_t screen; + xcb_depth_t firstDepth; + xcb_visualtype_t firstVisuals[2]; + xcb_depth_t secondDepth; + xcb_visualtype_t secondVisual; + } synthetic = { + .screen.allowed_depths_len = 2, + .firstDepth.visuals_len = 2, + .secondDepth.visuals_len = 1, + }; + CHECK(xcb_depth_sizeof(&synthetic.firstDepth) == + (int) (sizeof(synthetic.firstDepth) + + sizeof(synthetic.firstVisuals)) && + xcb_screen_sizeof(&synthetic.screen) == (int) sizeof(synthetic), + "multi-depth record size mismatch"); + xcb_screen_iterator_t syntheticScreen = { + .data = &synthetic.screen, + .rem = 1, + .index = 0, + }; + xcb_screen_next(&syntheticScreen); + CHECK(!syntheticScreen.rem && + syntheticScreen.data == + (xcb_screen_t *) ((char *) &synthetic + sizeof(synthetic)), + "multi-depth screen traversal mismatch"); + CHECK(!xcb_depth_sizeof(NULL) && !xcb_screen_sizeof(NULL) && + !xcb_setup_sizeof(NULL), + "NULL record size mismatch"); + uint32_t first = xcb_generate_id(connection), + second = xcb_generate_id(connection); + CHECK(first != UINT32_MAX && second > first, "XID allocation failed"); + IdThread idThreads[ID_THREAD_COUNT] = {{0}}; + pthread_t workers[ID_THREAD_COUNT]; + for (size_t i = 0; i < ID_THREAD_COUNT; i++) { + idThreads[i].connection = connection; + CHECK(!pthread_create(&workers[i], NULL, generateIds, &idThreads[i]), + "XID worker creation failed"); + } + uint32_t *generated = + malloc(sizeof(*generated) * ID_THREAD_COUNT * IDS_PER_THREAD); + CHECK(generated, "XID result allocation failed"); + for (size_t i = 0; i < ID_THREAD_COUNT; i++) { + CHECK(!pthread_join(workers[i], NULL), "XID worker join failed"); + memcpy(generated + i * IDS_PER_THREAD, idThreads[i].ids, + sizeof(idThreads[i].ids)); + } + qsort(generated, ID_THREAD_COUNT * IDS_PER_THREAD, sizeof(*generated), + compareIds); + for (size_t i = 0; i < ID_THREAD_COUNT * IDS_PER_THREAD; i++) { + CHECK(generated[i] != UINT32_MAX, + "concurrent XID allocation returned an error"); + CHECK(generated[i] >= setup->resource_id_base && + ((generated[i] - setup->resource_id_base) & + ~setup->resource_id_mask) == 0, + "generated XID is outside the advertised range"); + CHECK(i == 0 || generated[i] != generated[i - 1], + "concurrent XID allocation returned a duplicate"); + } + free(generated); + xcbCompatSetNextSequence(connection, UINT32_MAX); + CHECK(xcbCompatNextSequence(connection) == UINT32_MAX, + "wrap start mismatch"); + CHECK(xcbCompatNextSequence(connection) == (uint64_t) UINT32_MAX + 1, + "sequence wrap failed"); + xcbCompatSetNextSequence(connection, (uint64_t) UINT32_MAX + 1); + xcb_void_cookie_t wrappedCookie = xcbCompatVoidCookie( + connection, XCB_WINDOW, UINT32_C(0xdeadbeef), XCB_DESTROY_WINDOW, 1); + CHECK(wrappedCookie.sequence == 0, "request cookie did not wrap to zero"); + xcb_generic_error_t *wrappedError = + xcb_request_check(connection, wrappedCookie); + CHECK(wrappedError && wrappedError->error_code == XCB_WINDOW && + wrappedError->full_sequence == 0, + "zero sequence cookie was rejected after wrap"); + free(wrappedError); + xcb_generic_error_t *stored = calloc(1, sizeof(*stored)); + CHECK(stored, "allocation failed"); + stored->error_code = XCB_WINDOW; + xcbCompatStorePending(connection, 41, NULL, stored); + xcb_void_cookie_t cookie = {.sequence = 41}; + xcb_generic_error_t *received = xcb_request_check(connection, cookie); + CHECK(received && received->error_code == XCB_WINDOW, + "checked error missing"); + free(received); + CHECK(!xcb_request_check(connection, cookie), "error returned twice"); + xcb_generic_error_t *atomError = calloc(1, sizeof(*atomError)); + CHECK(atomError, "atom error allocation failed"); + atomError->error_code = XCB_ATOM; + xcbCompatStorePending(connection, 42, NULL, atomError); + cookie.sequence = 42; + received = xcb_request_check(connection, cookie); + CHECK(received && received->error_code == XCB_ATOM, + "BadAtom was not returned"); + free(received); + void *reply = calloc(1, 8); + CHECK(reply, "reply allocation failed"); + xcbCompatStorePending(connection, 43, reply, NULL); + CHECK(xcbCompatTakeReply(connection, 43, NULL) == reply, + "stored reply was not returned"); + free(reply); + xcbCompatStorePending(connection, 44, calloc(1, 8), NULL); + xcb_discard_reply(connection, 44); + xcb_discard_reply(connection, 44); + uint64_t wide = ((uint64_t) 1 << 32) + 45; + xcbCompatStorePending(connection, wide, calloc(1, 8), NULL); + xcb_discard_reply64(connection, wide); + wide = ((uint64_t) 1 << 32) + 46; + xcbCompatStorePending(connection, wide, calloc(1, 8), NULL); + xcbCompatSetNextSequence(connection, wide + 1); + xcb_discard_reply(connection, 46); + CHECK(!xcbCompatTakeReply(connection, wide, NULL), + "32-bit discard did not widen across wrap"); + + uint64_t *replacement = malloc(sizeof(*replacement)); + CHECK(replacement, "replacement allocation failed"); + *replacement = 2; + xcbCompatStorePending(connection, 100, calloc(1, sizeof(*replacement)), + NULL); + xcbCompatStorePending(connection, 100, replacement, NULL); + replacement = xcbCompatTakeReply(connection, 100, NULL); + CHECK(replacement && *replacement == 2, "pending replacement mismatch"); + free(replacement); + + enum { PENDING_STRESS_COUNT = 8192 }; + for (uint64_t i = 0; i < PENDING_STRESS_COUNT; i++) { + uint64_t *value = malloc(sizeof(*value)); + CHECK(value, "pending stress allocation failed"); + *value = i; + xcbCompatStorePending(connection, 1000 + i, value, NULL); + } + for (uint64_t i = PENDING_STRESS_COUNT; i > 0; i--) { + uint64_t expected = i - 1; + uint64_t *value = xcbCompatTakeReply(connection, 1000 + expected, NULL); + CHECK(value && *value == expected, "pending stress lookup mismatch"); + free(value); + } + xcb_disconnect(connection); + CHECK(xcb_connection_has_error(NULL) == XCB_CONN_ERROR, + "NULL error mismatch"); + puts("test-xcb-setup: ok"); + return 0; +} diff --git a/tests/test-xcb-window.c b/tests/test-xcb-window.c new file mode 100644 index 00000000..58ac9205 --- /dev/null +++ b/tests/test-xcb-window.c @@ -0,0 +1,453 @@ +#include +#include +#include +#include +extern Window libx11CompatCreateWindowWithId(Display *, + Window, + Window, + int, + int, + unsigned int, + unsigned int, + unsigned int, + int, + unsigned int, + Visual *, + unsigned long, + XSetWindowAttributes *); +extern Bool isXidAllocated(XID); +extern Bool reserveXidResource(XID); +extern void xcbCompatFailNextReplyAllocationForTest(void); +#define CHECK(c, m) \ + do { \ + if (!(c)) { \ + fprintf(stderr, "%s\n", m); \ + return 1; \ + } \ + } while (0) +int main(void) +{ + xcb_connection_t *c = xcb_connect(NULL, NULL); + CHECK(c && !xcb_connection_has_error(c), "connect"); + xcb_screen_t *s = xcb_setup_roots_iterator(xcb_get_setup(c)).data; + CHECK(!xcb_query_tree_children(NULL) && + !xcb_query_tree_children_length(NULL) && + !xcb_query_tree_children_end(NULL).data && + !xcb_query_tree_sizeof(NULL), + "NULL query-tree accessor"); + uint32_t initialInvalidMaskValue = 0; + xcb_void_cookie_t initialUnchecked = xcb_change_window_attributes( + c, s->root, UINT32_C(1) << 31, &initialInvalidMaskValue); + CHECK(!xcb_request_check( + c, xcb_change_window_attributes_checked(c, s->root, 0, NULL)), + "initialize pending table"); + xcb_generic_error_t *initialQueuedError = + (xcb_generic_error_t *) xcb_poll_for_queued_event(c); + CHECK(initialQueuedError && initialQueuedError->error_code == XCB_VALUE && + initialQueuedError->full_sequence == initialUnchecked.sequence, + "pending table growth preserves queued errors"); + free(initialQueuedError); + xcb_window_t w = xcb_generate_id(c); + uint32_t values[] = {0x112233, XCB_EVENT_MASK_STRUCTURE_NOTIFY}; + xcb_void_cookie_t bad; + xcb_generic_error_t *e; + xcb_void_cookie_t created = xcb_create_window_checked( + c, XCB_COPY_FROM_PARENT, w, s->root, 7, 9, 80, 60, 0, + XCB_WINDOW_CLASS_INPUT_OUTPUT, s->root_visual, + XCB_CW_BACK_PIXEL | XCB_CW_EVENT_MASK, values); + CHECK(!xcb_request_check(c, created), "checked create"); + xcb_get_geometry_reply_t *g = + xcb_get_geometry_reply(c, xcb_get_geometry_unchecked(c, w), NULL); + CHECK(g && g->x == 7 && g->y == 9 && g->width == 80 && g->height == 60, + "geometry"); + free(g); + xcb_get_window_attributes_reply_t *a = xcb_get_window_attributes_reply( + c, xcb_get_window_attributes_unchecked(c, w), NULL); + CHECK(a && a->your_event_mask == XCB_EVENT_MASK_STRUCTURE_NOTIFY, + "attributes"); + free(a); + xcb_query_tree_reply_t *t = + xcb_query_tree_reply(c, xcb_query_tree_unchecked(c, w), NULL); + CHECK(t && t->parent == s->root && xcb_query_tree_children_length(t) == 0, + "tree"); + xcb_generic_iterator_t childrenEnd = xcb_query_tree_children_end(t); + CHECK(xcb_query_tree_sizeof(t) == (int) sizeof(*t) && + childrenEnd.data == xcb_query_tree_children(t) && + childrenEnd.index == xcb_query_tree_sizeof(t), + "tree layout"); + free(t); + xcb_window_t uncheckedWindow = xcb_generate_id(c); + xcb_void_cookie_t uncheckedCreate = xcb_create_window( + c, XCB_COPY_FROM_PARENT, uncheckedWindow, s->root, 2, 3, 11, 12, 0, + XCB_WINDOW_CLASS_INPUT_OUTPUT, XCB_COPY_FROM_PARENT, 0, NULL); + g = xcb_get_geometry_reply(c, xcb_get_geometry(c, uncheckedWindow), NULL); + CHECK(uncheckedCreate.sequence && g && g->x == 2 && g->y == 3 && + g->width == 11 && g->height == 12, + "unchecked create window"); + free(g); + xcb_destroy_window(c, uncheckedWindow); + xcb_generic_error_t *queryError = NULL; + a = xcb_get_window_attributes_reply( + c, xcb_get_window_attributes(c, UINT32_C(0xdeadbeef)), &queryError); + CHECK(!a && queryError && queryError->error_code == XCB_WINDOW && + queryError->resource_id == UINT32_C(0xdeadbeef) && + queryError->major_code == XCB_GET_WINDOW_ATTRIBUTES, + "attributes reply BadWindow"); + free(queryError); + xcb_get_window_attributes_cookie_t uncheckedAttributes = + xcb_get_window_attributes_unchecked(c, UINT32_C(0xdeadbeef)); + queryError = (xcb_generic_error_t *) UINTPTR_MAX; + a = xcb_get_window_attributes_reply(c, uncheckedAttributes, &queryError); + CHECK(!a && !queryError, "unchecked attributes bypass reply error"); + queryError = (xcb_generic_error_t *) xcb_poll_for_queued_event(c); + CHECK(queryError && queryError->error_code == XCB_WINDOW && + queryError->full_sequence == uncheckedAttributes.sequence, + "unchecked attributes queue error"); + free(queryError); + xcb_get_geometry_cookie_t uncheckedGeometry = + xcb_get_geometry_unchecked(c, UINT32_C(0xdeadbeef)); + queryError = (xcb_generic_error_t *) UINTPTR_MAX; + g = xcb_get_geometry_reply(c, uncheckedGeometry, &queryError); + CHECK(!g && !queryError, "unchecked geometry bypasses reply error"); + xcb_generic_error_t *uncheckedGeometryError = + (xcb_generic_error_t *) xcb_poll_for_queued_event(c); + CHECK( + uncheckedGeometryError && + uncheckedGeometryError->error_code == XCB_DRAWABLE && + uncheckedGeometryError->full_sequence == uncheckedGeometry.sequence, + "unchecked geometry queues error"); + free(uncheckedGeometryError); + queryError = NULL; + g = xcb_get_geometry_reply(c, xcb_get_geometry(c, UINT32_C(0xdeadbeef)), + &queryError); + CHECK(!g && queryError && queryError->error_code == XCB_DRAWABLE && + queryError->resource_id == UINT32_C(0xdeadbeef) && + queryError->major_code == XCB_GET_GEOMETRY, + "geometry reply BadDrawable"); + free(queryError); + queryError = NULL; + t = xcb_query_tree_reply(c, xcb_query_tree(c, UINT32_C(0xdeadbeef)), + &queryError); + CHECK(!t && queryError && queryError->error_code == XCB_WINDOW && + queryError->resource_id == UINT32_C(0xdeadbeef) && + queryError->major_code == XCB_QUERY_TREE, + "query-tree reply BadWindow"); + free(queryError); + xcb_query_tree_cookie_t uncheckedTree = + xcb_query_tree_unchecked(c, UINT32_C(0xdeadbeef)); + queryError = (xcb_generic_error_t *) UINTPTR_MAX; + t = xcb_query_tree_reply(c, uncheckedTree, &queryError); + CHECK(!t && !queryError, "unchecked query tree bypasses reply error"); + queryError = (xcb_generic_error_t *) xcb_poll_for_queued_event(c); + CHECK(queryError && queryError->error_code == XCB_WINDOW && + queryError->full_sequence == uncheckedTree.sequence, + "unchecked query tree queues error"); + free(queryError); + + xcb_window_t retryWindow = xcb_generate_id(c); + bad = xcb_create_window_checked(c, 0, retryWindow, s->root, 0, 0, 4, 4, 1, + XCB_WINDOW_CLASS_INPUT_ONLY, + XCB_COPY_FROM_PARENT, 0, NULL); + e = xcb_request_check(c, bad); + CHECK(e && e->error_code == XCB_MATCH, "InputOnly border BadMatch"); + free(e); + CHECK(!xcb_request_check( + c, xcb_create_window_checked(c, 0, retryWindow, s->root, 0, 0, 4, + 4, 0, XCB_WINDOW_CLASS_INPUT_ONLY, + XCB_COPY_FROM_PARENT, 0, NULL)), + "retry caller ID after failed create"); + uint32_t inputOnlyBackground = 0; + e = xcb_request_check( + c, xcb_change_window_attributes_checked( + c, retryWindow, XCB_CW_BACK_PIXEL, &inputOnlyBackground)); + CHECK(e && e->error_code == XCB_MATCH && + e->resource_id == XCB_CW_BACK_PIXEL && + e->major_code == XCB_CHANGE_WINDOW_ATTRIBUTES, + "InputOnly background BadMatch"); + free(e); + xcb_destroy_window(c, retryWindow); + + xcb_window_t validationWindow = xcb_generate_id(c); + bad = xcb_create_window_checked( + c, XCB_COPY_FROM_PARENT, validationWindow, s->root, 0, 0, 0, 4, 0, + XCB_WINDOW_CLASS_INPUT_OUTPUT, XCB_COPY_FROM_PARENT, 0, NULL); + e = xcb_request_check(c, bad); + CHECK(e && e->error_code == XCB_VALUE && e->resource_id == 0, + "create-window zero width BadValue"); + free(e); + uint8_t invalidDepth = s->root_depth == 1 ? 16 : 1; + bad = xcb_create_window_checked( + c, invalidDepth, validationWindow, s->root, 0, 0, 4, 4, 0, + XCB_WINDOW_CLASS_INPUT_OUTPUT, XCB_COPY_FROM_PARENT, 0, NULL); + e = xcb_request_check(c, bad); + CHECK(e && e->error_code == XCB_MATCH && e->resource_id == invalidDepth, + "create-window depth BadMatch"); + free(e); + bad = xcb_create_window_checked( + c, XCB_COPY_FROM_PARENT, validationWindow, s->root, 0, 0, 4, 4, 0, + XCB_WINDOW_CLASS_INPUT_OUTPUT, UINT32_C(0xdeadbeef), 0, NULL); + e = xcb_request_check(c, bad); + CHECK(e && e->error_code == XCB_MATCH && + e->resource_id == UINT32_C(0xdeadbeef), + "create-window visual BadMatch"); + free(e); + bad = xcb_create_window_checked(c, s->root_depth, validationWindow, s->root, + 0, 0, 4, 4, 0, XCB_WINDOW_CLASS_INPUT_ONLY, + XCB_COPY_FROM_PARENT, 0, NULL); + e = xcb_request_check(c, bad); + CHECK(e && e->error_code == XCB_MATCH && e->resource_id == s->root_depth, + "InputOnly depth BadMatch"); + free(e); + CHECK(!xcb_request_check( + c, xcb_create_window_checked(c, XCB_COPY_FROM_PARENT, + validationWindow, s->root, 0, 0, 4, + 4, 0, XCB_WINDOW_CLASS_INPUT_OUTPUT, + XCB_COPY_FROM_PARENT, 0, NULL)), + "reuse window ID after structural errors"); + xcb_destroy_window(c, validationWindow); + bad = xcb_create_window_checked(c, XCB_COPY_FROM_PARENT, xcb_generate_id(c), + UINT32_C(0xdeadbeef), 0, 0, 4, 4, 0, + XCB_WINDOW_CLASS_COPY_FROM_PARENT, + XCB_COPY_FROM_PARENT, 0, NULL); + e = xcb_request_check(c, bad); + CHECK(e && e->error_code == XCB_WINDOW && + e->resource_id == UINT32_C(0xdeadbeef), + "inherited class invalid parent BadWindow"); + free(e); + + uint32_t invalidMaskValue = 0; + xcb_window_t invalidMaskWindow = xcb_generate_id(c); + e = xcb_request_check( + c, xcb_create_window_checked(c, 0, invalidMaskWindow, s->root, 0, 0, 4, + 4, 0, XCB_WINDOW_CLASS_INPUT_OUTPUT, + XCB_COPY_FROM_PARENT, UINT32_C(1) << 31, + &invalidMaskValue)); + CHECK(e && e->error_code == XCB_VALUE && e->major_code == XCB_CREATE_WINDOW, + "create-window unknown mask"); + free(e); + CHECK(!xcb_request_check(c, xcb_create_window_checked( + c, 0, invalidMaskWindow, s->root, 0, 0, 4, + 4, 0, XCB_WINDOW_CLASS_INPUT_OUTPUT, + XCB_COPY_FROM_PARENT, 0, NULL)), + "reuse window ID after unknown mask"); + e = xcb_request_check( + c, xcb_change_window_attributes_checked( + c, invalidMaskWindow, UINT32_C(1) << 31, &invalidMaskValue)); + CHECK(e && e->error_code == XCB_VALUE && + e->resource_id == (UINT32_C(1) << 31) && + e->major_code == XCB_CHANGE_WINDOW_ATTRIBUTES, + "change-attributes unknown mask"); + free(e); + uint32_t invalidGravity = 99; + e = xcb_request_check( + c, xcb_change_window_attributes_checked( + c, invalidMaskWindow, XCB_CW_BIT_GRAVITY, &invalidGravity)); + CHECK(e && e->error_code == XCB_VALUE && e->resource_id == invalidGravity && + e->major_code == XCB_CHANGE_WINDOW_ATTRIBUTES, + "change-attributes gravity BadValue"); + free(e); + uint32_t invalidCursor = s->root; + e = xcb_request_check( + c, xcb_change_window_attributes_checked(c, invalidMaskWindow, + XCB_CW_CURSOR, &invalidCursor)); + CHECK(e && e->error_code == XCB_CURSOR && e->resource_id == s->root && + e->major_code == XCB_CHANGE_WINDOW_ATTRIBUTES, + "change-attributes BadCursor"); + free(e); + e = xcb_request_check( + c, xcb_configure_window_checked(c, invalidMaskWindow, UINT16_C(1) << 15, + &invalidMaskValue)); + CHECK(e && e->error_code == XCB_VALUE && + e->resource_id == (UINT16_C(1) << 15) && + e->major_code == XCB_CONFIGURE_WINDOW, + "configure-window unknown mask"); + free(e); + xcb_void_cookie_t unchecked = xcb_change_window_attributes( + c, invalidMaskWindow, UINT32_C(1) << 31, &invalidMaskValue); + CHECK(!xcb_request_check(c, unchecked), + "unchecked request is not request-checkable"); + xcb_generic_error_t *queuedError = + (xcb_generic_error_t *) xcb_poll_for_queued_event(c); + CHECK(queuedError && queuedError->response_type == 0 && + queuedError->error_code == XCB_VALUE && + queuedError->resource_id == (UINT32_C(1) << 31) && + queuedError->major_code == XCB_CHANGE_WINDOW_ATTRIBUTES && + queuedError->full_sequence == unchecked.sequence, + "unchecked error is queued as an event"); + free(queuedError); + uint32_t invalidSibling[] = {UINT32_C(0xdeadbeef)}; + bad = xcb_configure_window_checked(c, w, XCB_CONFIG_WINDOW_SIBLING, + invalidSibling); + e = xcb_request_check(c, bad); + CHECK(e && e->error_code == XCB_WINDOW && + e->resource_id == UINT32_C(0xdeadbeef) && + e->major_code == XCB_CONFIGURE_WINDOW, + "configure sibling BadWindow metadata"); + free(e); + xcb_destroy_window(c, invalidMaskWindow); + + Display *helperDisplay = XOpenDisplay(NULL); + CHECK(helperDisplay, "helper display"); + xcb_window_t helperWindow = xcb_generate_id(c); + CHECK(libx11CompatCreateWindowWithId( + helperDisplay, helperWindow, DefaultRootWindow(helperDisplay), 0, + 0, 4, 4, 1, 0, InputOnly, CopyFromParent, 0, NULL) == None, + "helper InputOnly border failure"); + CHECK(!isXidAllocated(helperWindow), "failed helper retained requested ID"); + CHECK(reserveXidResource(helperWindow), "reserve released helper ID"); + CHECK(libx11CompatCreateWindowWithId(helperDisplay, helperWindow, + DefaultRootWindow(helperDisplay), 0, 0, + 4, 4, 0, 0, InputOnly, CopyFromParent, + 0, NULL) == helperWindow, + "reuse released helper ID"); + XDestroyWindow(helperDisplay, helperWindow); + XCloseDisplay(helperDisplay); + bad = xcb_create_window_checked(c, 24, xcb_generate_id(c), 0xdeadbeef, 0, 0, + 1, 1, 0, XCB_WINDOW_CLASS_INPUT_OUTPUT, + s->root_visual, 0, NULL); + e = xcb_request_check(c, bad); + CHECK(e && e->error_code == XCB_WINDOW, "BadWindow"); + free(e); + bad = xcb_create_window_checked(c, 24, 0x0f000000, s->root, 0, 0, 1, 1, 0, + XCB_WINDOW_CLASS_INPUT_OUTPUT, + s->root_visual, 0, NULL); + e = xcb_request_check(c, bad); + CHECK(e && e->error_code == XCB_ID_CHOICE, "BadIDChoice"); + free(e); + uint32_t changed[] = {XCB_GRAVITY_NORTH_EAST, XCB_GRAVITY_SOUTH_WEST, 1, + XCB_EVENT_MASK_PROPERTY_CHANGE}; + uint32_t changeMask = XCB_CW_BIT_GRAVITY | XCB_CW_WIN_GRAVITY | + XCB_CW_OVERRIDE_REDIRECT | XCB_CW_EVENT_MASK; + CHECK(!xcb_request_check(c, xcb_change_window_attributes_checked( + c, w, changeMask, changed)), + "change attributes"); + a = xcb_get_window_attributes_reply(c, xcb_get_window_attributes(c, w), + NULL); + CHECK(a && a->bit_gravity == XCB_GRAVITY_NORTH_EAST && + a->win_gravity == XCB_GRAVITY_SOUTH_WEST && + a->override_redirect && + a->your_event_mask == XCB_EVENT_MASK_PROPERTY_CHANGE, + "changed attributes mismatch"); + free(a); + + uint32_t configured[] = {(uint32_t) (int32_t) -3, 11, 64, 48, 2}; + uint16_t configureMask = + XCB_CONFIG_WINDOW_X | XCB_CONFIG_WINDOW_Y | XCB_CONFIG_WINDOW_WIDTH | + XCB_CONFIG_WINDOW_HEIGHT | XCB_CONFIG_WINDOW_BORDER_WIDTH; + CHECK(!xcb_request_check( + c, xcb_configure_window_checked(c, w, configureMask, configured)), + "configure"); + g = xcb_get_geometry_reply(c, xcb_get_geometry(c, w), NULL); + CHECK(g && g->x == -3 && g->y == 11 && g->width == 64 && g->height == 48 && + g->border_width == 2, + "configured geometry mismatch"); + free(g); + + xcb_window_t container = xcb_generate_id(c); + CHECK( + !xcb_request_check( + c, xcb_create_window_checked( + c, XCB_COPY_FROM_PARENT, container, s->root, 0, 0, 100, 100, + 0, XCB_WINDOW_CLASS_INPUT_OUTPUT, s->root_visual, 0, NULL)), + "container create"); + CHECK(!xcb_request_check( + c, xcb_reparent_window_checked(c, w, container, 4, 5)), + "reparent"); + t = xcb_query_tree_reply(c, xcb_query_tree(c, w), NULL); + CHECK(t && t->parent == container, "reparent parent mismatch"); + free(t); + + xcb_window_t sibling = xcb_generate_id(c); + CHECK( + !xcb_request_check( + c, xcb_create_window_checked( + c, XCB_COPY_FROM_PARENT, sibling, container, 0, 0, 10, 10, 0, + XCB_WINDOW_CLASS_INPUT_OUTPUT, s->root_visual, 0, NULL)), + "sibling create"); + uint32_t siblingOnly[] = {sibling}; + e = xcb_request_check(c, xcb_configure_window_checked( + c, w, XCB_CONFIG_WINDOW_SIBLING, siblingOnly)); + CHECK(e && e->error_code == XCB_MATCH && e->resource_id == w && + e->major_code == XCB_CONFIGURE_WINDOW, + "sibling without stack-mode BadMatch"); + free(e); + uint32_t selfStack[] = {w, XCB_STACK_MODE_ABOVE}; + e = xcb_request_check( + c, xcb_configure_window_checked( + c, w, XCB_CONFIG_WINDOW_SIBLING | XCB_CONFIG_WINDOW_STACK_MODE, + selfStack)); + CHECK(e && e->error_code == XCB_MATCH && e->resource_id == w, + "self sibling BadMatch"); + free(e); + uint32_t conditionalStack[] = {sibling, XCB_STACK_MODE_TOP_IF}; + CHECK(!xcb_request_check( + c, xcb_configure_window_checked( + c, w, + XCB_CONFIG_WINDOW_SIBLING | XCB_CONFIG_WINDOW_STACK_MODE, + conditionalStack)), + "conditional sibling stack configure"); + uint32_t stack[] = {sibling, XCB_STACK_MODE_ABOVE}; + CHECK(!xcb_request_check( + c, xcb_configure_window_checked( + c, w, + XCB_CONFIG_WINDOW_SIBLING | XCB_CONFIG_WINDOW_STACK_MODE, + stack)), + "stack configure"); + t = xcb_query_tree_reply(c, xcb_query_tree(c, container), NULL); + CHECK(t && xcb_query_tree_children_length(t) == 2, + "stacking changed child membership"); + CHECK(xcb_query_tree_children(t)[1] == w, + "Above stacking did not move window to top"); + free(t); + e = xcb_request_check(c, xcb_reparent_window_checked(c, w, w, 0, 0)); + CHECK(e && e->error_code == XCB_MATCH && e->resource_id == w && + e->major_code == XCB_REPARENT_WINDOW, + "self reparent BadMatch"); + free(e); + e = xcb_request_check(c, + xcb_reparent_window_checked(c, container, w, 0, 0)); + CHECK(e && e->error_code == XCB_MATCH && e->resource_id == container && + e->major_code == XCB_REPARENT_WINDOW, + "cyclic reparent BadMatch"); + free(e); + + bad = xcb_configure_window_checked(c, 0xdeadbeef, XCB_CONFIG_WINDOW_X, + configured); + e = xcb_request_check(c, bad); + CHECK(e && e->error_code == XCB_WINDOW, "configure BadWindow"); + free(e); + uint32_t invalidSize[] = {0}; + bad = xcb_configure_window_checked(c, w, XCB_CONFIG_WINDOW_WIDTH, + invalidSize); + e = xcb_request_check(c, bad); + CHECK(e && e->error_code == XCB_VALUE && e->resource_id == 0, + "configure BadValue"); + free(e); + uint32_t invalidStack[] = {255}; + bad = xcb_configure_window_checked(c, w, XCB_CONFIG_WINDOW_STACK_MODE, + invalidStack); + e = xcb_request_check(c, bad); + CHECK(e && e->error_code == XCB_VALUE && e->resource_id == 255, + "stack mode BadValue"); + free(e); + bad = xcb_change_window_attributes_checked(c, 0xdeadbeef, XCB_CW_EVENT_MASK, + changed); + e = xcb_request_check(c, bad); + CHECK(e && e->error_code == XCB_WINDOW, "attributes BadWindow"); + free(e); + xcb_destroy_window(c, sibling); + xcb_destroy_window(c, w); + xcb_destroy_window(c, container); + xcb_disconnect(c); + + c = xcb_connect(NULL, NULL); + CHECK(c && !xcb_connection_has_error(c), "OOM test connection"); + s = xcb_setup_roots_iterator(xcb_get_setup(c)).data; + xcbCompatFailNextReplyAllocationForTest(); + t = xcb_query_tree_reply(c, xcb_query_tree(c, s->root), NULL); + CHECK(!t && xcb_connection_has_error(c) == XCB_CONN_CLOSED_MEM_INSUFFICIENT, + "reply allocation failure did not close connection"); + xcb_disconnect(c); + puts("test-xcb-window: ok"); + return 0; +} diff --git a/tests/test-xlib-xcb.c b/tests/test-xlib-xcb.c new file mode 100644 index 00000000..993f7e1a --- /dev/null +++ b/tests/test-xlib-xcb.c @@ -0,0 +1,81 @@ +#include +#include +#include +#include +#include + +#define CHECK(c, m) \ + do { \ + if (!(c)) { \ + fprintf(stderr, "%s\n", m); \ + return 1; \ + } \ + } while (0) + +int main(void) +{ + Display *display = XOpenDisplay(NULL); + CHECK(display, "open display"); + xcb_connection_t *connection = XGetXCBConnection(display); + CHECK(connection && connection == XGetXCBConnection(display), + "one wrapper per display"); + Display *otherDisplay = XOpenDisplay(NULL); + CHECK(otherDisplay, "open second display"); + xcb_connection_t *otherConnection = XGetXCBConnection(otherDisplay); + CHECK(otherConnection && otherConnection != connection, + "distinct wrappers for two displays"); + Window window = XCreateSimpleWindow(display, DefaultRootWindow(display), 0, + 0, 16, 16, 0, 0, 0); + Atom property = XInternAtom(display, "X11_XCB_SHARED", False); + const char value[] = "shared"; + XChangeProperty(display, window, property, XA_STRING, 8, PropModeReplace, + (const unsigned char *) value, sizeof(value) - 1); + xcb_get_property_reply_t *reply = xcb_get_property_reply( + connection, + xcb_get_property(connection, 0, window, property, XCB_ATOM_STRING, 0, + sizeof(value)), + NULL); + CHECK(reply && xcb_get_property_value_length(reply) == sizeof(value) - 1 && + !memcmp(xcb_get_property_value(reply), value, sizeof(value) - 1), + "Xlib write visible to XCB"); + free(reply); + XSetEventQueueOwner(display, XCBOwnsEventQueue); + XSetEventQueueOwner(display, XlibOwnsEventQueue); + xcb_disconnect(otherConnection); + XCloseDisplay(otherDisplay); + xcb_disconnect(connection); + XDestroyWindow(display, window); + XCloseDisplay(display); + + display = XOpenDisplay(NULL); + CHECK(display, "reopen display"); + connection = XGetXCBConnection(display); + CHECK(connection, "second wrapper"); + xcb_screen_t *screen = + xcb_setup_roots_iterator(xcb_get_setup(connection)).data; + xcb_gcontext_t gc = xcb_generate_id(connection); + CHECK(!xcb_request_check( + connection, + xcb_create_gc_checked(connection, gc, screen->root, 0, NULL)), + "create shared-display GC"); + xcb_connection_t *survivor = xcb_connect(NULL, NULL); + CHECK(survivor && !xcb_connection_has_error(survivor), + "open close-hook survivor"); + XCloseDisplay(display); + CHECK(xcb_connection_has_error(connection) == XCB_CONN_ERROR, + "display-first close invalidates wrapper"); + xcb_point_t point = {0, 0}; + xcb_generic_error_t *error = xcb_request_check( + survivor, + xcb_poly_point_checked( + survivor, XCB_COORD_MODE_ORIGIN, + xcb_setup_roots_iterator(xcb_get_setup(survivor)).data->root, gc, 1, + &point)); + CHECK(error && error->error_code == XCB_G_CONTEXT, + "display close removes owned GC entries"); + free(error); + xcb_disconnect(connection); + xcb_disconnect(survivor); + puts("test-xlib-xcb: ok"); + return 0; +} diff --git a/tests/xcb-private-symbols.txt b/tests/xcb-private-symbols.txt new file mode 100644 index 00000000..f621780b --- /dev/null +++ b/tests/xcb-private-symbols.txt @@ -0,0 +1,4 @@ +xcbCompatConnectionForDisplay +xcbCompatEventWaiters +xcbCompatFailNextReplyAllocationForTest +xcbCompatSetQueueOwner diff --git a/tests/xcb-symbols.txt b/tests/xcb-symbols.txt new file mode 100644 index 00000000..46f59ac9 --- /dev/null +++ b/tests/xcb-symbols.txt @@ -0,0 +1,145 @@ +# Milestone-1 libxcb-compat ABI used by tests/probe-system-xcb.c. +# Keep sorted; later phases add symbols only with implementation tests. +xcb_change_gc +xcb_change_gc_checked +xcb_change_property +xcb_change_property_checked +xcb_change_window_attributes +xcb_change_window_attributes_checked +xcb_clear_area +xcb_clear_area_checked +xcb_configure_window +xcb_configure_window_checked +xcb_connect +xcb_connect_to_display_with_auth_info +xcb_connection_has_error +xcb_convert_selection +xcb_convert_selection_checked +xcb_copy_area +xcb_copy_area_checked +xcb_copy_gc +xcb_copy_gc_checked +xcb_copy_plane +xcb_copy_plane_checked +xcb_create_gc +xcb_create_gc_checked +xcb_create_pixmap +xcb_create_pixmap_checked +xcb_create_window +xcb_create_window_checked +xcb_delete_property +xcb_delete_property_checked +xcb_depth_end +xcb_depth_next +xcb_depth_sizeof +xcb_depth_visuals +xcb_depth_visuals_iterator +xcb_depth_visuals_length +xcb_destroy_window +xcb_destroy_window_checked +xcb_discard_reply +xcb_discard_reply64 +xcb_disconnect +xcb_flush +xcb_format_end +xcb_format_next +xcb_free_gc +xcb_free_gc_checked +xcb_free_pixmap +xcb_free_pixmap_checked +xcb_generate_id +xcb_get_atom_name +xcb_get_atom_name_name +xcb_get_atom_name_name_end +xcb_get_atom_name_name_length +xcb_get_atom_name_reply +xcb_get_atom_name_sizeof +xcb_get_atom_name_unchecked +xcb_get_file_descriptor +xcb_get_geometry +xcb_get_geometry_reply +xcb_get_geometry_unchecked +xcb_get_image +xcb_get_image_data +xcb_get_image_data_end +xcb_get_image_data_length +xcb_get_image_reply +xcb_get_image_sizeof +xcb_get_image_unchecked +xcb_get_maximum_request_length +xcb_get_property +xcb_get_property_reply +xcb_get_property_sizeof +xcb_get_property_unchecked +xcb_get_property_value +xcb_get_property_value_end +xcb_get_property_value_length +xcb_get_selection_owner +xcb_get_selection_owner_reply +xcb_get_selection_owner_unchecked +xcb_get_setup +xcb_get_window_attributes +xcb_get_window_attributes_reply +xcb_get_window_attributes_unchecked +xcb_image_text_16 +xcb_image_text_16_checked +xcb_image_text_8 +xcb_image_text_8_checked +xcb_intern_atom +xcb_intern_atom_reply +xcb_intern_atom_unchecked +xcb_list_properties +xcb_list_properties_atoms +xcb_list_properties_atoms_end +xcb_list_properties_atoms_length +xcb_list_properties_reply +xcb_list_properties_sizeof +xcb_list_properties_unchecked +xcb_map_window +xcb_map_window_checked +xcb_poll_for_event +xcb_poll_for_queued_event +xcb_poll_for_special_event +xcb_poly_arc +xcb_poly_arc_checked +xcb_poly_line +xcb_poly_line_checked +xcb_poly_point +xcb_poly_point_checked +xcb_poly_rectangle +xcb_poly_rectangle_checked +xcb_poly_segment +xcb_poly_segment_checked +xcb_put_image +xcb_put_image_checked +xcb_query_tree +xcb_query_tree_children +xcb_query_tree_children_end +xcb_query_tree_children_length +xcb_query_tree_reply +xcb_query_tree_sizeof +xcb_query_tree_unchecked +xcb_reparent_window +xcb_reparent_window_checked +xcb_request_check +xcb_screen_allowed_depths_iterator +xcb_screen_allowed_depths_length +xcb_screen_end +xcb_screen_next +xcb_screen_sizeof +xcb_set_selection_owner +xcb_set_selection_owner_checked +xcb_setup_pixmap_formats +xcb_setup_pixmap_formats_iterator +xcb_setup_pixmap_formats_length +xcb_setup_roots_iterator +xcb_setup_roots_length +xcb_setup_sizeof +xcb_setup_vendor +xcb_setup_vendor_end +xcb_setup_vendor_length +xcb_unmap_window +xcb_unmap_window_checked +xcb_visualtype_end +xcb_visualtype_next +xcb_wait_for_event diff --git a/tests/xlib-xcb-symbols.txt b/tests/xlib-xcb-symbols.txt new file mode 100644 index 00000000..292d6d96 --- /dev/null +++ b/tests/xlib-xcb-symbols.txt @@ -0,0 +1,2 @@ +XGetXCBConnection +XSetEventQueueOwner