A Little Experiment With C In Node.js

A Little Experiment With C In Node.js

Can you run a C program in Node.js?

Node.js is a JavaScript runtime that provides a good developer experience. The runtime has been in existence for a substantial time period; JavaScript syntax has become good with ES6; there are large communities around the ecosystem; and it provides sufficient performance. It pays a lot of people's bills.

However, it is not as fast as language runtimes such as that of C, C++, Go etc. So, if you ever have done/seen a benchmark test of Node.js v. the languages mentioned above crushes in performance compared to Node.js. That is not surprising; compiled languages beat interpreted (or in Node.js's case just-in-time compiled) languages.

But did you know that you can write programs in C/C++ and run it with Node.js?. By default, you can write C++ addons to use with Node.js. But I tried a little experiment based on this answer from Stack Overflow to run a C-based binary. I really like programming in JavaScript. And I really liked C (haven't done much in it though). I think JavaScript syntax for the most parts resembles C. I like C's simplicity.

I used the 'child process' module as mentioned in the answer to run the C-based executable using Node.js. However, for the C program I had written (O(n3)) the exec() method as mentioned in the answer did not work as there were some buffer issues. So, I used the spawn() method instead.

I wrote a three layered nested loop in C and JavaScript.

oN3Loop.c

#include<stdio.h>
void main() {
  int i, j, k;
  for(i = 0; i < 100; i++) {
    for(j = 0; j < 100; j++) {
      for(k = 0; k < 100; k++) {
        printf("i: %d, j: %d, k: %d\n", i, j, k);
      }
    }
  }
}

oN3Loop.js

let i, j, k;
for(i = 0; i < 100; i++) {
  for(j = 0; j < 100; j++) {
    for(k = 0; k < 100; k++) {
      console.log(`i: ${i}, j: ${j}, k: ${k}`);
    }
  }
}

I compiled the C program using the make tool:

$ make oN3Loop

Then I ran the C binary and then the .js file using node; timing both of them using the time command.

$ time ./oN3Loop

$ time node oN3Loop.js

It displayed the following results:

TimeCNode
real0m1.787s0m4.998s
user0m0.264s0m3.415s
sys0m0.819s0m0.917s

Then I ran the .js file which executes the C compiled binary using the following code with the time command.

oN3LoopNode.js

const { spawn } = require("child_process");
let loop = spawn("./oN3Loop");

loop.stdout.on('data', (data) => {
  console.log(`stdout: ${data}`);
});

loop.stderr.on('data', (data) => {
  console.error(`stderr: ${data}`);
});

loop.on('close', (code) => {
  console.log(`child process exited with code ${code}`);
});

I got the above script from the Node.js documentation here.

I then ran the above file using the time command:

$ time node ./oN3LoopNode.js

Any guess on how much time it took to run the binary using the "child process" module?

TimeCNodeC/Node
real0m1.787s0m4.998s0m1.047s
user0m0.264s0m3.415s0m0.151s
sys0m0.819s0m0.917s0m0.244s

Isn't it great? I think it is.

This gives you the ability to transfer a some compute intensive task that you need to pass off away from your Node application to a C/C++. This is work with any binary; for example a program written in Go.

However, when I had posted about the same in Reddit, I've been advised not to use this frequently as the developer experience of using this feature is not so great. I agree with this (even if I wish it were not true). And the other thing is that you won't be needing this frequently. The large number of compute intensive tasks that you write with Node may not be that high. Even it is you may not be using this on a daily basis. Adding to this, there are other factors to consider such as project deadline (if it is a business situation), developer time, etc.

What do you think?