Pular para o conteúdo principal

Asyncify e JSPI: troca de pilha no PHP WebAssembly

O Asyncify permite que código C ou C++ síncrono interaja com JavaScript assíncrono. Tecnicamente, ele salva toda a pilha de chamadas C antes de devolver o controle ao JavaScript e, em seguida, a restaura quando a chamada assíncrona termina. Isso é chamado de troca de pilha.

O suporte a rede na build WebAssembly do PHP é implementado com Asyncify. Quando o PHP faz uma requisição de rede, ele devolve o controle ao JavaScript, que executa a requisição e retoma o PHP quando a resposta está pronta. Funciona bem o suficiente para que a build PHP possa chamar APIs web, instalar pacotes do Composer e até conectar a um servidor MySQL.

Falhas do Asyncify

A troca de pilha exige envolver todas as funções C que possam estar na pilha de chamadas no momento de uma chamada assíncrona. Envolver indiscriminadamente cada função C adiciona uma sobrecarga significativa, por isso mantemos uma lista de nomes de funções específicas:

https://github.com/WordPress/wordpress-playground/blob/15a660940ee9b4a332965ba2a987f6fda0c159b1/packages/php-wasm/compile/Dockerfile#L624-L632

Infelizmente, omitir um único item dessa lista resulta em falha do WebAssembly sempre que essa função faz parte da pilha de chamadas quando uma chamada assíncrona é feita. O erro se parece com isto:

Uma captura de tela de um erro de asyncify no terminal

O Asyncify pode listar automaticamente todas as funções C necessárias quando compilado sem ASYNCIFY_ONLY, mas essa detecção automática é excessiva e acaba listando cerca de 70.000 funções C, o que aumenta o tempo de inicialização para 4,5 s. Por isso mantemos a lista manualmente.

Se quiser mais detalhes, veja a issue 251 no GitHub.

Corrigindo falhas do Asyncify

O Pull Request 253 adiciona um comando fix-asyncify que executa uma suíte de testes especializada e adiciona automaticamente à lista ASYNCIFY_ONLY quaisquer funções C em falta identificadas.

Se você encontrar uma falha como a acima, pode corrigi-la assim:

  1. Identificar um caminho de código PHP que dispara a falha — o rastreamento de pilha no terminal deve ajudar.
  2. Adicionar um caso de teste que dispara a falha em packages/php-wasm/node/src/test/php-asyncify.spec.ts
  3. Executar: npm run fix-asyncify
  4. Fazer commit do caso de teste, do Dockerfile atualizado e do PHP.wasm reconstruído

JSPI: a alternativa moderna ao Asyncify

A API JavaScript Promise Integration (JSPI) trata a troca de pilha nativamente no V8, eliminando a necessidade do envolvimento de funções do Asyncify. O WordPress Playground agora distribui builds JSPI junto com builds Asyncify para todas as versões do PHP (7.4–8.5).

Estado atual:

  • O Playground CLI detecta suporte a JSPI automaticamente e o ativa — sem flags manuais
  • Node.js 23+ oferece JSPI nativamente; Node.js 22 exige a flag --experimental-wasm-jspi (tratada automaticamente pelo CLI)
  • Espera-se que Node.js 24+ tenha JSPI sem flag
  • O suporte nos navegadores varia: o JSPI está disponível no Chrome e em navegadores baseados em Chromium atrás de flags

Otimização do tamanho do binário com MAIN_MODULE=2

As builds Asyncify e JSPI são compiladas com a flag MAIN_MODULE=2 do Emscripten, que elimina código morto nos símbolos exportados. Apenas os símbolos de que as extensões dinâmicas realmente precisam são exportados.

Impacto:

  • Tamanho total dos binários reduzido em 122 MB (13,7%)
  • Arquivos .wasm reduzidos em 109 MB (16%)
  • Código cola JavaScript reduzido em 14,5 MB (63%)

Essa otimização vale para todas as versões do PHP (7.4–8.5) nos alvos Node.js e Web. A lista de símbolos exportados é gerenciada centralmente no Dockerfile, com exportações condicionais para extensões específicas (por exemplo, __c_longjmp para Xdebug, _wasm_recv para Memcached).