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!