花了差不多兩周時間過了下primer C++5th,完成了《C++從入門到精通》。(手動滑稽)
這兩天看了下node源碼的一些入口方法,其實還是比較懵逼的,語法倒不是難點,主要是大量的宏造成直接閱讀上的不方便。
有些宏感覺真是一點鳥用都沒有,比如說:
#define LIKELY(expr) expr #define UNLIKELY(expr) expr #define PRETTY_FUNCTION_NAME ""
這玩意翻譯成JS大概就是:
const LIKELY = (expr) => expr;
JS中有些情況確實是需要這么一個函數,比如vue組件中的data,主要是為了防止復雜類型的引用問題。但是在C++我就不明白了,深拷貝簡直不要太簡單,可能這里為了強行語義化吧。
不過好在源碼的規范超級棒,比如說:
1、內置常量宏都是全大寫多單詞下划線連接:FILE_TYPE_UNKNOWN代表無法識別的文件類型,對應16進制數字0x0000
2、函數名基本上能猜到用處:IsWindow7OrGreater函數判斷操作系統是不是win7版本以上
3、注釋隨處可見
4、代碼結構划分非常清晰,部分看不懂(或者不想看)的可以直接跳過,不影響后續內容理解
另外IDE也好使,直接幫我把LINUX系統的兼容代碼直接置灰了,跳轉大部分情況也很好用。
本來打算從簡單一點的API來講,比如說require方法是如何運作的,但是這東西過於簡單,百度一搜一大把,已經是被人寫爛的內容,所以不打算開這個坑(更重要的是那玩意是JS,如何對得起我學了兩周C++)。
因此我決定開一個目錄坑,總覽一下node從啟動到運行,哪些部分我能看懂,理一理。
正文開始
聲明:本文目前基於node-v10.1.0,未來如有更改會說明。代碼來源於官網下載的源碼壓縮包,詳情見上一篇文章。
首先上個圖瞧一眼大概的流程:
是不是很少很簡單?對啊,因為我只能看懂這么多……
開始吹。
首先每個C++項目都有一個啟動項目,每個項目里有一個主函數,當初我以為只能叫main,后來發現wmain也是OK的。
而在node中,wmain是windows系統的主函數,main是LINUX系統的主函數。區分系統的關鍵在於一個特定的宏,windows是_WIN32,這個宏會被自動定義,相關源碼如下:
#ifdef _WIN32 int wmain(int argc, wchar_t *wargv[]) { // ... return node::Start(argc, argv); } #else // UNIX #ifdef __linux__ int main(int argc, char *argv[]) { // ... }
這里就不扯什么頭文件了,主函數會調用node模塊的Start方法,node模塊文件名是node.cc,相當於項目的主文件,從這里開始,也從這里結束。
從上面可以看到,這個函數有三大步(能看懂的),非常簡單粗暴,一個一個講。
Init
這個函數相關源碼如下:
void Init(int* argc, const char** argv, int* exec_argc, const char*** exec_argv) { // Initialize prog_start_time to get relative uptime. prog_start_time = static_cast<double>(uv_now(uv_default_loop())); // 加載內置模塊 RegisterBuiltinModules(); // Make inherited handles noninheritable. uv_disable_stdio_inheritance(); // 后面還有很多代碼 但是我看不懂 流下了沒技術的眼淚…… }
需要關注的只要那個RegisterBuiltinModules方法,從名字也可以看出來,就是加載內置模塊,描述有誤,下一節修正。
這個函數的定義也很奇妙,簡直就是宏函數,如下:
void RegisterBuiltinModules() { #define V(modname) _register_##modname(); NODE_BUILTIN_MODULES(V) #undef V }
函數的聲明相信學JS的也能看懂是什么,主要是函數內容很奇怪,竟然是一個宏定義。
用JS翻譯一下那個宏,意思大概就是:
// C++:#define V(modname) _register_##modname(); const V = (modname) => `_register_${modname}`();
當然,這個鳥代碼是不可能執行的,只是為了方便理解。
打個比方就能明白了,假如我調用了V(a),那么在執行的時候,實際上調用的是_register_a這個方法,雙警號只是一個字符串拼接。
好,解決了宏問題,可以看下這個NODE_BUILTIN_MODULES是什么函數了。然而,這也是一個宏,內容如下:
#define NODE_BUILTIN_MODULES(V) \ NODE_BUILTIN_STANDARD_MODULES(V) \ NODE_BUILTIN_OPENSSL_MODULES(V) \ NODE_BUILTIN_ICU_MODULES(V)
看到這閃亮亮的格式,應該可以猜到,這三個東西還是宏!!!
好在宏不過三代,隨便點一個跳轉就會發現,實際上這些宏都是為了調用具體的模塊加載方法,比如:
#define NODE_BUILTIN_STANDARD_MODULES(V) \ V(async_wrap) \ V(buffer) \ // 還有很多比如fs、url等
這樣把最初的函數簡單轉換一下,大概就是:
void RegisterBuiltinModules() { _register_async_wrap(); _register_buffer(); // 等等 }
至於這些方法的內容是什么?地點在哪?我還沒找到。
Start
V8引擎的加載就先忽略,觸及到我的知識盲區。最后看一下那個內聯Start函數,這個方法主要完成三件事:
1、初始化並加載全局變量global
2、加載輔助工具
3、事件輪詢
其中1、2兩個都是在同一個函數中執行的,即LoadEnviroment,上一些關鍵的源碼證明下:
void LoadEnvironment(Environment* env) { // ... Local<String> loaders_name = FIXED_ONE_BYTE_STRING(env->isolate(), "internal/bootstrap/loaders.js"); Local<Function> loaders_bootstrapper = GetBootstrapper(env, LoadersBootstrapperSource(env), loaders_name); // 還有"internal/bootstrap/node.js" // 生成全局global對象的引用 Local<Object> global = env->context()->Global(); // ... // 設置全局對象 global->Set(FIXED_ONE_BYTE_STRING(env->isolate(), "global"), global); // ... // 生成函數參數 Local<Value> loaders_bootstrapper_args[] = { env->process_object(), get_binding_fn, get_linked_binding_fn, get_internal_binding_fn }; // 加載輔助函數 Local<Value> bootstrapped_loaders; if (!ExecuteBootstrapper(env, loaders_bootstrapper, arraysize(loaders_bootstrapper_args), loaders_bootstrapper_args, &bootstrapped_loaders)) { return; } // 加載Bootstrap Node.js }
這里通過Environment類生成了global對象,然后掛載到全局。
輔助函數則是加載了internal/bootstrap中的兩個JS文件,加載的時候參數傳入了C++代碼生成的特殊對象,配合對應的JS源文件就很好理解:
'use strict'; // internal/bootstrap/loader.js (function bootstrapInternalLoaders(process, getBinding, getLinkedBinding,getInternalBinding) { // ...模塊內容 });
這個JS文件內容看起來只是一個函數定義,但是實際上在啟動后已經執行完了,四個參數是通過上述C++代碼注入的,依次對應上面的4個東西。
這里有一個小地方稍微講講吧,如果在啟動時已經執行完了,那么看一下下面的代碼:
// internal/bootstrap/loader.js const { NativeModule } = require('internal/bootstrap/loaders');
這是node中調用require方法的入口JS文件,理論上引入的應該是上面那個函數,不可能得到NativeModule對象的。
不過node的require並不像webpack那么簡單粗暴,readFile + parse直接得到文件輸出對象,而是區分了內部模塊與外部模塊。對於內部模塊,有一套新的邏輯,相關代碼如下:
// require => _load // 判斷傳入的字符串是否是內部模塊名 if (NativeModule.nonInternalExists(filename)) { debug('load native module %s', request); // 調用另一套邏輯代碼 return NativeModule.require(filename); }
而NativeModule就是一開始加載過的輔助工具JS,涉及到的代碼如下:
const loaderExports = { internalBinding, NativeModule }; const loaderId = 'internal/bootstrap/loaders'; NativeModule.require = function(id) { // Do not expose this to user land even with --expose-internals // 對此require進行特殊處理 if (id === loaderId) { return loaderExports; } // ... }
可以看到在require的地方做了特殊處理,會直接返回指定的對象,至於這兩個對象是什么,后面再慢慢講吧。
值得注意的是那個注釋,這個字符串十分特殊,node並不希望用戶獲取該模塊,因為得到的對象擁有直接調用底層C++代碼的能力,十分危險。
完成准備工作后,就開始了事件輪詢跑進程,相關代碼如下:
{ SealHandleScope seal(isolate); // 循環控制參數 bool more; // 標記事件輪詢開始 env.performance_state()->Mark( node::performance::NODE_PERFORMANCE_MILESTONE_LOOP_START); // 開始event_loop do { uv_run(env.event_loop(), UV_RUN_DEFAULT); v8_platform.DrainVMTasks(isolate); more = uv_loop_alive(env.event_loop()); if (more) continue; RunBeforeExit(&env); // Emit `beforeExit` if the loop became alive either after emitting // event, or after running some callbacks. more = uv_loop_alive(env.event_loop()); } while (more == true); // 標記事件輪詢結束 env.performance_state()->Mark( node::performance::NODE_PERFORMANCE_MILESTONE_LOOP_EXIT); }
事件機制的實現在第三方依賴模塊uv當中,輪詢過程則是這里的do-while循環。
一旦node輪詢結束,會返回一個exit_code,然后退出整個進程。
完結,下一節寫啥還得再想想。