Nodejs C++/JS Boundary: Crossing The Rubicon

--

image from pixabay

Most articles about Nodejs internals always talk about the C++/JS boundary and crossing it sorta thing. But most don’t usually go in-depth to explain what crossing the C++/JS boundary really meant and what crossing it is all about.

In this article, we will take an in-depth look at the C++/JS boundary to know what crossing it entails.

What we will gain from this article: Node.js powers over a million startups and companies. It is the most used backend framework. We use it every day. Developers with Node.js skill are in high demand, so learning how Node.js works in-depth will go a long way to broaden our horizon on Nodejs and be confident when building Nodejs apps.

Tip: When Building Node.js apps, you can share and manage common code at scale using Bit. Instead of duplicating code, just share and sync it. Try it out.

C++/JS: The Boundary between two worlds

It all starts with the compiler.

First, what is a compiler?

A compiler is a program that translates a piece of code into machine code.

Great!! into machine code. Remember that.

Compilers are complex programs, they are broken down into parts, each part has a specific job.

source code
|
v
lexical analyzer
|
v
parser
|
v
code generation

lexical analyzer: This generates tokens from the source code.

parser: This generates AST (Abstract Syntax Tree) tree from the tokens.

code generation: This generates machine/assembly code from the AST.

Most compilers generate the assembly equivalent of the source code and leave the assembler to mash everything up into a binary soup.

Nodejs uses v8 JS engine to compile and execute JS code. I hope, we know what v8 is.

if we don’t, v8 is an open-source high-performance JavaScript compiler from Google.

Whenever we run a js file in Node.js like this:

node script.js

Nodejs passes the script to v8 using its APIs. v8 compiles the JS code in the provided script (script.js) and returns the assembly equivalent. Node.js then uses another v8 API to run the generated assembly code.

The compiler compiles the code to assembly and copies it to memory.

Looking at the above image, the JS code is compiled to assembly code.

To run the compiled code, it allocates a space on the memory, moves the code to the allocated space and jumps to the memory space.

At the jump, execution now begins from the compiled code. Hence, it has crossed a boundary.

The code being executed now isn’t a C++ code but JS code. All are now in assembly.

If the compiled JS code execution ends, it jumps back to the C++ code.

The C++ code and JS code here doesn’t mean the C++ source code or the JS source code. No. It is the assembly code generated from their source codes by the compiler is what is being executed. You can say C++ code and JS code to differentiate which assembly code being run.

Call from JS to C++

Functions in C++ source code can be called from a JS code.

Example:

// script.js
let f = 90
function send() {
var f= 100
}
send()
sendMe()

In this code, we defined send function but there is no sendMe function.

The sendMe function will be defined in our C++ app:

// v8_demo.cpp
include "v8.h";
void sendMe() {
cout << "Greetings from C++ land";
}
int main() {
Maybe<Local> result = v8::Script::Compile('script.js');
result->Run();
}

Note: the above snippets won’t run. Its just for demo purposes.

Here, we have the sendMe function.

See what happens. On execution, our v8_demo.cpp runs like this:

MEMORY
; v8_demo.cpp
x00 sendMe:
x01 push ""Greetings from C++ land""
x02 call cout
x03 ret
x04
x05 main:
x06 push "script.js"
x07 call Compile
x08 push eax
x09 call Run
x10
x11
x12
x13
x14
x15
x16
x17
x18
x19
x20

see the sendMe function is present. Execution starts from main and proceeds downwards. On x07, the script.js is compiled and pushed to memory.

MEMORY
; v8_demo.cpp
x00 sendMe:
x01 push ""Greetings from C++ land""
x02 call cout
x03 ret
x04
x05 main:
x06 push "script.js"
x07 ➥ call Compile
x08 push eax
x09 call Run
x10
x11 ;script.js
x12 mov 90,f
x13 send:
x14 push 100
x15 call send
x16 call sendMe
x17
x18
x19
x20

See our script.js assembly code in memory. :) Here, there is no C++ or JS code. Everything is in assembly. Like I said earlier, we can just demarcate like, here is assembly code from the C++ script and here is assembly code from JS script.

So, when Run x09 is executed,

MEMORY
; v8_demo.cpp
x00 sendMe:
x01 push ""Greetings from C++ land""
x02 call cout
x03 ret
x04
x05 main:
x06 push "script.js"
x07 call Compile
x08 push eax
x09 ➥ call Run
x10
x11 ;script.js
x12 mov 90,f
x13 send:
x14 push 100
x15 call send
x16 call sendMe
x17
x18
x19
x20

it jumps to the JS assembly code memory space x12.

Note: Run is a v8 API to run the compiled JS code after compilation.

MEMORY
; v8_demo.cpp
x00 sendMe:
x01 push ""Greetings from C++ land""
x02 call cout
x03 ret
x04
x05 main:
x06 push "script.js"
x07 call Compile
x08 push eax
x09 call Run
x10
x11 ;script.js
x12 ➥ mov 90,f
x13 send:
x14 push 100
x15 call send
x16 call sendMe
x17
x18
x19
x20

Here, the code we wrote in script.js is executed in assembly form.

When execution reach x15 call send, it jumps to x13 send: and executes its block, after that, it returns and calls sendMe.

MEMORY
; v8_demo.cpp
x00 sendMe:
x01 push ""Greetings from C++ land""
x02 call cout
x03 ret
x04
x05 main:
x06 push "script.js"
x07 call Compile
x08 push eax
x09 call Run
x10
x11 ;script.js
x12 mov 90,f
x13 send:
x14 push 100
x15 call send
x16 ➥ call sendMe
x17
x18
x19
x20

This function wasn’t defined in our script.js, but because it was defined in v8_demo.cpp, it is now available in memory. It can be called because the assembly form of script.js and v8_demo.cpp, now executes in one language and in the same memory space.

So, execution jumps to x00 sendMe:, the start of the sendMe function.

MEMORY
; v8_demo.cpp
x00 ➥ sendMe:
x01 push ""Greetings from C++ land""
x02 call cout
x03 ret
x04
x05 main:
x06 push "script.js"
x07 call Compile
x08 push eax
x09 call Run
x10
x11 ;script.js
x12 mov 90,f
x13 send:
x14 push 100
x15 call send
x16 call sendMe
x17
x18
x19
x20

:) Greetings from C++ land is displayed !!

So, this kind of jump is called Crossing the C++/JS boundary.

Likewise call from C++ to JS is feasible, provided the function is defined in JS land.

Conclusion

You see it is quite simple, no magic, no big deal. Whenever you hear "Crossing C++/JS", always picture everything running in assembly and in the same memory space.

In our next articles, we will dive in deep to see how some major Node.js APIs works underneath:

  • setTimeout
  • process.nextTick
  • setImmediate
  • Promise
  • setInterval

If you have any question regarding this or anything I should add, correct or remove, feel free to comment, email or DM me. Thanks !!! đź‘Ť

--

--

JS | Blockchain dev | Author of “Understanding JavaScript” and “Array Methods in JavaScript” - https://app.gumroad.com/chidumennamdi 📕