Thread overview
Re: Why I love D: interfacing with XCB
April 06
On Wed, Apr 06, 2022 at 07:10:36PM +0000, Adam D Ruppe via Digitalmars-d wrote:
> On Wednesday, 6 April 2022 at 18:42:42 UTC, H. S. Teoh wrote:
> > The original core library is Xlib, which is still in widespread use but is dated and suffers from a variety of issues.
> 
> Those alleged issues are pretty overblown. My biggest sadness with it is it assumes i/o failures are fatal, but you can kinda hack around that by throwing a D exception from the handler lol.

I suspect some of the issues have since been solved, since Xlib nowadays is implemented on top of XCB. :-P


> > One such pain area is keyboard handling.  Won't get into the dirty details here, but if anybody's interested, just ask. ;-) (I *did* manage to get rudimentary keyboard handling without touching Xlib at all.)
> 
> keyboard handling is kinda inherently messy

Yeah, but with X (like *core* X protocol, without Xlib) it's a whole new level of messy that I hadn't expected before. :-P

Basically, the X server *only* trades in keycodes, which are arbitrary numbers associated with physical keys on your keyboard.  To make any sense of these keycodes, you need to map them to keysyms, i.e., standard numbers representing named keys. In Xlib, mapping a keycode to a keysym is just 1 or 2 function calls away.  In XCB, however, you have to reinvent what Xlib does under the hood:

1) First, you have to retrieve the keyboard mapping from the X server, which is a table of keysyms that a particular keycode may map to.

2) But each keycode may map to ≥4 keysyms; to decide which one is in
effect at the time of a keypress, you need to look at the current
modifiers in effect (shift, capsLock, etc), and based on the
algorithm described in the X protocol, decide which of 4 keysyms it will
be. This would've been a trivial task, except that the meaning of the
modifier bitmask may change depending on server configuration; in order
to figure out which bit corresponds with the "mode switch" modifier, you
need to ask the X server for the modifier map, and scan the table for
the "mode switch" keysym and remember which bit it corresponds to.

3) You'd think that solves the problem, but no, there's more. Both the modifier map and the keyboard map may change over time, so you also have to process MappingNotify events, and reload the relevant portions of the keyboard map or the modifier map (and potentially refresh your current understanding of modifier bits).  One potential gotcha here is that all subsequent KeyPress events after the MappingNotify will use the new mapping, so you have to make sure you refresh the keymap and modmap *before* processing any subsequent KeyPress events.

4) But even after this, you're not quite done yet. Most applications don't know nor care what keycodes or keysyms are; they usually want to translate keysyms into actual characters (ASCII, Unicode, what-have-you). To do this, you need to use a table of keysym -> ISO 10646 (i.e., Unicode) values:

	https://www.cl.cam.ac.uk/~mgk25/ucs/keysym2ucs.c

5) This isn't all there is to it, though. There's also XKB and the Input
extension, which you will need if you want to support things like
combining characters, dead keys, and input methods. (I haven't gotten
that far yet, 'cos with (1)-(4) I already have a pretty workable system.
XKB, if the server has it (pretty much all modern servers do), simulates
most of its functionality via X11 core protocol to clients who didn't
enable the XKB extension, so keyboard input will "mostly work" at this
point, as long as you handle (1)-(4) correctly.)

So there you have it, X11 keyboard handling without Xlib in a nutshell. ;-)


T

-- 
Ph.D. = Permanent head Damage
April 06
On Wednesday, 6 April 2022 at 20:20:26 UTC, H. S. Teoh wrote:

> 1) ...
> 2) ...
> 3) You'd think that solves the problem, but no, there's more. Both the modifier map and the keyboard map may change over time, so you also have to process MappingNotify events, and reload the relevant portions of the keyboard map or the modifier map (and potentially refresh your current understanding of modifier bits).  One potential gotcha here is that all subsequent KeyPress events after the MappingNotify will use the new mapping, so you have to make sure you refresh the keymap and modmap *before* processing any subsequent KeyPress events.
>
> 4) ...
> 5) ...
>
> So there you have it, X11 keyboard handling without Xlib in a nutshell. ;-)

6) If you care about key repeat events (which most applications that deal with text input should, outside of, perhaps, some games) - there are none. You have to look ahead at the next event every time you receive a KeyRelease. Which makes an event loop that much more "interesting".
April 08
On Wed, Apr 06, 2022 at 11:42:42AM -0700, H. S. Teoh via Digitalmars-d wrote: [...]
> (Conceivably, this API could be improved even further by keeping the futures array in the `XCB` wrapper itself, with a .flush method for flushing the queued response handlers. Or insert them into the event loop.)
[...]

Update: I actually went ahead and did this. Now XCB code looks even cleaner than before:

	auto xcb = new XCB(xcb_connect(null, null));
	...

	// This is an excerpt from actual code I'm running, that
	// retrieves various attributes from a window.
        xcb.get_window_attributes(winid, (attrs) {
            win.override_redirect = cast(bool) attrs.override_redirect;
            win.is_mapped = (attrs.map_state == XCB_MAP_STATE_VIEWABLE);
            win.win_gravity = attrs.win_gravity;
        });

        xcb.getStringProperty(winid, XCB_ATOM_WM_NAME, (s) {
            win.wmName = s.idup;
        });

        xcb.getStringProperty(winid, XCB_ATOM_WM_ICON_NAME, (s) {
            win.wmIconName = s.idup;
        });

        xcb.getStringProperty(winid, XCB_ATOM_WM_CLASS, (s) {
            auto split = s.indexOf('\0');
            if (split != -1 && split < s.length)
            {
                win.wmInstanceName = s[0 .. split].idup;
                win.wmClassName = s[split+1 .. $].idup;
            }
        });
	...

	xcb.flush(); // process all the responses

I changed the XCB wrapper into a final class instead, in order to avoid closure-over-stale-struct issues. Basically, the XCB object keeps track of the current xcb_connection_t* plus a queue of response callbacks that gets appended to every time you call an XCB.xxx function.  Requests are non-blocking as before, and responses are processed upon calling .flush.

.getStringProperty is syntactic sugar for xcb.get_property plus some standard boilerplate for handling string responses. A bit hackish atm but good enough for what I need to do for now.

The idea is pretty straightforward, though there was a tricky issue in the implementation of .flush: my initial implementation was buggy because I hadn't taken into account that response callbacks may trigger more requests and recursively invoke .flush again. So I had to tweak the implementation of .flush to make it reentrant.

The code is as follows:

------------
/**
 * Proxy object for nicer interface with xcb functions.
 */
final class XCB
{
    static struct OnError
    {
        static void delegate(lazy string msg) warn;
        static void delegate(lazy string msg) exception;
        static void delegate(lazy string msg) ignore;

        static this()
        {
            warn = (lazy msg) => stderr.writeln(msg);
            exception = (lazy msg) => throw new Exception(msg);
            ignore = (lazy msg) {};
        }
    }

    private xcb_connection_t* xc;
    private void delegate()[] fut;

    /**
     * Constructor.
     */
    this(xcb_connection_t* _conn)
        in (_conn !is null)
    {
        xc = _conn;
    }

    /**
     * Returns: The XCB connection object.
     */
    xcb_connection_t* conn() { return xc; }

    /**
     * Syntactic sugar for calling XCB functions.
     *
     * For every pair of XCB functions of the form "xcb_funcname" taking
     * arguments (xcb_connection_t* xc, Args...) and "xcb_funcname_reply"
     * returning a value of type Reply, this object provides a corresponding
     * method of the form:
     *
     * ------
     * void XCB.funcname(Args, void delegate(Reply) cb, OnError onError)
     * ------
     *
     * For every XCB function of the form "xcb_funcname_checked" that do not
     * generate a server reply, this object provides a corresponding method of
     * the form:
     *
     * ------
     * void delegate() XCB.funcname(Args, void delegate() cb, OnError onError)
     * ------
     *
     * The callback `cb` is registered in the internal queue after the request
     * is sent, and is not called immediately. Instead, .flush must be called
     * in order to retrieve the responses from the server, at which point `cb`
     * will be invoked if the server returns a success, or else the action
     * specified by onError will be taken if the server returns an error.
     */
    template opDispatch(string func)
    {
        enum reqFunc = "xcb_" ~ func;
        alias Args = Parameters!(mixin(reqFunc));
        static assert(Args.length > 0 && is(Args[0] == xcb_connection_t*));

        enum replyFunc = "xcb_" ~ func ~ "_reply";
        static if (__traits(hasMember, xcb.xcb, replyFunc))
        {
            alias Reply = ReturnType!(mixin(replyFunc));

            void opDispatch(Args[1..$] args, void delegate(Reply) cb,
                            void delegate(lazy string) onError = OnError.warn)
            {
                auto cookie = mixin(reqFunc ~ "(xc, args)");
                fut ~= {
                    import core.stdc.stdlib : free;
                    xcb_generic_error_t* e;

                    Reply reply = mixin(replyFunc ~ "(xc, cookie, &e)");
                    if (reply is null)
                        onError("%s failed: %s".format(reqFunc, e.toString));
                    else
                    {
                        scope(exit) free(reply);
                        cb(reply);
                    }
                };
            }
        }
        else // No reply function, use generic check instead.
        {
            void opDispatch(Args[1..$] args, void delegate() cb = null,
                            void delegate(lazy string) onError = OnError.warn)
            {
                auto cookie = mixin(reqFunc ~ "_checked(xc, args)");
                fut ~= {
                    xcb_generic_error_t* e = xcb_request_check(xc, cookie);
                    if (e !is null)
                        onError("%s failed: %s".format(reqFunc, e.toString));
                    if (cb) cb();
                };
            }
        }
    }

    unittest
    {
        alias F = opDispatch!"get_window_attributes";
        //pragma(msg, typeof(F));

        alias G = opDispatch!"map_window";
        //pragma(msg, typeof(G));
    }

    enum maxStrWords = 40; // effective length is this value * 4

    /**
     * Convenience method for retrieving string properties.
     *
     * IMPORTANT: The const(char)[] received by `cb` is transient; make sure
     * you .dup or .idup it if you intend it to persist beyond the scope of the
     * callback!
     */
    void getStringProperty(xcb_window_t winid,
                           xcb_atom_t attr,
                           void delegate(const(char)[]) cb,
                           void delegate(lazy string) onError = OnError.warn)
    {
        this.get_property(0, winid, attr, XCB_ATOM_STRING, 0, maxStrWords,
                          (resp) {
            if (resp.format != 8)
            {
                return onError(format(
                    "Could not retrieve string property %d on 0x%x", attr,
                    winid));
            }

            void* val = xcb_get_property_value(resp);
            cb((cast(char*)val)[0 .. resp.value_len]);
        });
    }

    /**
     * Run any queued response callbacks.
     */
    void flush()
    {
        if (xcb_flush(xc) < 0)
            stderr.writeln("xcb_flush failed");

        while (fut.length > 0)
        {
            auto f = fut[0];
            fut = fut[1 .. $]; // for reentrancy, must be done BEFORE calling f
            f();
        }
    }
}
------------


T

-- 
There are two ways to write error-free programs; only the third one works.