本作品采用知識共享署名 4.0 國際許可協議進行許可。轉載保留聲明頭部與原文鏈接https://luzeshu.com/blog/nodesource4
本博客同步在https://cnodejs.org/topic/56ed249356d74f3d3624b3ff
本博客同步在http://www.cnblogs.com/papertree/p/5285705.html
上面講到node調用Script::Compile()和Script::Run()解析執行app.js,並把io操作和callback保存到default_loop_struct,那么app.js里面js代碼如何調用C++的函數呢?
在4.2節進行解釋,先在4.1節來點知識預熱。
4.1 V8運行js代碼的基礎知識 —— V8的上下文
來看看google V8開發者文檔的一點介紹:(地址:https://developers.google.com/v8/get_started)
- A context is an execution environment that allows separate, unrelated, JavaScript code to run in a single instance of V8. You must explicitly specify the context in which you want any JavaScript code to be run.
大概意思就是context(上下文)是用來執行javascript代碼的運行環境,而且運行javascript代碼的時候必須指定一個context。
從文檔里面摘了一段hello world代碼:
int main(int argc, char* argv[]) { // Initialize V8. V8::InitializeICU(); V8::InitializeExternalStartupData(argv[0]); Platform* platform = platform::CreateDefaultPlatform(); V8::InitializePlatform(platform); V8::Initialize(); // Create a new Isolate and make it the current one. ArrayBufferAllocator allocator; Isolate::CreateParams create_params; create_params.array_buffer_allocator = &allocator; Isolate* isolate = Isolate::New(create_params); { Isolate::Scope isolate_scope(isolate); // Create a stack-allocated handle scope. HandleScope handle_scope(isolate); // Create a new context. Local<Context> context = Context::New(isolate); // Enter the context for compiling and running the hello world script. Context::Scope context_scope(context); // Create a string containing the JavaScript source code. Local<String> source = String::NewFromUtf8(isolate, "'Hello' + ', World!'", NewStringType::kNormal).ToLocalChecked(); // Compile the source code. Local<Script> script = Script::Compile(context, source).ToLocalChecked(); // Run the script to get the result. Local<Value> result = script->Run(context).ToLocalChecked(); // Convert the result to an UTF8 string and print it. String::Utf8Value utf8(result); printf("%s\n", *utf8); } // Dispose the isolate and tear down V8. isolate->Dispose(); V8::Dispose(); V8::ShutdownPlatform(); delete platform; return 0; }
你可能會發現,上面說了script->Run(context) 一定要指定一個context。那么看回3.1.2 中的圖3-1-3,node.cc里面的script->Run()並沒有context參數。
跳到v8的源碼,deps/v8/src/api.cc,就會發現這實際上是兩個重載函數,無參Script::Run()會先從Script對象取得當前的context,再調用Script::Run(Local<Context> context)。
圖4-1-1
4.2 理解js代碼如何調用C++函數 —— 運行時的上下文
看個例子:
左邊為node 原生lib模塊網絡socket操作部分的文件 —— net.js,我們平時使用server.listen()時,最終調用到net.js里面,先通過new TCP()創建一個handle對象,再調用handle.listen()。而這個TCP和listen,均來自左邊tcp_wrap.cc文件。
也就是說,通過net.js里面的handle.listen()調用了tcp_wrap.cc里面的TCPWrap::Listen()函數,並且傳給handle.listen()的 js參數—— backlog,被包裝到了C++的 FunctionCallbackInfo<Value>類對象args。

圖4-2-1
如果你第一感覺是js代碼調用C++代碼無法理解,那么一定是受到“語法”的干擾。
確實,從靜態的角度來看,js和C++是兩種語言,語法不互通,直接在js代碼調用C++函數那是不可能的。
那么,從動態的角度(運行時)來看呢?別忘了,任何編程語言最終運行起來都不過是進程空間里的二進制代碼和數據。

圖 4-2-2
4.2.1 從js代碼到context
4.1 中已經講了,Script::Compile()和Script::Run() 的時候必須為 js代碼指定一個運行環境(context)。那么 js代碼和context的關聯是很自然的。
4.2.2 設置C++函數到context
那么,上圖藍色標號1-5這幾個步驟,即在C++代碼層面,把C++函數設置到context的細節和相應的V8 接口是什么呢?
4.3 node的js模塊調用C++模塊的細節
在node里面,在C++代碼里面提供給運行時javascript代碼使用的無非就是這幾種:
1. 一個對象(比如process),對象上設置屬性(比如process.versions)、或者方法(比如process._kill)
2. 函數對象(比如TCP),設置原型方法(比如TCP.prototype.listen)
4.3.1 process對象 —— V8的Object類
在3.2中講到,main函數啟動后會加載執行src/node.js文件,並且把process對象傳給node.js文件,在里面設置process.nextTick()等方法。
那么來看看 C++如何創建一個給js使用的對象。
4.3.1.1 類型
回去3.1.2節看一下“圖3-1-3”。在LoadEnvironment() 里面執行 f->Call()調用node.js里的匿名函數時,傳過去的process對象是通過env->process_object()獲取的。
env->process_object()的實現如下:

圖 4-3-1
這里是個宏,展開就是
inline v8::Local<v8::Object> Environment::process_object() const { return StrongPersistentToLocal(process_object_); }
那么上面標紅的process_object_ 成員,定義如下:

圖 4-3-2
這里也是一個宏,展開就是
class Environment { v8::Persistent<v8::Object> process_object_; }
那么這里可以看到,C++里面提供給js代碼的對象,就是一個v8::Object類型的對象。
4.3.1.2 設置屬性或方法
那么,v8::Object類型的對象如何在C++里面設置屬性呢?

圖4-3-3
這里可以看到,v8::Object類提供了Set()方法,來讓你設置供js訪問的屬性或方法。
4.3.2 TCP類 —— v8的FunctionTemplate類
那么第二種類型,就是設置prototype方法。在js里面,沒有真正的類的概念,而是通過給函數對象TCP的prototype屬性設置方法,使用的時候通過new TCP()去創建實例。
那么,v8如何設置原型方法?
4.3.2.1 設置原型方法

圖4-3-4
這里可以看到,通過創建一個v8::FunctionTemplate類型的對象 t,通過 t->PrototypeTemplate() 去獲取函數對象的prototype,並進一步調用Set()去設置prototype上的方法。
最后再通過 t->GetFunction() 去獲取一個該函數模版的方法。
注:關於 js文件process.binding('tcp_wrap')引入TCP函數對象的機制,在下一篇博客講。

