Appearance
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-countThis 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.rsThe 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 buildThe build generates two artifacts:
target/- the Rust build directoryindex.node- the compiled Neon module
Run it:
shell
node
> require('.').hello()
hello nodeClean 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: FunctionContextcarries information about the call: the arguments and the value ofthis.JsResult<JsNumber>is a RustResultthat is eitherOk(returned a value) orErr(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 -- --releaseA 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()
4The number will vary by machine - that is the whole point.
Checklist
- [ ]
npm init neon cpu-countscaffolded the project - [ ] Starter built and
require('.').hello()returnedhello node - [ ]
num_cpus = "1"added to[dependencies]inCargo.toml - [ ]
get_num_cpusimplemented and exported as"get" - [ ]
npm run build -- --releasesucceeded - [ ]
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.