جارٍ نقل تطبيقات USB إلى الويب. الجزء الثاني: gPhoto2

تعرَّف على كيفية نقل gPhoto2 إلى WebAssembly للتحكُّم في الكاميرات الخارجية باستخدام USB من تطبيق ويب.

أوضحت في المشاركة السابقة كيفية نقل مكتبة libusb لتشغيلها على الويب باستخدام WebAssembly / Emscripten وAsyncify وWebUSB.

لقد عرضتُ أيضًا عرضًا توضيحيًا تم إنشاؤه باستخدام gPhoto2 يمكنه التحكّم في كاميرا DSLR والكاميرات غير المرايا عبر USB من تطبيق ويب. سأشرح في هذه المشاركة المزيد من التفاصيل الفنية وراء منفذ gPhoto2.

توجيه أنظمة بناء إلى الشوكات المخصّصة

بما أنّني كنت أستهدف WebAssembly، لم أتمكّن من استخدام libusb وlibgphoto2 اللذين تتوفّر فيهما توزيعات النظام. بدلاً من ذلك، احتجت إلى استخدام شوكة libgphoto2 المخصّصة في تطبيقي، في حين كان على شوكة libgphoto2 استخدام شوكة libusb المخصّصة.

بالإضافة إلى ذلك، يستخدم libgphoto2 أداة libtool لتحميل المكونات الإضافية الديناميكية، وعلى الرغم من أنني لم أضطر إلى استخدام libtool مثل المكتبتين الأخريين، إلا أنني ما زال عليّ إنشاؤه في WebAssembly، وتوجيه libgphoto2 إلى هذا الإصدار المخصص بدلاً من حزمة النظام.

في ما يلي مخطّط بياني تقريبي للتبعية (تشير الخطوط المتقطعة إلى الربط الديناميكي):

مخطّط بياني يعرض "التطبيق" اعتمادًا على 'libgphoto2 fork'، والذي يعتمد على 'libtool'. "libtool" يعتمد منع البيانات ديناميكيًا على "منافذ libgphoto2" و"libgphoto2 camlibs". وأخيرًا، "منافذ libgphoto2" بشكل ثابت على "شوكة libusb".

تسمح معظم أنظمة الإنشاء القائمة على التهيئة، بما في ذلك تلك المستخدمة في هذه المكتبات، بتجاوز المسارات للتبعيات عبر علامات مختلفة، وهذا ما حاولتُ القيام به أولاً. ومع ذلك، عندما يصبح الرسم البياني للتبعية معقدًا، تصبح قائمة إلغاء المسار لكل تبعيات مكتبة مطوّلة وعرضة للخطأ. وجدت أيضًا بعض الأخطاء التي لم تكن فيها أنظمة التصميم معدّة فعليًا لتبعياتها لتتواجد في مسارات غير قياسية.

بدلاً من ذلك، فإن الطريقة الأسهل هي إنشاء مجلد منفصل كجذر نظام مخصص (غالبًا ما يتم اختصاره إلى "sysroot") وتوجيه جميع أنظمة التصميم المعنيّة إليه. وبهذه الطريقة، ستبحث كل مكتبة عن تبعياتها في جذر النظام المحدد أثناء عملية الإنشاء، وستثبت أيضًا نفسها في نفس sysroot حتى يتمكن الآخرون من العثور عليها بسهولة أكبر.

لدى Emscripten بالفعل نظام sysroot الخاص به ضمن (path to emscripten cache)/sysroot، والذي يستخدمه لمكتبات النظام ومنافذ Emscripten، وأدوات مثل CMake وpkg-config. اخترتُ إعادة استخدام نفس sysroot لتبعياتي أيضًا.

# This is the default path, but you can override it
# to store the cache elsewhere if you want.
#
# For example, it might be useful for Docker builds
# if you want to preserve the deps between reruns.
EM_CACHE = $(EMSCRIPTEN)/cache

# Sysroot is always under the `sysroot` subfolder.
SYSROOT = $(EM_CACHE)/sysroot

# …

# For all dependencies I've used the same ./configure command with the
# earlier defined SYSROOT path as the --prefix.
deps/%/Makefile: deps/%/configure
        cd $(@D) && ./configure --prefix=$(SYSROOT) # …

باستخدام هذه الإعدادات، كان عليّ فقط تشغيل make install في كل اعتمادية، والتي تم تثبيتها من خلال sysroot، وبعد ذلك عثرت المكتبات على بعضها تلقائيًا.

التعامل مع التحميل الديناميكي

كما ذُكر أعلاه، يستخدم libgphoto2 أداة libtool لتعداد محوّلات منفذ الإدخال/الإخراج ومكتبات الكاميرا وتحميلها ديناميكيًا. على سبيل المثال، يبدو رمز تحميل مكتبات وحدات الإدخال والإخراج كما يلي:

lt_dlinit ();
lt_dladdsearchdir (iolibs);
result = lt_dlforeachfile (iolibs, foreach_func, list);
lt_dlexit ();

هناك بعض المشاكل في هذا الأسلوب على الويب:

  • ليس هناك توافق عادي مع الربط الديناميكي لوحدات WebAssembly. يتضمّن تطبيق Emscripten ميزة التنفيذ المخصص التي يمكنها محاكاة واجهة برمجة تطبيقات dlopen() التي تستخدمها أداة libtool، ولكنها تتطلب منك إنشاء "main" و"الجانب" الوحدات ذات العلامات المختلفة، والمخصَّصة لـ dlopen() أيضًا، من أجل التحميل المسبق للوحدات الجانبية في نظام الملفات الذي تتم محاكاته أثناء بدء تشغيل التطبيق. وقد يكون من الصعب دمج هذه العلامات والتعديلات في نظام إنشاء مؤتمر تلقائي حالي مع الكثير من المكتبات الديناميكية.
  • حتى إذا تم تنفيذ dlopen() نفسه، ما مِن طريقة لتعداد جميع المكتبات الديناميكية في مجلد معيّن على الويب، لأنّ معظم خوادم HTTP لا تعرض قوائم الدليل لأسباب تتعلّق بالأمان.
  • يمكن أن يؤدي أيضًا ربط المكتبات الديناميكية في سطر الأوامر بدلاً من التعداد في بيئة التشغيل إلى حدوث مشاكل مثل مشكلة الرموز المكررة ناجمة عن الاختلافات بين تمثيل المكتبات المشتركة في Emscripten وعلى الأنظمة الأساسية الأخرى.

من الممكن تكييف نظام التصميم مع هذه الاختلافات وترميز قائمة المكونات الإضافية الديناميكية في مكان ما أثناء عملية الإنشاء، ولكن هناك طريقة أسهل لحل جميع هذه المشاكل تتمثل في تجنب الربط الديناميكي للبدء.

تبين أن libtool يستخرج العديد من طرق الربط الديناميكي على الأنظمة الأساسية المختلفة، كما يتيح كتابة برامج تحميل مخصصة للآخرين. ويُطلق على إحدى برامج التحميل المضمّنة التي يتوافق معها اسم "Dlpreopening":

"توفّر Libtool دعمًا خاصًا لكائن dlopening libtool وملفات مكتبة libtool، بحيث يمكن حلّ رموزها حتى على المنصات التي لا تحتوي على أي دالّتَي dlopen وdlsym.
...
تحاكي Libtool -dlopen على المنصات الثابتة عن طريق ربط الكائنات في البرنامج في وقت التجميع، وإنشاء هياكل بيانات تمثل جدول رموز البرنامج. ولاستخدام هذه الميزة، عليك تعريف الكائنات التي تريد أن يميّز تطبيقك بها تطبيقك باستخدام العلامتَين -dlopen أو -dlpreopen عند ربط برنامجك (اطّلِع على وضع الربط)."

تتيح هذه الآلية محاكاة التحميل الديناميكي على مستوى libtool بدلاً من Emscripten، مع ربط كل العناصر بشكل ثابت في مكتبة واحدة.

المشكلة الوحيدة التي لا يحلها هذا هو تعداد المكتبات الديناميكية. ولا تزال قائمة تلك المواقع بحاجة إلى ترميز ثابت في مكان ما. لحسن الحظ، فإن مجموعة المكونات الإضافية التي أحتاجها للتطبيق ضئيلة جدًا:

  • من جانب المنافذ، لا أهتم سوى باتصال الكاميرا المستند إلى libusb وليس ببروتوكول PTP/IP أو الوصول التسلسلي أو أوضاع محرك أقراص USB.
  • على الجانب الآخر، هناك العديد من المكوّنات الإضافية الخاصة بالمورّدين والتي قد توفّر بعض الوظائف المتخصصة، إلا أنّ التحكّم في الإعدادات العامة والتقاط الصور يكفي لاستخدام بروتوكول نقل الصور الذي يمثّله بروتوكول ptp2 camlib ويدعمه تقريبًا كل كاميرا في السوق.

إليك ما يبدو عليه مخطط التبعية المحدث مع ربط كل شيء معًا بشكل ثابت:

مخطّط بياني يعرض "التطبيق" اعتمادًا على 'libgphoto2 fork'، والذي يعتمد على 'libtool'. "libtool" يعتمد على "المنافذ: libusb1" و"camlibs: libptp2". "المنافذ: libusb1" على "شوكة libusb".

هذا ما قمتُ بترميزه لإصدارات Emscripten:

LTDL_SET_PRELOADED_SYMBOLS();
lt_dlinit ();
#ifdef __EMSCRIPTEN__
  result = foreach_func("libusb1", list);
#else
  lt_dladdsearchdir (iolibs);
  result = lt_dlforeachfile (iolibs, foreach_func, list);
#endif
lt_dlexit ();

و

LTDL_SET_PRELOADED_SYMBOLS();
lt_dlinit ();
#ifdef __EMSCRIPTEN__
  ret = foreach_func("libptp2", &foreach_data);
#else
  lt_dladdsearchdir (dir);
  ret = lt_dlforeachfile (dir, foreach_func, &foreach_data);
#endif
lt_dlexit ();

في نظام إنشاء مؤتمر تلقائي، كان عليّ الآن إضافة -dlpreopen مع كلا الملفين كعلامات رابط لجميع الملفات التنفيذية (الأمثلة والاختبارات وتطبيقي التجريبي)، مثل هذا:

if HAVE_EMSCRIPTEN
LDADD += -dlpreopen $(top_builddir)/libgphoto2_port/usb1.la \
         -dlpreopen $(top_builddir)/camlibs/ptp2.la
endif

أخيرًا، الآن بعد أن تم ربط جميع الرموز بشكل ثابت في مكتبة واحدة، تحتاج libtool إلى طريقة لتحديد الرمز الذي ينتمي إلى أي مكتبة. لتحقيق ذلك، على المطوّرين إعادة تسمية جميع الرموز الظاهرة، مثل {function name}، إلى {library name}_LTX_{function name}. أسهل طريقة لفعل ذلك هي استخدام #define لإعادة تعريف أسماء الرموز في أعلى ملف التنفيذ:

// …
#include "config.h"

/* Define _LTX_ names - required to prevent clashes when using libtool preloading. */
#define gp_port_library_type libusb1_LTX_gp_port_library_type
#define gp_port_library_list libusb1_LTX_gp_port_library_list
#define gp_port_library_operations libusb1_LTX_gp_port_library_operations

#include <gphoto2/gphoto2-port-library.h>
// …

يمنع نظام التسمية هذا أيضًا تعارض الأسماء في حال قررت ربط المكونات الإضافية الخاصة بالكاميرا في نفس التطبيق في المستقبل.

بعد تنفيذ جميع هذه التغييرات، يمكنني إنشاء التطبيق التجريبي وتحميل المكونات الإضافية بنجاح.

إنشاء واجهة مستخدم الإعدادات

يسمح gPhoto2 لمكتبات الكاميرا بتحديد إعداداتها على شكل شجرة تطبيقات مصغّرة. يتألف التسلسل الهرمي لأنواع الأدوات مما يلي:

  • النافذة - حاوية إعدادات المستوى الأعلى
    • الأقسام - مجموعات مُسماة من الأدوات الأخرى
    • حقول الأزرار
    • حقول النص
    • الحقول الرقمية
    • حقول التاريخ
    • التبديلات
    • أزرار الاختيار

يمكن طلب البحث عن الاسم والنوع والعناصر الثانوية وجميع السمات الأخرى ذات الصلة بكل تطبيق مصغّر (ويتم تعديلها أيضًا في حال القيم) من خلال واجهة برمجة تطبيقات C مكشوفة. وتوفران معًا أساسًا لإنشاء واجهة المستخدم للإعدادات تلقائيًا بأي لغة يمكنها التفاعل مع لغة C.

يمكن تغيير الإعدادات من خلال gPhoto2 أو باستخدام الكاميرا نفسها في أي وقت. بالإضافة إلى ذلك، يمكن أن تكون بعض الأدوات بالقراءة فقط، وحتى حالة القراءة فقط تعتمد على وضع الكاميرا وإعدادات أخرى. على سبيل المثال، سرعة الإغلاق هي حقل رقمي قابل للكتابة في M (الوضع اليدوي)، ولكنه يصبح حقلاً للمعلومات للقراءة فقط في P (وضع البرنامج). في الوضع P، ستكون قيمة سرعة الغالق ديناميكية أيضًا وتتغير باستمرار بناءً على سطوع المشهد الذي تطّلع عليه الكاميرا.

وبوجه عام، من المهم عرض معلومات محدَّثة دائمًا من الكاميرا المتصلة في واجهة المستخدم، مع السماح للمستخدم في الوقت نفسه بتعديل هذه الإعدادات من واجهة المستخدم نفسها. يعد تدفق البيانات ثنائي الاتجاه هذا أكثر تعقيدًا.

لا تتوفر في gPhoto2 آلية لاسترداد الإعدادات التي تم تغييرها فقط، بل العرض التدرّجي بالكامل أو تطبيقات مصغّرة فردية. من أجل الحفاظ على تحديث واجهة المستخدم بدون وميض وفقدان تركيز الإدخال أو موضع التمرير، كنت بحاجة إلى طريقة لاختلاف أشجار الأدوات بين الاستدعاءات وتحديث خصائص واجهة المستخدم التي تم تغييرها فقط. لحسن الحظ، تم حلّ هذه المشكلة على الويب وهي الوظيفة الأساسية لأُطر عمل، مثل React أو Preact. لقد تعاونت مع Preact في هذا المشروع، حيث يوفر نموذجًا خفيفًا بدرجة أكبر ويتيح تنفيذ كل ما أحتاج إليه.

أمّا من ناحية C++ ، فأصبحت بحاجة الآن إلى استرداد شجرة الإعدادات والانتقال بشكل متكرّر عبر واجهة C API التي سبق ربطها، وتحويل كلّ تطبيق مصغّر إلى كائن JavaScript:

static std::pair<val, val> walk_config(CameraWidget *widget) {
  val result = val::object();

  val name(GPP_CALL(const char *, gp_widget_get_name(widget, _)));
  result.set("name", name);
  result.set("info", /* … */);
  result.set("label", /* … */);
  result.set("readonly", /* … */);

  auto type = GPP_CALL(CameraWidgetType, gp_widget_get_type(widget, _));

  switch (type) {
    case GP_WIDGET_RANGE: {
      result.set("type", "range");
      result.set("value", GPP_CALL(float, gp_widget_get_value(widget, _)));

      float min, max, step;
      gpp_try(gp_widget_get_range(widget, &min, &max, &step));
      result.set("min", min);
      result.set("max", max);
      result.set("step", step);

      break;
    }
    case GP_WIDGET_TEXT: {
      result.set("type", "text");
      result.set("value",
                  GPP_CALL(const char *, gp_widget_get_value(widget, _)));

      break;
    }
    // …

من ناحية JavaScript، يمكنني الآن استدعاء configToJS، والاطّلاع على تمثيل JavaScript المعروض لشجرة الإعدادات، وإنشاء واجهة المستخدم من خلال دالة Preact h:

let inputElem;
switch (config.type) {
  case 'range': {
    let { min, max, step } = config;
    inputElem = h(EditableInput, {
      type: 'number',
      min,
      max,
      step,
      attrs
    });
    break;
  }
  case 'text':
    inputElem = h(EditableInput, attrs);
    break;
  case 'toggle': {
    inputElem = h('input', {
      type: 'checkbox',
      attrs
    });
    break;
  }
  // …

من خلال تشغيل هذه الدالة بشكل متكرر في تكرار أحداث لا نهائي، يمكنني جعل واجهة مستخدم الإعدادات تعرض دائمًا أحدث المعلومات، مع إرسال الأوامر أيضًا إلى الكاميرا عندما يعدِّل المستخدم أحد الحقول.

يمكن أن يعتني Preact باختلاف النتائج وتعديل نموذج العناصر في المستند (DOM) فقط للأجزاء التي تم تغييرها في واجهة المستخدم، بدون التأثير في تركيز الصفحة أو حالات التعديل. هناك مشكلة واحدة متبقية هي تدفق البيانات ثنائي الاتجاه. تم تصميم أطر عمل مثل React وPreact حول تدفق البيانات الأحادي الاتجاه، لأنه يسهّل كثيرًا استنتاج البيانات ومقارنتها بين عمليات إعادة التشغيل، لكنني أتجاوز هذا التوقعات من خلال السماح لمصدر خارجي - الكاميرا - بتحديث واجهة مستخدم الإعدادات في أي وقت.

لقد حللتُ هذه المشكلة من خلال إيقاف تحديثات واجهة المستخدم لأي حقول إدخال يعدّلها المستخدم حاليًا:

/**
 * Wrapper around <input /> that doesn't update it while it's in focus to allow editing.
 */
class EditableInput extends Component {
  ref = createRef();

  shouldComponentUpdate() {
    return this.props.readonly || document.activeElement !== this.ref.current;
  }

  render(props) {
    return h('input', Object.assign(props, {ref: this.ref}));
  }
}

بهذه الطريقة، يكون هناك دائمًا مالك واحد فقط لأي حقل معين. إمّا أن المستخدم يعدّله حاليًا، ولن تتأثر بالقيم المعدَّلة من الكاميرا، أو أنّ الكاميرا تعدّل قيمة الحقل عندما يكون خارج التركيز.

إنشاء "فيديو" مباشر الخلاصة

خلال الجائحة، انتقل الكثير من المشاركين إلى الاجتماعات على الإنترنت. أدى ذلك، إلى جانب عوامل أخرى، إلى حدوث نقص في سوق كاميرا الويب. للحصول على جودة فيديو أفضل مقارنةً بالكاميرات المدمجة في أجهزة الكمبيوتر المحمولة، وفي استجابة للنقص المذكور، بدأ العديد من مالكي الكاميرات ذات العدسة الأحادية العاكسة (DSLR) ومالكي الكاميرات التي لا تشتمل على مرايا في البحث عن طرق لاستخدام كاميرات التصوير ككاميرات ويب. حتى أنّ العديد من مورّدي الكاميرات قد شحنوا مرافق رسمية لهذا الغرض.

تمامًا كالأدوات الرسمية، يتيح gPhoto2 بث الفيديو من الكاميرا إلى ملف مخزَّن محليًا أو مباشرةً إلى كاميرا ويب افتراضية أيضًا. أردت استخدام هذه الميزة لتقديم عرض مباشر في العرض التوضيحي. ومع ذلك، أثناء توفرها في الأداة المساعدة لوحدة التحكم، لم أتمكن من العثور عليها في أي مكان في واجهات برمجة تطبيقات مكتبة libgphoto2.

بالنظر إلى رمز المصدر للدالة المقابلة في الأداة المساعدة لوحدة التحكم، وجدتُ أنّه لا يتلقّى فيديو على الإطلاق، ولكنه يستمر في استرداد معاينة الكاميرا كصور JPEG فردية في تكرار لا نهائي، وكتابتها واحدة تلو الأخرى لتكوين بث M-JPEG:

while (1) {
  const char *mime;
  r = gp_camera_capture_preview (p->camera, file, p->context);
  // …

مندهشت أنّ هذه الطريقة فعّالة بما يكفي للحصول على تجربة سلسة عند مشاهدة فيديوهات في الوقت الفعلي. وحتى أنني لم تكن متأكدًا من قدرتي على مطابقة الأداء نفسه في تطبيق الويب أيضًا، مع كل التجريدات الإضافية وAsyncify في الطريق. ومع ذلك، قررت التجربة على أي حال.

من ناحية C++ ، اطّلعت على طريقة تُسمى capturePreviewAsBlob() تستدعي دالة gp_camera_capture_preview() نفسها، وتحوِّل الملف المحفوظ في الذاكرة إلى Blob يمكن تمريره إلى واجهات برمجة تطبيقات الويب الأخرى بسهولة أكبر:

val capturePreviewAsBlob() {
  return gpp_rethrow([=]() {
    auto &file = get_file();

    gpp_try(gp_camera_capture_preview(camera.get(), &file, context.get()));

    auto params = blob_chunks_and_opts(file);
    return Blob.new_(std::move(params.first), std::move(params.second));
  });
}

في جانب JavaScript، تتوفّر حلقة تكرارية مماثلة لتلك المتوفّرة في gPhoto2، وهي تسترجع صور المعاينة بتنسيق Blob وتفك ترميزها في الخلفية باستخدام createImageBitmap، ثم تنقلها إلى اللوحة في إطار الصورة المتحركة التالي:

while (this.canvasRef.current) {
  try {
    let blob = await this.props.getPreview();

    let img = await createImageBitmap(blob, { /* … */ });
    await new Promise(resolve => requestAnimationFrame(resolve));
    canvasCtx.transferFromImageBitmap(img);
  } catch (err) {
    // …
  }
}

ويضمن استخدام واجهات برمجة التطبيقات الحديثة هذه تنفيذ جميع أعمال فك الترميز في الخلفية، ولا يتم تحديث اللوحة إلا عندما تكون كل من الصورة والمتصفّح مستعدَين بالكامل للرسم. وتمكّنت هذه الفرقة من تحقيق معدّل ثابت يزيد عن 30 لقطة في الثانية على الكمبيوتر المحمول، ما طابق الأداء الأصلي لكل من gPhoto2 وبرنامج Sony الرسمي.

مزامنة الوصول إلى USB

عند طلب نقل البيانات عبر USB أثناء وجود عملية أخرى قيد التقدم، سيؤدي ذلك عادةً إلى "الجهاز مشغول" خطأ. بما أنّ المعاينة وواجهة مستخدم الإعدادات يتم تحديثها بانتظام، وقد يحاول المستخدم التقاط صورة أو تعديل الإعدادات في الوقت نفسه، اتضح أنّ هذا التعارض بين العمليات المختلفة متكرر جدًا.

ولتجنّبها، كان عليّ مزامنة جميع أذونات الوصول ضمن التطبيق. لذلك، أنشأتُ قائمة انتظار غير متزامنة مستندة إلى وعود:

let context = await new Module.Context();

let queue = Promise.resolve();

function schedule(op) {
  let res = queue.then(() => op(context));
  queue = res.catch(rethrowIfCritical);
  return res;
}

من خلال ربط كل عملية في سلسلة استدعاء then() بوعد queue الحالي، وتخزين النتيجة المتسلسلة كقيمة جديدة لـ queue، يمكنني التأكّد من تنفيذ جميع العمليات الواحد تلو الآخر، بالترتيب وبدون تداخل.

يتم إرجاع أي أخطاء في التشغيل إلى المتصل، بينما تشير الأخطاء الفادحة (غير المتوقّعة) إلى السلسلة بأكملها على أنّها وعد مرفوضة، وتضمن عدم جدولة أي عملية جديدة بعد ذلك.

من خلال إبقاء سياق الوحدة في متغيّر خاص (غير مصدَّر)، أنا أحدّ من مخاطر الوصول إلى context عن طريق الخطأ في مكان آخر في التطبيق بدون إجراء استدعاء schedule().

لربط العناصر معًا، يجب الآن إغلاق كل إذن وصول إلى سياق الجهاز في استدعاء schedule() على النحو التالي:

let config = await this.connection.schedule((context) => context.configToJS());

و

this.connection.schedule((context) => context.captureImageAsFile());

وبعد ذلك، تم تنفيذ جميع العمليات بنجاح بدون أي تعارضات.

الخاتمة

لا تتردد في تصفُّح قاعدة الترميز على GitHub للحصول على مزيد من إحصاءات التنفيذ. أريد أيضًا أن أشكر ماركوس مايسنر على صيانة gPhoto2 وعلى مراجعاته لمسؤولي العلاقات العامة الرئيسيين.

كما هو موضّح في هذه المنشورات، توفِّر واجهات برمجة التطبيقات WebAssembly وAsyncify وFugu API هدف التجميع الفعّال للتطبيقات الأكثر تعقيدًا. فهي تتيح لك أخذ مكتبة أو تطبيق تم إنشاؤه سابقًا لنظام أساسي واحد، ونقله إلى الويب، مما يجعله متاحًا لعدد أكبر بكثير من المستخدمين عبر أجهزة الكمبيوتر المكتبية والأجهزة المحمولة على حد سواء.