Skip to content
Chung Leong edited this page May 31, 2024 · 4 revisions

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.

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: … 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: Development console

If you click the button again, you'll notice that the top of the speech bubble is too long:

Development console

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:

Development console

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:

React app

Conclusion

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!

React app

Clone this wiki locally