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