ملحقات C++
الملحقات هي كائنات مشتركة مرتبطة ديناميكيًا مكتوبة بلغة C++. يمكن للدالة require()
تحميل الملحقات كمعاملات Node.js عادية. توفر الملحقات واجهة بين JavaScript ومكتبات C/C++.
هناك ثلاثة خيارات لتنفيذ الملحقات:
- واجهة برمجة تطبيقات العقدة (Node-API)
nan
(الواجهات المجردة الأصلية لـ Node.js)- الاستخدام المباشر لـ V8 الداخلية، وlibuv، ومكتبات Node.js
ما لم تكن هناك حاجة للوصول المباشر إلى الوظائف التي لا تعرضها واجهة برمجة تطبيقات العقدة، استخدم واجهة برمجة تطبيقات العقدة. راجع ملحقات C/C++ مع واجهة برمجة تطبيقات العقدة لمزيد من المعلومات حول واجهة برمجة تطبيقات العقدة.
عند عدم استخدام واجهة برمجة تطبيقات العقدة، يصبح تنفيذ الملحقات أكثر تعقيدًا، ويتطلب معرفة مكونات وواجهات برمجة تطبيقات متعددة:
- V8: مكتبة C++ تستخدمها Node.js لتوفير تنفيذ JavaScript. توفر الآليات لإنشاء الكائنات، واستدعاء الوظائف، إلخ. يتم توثيق واجهة برمجة تطبيقات V8 في الغالب في ملف الرأس
v8.h
(deps/v8/include/v8.h
في شجرة مصدر Node.js)، وهي متاحة أيضًا عبر الإنترنت. - libuv: مكتبة C التي تُنفذ حلقة أحداث Node.js، وخيوط عمالها وجميع السلوكيات غير المتزامنة للمنصة. كما تعمل كمكتبة تجريد عبر الأنظمة الأساسية، مما يوفر وصولًا سهلاً يشبه POSIX عبر جميع أنظمة التشغيل الرئيسية إلى العديد من مهام النظام الشائعة، مثل التفاعل مع نظام الملفات، والمقابس، والعدادات الزمنية، وأحداث النظام. توفر libuv أيضًا تجريدًا للخيوط مشابهًا لخيوط POSIX للملحقات غير المتزامنة الأكثر تطوراً التي تحتاج إلى الانتقال إلى ما هو أبعد من حلقة الأحداث القياسية. يجب على مؤلفي الملحقات تجنب حظر حلقة الأحداث باستخدام I/O أو مهام كثيفة الاستهلاك للوقت عن طريق تفريغ العمل عبر libuv إلى عمليات نظام غير مسدودة، أو خيوط العمال، أو استخدام مخصص لخيوط libuv.
- مكتبات Node.js الداخلية: تصدر Node.js نفسها واجهات برمجة تطبيقات C++ يمكن للملحقات استخدامها، وأهمها فئة
node::ObjectWrap
. - مكتبات أخرى مرتبطة ثابتًا (بما في ذلك OpenSSL): توجد هذه المكتبات الأخرى في دليل
deps/
في شجرة مصدر Node.js. يتم إعادة تصدير رموز libuv و OpenSSL و V8 و zlib فقط عن قصد بواسطة Node.js ويمكن استخدامها إلى حد كبير من قبل الملحقات. راجع الربط بمكتبات مضمنة مع Node.js لمزيد من المعلومات.
جميع الأمثلة التالية متاحة للتنزيل هنا ويمكن استخدامها كنقطة انطلاق لملحق.
مرحباً بالعالم
يُعدّ مثال "مرحباً بالعالم" هذا إضافة بسيطة، مكتوبة بلغة C++، تُعادل شفرة JavaScript التالية:
module.exports.hello = () => 'world'
أولاً، أنشئ الملف hello.cc
:
// hello.cc
#include <node.h>
namespace demo {
using v8::FunctionCallbackInfo;
using v8::Isolate;
using v8::Local;
using v8::NewStringType;
using v8::Object;
using v8::String;
using v8::Value;
void Method(const FunctionCallbackInfo<Value>& args) {
Isolate* isolate = args.GetIsolate();
args.GetReturnValue().Set(String::NewFromUtf8(
isolate, "world", NewStringType::kNormal).ToLocalChecked());
}
void Initialize(Local<Object> exports) {
NODE_SET_METHOD(exports, "hello", Method);
}
NODE_MODULE(NODE_GYP_MODULE_NAME, Initialize)
} // namespace demo
يجب على جميع إضافات Node.js تصدير دالة تهيئة تتبع النمط التالي:
void Initialize(Local<Object> exports);
NODE_MODULE(NODE_GYP_MODULE_NAME, Initialize)
لا يوجد فاصلة منقوطة بعد NODE_MODULE
لأنها ليست دالة (انظر node.h
).
يجب أن يتطابق module_name
مع اسم الملف الثنائي النهائي (باستثناء لاحقة .node
).
في مثال hello.cc
، إذن، دالة التهيئة هي Initialize
واسم وحدة الإضافة هو addon
.
عند بناء الإضافات باستخدام node-gyp
، فإن استخدام المعلمة الكلية NODE_GYP_MODULE_NAME
كالمعامل الأول لـ NODE_MODULE()
سيضمن تمرير اسم الملف الثنائي النهائي إلى NODE_MODULE()
.
لا يمكن تحميل الإضافات المُعرّفة باستخدام NODE_MODULE()
في سياقات متعددة أو خيوط متعددة في نفس الوقت.
الإضافات المُدركة للسياق
توجد بيئات قد تحتاج فيها إضافات Node.js إلى التحميل عدة مرات في سياقات متعددة. على سبيل المثال، يقوم وقت تشغيل Electron بتشغيل مثيلات متعددة من Node.js في عملية واحدة. سيكون لكل مثيل ذاكرته المُخزنة الخاصة بـ require()
، وبالتالي سيحتاج كل مثيل إلى إضافة أصلية للعمل بشكل صحيح عند التحميل عبر require()
. وهذا يعني أن الإضافة يجب أن تدعم عمليات تهيئة متعددة.
يمكن إنشاء إضافة مُدركة للسياق باستخدام المعلمة الكلية NODE_MODULE_INITIALIZER
، والتي تتوسع إلى اسم دالة سيتوقع Node.js العثور عليها عند تحميل إضافة. وبالتالي، يمكن تهيئة إضافة كما في المثال التالي:
using namespace v8;
extern "C" NODE_MODULE_EXPORT void
NODE_MODULE_INITIALIZER(Local<Object> exports,
Local<Value> module,
Local<Context> context) {
/* قم بأداء خطوات تهيئة الإضافة هنا. */
}
هناك خيار آخر وهو استخدام المعلمة الكلية NODE_MODULE_INIT()
، والتي ستنشئ أيضًا إضافة مُدركة للسياق. على عكس NODE_MODULE()
، المُستخدمة لإنشاء إضافة حول دالة تهيئة إضافة معينة، تعمل NODE_MODULE_INIT()
كإعلان لمثل هذه المُهيّئ لتليها جسم الدالة.
يمكن استخدام المتغيرات الثلاثة التالية داخل جسم الدالة بعد استدعاء NODE_MODULE_INIT()
:
Local\<Object\> exports
،Local\<Value\> module
، وLocal\<Context\> context
يتطلب بناء إضافة مُدركة للسياق إدارة دقيقة للبيانات الثابتة العالمية لضمان الاستقرار والصحة. نظرًا لأن الإضافة قد يتم تحميلها عدة مرات، وربما من خيوط مختلفة، يجب حماية أي بيانات ثابتة عالمية مخزنة في الإضافة بشكل صحيح، ويجب ألا تحتوي على أي مراجع دائمة لأشياء JavaScript. والسبب في ذلك هو أن أشياء JavaScript صالحة فقط في سياق واحد، ومن المحتمل أن تسبب تعطلًا عند الوصول إليها من السياق الخاطئ أو من خيط مختلف عن الخيط الذي تم إنشاؤها فيه.
يمكن هيكلة الإضافة المُدركة للسياق لتجنب البيانات الثابتة العالمية باتباع الخطوات التالية:
- تعريف فئة ستحتفظ ببيانات لكل مثيل إضافة ولديها عضو ثابت من النموذج
- تخصيص مثيل لهذه الفئة في مُهيئ الإضافة على الكومة. يمكن تحقيق ذلك باستخدام الكلمة المفتاحية
new
. - استدعاء
node::AddEnvironmentCleanupHook()
، ومرور المثيل الذي تم إنشاؤه أعلاه ومؤشر إلىDeleteInstance()
إليه. سيضمن هذا حذف المثيل عند تفكيك البيئة. - تخزين مثيل الفئة في
v8::External
، و - تمرير
v8::External
إلى جميع الطرق المُعرّضة لـ JavaScript عن طريق تمريرها إلىv8::FunctionTemplate::New()
أوv8::Function::New()
الذي ينشئ دوال JavaScript المدعومة أصلًا. يقبل المعامل الثالث لـv8::FunctionTemplate::New()
أوv8::Function::New()
v8::External
ويجعله متاحًا في المُستدعى الأصلي باستخدام طريقةv8::FunctionCallbackInfo::Data()
.
سيضمن هذا وصول بيانات كل مثيل إضافة إلى كل ارتباط يمكن استدعاؤه من JavaScript. يجب أيضًا تمرير بيانات كل مثيل إضافة إلى أي مُستدعيات غير متزامنة قد تنشئها الإضافة.
يُوضح المثال التالي تطبيق إضافة مُدركة للسياق:
#include <node.h>
using namespace v8;
class AddonData {
public:
explicit AddonData(Isolate* isolate):
call_count(0) {
// تأكد من حذف بيانات كل مثيل إضافة هذه عند تنظيف البيئة.
node::AddEnvironmentCleanupHook(isolate, DeleteInstance, this);
}
// بيانات كل إضافة.
int call_count;
static void DeleteInstance(void* data) {
delete static_cast<AddonData*>(data);
}
};
static void Method(const v8::FunctionCallbackInfo<v8::Value>& info) {
// استرداد بيانات كل مثيل إضافة.
AddonData* data =
reinterpret_cast<AddonData*>(info.Data().As<External>()->Value());
data->call_count++;
info.GetReturnValue().Set((double)data->call_count);
}
// تهيئة هذه الإضافة لتكون مُدركة للسياق.
NODE_MODULE_INIT(/* exports, module, context */) {
Isolate* isolate = context->GetIsolate();
// إنشاء مثيل جديد لـ `AddonData` لهذا المثيل من الإضافة و
// ربط دورة حياته بدورة حياة بيئة Node.js.
AddonData* data = new AddonData(isolate);
// لف البيانات في `v8::External` حتى نتمكن من تمريرها إلى الطريقة التي
// نعرضها.
Local<External> external = External::New(isolate, data);
// عرض الطريقة `Method` على JavaScript، والتأكد من أنها تتلقى
// بيانات كل مثيل إضافة التي أنشأناها أعلاه عن طريق تمرير `external` كمعامل
// ثالث لباني `FunctionTemplate`.
exports->Set(context,
String::NewFromUtf8(isolate, "method").ToLocalChecked(),
FunctionTemplate::New(isolate, Method, external)
->GetFunction(context).ToLocalChecked()).FromJust();
}
دعم عامل التشغيل
[السجل]
الإصدار | التغييرات |
---|---|
v14.8.0، v12.19.0 | يمكن الآن أن تكون خطافات التنظيف غير متزامنة. |
لكي يتم تحميل إضافة ما من بيئات Node.js متعددة، مثل مؤشر ترابط رئيسي ومؤشر ترابط عامل، يجب أن تكون الإضافة إما:
- إضافة Node-API، أو
- مُعلن عنها بأنها واعية للسياق باستخدام
NODE_MODULE_INIT()
كما هو موضح أعلاه
لدعم مؤشرات ترابط Worker
، تحتاج الإضافات إلى تنظيف أي موارد قد تكون قد خصصتها عند خروج مثل هذا المؤشر الترابطي. يمكن تحقيق ذلك من خلال استخدام دالة AddEnvironmentCleanupHook()
:
void AddEnvironmentCleanupHook(v8::Isolate* isolate,
void (*fun)(void* arg),
void* arg);
تضيف هذه الدالة خطافًا سيعمل قبل إغلاق مثيل Node.js معين. إذا لزم الأمر، يمكن إزالة هذه الخطافات قبل تشغيلها باستخدام RemoveEnvironmentCleanupHook()
، التي لها نفس البنية. يتم تشغيل الاستدعاءات العكسية بترتيب آخر دخول أول خروج.
إذا لزم الأمر، يوجد زوج إضافي من AddEnvironmentCleanupHook()
و RemoveEnvironmentCleanupHook()
، حيث يأخذ خطاف التنظيف دالة استدعاء عكسي. يمكن استخدام هذا لإغلاق الموارد غير المتزامنة، مثل أي مقابض libuv مسجلة بواسطة الإضافة.
يستخدم addon.cc
التالي AddEnvironmentCleanupHook
:
// addon.cc
#include <node.h>
#include <assert.h>
#include <stdlib.h>
using node::AddEnvironmentCleanupHook;
using v8::HandleScope;
using v8::Isolate;
using v8::Local;
using v8::Object;
// ملاحظة: في تطبيق حقيقي، لا تعتمد على البيانات الثابتة/العالمية.
static char cookie[] = "yum yum";
static int cleanup_cb1_called = 0;
static int cleanup_cb2_called = 0;
static void cleanup_cb1(void* arg) {
Isolate* isolate = static_cast<Isolate*>(arg);
HandleScope scope(isolate);
Local<Object> obj = Object::New(isolate);
assert(!obj.IsEmpty()); // التأكيد على أن آلة الظاهرية لا تزال حية
assert(obj->IsObject());
cleanup_cb1_called++;
}
static void cleanup_cb2(void* arg) {
assert(arg == static_cast<void*>(cookie));
cleanup_cb2_called++;
}
static void sanity_check(void*) {
assert(cleanup_cb1_called == 1);
assert(cleanup_cb2_called == 1);
}
// تهيئة هذه الإضافة لتكون واعية للسياق.
NODE_MODULE_INIT(/* exports, module, context */) {
Isolate* isolate = context->GetIsolate();
AddEnvironmentCleanupHook(isolate, sanity_check, nullptr);
AddEnvironmentCleanupHook(isolate, cleanup_cb2, cookie);
AddEnvironmentCleanupHook(isolate, cleanup_cb1, isolate);
}
اختبر في JavaScript عن طريق التشغيل:
// test.js
require('./build/Release/addon')
البناء
بمجرد كتابة التعليمات البرمجية المصدر، يجب تجميعها في ملف ثنائي addon.node
. للقيام بذلك، قم بإنشاء ملف يسمى binding.gyp
في المستوى الأعلى للمشروع يصف تكوين بناء الوحدة باستخدام تنسيق مشابه لـ JSON. يستخدم هذا الملف بواسطة node-gyp، وهي أداة مكتوبة خصيصًا لتجميع إضافات Node.js.
{
"targets": [
{
"target_name": "addon",
"sources": ["hello.cc"]
}
]
}
يأتي إصدار من أداة node-gyp
مجمعًا وموزعًا مع Node.js كجزء من npm
. هذا الإصدار غير متاح مباشرة للمطورين للاستخدام، وهو مخصص فقط لدعم القدرة على استخدام الأمر npm install
لتجميع وتثبيت الإضافات. يمكن للمطورين الذين يرغبون في استخدام node-gyp
مباشرة تثبيته باستخدام الأمر npm install -g node-gyp
. راجع تعليمات التثبيت الخاصة بـ node-gyp
لمزيد من المعلومات، بما في ذلك متطلبات كل نظام أساسي.
بمجرد إنشاء ملف binding.gyp
، استخدم node-gyp configure
لإنشاء ملفات بناء المشروع المناسبة للمنصة الحالية. سيؤدي هذا إلى إنشاء ملف Makefile
(على أنظمة Unix) أو ملف vcxproj
(على Windows) في دليل build/
.
بعد ذلك، قم باستدعاء الأمر node-gyp build
لإنشاء ملف addon.node
المترجم. سيتم وضعه في دليل build/Release/
.
عند استخدام npm install
لتثبيت إضافة Node.js، يستخدم npm إصداره الخاص المجمع من node-gyp
لأداء نفس مجموعة الإجراءات، مما ينتج عنه إصدار مُترجم من الإضافة لمنصة المستخدم عند الطلب.
بمجرد البناء، يمكن استخدام الإضافة الثنائية من داخل Node.js من خلال الإشارة إلى الوحدة addon.node
المُبنية باستخدام require()
:
// hello.js
const addon = require('./build/Release/addon')
console.log(addon.hello())
// يطبع: 'world'
بسبب اختلاف المسار الدقيق إلى الإضافة الثنائية المُترجمة اعتمادًا على كيفية تجميعها (أي في بعض الأحيان قد يكون في ./build/Debug/
), يمكن للإضافات استخدام حزمة bindings لتحميل الوحدة المُترجمة.
في حين أن تنفيذ حزمة bindings
أكثر تطوراً في كيفية تحديد موقع وحدات الإضافة، إلا أنه يستخدم نمط try…catch
مشابه لما يلي:
try {
return require('./build/Release/addon.node')
} catch (err) {
return require('./build/Debug/addon.node')
}
الربط بمكتبات مضمنة مع Node.js
يستخدم Node.js مكتبات مرتبطة ثابتًا مثل V8 و libuv و OpenSSL. يُطلب من جميع الوظائف الإضافية الربط مع V8، وقد تربط بأي من التبعيات الأخرى أيضًا. عادةً، يكون هذا بسيطًا مثل تضمين بيانات #include \<...\>
المناسبة (مثل #include \<v8.h\>
)، وسيحدد node-gyp
رؤوس الملفات المناسبة تلقائيًا. ومع ذلك، هناك بعض التحذيرات التي يجب الانتباه إليها:
- عندما يعمل
node-gyp
، فإنه سيكتشف إصدار إصدار Node.js المحدد، وسيقوم بتنزيل ملف tarball المصدر الكامل أو رؤوس الملفات فقط. إذا تم تنزيل المصدر الكامل، فسيكون للوظائف الإضافية وصول كامل إلى مجموعة كاملة من تبعيات Node.js. ومع ذلك، إذا تم تنزيل رؤوس Node.js فقط، فسيكون فقط الرموز التي تم تصديرها بواسطة Node.js متاحة. - يمكن تشغيل
node-gyp
باستخدام علم--nodedir
الذي يشير إلى صورة مصدر Node.js المحلية. باستخدام هذا الخيار، سيكون للوظيفة الإضافية حق الوصول إلى المجموعة الكاملة من التبعيات.
تحميل الوظائف الإضافية باستخدام require()
امتداد اسم ملف الوظيفة الإضافية الثنائية المترجمة هو .node
(على عكس .dll
أو .so
). تم كتابة دالة require()
للبحث عن الملفات ذات امتداد .node
وتهيئة هذه الملفات كمكتبات مرتبطة ديناميكيًا.
عند استدعاء require()
، يمكن عادةً حذف امتداد .node
، وسيظل Node.js يجد الوظيفة الإضافية ويقوم بتهيئتها. ومع ذلك، هناك تحذير واحد، وهو أن Node.js سيحاول أولاً تحديد موقع وتحميل الوحدات أو ملفات JavaScript التي تتشارك نفس الاسم الأساسي. على سبيل المثال، إذا كان هناك ملف addon.js
في نفس الدليل مثل الثنائي addon.node
، فستمنح require('addon')
الأفضلية لملف addon.js
وتحميله بدلاً من ذلك.
تجريدات أصلية لـ Node.js
يستخدم كل من الأمثلة الموضحة في هذه الوثيقة واجهات برمجة التطبيقات Node.js و V8 مباشرةً لتنفيذ الوظائف الإضافية. يمكن أن تتغير واجهة برمجة تطبيقات V8، وقد تغيرت بشكل كبير من إصدار V8 إلى آخر (ومن إصدار رئيسي لـ Node.js إلى آخر). مع كل تغيير، قد تحتاج الوظائف الإضافية إلى التحديث وإعادة التجميع من أجل مواصلة العمل. تم تصميم جدول إصدار Node.js لتقليل وتيرة وتأثير هذه التغييرات، ولكن لا يوجد الكثير مما يمكن أن يفعله Node.js لضمان استقرار واجهات برمجة تطبيقات V8.
توفر التجريدات الأصلية لـ Node.js (أو nan
) مجموعة من الأدوات التي يُوصى بمطوري الوظائف الإضافية باستخدامها للحفاظ على التوافق بين الإصدارات السابقة والمستقبلية من V8 و Node.js. راجع أمثلة nan
أمثلة للحصول على توضيح لكيفية استخدامها.
واجهة برمجة تطبيقات العقدة (Node-API)
[مستقر: 2 - مستقر]
مستقر: 2 استقرار: 2 - مستقر
واجهة برمجة تطبيقات العقدة هي واجهة برمجة تطبيقات لبناء الوظائف الإضافية الأصلية. إنها مستقلة عن بيئة تشغيل جافا سكريبت الأساسية (مثل V8) ويتم صيانتها كجزء من Node.js نفسها. ستكون هذه الواجهة مستقرة من حيث واجهة التطبيقات الثنائية (ABI) عبر إصدارات Node.js. والغرض منها هو عزل الوظائف الإضافية عن التغييرات في محرك جافا سكريبت الأساسي والسماح بتشغيل الوحدات المُجمعة لإصدار واحد على إصدارات لاحقة من Node.js بدون إعادة تجميع. يتم بناء/تعبئة الوظائف الإضافية بنفس النهج/الأدوات الموضحة في هذا المستند (node-gyp، إلخ). والفرق الوحيد هو مجموعة واجهات برمجة التطبيقات التي يستخدمها الرمز الأصلي. بدلاً من استخدام واجهات برمجة تطبيقات V8 أو الاستخراجيات الأصلية لـ Node.js، يتم استخدام الدوال المتاحة في واجهة برمجة تطبيقات العقدة.
إنشاء وصيانة وظيفة إضافية تستفيد من استقرار ABI الذي توفره واجهة برمجة تطبيقات العقدة يحمل معه بعض الاعتبارات التنفيذية.
لاستخدام واجهة برمجة تطبيقات العقدة في مثال "أهلاً بالعالم" أعلاه، استبدل محتويات hello.cc
بما يلي. تظل جميع التعليمات الأخرى كما هي.
// hello.cc باستخدام Node-API
#include <node_api.h>
namespace demo {
napi_value Method(napi_env env, napi_callback_info args) {
napi_value greeting;
napi_status status;
status = napi_create_string_utf8(env, "world", NAPI_AUTO_LENGTH, &greeting);
if (status != napi_ok) return nullptr;
return greeting;
}
napi_value init(napi_env env, napi_value exports) {
napi_status status;
napi_value fn;
status = napi_create_function(env, nullptr, 0, Method, nullptr, &fn);
if (status != napi_ok) return nullptr;
status = napi_set_named_property(env, exports, "hello", fn);
if (status != napi_ok) return nullptr;
return exports;
}
NAPI_MODULE(NODE_GYP_MODULE_NAME, init)
} // namespace demo
الوظائف المتاحة وكيفية استخدامها موثقة في الوظائف الإضافية C/C++ مع Node-API.
أمثلة الوظائف الإضافية
فيما يلي بعض الأمثلة على الوظائف الإضافية المصممة لمساعدة المطورين على البدء. تستخدم الأمثلة واجهات برمجة تطبيقات V8. راجع مرجع V8 عبر الإنترنت للحصول على مساعدة حول مختلف دعوات V8، ودليل المُدمِج لـ V8 للحصول على شرح لعدة مفاهيم مستخدمة مثل المقابض، ونطاقات العمل، وقوالب الدوال، وما إلى ذلك.
يستخدم كل من هذه الأمثلة ملف binding.gyp
التالي:
{
"targets": [
{
"target_name": "addon",
"sources": ["addon.cc"]
}
]
}
في الحالات التي يوجد فيها أكثر من ملف .cc
، ما عليك سوى إضافة اسم الملف الإضافي إلى مصفوفة sources
:
"sources": ["addon.cc", "myexample.cc"]
بمجرد أن يصبح ملف binding.gyp
جاهزًا، يمكن تهيئة الوظائف الإضافية وتجميعها باستخدام node-gyp
:
node-gyp configure build
وسيطات الدالة
عادةً ما تعرض الوظائف الإضافية كائنات ووظائف يمكن الوصول إليها من JavaScript الذي يعمل داخل Node.js. عندما يتم استدعاء الدوال من JavaScript، يجب تعيين الوسيطات الإدخال وقيمة الإرجاع من وإلى رمز C/C++.
يُوضح المثال التالي كيفية قراءة وسيطات الدالة المُمرّرة من JavaScript وكيفية إرجاع نتيجة:
// addon.cc
#include <node.h>
namespace demo {
using v8::Exception;
using v8::FunctionCallbackInfo;
using v8::Isolate;
using v8::Local;
using v8::Number;
using v8::Object;
using v8::String;
using v8::Value;
// هذا هو تنفيذ طريقة "add"
// يتم تمرير وسيطات الإدخال باستخدام
// بنية const FunctionCallbackInfo<Value>& args
void Add(const FunctionCallbackInfo<Value>& args) {
Isolate* isolate = args.GetIsolate();
// التحقق من عدد الوسيطات المُمرّرة.
if (args.Length() < 2) {
// رمي خطأ يُمرّر مرة أخرى إلى JavaScript
isolate->ThrowException(Exception::TypeError(
String::NewFromUtf8(isolate,
"عدد الوسيطات خاطئ").ToLocalChecked()));
return;
}
// التحقق من أنواع الوسيطات
if (!args[0]->IsNumber() || !args[1]->IsNumber()) {
isolate->ThrowException(Exception::TypeError(
String::NewFromUtf8(isolate,
"وسيطات خاطئة").ToLocalChecked()));
return;
}
// إجراء العملية
double value =
args[0].As<Number>()->Value() + args[1].As<Number>()->Value();
Local<Number> num = Number::New(isolate, value);
// تعيين قيمة الإرجاع (باستخدام
// FunctionCallbackInfo<Value>& المُمرّرة)
args.GetReturnValue().Set(num);
}
void Init(Local<Object> exports) {
NODE_SET_METHOD(exports, "add", Add);
}
NODE_MODULE(NODE_GYP_MODULE_NAME, Init)
} // namespace demo
بمجرد التجميع، يمكن طلب الوظيفة الإضافية واستخدامها من داخل Node.js:
// test.js
const addon = require('./build/Release/addon')
console.log('يجب أن يكون هذا ثمانية:', addon.add(3, 5))
وظائف الاستدعاء العكسي (Callbacks)
من الشائع في الإضافات تمرير دوال JavaScript إلى دالة C++ وتنفيذها من هناك. يوضح المثال التالي كيفية استدعاء وظائف الاستدعاء العكسي هذه:
// addon.cc
#include <node.h>
namespace demo {
using v8::Context;
using v8::Function;
using v8::FunctionCallbackInfo;
using v8::Isolate;
using v8::Local;
using v8::Null;
using v8::Object;
using v8::String;
using v8::Value;
void RunCallback(const FunctionCallbackInfo<Value>& args) {
Isolate* isolate = args.GetIsolate();
Local<Context> context = isolate->GetCurrentContext();
Local<Function> cb = Local<Function>::Cast(args[0]);
const unsigned argc = 1;
Local<Value> argv[argc] = {
String::NewFromUtf8(isolate,
"hello world").ToLocalChecked() };
cb->Call(context, Null(isolate), argc, argv).ToLocalChecked();
}
void Init(Local<Object> exports, Local<Object> module) {
NODE_SET_METHOD(module, "exports", RunCallback);
}
NODE_MODULE(NODE_GYP_MODULE_NAME, Init)
} // namespace demo
يستخدم هذا المثال نموذج Init()
ذي الوسيطين الذي يستقبل كائن module
الكامل كوسيط ثاني. يسمح هذا للإضافة بتغيير exports
بالكامل بدالة واحدة بدلاً من إضافة الدالة كخاصية لـ exports
.
لاختباره، قم بتشغيل JavaScript التالي:
// test.js
const addon = require('./build/Release/addon')
addon(msg => {
console.log(msg)
// يطبع: 'hello world'
})
في هذا المثال، يتم استدعاء دالة الاستدعاء العكسي بشكل متزامن.
مصنع الكائنات (Object factory)
يمكن للإضافات إنشاء كائنات جديدة وإعادتها من داخل دالة C++ كما هو موضح في المثال التالي. يتم إنشاء كائن وإعادته مع خاصية msg
تعكس السلسلة الممررة إلى createObject()
:
// addon.cc
#include <node.h>
namespace demo {
using v8::Context;
using v8::FunctionCallbackInfo;
using v8::Isolate;
using v8::Local;
using v8::Object;
using v8::String;
using v8::Value;
void CreateObject(const FunctionCallbackInfo<Value>& args) {
Isolate* isolate = args.GetIsolate();
Local<Context> context = isolate->GetCurrentContext();
Local<Object> obj = Object::New(isolate);
obj->Set(context,
String::NewFromUtf8(isolate,
"msg").ToLocalChecked(),
args[0]->ToString(context).ToLocalChecked())
.FromJust();
args.GetReturnValue().Set(obj);
}
void Init(Local<Object> exports, Local<Object> module) {
NODE_SET_METHOD(module, "exports", CreateObject);
}
NODE_MODULE(NODE_GYP_MODULE_NAME, Init)
} // namespace demo
لاختباره في JavaScript:
// test.js
const addon = require('./build/Release/addon')
const obj1 = addon('hello')
const obj2 = addon('world')
console.log(obj1.msg, obj2.msg)
// يطبع: 'hello world'
مُصنّع الدوال
سيناريو شائع آخر هو إنشاء دوال جافا سكريبت تُغلف دوال C++ وإرجاعها إلى جافا سكريبت:
// addon.cc
#include <node.h>
namespace demo {
using v8::Context;
using v8::Function;
using v8::FunctionCallbackInfo;
using v8::FunctionTemplate;
using v8::Isolate;
using v8::Local;
using v8::Object;
using v8::String;
using v8::Value;
void MyFunction(const FunctionCallbackInfo<Value>& args) {
Isolate* isolate = args.GetIsolate();
args.GetReturnValue().Set(String::NewFromUtf8(
isolate, "hello world").ToLocalChecked());
}
void CreateFunction(const FunctionCallbackInfo<Value>& args) {
Isolate* isolate = args.GetIsolate();
Local<Context> context = isolate->GetCurrentContext();
Local<FunctionTemplate> tpl = FunctionTemplate::New(isolate, MyFunction);
Local<Function> fn = tpl->GetFunction(context).ToLocalChecked();
// حذف هذا لجعله مجهولاً
fn->SetName(String::NewFromUtf8(
isolate, "theFunction").ToLocalChecked());
args.GetReturnValue().Set(fn);
}
void Init(Local<Object> exports, Local<Object> module) {
NODE_SET_METHOD(module, "exports", CreateFunction);
}
NODE_MODULE(NODE_GYP_MODULE_NAME, Init)
} // namespace demo
للاختبار:
// test.js
const addon = require('./build/Release/addon')
const fn = addon()
console.log(fn())
// يطبع: 'hello world'
تغليف كائنات C++
من الممكن أيضًا تغليف كائنات/فئات C++ بطريقة تسمح بإنشاء مثيلات جديدة باستخدام عامل التشغيل new
في جافا سكريبت:
// addon.cc
#include <node.h>
#include "myobject.h"
namespace demo {
using v8::Local;
using v8::Object;
void InitAll(Local<Object> exports) {
MyObject::Init(exports);
}
NODE_MODULE(NODE_GYP_MODULE_NAME, InitAll)
} // namespace demo
ثم، في myobject.h
، يرث مُغلف الفئة من node::ObjectWrap
:
// myobject.h
#ifndef MYOBJECT_H
#define MYOBJECT_H
#include <node.h>
#include <node_object_wrap.h>
namespace demo {
class MyObject : public node::ObjectWrap {
public:
static void Init(v8::Local<v8::Object> exports);
private:
explicit MyObject(double value = 0);
~MyObject();
static void New(const v8::FunctionCallbackInfo<v8::Value>& args);
static void PlusOne(const v8::FunctionCallbackInfo<v8::Value>& args);
double value_;
};
} // namespace demo
#endif
في myobject.cc
، قم بتنفيذ الطرق المختلفة التي سيتم عرضها. في التعليمات البرمجية التالية، يتم عرض طريقة plusOne()
عن طريق إضافتها إلى النموذج الأولي للبناء:
// myobject.cc
#include "myobject.h"
namespace demo {
using v8::Context;
using v8::Function;
using v8::FunctionCallbackInfo;
using v8::FunctionTemplate;
using v8::Isolate;
using v8::Local;
using v8::Number;
using v8::Object;
using v8::ObjectTemplate;
using v8::String;
using v8::Value;
MyObject::MyObject(double value) : value_(value) {
}
MyObject::~MyObject() {
}
void MyObject::Init(Local<Object> exports) {
Isolate* isolate = exports->GetIsolate();
Local<Context> context = isolate->GetCurrentContext();
Local<ObjectTemplate> addon_data_tpl = ObjectTemplate::New(isolate);
addon_data_tpl->SetInternalFieldCount(1); // 1 حقل لـ MyObject::New()
Local<Object> addon_data =
addon_data_tpl->NewInstance(context).ToLocalChecked();
// إعداد قالب المُنشئ
Local<FunctionTemplate> tpl = FunctionTemplate::New(isolate, New, addon_data);
tpl->SetClassName(String::NewFromUtf8(isolate, "MyObject").ToLocalChecked());
tpl->InstanceTemplate()->SetInternalFieldCount(1);
// النموذج الأولي
NODE_SET_PROTOTYPE_METHOD(tpl, "plusOne", PlusOne);
Local<Function> constructor = tpl->GetFunction(context).ToLocalChecked();
addon_data->SetInternalField(0, constructor);
exports->Set(context, String::NewFromUtf8(
isolate, "MyObject").ToLocalChecked(),
constructor).FromJust();
}
void MyObject::New(const FunctionCallbackInfo<Value>& args) {
Isolate* isolate = args.GetIsolate();
Local<Context> context = isolate->GetCurrentContext();
if (args.IsConstructCall()) {
// تم استدعاء كمنشئ: `new MyObject(...)`
double value = args[0]->IsUndefined() ?
0 : args[0]->NumberValue(context).FromMaybe(0);
MyObject* obj = new MyObject(value);
obj->Wrap(args.This());
args.GetReturnValue().Set(args.This());
} else {
// تم استدعاء كدالة عادية `MyObject(...)`، تحويلها إلى استدعاء منشئ.
const int argc = 1;
Local<Value> argv[argc] = { args[0] };
Local<Function> cons =
args.Data().As<Object>()->GetInternalField(0)
.As<Value>().As<Function>();
Local<Object> result =
cons->NewInstance(context, argc, argv).ToLocalChecked();
args.GetReturnValue().Set(result);
}
}
void MyObject::PlusOne(const FunctionCallbackInfo<Value>& args) {
Isolate* isolate = args.GetIsolate();
MyObject* obj = ObjectWrap::Unwrap<MyObject>(args.This());
obj->value_ += 1;
args.GetReturnValue().Set(Number::New(isolate, obj->value_));
}
} // namespace demo
لبناء هذا المثال، يجب إضافة ملف myobject.cc
إلى binding.gyp
:
{
"targets": [
{
"target_name": "addon",
"sources": ["addon.cc", "myobject.cc"]
}
]
}
اختبره باستخدام:
// test.js
const addon = require('./build/Release/addon')
const obj = new addon.MyObject(10)
console.log(obj.plusOne())
// يطبع: 11
console.log(obj.plusOne())
// يطبع: 12
console.log(obj.plusOne())
// يطبع: 13
سيتم تشغيل مُدمّر كائن المُغلف عندما يتم جمع القمامة للكائن. لاختبار المُدمّر، هناك علامات سطر أوامر يمكن استخدامها لجعل من الممكن إجبار جمع القمامة. تُوفر هذه العلامات محرك جافا سكريبت V8 الأساسي. وهي عرضة للتغيير أو الإزالة في أي وقت. لم يتم توثيقها بواسطة Node.js أو V8، ويجب عدم استخدامها أبدًا خارج الاختبار.
خلال إيقاف تشغيل العملية أو خيوط العامل، لا يتم استدعاء المُدمّرات بواسطة محرّك JS. لذلك، تقع مسؤولية تتبع هذه الكائنات وضمان التدمير الصحيح على المستخدم لتجنب تسرب الموارد.
مصنع الكائنات الملفوفة
بدلاً من ذلك، من الممكن استخدام نمط المصنع لتجنب إنشاء مثيلات الكائنات صراحةً باستخدام عامل التشغيل new
في جافا سكريبت:
const obj = addon.createObject()
// بدلاً من:
// const obj = new addon.Object();
أولاً، يتم تنفيذ طريقة createObject()
في addon.cc
:
// addon.cc
#include <node.h>
#include "myobject.h"
namespace demo {
using v8::FunctionCallbackInfo;
using v8::Isolate;
using v8::Local;
using v8::Object;
using v8::String;
using v8::Value;
void CreateObject(const FunctionCallbackInfo<Value>& args) {
MyObject::NewInstance(args);
}
void InitAll(Local<Object> exports, Local<Object> module) {
MyObject::Init(exports->GetIsolate());
NODE_SET_METHOD(module, "exports", CreateObject);
}
NODE_MODULE(NODE_GYP_MODULE_NAME, InitAll)
} // namespace demo
في myobject.h
، تمت إضافة الطريقة الثابتة NewInstance()
للتعامل مع إنشاء مثيل الكائن. تأخذ هذه الطريقة مكان استخدام new
في جافا سكريبت:
// myobject.h
#ifndef MYOBJECT_H
#define MYOBJECT_H
#include <node.h>
#include <node_object_wrap.h>
namespace demo {
class MyObject : public node::ObjectWrap {
public:
static void Init(v8::Isolate* isolate);
static void NewInstance(const v8::FunctionCallbackInfo<v8::Value>& args);
private:
explicit MyObject(double value = 0);
~MyObject();
static void New(const v8::FunctionCallbackInfo<v8::Value>& args);
static void PlusOne(const v8::FunctionCallbackInfo<v8::Value>& args);
static v8::Global<v8::Function> constructor;
double value_;
};
} // namespace demo
#endif
التنفيذ في myobject.cc
مشابه للمثال السابق:
// myobject.cc
#include <node.h>
#include "myobject.h"
namespace demo {
using node::AddEnvironmentCleanupHook;
using v8::Context;
using v8::Function;
using v8::FunctionCallbackInfo;
using v8::FunctionTemplate;
using v8::Global;
using v8::Isolate;
using v8::Local;
using v8::Number;
using v8::Object;
using v8::String;
using v8::Value;
// Warning! This is not thread-safe, this addon cannot be used for worker
// threads.
Global<Function> MyObject::constructor;
MyObject::MyObject(double value) : value_(value) {
}
MyObject::~MyObject() {
}
void MyObject::Init(Isolate* isolate) {
// Prepare constructor template
Local<FunctionTemplate> tpl = FunctionTemplate::New(isolate, New);
tpl->SetClassName(String::NewFromUtf8(isolate, "MyObject").ToLocalChecked());
tpl->InstanceTemplate()->SetInternalFieldCount(1);
// Prototype
NODE_SET_PROTOTYPE_METHOD(tpl, "plusOne", PlusOne);
Local<Context> context = isolate->GetCurrentContext();
constructor.Reset(isolate, tpl->GetFunction(context).ToLocalChecked());
AddEnvironmentCleanupHook(isolate, [](void*) {
constructor.Reset();
}, nullptr);
}
void MyObject::New(const FunctionCallbackInfo<Value>& args) {
Isolate* isolate = args.GetIsolate();
Local<Context> context = isolate->GetCurrentContext();
if (args.IsConstructCall()) {
// Invoked as constructor: `new MyObject(...)`
double value = args[0]->IsUndefined() ?
0 : args[0]->NumberValue(context).FromMaybe(0);
MyObject* obj = new MyObject(value);
obj->Wrap(args.This());
args.GetReturnValue().Set(args.This());
} else {
// Invoked as plain function `MyObject(...)`, turn into construct call.
const int argc = 1;
Local<Value> argv[argc] = { args[0] };
Local<Function> cons = Local<Function>::New(isolate, constructor);
Local<Object> instance =
cons->NewInstance(context, argc, argv).ToLocalChecked();
args.GetReturnValue().Set(instance);
}
}
void MyObject::NewInstance(const FunctionCallbackInfo<Value>& args) {
Isolate* isolate = args.GetIsolate();
const unsigned argc = 1;
Local<Value> argv[argc] = { args[0] };
Local<Function> cons = Local<Function>::New(isolate, constructor);
Local<Context> context = isolate->GetCurrentContext();
Local<Object> instance =
cons->NewInstance(context, argc, argv).ToLocalChecked();
args.GetReturnValue().Set(instance);
}
void MyObject::PlusOne(const FunctionCallbackInfo<Value>& args) {
Isolate* isolate = args.GetIsolate();
MyObject* obj = ObjectWrap::Unwrap<MyObject>(args.This());
obj->value_ += 1;
args.GetReturnValue().Set(Number::New(isolate, obj->value_));
}
} // namespace demo
مرة أخرى، لبناء هذا المثال، يجب إضافة ملف myobject.cc
إلى binding.gyp
:
{
"targets": [
{
"target_name": "addon",
"sources": ["addon.cc", "myobject.cc"]
}
]
}
اختبره باستخدام:
// test.js
const createObject = require('./build/Release/addon')
const obj = createObject(10)
console.log(obj.plusOne())
// يطبع: 11
console.log(obj.plusOne())
// يطبع: 12
console.log(obj.plusOne())
// يطبع: 13
const obj2 = createObject(20)
console.log(obj2.plusOne())
// يطبع: 21
console.log(obj2.plusOne())
// يطبع: 22
console.log(obj2.plusOne())
// يطبع: 23
تمرير الكائنات المُغلّفة
بالإضافة إلى تغليف وإرجاع كائنات C++، من الممكن تمرير الكائنات المُغلّفة باستخدام دالة المساعدة node::ObjectWrap::Unwrap
في Node.js. يُظهر المثال التالي دالة add()
التي يمكنها أخذ كائنين من نوع MyObject
كوسيطي إدخال:
// addon.cc
#include <node.h>
#include <node_object_wrap.h>
#include "myobject.h"
namespace demo {
using v8::Context;
using v8::FunctionCallbackInfo;
using v8::Isolate;
using v8::Local;
using v8::Number;
using v8::Object;
using v8::String;
using v8::Value;
void CreateObject(const FunctionCallbackInfo<Value>& args) {
MyObject::NewInstance(args);
}
void Add(const FunctionCallbackInfo<Value>& args) {
Isolate* isolate = args.GetIsolate();
Local<Context> context = isolate->GetCurrentContext();
MyObject* obj1 = node::ObjectWrap::Unwrap<MyObject>(
args[0]->ToObject(context).ToLocalChecked());
MyObject* obj2 = node::ObjectWrap::Unwrap<MyObject>(
args[1]->ToObject(context).ToLocalChecked());
double sum = obj1->value() + obj2->value();
args.GetReturnValue().Set(Number::New(isolate, sum));
}
void InitAll(Local<Object> exports) {
MyObject::Init(exports->GetIsolate());
NODE_SET_METHOD(exports, "createObject", CreateObject);
NODE_SET_METHOD(exports, "add", Add);
}
NODE_MODULE(NODE_GYP_MODULE_NAME, InitAll)
} // namespace demo
في myobject.h
، تمت إضافة طريقة عامة جديدة للسماح بالوصول إلى القيم الخاصة بعد إلغاء تغليف الكائن.
// myobject.h
#ifndef MYOBJECT_H
#define MYOBJECT_H
#include <node.h>
#include <node_object_wrap.h>
namespace demo {
class MyObject : public node::ObjectWrap {
public:
static void Init(v8::Isolate* isolate);
static void NewInstance(const v8::FunctionCallbackInfo<v8::Value>& args);
inline double value() const { return value_; }
private:
explicit MyObject(double value = 0);
~MyObject();
static void New(const v8::FunctionCallbackInfo<v8::Value>& args);
static v8::Global<v8::Function> constructor;
double value_;
};
} // namespace demo
#endif
يبقى تنفيذ myobject.cc
مشابهًا للنسخة السابقة:
// myobject.cc
#include <node.h>
#include "myobject.h"
namespace demo {
using node::AddEnvironmentCleanupHook;
using v8::Context;
using v8::Function;
using v8::FunctionCallbackInfo;
using v8::FunctionTemplate;
using v8::Global;
using v8::Isolate;
using v8::Local;
using v8::Object;
using v8::String;
using v8::Value;
// Warning! This is not thread-safe, this addon cannot be used for worker
// threads.
Global<Function> MyObject::constructor;
MyObject::MyObject(double value) : value_(value) {
}
MyObject::~MyObject() {
}
void MyObject::Init(Isolate* isolate) {
// Prepare constructor template
Local<FunctionTemplate> tpl = FunctionTemplate::New(isolate, New);
tpl->SetClassName(String::NewFromUtf8(isolate, "MyObject").ToLocalChecked());
tpl->InstanceTemplate()->SetInternalFieldCount(1);
Local<Context> context = isolate->GetCurrentContext();
constructor.Reset(isolate, tpl->GetFunction(context).ToLocalChecked());
AddEnvironmentCleanupHook(isolate, [](void*) {
constructor.Reset();
}, nullptr);
}
void MyObject::New(const FunctionCallbackInfo<Value>& args) {
Isolate* isolate = args.GetIsolate();
Local<Context> context = isolate->GetCurrentContext();
if (args.IsConstructCall()) {
// Invoked as constructor: `new MyObject(...)`
double value = args[0]->IsUndefined() ?
0 : args[0]->NumberValue(context).FromMaybe(0);
MyObject* obj = new MyObject(value);
obj->Wrap(args.This());
args.GetReturnValue().Set(args.This());
} else {
// Invoked as plain function `MyObject(...)`, turn into construct call.
const int argc = 1;
Local<Value> argv[argc] = { args[0] };
Local<Function> cons = Local<Function>::New(isolate, constructor);
Local<Object> instance =
cons->NewInstance(context, argc, argv).ToLocalChecked();
args.GetReturnValue().Set(instance);
}
}
void MyObject::NewInstance(const FunctionCallbackInfo<Value>& args) {
Isolate* isolate = args.GetIsolate();
const unsigned argc = 1;
Local<Value> argv[argc] = { args[0] };
Local<Function> cons = Local<Function>::New(isolate, constructor);
Local<Context> context = isolate->GetCurrentContext();
Local<Object> instance =
cons->NewInstance(context, argc, argv).ToLocalChecked();
args.GetReturnValue().Set(instance);
}
} // namespace demo
اختبره باستخدام:
// test.js
const addon = require('./build/Release/addon')
const obj1 = addon.createObject(10)
const obj2 = addon.createObject(20)
const result = addon.add(obj1, obj2)
console.log(result)
// Prints: 30