Hello world (Node) - chung-leong/zigar GitHub Wiki
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
A message will appear informing you that the Node API addon is being built. This will be followed by the module "hello" itself. After 30 seconds or so, the following should appear in the terminal:
Hello world!
Using the CommonJS version
In your text editor, create a 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 requiring 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
✓ Built module "hello" (linux/x64)
✓ Built module "hello" (linux/arm64)
✓ Built module "hello" (linux-musl/x64)
✓ Built module "hello" (linux-musl/arm64)
✓ Built Node.js addon (linux/x64)
✓ Built Node.js addon (linux/arm64)
✓ Built Node.js addon (linux-musl/x64)
✓ Built Node.js addon (linux-musl/arm64)
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.
Source dode
You can find the complete source code for this example here.
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.