The long way(land) round#
Mon, 18 Mar 2013 21:07:47 +0000
The latest round of psadan hacking was motivated by two goals
- that it would be good to actually remember the globals we're
receiving when we send
get_registry
to the magic registry object
- that if we're going to ask for a callback when all the globals are notified, we should wait for it before continuing.
We went down a couple or three dead ends on our way to this goal, but
eventually we settled on creating an agent responsible for listening
to the connection (I called it channel
, in the absence of any better
ideas) and dispatching (using a handle-message
multimethod) to some
code appropriate for each kind of message we're receiving.
Learnings
- multimethods the clojure way are pretty versatile: you can dispatch
on pretty much any property - or derived property - of the function
argument, not just on type. In our case that's the interface name
and the message (event) name:
(defmulti handle-message (fn [conn m] [(:name (:interface m)) (:message m)])) (defmethod handle-message [:wl_registry :global] [conn m] (let [[name interface version] (:args m)] (conn/register-global conn name interface version))) (defmethod handle-message [:wl_callback :done] [conn m] (let [object (conn/get-object conn (:object-id m)) promise (:promise object)] (when promise (deliver promise m)))) (defmethod handle-message :default [conn m] (println ["unknown message" (:name (:interface m)) (:message m)]))
- we handle the "tell me when you're done" requirement with a
promise. The
get-globals
code adds "an unfulfilled promise"::https://github.com/telent/psadan/blob/a54f6d366477c85940d20164d6f5ed279a604fd0/src/psadan/channel.clj#L53 to the callback object it creates, then once it has sent out its messages it derefs the promise , causing it to wait until something delivers the promise. That something is thehandle-message
implementation forwl_callback
, for which, see above.
- we were trying to map
handle-message
onto each of the messages parsed out of the buffer, but not doing anything with the result. Given thatmap
is lazy, this meant ourhandle-message
code was for the most part not being called. Surrounding themap
form with adorun
fixed this.
- you send work to an agent with
(send fn ...)
or(send-off fn ...)
which invoke the fn with the current state of the agent, not with the agent itself. Which is fine but offers no facility for the agent to send work to itself - happily, the global/magic/special variable*agent*
, which evaluates to the currently running agent if any is, works nicely for this purpose(defn listen [conn] (let [buf (conn/read-buffer conn) messages (buf/parse-messages buf conn :events)] (dorun (map #(handle-message conn %) messages))) (send-off *agent* listen) conn) (send-off channel listen)
- the 'name' field in a global is (confusingly) a number, and (more
confusingly still) not an object id. Object number 3 in our client is a
wl_callback
object, whereas the global named 3 is .. well, let's check ...psadan.core> (def channel (chan/open-channel "/home/dan/private/wayland-0"))
- 'psadan.core/channel
psadan.core> (chan/get-registry channel)
;; [debug output elided]
{:object-id 3, :bytes 12, :interface {:index 2, :name :wl_callback, :requests (), :events ({:index 0, :name :done, :summary nil, :args ({:name "serial", :type :uint, :interface nil})}), :enums ()}, :message :done, :args (-115)}
psadan.core> (get
(:globals
channel) 3) {:interface :screenshooter, :version 1}
Next up? At some point we need to decide whether sending messages should be done by the channel or whether it's OK to carry on doing that directly in whatever thread we happen to be in. But what would be much more fun is to see if we can actually render a window...