前言:
最近在看Node.js,看了一段時間后便想着看看Node.js源碼,自己本地調試調試;現在便說說這個過程中的坑,以及一些需要注意的地方;
Node.js需要一定C++基礎,建議看完C++Primer再看,否則V8的好多表達方式,指針,引用,模板之類的會看不懂;
代碼已上傳GitHub地址: https://github.com/sven36/cNode
完整編譯的文件太大,上傳GitHub不成功,下載請用百度網盤https://pan.baidu.com/s/1jIC4xCy
先說一下node.js啟動過程:
node.js的src目錄下的源代碼大部分都是node.js的模塊文件;其實初始化node.js用到的文件只有:node.h , node.cc , env.h , env_inl.h , node_internals.h , node_javascript.h , node_javascript.cc , util.h , util.cc ,以及用js2c.py工具將內置JavaScript代碼轉成C++里面的數組,生成的node_natives.h文件;
我實現的過程是按照node.js的啟動過程,需要哪個方法就實現哪個方法,能合並的方法都盡量合並,能忽略的細節都盡量忽略,下面簡單說說node.js啟動主要的方法和過程;
入口是在node_main.cc中,根據平台的不同會進入不同的Start方法,我的是windows平台,運行的wmain方法,然后調用了node::Start方法;
node::Start方法的具體實現是在node.cc中,node.cc也是node的核心代碼;在啟動過程中需要注意的有四個方法:StartNodeInstance,在StartNodeInstance里面調用的CreateEnvironment方法,
在CreateEnvironment方法里面調用的SetupProcessObject方法,以及CreateEnvironment結束之后調用的LoadEnvironment方法;
StartNodeInstance在初始化v8虛擬機,綁定作用域之后就會調用CreateEnvironment方法;CreateEnvironment會初始化Environment類,該方法定義在env_inl.h文件中;
CreateEnvironment在初始化Environment類之后,會先初始化v8的的CPU分析器,再初始化handle的回收方法,然后就會初始化全局process對象;
代碼如下:
Local<FunctionTemplate> process_template = FunctionTemplate::New(isolate);
process_template->SetClassName(node::OneByteString(isolate, "process", sizeof("process") - 1));
Local<Object> process_object = process_template->GetFunction()->NewInstance(context).ToLocalChecked();
env->set_process_object(process_object);
在v8里面一個template是javascript函數的藍圖。你可以使用一個template來將c++函數和結構體包裝到javascript對象中,讓javascirpt腳本來使用它。所以我們經常調用的process.binding,process.cpuUsage,process.dlopen等方法,其實是在調用包裝成js腳本的C++方法;
接下來的SetupProcessObject方法就是具體初始化process對象的方法了,除了只讀屬性process.versions,process.moduleLoadList等;更重要的是通過Environment::SetMethod方法,把C++方法包裝成js腳本方法;比如:
env->SetMethod(process, "binding", Binding);
就把process.binding方法綁定成C++里面的Binding方法;
初始化process對象之后就會調用LoadEnvironment方法,在該方法中我們可以看的:
Local<String> script_name = FIXED_ONE_BYTE_STRING(env->isolate(),
"bootstrap_node.js");
Local<Value> f_value = ExecuteString(env, MainSource(env), script_name);
其中ExecuteString方法會調用v8::Script::Compile方法來編譯傳入的js文件;那我們知道了bootstrap_node.js是一個被調用的js文件,在這個里面又發生了什么呢?
大概可以分為:初始化全局 process
對象上的部分屬性 / 行為,初始化全局的一些 timer
方法,初始化全局 console
對象等一些方法;這里我們不展開了,我們看一看node的js模塊是如何引入的;
function NativeModule(id) {
this.filename = `${id}.js`;
this.id = id;
this.exports = {};
this.loaded = false;
this.loading = false;
}
NativeModule._source = process.binding('natives');
NativeModule._cache = {};
我們看的原生模塊會調用process.binding('natives')方法,我們找到node.cc里面的Binding方法看看會進行哪些操作;
else if (!strcmp(*module_v, "natives")) {
exports = Object::New(env->isolate());
DefineJavaScript(env, exports);
cache->Set(module, exports);
我們看到當傳入的參數是natives的時候會調用DefineJavaScript方法,我們看看這個方法做了什么;
void DefineJavaScript(Environment* env, Local<Object> target) {
auto context = env->context();
#define V(id) \
do { \
auto key = \
String::NewFromOneByte( \
env->isolate(), id##_name, NewStringType::kNormal, \
sizeof(id##_name)).ToLocalChecked(); \
auto value = \
String::NewExternalOneByte( \
env->isolate(), &id##_external_data).ToLocalChecked(); \
CHECK(target->Set(context, key, value).FromJust()); \
} while (0);
NODE_NATIVES_MAP(V)
#undef V
}
是一個復雜的宏定義,看着不太好理解那我們自己抽離一個實現看看;
void DefineJavaScript(Environment* env, Local<Object> target){
auto context = env->context();
do {
auto key = String::NewFromOneByte(env->isolate(), buffer_name, NewStringType::kNormal, sizeof(buffer_name)).ToLocalChecked();
auto value = String::NewExternalOneByte(env->isolate(), &buffer_external_data).ToLocalChecked();
} while (0);
}
buffer_name是什么呢?在js2c.py把內置js文件轉成C++數組的node_natives.h文件里面我們可以找到:
static const uint8_t buffer_name[] = {
98,117,102,102,101,114};
所以process.binding('natives')其實是使用v8引擎,編譯我們內置的js文件;
到這里node.js的啟動過程和文件模塊機制基本上就說了個大概了,其它諸如非核心模塊的引入,和buffer,stream等應C++完成核心部分,其它部分用js包裝或導出的模塊就需要大家自己去了解了;
我在過程中碰到一些問題和需要注意的地方:
說明:https://github.com/sven36/cNode這個項目是不能編譯的,因為這個是我最開始的版本(不過代碼我都同步成最新的了),v8等編譯命令都沒設置,我本來想上傳一個新的GitHub的可是項目太大了,老上傳失敗;
所以我就上傳了個百度網盤https://pan.baidu.com/s/1jIC4xCy
這個是可以直接編譯的,有需要的可以去下載;
編譯:我用的win10的環境,具體編譯請參考Node.js的編譯說明:https://github.com/nodejs/node/blob/master/BUILDING.md
其中的坑:Python應該是2.6或2.7,不要裝3.0或以上的,因為node.js有的py文件3.0編譯出錯; Visual C++ Build Tools必須是2015,安裝官方的鏈接就是,因為Node.js在Windows平台的編譯用的是vs2015的v140平台工具集,用低版本的或者vs2017的v141平台工具集都會報錯;
編譯流程:安裝完python和Visual C++ Build Tools2015之后,下載node.js的源碼 node-v6.10.0.tar.gz 然后用Visual C++ Build Tools2015的命令行運行解壓目錄下的vcbuild.bat處理文件就會開始編譯了;編譯成功后解壓目錄下就會出現.sln文件就可以使用vs打開了(vs也需要是2015或2017,並且項目的平台工具集也需要是v140否則編譯報錯);如圖:
V8引擎:
因為node.js其實就是嵌入V8的一個C++程序;首先要對v8的Isolate,LocalHandle,Scope等概念有一個了解,此處不展開了,請參考這個文檔:
https://github.com/Chunlin-Li/Chunlin-Li.github.io/blob/master/blogs/javascript/V8_Embedder's_Guide_CHS.md
在我的代碼里面我也加了一些注釋,在src目錄下的node.cpp文件內,可以參考;
C++編譯與平常的面向對象編譯方式的不同:
在.NET或Java之類的語言中,可以不必關注方法的聲明順序,比如:
private void a(){
b();
}
private void b(){
console.log(1);
}
不過在C++中這樣是不行的,調用b之前,必須完全聲明b;也就是把b放在方法a之前(這也是node.cpp文件中為什么把開始方法Start放在最下端);
為什么Node.js的頭文件要用namespace node包起來:
是為了更好的解耦node.js的各個模塊;在C++中命名空間相同而內部成員名字不同,它們會自動合並為同一個命名空間,可以理解為追加;
Node.js里面比較復雜的宏定義:
Node.js和V8用了很多復雜的宏定義,如果不理解它們看起來會很費力;在C++中,宏定義里面的##符號是連接字符串的意思;
比如:env-inl.h文件下的宏定義:
#define VP(PropertyName, StringValue) V(v8::Private, PropertyName, StringValue)
#define VS(PropertyName, StringValue) V(v8::String, PropertyName, StringValue)
#define V(TypeName, PropertyName, StringValue) \
inline \
v8::Local<TypeName> Environment::IsolateData::PropertyName() const { \
/* Strings are immutable so casting away const-ness here is okay. */ \
return const_cast<IsolateData*>(this)->PropertyName ## _.Get(isolate()); \
}
PER_ISOLATE_PRIVATE_SYMBOL_PROPERTIES(VP)
PER_ISOLATE_STRING_PROPERTIES(VS)
#undef V
#undef VS
#undef VP
它最后的編譯形式是:
inline v8::Local<v8::String> Environment::IsolateData::async_queue_string() const {
return const_cast<IsolateData*>(this)->async_queue_string_.Get(isolate());
}
這種地方多一些耐心仔細分析一下就會懂了;我寫的代碼里面基本上這種宏定義第一個字符串我都是這種手寫的,其余的是按照原先的宏定義的方式聲明,可以參考;
使用VS編譯需要注意的地方:
node.js項目文件其實是用google的GYP工具生產的,所以VS項目編譯的過程也在各個項目下的.gyp文件內。
了解gyp工具請參考:http://www.cnblogs.com/nanvann/p/3913880.html#conditions
當然還有很多具體的C++問題就需要靠自己多思考,勤搜索了;
最后附上編譯成功的圖片: