Donut - chung-leong/zigar GitHub Wiki

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.14.1 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
Need to install the following packages:
[email protected]
Ok to proceed? (y) y
✔ Project name: … donut
✔ Select a framework: › React
✔ Select a variant: › JavaScript + SWC
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 is no longer allowed under the C99 standard. We have to add the C file in build.zig instead so that a compiler flag can be added. That means we need to create a custom build.zig for our project.

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

npx rollup-plugin-zigar custom

This create a copy of the build file used by Zigar. Open the file and add the following:

    mod.addCSourceFile(.{
        .file = .{ .cwd_relative = cfg.module_dir ++ std.fs.path.sep_str ++ "donut.c" },
        .flags = &.{"-std=c89"},
    });

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({ topLevelAwait: false, 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.connect({
      log(s) {
        const newLines = s.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 ]);
      }
    });
    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 the ANSI escape code.

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.

Source code

You can find the complete source code for this example 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.

⚠️ **GitHub.com Fallback** ⚠️