Skip to content

Hello world (Node)

Chung Leong edited this page Apr 25, 2024 · 7 revisions

In this example we're going to create a very simple server-side app that outputs "Hello world!" to the console through Zig. It demonstrates the basics of working with node-zigar and the steps for deploying an app in different server environments.

Creating the app

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

mkdir hello
cd hello
npm init -y

Next, we install node-zigar:

npm install node-zigar

Then we create one directory for JavaScript files and another for Zig files:

mkdir src zig

In a text editor, we create hello.zig:

const std = @import("std");

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

Followed by index.js:

import { hello } from '../zig/hello.zig';

hello();

In package.json, we activate ESM and add a script entry for running our app:

  "type": "module",
  "scripts": {
    "start": "node --loader=node-zigar --no-warnings src/index.js"
  },

Then we run it:

npm run start

Nothing will happen for some time as the Zig compiler compiles the Zig file into native code. The native addon used by node-zigar is also compiled at this point. After 30 seconds or so, the following should appear in the terminal:

Hello world!

Using the CommonJS version

In your text editor, create the new file index.cjs:

require('node-zigar/cjs');
const { hello } = require('../zig/hello.zig');
hello();

Add an script entry for running it:

  "scripts": {
    "start": "node --loader=node-zigar --no-warnings src/index.js",
    "start:cjs": "node src/index.cjs"
  },

And run it:

npm run start:cjs

The CommonJS version of node-zigar has the advantage of not needing command-line flags. For this reason, you might choose to use it even in a ESM project.

Create another file called index.async.js:

const { createRequire } = await import('node-zigar/cjs');
const { hello } = createRequire(import.meta.url)('../zig/hello.zig');
hello();

Add another script entry:

  "scripts": {
    "start": "node --loader=node-zigar --no-warnings src/index.js",
    "start:cjs": "node src/index.cjs",
    "start:async": "node src/index.async.js"
  },

And run it:

npm run start:async

Configuring the app for deployment

When you use import or require on a Zig file, node-zigar will save the resulting shared library file to a temporary location. At the root level of the app you will notice a zigar-cache sub-directory with the following structure:

📁 zigar-cache
  📁 zig-6c2c904
    📁 Debug
      📁 hello.zigar
        📑 linux.x64.so
      📁 node-zigar-addon
        📑 linux.x64.node

hello.zigar is a node-zigar module. It's a directory containing dynamic-link libraries for different platforms. node-zigar-addon is the Node.js native addon used to load node-zigar modules. It too comes in platform-specific versions.

The files in the cache directory aren't ones we want delivered to end-users. They're compiled at the Debug level and are therefore large and slow. Moreover, they only cover the platform we're using for development (Linux in this case), which probably does not match the eventual server environment.

To prepare our app for deployment, we first change the import statement so that it references a .zigar instead, stored at a more permanent location:

import { hello } from '../lib/hello.zigar';

hello();

We then create a configure file for node-zigar with the help of its CLI script:

npx node-zigar init

node-zigar.config.json will be populated with some default options:

{
  "optimize": "ReleaseSmall",
  "sourceFiles": {},
  "targets": [
    {
      "platform": "linux",
      "arch": "x64"
    }
  ]
}

sourceFiles maps .zigar modules to source files. Paths are relative to the config file.

optimize can be Debug, ReleaseSafe, ReleaseSmall, or ReleaseFast.

targets is a list of cross-compile targets. platform and arch can be one of the possible values returned by os.platform and os.arch.

We insert the following into our config file:

{
  "optimize": "ReleaseSmall",
  "sourceFiles": {
    "lib/hello.zigar": "zig/hello.zig"
  },
  "targets": [
    { "platform": "linux", "arch": "x64" },
    { "platform": "linux", "arch": "arm64" },
    { "platform": "linux-musl", "arch": "x64" },
    { "platform": "linux-musl", "arch": "arm64" }
  ]
}

Then we ask node-zigar to create the necessary library files:

npx node-zigar build
Building hello.zigar:
  linux.x64.so
  linux.arm64.so
  linux-musl.x64.so
  linux-musl.arm64.so
Building node-zigar-addon:
  linux.x64.node
  linux.arm64.node
  linux-musl.x64.node
  linux-musl.arm64.node

Run npm run start again to confirm that the configuration is correct.

Inclusion of linux-musl in the configuration above enables our app to work in a Linux environment where glibc is absent like Alpine. If you have Docker installed, you can verify that it does indeed work with the following command:

docker run --rm -v ./:/test -w /test node:alpine npm run start

When you upload the app to a server, omit files in the zig directory and node-zigar.config.json so that node-zigar will never attempt recompilation.

Conclusion

You have just learned the basic of using node-zigar in server-side applications. The app we created doesn't do much. In the next example we're going to create an app that actually does something useful in Zig.


SHA1 digest example