Skip to content
Chung Leong edited this page Dec 5, 2025 · 11 revisions

This is another example demonstrating how you can compile C code for running in a web browser. Like our earlier effort, the program in question serves no practical purpose. We're going to use C code that's in the shape of a donut to draw a donut. It's a neat example that has generated a bit of buzz on the Internet in recent years.

This example makes use of WebAssembly threads. As support in Zig 0.15.2 is still immature, you'll need to patch the standard library before continuing.

Creating the app

We begin by creating the basic app skeleton:

npm create vite@latest
│
◇  Project name:
│  donut
│
◇  Select a framework:
│  React
│
◇  Select a variant:
│  JavaScript + SWC
│
◇  Use rolldown-vite (Experimental)?:
│  No
│
◇  Install with npm and start now?
│  No
│
◇  Scaffolding project in /home/rwiggum/donut...
│
└  Done. Now run:

  cd donut
  npm install
  npm run dev
cd donut
npm install
npm install --save-dev rollup-plugin-zigar
mkdir zig 

Then copy and paste the following code into zig/donut.c:

             k;double sin()
         ,cos();main(){float A=
       0,B=0,i,j,z[1760];char b[
     1760];printf("\x1b[2J");for(;;
  ){memset(b,32,1760);memset(z,0,7040)
  ;for(j=0;6.28>j;j+=0.07)for(i=0;6.28
 >i;i+=0.02){float c=sin(i),d=cos(j),e=
 sin(A),f=sin(j),g=cos(A),h=d+2,D=1/(c*
 h*e+f*g+5),l=cos      (i),m=cos(B),n=s\
in(B),t=c*h*g-f*        e;int x=40+30*D*
(l*h*m-t*n),y=            12+15*D*(l*h*n
+t*m),o=x+80*y,          N=8*((f*e-c*d*g
 )*m-c*d*e-f*g-l        *d*n);if(22>y&&
 y>0&&x>0&&80>x&&D>z[o]){z[o]=D;;;b[o]=
 ".,-~:;=!*#$@"[N>0?N:0];}}/*#****!!-*/
  printf("\x1b[H");for(k=0;1761>k;k++)
   putchar(k%80?b[k]:10);A+=0.04;B+=
     0.02;}}/*****####*******!!=;:~
       ~::==!!!**********!!!==::-
         .,~~;;;========;;;:~-.
             ..,--------,*/

The above code comes from this blog post, which also provides a detailed explanation on how it works.

Unlike the Cowsay example, here we cannot use @cImport() to bring in main(). The donut code relies on implicit declarations, which are no longer allowed under the C99 standard. We have to add the C file in build.extra.zig instead so that a compiler flag can be added.

While inside the zig sub-directory, run the following command:

npx zigar extra

Open the file and add the necessary call in getCSourceFiles():

const std = @import("std");

const cfg = @import("build.cfg.zig");

pub fn getCSourceFiles(_: *std.Build, args: anytype) []const []const u8 {
    args.module.addCSourceFile(.{
        .file = .{ .cwd_relative = cfg.module_dir ++ "donut.c" },
        .flags = &.{"-std=c89"},
    });
    return &.{};
}

Then create donut.zig:

const std = @import("std");
const allocator = std.heap.wasm_allocator;

extern fn main(c_int, [*c][*c]u8) c_int;

fn run() void {
    _ = main(0, null);
}

pub fn spawn() !void {
    const thread = try std.Thread.spawn(.{
        .allocator = allocator,
        .stack_size = 256 * 1024,
    }, run, .{});
    thread.detach();
}

Next, configure rollup-plugin-zigar in vite.config.js:

import { defineConfig } from 'vite'
import react from '@vitejs/plugin-react-swc'
import zigar from 'rollup-plugin-zigar';

// https://vite.dev/config/
export default defineConfig({
  plugins: [
    react(),
    zigar({ useLibc: true, multithreaded: true }),
  ],
  server: {
    headers: {
      'Cross-Origin-Opener-Policy': 'same-origin',
      'Cross-Origin-Embedder-Policy': 'require-corp',
    }
  },
})

Open src/App.jsx. Get rid of all the boilerplate stuff and add a useEffect() hook that invoke spawn():

import { useEffect } from 'react'
import { spawn } from '../zig/donut.zig';
import './App.css'

function App() {
  useEffect(() => {
    spawn();
  }, []);
  return (
    <>
    </>
  )
}

export default App

Open main.jsx and get rid of <StrictMode> so spawn() isn't called twice:

createRoot(document.getElementById('root')).render(
  <App />
)

Now launch Vite in dev mode:

npm run dev

When you open the page, the document will be empty. When you open the development console, you should see ASCII drawings of a donut rapidly flashing by:

Dev console

At this stage, it's just a matter of redirecting the console so that we draw into the DOM instead:

import { useEffect, useState } from 'react'
import { __zigar, spawn } from '../zig/donut.zig';
import './App.css'

function App() {
  const [ lines, setLines ] = useState([]);
  useEffect(() => {
    const lines = [];
    let r = 0;    __zigar.on('log', ({ source, message }) => {
      if (source === 'stdout') {
        const newLines = message.split('\n');
        for (let line of newLines) {
          if (line.startsWith('\x1b[2J')) {
            line = line.slice(4);
          }
          if (line.startsWith('\x1b[H')) {
            r = 0;
            line = line.slice(3);
          }
          if (line) {
            lines[r++] = line;
          }
        }
        setLines([ ...lines ]);
        return true;
      }
    });
    spawn();
  }, []);
  return (
    <>
      <div className="display">
        {lines.map((s, i) => <div key={i}>{s}</div>)}
      </div>
    </>
  )
}

export default App

Because we won't receive the whole donut at once, we need to preserve lines written earlier in a variable. We also need to deal with ANSI escape codes.

And we need a bit of new CSS in App.css:

.display {
  text-align: left;
  font-family: monospace;
  white-space: pre;
  font-weight: bold;
  line-height: 120%;
  color: orange;
  background-color: black;
}

After a page refresh, you should see the following:

Donut

Yeh, we got ourselves a donut!

As a final touch, let us add a title to our page:

      <h1 id="title">
        <span id="homer">~(_8^(I)</span>
        Mmm, donut
      </h1>
#title {
  position: relative;
}

#homer {
  position: absolute;
  transform: rotate(90deg);
  left: 0;
  top: -1em;
}

And we're done:

Donut

Source code

You can find the complete source code for this example here.

You can see the code in action here.

Conclusion

Admittedly, the code employed in this example is rather friviolous. I hope you found it useful none the less. There're scenarios where you might want to run C programs in the the browser. If you're building an online programming course, for instance.

Clone this wiki locally