-
Notifications
You must be signed in to change notification settings - Fork 6
Donut
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.
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 extraOpen 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 AppOpen 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 devWhen 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:

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 AppBecause 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:

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:

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