< Back to blog
Share on:

Node.js Inject: How to Conduct and Why to Use

This technique is not as popular as it should be

What Is Node JS Dependency Injection?

Dependency injection is a code wiring technique that presumes that the dependencies are input by an external entity (passed as parameters or references), not hardcoded (created or required) inside a module. Dependencies are the objects or services that a module can use.

Dependency injections are a great benefit of JavaScript and many other languages because this technique helps create independent, scalable, and reusable components. At the same time, dependency injections in Node.js web development are not as popular as you might expect. 

Most would say that dependency injection in NodeJS is unnecessary because you can import dependencies to a module using the command “require”. Yet, such an approach creates a more complex code to maintain and test.

NodeJS dependency injection solves this problem.

Why to Use Node JS Dependency Injection?

JavaScript dependency injection offers four essential benefits:

1. Simple maintenance

The code becomes more reusable and easier to configure since the dependency injection technique uncouples an object and its dependency. Imagine that there are three components: X, Y, and Z. X depends on Y, and Y depends on Z. What if you need to add something to Y or Z? If you do not use dependency injection, you have to do a lot of additional coding and refactoring in X as well. If you use dependency injections, adding anything becomes much more straightforward. After all, X only cares about Y and not about Z or anything else they depend upon.

2. Simple testing

The unit testing becomes much easier to perform. If Y is directly referenced in X, you cannot test X without testing Y. If Y is injected in X, you can create a mocked or stubbed Y to test X. For instance, if Y is a web service response, you can substitute it with a hard-coded file, which would work perfectly for testing purposes.

3. Simple management

When a large team is working on a single project, sooner or later, their tasks will intersect. With Node.js dependency injections, team members isolate their components and become less dependent on others’ work. Thus, the work on a few parts can be conducted simultaneously, and compatibility errors are less likely to occur.

4. Single responsibility

Single responsibility is the first of the five principles of object-oriented design, called SOLID principles. Each component should be responsible for one thing. Otherwise, if your component solves two tasks, you will need to test both scenarios with each change in the code. JavaScript dependency injection is a way to implement the single responsibility principle.

How to Perform NodeJS Injection?

There are different ways of how dependency injection can be performed. You can find plenty of primitive examples on the web to understand the essence of this technique. Meanwhile, I would like to show you how to execute a dependency injection in an actual code with the help of the Frida framework.

Frida Framework

Frida by itself already has an example of code injection for Node.js, but it seems that it is a bit outdated and could work only with a particular Node.js version – GUIDE. It uses V8 embed code to execute a JS string. I have updated the code to Node.js v8.16.0 x64, but I will not elaborate on injection details here. You can easily find out more via the following link, so instead, let’s go to the injected code.At first, let’s see which Frida code we will use (you can get more details here).

  1. Module.findExportByName finds C exported function by its name. There is one little caveat – so-called “mangled names”. In short, when we investigate C++ compiled code, we could see a lot of names like ?GetCurrent@Isolate@v8@@SAPEAV12@XZ. First of our points is to find all required function names in a disassembler like IDA or OllyDbg and to get the memory address of it.
  2. NativeFunction(POINTER, RETURN_VALUE, ARGUMENTS) is used to define in-js bindings for a native function. Here, we need to describe correct arguments count and return values to call these functions later.
  3. NativeCallback is a code that will be executed after a native function call.
  4. WeakRef.bind is used to monitor specified pointer and the callback when it gets garbage-collected or on Frida detach. 
  5. Memory.alloc(N) allocates N bytes in memory and returns pointer for this memory region.

Concepts and Functions Used

Here, I want to define some concepts that I am going to use:

  • Isolate represents an isolated instance of the V8 engine. V8 isolates have completely separate states. Objects from one isolate cannot be used in another isolate. When V8 is initialized, a default isolate is implicitly created and entered. The embedder can create additional isolates and use them in parallel in multiple threads. An isolate can be entered by at most one thread at any given point in time. The Locker/Unlocker API must be used to synchronize. In short, it is a kind of sandbox, which contains its own states.
  • Context stands for a sandboxed execution context with its own set of built-in objects and functions.
  • HandleScope is a stack-allocated class, which governs a number of local handles. After a handle scope has been created, all local handles will be allocated within that handle scope until either the handle scope is deleted or another handle scope is created. If there is already a handle scope and a new one is created, all allocations will take place in the new handle scope until it is deleted. After that, new handles will again be allocated in the original handle scope. After the handle scope of a local handle has been deleted, the garbage collector will no longer track the object stored in the handle and may deallocate it. The behavior of accessing a handle for which the handle scope has been deleted is undefined.
  • Script stands for a compiled JavaScript script.
  • String is a JS string value.
  • Value is a superclass of all JavaScript values and objects.

Next, I describe the functions of libuv that we will use – a lib that allows using an async, event-driven style of programming:

  1. uv_async_init initializes handle, specifies the callback that will be executed inside the event loop;
  2. uv_default_loop takes default event loop;
  3. uv_async_send calls the callback;
  4. uv_unref destroys the object created;
  5. uv_close requests handle to be closed.

We will also use some V8 functions (formatted as real method name –> name of binded js function). In particular:

  1. V8::Isolate::GetCurrent(v8_Isolate_GetCurrent) takes the instance of a current Isolate.
  2. V8::Isolate::GetCurrentContext(v8_Isolate_GetCurrentContext) gets context from the current Isolate.
  3. V8::Context::Enter(v8_Context_Enter) enters this context. After entering a context, all code compiled and run is compiled and run in this context. If another context is already entered, this old context is saved so it can be restored when the new context is exited.
  4. V8::HandleScope::Init(v8_HandleScope_init) initializes HandleScope.
  5. V8::String::NewFromUtf8(v8_String_NewFromUtf8) gets JS string from const char *.
  6. V8::Script::Compile(v8_Script_Compile) compiles the specified script (bound to current context).
  7. V8::Script::Run(v8_Script_Run) runs the script returning the resulting value. If the script is context-independent (created using ::New), it will be run in the currently entered context. If it is context-specific (created using ::Compile), it will be run in the context in which it was compiled.
  8. v8::Value::Int32Value(v8_Value_Int32Value) gets C int from JS value. Value has a lot of functions for different types.

Code Listing

Let’s go through the code listing step by step:

createFunc is a helper that is used to create JS bind with a specified signature to reuse later.

try {
 const createFunc = (name, retval, args) => {
   const _ptr = Module.findExportByName(null, name);
   return new NativeFunction(_ptr, retval, args);

All of the calls like const uv_default_loop = createFunc(‘uv_default_loop’, ‘pointer’, []) are required to define JS bindings to native functions.


 const uv_default_loop = createFunc('uv_default_loop', 'pointer', []);
 const uv_async_init = createFunc('uv_async_init', 'int', ['pointer', 'pointer', 'pointer']);
 const uv_async_send = createFunc('uv_async_send', 'int', ['pointer']);
 const uv_unref = createFunc('uv_unref', 'void', ['pointer']);
 const v8_Isolate_GetCurrent = createFunc('?GetCurrent@Isolate@v8@@SAPEAV12@XZ', 'pointer', []);
 const v8_Isolate_GetCurrentContext = createFunc('?GetCurrentContext@Isolate@v8@@QEAA?AV?$Local@VContext@v8@@@2@XZ', 'pointer', ['pointer', 'pointer']);
 const v8_Context_Enter = createFunc('?Enter@Context@v8@@QEAAXXZ', 'pointer', ['pointer']);
 const v8_HandleScope_init = createFunc('??0HandleScope@v8@@QEAA@PEAVIsolate@1@@Z', 'void', ['pointer', 'pointer']);
 const v8_String_NewFromUtf8 = createFunc('?NewFromUtf8@String@v8@@SA?AV?$MaybeLocal@VString@v8@@@2@PEAVIsolate@2@PEBDW4NewStringType@2@H@Z', 'pointer', ['pointer', 'pointer', 'pointer', 'int', 'int']);
 const v8_Script_Compile = createFunc('?Compile@ScriptCompiler@v8@@SA?AV?$MaybeLocal@VScript@v8@@@2@V?$Local@VContext@v8@@@2@PEAVSource@12@W4CompileOptions@12@@Z', 'pointer', ['pointer', 'pointer', 'pointer', 'pointer']);
 const v8_Script_Run = createFunc('?Run@Script@v8@@QEAA?AV?$Local@VValue@v8@@@2@XZ', 'pointer', ['pointer', 'pointer']);
 const v8_Value_Int32Value = createFunc('?Int32Value@Value@v8@@QEBAHXZ', 'int64', ['pointer']);

scriptToExecute is our injected code.

const scriptToExecute = `((a, b)=>{
   console.log("Hello from Frida", a, b);
   return a+b;
 })(5, 17)`;

We define uv async handler, then bind processPending to the default event loop, then “wake up” that event loop for a callback call. Inside the callback, we receive an Isolate instance, initialize new HandleScope, and take current context. Then, we convert the JS string to V8 string, compile, and run it.  After all, we just extract int value from the script execution result.

 const processPending = new NativeCallback(function () {
   const isolate = v8_Isolate_GetCurrent();
   const scope = Memory.alloc(128);
   v8_HandleScope_init(scope, isolate);
   const opts = Memory.alloc(128);
   const context = v8_Isolate_GetCurrentContext(isolate, opts);
   const item = scriptToExecute;
   const unkMem = Memory.alloc(128);
   const source = v8_String_NewFromUtf8(unkMem, isolate, Memory.allocUtf8String(item), 0, -1);
   const script = v8_Script_Compile(context, Memory.readPointer(context), source, NULL);
   const result = v8_Script_Run(Memory.readPointer(context), context);
   const intResult = v8_Value_Int32Value(Memory.readPointer(result));
   console.log('Result', intResult);
 }, 'void', ['pointer']);
 const onClose = new NativeCallback(function () { }, 'void', ['pointer']);
 const async = Memory.alloc(24);
 uv_async_init(uv_default_loop(), async, processPending);
catch (ex) {
 console.log("Injected code execution error", ex);

To Wrap Up

Node js dependency injection is not as popular as it should be. It is designed to simplify and create a time-efficient coding experience, not vice versa.

There are different ways to perform this technique, and the one I offered you here is just one of them. You can find plenty of other examples on StackOverflow.

Yet, regardless of how you implement it, the benefits remain simple maintenance, testing, management, and single responsibility.

Do you have an idea for a Node JS app?

Does it seem to you that Node.js is the framework you need? Please, find out more about Node.js development services that we offer.

Written by Anton Trofimov, Full-stack JavaScript Developer, and Tania Matviiok, Content Manager for helping with the article @ KeenEthics 

Share on: