Defining JS Dependency Injection

What is dependency (here, we define this notion as objects necessary for an average module in the code) injection? It’s a unique approach to writing code. Instead of creating dependencies within the modules, the method allows you to enter critical parameters from the outside. This method is specifically very important for any well-regarded node web development company. So, let’s continue.

One of the main benefits of dependency injections regarding JavaScript is their ability to forge separate, controllable, and multi-use code elements. You can also use the technique in other programming languages. Surprisingly, the presented approach isn’t very popular, however. In this article, we will try to resolve the injustice.

Some individuals believe the method isn’t necessary. They point at the ‘require’ command, using which a programmer can integrate critical dependencies into new elements. Still, all easy paths are simple only on paper. Yes, you can use the ‘require’ approach and save time.

What is the cost? The clarity of your code. You will simply spend more time maintaining it. Want transparency? Node.js dependency injection is a perfect solution.

What Are the Tangible Benefits of Node.js Dependency Injection?

You should pay attention to four key positives of the approach:

1. Adherence to the single responsibility principle.

As you undoubtedly know, modern object-oriented programming features so-called single responsibility. This vital principle is basic: one element or module of code must concentrate on a singular task. The logic here is relatively simple. A module focusing on two aims would require additional retesting in case of any changes.

You will need to see whether it fulfills two goals at the same time. Don’t want such problems? JS dependency injection solves the issues. It’s a single responsibility approach by default.

2. Problemless testing.

Another important reason for using Node.js dependency injections includes the simplicity of double-checking your code. Let’s say you reference one bit of code (we will mark it as G) in module A. The test of module A will then get impossible without the G.

But, what if you inject G into A? Your goal becomes simple: you can make an emulation of module G for testing. For example, G may be a web service element of any kind. Instead of using it completely, you can create a small file. This approach will take little time for analysis.

3. Maintenance is also relatively simple.

js dependency injection makes code much more reusable. It becomes simpler to modify. After all, this method separates dependencies and coding objects. Let’s say you have three modules, W, K, and L. W connects to K, and K relies on L.

You decide to add something to L/K… and your work certainly doesn’t end there. Code changes will demand transformations in W too. Dependency injection solves this problem once and for all. With its use, W will only depend on K. L will be entirely out of the equation, making additions to programs simple.

4. Management requires less time.

Every large project creates a situation when people start to depend on each other. Their code and goals begin to intersect. As a result, many people can’t solve minor issues without contacting colleagues. Dependency injection offers a strong answer to the problem: it isolates different elements of code.

Cases in which your work interrupts the tasks of others become less common. From a business standpoint, the solution is also great since it decreases many challenges with the compatibility of components.

Implementing Node.js Injection

The number of ways to do dependency injection is relatively large. The Internet is full of examples capable of teaching you about the basics in a simple way. I want to show you a very robust approach (in my opinion, of course). What is this method? Frida framework.

Introducing Frida

The introduction of code injection is present on the website of the framework. However, the guide is slightly outdated: it will only function with some Node.js versions. The method concentrates on using V8 embed code for launching a JS fragment.

Personally, I have upgraded my programming to fit the Node.js 8.16.0 x64 version. But, talking about specific version-centric injection issues will be too tedious for the reader. A lot of information concerning this aspect is present on the GitHub of Frida Node. Let’s go to the code we inject: this information will be extremely important to you.

Above all, we need to define all critical aspects of Frida code central for Node.js injections (for a complete list of functions, go to Frida framework official guide):

  1. NativeFunction (POINTER, RETURN_VALUE, ARGUMENTS): this element is necessary for creating internal JS connections regarding native functions. Your goal is to give correct values for the pointer and return to let them emerge further.
  2. NativeCallback: this module signifies code that you will start after a native function reference.
  3. bind: use this code element to observe pointers and callbacks, preventing losses during garbage collection, for instance.
  4. findExportByName: the framework helps search for functions exported from C based on their titles. Beware, however: a phenomenon of “mangled names” can disrupt your programming. Compiled C++ code tends to include many disfigured names. Your goal here is to look for function titles via, for example, IDA (any good disassembler will work) to find their addresses in memory.
  5. alloc(N): this element gives a certain number of bytes within memory and restores pointers in a specified region.

Main Concepts and Functions to Consider

Several concepts will be central for the task at hand:

  • HandleScope controls the overall number of handles available locally. This stack-allocated class is essential because it can help you manage the complexity of code. Once you make a handle scope, you will be able to assign all key handles to it. Obviously, you can rearrange the number and structure of handle scopes. Another important feature is the behavior of new and old objects of this type. When a new handle scope emerges, assignments automatically work within it. The old one will not function until you eliminate the novel scope. You also have to know about the memory-related issues of HandleScope. If you delete this element and don’t create anything new, the garbage collector process will stop monitoring particular objects and remove priority from them. No definitions are present for handles with lost scope.
  • Isolate involves a separate V8 engine instance. Different instances feature diverging conditions. Code from one isolated fragment of the V8 process can’t function in the others. Upon its start, the V8 engine initiates the first Isolate. You can then create other isolates and apply various threads to their functioning. Typically, V8 allocates one thread per isolate. I recommend using the Locker/Unlocker API for synchronization concerning threads. If you need a short description of the element, I believe one can call it a sandbox. You can isolate states and manage them in a simple fashion.
  • Context represents a sandbox-centric environment that has some unique embedded objects/functions.
  • Script is nothing but a JS scenario that you have compiled.
  • String stands for JavaScript string-oriented values of different kinds.
  • Values have to do with superclasses that relate to diverging JavaScript elements.

Another vital element is the libuv we need for async- and event-driven programming:

  1. uv_async_init starts an average handle and highlights the callback necessary for an event loop;
  2. uv_default_loop begins work, as its name suggests, with the default event loop;
  3. uv_async_send initializes the callback process;
  4. uv_close helps close any handle;
  5. uv_unref eliminates any specified object.

You will also need to know the overall approach to essential V8 functions. Here, we put them into a format of the method name that transitions into the title of the JS function. So, let’s start our list:

  1. V8::Isolate::GetCurrent(v8_Isolate_GetCurrent) highlights the current Isolate and allows starting work on it.
  2. V8::Script::Run(v8_Script_Run) begins a script you have created. Before you use this function, define whether you want a context-independent or context-specific script. Functions such as :New and :Compile are perfect for this task.
  3. V8::Context::Enter(v8_Context_Enter) helps to insert the critical context, as its name suggests. A new context offers novel ways for running all the connected code. And, what if you have some old approaches? You can restore them at any time you want.
  4. V8::Isolate::GetCurrentContext(v8_Isolate_GetCurrentContext) receives vital context from the isolate you currently use.
  5. V8::HandleScope::Init(v8_HandleScope_init) is also self-evident since it jumpstarts a handle scope.
  6. V8::String::NewFromUtf8(v8_String_NewFromUtf8) analyzes a const char to obtain a proper JS string.
  7. V8::Script::Compile(v8_Script_Compile) helps compile a specific script (beware: the function depends on the context you have).
  8. v8::Value::Int32Value(v8_Value_Int32Value) uses JS values to receive C int. You can configure the value parameter to adapt to a tremendous number of types.

How it all Looks in Code

It’s time for some examples. Here is a step by step description:

Use createFunc to implement a JS bind with a clear signature for future use:

try {

const createFunc = (name, retval, args) => {

const _ptr = Module.findExportByName(null, name);

return new NativeFunction(_ptr, retval, args);

}

Calls such as const uv_default_loop = createFunc(‘uv_default_loop’, ‘pointer’, []) are crucial for creating bindings necessary for 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’]);

And, here is the command that helps to inject our code (scriptToExecute):

const scriptToExecute = `((a, b)=>{

console.log(“Hello from Frida”, a, b);

return a+b;

})(5, 17)

Start with the definition of the uv async handler. After that, connect processPending and the default event loop. Lastly, stir it to participate in the full-scale callback call. Further processes are relatively intuitive too. The callback will give you an Isolate instance, where you can initialize handle scopes and define the context. Lastly, convert JS to V8 code, start the compilation, and test the results.

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);

uv_async_send(async);

uv_unref(async);

}

catch (ex) {

console.log(“Injected code execution error”, ex);

}

Conclusion

Dependency injections should undoubtedly get more attention. They make coding much more straightforward and understandable.

Many ways are present for engaging in this approach. I prefer the one outlined above, but other options exist too. Sites like StackOverflow are full of examples.

In the end, what matters are the key strengths of the method, which involve better control over your code.

To add to this or start a conversation, join our forum to share your opinions with other readers. For stories of this sort and more, do well to log on to www.jbklutse.com or visit us on Facebook

Website | + posts