使用Node.js Addon實(shí)現(xiàn)類繼承
本文轉(zhuǎn)載自微信公眾號(hào)「編程雜技」,作者theanarkh。轉(zhuǎn)載本文請(qǐng)聯(lián)系編程雜技公眾號(hào)。
前言:昨天有個(gè)同學(xué)問怎么通過NAPI把C++類的繼承關(guān)系映射到JS,很遺憾,NAPI貌似還不支持,但是V8支持,因?yàn)閂8在頭文件里導(dǎo)出了這些API,并Node.js里也依賴這些API,所以可以說是比較穩(wěn)定的。本文介紹一下如何實(shí)現(xiàn)這種映射(不確定是否能滿足這位同學(xué)的需求)。
下面我們看一下Addon的實(shí)現(xiàn)。會(huì)涉及到V8的一些使用,可以先閱讀該文章《一段js理解nodejs中js調(diào)用c++/c的過程》。首先看一下基類的實(shí)現(xiàn)。
- #ifndef BASE_H
- #define BASE_H
- #include <stdio.h>
- #include <node.h>
- #include <node_object_wrap.h>
- using namespace node;
- using namespace v8;
- class Base: public ObjectWrap {
- public:
- static void New(const FunctionCallbackInfo<Value>& info) {
- // 新建一個(gè)對(duì)象,然后包裹到info.This()中,后面會(huì)解包出來使用
- Base* base = new Base();
- base->Wrap(info.This());
- }
- static void Print(const FunctionCallbackInfo<Value>& info) {
- // 解包出來使用
- Base* base = ObjectWrap::Unwrap<Base>(info.This());
- base->print();
- }
- void print() {
- printf("base print\n");
- }
- void hello() {
- printf("base hello\n");
- }
- };
- #endif
Node.js提供的ObjectWrap類實(shí)現(xiàn)了Wrap和UnWrap的功能,所以我們可以繼承它簡(jiǎn)化封包解包的邏輯。Base類定義了兩個(gè)功能函數(shù)hello和print,同時(shí)定義了兩個(gè)類靜態(tài)函數(shù)New和Print。New函數(shù)是核心邏輯,該函數(shù)在js層執(zhí)行new Base的時(shí)候會(huì)執(zhí)行并傳入一個(gè)對(duì)象,這時(shí)候我們首先創(chuàng)建一個(gè)真正的有用的對(duì)象,并且通過Wrap把該對(duì)象包裹到傳進(jìn)來的對(duì)象里。我們繼續(xù)看一下子類。
- #ifndef DERIVED_H
- #define DERIVED_H
- #include <node.h>
- #include <node_object_wrap.h>
- #include"Base.h"
- using namespace node;
- using namespace v8;
- class Derived: public Base {
- public:
- static void New(const FunctionCallbackInfo<Value>& info) {
- Derived* derived = new Derived();
- derived->Wrap(info.This());
- }
- static void Hello(const FunctionCallbackInfo<Value>& info) {
- Derived* derived = ObjectWrap::Unwrap<Derived>(info.This());
- // 調(diào)用基類的函數(shù)
- derived->hello();
- }
- };
- #endif
子類的邏輯類似,New函數(shù)和基類的邏輯一樣,除了繼承基類的方法外,額外定義了一個(gè)Hello函數(shù),但是我們看到這只是個(gè)殼子,底層還是調(diào)用了基類的函數(shù)。定義完基類和子類后,我們把這兩個(gè)類導(dǎo)出到JS。
- #include <node.h>
- #include "Base.h"
- #include "Derived.h"
- namespace demo {
- using v8::FunctionCallbackInfo;
- using v8::Isolate;
- using v8::Local;
- using v8::Object;
- using v8::String;
- using v8::Value;
- using v8::FunctionTemplate;
- using v8::Function;
- using v8::Number;
- using v8::MaybeLocal;
- using v8::Context;
- using v8::Int32;
- using v8::NewStringType;
- void Initialize(
- Local<Object> exports,
- Local<Value> module,
- Local<Context> context
- ) {
- Isolate * isolate = context->GetIsolate();
- // 新建兩個(gè)函數(shù)模板,基類和子類,js層New導(dǎo)出的函數(shù)時(shí),V8會(huì)執(zhí)行New函數(shù)并傳入一個(gè)對(duì)象
- Local<FunctionTemplate> base = FunctionTemplate::New(isolate, Base::New);
- Local<FunctionTemplate> derived = FunctionTemplate::New(isolate, Derived::New);
- // js層使用的類名
- NewStringType type = NewStringType::kNormal;
- Local<String> base_string = String::NewFromUtf8(isolate, "Base", type).ToLocalChecked();
- Local<String> derived_string = String::NewFromUtf8(isolate, "Derived", type).ToLocalChecked();
- // 預(yù)留一個(gè)指針空間
- base->InstanceTemplate()->SetInternalFieldCount(1);
- derived->InstanceTemplate()->SetInternalFieldCount(1);
- // 定義兩個(gè)函數(shù)模板,用于屬性的值
- Local<FunctionTemplate> BasePrint = FunctionTemplate::New(isolate, Base::Print);
- Local<FunctionTemplate> Hello = FunctionTemplate::New(isolate, Derived::Hello);
- // 給基類定義一個(gè)print函數(shù)
- base->PrototypeTemplate()->Set(isolate, "print", BasePrint);
- // 給子類定義一個(gè)hello函數(shù)
- derived->PrototypeTemplate()->Set(isolate, "hello", Hello);
- // 建立繼承關(guān)系
- derived->Inherit(base);
- // 導(dǎo)出兩個(gè)函數(shù)給js層
- exports->Set(context, base_string, base->GetFunction(context).ToLocalChecked()).Check();
- exports->Set(context, derived_string, derived->GetFunction(context).ToLocalChecked()).Check();
- }
- NODE_MODULE_CONTEXT_AWARE(NODE_GYP_MODULE_NAME, Initialize)
- }
我們看到給基類原型定義了一個(gè)print函數(shù),給子類定義了hello函數(shù)。最后我們看看如何在JS層使用。
- const { Base, Derived } = require('./build/Release/test.node');
- const base = new Base();
- const derived = new Derived();
- base.print();
- derived.hello();
- derived.print();
- console.log(derived instanceof Base, derived instanceof Derived)
下面是具體的輸出
- base print
- base hello
- base print
- true true
我們逐句分析
1 base.print()比較簡(jiǎn)單,就是調(diào)用基類定義的Print函數(shù)。
2 derived.hello()看起來是調(diào)用了子類的Hello函數(shù),但是Hello函數(shù)里調(diào)用了基類的hello函數(shù),實(shí)現(xiàn)了邏輯的復(fù)用。
3 derived.print()子類沒有實(shí)現(xiàn)print函數(shù),這里調(diào)用的是基類的print函數(shù),和1一樣。
4 derived instanceof Base, derived instanceof Derived。根據(jù)我們的定義,derived不僅是Derived的實(shí)例,也是Base的實(shí)例。
實(shí)現(xiàn)代碼分析完了,我們看到把C++類映射到JS的方式有兩種,第一種就是兩個(gè)C++ 類沒有繼承關(guān)系,通過V8的繼承API實(shí)現(xiàn)兩個(gè)JS層存在繼承關(guān)系的類(函數(shù)),比如print函數(shù)的實(shí)現(xiàn),我們看到子類沒有實(shí)現(xiàn)print,但是可以調(diào)用print,因?yàn)榛惗x了,Node.js就是這樣處理的。第二種就是兩個(gè)存在繼承關(guān)系的C++類,同樣先通過V8的API實(shí)現(xiàn)兩個(gè)繼承的類導(dǎo)出到JS使用,因?yàn)镴S層使用的只是殼子,具體執(zhí)行到C++代碼的時(shí)候,我們?cè)袤w現(xiàn)出這種繼承關(guān)系。比如Hello函數(shù)的實(shí)現(xiàn),雖然我們是在子類里導(dǎo)出了hello函數(shù),并且JS執(zhí)行hello的時(shí)候的確執(zhí)行到了子類的C++代碼,但是最后會(huì)調(diào)用基類的hello函數(shù)。
最后我們通過Nodej.js看看是如何做這種映射的,我們通過PipeWrap.cc的實(shí)現(xiàn)進(jìn)行分析。
- // 新建一個(gè)函數(shù)模板
- Local<FunctionTemplate> t = env->NewFunctionTemplate(New);// 繼承兩個(gè)函數(shù)模板
- t->Inherit(LibuvStreamWrap::GetConstructorTemplate(env));// 導(dǎo)出給JS使用
- exports->Set(env->context(),
- pipeString,
- t->GetFunction(env->context()).ToLocalChecked()).Check();
上面代碼實(shí)現(xiàn)了繼承,我們看看GetConstructorTemplate的實(shí)現(xiàn)。
- tmpl = env->NewFunctionTemplate(nullptr);
- env->SetProtoMethod(tmpl, "setBlocking", SetBlocking);
- env->SetProtoMethod(tmpl, "readStart", JSMethod<&StreamBase::ReadStartJS>);
- env->SetProtoMethod(t, "readStop", JSMethod<&StreamBase::ReadStopJS>);// ...
上面代碼新建了一個(gè)新的函數(shù)模板并且設(shè)置了一系列的原型屬性,那么模板t就繼承了這些屬性。我們看看Node.js里怎么使用的。
- function createHandle(fd, is_server) {
- // ...
- return new Pipe(
- is_server ? PipeConstants.SERVER : PipeConstants.SOCKET
- );}
- this._handle = createHandle(fd, false);
- err = this._handle.setBlocking(true);
上面的代碼首先會(huì)創(chuàng)建一個(gè)Pipe對(duì)象,然后調(diào)用它的setBlocking方法。我們發(fā)現(xiàn)Pipe(pipe_wrap.cc)是沒有實(shí)現(xiàn)setBlocking函數(shù)的,但是好為什么他可以調(diào)用setBlocking呢?答案就是它的基類實(shí)現(xiàn)了。
后記:在JS里實(shí)現(xiàn)繼承是簡(jiǎn)單的,但是在底層實(shí)現(xiàn)起來還是比較復(fù)雜的,但是從代碼設(shè)計(jì)的角度來看是非常有必要的。
代碼可以在倉(cāng)庫(kù)獲取:
https://github.com/theanarkh/learn-to-write-nodejs-addons。