寫這本書是我在2016 年底許下的願望,希望在2018 年初完成一本技術專著。
我於2012 年加入Node.js 開發的大軍, 現在也有幸成為Node.js 這個項目的Core Collaborator 之一。所以,我的意向就是為大家呈現一本Node.js 領域相關的書。但是現在市面上相關的書籍其實有很多了,我再寫一本日常開發類的圖書就顯得有些多餘。反而是在Node.js 的C++ 擴展開發方面,無論是在國內還是在國外,都是一塊死角。就目前而言,國外市場我也隻看到過一本電子書,並沒有紙質圖書出版,國內就更沒有了。
Node.js 作為近幾年新興的一種編程運行時,托Chrome V8 引擎的福,在作為後端服務時有比較高的運行效率,在很多場景下對於我們的日常開發已經足夠用了。不過,它也跟“PHP 提供了C 語言開發其原生擴展的方式”類似,為開發者開了一個使用C++ 開發Node.js 原生擴展的口子,讓開發者進行項目開發時有了更多的選擇。
實際上,在Node.js 的生態圈中,就有很多使用C++ 完成的包。如最近比較火的深度學習TensorFlow,其Node.js 版本的封裝就是基於官方的C++ 源碼完成的。我自己就是在日常開發中有一些相應的需求,使用純粹的Node.js 來開發可能會使開發成本有點大,或者基本上做不到,又或者有性能上的要求。這時,我就會選擇使用C++ 來實現它的一個擴展。在我寫了一段時間C++ 擴展之後,想到可能在社區中有很多像我一樣的人,苦於Node.js在底層操作時的一些局限性,如果他們也加入C++ 原生擴展開發陣營的話,興許要再踩一遍我以前踩過的“坑”,找我之前找過的資料。因此,我就想把自己一路走來的經驗分享給大家,讓更多的人順利地加入Node.js 的C++ 原生擴展開發的大軍中。
我的Node.js 之路
我個人從小學開始接觸靜態網頁的開發,直到高中開始參加信息學奧林匹克競賽(Olympiad in Informatics, OI),纔算正式踏入了編程之路。
在大學的時候我仍舊堅持參加大學生程序設計競賽(ACM International Collegiate Programming Contest, ICPC),並且一直使用C++ 和PHP 進行開發。也是那時我打下了C++ 基礎,這樣纔有機會現在完成這本書的寫作工作。
我接觸Node.js 其實並沒有國內一些早期的布道者們早,相反還是有點遲的。在2012 年底,我決心學習Node.js,從而完成自己的一個創業項目。我當時的學習方法特別簡單,買了一本BYVoid 的《Node.js 開發指南》,就算正式踏入了Node.js 領域。
在熟悉了Node.js 之後,我開始為Node.js 生態圈造輪子,如Toshihiko1、ThmclrX、Huaming3、mcnbt4 等。其實我個人認為,造輪子與寫業務的一個不同點在於,造輪子可能會更容易遇到語言或是Node.js 運行時本身的“坑”。所以,這就促使我去深究Node.js 的文檔,甚至源碼。托我之前習得的C++ 基礎的福,在閱讀Node.js 源碼時並不覺得特別艱難。
我在老東家花瓣網的時候,就已經初步開始了Node.js 的C++ 擴展開發。
後來我去了上一家就職的公司——大搜車,負責公司Node.js 團隊的建設。當時我就開始更深入地挖掘Node.js 的一些內容了。甚至在2017 年年中的時候,我通過給Node.js 貢獻源碼,成為Node.js Core Collaborator 之一。我在為Node.js 貢獻源碼的時候,也為本書第3章和第6 章的寫作打下了基礎。
原生擴展的一些示例
在Node.js 早期的版本中, 運行子進程是純異步的, 並沒有像現在一樣的各種spawnSync() 等函數。我當時寫了一個命令行工具,在其所用到的一個幫助類中實現一個參數校驗的函數必須同步返回一個布爾類型的值;然而我在這個校驗函數中所需要做的事情就是判斷當前繫統的Git 版本。也就是說,我要通過子進程啟動$ git -v,並得到它的結果看看版本是不是符合要求。當時Node.js 中運行子進程是異步的,達不到我的要求,所以我自己使用C++ 封裝了一個原生擴展,使其能在Node.js 的事件循環中同步開啟子進程並在其結束後獲得它的終端輸出——雖然Node.js 天生異步,但是在我的一個命令行工具中用同步形式執行這些內容也是沒有問題的。
再比如,在大搜車的時候,項目用了阿裡雲的消息隊列服務,而這款產品當時隻有閉源的Java、C++ 等SDK,而C++ 的SDK 就隻提供了幾個動態鏈接庫和一堆頭文件,我們使用Node.js 的開發者就完全沒法使用其服務。如果一定要用Node.js 進行開發,一個成本比較高的做法就是自己去逆向分析及研究消息隊列服務的各種網絡包的結構,自己解析,然後用Node.js 實現一個同樣功能的庫。然而這個方法基本不可行——尤其是在我們的項目高速迭代的時候。那麼另一個辦法就是基於其閉源的C++ SDK,使用本書中的各種開發方式,寫一份Node.js 的C++ 擴展。這樣就能把它們的C++ SDK 集成到我們的Node.js 項目中了。這是一個非常好的降低開發成本的方法。
當年我還在花瓣網的時候,有一個需求是提取一張圖片的主題色。我當時翻閱了不少論文,最終采用了一種八叉樹加最小差值法的結合體1 來完成這個需求。在數據結構和整型數字處理方面,我個人認為C++ 的開發效率和執行效率比Node.js 要高,於是我自然而然地就使用了C++ 把核心算法部分完成了(現在我甚至使用C 語言又重構了一套,開源在GitHub 上面2)。然後為了將其集成到我們的Node.js 任務調度繫統中,我又將其封裝成了一個Node.js 的C++ 的擴展。這樣一來,主題色提取的任務就歡快地運行了——它也被開源在我的GitHub 上面,就是前面提到過的ThmclrX。而且借這個包的“東風”,我的碩士畢業論文寫的就是這麼一套主題色提取的任務繫統相關內容。
類似的案例還有很多。如計算字符串哈希值等,由於用JavaScript 重寫代碼的時候,在整數的各種操作上會有很多“坑”,因此拿C++ 源碼封裝一下就非常簡單了。甚至谷歌推出的CityHash 這個算法隻有一份冗長的C++ 源碼,使用JavaScript 重寫的話將會是一個比較龐大的工作量;再比如解析MP4 文件的時長,我個人不是多媒體相關領域的開發者,所以並不擅長。於是我弄了一份C++ 的源碼,懶得轉換——嘿,用C++ 擴展一包,直接就發布了;還有同步獲取HTTP API 的內容,寫一個能繼承的類似於ECMAScript 6 中Proxy 特性的攔截器;等等。
本書面向的讀者
在閱讀本書前,我希望你對Node.js 比較熟悉,並且對於C++ 這門語言至少要有一個初步的認識。當然,如果你的C++ 基礎並不是很好的話,也不要怕,可以多讀幾遍本節最後的一段話。
本書不僅僅講實踐,我還花了不少篇幅來講解它的前驅知識,如Chrome V8 引擎開發的一些基本概念,如句柄、句柄作用域等,以及各種API 的初步介紹。另外,書中還介紹了libuv 層面的內容,尤其是在異步方面,像libuv 中的線程、同步原語,以及如何在Node.js 的主時間循環中與你自身寫的線程進行跨線程通信等。這麼一算,Chrome V8、libuv,加上Node.js 的C++ 擴展開發,你相當於一下子買了3 本書,是不是覺得很超值?也就是說,你閱讀本書的目的不一定是想要開發Node.js 的C++ 擴展;如果你想學習Chrome V8,或者想學習libuv,也可以參考本書。
本書的最後還簡單展望了一下Node.js 8.0 之後出現的一個新特性,就是新一代Node.jsC++ 原生擴展接口N-API。不過由於N-API 還處於試驗階段,各種接口還不是很穩定,在未來隨時會變,因此本書中並沒有詳細地介紹N-API,而隻是簡單講解了它的思想,讓大家在心中有一個思想準備。這樣,哪一天N-API 正式發布了,讀者就可以比較快地上手了。
不過,不要忘本,哪怕N-API 真的出來了,我還是希望大家多了解一下底層的基礎,比如像Chrome V8、libuv 以及Node.js 源碼相關的內容。因為學習了這些基礎知識,對大家肯定沒有壞處(甚至對於Node.js,大家說不定會有一個新的認識)。
最後,奉上我在一次技術直播中說過的一句話:“當我們在學習Node.js 的時候,我們其實就是在學編程。語言隻是最表像的東西,思想纔是核心內容。”如果還有部分讀者由於本書需要有C++ 基礎望而卻步的話,多讀幾遍我剛纔說的話,然後鼓起勇氣入“坑”吧。
本書的結構
本書共分為9 章。其中前兩章描述了一些基礎的前驅理論知識;第3 章到第6 章講的是Node.js 的C++ 擴展開發中用到的各種知識,並輔以簡單的樣例;第7 章和第8 章為實戰章節,根據現實需求來完成相應的Node.js C++ 擴展;第9 章為對未來的N-API 的一個展望。
第1 章講述了我們在學習本書內容之前所需要了解的基礎,如Node.js 的模塊機制與包機制,以及Node.js 都是由什麼三方依賴構成的。其中就提到了很重要的Chrome V8 和libuv。本章的最後還講述了要進行Node.js 的C++ 擴展開發所需要做的準備工作,包括但不限於編輯器的挑選、開發環境的搭建等。
第2 章主要講述了什麼是Node.js 的C++ 擴展,它的本質是什麼,並且什麼情況下需要使用C++ 擴展,以及闡述了為什麼在這些情況下要使用 C++ 擴展。
第3 章介紹了谷歌的Chrome V8 引擎,從它與Node.js 的關繫講到它的一些基本概念,例如V8 的內存機制、基本對像等。在後續的章節中將開始介紹Chrome V8 的各種類及其概念,以及它們的用法,如句柄、句柄作用域、模板和各種常用的數據類型等。
第4 章相當於各種編程語言書籍中的“Hello World”,向讀者介紹了binding.gyp 這個重要的配置文件,以及GYP 文件格式的基礎,然後以幾個最簡單的例子向讀者展示了Node.js 的C++ 擴展最簡單的一些代碼,包括函數的參數、回調函數的用法、對像的返回、函數的返回等,以及如何將一個C++ 的類封裝成Node.js 中直接能用的類。
第5 章為大家介紹了NAN(Native Abstractions for Node.js)這個非常實用的包,使大家能在不同的Node.js 版本(本質上是各不兼容的Chrome V8 版本)中使用同一份C++ 代碼。
第6 章講解了如何使用libuv 進行異步Node.js 的C++ 擴展代碼編程,首先介紹了libuv的一些基礎概念,如句柄與請求等,然後講述了如何使用libuv 進行跨線程編程。
第7 章就開始進入了實戰環節。本章通過從零開始寫一個基於C++ 的文件監視器擴展,講述了要完成一個Node.js 原生擴展的一些流程。本章所述的文件監視器源碼地址在https://github.com/XadillaX/node-efsw。
第8 章與第7 章的實戰不同,對兩個現有的簡單C++ 擴展包進行分析,從另一個角度剖析了一個Node.js 的C++ 擴展包的源碼。
第9 章展望了如何使用Node.js 的最新特性N-API 進行原生擴展的開發。不過我估計等到本書正式上市的時候,第9 章已經變成一個僅供參考的章節了。
閱讀本書的注意事項
聲明:我在編寫本書之際,還在大搜車工作,所以書中的很多內容都是基於大搜車的角度來寫的。比如8.2 節中有一處內容是這樣的:
在筆者所在公司的內部,用了一套基於Dubbo 深度定制的RPC 服務框架。Node.js要訪問這些Java 服務的RPC 函數是通過定制的HTTP 協議來完成的,所有的RPC服務節點都到Zookeeper 進行注冊。
這裡指的公司就是大搜車。再比如2.1.2 節中的一段話:
在官方的Node.js 版本ONS SDK 出來之前,筆者自己造了一個基於其官方C++ 版本的ONS SDK封裝的輪子,用的當然是本書所講的姿勢——Node.js 的C++擴展了。
由於編寫本書時我還並未從大搜車離職,因此這仍然是站在就職於大搜車的角度寫的。我在編寫本書之際,Node.js 的8.x 版本並未進入LTS1 階段。於是我采用了Node.js 6.x作為樣例進行了講解,而Node.js 6.x 距離LTS 結束也還有一段時間。而且使用本書的方法進行Node.js 的原生擴展開發,在Node.js 6.x、Node.js 8.x 甚至是Node.js 9.x 下都是通用的。
本書中的樣例都是基於Node.js v6.9.4 進行講解的,讀者在參考的時候上調或者下調幾個中、小版本號問題都不大。
至於N-API 一章(第9 章),我在該章中也曾談道:
本章內容在書中將會一帶而過,因為在筆者寫書的時候,N-API 還沒有完全穩定下來,隨時會改變。而且筆者個人認為,距離N-API 能正式投入生產用途的時間還很長。所以本章內容在本書中僅以擴展閱讀的形式存在,其實關於N-API 的內容在5.1.2 節中曾略微提及。
因此,該章內容僅供參考,具體內容應以官方文檔為準。
另外,本書所有的隨書代碼均在macOS 命令行下測試通過。理論上,它們也可以在Windows 和UNIX 上運行良好,但我並沒有驗證過。
最後,給出本書中經常用到的一些地址。
? 本書隨書代碼的Git 倉庫:https://github.com/XadillaX/nyaa-nodejs-demo
? Node.js v6.9.4 代碼倉庫:https://github.com/nodejs/node/tree/v6.9.4
? Node.js v6.x 所對應的Chrome V8 文檔:https://v8docs.nodesource.com/node-6.12/。若讀者打開該地址,卻發現頁面不存在,可直接前往https://v8docs.nodesource.com/,
並點擊“6.x”字樣的超鏈接進入(注意該地址經常換)。
? 作者的個人技術博客:https://xcoder.in
? 作者的GitHub 地址:https://github.com/XadillaX
? Me:https://github.com/XadillaX/me
致謝
感謝我的妻子,她也是一位優秀的Node.js 研發工程師。她的支持是對我的最大鼓勵,如果不是她,這本書的問世也許會更晚。
感謝我的父母,在我的背後默默地支持我的事業。在我很小的時候,他們就一直支持我追尋自己的夢想,這纔使我能夠在編程領域一路走下來。
感謝我的老東家大搜車,它營造了良好的技術與實踐氛圍,同事(包括領導)給予了我不少幫助,如書中圖示的優化、閱讀體驗的建議等。這些同事有段鵬飛1、紀清華、劉佳楠、許波、王琦、袁小山……
感謝現實以及社區中的朋友們在本書創作的時候進行試讀和探討,並提供了一些其他幫助,他們包括但不限於Akagi201、ADoyle、David Cai、Hax(賀老)、賀星星、精子(jysperm)、孟德森、天然、五花肉、引證、張秋怡。
感謝為本書寫序和推薦語的作者們: 安娜· 亨寧森(Anna Henningsen)、曹力(ShiningRay)、顧天騁(Timothy Gu)、桑世龍(狼叔)、雷宗民(老雷)、劉亞中(Yorkie)、迷渡(justjavac)、潘旻琦(pmq20)、田永強(樸靈)、袁鋒(蘇千)、孫信宇(芋頭)、
王文睿博士、響馬,你們一直是我們的楷模與學習對像。
感謝我的高中計算機老師兼NOIP 集訓教練王震老師,王震老師是我在編程路上的啟蒙老師,沒有他就沒有今天會寫代碼的我;也感謝當時陪我堅持走這一條路到畢業的好隊友jiecchen 和MatRush;感謝我的大學ACM 教練宣江華老師和一直為集訓隊默默付出的陳萌老師;還要感謝我的研究生導師李啟雷博士傳道受業。
感謝博文視點的劉皎女士以及她的團隊,是他們的努力使本書最終能與廣大讀者見面,他們提出的專業意見給了我很多幫助。
最後,還要特別感謝董偉明(《Python Web 開發實戰》作者)。在閱讀了他的一篇文章《寫一本技術書籍》後,我纔有了寫作本書的想法,並最終付諸實施。
死月(朱凱迪)
2018 年3 月於杭州
序一
1995 年Brendan Eich 花了10 天時間開發出了一門腳本語言,用來彌補Java Applet 的不足,隨後Marc Andreessen 給它起名為Mocha。其最初的定位是,Java 用於大型專業級開發,而Mocha 則是給測試腳本編寫人員、業餘愛好者、設計師使用的。
1995 年5 月,Mocha 被集成到了Netscape 瀏覽器中,其不久後改名為LiveScript,當年年底網景公司和Sun 公司達成協議並獲得了Java 商標的使用權,其正式更名為JavaScript。
有人說Sun 公司的介入限制了Brendan Eich 的手腳。JavaScript 除了某些語法和Java 類似以外,骨子裡卻是完全不一樣的東西。
也有人說正式改名為JavaScript 纔使得這門語言成為瀏覽器執行的唯一語言。
時至今日JavaScript 已經不僅僅局限於為網頁做特效了,而真正發展成為一門全功能的編程語言:
? 2008 年Chrome 發布、V8 發布;
? 2009 年Node.js 發布;
? 2010 年NPM 發布;
? 2014 年12 月,多位核心Node.js 開發者不滿於Joyent 對Node.js 的管理制度,創建了io.js;
? 2015 年初Node.js 基金會成立;
? 2015 年9 月Node.js 4.0 發布,Node.js 和io.js 正式合並。
Node.js 4.0 版引入了ES6 的語言特性和“長期支持版本”的發布周期。
如今Node.js 社區已經成為最活躍的編程社區之一,而從NPM 的包數量來看,其已經超越了Java 的Manven、Ruby 的gem、PHP 的composer。VIII Node.js:來一打C++ 擴展但是Node.js 仍有很多不足之處,Node.js 的使用者絕大部分僅僅把Node.js 作為前端開發的輔助工具。大家把Node.js 作為後端主力開發平臺使用時,遇到CPU 密集的場景時又不得不借助Java 或者Go。雖然V8 引擎一直致力於讓JavaScript 運行得更快,但是和Java、C++ 相比,還有不小的性能差距。
雖然關於JavaScript 的書已經汗牛充棟,但是有關Node.js 原理的書卻屈指可數。而目前真正能夠深入介紹原理的,國內的圖書中也隻有樸靈的《深入淺出Node.js》了,但如今四五年過去了依然沒有等到該書的第2 版,而死月的這本書卻可以彌補這一方面的不足。
所有的編程語言底層都會回歸C/C++,Node.js 的底層依賴庫V8 使用C++ 開發,libuv 則使用C 語言。而使用C++ 開發Node.js 擴展將直接把擅長CPU 的C++ 和擅長I/O 的Node.js結合在了一起,彌補了JavaScript 在計算密集型應用方面的不足。
我從2015 年開始研究V8,認識死月的時間則更早。死月不僅僅精通C++,他也是國內的Node.js 布道師之一。從我認識他起,他就一直在使用Node.js。如果你想深入了解Node.js 的原理,或者想打開Node.js 另一個世界的大門,這本《Node.js:來一打C++ 擴展》值得你精讀。
——迷渡(justjavac),Flarum 中文社區創始人,國內知名前端技術專家
2018 年3 月22 日於天津
序二
我跟死月相識於GitHub,那時我們經常會向Node.js 貢獻一些代碼,彼此也會在微信上討論一些技術問題。當我聽說死月在寫一本關於Node.js C++ 擴展相關的圖書時,激動得幾乎要從床上蹦起來。因為我深知一個對Node.js 與V8 引擎都如此了解之人,願意將他所知所想分享出來,這將是給予社區的一份大禮。
從我個人的角度來看,這本書非常適合這類開發者:他們對於Node.js 的使用已經了然於胸,但卻苦於沒有底層開發經驗,對整個V8 虛擬機也一知半解。這時,他們可以從第3 章開始讀起。本書用了很長的篇幅介紹JavaScript 代碼究竟在虛擬機裡是怎麼運行的,它們又都分別對應著哪一類數據結構等。因為作者深知,隻有把這些基礎理解透了,則無論是開發C++ 擴展,還是寫純JavaScript 代碼,大家都能更得心應手。
本書像是在述說著Node.js 在C++ 擴展這一課題中曲折而又有趣的歷史進程。首先從最原始的V8 API 時代開始。對於每個原始時代,開發者最痛苦的莫過於解決各種版本的兼容問題。之後迎來的是NAN 時代。它解決了原始時代的接口抽像問題,接口也更豐富多樣,異步接口也封裝在內。最後,是還在路上的N-API。它與NAN 一脈相承,擁有更官方的支持和更友好的接口。
另外,我們通常在寫一個C++ 擴展時,多數情況下會跟異步打交道,這其中包含著如何非阻塞地調用底層接口,如何將異步的結果返回到JavaScript 的回調函數中,以及如何正確地在異步封裝中釋放你的資源。對這些內容特別感興趣的讀者,可以打開第6 章一睹為快。
Node.js 已快走完它的第一個10 年,盡管被人詬病於其回調地獄、虛假繁榮、超高並發場景下的不適應性,以及低端設備上的內存等問題,但這仍舊無法阻止它前進的步伐。然而對於我們Node.js 工程師來說,除了掌握好這門語言之外,學習如何寫C++ 擴展、了解它如X Node.js:來一打C++ 擴展
何運轉將是我們下一階段的重要功課。相信《Node.js:來一打C++ 擴展》將會成為常伴大家左右的另一本《代碼大全》。
——劉亞中(Yorkie),Rokid 繫統工程師,tensorflow-nodejs 作者
2018 年3 月22 日於杭州