Skip to content

Hello world (Rollup)

Chung Leong edited this page Jun 2, 2024 · 8 revisions

In this example we're going to export a simple function written in Zig into a JavaScript library. It demonstrates the basics of using rollup-plugin-zigar.

Creating project

We'll first initialize the project. Open a terminal window and run the following commands:

mkdir hello
cd hello
npm init -y

Next, we'll install the necessary npm modules, including rollup-plugin-zigar:

npm install --save-dev rollup rollup-plugin-zigar @rollup/plugin-node-resolve

Then we create a directory for zig files:

mkdir zig

And add hello.zig:

const std = @import("std");

pub fn hello() void {
    std.debug.print("Hello world!", .{});
}

We populate rollup.config.js with the following:

import nodeResolve from '@rollup/plugin-node-resolve';
import zigar from 'rollup-plugin-zigar';

const input = './zig/hello.zig';

export default [
  {
    input,
    plugins: [
      zigar({ optimize: 'ReleaseSmall' }),
      nodeResolve(),
    ],
    output: {
      file: './dist/index.js',
      format: 'esm',
    },
  },
];

And proceed to create the library:

npx rollup -c rollup.config.js

Compilation will takes some a while the first time. When it finishes, we starts Node in interactive mode and try to use our function:

node
Welcome to Node.js v20.10.0.
Type ".help" for more information.
> const { hello } = await import('./dist/index.js');

Before we managed to call it we'd run into an error:

Uncaught TypeError: fetch failed
    at Object.fetch (node:internal/deps/undici/undici:11730:11)
    at async WebAssemblyEnvironment.instantiateWebAssembly (file:///.../hello/dist/index.js:2414:22)
    at async file:///.../hello/dist/index.js:2421:28 {
  cause: Error: not implemented... yet...```

By default, rollup-plugin-zigar tries to load WASM using the fetch API. Since fetch is incapable of fetching local files (or is absent altogether) in Node, that does not work. To fix this, we can either tell rollup-plugin-zigar to use readFile whenever Node is detected:

  Zigar({
    optimize: 'ReleaseSmall',
    useReadFile: true,  // <---
  }),

Or we can embed the WASM bytecodes in our JavaScript file (as Base64 text) so there is no loading at all:

  Zigar({
    optimize: 'ReleaseSmall',
    embedWASM: true,  // <---
  }),

After making either change the generated code will work in Node:

Welcome to Node.js v20.10.0.
Type ".help" for more information.
> const { hello } = await import('./dist/index.js');
undefined
> hello();
Hello world!
undefined
>

Creating CommonJS library

With the help of copy-and-paste, add a new entry to our configuration so that a CommonJS file gets created also:

    input,
    plugins: [
      zigar({
        optimize: 'ReleaseSmall',
        embedWASM: true,
        topLevelAwait: false,  // <---
      }),
      nodeResolve(),
    ],
    output: {
      file: './dist/index.cjs',
      format: 'cjs',  // <---
    },

Because top-level await is only supported by ESM, we must disable the feature here.

Our function should work the same when we import it using require:

Welcome to Node.js v20.10.0.
Type ".help" for more information.
> const { hello } = require('./dist/index.cjs');
undefined
> hello();
Hello world!
undefined

Without top-level await, however, the function can be asynchronous for a brief moment as the computer compile the WASM bytecodes. You can see this if you combine the two lines of code above into a single a line:

Welcome to Node.js v20.10.0.
Type ".help" for more information.
> const { hello } = require('./dist/index.cjs'); hello();
Promise {
  <pending>,
  [Symbol(async_id_symbol)]: 290,
  [Symbol(trigger_async_id_symbol)]: 288
}
> Hello world!

You can deal with this unavoidable shortcoming in two ways. Either always use await when calling functions imported in this manner:

Welcome to Node.js v20.10.0.
Type ".help" for more information.
> const { hello } = require('./dist/index.cjs'); await hello();
Hello world!
undefined

Or perform await on the init function from the special exports __zigar somewhere in your code prior to anything happening:

Welcome to Node.js v20.10.0.
Type ".help" for more information.
> const { hello, __zigar } = require('./dist/index.cjs'); await __zigar.init(); hello();
Hello world!
undefined

Creating UMD library

To create a UMD-compatible JavaScript file, which can be loaded using a <script> tag, copy-and-paste the settings from the previous section and add an additional entry:

    input,
    plugins: [
      zigar({
        optimize: 'ReleaseSmall',
        embedWASM: true,
        topLevelAwait: false,
      }),
      nodeResolve(),
    ],
    output: {
      file: './dist/index.umd.cjs',
      format: 'umd',  // <---
      name: 'Hello',  // <---
    },

After building the library once more, you can test it using the following HTML code:

<!DOCTYPE html>
<html>
  <head>
    <script src="dist/index.umd.cjs"></script>
    <script>
      window.onclick = () => Hello.hello();
    </script>
  </head>
  <body>
    <h1>Click me!</h1>
  </body>
</html>

Source dode

You can find the complete source code for this example here.

Conclusion

You have just learned how you can easily create a JavaScript library powered by WebAssembly with the help of Zig and Zigar. The library we built in this example doesn't do anything. Our code will do something more useful in the next example.


SHA1 digest example

Clone this wiki locally