[nodejs]Nodejs + Cpp

๐ŸŒ Nodejs์—์„œ C++ ์• ๋“œ์˜จ(Native Addons)์„ ์‚ฌ์šฉํ•˜๋Š” ์ด์œ ๋Š” Node.js์˜ย ์„ฑ๋Šฅ ์ œํ•œ์„ ๊ทน๋ณตํ•˜๊ณ ,ย ๊ธฐ์กด์˜ C++ ์ƒํƒœ๊ณ„์™€ ์ž์›์„ ํ™œ์šฉํ•˜๊ธฐ ์œ„ํ•จ์ž…๋‹ˆ๋‹ค.
The reason for using C++ addons (Native Addons) in nodejs is to overcome the performance limitations of Node.js and to leverage the existing C++ ecosystem and resources.

๐ŸŒ ์•„๋ž˜๋Š” ๋งฅos์—์„œ ์‹คํ–‰๋˜์—ˆ์Šต๋‹ˆ๋‹ค.
Below was run on macOS.

โœ”๏ธ์ฝ”๋“œ์ž‘์„ฑ / Write code

1)server.js

// server.js
const express = require("express");
const app = express();
const port = 3000;

// 1. C++ ์• ๋“œ์˜จ ๋กœ๋“œ / Loading C++ add-ons
// node-gyp ๋นŒ๋“œ ํ›„ ์ƒ์„ฑ๋˜๋Š” .node ํŒŒ์ผ์˜ ๊ฒฝ๋กœ๋ฅผ ์ง€์ •ํ•ฉ๋‹ˆ๋‹ค.
// Specifies the path to the .node file generated after building node-gyp.
// (๋นŒ๋“œ๊ฐ€ ์„ฑ๊ณตํ•˜๋ฉด build/Release/addon_module.node ํŒŒ์ผ์ด ์ƒ์„ฑ๋ฉ๋‹ˆ๋‹ค.)
// (If the build is successful, the build/Release/addon_module.node file will be created.)
const cppAddon = require("./build/Release/addon_module");

app.use(express.json());

// 2. C++ ์• ๋“œ์˜จ์„ ์‚ฌ์šฉํ•˜๋Š” API ์—”๋“œํฌ์ธํŠธ ์ •์˜
//    Defining API endpoints using C++ add-ons
app.get("/calculate", (req, res) => {
  // ์ฟผ๋ฆฌ ํŒŒ๋ผ๋ฏธํ„ฐ์—์„œ ๋‘ ์ˆซ์ž๋ฅผ ์ถ”์ถœ
  // Extract two numbers from query parameters
  const num1 = parseFloat(req.query.a);
  const num2 = parseFloat(req.query.b);

  if (isNaN(num1) || isNaN(num2)) {
    return (
      res
        .status(400)
        //Please enter valid numbers for query parameters 'a' and 'b'.
        .send("์ฟผ๋ฆฌ ํŒŒ๋ผ๋ฏธํ„ฐ 'a'์™€ 'b'์— ์œ ํšจํ•œ ์ˆซ์ž๋ฅผ ์ž…๋ ฅํ•˜์„ธ์š”.")
    );
  }

  try {
    // 3. C++ ์• ๋“œ์˜จ์˜ ํ•จ์ˆ˜ ํ˜ธ์ถœ / Calling functions in C++ add-ons
    const result = cppAddon.multiply(num1, num2);

    // 4. ๊ฒฐ๊ณผ๋ฅผ ํด๋ผ์ด์–ธํŠธ์—๊ฒŒ ์‘๋‹ต / Reply the results to the client
    res.json({
      input_a: num1,
      input_b: num2,
      source: "C++ Addon",
      result: result,
    });
  } catch (error) {
    console.error("C++ add-on call error:", error.message);
    res.status(500).send("Server internal error occurred");
  }
});

app.listen(port, () => {
  console.log(`Node.js server is running at http://localhost:${port} .`);
  console.log(`Test URL: http://localhost:${port}/calculate?a=123&b=4.5`);
});

2) addon.cpp

// addon.cpp
/*
 * ์ด ์ฝ”๋“œ๋Š” "ํ˜„์žฌ Node.js ํ™˜๊ฒฝ(env)์—์„œ C++ ๋ณ€์ˆ˜ result๋ฅผ ์ด์šฉํ•˜์—ฌ ์ƒˆ๋กœ์šด JavaScript ์ˆซ์ž ๊ฐ์ฒด๋ฅผ ๋งŒ๋“ค์–ด ๋ฐ˜ํ™˜ํ•˜๋ผ"๋Š” ์˜๋ฏธ๊ฐ€ ๋ฉ๋‹ˆ๋‹ค.
 * This code means "Create a new JavaScript numeric object
 * using the C++ variable result in the current Node.js environment (env) and return it."
 *
 * C++ ์• ๋“œ์˜จ์€ mainํ•จ์ˆ˜๊ฐ€ ์—†๋Š” ์ด์œ :Node.js ์ž์ฒด์— ์ด๋ฏธ ๊ฑฐ๋Œ€ํ•œ main ํ•จ์ˆ˜๊ฐ€ ํฌํ•จ๋˜์–ด ์žˆ๊ธฐ ๋•Œ๋ฌธ์ž…๋‹ˆ๋‹ค.
 * Why C++ addons don't have a main function: Because Node.js itself already contains a huge main function.
 *
 * Napi::Number๋Š” node-addon-api ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ์—์„œ ์ œ๊ณตํ•˜๋Š” C++ ํด๋ž˜์Šค๋กœ, Node.js ํ™˜๊ฒฝ์—์„œ ํ†ต์‹ ํ•  ๋•Œ JavaScript์˜ number ํƒ€์ž…์„ ๋‚˜ํƒ€๋ƒ…๋‹ˆ๋‹ค.
 * Napi::Number is a C++ class provided by the node-addon-api library that represents the JavaScript number type when communicating in a Node.js environment.
 *
 * Napi::Number ํด๋ž˜์Šค๋Š” ๋‚ด๋ถ€์ ์œผ๋กœ C++์˜ ๊ธฐ๋ณธ ์ˆซ์ž ํƒ€์ž…์„ JavaScript๊ฐ€ ์ดํ•ดํ•˜๋Š” ์ˆซ์ž ๊ฐ์ฒด ํฌ๋งท์œผ๋กœ ๋ณ€ํ™˜ํ•ด ์ค๋‹ˆ๋‹ค.
 * The Napi::Number class internally converts C++'s basic number type into a numeric object format that JavaScript understands.
*/

// C++ ๊ฐœ๋ฐœ์ž๊ฐ€ Node.js์˜ ๊ณ ์„ฑ๋Šฅ ๋„ค์ดํ‹ฐ๋ธŒ ๊ธฐ๋Šฅ์„ ์‚ฌ์šฉํ•˜๊ธฐ์œ„ํ•œ ํ—ค๋”
// Header for C++ developers to use high-performance native features of Node.js
#include <napi.h>
#include <iostream>

// JavaScript์—์„œ ํ˜ธ์ถœ๋  C++ ํ•จ์ˆ˜ ์ •์˜
// Define a C++ function to be called from JavaScript
Napi::Number Multiply(const Napi::CallbackInfo& info) {

    // env: ํ˜„์žฌ Node.js์˜ ์‹คํ–‰ ํ™˜๊ฒฝ(Napi::Env)์„ ๋‚˜ํƒ€๋ƒ…๋‹ˆ๋‹ค.
    // env: Represents the current Node.js execution environment (Napi::Env).
    Napi::Env env = info.Env();

    // ์ธ์ž ๊ฐœ์ˆ˜ ํ™•์ธ / Check number of arguments
    if (info.Length() < 2 || !info[0].IsNumber() || !info[1].IsNumber()) {
        Napi::TypeError::New(env, "๋‘ ๊ฐœ์˜ ์ˆซ์ž๋ฅผ ์ž…๋ ฅํ•ด์•ผ ํ•ฉ๋‹ˆ๋‹ค.").ThrowAsJavaScriptException();
        return env.Null().As<Napi::Number>();
    }

    // JavaScript ์ธ์ž๋ฅผ C++ ์ˆซ์ž๋กœ ๋ณ€ํ™˜
    // Convert JavaScript arguments to C++ numbers
    double a = info[0].As<Napi::Number>().DoubleValue();
    double b = info[1].As<Napi::Number>().DoubleValue();

    // C++ ๋กœ์ง ์‹คํ–‰
    // Execute C++ logic
    double result = a * b;
    std::cout << "C++ ์• ๋“œ์˜จ์—์„œ ๊ณฑ์…ˆ ๋กœ์ง ์‹คํ–‰๋จ: " << a << " * " << b << std::endl;

    // ๊ฒฐ๊ณผ๋ฅผ JavaScript Number ํƒ€์ž…์œผ๋กœ ๋ณ€ํ™˜ํ•˜์—ฌ ๋ฐ˜ํ™˜
    // Convert the result to JavaScript Number type and return it
    // New()๋Š” ์ƒˆ๋กœ์šด JavaScript ๊ฐ์ฒด๋ฅผ ์ƒ์„ฑํ•˜๋Š” ๋ฉ”์„œ๋“œ
    // New() is a method that creates a new JavaScript object.
    return Napi::Number::New(env, result);
}

// ๋ชจ๋“ˆ ์ดˆ๊ธฐํ™” (exports ๊ฐ์ฒด์— ํ•จ์ˆ˜ ๋“ฑ๋ก)
// Initialize module (register functions in exports object)
Napi::Object Init(Napi::Env env, Napi::Object exports) {
    exports.Set(
        // ์ž๋ฐ”์Šคํฌ๋ฆฝํŠธ์—์„œ addon.multiply(a, b);๋กœ ์‚ฌ์šฉ๋จ.
        // Used in JavaScript as addon.multiply(a, b);
        Napi::String::New(env, "multiply"),

        // Functionํƒ€์ž…์€ ์ž๋ฐ”์Šคํฌ๋ฆฝํŠธ์—์„œ ํ•จ์ˆ˜๋กœ ์‚ฌ์šฉ ํ•  ์ˆ˜ ์žˆ๊ฒŒ ํ•ด์คŒ.
        // The Function type allows you to use it as a function in JavaScript.
        // ์‹ค์ œ๋กœ ์‹คํ–‰๋  c++ ํ•จ์ˆ˜์ž…๋‹ˆ๋‹ค. / This is the c++ function that will actually be executed.
        Napi::Function::New(env, Multiply)
    );
    return exports;
}

// Node.js ๋ชจ๋“ˆ๋กœ ๋“ฑ๋ก
// Register as a Node.js module
NODE_API_MODULE(NODE_GYP_MODULE_NAME, Init)

3) binding.gyp

# GYP (Generate Your Projects): ์ด ํ˜•์‹์€ Google์—์„œ ๊ฐœ๋ฐœ๋˜์—ˆ์œผ๋ฉฐ,
# ์›๋ž˜๋Š” Chromium ํ”„๋กœ์ ํŠธ๋ฅผ ์œ„ํ•ด ์‚ฌ์šฉ๋˜์—ˆ์Šต๋‹ˆ๋‹ค.
# ์ด๋Š” ๋‹ค์–‘ํ•œ ํ”Œ๋žซํผ(macOS, Linux, Windows)์—์„œ C++ ํ”„๋กœ์ ํŠธ๋ฅผ ๋นŒ๋“œํ•˜๊ธฐ ์œ„ํ•œ ๋นŒ๋“œ ์‹œ์Šคํ…œ ์„ค์ • ์–ธ์–ด์ž…๋‹ˆ๋‹ค.

# GYP (Generate Your Projects): This format was developed by Google and was originally used for the Chromium project.
# It is a build system configuration language for building C++ projects on various platforms (macOS, Linux, Windows).

# binding.gyp
{
  "targets": [
    {
      # target_name : ์ตœ์ข…์ ์œผ๋กœ ์ƒ์„ฑ๋  .node ํŒŒ์ผ์˜ ์ด๋ฆ„ / The name of the .node file that will ultimately be created
      # sources : ๋นŒ๋“œํ•  ์†Œ์Šค ์ฝ”๋“œ ํŒŒ์ผ ๋ชฉ๋ก์„ ์ง€์ •ํ•ฉ๋‹ˆ๋‹ค. / Specifies a list of source code files to build.
      # include_dirs : C++ ์ฝ”๋“œ์—์„œ #include <napi.h>๋ฅผ ์‚ฌ์šฉํ•  ์ˆ˜ ์žˆ๋„๋ก ํ•ฉ๋‹ˆ๋‹ค. / Enables you to use #include <napi.h> in your C++ code.

      "target_name": "addon_module",
      "sources": [ "addon.cpp" ],
      "include_dirs": [
        "<!@(node -p \"require('node-addon-api').include\")"
      ],
      "defines": [ "NAPI_CPP_EXCEPTIONS" ],

      # C++ ์ปดํŒŒ์ผ๋Ÿฌ ํ”Œ๋ž˜๊ทธ ์„ค์ •: ์˜ˆ์™ธ ์ฒ˜๋ฆฌ ํ™œ์„ฑํ™” / Setting C++ Compiler Flags: Enable Exception Handling
      # -- C++์˜ ํ•ต์‹ฌ ๊ธฐ๋Šฅ์ธ try, catch, throw ๊ตฌ๋ฌธ์ด ์ปดํŒŒ์ผ๋˜์–ด ๋Ÿฐํƒ€์ž„์— ์ œ๋Œ€๋กœ ์ž‘๋™ํ•  ์ˆ˜ ์žˆ๋„๋ก ๋งŒ๋“ญ๋‹ˆ๋‹ค.
      #    The try, catch, and throw statements, which are core features of C++, are compiled so that they work properly at runtime.

      "cflags_cc": [
          "-fexceptions"
      ],

      # (macOS ์ „์šฉ) XCode/Darwin ๋นŒ๋“œ ์„ค์ • ์˜ค๋ฒ„๋ผ์ด๋“œ
      # (macOS only) Override XCode/Darwin build settings

      # ์˜ˆ์™ธ์ฒ˜๋ฆฌ ๊ธฐ๋Šฅ์ด ๋น„ํ™œ์„ฑํ™”๋˜๋ฉด ์˜ค๋ฅ˜๊ฐ€ ๋ฐœ์ƒํ•ฉ๋‹ˆ๋‹ค.
      # ์•„๋ž˜์˜ ์ฝ”๋“œ๋Š” ๋งฅos์˜ ์˜ˆ์™ธ์ฒ˜๋ฆฌ ๊ธฐ๋Šฅ์„ ํ™œ์„ฑํ™”ํ•˜๋Š” ์ฝ”๋“œ ์ž…๋‹ˆ๋‹ค.
      # An error will occur if exception handling is disabled.
      # The code below enables exception handling in macOS.

      'xcode_settings': {
          'GCC_ENABLE_CPP_EXCEPTIONS': 'YES', # C++ ์˜ˆ์™ธ ํ™œ์„ฑํ™”
          'OTHER_CPLUSPLUSFLAGS': [ '-fexceptions' ] # ์ถ”๊ฐ€ ํ”Œ๋ž˜๊ทธ
      },

      # ์•ˆ์ „์„ ์œ„ํ•ด ๊ธฐ๋ณธ -fno-exceptions๋ฅผ ์ œ๊ฑฐ (๋งŒ์•ฝ node-gyp ๊ธฐ๋ณธ ์„ค์ •์— ์žˆ๋‹ค๋ฉด)
      # Remove default -fno-exceptions for safety (if it's in node-gyp default config)

      # cflags! : C ์ปดํŒŒ์ผ๋Ÿฌ์˜ ์˜ˆ์™ธ ๋น„ํ™œ์„ฑํ™” ์˜ต์…˜ ์ œ๊ฑฐ / Removed the option to disable exceptions from the C compiler.
      # cflags_cc! : C++ ์ปดํŒŒ์ผ๋Ÿฌ์˜ ์˜ˆ์™ธ ๋น„ํ™œ์„ฑํ™” ์˜ต์…˜ ์ œ๊ฑฐ / Removed the option to disable exceptions from the C++ compiler.

      "cflags!": [ "-fno-exceptions" ],
      "cflags_cc!": [ "-fno-exceptions" ]
    }
  ]
}
โœ”๏ธ ์˜์กด์„ฑ ์„ค์น˜ / install dependencies
npm install node-gyp node-addon-api express --save

โœ”๏ธ C++ ์• ๋“œ์˜จ ๋นŒ๋“œ / C++ add-on build

npx node-gyp configure
npx node-gyp build

โœ”๏ธ ์„œ๋ฒ„์‹คํ–‰ / server run

node server.js

โœ”๏ธ ํ„ฐ๋ฏธ๋„์—์„œ ํ…Œ์ŠคํŠธ / Test in terminal

curl "http://localhost:3000/calculate?a=1203&b=4.5"

โœ”๏ธ ๋ธŒ๋ผ์šฐ์ € ํ…Œ์ŠคํŠธ ๊ฒฐ๊ณผ / Browser test results

{
  "input_a": 123,
  "input_b": 4.5,
  "source": "C++ Addon",
  "result": 553.5
}
โœ”๏ธ ์‹คํ–‰ / Run
โœ”๏ธ์„ค์ •ํŒŒ์ผ์„ ์ˆ˜์ •ํ•œ ๊ฒฝ์šฐ / If you have modified the settings file

1)๋นŒ๋“œํŒŒ์ผ์ •๋ฆฌ / Build file organization

npx node-gyp clean

2) ๊ตฌ์„ฑ ์žฌ์‹คํ–‰ / Rerun configuration

npx node-gyp configure

3) ๋นŒ๋“œ ์žฌ์‹คํ–‰ / Rerun the build

npx node-gyp build

Leave a Reply