GSOC 2016 #5: Creating bridges
August 10, 2016 · 5 mins to read
Last week I started working on some killer-feature for Splash. It will allow you to write Lua scripts using almost the same Element (Node, HTMLElement) API as in JavaScript plus some additional helpful methods.
For example, you want to save the screenshot of the image when it will be loaded. Here is the script for it:
function main(splash) {
assert(splash:go(splash.args.url))
assert(splash:wait(1))
local shots = {}
local element = splash:select('#myImage') -- selecting the element by its CSS selector
element.onload = function(event) -- ataching the event listener
event:preventDefault()
table.insert(shots, element:png()) -- making a screenshot of the element
end
return shots
end
The Element API is still in development and can be changed
JS <-> PyQt <-> Python <-> Lua
Let’s see how the communication between JS and Lua is implemented. Imagine that we are going to execute the following Lua code:
element:click()
Lua
splash
is a table which has metatable and prototype Splash
. In Lua it means that splash
is an instance of Splash
class. click
method is wrapped into several Lua functions. After executing those function, we eventually will call the click
Python method. This is possible because of [Lupa] runtime for Lua which allows to inject Python methods into Lua code.
Python
click
is a method of _ExposedElement
Python class which contains all the methods and properties which can be accessed in Lua. It binds Python functions with Lua functions.
Let’s return to our click
method. It do the following procedure when it’s called:
- calls
private_node_method
passing the"click"
string which means that we want to call the click method of our JavaScript DOM element private_node_method
is another method_ExposedElement
and it calls thenode_method
method ofself.element
object which is an instance ofHTMLElement
class;HTMLElement
is a class which have API for communicating with the JavaScript HTMLElementHTMLElement#node_method
calls PyQt methodevaluateJavaScript()
with the following JS code:
window[elements_storage][element_id]['click']();
-
Description
elements_storage
is our elements storage which is a PyQT object; it allows us to save DOM elements for the further accesselement_id
is a unique ID which allows us to identify our element object"click"
is a method name which want to call (in this case it is “click”)
The elements storage is added to the JS window
object using the addToJavaScriptWindowObject
method of PyQt.
So, our Python self.element
is connected to the JS node
using the element_id
.
PyQt
PyQt allows us to have WebKit runtime environment in our Python application. Using addToJavaScriptWindowObject
we can add instances of QObject
to the JS window
object. Thereby it will allow us to call Python methods in JS.
JS
In JS our node
can be accessed through window[storage_name][element_id]
object.
This flow was OK for the one direction: from Lua to JS. But what if want to call Lua function from JS? That can happen when we assign an event handler for some event. In our first example we’ve assigned an event handler for the load
event.
JS -> Lua
Let’s examine this code:
element.onload = function(event)
event:preventDefault()
table.insert(shots, element:png())
end
We assign an event handler for load
event of our element. How it’s working?
- When
onload
property ofelement
is accessed it calls the__newindex
metamethod ofelement
. - This metamethod checks whether the requested property has the
'on'
prefix. If it does, we calls the private methodset_event_handler
ofelement
. - In its turn
set_event_handler
calls Python methodprivate_set_event_handler
of_ExposedElement
passing the event name for which we want to assign a handler, and the reference to handler function itself. - The crazy parts start here. We wrap our Lua function in Lua coroutine which will allow us to execute it when the event will be fired.
- We pass that coroutine to
set_event_handler
method ofHTMLElement
Python class. - It saves that coroutine and in another storage which is called event handlers storage returns its ID .
- Using PyQt
evaluateJavaScript()
method we execute the following JS code:
window[elements_storage][element_id].onload = function(event) {
window[event_handlers_storage].run(
event_handler_id,
window[events_storage].add(event)
);
};
You may think: what does window[event_handlers_storage].run
do and what is window[events_storage]
?
-
window[event_handlers_storage].run
- it calls our event handlers storage (which was injected in the same way as elements storage)
run
method; - that method, using the specified
event_handler_id
, calls the saved coroutine; - that coroutine will call our Lua function that was assigned to
onload
property of ourelement
Lua table;
- it calls our event handlers storage (which was injected in the same way as elements storage)
-
window[events_storage]
- it’s another storage, but now for events;
- the main reason for it is calling few methods of our event (preventDefault, stopPropagation, etc).
As you can see, in order to access our DOM element we should go through all the layers until we reach to JS.
The following days I will finish up writing tests and documentation for the newly created Element object. Also I will try to refactor those classes and methods to make the Lua <-> JS path more simple.