{"id":15612270,"url":"https://github.com/euberdeveloper/nodejs-cpp-addons-first-approach","last_synced_at":"2025-03-29T15:12:58.704Z","repository":{"id":97419997,"uuid":"448356513","full_name":"euberdeveloper/nodejs-cpp-addons-first-approach","owner":"euberdeveloper","description":"A first trial with nodejs native addons","archived":false,"fork":false,"pushed_at":"2022-01-20T12:54:31.000Z","size":56,"stargazers_count":0,"open_issues_count":0,"forks_count":0,"subscribers_count":3,"default_branch":"main","last_synced_at":"2025-02-04T15:49:52.777Z","etag":null,"topics":[],"latest_commit_sha":null,"homepage":null,"language":"C","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":null,"status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/euberdeveloper.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":null,"code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null}},"created_at":"2022-01-15T18:17:10.000Z","updated_at":"2022-01-23T18:16:28.000Z","dependencies_parsed_at":"2023-03-30T15:19:21.794Z","dependency_job_id":null,"html_url":"https://github.com/euberdeveloper/nodejs-cpp-addons-first-approach","commit_stats":null,"previous_names":[],"tags_count":0,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/euberdeveloper%2Fnodejs-cpp-addons-first-approach","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/euberdeveloper%2Fnodejs-cpp-addons-first-approach/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/euberdeveloper%2Fnodejs-cpp-addons-first-approach/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/euberdeveloper%2Fnodejs-cpp-addons-first-approach/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/euberdeveloper","download_url":"https://codeload.github.com/euberdeveloper/nodejs-cpp-addons-first-approach/tar.gz/refs/heads/main","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":246200323,"owners_count":20739566,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":[],"created_at":"2024-10-03T06:41:07.519Z","updated_at":"2025-03-29T15:12:58.679Z","avatar_url":"https://github.com/euberdeveloper.png","language":"C","funding_links":[],"categories":[],"sub_categories":[],"readme":"# nodejs-cpp-addons-first-approach\nThis is my first experience with a **C++ addon** for **Node.js**.\n\n## Introduction\n\nNode.js is known to be effective for tasks like **handling several HTTP requests**, thanks to its events loop. It is also known to be (relatively) **fast**, but it remains an **interpreted language** and **can not compete** with compiled languages such as **C**, **Rust** or **Go**. The performance problems of Node.js are expecially noticeable when dealing with **heavy computations**.\n\n## C++ addons\n\nNode.js is written in **C++** (and Javascript), based on the **V8 engine**, the same engine that powers **Chrome** for executing Javascript. Node.js supports an interoperability beween **C** and **Javascript**, so that parts of the code can be written in a compiled language in order to be faster.\n\nThere are four ways to develop a C++ addon for Node.js:\n* By using directly the **internal V8, libuv and Node.js libraries**\n* By using **nan** (Native Abstractions for Node.js)\n* By using **Node-API**\n* By using **node-addon-api**\n\n### Using the internal V8, libuv and Node.js libraries\n\nUsing the internal V8, libuv and Node.js libraries is totally discouraged. First of all, because it is the **most low-level** way, so the code would be very **verbose** and **difficult to handle**. But the **biggest reason** not to use it is that there can be **huge differences** between **different Node.js versions**, expecially due to changes in the V8 engine. This means that the code that you will write, if it is not using just the most basic functionalities, **will probably work just on your current Node.js version**. This should be used only if you need to use **functionalities not available in the other methods**.\n\n### Using nan (Native Absractions for Node.js)\n\n**nan** (which in this case, is not not-a-number), is an **abstraction layer** between your code and the previous citated libraries. The aim of nan is providing a **stable ABI**, so that the code that you write will be at least **forward compatible** with future versions of Node.js. Being an abstraction layer, the code will be also **quite simpler** than the one of the previous method. The problem with this method is that, even if the **code is forward compatible** with future versions, the **compiled binaries are not**, so they need a **recompilation**. The newer \n**Node-API is a better alternative**, so the biggest reason to use **nan** is if you need functionalities that **are not available on the Node-API** or if you need your code to work with **very old versions of Node.js**, that don't support Node-API.\n\n### Using Node-API\n\n**Node-API** is **not a layer** between your code and the internal libraries used by Node.js itself. It is an **ad-hoc API** made to make writing native **addons way easier and more convenient**. Being designed exactly for this purpose, it is the **nicest way** to write addons. The most important thing is that **not only the code, but also the binaries are forward compatible** with future versions of Node.js.\n\n### Using node-addon-api\n\n**node-addon-api** is exactly **the same of Node-API**, with the difference that the **code becomes easier**. It is just a **header wrapper**, that has consequently **exactly the same functionalities**, that **furtherly simplifies the code**. This should be **the preferred way** to write addons for Node.js.\n\n\n## How are addons built?\n\nNode.js addons are build with **node-gyp**, a node-js flavour of Google's **GYP** building system. **GYP** is an **obsolete build automation** tool that was used, among the others, to build Chromium, V8 and Dart. In 2016 these projects migrated to **GN**, a far quicker tool that obtains the same result. While **GYP** generates **Make** files, **GN** generates **Ninja** files. **Node.js** still relies on this obsolete tool (of course through the still-maintained **node-gyp**) to build native modules. As of today (I dare you to look through the commits' history to understand when is today), also **Telegram** is using **GYP** to build its native modules.\n\nGYP takes a **binding.gyp** file as configuration and generates a **.node** file, that can be **directly imported in js code**, as if it were a normal javascript module. It will generate with the **configuration command** a **Makefile** and other building files and, with the **build** command, it will generated the **compiled module**.\n\nThe **binding.gyp**, written in **json**, looks like this:\n\n```json\n{\n  \"targets\": [\n    {\n      \"target_name\": \"addon\",\n      \"sources\": [\n        \"addon/main.cc\",\n        \"addon/utils/fibonacci.cc\",\n        \"addon/utils/sum.cc\",\n        \"addon/modules/hello.cc\",\n        \"addon/modules/fibonacci.cc\",\n        \"addon/modules/sum.cc\"\n      ]\n    }\n  ]\n}\n```\n\nWhere \"sources\" indicates the source c++ files and target_name will generate **addon.node**. This output file is usually placed on **build/Release** or **build/Debug**.\n\nIt will be **during the installation of a native package** that the native code will be **compiled**. The destination of the compiled addon is not always the same, so the **safest way** to **import it** is by using the **[bindings](https://github.com/TooTallNate/node-bindings)** module.\n\n## My first approach\n\nIn my first approach, I followed the **[first documentation on the official Node.js website](https://nodejs.org/api/addons.html)**, by choosing the **hardest way**, so directly the interlal node.js and V8 libraries. I did this because it would have been probably the **first and last** times that I would have done it (and I was right).\n\nWhat I wanted to know was: **does it really make sense** to loose time writing C++ code? Are the **performance benefits worth it**? \n\nSo I **developed some C++ addons** and **javascript analogues** and **compared their performance**.\n\nThe **main.cc** file looks like this:\n\n```cpp\n#include \u003cnode.h\u003e\n#include \"./headers/modules/hello.h\"\n#include \"./headers/modules/fibonacci.h\"\n#include \"./headers/modules/sum.h\"\n\nusing v8::Local;\nusing v8::Object;\n\nvoid Initialize(Local\u003cObject\u003e exports)\n{\n    NODE_SET_METHOD(exports, \"hello\", Hello);\n    NODE_SET_METHOD(exports, \"fibonacci35\", Fibonacci35);\n    NODE_SET_METHOD(exports, \"fibonacci40\", Fibonacci40);\n    NODE_SET_METHOD(exports, \"sum\", Sum);\n    NODE_SET_METHOD(exports, \"sum1e7\", Sum1e7);\n}\n\nNODE_MODULE(NODE_GYP_MODULE_NAME, Initialize)\n```\n\nwhile the **modules c++** files look like this:\n\n```cpp\n#include \"../headers/modules/hello.h\"\n\nvoid Hello(const FunctionCallbackInfo\u003cValue\u003e \u0026args)\n{\n    Isolate *isolate = args.GetIsolate();\n    args.GetReturnValue().Set(String::NewFromUtf8(isolate, \"world\").ToLocalChecked());\n}\n```\nThis is the ugly code that you will write if you choose the bad way.\n\n### hello bechmark\n\nThe hello bechmark is the same of the official documentation: **it is just a function that returns the string `world`**.\n\n```js\n hello() {\n    return 'world';\n}\n```\n\nIn the **benchmarks** I compared both the addon and the js implementations first **by just calling the function 10^6 times** and then by **writing the result in a file (/dev/null) 10^6 times**. \n\n| **BENCHMARK**   | **ITERATIONS** | **JS TIME** | **ADDON TIME** | **WINNER**                          |\n| --------------- | -------------- | ----------- | -------------- | ----------------------------------- |\n| HELLO           | 10^6           | 7ms         | 67ms           | *Javascript* is *9.57* times faster |\n| HELLO OVER FILE | 10^6           | 509ms       | 428ms          | *Javascript* is *1.18* times faster |\n\n**What happened?**. After all the fatigue made to create the addon, **Javascript** is faster. The reason is simple: **loading the addon introduces a small overhead**. This overhead does not make sense in cases like this, where the **function computation cost is irrelevant**. The reason why Javascript is almost **tenfold faster** is because **the overhead for loading tha addon is about tenfold the function's execution time itself**.\n\nIf we run the same javascript function agains this **other javascript** function:\n\n```js\nfunction hello() {\n    function hi() {\n        function servus() {\n            function ciao() {\n                return 'world';\n            }\n            return ciao();\n        }\n        return servus();\n    }\n    return hi();\n}\n```\n\nWe can see that the **second version** is **1.33 times slower** and this is because of the **function calls overhead**, a similar readon of why, in this case, the addon is slower.\n\n**But why in writing in the file the gap was smaller?**. Here the reason is obvious. The **most computational expensive part** in the second benchmark **is the file writing**, not the string creation. If you look at the **times**, for the same number of iterations the **second benchmark** was definitely slower. It is as if we took the first benchmark times and added 500 millis. For numbers bigger than 1, `x \u003e y; x / y` is bigger than `(x + 500) / (y + 500)`. This can be interpreted as: **the addon is slower to the same extent of the first benchmark**, but this overhead is **almost negligible**, because **the bottleneck here is the file writing**.\n\n### fibonacci benchmark\n\nThis benchmark covers a **very stupid implementation** of the **fibonacci** algorithm. \n\n```cpp\nint fibonacci(int n)\n{\n    return n \u003c= 1 ? n : fibonacci(n - 1) + fibonacci(n - 2);\n}\n```\n\nThis recursive function is **exponential**, while it could be just linear by using **Dynamic programming** and **Memoization**. With even quite small arguments, it involves **heavy computations**.\n\nI tested the computation of `fibonacci(35)` and `fibonacci(40)` with `5` iterations:\n\n| **BENCHMARK** | **ITERATIONS** | **JS TIME** | **ADDON TIME** | **WINNER**                     |\n| ------------- | -------------- | ----------- | -------------- | ------------------------------ |\n| fibonacci(35) | 5              | 500ms       | 116ms          | *Addon* is *4.31* times faster |\n| fibonacci(40) | 5              | 12279ms     | 1284ms         | *Addon* is *9.56* times faster |\n\nThis time we can see, how it was expected, that **the addon is way faster than javascript**. Here we can see the power of compiled (and optimized) languages such as C++. The **addon** is **4 times faster for fibonacci(35)** and **almost 10 times faster for fibonacci(40)**. Again, this gab between the two tests is just **an illusion**: being the algorithm **exponential**, **by increasing** even slightly **the input**, the **number of computations increase in an exponential way**. Being C++ faster, the gap increases exponentially by increasing the input.\n\nHere we can see how with **heavy computation functions**, **native addons make sense**.\n\n### sum benchmark\n\nIn this benchmark I made something that **consolidates** what learnt from the two other tests. I wrote two functions.\n\nThe first function is just **the sum of 1 + 1**:\n\n```cpp\nint sum()\n{\n    int result =  1 + 1;\n    return result;\n}\n```\n\nWhile in the other there is **the sum of he first n natural numbers**.\n\n```cpp\nint multisum(int n) {\n    int result = 0;\n    for (int i = 0; i \u003c n; i++) {\n        result += i;\n    }\n    return result;\n}\n```\n\nWhile the first one executes **a single sum for 10^7 times**, while the second one executes **10^7 sums for one time**.\n\nThe results are:\n\n| **BENCHMARK** | **ITERATIONS** | **JS TIME** | **ADDON TIME** | **WINNER**                          |\n| ------------- | -------------- | ----------- | -------------- | ----------------------------------- |\n| sum           | 10^7           | 63ms        | 293ms          | *Javascript* is *4.65* times faster |\n| multisum(1e7) | 1              | 12ms        | 1ms            | *Addon* is *12* times faster        |\n\nAs we can see, **the function behind is the same**, it is the sum, but the fact that **all the addon sums are executed together**, in just one call, or if **they are executed one by call** makes the difference. **If they are executed in one call, the addon is 12 times faster than javascript**, **otherwise, it is 4 times slower**.\n\n### when to use the addon\n\nThis explains well that **C++ addons should be used just for computational heavy tasks**, while they are **even slower if the task is simple**, due to a **small overhead, which is negligible if the algorithm is complex**. This means that **one can not just think of writing simple code in C++ and calling it by complex javascript code to make programs faster**, but **the C++ code has to execute a quite complex task in itself in order to have a benefit**. \n\nOne of these cases is **cryptography**, something that **requires big computations** but that is much used, for instance, in **Rest APIs**, to **check hashed passwords or json web tokens**. One of the **few npm packages written in C++** is indeed **[bcrypt](https://github.com/kelektiv/node.bcrypt.js#readme)**. It is also curious to see how, during the years, they passed **from v8** [see here](https://github.com/kelektiv/node.bcrypt.js/blob/0d0e09087b41032795b4ccf7babfbb69f2a06555/src/bcrypt_node.cc) **to nan** [see here](https://github.com/kelektiv/node.bcrypt.js/blob/1012f9a0dc7b517e1511461c384281dc0f3f3059/src/bcrypt_node.cc) to **Node-API** [see here](https://github.com/kelektiv/node.bcrypt.js/blob/184200eb54692b476c8fe31cdb5a6d7789c38ca1/src/bcrypt_node.cc).\n\n### But is the C++ addon's code as faster as normal C code?\n\nExcluding the small **loading overhead** and the **Node.js C++ functions wrapping overhead**, the **C++ addon code is just normal C++ code**, so it has **the same speed as normal C code**.\n\nTo check this, I wrote the **multisum** function in a normal **C** file and compiled it with **gcc**. \n\nNote that the `get_current_timestamp` implementation is omitted.\n\n```c\n#include \u003cstdio.h\u003e\n#include \u003ctime.h\u003e\n#include \u003csys/time.h\u003e\n\nlong long int get_current_timestamp();\n\nint multisum(int n)\n{\n    int result = 0;\n    for (int i = 0; i \u003c n; i++)\n    {\n        result += i;\n    }\n    return result;\n}\n\nint main()\n{\n    long long int start = get_current_timestamp();\n\n    int result = 0;\n    result += multisum(1e7);\n    printf(\"result: %d\\n\", result);\n\n    long long int end = get_current_timestamp();\n    printf(\"Multisum 1e7 took %lld millis\\n\", end - start);\n\n    return 0;\n}\n```\n\nBy running it we can obtain `Multisum 1e7 took 23 millis`.\n\nIf we look at the **benchmark**, we can see that the **addon code took just 1ms**, while the **javascript code took 12ms**. **How is this possible that javascript is 2 times faster than normal C?**. It is because this **C file was not compiled with the optimization flag**.\n\nBy running it with a `gcc -O3` flag, we can obatin `Multisum 1e7 took 1 millis`, which is **exactly the same performance of the addon**.\n\nThe fact that **javascript was even faster than the not-optimized C code** should in any case make you think that **javascript is not so slow** and that the **V8 engine is highly optimized**, making Javascript, **of course not always**, quite **faster than other interpreted languages** such as python, PHP, Ruby and Perl.","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Feuberdeveloper%2Fnodejs-cpp-addons-first-approach","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Feuberdeveloper%2Fnodejs-cpp-addons-first-approach","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Feuberdeveloper%2Fnodejs-cpp-addons-first-approach/lists"}