Nasza strona używa cookies. Dowiedz się więcej o celu ich używania i zmianie ustawień w przeglądarce. Korzystając ze strony, wyrażasz zgodę na używanie cookies, zgodnie z aktualnymi ustawieniami przeglądarki. Rozumiem

Rozszerzanie Node.js natywnymi modułami C++

Marcin Baraniecki Front-end Lion / SoftwareMill
Sprawdź, jak rozszerzyć Node.js o moduły natywne skompilowanego kodu C/C++.
Rozszerzanie Node.js natywnymi modułami C++

Node.js może nie tylko ładować biblioteki JavaScript, ale także zostać rozszerzony o moduły natywne (skompilowany kod C/C++). Chociaż nie oznacza to, że należy wyrzucić istniejące moduły JavaScript na rzecz starego dobrego C++. Wiedza ta może okazać się przydatną w kilku konkretnych przypadkach.


Jestem programistą JavaScript, dlaczego miałbym używać C++?

Po pierwsze

Masz bezpośredni dostęp do istniejących (starszych) bibliotek C/C++. Zamiast wywoływać je jak aplikacje zewnętrzne w stylu „wykonaj komendę”, sięgnij bezpośrednio po istniejący kod źródłowy i przekaż wyniki z powrotem do Node.js w formie zrozumiałej dla środowiska wykonawczego JavaScript. W ten sposób możesz również uzyskać dostęp do niskopoziomowego API systemu operacyjnego.

Po drugie

Wydajność. W wielu sytuacjach dobrze napisany kod natywny może okazać się szybszy i bardziej wydajny niż odpowiednik w JavaScripcie.

Disclaimer: Niektórzy z Was mogą być zaskoczeni, że stawiam wydajność jako drugą, ale zrobiłem to celowo - ogólnie lepsza wydajność kodu natywnego nie powinna być powodem ponownego przepisywania każdego fragmentu kodu JavaScript do odpowiednika w C/C ++. Istnieje wiele przypadków, w których zwykły kod JavaScript będzie nadal bardziej skuteczny, niż ponowne odkrycie koła w formie kodu C/C ++, napisanego przez niedoświadczonego programistę.

Uważaj - przechodzenie na niższy poziom może skończyć się tak, jak odpicowanie starej bryki w warsztacie. Zamontowanie nitro w Twoim starym silniku może mieć katastrofalne skutki:

Ten koleś był zbyt entuzjastycznie nastawiony do modułów C ++ w aplikacjach Node.js.


Witaj, (natywny) świecie!

Wystarczy już tych ostrzeżeń, jesteś już gotowy na swoje pierwsze wiersze kodu C++, które zostaną skompilowane do natywnego modułu Node’a.

Na potrzeby tego artykułu będę używał natywnych abstrakcji dla Node.js (Nan), który jest zasadniczo plikiem nagłówkowym C++, zapewniającym namespace’y i zestaw przydatnych makr dla łatwiejszej interakcji z API w V8 (silnik JavaScript używany w Node.js). Sprawia to, że Twój kod jest odporny na przyszłe zmiany (w przypadku nowych wersji Node.js), ponieważ API w V8 mają tendencję do dramatycznych zmian między wersjami. Nan jest zalecanym rozwiązaniem w docsach API node.js.

Utwórz plik .cpp (nazwałem go main.cpp) i wypełnij go następującym fragmentem kodu:

#include <nan.h>

// NAN_METHOD is a Nan macro enabling convenient way of creating native node functions.
// It takes a method's name as a param. By C++ convention, I used the Capital cased name.
NAN_METHOD(Hello) {
    // Create an instance of V8's String type
    auto message = Nan::New("Hello from C++!").ToLocalChecked();
    // 'info' is a macro's "implicit" parameter - it's a bridge object between C++ and JavaScript runtimes
    // You would use info to both extract the parameters passed to a function as well as set the return value.
    info.GetReturnValue().Set(message);
}

// Module initialization logic
NAN_MODULE_INIT(Initialize) {
    // Export the `Hello` function (equivalent to `export function Hello (...)` in JS)
    NAN_EXPORT(target, Hello);
}

// Create the module called "addon" and initialize it with `Initialize` function (created with NAN_MODULE_INIT macro)
NODE_MODULE(addon, Initialize);

Hello, (native) world!


Zauważ, że metody utworzone w C++ nie zwracają żadnej wartości jawnie. Zamiast tego zwracana wartość jest ustawiana na "pośredniczącym" obiekcie info (referencji do typu Nan::FunctionCallbackInfo, “pośrednio” przekazanej w marko NAN_METHOD).

Aby uruchomić kod, nadal musisz go skompilować. Nie bój się, nie musisz ręcznie wywoływać kompilatora C++. Node.js Cię wyręczy.

Na początek zainicjuj projekt Node.js w tym samym katalogu, w którym znajduje się plik main.cpp:

npm init -y


Dalej, zainstaluj Nan i node-gyp (znasz już Nan; node-gyp to zestaw narzędzi dla kompilacji kodu natywnego):

npm install nan node-gyp --save


Zaktualizuj plik package.json, aby zawierał następujące wiersze (wersje Twoich zależności mogą się nieznacznie różnić, ale to całkowicie w porządku!):

{
  "name": "node-native-addons-example",
  "version": "1.0.0",
  "dependencies": {
    "nan": "^2.6.1",
    "node-gyp": "^3.6.0"
  },
  "scripts": {
    "compile": "node-gyp rebuild",
    "start": "node main.js"
  },
  "gypfile": true
}


Ważną częścią jest tutaj sekcja “scripts”:

  • :compile": “node-gyp rebuild” - dla kompilacji kodu C++
  • "start": "node main.js" - dla naszego głównego skryptu wykonawczego

Teraz utwórz plik bindings.gyp - to rodzaj pliku konfiguracyjnego dla node-gyp. Zwróć uwagę, jak używa nazwy pliku „main.cpp” (źródła „hello world”) w tablicy „sources”:

{
  "targets": [
    {
      "include_dirs": [
        "<!(node -e \"require('nan')\")"
      ],
      "target_name": "addon",
      "sources": [ "main.cpp" ]
    }
  ]
}


Te trzy narzędzia będą musiały być zainstalowane na Twoim komputerze (na MacOS nie powinieneś mieć problemów - są one zainstalowane fabrycznie; są również łatwe do zainstalowania na Linuksie, np. na Ubuntu z apt-get):

  • make
  • g++
  • python 2.7


Jeśli czegoś brakuje, node-gyp to zgłosi, więc nie trać czasu, zastanawiając się, czy masz je w swoim systemie operacyjnym, czy nie.

Teraz nadszedł czas na pierwszą kompilację - npm run compile (pamiętaj, uruchomi ona node-gyp i skompiluje źródła C++ tak jak skonfigurowano w pliku bindings.gyp). Jeśli wszystko przebiegnie gładko, powinieneś zobaczyć podobny output:

 node-native-addons-example@1.0.0 compile /Users/marcin/projects/node-native-addons-example
> node-gyp rebuild
CXX(target) Release/obj.target/addon/main.o
 SOLINK_MODULE(target) Release/addon.node


Super, masz tu swój pierwszy skompilowany i gotowy do użycia moduł natywny. Stwórzmy plik JavaScript, który będzie uruchamiany z Node.js.

// note that the compiled addon is placed under following path
const {Hello} = require('./build/Release/addon');

// `Hello` function returns a string, so we have to console.log it!
console.log(Hello());


Woohoo! Jeśli wszystko poszło dobrze, powinieneś zobaczyć każdą wiadomość, którą zwróciłeś ze swojego dodatku C++ w konsoli.


Urocze, ale co powiesz na coś bardziej użytecznego?

Oczywiście. Chciałbyś zobaczyć prawdziwą moc natywnych modułów.

W tym celu zaimplementujemy prostą funkcję, która sprawdzi, czy podana liczba jest liczbą pierwszą i zwraca wartość logiczną (prawda lub fałsz). Będziemy używać tego samego algorytmu zarówno dla wersji C++, jak i funkcji JavaScript, i ostatecznie porównamy czasy wykonania z tą samą, stosunkowo dużą liczbą.

Algorytm:

  • najpierw sprawdzi, czy jedynym argumentem jest liczba (jeśli nie - wyrzuci TypeError);
  • po drugie sprawdzi, czy liczba jest mniejsza niż 2 (jeśli tak, zwróć true);
  • po trzecie, będzie iterować w zakresie od 2 do jakiejś liczby (wyłącznie) i sprawdzi, czy operacja modulo daje zero (jeśli tak, przerwie pętlę i zwróci false, ponieważ taka liczba nie jest liczbą pierwszą);
  • zwróci true na samym końcu - liczbie udało się przedostać przez pętle, więc musi być pierwsza.


(dla uproszczenia celowo pominąłem sprawdzenie liczb całkowitych lub ujemnych; Potraktuj to jako ćwiczenie!)

Utwórz plik: isPrime.js :

module.exports = (number) => {
  if (typeof number !== 'number') {
    throw new TypeError('argument must be a number!');
  }

  if (number < 2) {
    return false;
  }

  for (let i = 2; i < number; i++) {
    if (number % i === 0) {
      return false;
    }
  }

  return true;
};


Teraz dla pliku main.cpp zastąp jego zawartość następującym kodem (zauważ, że usunąłem funkcję Hello):

#include <nan.h>

NAN_METHOD(IsPrime) {
    if (!info[0]->IsNumber()) {
        Nan::ThrowTypeError("argument must be a number!");
        return;
    }
    
    int number = (int) info[0]->NumberValue();
    
    if (number < 2) {
        info.GetReturnValue().Set(Nan::False());
        return;
    }
    
    for (int i = 2; i < number; i++) {
        if (number % i == 0) {
            info.GetReturnValue().Set(Nan::False());
            return;
        }
    }
    
    info.GetReturnValue().Set(Nan::True());
}

NAN_MODULE_INIT(Initialize) {
    NAN_EXPORT(target, IsPrime);
}

NODE_MODULE(addon, Initialize);


Otwórz swój plik main.js i zamień istniejące treści następującym kodem:

const {IsPrime} = require('./build/Release/addon'); // native c++
const isPrime = require('./isPrime'); // js

const number = 654188429; // thirty-fifth million first prime number (see https://primes.utm.edu/lists/small/millions/)
const NATIVE = 'native';
const JS = 'js';

console.time(NATIVE);
console.log(`${NATIVE}: checking whether ${number} is prime... ${IsPrime(number)}`);
console.timeEnd(NATIVE);
console.log('');
console.time(JS);
console.log(`${JS}: checking whether ${number} is prime... ${isPrime(number)}`);
console.timeEnd(JS);


Odpal npm run compile, by skompilować moduł C++ oraz npm start, by wykonać plik main.js. Wykonujemy zarówno funkcje C++, jak i JavaScript, aby określić, czy 654188429 (dość duża liczba) jest liczbą pierwszą, czy nie.

Zajmuje to średnio 1800 ms dla funkcji C++ i 3100 ms dla funkcji JavaScript w moim Macbooku Pro 2015 (macOS Sierra 10.12, Intel i5, 8 GB RAM), co oznacza, że wersja algorytmu C++ jest 1,7 razy szybsza niż jego odpowiednik JavaScript.

Repo z kompletnym przykładem jest dostępne na moim Githubie.


Wniosek

W tym artykule chciałem pokazać, że platforma Node.js jest otwarta na natywne rozszerzenia niskiego poziomu. Jednak proces pisania wymaga pewnego przygotowania i dodatkowego oprzyrządowania. Chociaż kod natywny może być rzeczywiście bardziej wydajny, nadal powinieneś preferować rozwiązania JavaScript, chyba że istnieje pilna potrzeba dokonania dużych optymalizacji lub uzyskania dostępu do niskopoziomowych API.

Linki

Masz coś do powiedzenia?

Podziel się tym z 120 tysiącami naszych czytelników

Dowiedz się więcej
Rocket dog