-
Notifications
You must be signed in to change notification settings - Fork 3
Cowsay
This example demonstrates how you can use code written in C in your JavaScript project. The code is going to be the famous cowsay program. We'll make it run in a React 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: … cowsay
✔ Select a framework: › React
✔ Select a variant: › JavaScript + SWC
cd cowsay
npm install
npm install --save-dev rollup-plugin-zigar
mkdir zig
Next, we download the C code in question from GitHub. The project was the top result when I googled "Cowsay C source code". I suspect that it's someone's homework assignment. The actual cowsay program that comes with Linux is written in Perl so there's no real C source code for it.
Save cowsay.c
to the zig
sub-directory so that our Zig code can import it. Our goal is to run
it without any modification. That means we'll be importing the C main
function. Here's
cowsay.zig
:
const c = @cImport({
@cInclude("cowsay.c");
});
pub fn cowsay(args: [][*:0]const u8) void {
_ = c.main(@intCast(args.len), @ptrCast(args));
}
cowsay()
basically translates a safe Zig slice pointer into an unsafe C pointer plus a length and
passes them to main()
as argv
and argc
.
In app.jsx
, we're going to add an import statement:
import { cowsay } from '../zig/cowsay.zig';
And change the onclick
handler of the boilerplate app's button so it calls our function:
<button onClick={() => cowsay([ 'cowsay', 'Hello' ])}>
We're putting a dummy argument in front of our message here since main()
expects argv[0]
to be
the program path.
Before we can start the app we need to configure rollup-plugin-zigar in vite.config.js
:
import react from '@vitejs/plugin-react-swc';
import zigar from 'rollup-plugin-zigar';
import { defineConfig } from 'vite';
// https://vitejs.dev/config/
export default defineConfig({
plugins: [react(), zigar({ topLevelAwait: false, useLibc: true })],
})
Once that's done we're ready:
npm run dev
Open the on-screen link and wait for the app to become accessible. When the UI appears, open the development console (Control-shift-J) so you can see the console output. Now click the button. A cow should appear:
If you click the button again, you'll notice that the top of the speech bubble is too long:
This glitch is due to the program not reinitializing argscharcount
to zero at the beginning
of main()
. It just keeps printing more and more dashes as a result. To fix that we need to
add a line of code to the C file:
int main(int argc, char *argv[]) {
argscharcount = 0;
After this minor change our app will behave as it should:
Since dumping the same text to the console isn't very functional, let us create a proper user
interface. Replace the code in App.jsx
with the following:
import { useCallback, useState } from 'react';
import { __zigar, cowsay } from '../zig/cowsay.zig';
import './App.css';
function App() {
const [text, setText] = useState('');
const [output, setOutput] = useState('');
const onChange = useCallback((e) => {
const { value } = e.target;
setText(value);
const args = value.split(/[ \t]+/);
const lines = [];
__zigar.connect({ log: line => lines.push(line) });
cowsay([ 'cowsay', ...args ]);
setOutput(lines.join('\n'));
}, []);
return (
<>
<h1>Vite + React + Cow</h1>
<div className="card">
<input className="input" value={text} onChange={onChange}/>
</div>
<div className="card">
<textarea class="output" value={output} readOnly />
</div>
</>
)
}
export default App
__zigar
contains utility functions provided by Zigar. connect()
redirects output to stdout
and stdin
to a different console
object. It allows us to
capture the cow and place it in a <textarea>
.
We'll also need to add a couple classes to App.css
:
.input {
font-family: 'Courier New', Courier, monospace;
width: 100%;
}
.output {
font-family: 'Courier New', Courier, monospace;
width: 100%;
height: 12em;
white-space: pre;
}
And here's the result:
This example is rather silly, but it does shows how easy it is to use C code in a web project. It also shows how you can make use of the Zig compiler today. While the Zig language itself is under heavy development, the part of the Zig compiler that handles C integration is based on battle-tested components like LLVM. You're not really dealing with something experimental when you employ the compiler for this purpose. It's also unlikely that interface code written in Zig would become obsolete in the future, since the type system is firmly established at this point. In the example, the role of Zig is limited to providing information missing from C pointers, namely the extent of the memory regions they point to. That's the key ingredient that permits seamless integration with JavaScript.
The source code we used for this example is actually not feature-complete: there is no support for cow files. In a future example we'll implement that. It's going to show how you can provide file access to a C program through a custom WASI interface. Stay tuned!