Skip to content

SOP: Build Your First Module (cpu-count)

Fresh 🌱

Goal: Build a real native module that returns the number of processors on the current machine. Along the way you will scaffold a project, add a Rust crate dependency, implement a function, and run it from Node.

Done when: require('.').get() returns a number from the Node console.

Step 1 - Create a new project

shell
npm init neon cpu-count

This asks a short series of questions (similar to npm init) and produces a cpu-count directory:

text
cpu-count/
├── Cargo.toml
├── README.md
├── package.json
└── src
    └── lib.rs

The key thing to notice: a Neon project is both a Node package and a Rust crate. Rust source lives in src/, and JavaScript that augments Rust can live side by side.

You can target a specific Node version by adjusting the napi feature in Cargo.toml. By default, npm init neon uses your currently installed Node version:

toml
[dependencies.neon]
features = ["napi-6"]

Step 2 - Build and run the starter

Even before writing any code, confirm the scaffold builds:

shell
cd cpu-count
npm install
npm run build

The build generates two artifacts:

  • target/ - the Rust build directory
  • index.node - the compiled Neon module

Run it:

shell
node
> require('.').hello()
hello node

Clean up build artifacts

To remove Rust build output, run cargo clean.

Step 3 - Add a Rust dependency

We will lean on the Rust crate ecosystem instead of writing CPU-detection code ourselves. In Cargo.toml, add a dependency on the num_cpus crate:

toml
[dependencies]
num_cpus = "1"

This tells Cargo to fetch a version semver-compatible with 1. (The package.json equivalent would be "num_cpus": "^1".)

Step 4 - Implement the function

Replace the sample hello function in src/lib.rs. Our function returns a JavaScript number, so we use the cx.number() helper. Because cx.number() expects an f64 and num_cpus::get() returns a usize, we cast with Rust's as operator:

rust
use neon::prelude::*;

fn get_num_cpus(mut cx: FunctionContext) -> JsResult<JsNumber> {
    Ok(cx.number(num_cpus::get() as f64))
}

Three things to understand about this signature:

  • cx: FunctionContext carries information about the call: the arguments and the value of this.
  • JsResult<JsNumber> is a Rust Result that is either Ok (returned a value) or Err (threw a JavaScript exception). It also tracks the lifetime of the returned handle.
  • cx.number() registers the value with the JavaScript garbage collector so it stays alive long enough to be returned to the caller.

Now update the module entry point to export get_num_cpus as "get":

rust
#[neon::main]
fn main(mut cx: ModuleContext) -> NeonResult<()> {
    cx.export_function("get", get_num_cpus)?;
    Ok(())
}

#[neon::main] marks the function Neon runs when the module is first loaded. It creates a JavaScript function backed by get_num_cpus and exports it under the property name "get".

Step 5 - Build a release and try it

shell
npm run build -- --release

A release build takes longer to compile but runs faster. Test it from the Node console at the project root:

shell
node
> const cpuCount = require('.')
> cpuCount.get()
4

The number will vary by machine - that is the whole point.

Checklist

  • [ ] npm init neon cpu-count scaffolded the project
  • [ ] Starter built and require('.').hello() returned hello node
  • [ ] num_cpus = "1" added to [dependencies] in Cargo.toml
  • [ ] get_num_cpus implemented and exported as "get"
  • [ ] npm run build -- --release succeeded
  • [ ] require('.').get() returns your CPU count

Debug vs release

npm run build produces a fast-to-compile debug binary. npm run build -- --release produces an optimized binary. Use debug while iterating, release when measuring performance or shipping.

Next: learn the full export surface in Export Functions to JavaScript.