Fork me on GitHub
#membrane
<
2024-03-31
>
Omer ZAK20:03:09

In my work to build a Babashka pod which runs membrane, I have built a special purpose walker which walks a tree and transforms any elements, not serializable via EDN, into proxy and vice versa. The proxy scheme works when the membrane side (including the pod code) creates special objects. It then passes to the Babashka side proxies for those objects, and the Babashka side considers them to be opaque objects. However, when the Babashka side wants to create functions and pass them to the membrane side, it does not work. Turns out that the simple code

(java2d/run #(ui/label "Hello World!"))
creates a function #(ui/label "Hello World!") and wants to pass it to java2d/run as an argument. We need to somehow create the function on the membrane side, using instructions given in some form by the Babashka side. I feel that there must be an idiomatic way to accomplish this in Clojure. Any suggestions?

phronmophobic20:03:59

How do you serialize a function?

Omer ZAK21:03:52

The idea is not to directly serialize a function. In the Babashka side, we'll represent all functions in the various membrane's namespaces as {:proxyfunc "ui/label"} and equivalents for other functions. The membrane side (in pod code) will create the appropriate function and pass back a proxy object to the Babashka side. For example, our ui/label call will become ({:proxyfunc "ui/label"} "Hello world"). However, maybe you can propose a simpler scheme. Such as getting java2d/run (in membrane side) to accept forms starting with a string and interpret the string by itself to create the appropriate function.

phronmophobic21:03:45

I'm not sure I follow. Can you give a few more details?

Omer ZAK21:03:28

I do not have, yet, a clear mental model for this. For now, I'd like to know if you expose any macros to membrane's users? They will be even more problematic to serialize (or pseudo-serialize) than functions.

Omer ZAK21:03:33

Trying to explain. I understand that in membrane, you specify the GUI objects by means of functions which create them, such as (ui/label "Hello world"). In the Babashka side, we'll use instead some other data structure, which membrane side (in the Babashka pod code) will interpret as instructions to create a GUI object generating function. Such a data structure could be something like ( { :pod.tddpirate.condwalk/proxyfunc "ui/label" } "Hello world" ) . This form will be interpreted as instructions to create the #(ui/label "Hello world") function in membrane side.

phronmophobic16:04:17

> This form will be interpreted as instructions to create the #(ui/label "Hello world") function in membrane side. That's essentially the same thing as serializing the function. I think the proxy technique is useful as part of the solution, but I don't think just proxying the API is the best option.

phronmophobic16:04:01

The nice thing is that the membrane data model (ie. membrane.ui ) is almost all pure clojure. The only code that has to be on the pod side is the 1) window creation 2) drawing 3) text measurement. Here's what I'm thinking: On the bb side, membrane.ui is loaded via its source. Most of the ui stuff happens normally on the bb side. When the ui should be updated, it sends the ui data to the pod for rendering. On the pod side, the view is rendered. Any user events generated in the window are sent back to the bb side as data. On the bb side, the events are passed to the event function as normal. There's a few caveats that can be worked through: • text and image measurement need to be proxied or implemented for the bb side to work with labels and images • Not sure if you'll be using membrane.component, but since components are implemented via protocols which the pod side won't know about, there will have to be some special treatment when serializing them (I think that just means calling ui/children on them).

Omer ZAK16:04:44

Current status is that I got UI rendering to work (at least for ui/label). I defined, in the pod side, a run* function as follows.

(defn run*
  "Used instead of java2d/run due to special handling of first argument"
  [constfn & arg]
  (apply membrane.java2d/run (constantly constfn) arg))
Going through the usual Babashka pod mechanism, it is invoked from the bb script as follows.
(somenamespace/run* (ui/label "Hello World!"))
In other words, the ui/label form is passed as data rather than as a 0-arity function. So the proxying mechanism that I developed works for it. Currently, event handling is not working. Babashka has some mechanism for declaring and invoking callback functions. They can be used to transmit events from pod side to Babashka side. I am studying now how the stuff works and then hopefully I'll have some idea how to implement event handling in the pod.

Omer ZAK16:04:09

https://clojurians.slack.com/archives/CVB8K7V50/p1712074321097249?thread_ts=1711918689.311959&amp;cid=CVB8K7V50 My architectural approach keeps most of processing in the pod side (where the code is compiled into bytecode by liberica (and hopefully by graal in the future). The architectural approach, described by you, will move most of the processing to Babashka side and leave to the pod to do essentially only backend work. My gut feeling is that keeping most of the processing in the pod side is the way to go (shorter startup time? less source code interpreting?). But I am in favor of trying both architectures and letting the better one win.

phronmophobic16:04:02

> In other words, the ui/label form is passed as data rather than as a 0-arity function. So the proxying mechanism that I developed works for it. That's exactly what I described.

phronmophobic16:04:56

> Babashka has some mechanism for declaring and invoking callback functions. They can be used to transmit events from pod side to Babashka side. that could work.

phronmophobic16:04:06

If you send (somenamespace/run* (ui/label "Hello World!")), you're sending a constant. You'll want some way to update the view and have it redraw.

phronmophobic16:04:38

I suggest something like

(def window (somenamespace/make-window))
(somenamespace/render (ui/label "Hello World"))
(somenamespace/render (ui/label "Hi there"))

Omer ZAK16:04:49

https://clojurians.slack.com/archives/CVB8K7V50/p1712075402554939?thread_ts=1711918689.311959&cid=CVB8K7V50 ... except that you could wrap the ui data by constantly in the */run and */run-sync functions instead of my doing it in */run* and (currently unimplemented) */run-sync* and save me the need to add proxying code for each backend's run and run-sync functions. Since you are not doing it, I'd like to understand what philosophical reason do you have against this. It could point at problems in my approach that I didn't stumble upon, yet.

phronmophobic16:04:35

Constantly always returns the exact same value. A UI that only shows a static view is not very interesting and useful. That's why the backends accept a function.

phronmophobic16:04:26

and save me the need to add proxying code for each backend'sI suggest focusing on a single backend for now. All of the different backends require native code which will require different build setups. If you do get to the point where you want multiple backends there's https://phronmophobic.github.io/membrane/api/membrane.toolkit.html#var-run

Omer ZAK16:04:12

Oh, the value picked up by java2d/run & Co needs to be mutable. So you implement it as a function, which may yield a different value if something in the environment (such as refs, atoms or whatever) changed. How does the backend know when the value has changed i.e. the function needs to be reinvoked? Edited: in the toolkit you mention that the function is invoked for each redraw. So I need to implement there a Babashka callback mechanism in lieu of constantly.

phronmophobic16:04:05

java2d/run returns a map representing a window contains a :membrane.java2d/repaint key. It's a function that can force repaint. It also automatically repaints after events.

phronmophobic16:04:02

> So I need to implement there a Babashka callback mechanism in lieu of constantly. You could do that, but I would recommend something else. The reason is that you send a callback, which is serialized as proxy. To call the callback on the pod side, you need to then serialize the call to the bb side, which needs to serialize the response. This causes a bunch of extra unnecessary serialization on every redraw.

phronmophobic16:04:50

Then, on the bb side, you can just send over the view when you want it to be redrawn.

phronmophobic16:04:16

You can connect repaints to an atom using add-watch or whatever state mechanism you want.