GSOC 2016 #5: Creating bridges

Aug 10, 2016

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:

functionmain(splash){assert(splash:go(splash.args.url))assert(splash:wait(1))localshots={}localelement=splash:select('#myImage')-- selecting the element by its CSS selectorelement.onload=function(event)-- ataching the event listenerevent:preventDefault()table.insert(shots,element:png())-- making a screenshot of the elementendreturnshotsend

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_methodis another method _ExposedElement and it calls the node_method method of self.element object which is an instance of HTMLElement class;

HTMLElement is a class which have API for communicating with the JavaScript HTMLElement

HTMLElement#node_method calls PyQt method evaluateJavaScript() 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 access

element_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

We assign an event handler for load event of our element. How it’s working?

When onload property of element is accessed it calls the __newindex metamethod of element.

This metamethod checks whether the requested property has the'on' prefix. If it does, we calls the private method set_event_handler of element.

In its turn set_event_handler calls Python method private_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 of HTMLElement 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: