Writing native NodeJS modules in rust for performance optimization.
Using Rust for writing NodeJS modules
Around 2009 I watched some presentations by Ryan introducing NodeJS and was really impressed by the possibilities. That was one of the reasons, around 2011-2012, I started working on a project that used NodeJS to expose Clutter C++ library ( https://wiki.gnome.org/Projects/Clutter ).
The idea was to integrate Clutter, which allowed us to write code in JS to create near-native UI for Linux. Here is a video ( by CEO of the company I was working for then ) during a JS conference
After years of development in C++ and JS, Rust has become my new go-to language. This article acts as a guide/reference for writing Rust modules for NodeJS in order to achieve better performance where required.
For the sake of article let us assume we need an API that needs to return size of directory on server.
That article assumes basic understanding of NodeJS and Rust.
constexpress=require('express')const{readdir,stat}=require('fs/promises');const{join}=require('path');constapp=express()constport=3000constdirSize=asyncdir=>{constfiles=awaitreaddir(dir,{withFileTypes:true});constpaths=files.map(asyncfile=>{constpath=join(dir,file.name);if(file.isDirectory())returnawaitdirSize(path);if(file.isFile()){const{size}=awaitstat(path);returnsize;}return0;});return(awaitPromise.all(paths)).flat(Infinity).reduce((i,size)=>i+size,0);}app.get('/',async(req,res)=>{console.time('dir');letsize=awaitdirSize('./assets/');console.timeEnd('dir');res.send(size)})app.listen(port,()=>{console.log(`App listening on port ${port}`)})
The test was performed on the assets folder having 40 directories with 1000 files each. On Macbook Pro 2017 ( i7, 16 GB Ram ) it takes about 591ms for above API to give response. The dirSize function reads the directory and uses stat to compute the size of the file in recursive manner. We will try to replicate the same in Rust.
To use Rust in our API we have two options.
Compile a function as dylib and use ffi to load the dynamic library.
node-ffi is a Node.js addon for loading and calling dynamic libraries using pure JavaScript. It can be used to create bindings to native libraries without writing any C++ code. : From Github Readme of Node-FFI
Integrate with NodeJS bindings directly.
We will use the second option since using ffi might be a little slow and we already have support of awesome libraries like node-bindgen ( which automatically generates a lot of binding related code for us )
constexpress=require("express");const{readdir,stat}=require("fs/promises");const{join}=require("path");constextramodule=require("./dist")// import our custom module
constapp=express();constport=3000;constdirSize=async(dir)=>{constfiles=awaitreaddir(dir,{withFileTypes:true});constpaths=files.map(async(file)=>{constpath=join(dir,file.name);if(file.isDirectory())returnawaitdirSize(path);if(file.isFile()){const{size}=awaitstat(path);returnsize;}return0;});return(awaitPromise.all(paths)).flat(Infinity).reduce((i,size)=>i+size,0);};app.get("/v1",async(req,res)=>{console.time("dir");letsize=awaitdirSize("./assets/");console.timeEnd("dir");res.json(size);});app.get("/v2",async(req,res)=>{console.time("dir");letsize=awaitextramodule.size("./assets/");console.timeEnd("dir");res.json({size:Number(size)});});app.listen(port,()=>{console.log(`App listening on port ${port}`);});