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