일.모듈 타입
Node.js 는 기본적으로 2 가지 모듈을 지원합니다:
-
코어 모듈 (Core Modules): 바이너리로 컴파일되며, 소스 코드는 lib/ 디렉토리에 위치
-
파일 모듈 (File Modules): JavaScript 파일 (
.js), JSON 파일 (.json), C++ 확장 파일 (.node) 포함
쉬운 것에서 어려운 것으로, 먼저 가장 자주 접하는 JS 모듈부터 보다
이.JS 모듈
[caption id="attachment_2169" align="alignnone" width="496"]
js module[/caption]
한 가지 세부 사항에 주의:모듈 파일을 로딩&실행하기 전에module 인스턴스를 캐시하는 것이지, 후에 캐시하는 것이 아닙니다. 이것이Node.js 가 순환 의존에从容히 대처할 수 있는 근본적인 이유입니다:
When there are circular require() calls, a module might not have finished executing when it is returned.
모듈 로딩 프로세스 중에 순환 참조가 발생하여, 아직 로딩이 완료되지 않은 모듈이 참조된 경우, 도시된 모듈 로딩 플로우에 따라도 캐시에 히트합니다 (무한 재귀에 들어가지 않음). 비록 이때의module.exports 가 완전하지 않을지라도 (모듈 코드가 실행이 끝나지 않아, 몇 가지 것이 아직挂载되지 않음)
P.S. 모듈 식별자에 기반하여 대응하는 모듈 (엔트리) 파일의 절대 경로를 찾는 방법, 동명 모듈 로딩 우선순위, 및 관련 Node.js 소스 코드 해석에 대해서는, [Node 모듈 로딩 메커니즘](/articles/node 모듈 로딩 메커니즘/) 참조
삼.JSON 모듈
JS 모듈과 유사하게, JSON 파일도 모듈로서 직접require 로 로딩할 수 있습니다. 구체적인 플로우는 다음과 같습니다:
[caption id="attachment_2170" align="alignnone" width="541"]
json module[/caption]
로딩&실행 방식이 다른 것을 제외하고, JS 모듈의 로딩 플로우와 완전히 일치
사.C++ 확장 모듈
JS, JSON 모듈과 비교하여, C++ 확장 모듈 (.node) 의 로딩 프로세스는 C++ 층과의 관계가 더욱 밀접합니다:
[caption id="attachment_2171" align="alignnone" width="532"]
addon module[/caption]
JS 층의 처리 플로우는process.dlopen() 까지이며, 실제 로딩, 실행, 및 확장 모듈이 노출하는 속성/메서드를 어떻게 JS 런타임에 전달하는지는, 모두 C++ 층에 의해 완료됩니다:
[caption id="attachment_2172" align="alignnone" width="625"]
addon module cpp[/caption]
핵심은 dlopen()/uv_dlopen 을 통해 C++ 동적 링크 라이브러리 (즉.node 파일) 를 로딩하는 것입니다. 관련 Node.js 소스 코드는 다음과 같습니다 (Node v14.0.0):
-
모듈 로딩: DLOpen, DLib::Open, DLib::Close
-
모듈 자기 등록: NODE_MODULE 매크로, node_module_register
외부에서 확장 모듈의module 인스턴스를 가져올 수 있는 것은, 확장 모듈에 자기 등록 메커니즘이 있기 때문입니다:
// 모듈 등록 시
extern "C" void node_module_register(void* m) {
struct node_module* mp = reinterpret_cast<struct node_module*>(m);
if (mp->nm_flags & NM_F_INTERNAL) {
mp->nm_link = modlist_internal;
modlist_internal = mp;
} else if (!node_is_initialized) {
// "Linked" modules are included as part of the node project.
// Like builtins they are registered *before* node::Init runs.
mp->nm_flags = NM_F_LINKED;
mp->nm_link = modlist_linked;
modlist_linked = mp;
} else {
// 모듈 인스턴스를 글로벌 변수에挂载하여, 노출
thread_local_modpending = mp;
}
}
// 모듈 로딩 시
void DLOpen(const FunctionCallbackInfo<Value>& args) {
/* ...일부 비핵심 코드 생략 */
const bool is_opened = dlib->Open();
// 동적 링크 라이브러리 로딩 후, 글로벌 변수를 읽어, 모듈 인스턴스를 꺼냄
node_module* mp = thread_local_modpending;
thread_local_modpending = nullptr;
// 마지막으로 exports 와 module 을 모듈 엔트리 함수에 전달하여, 모듈이 노출하는 속성/메서드를 꺼냄
if (mp->nm_context_register_func != nullptr) {
mp->nm_context_register_func(exports, module, context, mp->nm_priv);
} else if (mp->nm_register_func != nullptr) {
mp->nm_register_func(exports, module, mp->nm_priv);
}
}
P.S. C++ 확장 모듈 개발, 컴파일, 실행에 대한 상세 정보는, [Node.js C++ 확장 입문 가이드](/articles/node-js-c 확장 입문 가이드/) 참조
오.코어 모듈
C++ 확장 모듈과 유사하게, 코어 모듈의 구현은 대부분이 대응하는 하층 C++ 모듈에 의존합니다 (파일 I/O, 네트워크 요청, 암호화/복호화 등). 단지 JS 를 통해 사용자용 상위 인터페이스 (fs.writeFile, fs.writeFileSync 등) 를 캡슐화했을 뿐입니다
본질적으로는 모두 C++ 클래스 라이브러리로, 가장 주된 차이점은 코어 모듈이 Node.js 설치 패키지에 컴파일된다는 것 (상위 캡슐화된 JS 코드를 포함하며, 컴파일 시 이미 실행 파일에 링크되어 있음) 이며, 확장 모듈은 런타임에 동적으로 로딩해야 한다는 것입니다
P.S. C++ 동적 링크 라이브러리, 정적 라이브러리에 대한 상세 정보는, [Node.js C++ 확장 입문 가이드](/articles/node-js-c 확장 입문 가이드/#articleHeader1) 참조
따라서, 앞의 몇 가지 모듈과 비교하여, 코어 모듈의 로딩 프로세스는 조금 더 복잡하며, 4 부분으로 나뉩니다:
-
(사전 컴파일 단계) JS 코드 "컴파일"
-
(시작 시) JS 코드 로딩
-
(시작 시) C++ 모듈 등록
-
(런타임 시) 코어 모듈 로딩 (JS 코드 및 그것이 참조하는 C++ 모듈 포함)
[caption id="attachment_2173" align="alignnone" width="625"]
core module[/caption]
그 중 비교적 재미있는 것은 JS2C 변환과 코어 C++ 모듈 등록 두 부분입니다
JS2C 변환
컴파일 전 전처리를 통해, 코어 모듈의 JS 코드 부분이 C++ 파일 (./out/Release/obj/gen/node_javascript.cc 에 위치) 로 변환되어, 실행 파일에打入됩니다:
NativeModule: a minimal module system used to load the JavaScript core modules found in lib/**/*.js and deps/**/*.js. All core modules are compiled into the node binary via node_javascript.cc generated by js2c.py, so they can be loaded faster without the cost of I/O. This class makes the lib/internal/*, deps/internal/* modules and internalBinding() available by default to core modules, and lets the core modules require itself via require('internal/bootstrap/loaders') even when this file is not written in CommonJS style.
(node/lib/internal/bootstrap/loaders.js 에서 인용)
생성된node_javascript.cc 의 주요 내용은 다음과 같습니다:
static const uint8_t internal_bootstrap_environment_raw[] = {
39,117,115,101, 32,115,116,114,105, 99,116, 39, 59, 10, 10, 47, 47, 32, 84,104,105,115, 32,114,117,110,115, 32,110,101,
99,101,115,115, 97,114,121, 32,112,114,101,112, 97,114, 97,116,105,111,110,115, 32,116,111, 32,112,114,101,112, 97,114
// ...
}
void NativeModuleLoader::LoadJavaScriptSource() {
source_.emplace("internal/bootstrap/environment", UnionBytes{internal_bootstrap_environment_raw, 374});
source_.emplace("internal/bootstrap/loaders", UnionBytes{internal_bootstrap_loaders_raw, 10110});
// ...
}
UnionBytes NativeModuleLoader::GetConfig() {
return UnionBytes(config_raw, 3030); // config.gypi
}
즉, 소스 코드를 아무리 찾아도 찾을 수 없는LoadJavaScriptSource 는, 실제로는 사전 컴파일 단계에서 자동 생성되는 것입니다:
// ref https://github.com/nodejs/node/blob/v14.0.0/src/node_native_module.cc#L24
NativeModuleLoader::NativeModuleLoader() : config_(GetConfig()) {
// 該 함수의 구현은 소스 코드 중에 없고, 컴파일 생성된 node_javascript.cc 중에 위치
LoadJavaScriptSource();
}
코어 C++ 모듈 등록
모든 코어 모듈이 의존하는 C++ 부분 코드 끝에는 한 줄의 등록 코드가 있습니다. 예를 들어:
// src/node_file.cc
NODE_MODULE_CONTEXT_AWARE_INTERNAL(fs, node::fs::Initialize)
// src/timers.cc
NODE_MODULE_CONTEXT_AWARE_INTERNAL(timers, node::Initialize)
// src/js_stream.cc
NODE_MODULE_CONTEXT_AWARE_INTERNAL(js_stream, node::JSStream::Initialize)
NODE_MODULE_CONTEXT_AWARE_INTERNAL 매크로를 전개하면 node_module_register 가 되며, 등록된 C++ 모듈을modlist_internal 링크드 리스트에 기록합니다:
extern "C" void node_module_register(void* m) {
struct node_module* mp = reinterpret_cast<struct node_module*>(m);
if (mp->nm_flags & NM_F_INTERNAL) {
// 내부 C++ 모듈 기록
mp->nm_link = modlist_internal;
modlist_internal = mp;
} else if (!node_is_initialized) {
// "Linked" modules are included as part of the node project.
// Like builtins they are registered *before* node::Init runs.
mp->nm_flags = NM_F_LINKED;
mp->nm_link = modlist_linked;
modlist_linked = mp;
} else {
thread_local_modpending = mp;
}
}
런타임에 internalBinding 을 통해 이러한 내장 C++ 모듈을 로딩합니다
관련 Node.js 소스 코드는 다음과 같습니다 (Node v14.0.0):
-
JS 층 모듈 로딩: Module._load, loadNativeModule, compileForInternalLoader, nativeModuleRequire, internalBinding
-
JS2C 변환: tools/js2c.py, LoadJavaScriptSource, NativeModule.map, moduleIds, ModuleIdsGetter, GetModuleIds
-
코어 C++ 모듈 등록: NODE_MODULE_CONTEXT_AWARE_INTERNAL, node_module_register, InitModule
-
C++ 층 모듈 로딩: internalBinding, getInternalBinding, FindModule, InitModule
아직 댓글이 없습니다