Node.js eignet sich hervorragend für die Verwendung von JavaScript zum Schreiben von Backends, und es lohnt sich, es noch einmal auszuprobieren. Wenn Sie jedoch einige Funktionen benötigen, die nicht direkt verwendet werden können, oder sogar Module, die überhaupt nicht implementiert werden können, können Sie dann solche Ergebnisse aus der C/C-Bibliothek einführen? Die Antwort lautet: Ja, Sie müssen lediglich ein Plug-In schreiben und die Ressourcen anderer Codebibliotheken in Ihrem eigenen JavaScript-Code verwenden. Beginnen wir gemeinsam die heutige Forschungsreise.
Einführung
Wie in der offiziellen Dokumentation von Node.js angegeben, sind Plug-Ins gemeinsam genutzte Objekte, die dynamisch verknüpft sind und JavaScript-Code mit C/C-Bibliotheken verbinden können. Das bedeutet, dass wir auf alles aus einer C/C++-Bibliothek verweisen und es durch die Erstellung von Plugins in Node.js integrieren können.
Als Beispiel erstellen wir einen Wrapper für das Standardobjekt std::string.
Vorbereitung
Bevor wir mit dem Schreiben beginnen, müssen Sie zunächst sicherstellen, dass Sie alle Materialien vorbereitet haben, die für die spätere Modulzusammenstellung erforderlich sind. Sie benötigen Node-Gyp und alle seine Abhängigkeiten. Sie können den folgenden Befehl verwenden, um node-gyp zu installieren:
npm install -g node-gyp
In Bezug auf Abhängigkeiten müssen wir die folgenden Elemente für Unix-Systeme vorbereiten: • Python (erfordert Version 2.7, 3.x funktioniert nicht ordnungsgemäß)
• machen
• Eine C-Compiler-Toolkette (z. B. gpp oder g)
Unter Ubuntu können Sie beispielsweise den folgenden Befehl verwenden, um alle oben genannten Projekte zu installieren (Python 2.7 sollte vorinstalliert sein):
sudo apt-get install build-essentials
In einer Windows-Systemumgebung benötigen Sie Folgendes:
• Python (Version 2.7.3, 3.x funktioniert nicht richtig)
• Microsoft Visual Studio C 2010 (für Windows XP/Vista)
• Microsoft Visual Studio C 2012 für Windows Desktop (für Windows 7/8)
Ich betone, dass die Express-Version von Visual Studio auch normal funktioniert.
binding.gyp-Datei
Diese Datei wird von Node-Gyp verwendet, um entsprechende Build-Dateien für unser Plugin zu generieren. Sie können hier klicken, um die von Wikipedia bereitgestellte .gyp-Dateidokumentation anzuzeigen. Das Beispiel, das wir heute verwenden werden, ist jedoch sehr einfach. Verwenden Sie daher einfach den folgenden Code:
{ "targets": [ { "target_name": "stdstring", "sources": [ "addon.cc", "stdstring.cc" ] } ] }
Der Zielname kann beliebig eingestellt werden. Das Array „sources“ enthält alle Quelldateien, die das Plug-in verwenden muss. In unserem Beispiel schließen wir auch addon.cc ein, das den zum Kompilieren des Plug-ins erforderlichen Code enthält, und stdstring.cc sowie unsere Wrapper-Klasse.
STDStringWrapper-Klasse
Der erste Schritt besteht darin, unsere eigene Klasse in der Datei stdstring.h zu definieren. Wenn Sie mit der C-Programmierung vertraut sind, werden Ihnen die folgenden beiden Codezeilen sicherlich nicht unbekannt sein.
#ifndef STDSTRING_H #define STDSTRING_H
Dies ist ein standardmäßiger Include-Schutz. Als nächstes müssen wir die folgenden zwei Header in die Include-Kategorie aufnehmen:
#include
#include
Der erste ist für die Klasse std::string, während der zweite für alles gilt, was mit Node und V8 zu tun hat.
Nachdem dieser Schritt abgeschlossen ist, können wir unsere eigene Klasse deklarieren:
Klasse STDStringWrapper: öffentlicher Knoten::ObjectWrap {
Für alle Klassen, die wir in das Plugin aufnehmen möchten, müssen wir die Klasse node::ObjectWrap erweitern.
Jetzt können wir mit der Definition der privaten Eigenschaften dieser Klasse beginnen:
private: std::string* s_; explicit STDStringWrapper(std::string s = ""); ~STDStringWrapper();
Zusätzlich zum Konstruktor und der Parsing-Funktion müssen wir auch einen Zeiger für std::string definieren. Dies ist der Kern der Technologie, die verwendet werden kann, um eine C/C-Codebasis mit Node zu verbinden – wir definieren einen privaten Zeiger für die C/C-Klasse und werden diesen Zeiger verwenden, um Operationen in allen nachfolgenden Methoden zu implementieren.
Jetzt deklarieren wir die statische Eigenschaft des Konstruktors, die Funktionen für die Klasse bereitstellt, die wir in V8 erstellt haben:
statischer v8::Persistenter Konstruktor;
Interessierte Freunde können hier klicken, um weitere Einzelheiten im Beschreibungsplan der Vorlage zu erfahren.
Jetzt benötigen wir auch eine neue Methode, die dem zuvor erwähnten Konstruktor zugewiesen wird, und V8 wird unsere Klasse initialisieren:
static v8::Handle New(const v8::Arguments& args);
Jede Funktion, die auf V8 wirkt, sollte die folgenden Anforderungen erfüllen: Sie akzeptiert einen Verweis auf ein v8::Arguments-Objekt und gibt ein v8::Handle>v8::Value> zurück – das ist es, was wir mit stark typisierten C-Codierungs-V8-Handles verwenden Schwach typisiertes JavaScript konsistent.
Danach müssen wir noch zwei weitere Methoden in den Prototyp des Objekts einfügen:
static v8::Handle add(const v8::Arguments& args); static v8::Handle toString(const v8::Arguments& args);
Mit der toString()-Methode können wir den Wert von s_ anstelle des Werts von [Object object] erhalten, wenn wir sie mit gewöhnlichen JavaScript-Strings verwenden.
Abschließend stellen wir die Initialisierungsmethode vor (diese Methode wird von V8 aufgerufen und der Konstruktorfunktion zugewiesen) und deaktivieren den Include-Schutz:
public: static void Init(v8::Handle exports); }; #endif
其中exports对象在JavaScript模块中的作用等同于module.exports。
stdstring.cc文件、构造函数与解析函数
现在来创建stdstring.cc文件。我们首先需要include我们的header:
#include "stdstring.h"
下面为constructor定义属性(因为它属于静态函数):
v8::Persistent STDStringWrapper::constructor;
这个为类服务的构造函数将分配s_属性:
STDStringWrapper::STDStringWrapper(std::string s) { s_ = new std::string(s); }
而解析函数将对其进行delete,从而避免内存溢出:
STDStringWrapper::~STDStringWrapper() { delete s_; }
再有,大家必须delete掉所有与new一同分配的内容,因为每一次此类情况都有可能造成异常,因此请牢牢记住上述操作或者使用共享指针。
Init方法
该方法将由V8加以调用,旨在对我们的类进行初始化(分配constructor,将我们所有打算在JavaScript当中使用的内容安置在exports对象当中):
void STDStringWrapper::Init(v8::Handle exports) {
首先,我们需要为自己的New方法创建一个函数模板:
v8::Local tpl = v8::FunctionTemplate::New(New);
这有点类似于JavaScipt当中的new Function——它允许我们准备好自己的JavaScript类。
现在我们可以根据实际需要为该函数设定名称了(如果大家漏掉了这一步,那么构造函数将处于匿名状态,即名称为function someName() {}或者function () {}):
tpl->SetClassName(v8::String::NewSymbol("STDString"));
我们利用v8::String::NewSymbol()来创建一个用于属性名称的特殊类型字符串——这能为引擎的运作节约一点点时间。
在此之后,我们需要设定我们的类实例当中包含多少个字段:
tpl->InstanceTemplate()->SetInternalFieldCount(2);
我们拥有两个方法——add()与toString(),因此我们将数量设置为2。现在我们可以将自己的方法添加到函数原型当中了:
tpl->PrototypeTemplate()->Set(v8::String::NewSymbol("add"), v8::FunctionTemplate::New(add)->GetFunction());
tpl->PrototypeTemplate()->Set(v8::String::NewSymbol("toString"), v8::FunctionTemplate::New(toString)->GetFunction());
这部分代码量看起来比较大,但只要认真观察大家就会发现其中的规律:我们利用tpl->PrototypeTemplate()->Set()来添加每一个方法。我们还利用v8::String::NewSymbol()为它们提供名称与FunctionTemplate。
最后,我们可以将该构造函数安置于我们的constructor类属性内的exports对象中:
constructor = v8::Persistent::New(tpl->GetFunction()); exports->Set(v8::String::NewSymbol("STDString"), constructor); }
New方法
现在我们要做的是定义一个与JavaScript Object.prototype.constructor运作效果相同的方法:
v8::Handle STDStringWrapper::New(const v8::Arguments& args) {
我们首先需要为其创建一个范围:
v8::HandleScope scope;
在此之后,我们可以利用args对象的.IsConstructCall()方法来检查该构造函数是否能够利用new关键词加以调用:
if (args.IsConstructCall()) {
如果可以,我们首先如下所示将参数传递至std::string处:
v8::String::Utf8Value str(args[0]->ToString()); std::string s(*str);
……这样我们就能将它传递到我们封装类的构造函数当中了:
STDStringWrapper* obj = new STDStringWrapper(s);
在此之后,我们可以利用之前创建的该对象的.Wrap()方法(继承自node::ObjectWrap)来将它分配给this变量:
obj->Wrap(args.This());
最后,我们可以返回这个新创建的对象:
return args.This();
如果该函数无法利用new进行调用,我们也可以直接调用构造函数。接下来,我们要做的是为参数计数设置一个常数:
} else { const int argc = 1;
现在我们需要利用自己的参数创建一个数组:
v8::Local argv[argc] = { args[0] };
然后将constructor->NewInstance方法的结果传递至scope.Close,这样该对象就能在随后发挥作用(scope.Close基本上允许大家通过将对象处理句柄移动至更高范围的方式对其加以维持——这也是函数的起效方式):
return scope.Close(constructor->NewInstance(argc, argv)); } }
add方法
现在让我们创建add方法,它的作用是允许大家向对象的内部std::string添加内容:
v8::Handle STDStringWrapper::add(const v8::Arguments& args) {
首先,我们需要为我们的函数创建一个范围,并像之前那样把该参数转换到std::string当中:
v8::HandleScope scope; v8::String::Utf8Value str(args[0]->ToString()); std::string s(*str);
现在我们需要对该对象进行拆包。我们之前也进行过这种反向封装操作——这一次我们是要从this变量当中获取指向对象的指针。
STDStringWrapper* obj = ObjectWrap::Unwrap(args.This());
接着我们可以访问s_属性并使用其.append()方法:
obj->s_->append(s);
最后,我们返回s_属性的当前值(需要再次使用scope.Close):
return scope.Close(v8::String::New(obj->s_->c_str()));
由于v8::String::New()方法只能将char pointer作为值来接受,因此我们需要使用obj->s_->c_str()来加以获取。
这时大家的插件文件夹中还应该创建出一个build目录。
测试
现在我们可以对自己的插件进行测试了。在我们的插件目录中创建一个test.js文件以及必要的编译库(大家可以直接略过.node扩展):
var addon = require('./build/Release/addon');
下一步,为我们的对象创建一个新实例:
var test = new addon.STDString('test');
下面再对其进行操作,例如添加或者将其转化为字符串:
test.add('!'); console.log('test\'s contents: %s', test);
在运行之后,大家应该在控制台中看到以下执行结果:
结论
我希望大家能在阅读了本教程之后打消顾虑,将创建与测试以C/C++库为基础的定制化Node.js插件视为一项无甚难度的任务。大家可以利用这种技术轻松将几乎任何C/C++库引入Node.js当中。如果大家愿意,还可以根据实际需求为插件添加更多功能。std::string当中提供大量方法,我们可以将它们作为练习素材。
实用链接
感兴趣的朋友可以查看以下链接以获取更多与Node.js插件开发、V8以及C事件循环库相关的资源与详细信息。
• Node.js插件说明文档
• V8说明文档
• libuv (C事件循环库),来自GitHub
英文:http://code.tutsplus.com/tutorials/writing-nodejs-addons--cms-21771