This article was originally posted on Medium as an extension to my article series on NodeJS Event Loop on Medium:
Crossing the JS/C++ Boundary — Advanced NodeJS Internals — Part 1 | by Deepal Jayasekara | Deepal’s Blog
Deepal Jayasekara ・ ・
blog.insiderattack.net
In NodeJS Event Loop article series, I explained,
- Event Loop and the Big Picture
- Timers, Immediates and Next Ticks
- Promises, Next-Ticks, and Immediates
- Handling I/O
- Event Loop Best Practices
- New changes to timers and microtasks in Node v11
In this article, I’m going to explain how NodeJS internally glue a bunch of C
/C++
and JavaScript pieces all together to build an amazing server-side Javascript framework.
For you to clearly understand how it works, I have included few code snippets in this post from the NodeJS codebase. Please note that I have truncated certain parts of those for brevity and have added some comments and log lines myself. Also, I would like you to understand that NodeJS codebase is very active and is subject to frequent code refactoring. Therefore, these code snippets might be drastically different across revisions. However, the overall functionality will be almost similar. Although these snippets would be gone in future releases, understanding how they work right now could make it really easy for you to understand the entire NodeJS codebase.
In the Event Loop series, I explained to you how it works, different phases of it and the sequence. But the event loop is implemented in libuv and nowhere in its code is mentioned about process.nextTick
. Now the weird question is….Where is process.nextTick
callbacks are called? The only thing we know is that they are called at the start and between each phase of the event loop.
I presume you have a sound understanding of the different phases of the event loop, microtasks,
process.nextTick
and other terminology related to Node. If you haven’t I do appreciate if you can read the aforementioned Event Loop article series before reading this article as you might feel it difficult to follow some of the concepts mentioned here.
First of all, let’s see how process.nextTick
is defined in JavaScript context so that we can call it. To come to that, let’s see how NodeJS starts up.
Initialization: Starting V8 Environment
During the initialization of NodeJS runtime, an instance of V8
environment is created and the environment is started by calling Environment::Start
method. In this method, SetupProcessObject
is executed which makes some interesting C++
functions accessible from JavaScript.
As you can see _setupNextTick
function is set in the process
object during the initialization of V8
environment. _setupNextTick
is a reference to the SetupNextTick
function in C++
context. Following is the definition of the SetupNextTick
function.
I will explain how this SetupNextTick
function comes into play in a while.
Loading Bootstrappers
After initializing V8
environment, two JS scripts called bootstrappers
are run.
They are,
Loaders Bootstrapper: internal/bootstrap/loaders.js
Node Bootstrapper: internal/bootstrap/node.js
Node bootstrapper calls its startup
function during execution.
During the execution of startup()
function, NodeJS require
s the next tick module from internal/process/next_tick
and executes its setup()
export function.
This setup()
function is exported from next_tick.js
and is a reference to the function setupNextTick
defined in the same file.
- Upon calling,
setupNextTick
function sets the attributenextTick
in theprocess
object (line 22) as a reference to thenextTick
function defined in the same scope. This is howprocess.nextTick
can be called from userland. -
nextTick
function (line 37) merely adds a given callback into a queue. - There’s another function called
_tickCallback
(line 27) defined in the same scope which is where thenextTick
queue is processed. For ease of reading, I extracted it out as a separate gist as follows. Let’s read it carefully.
- Once
_tickCallback()
function is called, it will iterate through the queue wherenextTick
callbacks are queued and will execute each and every callback until there are no callbacks left in the queue (line 4, innerwhile
loop). - Then, the
_tickCallback()
function will callrunMicrotasks()
function (line 21). This function will process the microtasks queue (e.g, callbacks of resolved/rejected promises). It’s possible that newnextTick
callbacks are added while executing microtasks (e.g, call ofprocess.nextTick
in promise resolve/reject callback). - Above step1 and step2 is repeated until no more callbacks left in the
nextTick
queue (line 3, outerdo-while
loop)
The golden point is….. You need to trigger **_tickCallback**
JavaScript function somehow during two phases of the event loop in order to process the **nextTick**
queue and the microtasks queue.
To do this, _tickCallback
function should be passed somehow to C++
context.
Binding JS function to C++
_tickCallback
function is referenced in C++
context by calling process._setupNextTick
inside setupNextTick
. Upon the execution of process._setupNextTick
, it’s passed tickCallback
as the only parameter (see the next_tick.js
gist above).
Now if you remember I explain before, process._setupNextTick
is actually a C++
function which is referred in JavaScript context upon the initialization of V8
environment. For clarity, I’ll just copy/paste the gist again (It’s time to scroll up if you don’t remember ;))
The equivalent C++
function to process._setupNextTick
JS function is SetupNextTick
which is defined in node.cc
.
This method will call set_tick_callback_function
with the first parameter provided. Therefore, whatever you pass as the first parameter to process._setupNextTick
is passed to set_tick_callback_function
. Now go up and check what we call process._setupNextTick
within setupNextTick
JS definition.
Wow!! Eventually, the reference to _tickCallback
function is passed to set_tick_callback_function
in C++
. set_tick_callback_function
will set the reference to the _tickCallback
function as tick_callback_function
in the V8
environment. The conclusion is, calling tick_callback_function
in V8
environment triggers the execution of JavaScript _tickCallback
function which subsequently processes the nextTick
queue and the microtask queue.
Now if you recall what I mentioned above….
The golden point is….. You need to trigger
**_tickCallback**
JavaScript function somehow during two phases of the event loop in order to process the**nextTick**
queue and the microtasks queue.
You know how ;)
Now we need to know where tick_callback_function
is called in C++
context. Let’s go back to the event loop now.
Crossing the boundary
In NodeJS, we write all our code in Javascript, which means all of our callbacks are JavaScript. So how are they triggered by C
? The answer is, this binding glue is the C++
bindings for libuv. Those are the functions written in C++
who bridges C++
and JavaScript and invoke JavaScript functions when libuv asks them to. Let’s try to understand it in a clear way by an example.
When you are adding multiple timers using setTimeout
, they all will be grouped together by their timeout value so that timers with the same timeout value will be in a single list. And Javascript will provide a callback function processTimers
with each list to execute its callbacks. This callback function will be passed from JavaScript to C++
as OnTimeout
in C++
, and then C++
to libuv(C
) as timer_cb
attached to a handle which is invoked by libuv. Essentially, calling the timer_cb
of a handle in libuv will trigger the execution of the multiple timers which were set at the same time with the same timeout value (I know you have questions now, but just keep them in mind for my next post on NodeJS timers in detail ;))
How a processTimers() JavaScript function is triggered from libuv
When OnTimeout
function in C++
is called, NodeJS will cross the C++
boundary all the way to JavaScript and will execute the given JavaScript callback. When it crosses the C++
/JS
Boundary, an interesting function is called named MakeCallback
.
Now if you search for MakeCallback
, you will find multiple overloaded MakeCallback
functions in node.cc
and async_wrap.cc
with different signatures:
At the time of this writing:
Each of them is written for very distinct purpose but if you look closely, you’ll see that they all eventually calls the InternalMakeCallback
function defined in node.cc
.
When MakeCallback
is called, it will pass the appropriate JS callback which needs to be called by the InternalMakeCallback
as the 3rd parameter (callback
) of the InternalMakeCallback
.
This **callback**
is the almighty who processes a single phase of the event loop.
Then comes the most important part. You’ll see that after calling the provided callback
, scope.Close()
function is called.
Let’s look at the definition of scope.Close()
function defined in node.cc
.
Within this function, it executes the tick_callback_function
in V8
environment.
Do you remember what tick_callback_function
is? It’s the same _tickCallback
JavaScript function which is referred in the C++
context which indicates that every time InternalMakeCallback
is called from C++
_tickCallback
function is called at the end. Therefore, at the end of each phase, nextTick
queue and the microtask queue are processed.
But, If you read my Event Loop article series or if you have personally experienced process.nextTick
callbacks and the microtasks are run even before the timers phase start. How is this happen?
This is because the Node Bootstrapper which I mentioned above also loads the CommonJS module loader internally. During the bootstrap of CommonJS module loader, it manually triggers _tickCallback
to process any nextTick
callbacks added at the start.
PS: For the above examples, I referred to the NodeJS source at revision b267d2aae6. Due to many refactoring processes, the above example codes might be different across different NodeJS versions.
I suppose you now know how the mysterious process.nextTick
works. If you have any questions regarding anything I’ve mentioned or if you like to add something please feel free to comment. Thanks.
Background Image Courtesy: https://www.hdwallpapers.in/walls/earth_horizon-wide.jpg
Top comments (0)