Site menu Emscripten: rode código C em ambiente Javascript
e-mail icon
Site menu

Emscripten: rode código C em ambiente Javascript

e-mail icon

Goste-se ou não, Javascript é o "esperanto", a linguagem universal. Praticamente todo ambiente possui um interpretador JS em que se pode rodar código embutido, sem falar em ambientes como Web, Electron, WebOS, etc. onde JS é a linguagem principal. Se você vai escrever uma biblioteca de uso geral hoje, deveria escrevê-la em JS.

Mas isto é um fenômeno recente. Muito código em C e C++ foi escrito antes disso acontecer. Reescrever esse código em JS custaria tempo e dinheiro, para no final entregar uma funcionalidade que você já tinha. Joel Spolsky já ensinou há 20 anos: nunca reescreva!

Pelo menos em se tratando de Node.js/Electron, uma forma sensata de reutilizar código de legado é escrever um módulo nativo. Mas isso implica em escrever o código-cola em C++, e lidar com compilação nativa em pelo menos três plataformas: Windows, Linux e Mac. Sim, existem ferramentas como node-gyp que auxiliam nessas tarefas, mas continua sendo uma tarefa não-trivial.

Existe outra opção: compilar código C para "binário" Javascript, usando o compilador Emscripten. Soa como algo extremamente subversivo, mas funciona.

O Emscripten é um projeto vasto, e possui um grande número de bibliotecas portadas para ele. Por exemplo, em se tratando de programa baseado na biblioteca SDL (geralmente utilizada em jogos) basta compilar e rodar; a adaptação do SDL para o ambiente HTML5 está inclusa!

Meu objetivo ao pesquisar sobre o Emscripten era bem mais modesto: portar uma biblioteca C, análoga a projetos tipo libpng com libjpeg. São bibliotecas "surdas" e "mudas": não interagem com a plataforma, só executam uma tarefa quando requisitadas. Geralmente isso é de propósito para facilitar o porte — o que vem a calhar na hora de portar para JS.

Uma parte do Emscripten é bem documentada no próprio site, por exemplo como instalar e usar, bem como chamar funções de JS para C e vice-versa, passando e retornando parâmetros numéricos. (A propósito, o Homebrew e distribuições Linux empacotam o Emscripten, tornando a instalação mais simples do que as instruções da documentação.)

O bicho pega quando se trata de passar ou retornar objetos como strings ou arrays. A documentação é esparsa e taciturna. Precisei caçar em inúmeros posts de blog e códigos de exemplo para descobrir como proceder. (Curiosamente, a documentação do interfaceamento com outras linguagens é um problema crônico em todos os interpretadores Javascript com que já lidei: Rhino, UIWebView, WebView do Android...)

O resultado dos meus esforços estão condensados neste projeto do GitHub. Se você é safo e está com pressa, não precisa nem continuar lendo este artigo. Vá direto ao GitHub.

Os exemplos do GitHub são voltados para o ambiente Node.js, e escolhi o "formato binário" asm.js (que é Javascript puro, embora escrito com dicas de otimização). O padrão seria binário WebAssembly, cujo suporte é relativamente recente no Node.js. É interessante olhar os flags do meu Makefile, e estudar a documentação do Emscripten para identificar quais flags são ideais para seu projeto.

Interfacear duas linguagens diferentes parece fácil, pois há apenas dois casos: linguagem A chamando função da linguagem B, e vice-versa. O problema é que uma função envia e retorna dados, e o tratamento em cada direção é diferente. Há três tipos principais de dados para tratar: números, strings e buffers, e é preciso definir quem vai chamar free() para os dois últimos. Os 2 casos viraram 20, e nem falamos ainda de como passar closures ou ponteiros de função.

Um módulo Emscripten, quando compilado com asm.js, carrega assincronamente. Precisei apelar para o escopo global para detectar quando o módulo está pronto para uso. Se alguém tiver uma ideia melhor, aceito push requests.

O heap do C compilado pelo Emscripten é simplesmente um array de bytes diretamente acessível via m.HEAPU8.buffer onde m é o módulo. Ponteiros são simplesmente offsets dentro desse array, e podem ser passados e retornados como números.

Neste ponto, esta "plataforma" C é VAXocentrista, vide itens 3 e 5. Por outro lado, o Emscripten é anti-VAX no item 6. Ele bronqueia com algo que é "comportamento indefinido" mas geralmente funciona nas demais plataformas: acesso desalinhado. A linha abaixo falha no Emscripten quando buffer_position é ímpar:

*((uint16_t *) buffer_position) = value;

Esse tipo de coisa é o mais assustador quando se porta código C para uma nova plataforma. Por essas e outras é importante habilitar a maior quantidade possível de flags de checagem, e não usar flags de otimização a não ser que absolutamente necessário. Do contrário, o código acima falharia silenciosamente. Uma vez identificado o problema, fazer a coisa do jeito certo é simples:

memcpy(buffer_position, &data, 2);

Segue uma descrição sucinta das técnicas usadas em cada fonte do exemplo.

Referências

e-mail icon