使用Node.js Addon实现类继承

开发 前端
昨天有个同学问怎么通过NAPI把C++类的继承关系映射到JS,很遗憾,NAPI貌似还不支持,但是V8支持,因为V8在头文件里导出了这些API,并Node.js里也依赖这些API,所以可以说是比较稳定的。本文介绍一下如何实现这种映射(不确定是否能满足这位同学的需求)。

[[411576]]

本文转载自微信公众号「编程杂技」,作者theanarkh。转载本文请联系编程杂技公众号。

前言:昨天有个同学问怎么通过NAPI把C++类的继承关系映射到JS,很遗憾,NAPI貌似还不支持,但是V8支持,因为V8在头文件里导出了这些API,并Node.js里也依赖这些API,所以可以说是比较稳定的。本文介绍一下如何实现这种映射(不确定是否能满足这位同学的需求)。

下面我们看一下Addon的实现。会涉及到V8的一些使用,可以先阅读该文章《一段js理解nodejs中js调用c++/c的过程》。首先看一下基类的实现。

  1. #ifndef BASE_H 
  2. #define BASE_H 
  3. #include <stdio.h> 
  4. #include <node.h> 
  5. #include <node_object_wrap.h> 
  6.  
  7. using namespace node; 
  8. using namespace v8; 
  9. class Base: public ObjectWrap { 
  10.     public
  11.         static void New(const FunctionCallbackInfo<Value>& info) { 
  12.             // 新建一个对象,然后包裹到info.This()中,后面会解包出来使用 
  13.             Base* base =  new Base(); 
  14.             base->Wrap(info.This()); 
  15.         } 
  16.  
  17.         static void Print(const FunctionCallbackInfo<Value>& info) { 
  18.             // 解包出来使用 
  19.             Base* base = ObjectWrap::Unwrap<Base>(info.This()); 
  20.             base->print(); 
  21.         } 
  22.  
  23.         void print() { 
  24.             printf("base print\n"); 
  25.         } 
  26.  
  27.         void hello() { 
  28.             printf("base hello\n"); 
  29.         } 
  30. }; 
  31.  
  32. #endif 

Node.js提供的ObjectWrap类实现了Wrap和UnWrap的功能,所以我们可以继承它简化封包解包的逻辑。Base类定义了两个功能函数hello和print,同时定义了两个类静态函数New和Print。New函数是核心逻辑,该函数在js层执行new Base的时候会执行并传入一个对象,这时候我们首先创建一个真正的有用的对象,并且通过Wrap把该对象包裹到传进来的对象里。我们继续看一下子类。

  1. #ifndef DERIVED_H 
  2. #define DERIVED_H 
  3. #include <node.h> 
  4. #include <node_object_wrap.h> 
  5. #include"Base.h" 
  6.  
  7. using namespace node; 
  8. using namespace v8; 
  9. class Derived: public Base { 
  10.     public
  11.         static void New(const FunctionCallbackInfo<Value>& info) { 
  12.             Derived* derived =  new Derived(); 
  13.             derived->Wrap(info.This()); 
  14.         } 
  15.  
  16.         static void Hello(const FunctionCallbackInfo<Value>& info) { 
  17.             Derived* derived = ObjectWrap::Unwrap<Derived>(info.This()); 
  18.             // 调用基类的函数 
  19.             derived->hello(); 
  20.         } 
  21. }; 
  22.  
  23. #endif 

子类的逻辑类似,New函数和基类的逻辑一样,除了继承基类的方法外,额外定义了一个Hello函数,但是我们看到这只是个壳子,底层还是调用了基类的函数。定义完基类和子类后,我们把这两个类导出到JS。

  1. #include <node.h> 
  2. #include "Base.h" 
  3. #include "Derived.h" 
  4.  
  5. namespace demo { 
  6.  
  7. using v8::FunctionCallbackInfo; 
  8. using v8::Isolate; 
  9. using v8::Local
  10. using v8::Object; 
  11. using v8::String; 
  12. using v8::Value; 
  13. using v8::FunctionTemplate; 
  14. using v8::Function
  15. using v8::Number; 
  16. using v8::MaybeLocal; 
  17. using v8::Context; 
  18. using v8::Int32; 
  19. using v8::NewStringType; 
  20.  
  21. void Initialize( 
  22.   Local<Object> exports, 
  23.   Local<Value> module, 
  24.   Local<Context> context 
  25. ) { 
  26.   Isolate * isolate = context->GetIsolate(); 
  27.   // 新建两个函数模板,基类和子类,js层New导出的函数时,V8会执行New函数并传入一个对象 
  28.   Local<FunctionTemplate> base = FunctionTemplate::New(isolate, Base::New); 
  29.   Local<FunctionTemplate> derived = FunctionTemplate::New(isolate, Derived::New); 
  30.  
  31.   // js层使用的类名 
  32.   NewStringType type = NewStringType::kNormal; 
  33.   Local<String> base_string = String::NewFromUtf8(isolate, "Base", type).ToLocalChecked(); 
  34.   Local<String> derived_string = String::NewFromUtf8(isolate, "Derived", type).ToLocalChecked(); 
  35.  
  36.   // 预留一个指针空间 
  37.   base->InstanceTemplate()->SetInternalFieldCount(1); 
  38.   derived->InstanceTemplate()->SetInternalFieldCount(1); 
  39.  
  40.   // 定义两个函数模板,用于属性的值 
  41.   Local<FunctionTemplate> BasePrint = FunctionTemplate::New(isolate, Base::Print); 
  42.   Local<FunctionTemplate> Hello = FunctionTemplate::New(isolate, Derived::Hello); 
  43.  
  44.   // 给基类定义一个print函数 
  45.   base->PrototypeTemplate()->Set(isolate, "print", BasePrint); 
  46.   // 给子类定义一个hello函数 
  47.   derived->PrototypeTemplate()->Set(isolate, "hello", Hello); 
  48.   // 建立继承关系 
  49.   derived->Inherit(base); 
  50.   // 导出两个函数给js层 
  51.   exports->Set(context, base_string, base->GetFunction(context).ToLocalChecked()).Check(); 
  52.   exports->Set(context, derived_string, derived->GetFunction(context).ToLocalChecked()).Check(); 
  53.  
  54. NODE_MODULE_CONTEXT_AWARE(NODE_GYP_MODULE_NAME, Initialize) 

我们看到给基类原型定义了一个print函数,给子类定义了hello函数。最后我们看看如何在JS层使用。

  1. const { Base, Derived } = require('./build/Release/test.node'); 
  2. const base = new Base(); 
  3. const derived = new Derived(); 
  4. base.print(); 
  5. derived.hello(); 
  6. derived.print(); 
  7. console.log(derived instanceof Base, derived instanceof Derived) 

下面是具体的输出

  1. base print 
  2. base hello 
  3. base print 
  4. true true 

我们逐句分析

1 base.print()比较简单,就是调用基类定义的Print函数。

2 derived.hello()看起来是调用了子类的Hello函数,但是Hello函数里调用了基类的hello函数,实现了逻辑的复用。

3 derived.print()子类没有实现print函数,这里调用的是基类的print函数,和1一样。

4 derived instanceof Base, derived instanceof Derived。根据我们的定义,derived不仅是Derived的实例,也是Base的实例。

实现代码分析完了,我们看到把C++类映射到JS的方式有两种,第一种就是两个C++ 类没有继承关系,通过V8的继承API实现两个JS层存在继承关系的类(函数),比如print函数的实现,我们看到子类没有实现print,但是可以调用print,因为基类定义了,Node.js就是这样处理的。第二种就是两个存在继承关系的C++类,同样先通过V8的API实现两个继承的类导出到JS使用,因为JS层使用的只是壳子,具体执行到C++代码的时候,我们再体现出这种继承关系。比如Hello函数的实现,虽然我们是在子类里导出了hello函数,并且JS执行hello的时候的确执行到了子类的C++代码,但是最后会调用基类的hello函数。

最后我们通过Nodej.js看看是如何做这种映射的,我们通过PipeWrap.cc的实现进行分析。

  1. // 新建一个函数模板 
  2. Local<FunctionTemplate> t = env->NewFunctionTemplate(New);// 继承两个函数模板 
  3. t->Inherit(LibuvStreamWrap::GetConstructorTemplate(env));// 导出给JS使用 
  4. exports->Set(env->context(), 
  5.               pipeString, 
  6.               t->GetFunction(env->context()).ToLocalChecked()).Check(); 

上面代码实现了继承,我们看看GetConstructorTemplate的实现。

  1. tmpl = env->NewFunctionTemplate(nullptr); 
  2. env->SetProtoMethod(tmpl, "setBlocking", SetBlocking); 
  3. env->SetProtoMethod(tmpl, "readStart", JSMethod<&StreamBase::ReadStartJS>); 
  4. env->SetProtoMethod(t, "readStop", JSMethod<&StreamBase::ReadStopJS>);// ... 

上面代码新建了一个新的函数模板并且设置了一系列的原型属性,那么模板t就继承了这些属性。我们看看Node.js里怎么使用的。

  1. function createHandle(fd, is_server) { 
  2.   // ... 
  3.   return new Pipe( 
  4.       is_server ? PipeConstants.SERVER : PipeConstants.SOCKET 
  5.   );} 
  6.  
  7. this._handle = createHandle(fd, false); 
  8. err = this._handle.setBlocking(true); 

上面的代码首先会创建一个Pipe对象,然后调用它的setBlocking方法。我们发现Pipe(pipe_wrap.cc)是没有实现setBlocking函数的,但是好为什么他可以调用setBlocking呢?答案就是它的基类实现了。

后记:在JS里实现继承是简单的,但是在底层实现起来还是比较复杂的,但是从代码设计的角度来看是非常有必要的。

代码可以在仓库获取:

https://github.com/theanarkh/learn-to-write-nodejs-addons。

 

 

责任编辑:武晓燕 来源: 编程杂技
相关推荐

2023-11-24 14:54:03

Node.jsElectronAddon

2014-04-10 09:43:00

Node.jsTwilio

2022-08-28 16:30:34

Node.jsDocker指令

2020-08-07 10:40:56

Node.jsexpress前端

2023-01-10 14:11:26

2015-03-10 10:59:18

Node.js开发指南基础介绍

2013-11-01 09:34:56

Node.js技术

2021-03-09 08:03:21

Node.js 线程JavaScript

2012-09-29 11:13:15

Node.JS前端开发Node.js打包

2016-08-25 21:28:04

前端node截图

2022-08-22 07:26:32

Node.js微服务架构

2021-07-30 11:20:53

JavaScriptNode.jsWeb Develop

2021-03-03 06:39:05

Nodejs前端开发

2011-09-08 13:46:14

node.js

2011-09-02 14:47:48

Node

2011-09-09 14:23:13

Node.js

2011-11-01 10:30:36

Node.js

2012-10-24 14:56:30

IBMdw

2011-11-10 08:55:00

Node.js

2014-04-10 09:55:46

手机Node.jswilio
点赞
收藏

51CTO技术栈公众号