Why I love D: interfacing with XCB

H. S. Teoh hsteoh at quickfur.ath.cx
Fri Apr 8 16:47:27 UTC 2022


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.


More information about the Digitalmars-d mailing list