Tag: python

Executando Python (ou qualquer outra linguagem) no navegador

browsers-war-java-script-engines

Há alguns dias surgiu uma questão muito interessante no StackOverflow em Português sobre a possibilidade de executar uma linguagem arbitrária num browser, assim como é feito hoje com o Javascript.

Como poderíamos executar a nossa própria linguagem numa página web? Vamos encarar o desafio e implementar isso na prática!

Requisitos

Temos dois métodos para incluir código Javascript numa página web.

Dentro da tag <script>:

<script type="text/javascript">
alert('código javascript executando');
</script>

De uma fonte externa:

<script type="text/javascript" src="script.js"></script>

A ideia é permitir a inclusão de nossa linguagem da mesma forma.

Dentro da tag <script>:

<script type="text/nossalinguagem">
...
</script>

De uma fonte externa:

<script type="text/nossalinguagem" src="nosso-script.abc"></script>

Não estou ainda considerando código inline, como às vezes usado em eventos onclick, onchange, etc.

No entanto, existem particularidades na execução de tags <script>:

  • Elas são executadas sequencialmente, na ordem em que aparecem no HTML.
  • Se um script é incluído com o atributo src, o mesmo deve ser baixado e executado antes do navegador continuar a exibir o restante da página ou executar outros scripts. Este é o motivo pelo qual os scripts incluídos em um site devem ficar no final da página (perto do </body>) sempre que possível.

Nossa linguagem deve co-existir com o Javascript numa página HTML, como no trecho abaixo:

<script type="text/javascript">
var a = 6;
</script>
<script type="text/nossalinguagem">
print(a) # deve mostrar 6
</script>

Análise da solução

Como o navegador por padrão ignora linguagens que ele não conhece, não há problemas em declarar tags <script> com um atributo type qualquer. Entretanto, como fazer o navegador executá-lo?

Inicialmente, havia pensado que a única solução seria criar um loader em Javascript capaz de carregar a página e processar o conteúdo, adicionando programaticamente o HTML ao DOM e executando os scripts encontrados. Isso nada mais é que do uma versão do PHP ou algum template processor em Javascript, não é mesmo?

Porém, os navegadores modernos disponibilizam uma API que permite interceptar alterações na página, incluindo a criação de tags durante o carregamento do HTML. Trata-se do MutationObserver.

As possibilidades são extraordinárias!

Usando o MutationObserver

Para usar o MutationObserver, basta instanciá-lo passando em seu construtor uma função listener, a qual receberá como parâmetro as alterações na página, e invocar o método observe informando o elemento que desejamos monitorar, neste caso, todo o documento.

Vejamos um exemplo básico:

// cria instância do observer
var mutationObserver = new MutationObserver(function(mutations) {

    // itera sobre as alterações no documento
    mutations.forEach(function(mutation) {

        // recupera nós adicionados
        var addedNodes = mutation.addedNodes;

    });

});

// inicia monitoração das alterações de nós em qualquer nível do documento
mutationObserver.observe(document, { childList: true, subtree: true })

Só resta verificar se os nós adicionados são da nossa nova linguagem e delegar a execução a um interpretador.

Definindo uma linguagem: Python

Mas qual linguagem utilizar? Deveria ser uma linguagem de alto nível e poderosa, para obter um ganho de produtividade em relação ao Javascript.

Python me parece uma boa escolha!

Construindo (ou emprestando) um interpretador

O primeiro passo seria construir um interpretador em Javascript para nossa linguagem escolhida. Porém, não vamos reinventar a roda! Há diversas implementações desta linguagem em diversas plataformas. Para Javascript temos o Brython, uma implementação do Python 3.

Este projeto já cumpre parcialmente nossos requisitos quanto à interpretação da linguagem em tags <script> e acesso aos valores do Javascript. Porém, os scripts em Python são executados somente após o carregamento de toda a página através do evento onload do body e não conforme a sequência das tags <script> como desejamos.

Veja o exemplo de uso original do Brython:

<body onload="brython({debug:0, cache:'none'})">

Então vamos usarmos o MutationObserver para delegar a execução dos scripts ao Brython assim que o script for encontrado na página, oferecendo uma integração melhor da linguagem e atendendo os requisitos propostos.

Delegando a execução ao Brython

Nossa linguagem depende do interpretador Brython, então precisamos saber como delegar a execução dos scripts para ele. A má notícia é que o Brython não disponibiliza um meio direto para isso. A boa notícia é que lidamos com um projeto open source, então nada melhor que inspecionar o fonte para descobrir como ele faz isso!

Tudo o que precisamos é do trecho:

//define o nome do módulo como 'main' por que está na página principal
**BRYTHON**.$py_module_path['**main**'] = window.location.href;

//executa o analisador/conversor para Javascript 
var $root = **BRYTHON**.py2js($python_source, '**main**');

//recupera o código Javascript 
$javascript = $root.to_js();

Definindo um nome para nossa linguagem de script: Pyscript

Nossa linguagem de script merece um nome próprio. Como luizscript é muito feio, decidi calcular a equação:

Javascript – Java + Python = Pyscript

Então, nossa tag de script será:

<script type="text/pyscript"></script>

Adicionando suporte à linguagem Pyscript

Com toda a bagagem que juntamos até aqui já podemos criar a implementação do MutationObserver para dar suporte ao Pyscript em uma página web.

Vamos criar o arquivo pyscript.js:

//init brython 
brython();

//create observer instance
var mutationObserver = new MutationObserver(function(mutations) {

    //iterate over document changes
    mutations.forEach(function(mutation) {

        //get new node
        var node = mutation.addedNodes[0];

        //is it the type we want?
        if(node && node.tagName === 'SCRIPT' && node.type === 'text/pyscript') {

            //test log
            console.log('Pyscript found!');

            //python source
            var $src;

            //If src attribute is found, do a synchronous ajax to get 
            //the code in order to execute it immediately
            if (node.src!=='') {

                if (window.XMLHttpRequest){
                    // for IE7+, Firefox, Chrome, Opera, Safari
                    var $xmlhttp = new XMLHttpRequest();
                } else {
                    // for IE6, IE5
                    var $xmlhttp = new ActiveXObject("Microsoft.XMLHTTP");
                }
                $xmlhttp.open('GET', node.src, false);
                $xmlhttp.send();
                if ($xmlhttp.readyState === 4 && $xmlhttp.status === 200) {
                    $src = $xmlhttp.responseText;
                }
                if ($src === undefined) { // houston, we have a problem!!!
                    console.log('Error loading pyscript: ' + node.src);
                    return;
                }

            } else {

                //without src, source is tag content
                $src = node.textContent || node.innerText;

            }

            //python -> javascript
            __BRYTHON__.$py_module_path['__main__'] = window.location.href;
            var $root = __BRYTHON__.py2js($src, '__main__');
            var $jssrc = $root.to_js();

            //eval in global scope
            if (window.execScript) {
               window.execScript($jssrc);
               return;
            }

            //fix for old browsers
            var fn = function() {
                window.eval.call(window, $jssrc);
            };
            fn();
        } 
    });    

});

//init observer, monitoring changes in all nodes of any level
mutationObserver.observe(document, { childList: true, subtree: true });

O código acima inicia um observador (MutationObserver) para todos os elementos da página. Se uma tag <script> for encontrada cuja linguagem (atributo type) for text/pyscript, então o código será delegado ao Brython. No caso da tag ter o atributo src, nos dizendo que o código Python está em um arquivo externo, então fazemos uma chamada Ajax síncrona para recuperar o código e executá-lo imediatamente.

Aplicando numa página web

Para a utilização da nossa linguagem Pyscript em uma página qualquer, precisamos incluir tanto o Brython quanto nossa implementação do MutationObserver, nesta ordem, logo no início do código HTML.

Vejamos o exemplo:

<html> 
<head> 
<script type="text/javascript" src="brython.js"></script> 
<script type="text/javascript" src="pyscript.js"></script>

E – voilà – temos nossa própria linguagem rodando na página!

Um exemplo prático

<!DOCTYPE html>  
<html> 
<head> 
<meta charset="UTF-8"> 
<title>Pyscript Test Page</title>

<!-- init brython and pyscript --> 
<script type="text/javascript" src="brython.js"></script> 
<script type="text/javascript" src="pyscript.js"></script>

<!-- set javascript variable --> 
<script type="text/javascript"> 
var value = 1; 
</script>

<!-- python: print text and javascript variable --> 
<script type="text/pyscript"> 
print('Print Test!!!') 
print(value) 
</script>

</head>

<body> 
    <div id="content">Conteúdo</div> 
</body>

<!-- python: browser interaction --> 
<script type="text/pyscript"> 
from _browser import doc, alert

alert('Variable value: ' + str(value))

mylist = ['items', 'of', 'mylist'] 
doc['content'].text = 'Content from python: ' + ' '.join(mylist) 
</script>

<!-- python: execute external script --> 
<script type="text/pyscript" src="myscript.py"></script>

</html>

Nesta página, após incluir nossas dependências no início, existem várias tags de script:

  • A primeira executa Javascript que inicializa a variável value.
  • A segunda executa Python e imprime a variável value no console do navegador.
  • A terceira executa Python e interage com o navegador, exibindo uma caixa de alerta e manipulando o conteúdo (texto) do elemento <div id="content">.
  • A quarta e última executa Python a partir de um arquivo externo.

O conteúdo do arquivo myscript.py é bem simples. Vejamos:

d = { '11': 'um', '22': 'dois' }
for i in d: print(i, '=', d[i])

O script acima cria um “dicionário” d (dict) e então exibe chaves e valores de seus elementos num loop for.

Voltando um pouco ao terceiro script, note que importamos a biblioteca _browser, pertencente ao Brython. Ela nos dá uma interface para acessar as funcionalidades do navegador de forma simples e direta.

É importante observar que o Brython não contém todas as bibliotecas em seu script principal. Para cada import é feito o download da respectiva biblioteca, a não ser que a mesma já tenha sido carregada em um import anterior ou incluída numa tag <script>.

Assista ao vivo

Quer ver o exemplo funcionando? Ele está disponível no link http://utluiz.github.io/pyscript/!

Nota: não se esqueça de acessar a ferramenta do desenvolvedor (F12) para verificar o console!

Quer o código-fonte? Ele está disponível na minha conta do Github: https://github.com/utluiz/utluiz.github.io!

Algumas considerações sobre desempenho

A esta altura, um desenvolvedor mais experiente já estaria se coçando todo sobre a questão do desempenho dessa solução.

Existem iniciativas das empresas que desenvolvem navegadores (Google, Mozilla, etc.) para criar soluções que melhoram o desempenho do Javascript. Hoje, executar código no navegador não é mais um problema. Mas será que isso é suficiente para suportar algo tão profundo quanto uma nova linguagem?

Obviamente, a conversão de código Python para Javascript é muito lenta! Por outro lado, o Brython coloca o código-fonte convertido em um cache para não precisar realizar o processo novamente. Ao analisar o código-fonte do Brython me deparei com soluções de armazenamento usando a API local storage do HTML5, então mesmo se a página for recarregada a conversão não será feita novamente para o mesmo bloco de script.

Mas e quanto à execução? Se o código final executado é em Javascript, então o navegador consegue fazer as mesmas otimizações que no código nativo! Bem, mas isso não significa que será tão eficiente quanto código nativo, afinal existe um overhead natural, pois cada instrução Python pode gerar várias instruções em Javscript.

Limitações da solução

Devido a limitações do Brython, o código Javascript não consegue acessar os objetos e variáveis definidos no código Python. De qualquer forma, não consigo ver isso como uma grande necessidade.

Outra limitação é quanto a código inline mencionado no início do artigo. Por enquanto, não podemos fazer algo como no código abaixo:

<button onclick="python: print(1)">Botão</button>

Não que seja impossível. Poderíamos manipular o valor do atributo onclick dentro do MutationObserver, substituindo-o por uma função Javascript que delega a execução do código para o Brython! Bem, já está meio tarde, então deixo isso como lição de casa. 😉

De qualquer forma, essa limitação é facilmente contornada removendo o atributo onclick e adicionando um listener ao respectivo evento através de código Python, como nos é informado na própria documentação do Brython:

btn.bind('click', show)

Compatibilidade com navegadores

Segundo a documentação, o MutationObserver é compatível com o seguintes navegadores:

  • Google Chrome: versão 26 em diante
  • Mozilla Firefox: versão 14 em diante
  • Internet Explorer: versão 11 em diante
  • Opera: versão 15 em diante
  • Safari: versão 6 em diante

Como sempre, o mais “atrasadinho” é o Internet Explorer. Porém, estamos olhando para o futuro. Em alguns anos a maioria dos usuários do IE poderá executar nossa solução sem dificuldades.

A documentação do Brython não informa sobre sua compatibilidade, mas olhando o código-fonte encontrei implementações específicas para IE 6 e 7. Portanto, a limitação deve ficar mesmo com o MutationObserver.

Considerações finais

Na prática, creio ainda não ser viável aplicar esse tipo de solução em aplicações reais. Por outro lado, é um ótimo exercício!

O que podemos esperar do futuro? Muitos desenvolvedores estão ansiosos por rodar suas linguagens prediletas no navegador!

Hoje JVMs como a do Java já executam diversas linguagens como Scala, Ruby, Python, PHP e assim por diante. Quem sabe, em breve, poderemos testemunhar a ascensão da liberdade de linguagem dentro dos navegadores web!

Running Python (or any language) in your browser

browsers-war-java-script-engines

Some time ago a very interesting question arose in StackOverflow in Portuguese about running an arbitrary programming language in the browser, just like Javascript does.

How could we run our own language in a wesob page? Let’s face the challenge and implement it!

Requirements

There are two ways to include Javascript in HTML documents:

Inside a <script> tag:

<script type="text/javascript">
alert('javascript running');
</script>

From an external source:

<script type="text/javascript" src="script.js"></script>

We want to use our language in the same fashion.

Inside a <script> tag:

<script type="text/ourlanguage">
...
</script>

From an external source:

<script type="text/ourlanguage" src="our-script.abc"></script>

We won’t consider inline code, as in onclick, onchange, and other HTML tag attributes.

Keep in mind that <script> tags have certain characteristics we want to preserve;

  • They’re executed sequentially, in the order they appear in HTML.
  • If a script has the src attribute defined, it has to be downloaded and executed before the browser continues to parse the rest of the page or execute any other script. This is the reason we should only include scripts in the end of the source (close to </body>) whenever it’s possible.

Also, our new language should coexist with Javascript code in the web page, as in the example below:

<script type="text/javascript">
var a = 6;
</script>
<script type="text/ourlanguage">
print(a) # should prints 6
</script>

Working the solution

As browsers ignore script languages they don’t recognize, there is no problem if we use the <script> tag with an arbitrary type attribute. But how can we do the browser run it?

At first, I thought the only solution would be through a generic loader in javascript, which would able to load the entire page and process its content, adding all the HTML tags programmatically to the DOM and running scripts as they would be found.

It’s nothing more, nothing less, than a Javascript version of PHP or some template processor, isn’t it? It looks to complicated to be useful.

However, what I found out is that modern browsers provide an API to intercept web page changes, including those when new tags are added to the DOM during the HTML parsing. We’re talking about MutationObserver.

The possibilities are extraordinary!

Using MutationObserver

To use MutationObserver we just need to instantiate it passing a listener function to the constructor. The function will receive changes in the page as an argument. Then, we just call observe method in the instance passing the element we want to monitor. In this case, it’s the entire document.

Let’s see a basic example:

// creates the observer instance
var mutationObserver = new MutationObserver(function(mutations) {

    // iterates over the changes in the document
    mutations.forEach(function(mutation) {

        // gets all added nodes
        var addedNodes = mutation.addedNodes;

    });

});

// starts to monitor for changes in any level of the document
mutationObserver.observe(document, { childList: true, subtree: true })

Now we can check if there’s added notes of our language and then delegate the execution to its interpreter.

Choosing a language: Python

Which language to use? It should be a high-level, powerful, productive language when compared to Javascript.

Python looks like a great choice!

Building (or borrowing) an interpreter

The first step would be to build an interpreter in Javascript for our language. However, we won’t reinvent the wheel! There is implementations of the language for various platforms already. In Javascript we have a Python 3 implementation called Brython.

This project accomplishes partially our requirements about execution in <script> tags and acessing Javascript objects and variables. But Python scripts run only after the webpage is fully loaded in the the onload event from body. We want it like movie credits, that is, in the order of appearance of <script> tags, remember?

Look at the original Brython use case:

<body onload="brython({debug:0, cache:'none'})">

But, we can use MutationObserver to delegate Python scripts to Brython as they are found in the web page, offering a better integration and meeting our requirements.

Delegating the execution to Brython

Our language depends on Brython interpreter, so we need to know how to delegate scripts to it.

The bad news is that Brython don’t provide a public API to do that. The good news is that we’re dealing with an open-source project, so we can inspect the source to find out how he does its magic!

All we need is in the following excerpt:

//defines the module name as 'main' because it's in the main page
**BRYTHON**.$py_module_path['**main**'] = window.location.href;

//runs the analyzer/converter to Javascript 
var $root = **BRYTHON**.py2js($python_source, '**main**');

//gets the actual Javascript code
$javascript = $root.to_js();

Choosing the name for our scripting language: Pyscript

Our scripting language deserves a good name. As luizscript is too ugly, I decided to calculate:

Javascript – Java + Python = Pyscript

So, the tag will be like this:

<script type="text/pyscript"></script>

Adding support to Pyscript language

With all we have learned until now, let’s implement a solution using MutationObserver to add support to Pyscript in a web page.

This is the main implementation in the file pyscript.js:

//inits brython 
brython();

//creates observer instance
var mutationObserver = new MutationObserver(function(mutations) {

    //iterates over document changes
    mutations.forEach(function(mutation) {

        //gets new node
        var node = mutation.addedNodes[0];

        //is it the type we want?
        if(node && node.tagName === 'SCRIPT' && node.type === 'text/pyscript') {

            //test log
            console.log('Pyscript found!');

            //python source
            var $src;

            //If src attribute is found, do a synchronous ajax to get 
            //the code in order to execute it immediately
            if (node.src!=='') {

                if (window.XMLHttpRequest){
                    // for IE7+, Firefox, Chrome, Opera, Safari
                    var $xmlhttp = new XMLHttpRequest();
                } else {
                    // for IE6, IE5
                    var $xmlhttp = new ActiveXObject("Microsoft.XMLHTTP");
                }
                $xmlhttp.open('GET', node.src, false);
                $xmlhttp.send();
                if ($xmlhttp.readyState === 4 && $xmlhttp.status === 200) {
                    $src = $xmlhttp.responseText;
                }
                if ($src === undefined) { // houston, we have a problem!!!
                    console.log('Error loading pyscript: ' + node.src);
                    return;
                }

            } else {

                //without src, source is tag content
                $src = node.textContent || node.innerText;

            }

            //python -> javascript
            __BRYTHON__.$py_module_path['__main__'] = window.location.href;
            var $root = __BRYTHON__.py2js($src, '__main__');
            var $jssrc = $root.to_js();

            //eval in global scope
            if (window.execScript) {
               window.execScript($jssrc);
               return;
            }

            //fix for old browsers
            var fn = function() {
                window.eval.call(window, $jssrc);
            };
            fn();
        } 
    });    

});

//init observer, monitoring changes in all nodes of any level
mutationObserver.observe(document, { childList: true, subtree: true });

The code above initiates an observer (MutationObserver) for all elements that will be loaded in the webpage. If a <script> tag is found with language (type attribute) equals text/pyscript, the code will be converted by Bryton. If the tag has the src attribute, we do an Ajax synchronous call to get the source and then run it immediately.

Using in a web page

In order to use Pyscript language in any web page, we need to include both Brython and our MutationObserver implementation, in this order, in the beginning of the HTML code.

See this example:

<html> 
<head> 
<script type="text/javascript" src="brython.js"></script> 
<script type="text/javascript" src="pyscript.js"></script>

And – voilà – we have our own language running in a web page! 😀

A practical example

<!DOCTYPE html>  
<html> 
<head> 
<meta charset="UTF-8"> 
<title>Pyscript Test Page</title>

<!-- init brython and pyscript --> 
<script type="text/javascript" src="brython.js"></script> 
<script type="text/javascript" src="pyscript.js"></script>

<!-- set javascript variable --> 
<script type="text/javascript"> 
var value = 1; 
</script>

<!-- python: print text and javascript variable --> 
<script type="text/pyscript"> 
print('Print Test!!!') 
print(value) 
</script>

</head>

<body> 
    <div id="content">Content Here</div> 
</body>

<!-- python: browser interaction --> 
<script type="text/pyscript"> 
from _browser import doc, alert

alert('Variable value: ' + str(value))

mylist = ['items', 'of', 'mylist'] 
doc['content'].text = 'Content from python: ' + ' '.join(mylist) 
</script>

<!-- python: execute external script --> 
<script type="text/pyscript" src="myscript.py"></script>

</html>

In the page above, after including the two dependencies in the beginning, there’s many script tags. I’ll explain what each one does:

  • The first one runs Javascript and sets the value variable.
  • The second runs Python and prints the value variable in the browser console.
  • The third runs Python and interacts with the browser, showing an alert box and manipulating the text of the <div id="content"> element.
  • The fourth runs Python from an external file.

The content of myscript.py file is very simple. have a look at it:

d = { '11': 'one', '22': 'two' }
for i in d: print(i, '=', d[i])

The script above creates a dictionary d (dict) and then outputs its keys and values in a for loop.

If you look back to the third script, you’ll notice the _browser library. It’s from Brython and gives us access to browser features in a simple and straightforward manner.

Notice that Brython doesn’t have all its libraries in the main script we included in the HTML. For each import it downloads the corresponding library, unless it was previously loaded in a previous import or included in a <script> tag.

Check it out

Do you want to see a working example? It’s available in my GitHub site: http://utluiz.github.io/pyscript/!

Note: do not forget to open the developer tools of your browser (F12) and check the console output!

Wanna see the source code? It’s availabel in my GitHub account: https://github.com/utluiz/utluiz.github.io!

Performance issues

At this point, good engineers should be very worried about the performance of this solution.

Well, you probably know about a lot of initiatives from bowser developers (Google, Mozilla, Microsoft) to improve Javascript performance. Today, running code in a browser is not a problem anymore. But is it enough to support something like a new language?

Obviously the conversion from Python to Javascript is very, very slow! On the other hand, Brython caches the translated code. And it gets even better. Looking Brython source code over, I found that it uses HTML5 local storage API to cache translated scripts, so the cache persists through page reloading.

“And about the runtime?”, you can ask. The final code is pure Javascript, so the browser is able to do the same optimizations it does with native Javascript code! Well, of course it doesn’t mean it’ll be as efficient as native code, after all there is a natural overhead due to instruction “adaptation” and each Python instruction can be translated into several Javascript commands.

Limitations

Brython has some limitations.

For instance, Javascript code can’t access objects and variables defined in Python. But I think it’s not a great need anyway.

Another point is about inline code as I have mentioned in the beginning. For now, we can’t do something like this:

<button onclick="python: print(1)">Button</button>

Indeed, it’s not impossible. We could manipulate the onclick attribute in our MutationObserver implementation, replacing the value by a Javascript function that delegates the execution to Brython!

Well, this article is bit long already, so I’ll assign it to the reader as homework. 😉

Anyway, you should know that is much better don’t use event attributes. It’s a good practice doing unobstructive Javascript through listener. Brython docs tell us how we can do that:

btn.bind('click', show)

Browser compatibility

According to the documentation, MutationObserver is compatible with the following browsers:

  • Google Chrome: version 26 or greater
  • Mozilla Firefox: version 14 or greater
  • Internet Explorer: version 11 or greater
  • Opera: version 15 or greater
  • Safari: version 6 or greater

As always, Internet Explorer is late. However, we’re looking to the future now. In a few years the majority of the IE users will be able to run this solutions without any problem.

Brython tell us nothing about compatibility, but in its source code I found specific implementations to IE 6 and 7. Therefore the limiting factor dwells with MutationObserver.

Final thoughts

In practice, I believe this kind of implementation is not viable for real applications. However, it was a great exercise!

What to expect in the future? Many developers are anxious to run their favorite language!

Today, we have the JVM already supporting many languages like Scala, Ruby, Python, PHP, and so forth. Perhaps, in a near future, we’ll testimony freedom to run any language in a web browser!

Creative Commons O blog State of the Art de Luiz Ricardo é licenciado sob uma Licença Creative Commons. Copie, compartihe e modifique, apenas cite a fonte.