Skip to main content

Running Drupal on the Edge with WebAssembly

Published on

At DrupalCon Portland, Dries announced Starshot during his State of Drupal presentation. Part of Starshot is the idea that we have Drupal CMS and Core. The big difference is that the Drupal CMS offering comes with standard contributed modules used by almost every existing Drupal build. Then, Dries showed a proposed wireframe for the Drupal.org download page. One of the first things I noticed was the "Launch" in the Drupal CMS section. I wasn't sure about the end goal: 

  • Was the idea to integrate with existing hosting companies like the Try Drupal page?
  • Was Dries expecting Simplytest (via Tugboat) to handle this?
  • Is the Drupal Association going to build bespoke infrastructure to support this?
  • Would we try to leverage WebAssembly (WASM)?

Then Dries said, "Rather than downloading a Drupal CMS, we're going to allow people to launch it right from the browser." (~52:40 in the recording.) 

Wireframe of the Drupal.org download page, taken from Dries's State of Drupal presentation at DrupalCon Portland.

During DrupalCon Portland, attendees were asked to pledge how they would contribute to Starshot. I told Dries I would pull off the "Launch" button with web assembly. 

Try out Drupal from your browser!

Now you can try launching a Drupal core site in your browser—no server is required: https://wasm-drupal.mglaman.dev. Click "Install," and Drupal will be installed in your browser. You can log in using the username admin and password admin. When you revisit, you'll see a "Launch" button to return to your Drupal site. Notes: 

Before diving into the details, you can find the repository on GitHub mglaman/wasm-drupal:

This was only possible with Sean Morris's work and his PHP WASM package. His fork of the original "PHP in Browser" laid the groundwork for my earliest experiments with PHP and WASM and inspired the WordPress Playground. He can accept funding via GitHub Sponsors.

I would also like to thank Acquia. While it wasn't part of my everyday work, I could use my workdays to hack around and experiment. This wouldn't have been possible if I only used my spare time.

Wait, what is WebAssembly?

WebAssembly, known as WASM, is part of the open web platform and allows the execution of compiled code in the browser. I first heard of WebAssembly through the Rust community and Cloudflare executing Rust code via browsers on serverless workers on the edge in browsers. If you need to get more familiar with edge computing, Cloudflare has a great explainer: What is edge computing?

I then heard about it from a family member whose company transitioned from distributing a binary of their C++ application to delivering it through the browser with WASM.

The Emscripten project allows the compiling of C or C++ projects for WASM. Since PHP is written in C, it can be compiled to WASM for execution in the browser.

The nitty gritty details of running Drupal on PHP in Web Assembly

First, thanks to Kevin Quillen for jumping in and working on this with me in an initial sandbox. We were diving into soyuka/php-wasm, seanmorris/php-wasm, and WordPress/wordpress-playground to see what we could build from or learn from. Until recently, it was mostly "We can't reuse any of this and need to learn and build from scratch." That all changed once the 0.0.9@alpha release of seanmorris/php-wasm was released. 

Running Drupal with a PHP runtime in WASM isn't as simple as "compile PHP with Emscripten to WASM." The basic build of PHP WASM allows you to run PHP in the browser for scripts, though. Andy Blum made an interface for trying out PHP date formatting, all running PHP in the browser. This works out great until the script returns CSS and JavaScript assets that need to be returned from the bundled code or have routing for dynamic pages.

That's where we need to leverage a service worker. Service workers can handle outgoing HTTP requests from the browser and allow the normal fetch operation to proceed or provide a custom response. Once I realized this, I looked at the WordPress Playground codebase. I noticed they were also using service workers. However, their codebase is highly complex and made up of many subpackages. However, I noticed that Sean Morris had started working on support for PHP WASM in service workers, which he was calling PHP CGI WASM. 

I could not get seanmorris/php-wasm to build locally, so I could try using his PHP CGI service worker scripts. So, I built off soyuka/php-wasm to build my CGI service worker script. However, I hit one huge problem. Emscripten loads preloaded data using an XMLHttpRequest and does not use fetch. Service workers do not support XMLHttpRequest. 🤯 I built a basic service worker that acted like CGI and rendered a simplistic PHP application. It routes based on the browser's request path and appropriately loads CSS and JavaScript files. 

Here's a summarized version of that code. There are supporting methods that I won't include, but overall, the concept is there.

// Check if the request path has an extension.
if (requestPath.split("/").pop().indexOf(".") !== -1) {
  const extension = requestPath.split(".").pop();
  if (extension !== "php") {
    // Check if the path exists in the filesystem.
    const analyzePath = php.FS.analyzePath(requestPath);
    if (analyzePath.exists && php.FS.isFile(analyzePath.object.mode)) {
      const types = {
        jpeg: "image/jpeg",
        jpg: "image/jpeg",
        gif: "image/gif",
        png: "image/png",
        svg: "image/svg+xml",
        css: "text/css",
      };
      const response = new Response(
        php.FS.readFile(requestPath, { encoding: "binary", url }),
        {}
      );
      if (extension in types) {
        response.headers.append("Content-type", types[extension]);
      }
      // Return the file contents for the response.
      return event.respondWith(response);
    }
  }
  // Doesn't exist, let normal `fetch` take over
  return fetch(event.request);
}

// This is a .php script or path intended for the app to route
this.putEnv(php, 'DOCUMENT_ROOT', this.docroot)
this.putEnv(php, 'DOCUMENT_URI', '/test.php')
this.putEnv(php, 'REQUEST_URI', requestPath);
this.putEnv(php, 'PHP_SELF', '/test.php')
this.putEnv(php, 'SCRIPT_NAME', '/test.php')
this.putEnv(php, 'SCRIPT_FILENAME', '/test.php')

php.ccall("phpw", null, ["string"], ["/test.php"]);
const response = new Response(
  new TextDecoder().decode(new Uint8Array(this.output).buffer),
  {
    headers: {
      'Content-Type': 'text/html; charset=utf-8',
    },
    status: 200,
  }
);
return event.respondWith(response);

The next step was being able to run Drupal. We wanted to add the Zip extension to the PHP WASM build so that Drupal could be extracted into the filesystem provided by Emscripten. This is how the WordPress Playground works. They maintain archives of various WordPress versions and extract them on demand.

As we sorted this out, the 0.0.9@alpha of seanmorris/php-wasm, which included php-cgi-wasm, was released. This release allowed the required PHP extensions to be included and loaded dynamically instead of compiling directly in the PHP WASM. I reached out to Sean in his Discord, and he was extremely helpful in solving some issues where packages were missing files and doing some troubleshooting along the way.

For instance, he warned me that Drupal 10 crashed due to our usage of Fibers for rendering placeholders. One fix is to turn off the Big Pipe module. But I forgot that the renderer service also supported Fibers. This had to be reverted via a local patch to allow Drupal to run. Since the PHP WASM is compiled using Asyncify to support asynchronous code. I hope to help jump in and work with Sean to see how we can support Fibers in PHP WASM. We'll also need a similar fix to handle HTTP requests from Guzzle.

There is a lot to do and a lot of exciting things we can try.

You can learn more at DrupalCon Barcelona.

I will head to DrupalCon Barcelona to discuss running Drupal on the edge with web assembly. There are a lot of things we can do with this, such as exporting the database and codebase for local use with DDEV (#10, #11). It could be possible to export to a hosting provider directly. There may be solutions for clients that allow running local versions of Drupal for authors that connect to a main Drupal site; who knows?!