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
endThe 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_methodpassing the"click"string which means that we want to call the click method of our JavaScript DOM element private_node_methodis another method_ExposedElementand it calls thenode_methodmethod ofself.elementobject which is an instance ofHTMLElementclass;HTMLElementis a class which have API for communicating with the JavaScript HTMLElementHTMLElement#node_methodcalls PyQt methodevaluateJavaScript()with the following JS code:
window[elements_storage][element_id]['click']();-
Description
elements_storageis our elements storage which is a PyQT object; it allows us to save DOM elements for the further accesselement_idis 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())
endWe assign an event handler for load event of our element. How it’s working?
- When
onloadproperty ofelementis accessed it calls the__newindexmetamethod ofelement. - This metamethod checks whether the requested property has the
'on'prefix. If it does, we calls the private methodset_event_handlerofelement. - In its turn
set_event_handlercalls Python methodprivate_set_event_handlerof_ExposedElementpassing 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_handlermethod ofHTMLElementPython 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)
runmethod; - that method, using the specified
event_handler_id, calls the saved coroutine; - that coroutine will call our Lua function that was assigned to
onloadproperty of ourelementLua 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.