https://antonz.org/in-browser-code-playgrounds/ [logo] Anton Zhiyanov projects blog about [ ] In-browser code playgrounds I'm a big fan of interactive code snippets in all kinds of technical writing, from product docs to online courses to blog posts. Like this one: def greet(name): print(f"Hello, {name}!") greet("World") In fact, I even built an open source tool called Codapi^1 for embedding such snippets. Typically, a code playground consists of a client-side widget and a server-side part that executes the code and returns the result: browser +-------------------------------+ | def greet(name): | | print(f"Hello, {name}!") | | | | greet("World") | +-------------------------------+ Run > | server +-------------------------------+ | docker run codapi/python | | python main.py | +-------------------------------+ | browser +-------------------------------+ | Hello, World! | +-------------------------------+ Personally, I'm quite happy with this setup. But often people prefer not to depend on a server and run the code entirely in the browser. So I decided to look into it and implemented embeddable in-browser code playgrounds for JavaScript, Python, PHP, Ruby, Lua, and SQLite. Running language runtimes in the browser The modern way to run arbitrary programs in the browser seems to be WebAssembly System Interface (WASI^2) -- an executable binary format based on WebAssembly. With WASI, you compile a program (originally written in C, Rust, Go, or some other language) into a WASI binary and then run it with a WASI runtime (there are a number of these runtimes from different vendors). Just as we can compile an arbitrary program into WASI binary, we can take a language interpreter like Lua or CPython, compile it into WASI, and run it with the WASI runtime to execute Lua or Python code. In practice, however, it's not that easy, because WASI compilers do not (yet) implement all the features of traditional compilers like GCC. Fortunately, VMWare Labs has already done the hard part and compiled PHP^3, Python^4 and Ruby^5 into WASI. So all I had to do was publish the WASI binaries as NPM packages to make them available on the CDN. I've also compiled Lua^6 and SQLite^7 to WASI. There is also Kohei Tokunaga's container2wasm initiative^8, which converts arbitrary Docker images into WASI binaries. It looks promising, but it generates 100+ MB binaries for even the smallest Alpine-based images. And since downloading hundreds of megabytes just to read an interactive article is probably not the best idea, this approach is not very practical (yet). Language runtimes compiled into WASI are one part of the equation. The other one is the WASI runtime (the thing that runs the binaries) capable of working in the browser. I chose the Runno^9 runtime by Ben Taylor because it's simple and lightweight (27 KB). The last step was to modify the JavaScript widget^10 to support pluggable engines (WASI is one of them). And that was it! Showcase Here are some interactive code snippets implemented as described above. Note that the language runtime is downloaded when you click the Run button, so the first run may take some time. Subsequent runs are almost instantaneous. Python Executes the code using the Python 3.12 WASI runtime (26.3 MB). def greet(name): print(f"Hello, {name}!") greet("World") PHP Executes the code using the PHP 8.2 WASI runtime (13.2 MB). function greet($name) { echo "Hello, $name!"; } greet("World"); Ruby Executes the code using the Ruby 3.2 WASI runtime (24.5 MB). def greet(name) puts "Hello, #{name}!" end greet("World") Lua Executes the code using the Lua 5.4 WASI runtime (330 KB). function greet(name) print("Hello, " .. name .. "!") end greet("World") JavaScript Executes the code using the AsyncFunction^11. const greet = (name) => { console.log(`Hello, ${name}!`); }; greet("World"); Fetch Executes the code using the Fetch API^12. POST https://httpbingo.org/dump/request content-type: application/json { "message": "hello" } SQLite Executes the code using the SQLite 3.44 WASI runtime (2.1 MB). select id, name, department from employees order by id limit 3; Advanced features Because the WASI runtime plugs into the existing architecture, WASI-powered code snippets support advanced Codapi features such as templates or code cells. Templates^13 allow you to hide some code behind the scenes and show only the relevant part. For example, in the SQLite example above, the employees table is created as part of the template, so the snippet can take it for granted: select id, name, department from employees order by id limit 3; Code cells^14 allow you to make code snippets depend on each other. For example, the first snippet defines the wrap function, while the second snippet uses it: import textwrap def wrap(text, width=20): """Wraps the text so every line is at most width characters long.""" return textwrap.fill(text, width) text = ( "Python is a programming language that lets you work quickly " "and integrate systems more effectively." ) print(wrap(text)) Usage To use native browser playgrounds (e.g. JavaScript or Fetch), include the snippet.js script and add the codapi-snippet element next to the static code example. Use the browser engine:
console.log("hello")
print("hello")
fn main() {
println!("Hello, World!");
}