개요
Node.js는 백엔드 애플리케이션을 작성하기 위한 매우 인기있는 JavaScript 런타임입니다. Node.js 는 그 유연성 덕분에 많은 사랑을 받았고, 다양한 곳에서 활용하고 있습니다.
하지만, JavaScript는 스크립팅 언어이기 때문에 상당히 느릴 수 있습니다. 그러나 V8 최적화 덕분에 일반적인 애플리케이션에 충분히 빠릅니다. 그렇지만 Node.js는 무거운 작업에 적합하지 않습니다. 단일 스레드이기 때문에 긴 계산을 위해 메인 스레드를 차단하는 것은 위험합니다. 이것이 바로 worker 스레드가 필요한 이유입니다. Node.js에는 worker 스레드를 지원하기 때문에 긴 계산을 수행하는 데 사용할 수 있습니다.
worker 스레드가 좋은 점이 있지만, JavaScript는 여전히 느립니다. 또한 worker 스레드는 LTS 버전의 Node에서 사용할 수 없습니다. 다행히도 Node.js를 위한 네이티브 애드온을 빌드하기 위해 Rust를 사용할 수 있습니다. 다른 방식으로는 FFI가 있습니다. 그러나 애드온 접근 방식보다 느립니다. Rust는 빠르고 fearless concurrency를 가지고 있으며, 매우 작은 런타임(또는 "not runtime")을 가지고 있기 때문에 실행 파일 크기도 꽤 작습니다.
Rust는 기본적으로 C 라이브러리를 호출하고 C에 대한 함수 내보내기를 위한 일급 지원 기능이 있습니다.
Rust는 저수준 제어와 고수준 유연성을 제공합니다. 이를 통해 메모리 관리를 제어하지만 관련된 문제 없이 이러한 제어를 사용할 수 있습니다. 또한 zero-cost 추상화를 제공하여 필요한 것만 계산하게 됩니다.
Rust는 Node.js 컨텍스트에서 여러 가지 방법으로 호출할 수 있습니다. 가장 널리 사용되는 몇 가지 방법을 아래에 나열했습니다.
•
Node.js와 Rust 사이에서 FFI를 사용할 수 있지만, 이는 매우 느립니다.
•
WebAssembly를 사용하여 node_module을 만들 수 있지만, 모든 Node.js 기능을 사용할 수는 없습니다.
•
네이티브 애드온이란 무엇인가요?
Node.js 애드온은 동적으로 링크된 C++로 작성된 공유 객체입니다. require() 함수를 사용하여 Node.js에 로드하고 일반적인 Node.js 모듈처럼 사용할 수 있습니다. 이들은 주로 JavaScript가 Node.js에서 실행되는 것과 C/C++ 라이브러리 사이의 인터페이스를 제공합니다.
네이티브 애드온은 V8 런타임에서 로드하여 다른 이진 파일과 작업하는 간단한 인터페이스를 제공합니다. 매우 빠르며, 언어 간 호출에 대해 안전합니다. 현재 Node.js는 C++ 애드온과 N-API C++/C 애드온 두 가지 유형의 애드온 방법을 지원합니다.
•
C++ 애드온
C++ 애드온은 Node.js에서 마운트하고 런타임에서 사용할 수 있는 객체입니다. C++은 컴파일된 언어이기 때문에 이러한 애드온의 속도가 매우 빠릅니다. C++에는 Node.js 생태계를 확장하기 위해 사용할 수 있는 프로덕션 레디 라이브러리가 많이 있습니다. 많은 인기있는 라이브러리가 네이티브 애드온을 사용하여 성능과 코드 품질을 개선합니다.
•
N-API C++/C 애드온
C++ 애드온의 주요 문제점은 JavaScript 런타임의 변경 사항이 있을 때마다 재컴파일해야 한다는 것입니다. 이로 인해 애드온을 유지 관리하는 문제가 발생합니다. N-API는 표준 응용 프로그램 이진 인터페이스(ABI)를 도입하여 이를 해결하려고 시도합니다. C 헤더 파일은 하위 호환성이 유지됩니다. 즉, 특정 버전의 Node.js용으로 컴파일된 애드온을 해당 버전보다 큰 모든 버전에서 사용할 수 있습니다. 이 방법은 자신의 애드온을 구현하기 위해 이 방법을 사용할 수 있습니다.
Rust의 역할은 무엇인가요?
Rust는 C 라이브러리의 동작을 모방할 수 있습니다. 즉, C가 이해하고 사용할 수 있는 형식으로 함수를 내보내고 호출하여 Node.js에서 제공되는 API를 액세스하고 사용할 수 있습니다. 이러한 API는 JavaScript 문자열, 배열, 숫자, 오류, 객체, 함수 등을 만드는 방법을 제공합니다. 하지만 Rust에게 이러한 외부 함수, 구조체, 포인터 등이 어떻게 보이는지 알려줘야 합니다.
Rust는 네이티브 애드온을 작성하기에 이상적인 언어입니다. Rust는 매우 빠르며, 메모리 안전성 및 고성능을 제공합니다. Rust 코드는 Node.js와 C++ 애드온 모두에서 사용할 수 있으며, 빠르고 안전한 호출을 통해 두 언어 간의 상호 운용성을 제공합니다.
Rust를 사용하면 C++ 애드온보다 유지 관리 측면에서 편리합니다. Rust 코드는 안전하고 메모리 누수 및 다른 보안 문제를 방지하는데 도움이 됩니다. Rust는 또한 자동 메모리 관리, 스레드 안전성, zero-cost 추상화 및 다른 고급 기능을 제공합니다.
결론적으로, Rust와 Node.js는 상호 보완적인 관계에 있습니다. Rust는 높은 성능과 안전성을 제공하고, Node.js는 유연성과 논블로킹을 사용할 수 있습니다. Rust와 Node.js의 조합을 사용하면 고성능 및 안전성을 유지하면서도 유연한 백엔드 애플리케이션을 작성할 수 있습니다.
프로젝트 설정
이 튜토리얼에서는 시스템에 Node.js와 Rust가 설치되어 있어야 하며, Cargo와 npm도 설치되어 있어야 합니다. Rust 설치를 위해서는 Rustup, Node.js 설치를 위해서는 nvm을 사용하는 것이 좋습니다.
rust-addon이라는 디렉터리를 만들고 npm init 명령어를 실행하여 새로운 npm 프로젝트를 초기화합니다. 그런 다음, cargo init --lib 명령어로 새로운 cargo 프로젝트를 초기화합니다. 프로젝트 디렉터리는 다음과 같은 모습이어야 합니다.
├── Cargo.toml
├── package.json
└── src
└── lib.rs
Shell
복사
Rust를 애드온으로 컴파일하도록 구성하기
Rust는 동적 C 라이브러리 또는 오브젝트로 컴파일되어야 합니다. cargo를 .so(Linux), .dylib(OS X), .dll(Windows) 파일로 컴파일하도록 구성합니다. Rust는 Rustc 플래그나 Cargo를 사용하여 많은 다른 유형의 라이브러리를 생성할 수 있습니다.
[package]
name = "rust-addon"
version = "0.1.0"
authors = ["Author name"]
edition = "2018"
[lib]
crate-type=["cdylib"]
[dependencies]
nodejs-sys = "0.2.0"
Shell
복사
lib 키는 Rustc를 구성하는 옵션을 제공합니다. name 키는 공유 객체의 라이브러리 이름을 lib{name} 형식으로 제공하고 type은 컴파일할 라이브러리의 유형을 제공합니다. 예를 들어 cdylib은 동적으로 링크된 C 라이브러리를 생성합니다. 이 공유 객체는 C 라이브러리와 같은 동작을 합니다.
N-API로 시작하기
N-API 라이브러리를 만듭니다. 먼저 의존성을 추가해야 합니다. nodejs-sys는 napi-header 파일에 필요한 바인딩을 제공합니다. napi_register_module_v1은 애드온의 진입점입니다. N-API 문서에서는 모듈 등록을 위해 N-API_MODULE_INIT 매크로를 권장합니다. 이 매크로는 napi_register_module_v1 함수로 컴파일됩니다.
Node.js는 이 함수를 호출하고 napi_env라는 불투명한 포인터를 제공합니다. 이는 JavaScript 런타임에서 모듈의 구성을 나타냅니다. 그리고 napi_value라는 또 다른 불투명한 포인터를 제공합니다.
이 포인터는 JavaScript 값, 실제로는 익스포트라고 하는 객체를 나타냅니다. 이러한 익스포트는 require 함수가 JavaScript에서 Node.js 모듈에 제공하는 것과 동일합니다.
use nodejs_sys::{napi_create_string_utf8, napi_env, napi_set_named_property, napi_value};
use std::ffi::CString;
#[no_mangle]
pub unsafe extern "C" fn napi_register_module_v1(
env: napi_env,
exports: napi_value,
) -> nodejs_sys::napi_value {
// C 문자열 생성
let key = CString::new("hello").expect("CString::new failed");
// napi_value를 저장할 메모리 위치 생성
let mut local: napi_value = std::mem::zeroed();
// C 문자열 생성
let value = CString::new("world!").expect("CString::new failed");
// 문자열을 위한 napi_value 생성
napi_create_string_utf8(env, value.as_ptr(), 6, &mut local);
// exports 객체에 문자열 추가
napi_set_named_property(env, exports, key.as_ptr(), local);
// 객체 반환
exports
}
Rust
복사
Rust는 소유된 문자열을 String 타입으로 나타내고, 문자열의 대여 슬라이스를 str 원시 타입으로 나타냅니다. 이 두 가지 모두 항상 UTF-8 인코딩이며 중간에 null 바이트를 포함할 수 있습니다. 문자열을 구성하는 바이트를 살펴보면 그 중에 \0이 있을 수 있습니다. String과 str 모두 길이를 명시적으로 저장합니다. 문자열 끝에는 C 문자열과 달리 null 종료 문자가 없습니다.
Rust 문자열은 C의 문자열과 매우 다르므로 N-API 함수와 함께 사용하기 전에 Rust 문자열을 C 문자열로 변경해야 합니다. exports는 exports로 나타나는 객체이므로 함수, 문자열, 배열 또는 기타 JavaScript 객체를 키-값 쌍으로 추가할 수 있습니다.
JavaScript 객체에 키를 추가하려면 N-API에서 제공하는 napi_set_named_property 메서드를 사용할 수 있습니다. 이 함수는 우리가 속성을 추가하려는 객체, 속성으로 사용할 문자열을 가리키는 포인터, JavaScript 값(문자열, 배열 등)을 가리키는 포인터 및 Rust와 Node.js 사이에 앵커 역할을 하는 napi_env를 인수로 받습니다.
N-API 함수를 사용하여 JavaScript 값 생성 가능합니다. 예를 들어, 여기에서 napi_create_string_utf8를 사용하여 문자열을 생성했습니다. 우리는 문자열의 포인터, 길이 및 새롭게 생성된 값을 저장할 빈 메모리 위치의 포인터를 환경에 전달했습니다. 이 코드는 Rust 보증을 제공할 수 없는 많은 외부 함수 호출을 포함하므로 안전하지 않습니다. 마지막으로, 우리는 world! 값을 가진 속성을 설정하여 제공된 모듈을 반환했습니다.
알아야 할 중요한 점은 nodejs-sys가 사용하는 함수의 구현이 아닌 필요한 정의만 제공한다는 것입니다. N-API 구현은 Node.js에 포함되어 있으며 Rust 코드에서 호출할 수 있습니다.
Node.js에서 Addon 사용하기
다음 단계는 다른 운영 체제에서 N-API 파일을 링크하기 위해 링크 구성을 추가하고 컴파일하는 것입니다.
build.rs 파일을 만들어 다양한 운영 체제에서 N-API 파일을 링크하기 위한 몇 가지 구성 플래그를 추가합니다.
fn main() {
println!("cargo:rustc-cdylib-link-arg=-undefined");
if cfg!(target_os = "macos") {
println!("cargo:rustc-cdylib-link-arg=dynamic_lookup");
}
}
Rust
복사
디렉토리는 다음과 같아야합니다.
├── build.rs
├── Cargo.lock
├── Cargo.toml
├── index.node
├── package.json
├── src
└── lib.rs
Rust
복사
이제 Rust addon을 컴파일해야합니다. 이것은 간단한 명령 cargo build --release을 사용하여 쉽게 할 수 있습니다. 이것은 처음 실행될 때 시간이 걸릴 수 있습니다.
모듈이 컴파일되면 ./target/release/libnative.so에서 바이너리의 복사본을 만들어 루트 디렉토리에 index.node로 이름을 바꾸어야합니다. cargo에 의해 생성된 바이너리는 크레이트 설정 및 운영 체제에 따라 다른 확장명이나 이름을 가질 수 있습니다.
이제 Node.js에서 파일을 require하고 사용할 수 있습니다. 또한 스크립트에서도 사용할 수 있습니다. 예를 들면:
let addon=require('./index.node');
console.log(addon.hello);
Rust
복사
Addon을 Node.js에서 사용하기
다음으로 함수, 배열 및 프로미스를 생성하고 libuv 스레드 풀을 사용하여 주요 스레드를 차단하지 않고 무거운 작업을 수행하는 방법으로 이동하겠습니다.
N-API에 대한 깊은 이해
이제 N-API와 Rust를 사용하여 일반적인 패턴을 구현하는 방법을 알았으니 이제 사용자가 라이브러리 또는 Node 모듈에서 호출할 수 있는 export 함수를 만드는 일반적인 패턴을 만들어보겠습니다. 함수를 만드는 것부터 시작해 보겠습니다.
napi_create_function을 사용하여 함수를 만들어야합니다. 이렇게 하면 Node.js에서 이러한 함수를 사용할 수 있습니다. 이러한 함수를 exports 속성으로 추가하여 JavaScript에서 사용할 수 있습니다.
함수 만들기
JavaScript 함수는 napi_value 포인터로도 나타낼 수 있습니다. N-API 함수는 꽤 쉽게 만들고 사용할 수 있습니다.
use nodejs_sys::{
napi_callback_info, napi_create_function, napi_create_string_utf8, napi_env,
napi_set_named_property, napi_value,
};
use std::ffi::CString;
pub unsafe extern "C" fn say_hello(env: napi_env, _info: napi_callback_info) -> napi_value {
// creating a javastring string
let mut local: napi_value = std::mem::zeroed();
let p = CString::new("Hello from rust").expect("CString::new failed");
napi_create_string_utf8(env, p.as_ptr(), 13, &mut local);
// returning the javascript string
local
}
#[no_mangle]
pub unsafe extern "C" fn napi_register_module_v1(
env: napi_env,
exports: napi_value,
) -> nodejs_sys::napi_value {
// creating a C String
let p = CString::new("myFunc").expect("CString::new failed");
// creating a location where pointer to napi_value be written
let mut local: napi_value = std::mem::zeroed();
napi_create_function(
env,
// pointer to function name
p.as_ptr(),
// length of function name
5,
// rust function
Some(say_hello),
// context which can be accessed by the rust function
std::ptr::null_mut(),
// output napi_value
&mut local,
);
// set function as property
napi_set_named_property(env, exports, p.as_ptr(), local);
// returning exports
exports
}
Rust
복사
N-API로 만든 함수
위의 예제에서는 JavaScript에서 함수를 호출할 때 실행되는 Rust에서 say_hello라는 함수를 만들었습니다. 우리는 napi_create_function을 사용하여 함수를 만들었으며 다음 인수를 사용했습니다.
환경의 napi_env 값
•
JavaScript 함수에 제공할 이름 문자열
•
함수 이름 문자열의 길이
•
JavaScript에서 새로 생성된 함수를 호출할 때 실행되는 함수
•
이후에 사용자가 전달하고 Rust 함수에서 액세스할 수 있는 컨텍스트 데이터
•
JavaScript 함수의 포인터를 저장할 수 있는 빈 메모리 주소
이 함수를 만들면 exports 객체에 속성으로 추가하여 JavaScript에서 사용할 수 있습니다.
Rust 측의 함수는 위의 예제에서 보여진 것과 같은 시그니처를 가져야합니다. 다음으로 napi_callback_info를 사용하여 함수 내부에서 인수에 액세스하는 방법에 대해 설명하겠습니다. 함수 내부와 다른 인수에서도 이를 액세스할 수 있습니다.
코드
napi_callback_info를 사용한 함수 인수 액세스 위의 예제에서는 napi_callback_info를 사용하여 Rust 함수 내부에서 전달된 인수를 액세스하는 방법을 보여줍니다. 이를 수행하기 위해 napi_get_cb_info를 호출하고 결과를 사용합니다. 이 호출로부터 우리는 다음 정보를 얻을 수 있습니다.
•
전달된 인수의 수
•
전달된 인수 목록
•
this 객체
•
사용자 지정 데이터
그 다음에는 napi_get_value_string_utf8를 사용하여 각 인수에 대한 정보를 가져올 수 있습니다. 인수가 문자열인 경우 이를 CString으로 변환한 다음 napi_create_string_utf8를 사용하여 JavaScript에서 사용할 수 있는 문자열로 다시 변환합니다.
이제 JavaScript에서 함수를 호출하면이 함수가 실행되고, 전달된 인수를 확인하고 Rust 코드를 실행하고 JavaScript에서 사용할 수 있는 결과를 반환합니다.
다음으로, Rust에서 배열을 만들고 JavaScript에서 사용할 수 있는 방법을 살펴보겠습니다.
인자 접근
함수 인자는 매우 중요합니다. N-API는 JavaScript 측 함수에 대한 자세한 정보를 제공하는 napi_callback_info를 제공하여 이러한 인수에 액세스하는 방법을 제공합니다.
코드
N-API로 인자에 액세스하려면 napi_get_cb_info를 사용합니다. 다음 인수를 제공해야 합니다.
•
napi_env
•
info 포인터
•
예상 인수 수
•
napi_value로 쓸 수 있는 버퍼
•
JavaScript 함수가 생성될 때 사용자가 제공한 메타데이터를 저장할 메모리 위치
•
this 값 포인터를 쓸 수 있는 메모리 위치
C에서 포인터를 인자로 쓸 수 있는 배열을 만들고 이 배열의 포인터를 N-API 함수에 전달해야 합니다. 마지막 인자는 우리가 이 예제에서 사용하지 않는 것입니다.
Promise와 libuv 스레드 풀을 사용하여 작업하는 방법
Node.js의 메인 스레드를 계산에 차단하는 것은 좋지 않습니다. libuv 스레드를 사용하여 무거운 작업을 수행할 수 있습니다.
먼저, 프로미스를 만듭니다. 프로미스는 작업의 성공 여부에 따라 거부되거나 해결됩니다. 이를 위해 세 개의 함수를 만들어야합니다. 첫 번째 함수는 JavaScript 세계에서 호출되며 두 번째 함수는 libuv 스레드에서 실행되며 JavaScript에 액세스 할 수 없습니다. 세 번째 함수는 두 번째 함수가 완료되면 호출됩니다. libuv 스레드에서 작업을 수행하기 위해 napi_create_async_work 메서드를 사용할 수 있습니다.
프로미스 생성
프로미스를 만들려면 napi_create_promise를 사용하면됩니다. 이렇게하면 napi_deferred 포인터가 제공되며 다음 함수를 사용하여 프로미스를 해결하거나 거부 할 수 있습니다.
napi_resolve_deferred
napi_reject_deferred
오류 처리
napi_create_error와 napi_throw_error를 사용하여 Rust 코드에서 오류를 생성하고 throw 할 수 있습니다. 모든 N-API 함수는 확인해야하는 napi_status를 반환합니다.
실제 코드
다음 예제는 비동기 작업을 예약하는 방법을 보여줍니다.
코드
feb function
코드
perform function
코드
complete function
코드
Conclusion
N-API로 할 수 있는 일에 대해서는 이 글이 그저 대략적인 내용일 뿐입니다. 몇 가지 패턴과 함수를 수행하는 방법, 예를 들어 문자열, 숫자, 배열, 객체 등을 생성하는 방법, 함수에서 전달받은 인자와 this를 가져오는 방법 등 기본적인 내용들을 다뤘습니다.
또한, libuv 스레드를 사용하고, 무거운 계산을 백그라운드에서 수행하기 위해 async_work를 만드는 방법을 깊이있게 살펴보았습니다. 마지막으로, 우리는 JavaScript의 프로미스를 만들고 사용하며 N-API에서 에러 처리하는 방법도 배웠습니다.
코드를 직접 작성하기 싫다면, 많은 라이브러리가 있습니다. 이들은 편리한 추상화를 제공하지만, 모든 기능을 지원하지는 않는다는 단점이 있습니다.