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

تعرَّف على كيفية نقل الرموز التي تتفاعل مع الأجهزة الخارجية إلى الويب باستخدام WebAssembly وFugu APIs.

في مشاركة سابقة، أوضحتُ كيفية نقل التطبيقات باستخدام واجهات برمجة التطبيقات لنظام الملفات إلى الويب باستخدام File System Access API وWebAssembly وAsyncify. وأريد الآن مواصلة الموضوع نفسه المتمثل في دمج واجهات برمجة تطبيقات Fuuu API مع WebAssembly ونقل التطبيقات إلى الويب بدون فقدان الميزات المهمة.

سأوضح كيف يمكن نقل التطبيقات التي تتصل بأجهزة USB إلى الويب من خلال نقل libusb، وهي مكتبة USB شائعة مكتوبة بلغة C، إلى WebAssembly (من خلال Emscripten) وAsyncify وWebUSB.

الأهم قبل العرض: العرض التوضيحي

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

وكانت الفكرة التي اخترتها هي جهاز التحكّم عن بُعد في كاميرا رقمية ذات عدسة أحادية عاكسة (DSLR). وعلى وجه الخصوص، يوجد مشروع مفتوح المصدر gPhoto2 في هذا المجال لفترة طويلة بما يكفي لإجراء هندسة عكسية وتنفيذ دعم لمجموعة كبيرة من الكاميرات الرقمية. وهو يتوافق مع العديد من البروتوكولات، ولكن أكثرها اهتمامي كان دعم USB، وهو يعمل عن طريق libusb.

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

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

العرض التوضيحي على كمبيوتر محمول متصل بكاميرا Sony.

ملاحظة حول الميزات الخاصة بالكاميرا

ربما لاحظت أن تغيير الإعدادات يستغرق بعض الوقت في الفيديو. كما هو الحال مع معظم المشاكل الأخرى التي قد تظهر، لا يعود السبب في ذلك إلى أداء WebAssembly أو WebUSB، بل بسبب كيفية تفاعل gPhotos2 مع الكاميرا المحدّدة التي تم اختيارها للعرض التوضيحي.

لا يعرض Sony a6600 واجهة برمجة تطبيقات لضبط قيم مثل ISO أو فتحة العدسة أو سرعة الغالق مباشرةً، بل يوفّر بدلاً من ذلك أوامر لزيادتها أو خفضها بعدد محدّد من الخطوات. ولجعل الأمور أكثر تعقيدًا، فإنها لا تعرض قائمة بالقيم المعتمدة بالفعل أيضًا - تبدو القائمة التي تم إرجاعها مشفرة بشكل ثابت عبر العديد من طُرز كاميرات Sony.

عند تعيين إحدى هذه القيم، لا يكون لدى gPhotos2 خيار آخر سوى:

  1. اتّبِع خطوة (أو بضع خطوات) في اتجاه القيمة المحدّدة.
  2. يُرجى الانتظار قليلاً إلى أن تعدّل الكاميرا الإعدادات.
  3. يُرجى إعادة قراءة القيمة التي وصلت إليها الكاميرا.
  4. تأكَّد من أنّ الخطوة الأخيرة لم تتخطّى القيمة المطلوبة ولم يتم استيعابها في نهاية القائمة أو بدايتها.
  5. والتكرار.

قد تستغرق هذه العملية بعض الوقت، ولكن إذا كانت الكاميرا متوافقة مع القيمة، سيتم احتسابها، وفي حال عدم توفّرها، ستتوقف عند القيمة الأقرب المسموح بها.

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

ملاحظات مهمة بشأن التوافق مع عدّة منصات

ولكن للأسف، يتم تعيين برنامج تشغيل نظام غير متوافق مع WebUSB لأي أجهزة "مشهورة"، بما في ذلك الكاميرات ذات العدسة الأحادية العاكسة (DSLR) على نظام التشغيل Windows. إذا كنت ترغب في تجربة العرض التوضيحي على نظام التشغيل Windows، يجب استخدام أداة مثل Zadig لاستبدال برنامج التشغيل لكاميرا DSLR المتصلة إما بـ WinUSB أو libusb. يعمل هذا الأسلوب بشكل جيد بالنسبة لي وإلى العديد من المستخدمين الآخرين، ولكن يجب أن أستخدمه على مسؤوليتك الخاصة.

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

من المفترض أن يعمل العرض التوضيحي بشكل تلقائي على نظامَي التشغيل macOS وAndroid. إذا كنت تجرِّب استخدام هذا الوضع على هاتف Android، فتأكد من التبديل إلى الوضع الأفقي لأنني لم أبذل جهدًا كبيرًا لجعله سريع الاستجابة (نرحب بـ "PR" (PR)

تم توصيل هاتف Android بكاميرا Canon باستخدام كابل USB-C.
الإصدار التجريبي نفسه الذي يتم تشغيله على هاتف Android. تصوير سورما

للحصول على دليل أكثر تفصيلاً حول استخدام WebUSB على العديد من الأنظمة الأساسية، يمكنك الاطّلاع على قسم"الاعتبارات الخاصة بالنظام الأساسي" ضمن "تصميم جهاز لـ WebUSB".

إضافة خلفية جديدة إلى libusb

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

لحسن الحظ، يقول موقع libusb README:

"يتم تجريد libusb داخليًا بطريقة نأمل أن يتم نقلها إلى أنظمة تشغيل أخرى. يُرجى الاطّلاع على الملف PORTING لمزيد من المعلومات".

يتم تنظيم libusb بطريقة تكون فيها واجهة برمجة التطبيقات العامة منفصلة عن "الخلفيات" (backends). وتكون هذه الخلفيات مسؤولة عن إدراج الأجهزة وفتحها وإغلاقها والاتصال بها فعليًا عبر واجهات برمجة التطبيقات المنخفضة المستوى في نظام التشغيل. وهذه هي الطريقة التي يعمل بها libusb على التخلّص من الاختلافات بين أنظمة التشغيل Linux وmacOS وWindows وAndroid وOpenBSD/NetBSD وHaiku وSolaris، ويعمل على جميع هذه الأنظمة الأساسية.

ما كان عليَّ فعله هو إضافة خلفية أخرى لـ "نظام التشغيل"Emscripten+WebUSB. تتوفّر عمليات تنفيذ هذه الخلفيات في المجلد libusb/os:

~/w/d/libusb $ ls libusb/os
darwin_usb.c           haiku_usb_raw.h  threads_posix.lo
darwin_usb.h           linux_netlink.c  threads_posix.o
events_posix.c         linux_udev.c     threads_windows.c
events_posix.h         linux_usbfs.c    threads_windows.h
events_posix.lo        linux_usbfs.h    windows_common.c
events_posix.o         netbsd_usb.c     windows_common.h
events_windows.c       null_usb.c       windows_usbdk.c
events_windows.h       openbsd_usb.c    windows_usbdk.h
haiku_pollfs.cpp       sunos_usb.c      windows_winusb.c
haiku_usb_backend.cpp  sunos_usb.h      windows_winusb.h
haiku_usb.h            threads_posix.c
haiku_usb_raw.cpp      threads_posix.h

تشتمل كل خلفية على رأس libusbi.h مع الأنواع الشائعة والمساعدة، ويجب عرض متغير usbi_backend من النوع usbi_os_backend. على سبيل المثال، إليك ما تبدو عليه الخلفية الخلفية لنظام التشغيل Windows:

const struct usbi_os_backend usbi_backend = {
  "Windows",
  USBI_CAP_HAS_HID_ACCESS,
  windows_init,
  windows_exit,
  windows_set_option,
  windows_get_device_list,
  NULL,   /* hotplug_poll */
  NULL,   /* wrap_sys_device */
  windows_open,
  windows_close,
  windows_get_active_config_descriptor,
  windows_get_config_descriptor,
  windows_get_config_descriptor_by_value,
  windows_get_configuration,
  windows_set_configuration,
  windows_claim_interface,
  windows_release_interface,
  windows_set_interface_altsetting,
  windows_clear_halt,
  windows_reset_device,
  NULL,   /* alloc_streams */
  NULL,   /* free_streams */
  NULL,   /* dev_mem_alloc */
  NULL,   /* dev_mem_free */
  NULL,   /* kernel_driver_active */
  NULL,   /* detach_kernel_driver */
  NULL,   /* attach_kernel_driver */
  windows_destroy_device,
  windows_submit_transfer,
  windows_cancel_transfer,
  NULL,   /* clear_transfer_priv */
  NULL,   /* handle_events */
  windows_handle_transfer_completion,
  sizeof(struct windows_context_priv),
  sizeof(union windows_device_priv),
  sizeof(struct windows_device_handle_priv),
  sizeof(struct windows_transfer_priv),
};

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

تكون حقول البيانات الخاصة مفيدة على الأقل لتخزين مؤشرات نظام التشغيل لكل هذه الأشياء، فبدون الأسماء المعرِّفة، لا نعرف العنصر الذي تنطبق عليه أي عملية معينة. في تنفيذ الويب، ستكون مؤشرات نظام التشغيل هي كائنات JavaScript JavaScript الأساسية. الطريقة الطبيعية لتمثيلها وتخزينها في Emscripten هي من خلال الفئة emscripten::val، التي يتم توفيرها كجزء من Embind (نظام ربط Emscripten).

يتم تنفيذ معظم الخلفيات في المجلد بلغة C، ولكن يتم تنفيذ عدد قليل منها باستخدام C++. لا يعمل Embind إلا مع C++ ، لذلك تم الاختيار لي وأضفتُ libusb/libusb/os/emscripten_webusb.cpp بالبنية المطلوبة وباستخدام sizeof(val) لحقول البيانات الخاصة:

#include <emscripten.h>
#include <emscripten/val.h>

#include "libusbi.h"

using namespace emscripten;

// …function implementations

const usbi_os_backend usbi_backend = {
  .name = "Emscripten + WebUSB backend",
  .caps = LIBUSB_CAP_HAS_CAPABILITY,
  // …handlers—function pointers to implementations above
  .device_priv_size = sizeof(val),
  .transfer_priv_size = sizeof(val),
};

تخزين عناصر WebUSB كمقبضات للأجهزة

يوفر libusb مؤشرات جاهزة للاستخدام للمنطقة المخصصة للبيانات الخاصة. للتعامل مع هذه المؤشرات كمثيلات val، أضفت أدوات مساعدة صغيرة تعمل على إنشائها في مكانها واستردادها كمَراجع ونقل القيم:

// We store an Embind handle to WebUSB USBDevice in "priv" metadata of
// libusb device, this helper returns a pointer to it.
struct ValPtr {
 public:
  void init_to(val &&value) { new (ptr) val(std::move(value)); }

  val &get() { return *ptr; }
  val take() { return std::move(get()); }

 protected:
  ValPtr(val *ptr) : ptr(ptr) {}

 private:
  val *ptr;
};

struct WebUsbDevicePtr : ValPtr {
 public:
  WebUsbDevicePtr(libusb_device *dev)
      : ValPtr(static_cast<val *>(usbi_get_device_priv(dev))) {}
};

val &get_web_usb_device(libusb_device *dev) {
  return WebUsbDevicePtr(dev).get();
}

struct WebUsbTransferPtr : ValPtr {
 public:
  WebUsbTransferPtr(usbi_transfer *itransfer)
      : ValPtr(static_cast<val *>(usbi_get_transfer_priv(itransfer))) {}
};

واجهات برمجة تطبيقات الويب غير المتزامنة في سياقات C المتزامنة

هناك حاجة الآن إلى طريقة للتعامل مع واجهات برمجة تطبيقات WebUSB غير المتزامنة، حيث يتوقع libusb عمليات متزامنة. لهذا السبب، يمكنني استخدام ميزة Asyncify، أو بشكل أكثر تحديدًا، تكامل Embind من خلال val::await().

أردتُ أيضًا معالجة أخطاء WebUSB بشكل صحيح وتحويلها إلى رموز أخطاء libusb، إلا أنّ Embind لا يتضمن حاليًا أي طريقة للتعامل مع استثناءات JavaScript أو حالات رفض Promise من جانب C++. يمكن التغلب على هذه المشكلة من خلال رصد الرفض من جانب JavaScript وتحويل النتيجة إلى كائن { error, value } يمكن تحليله الآن بأمان من جانب C++. لقد أجريت ذلك باستخدام مجموعة من وحدات الماكرو في EM_JS وواجهات برمجة التطبيقات Emval.to{Handle, Value}:

EM_JS(EM_VAL, em_promise_catch_impl, (EM_VAL handle), {
  let promise = Emval.toValue(handle);
  promise = promise.then(
    value => ({error : 0, value}),
    error => {
      const ERROR_CODES = {
        // LIBUSB_ERROR_IO
        NetworkError : -1,
        // LIBUSB_ERROR_INVALID_PARAM
        DataError : -2,
        TypeMismatchError : -2,
        IndexSizeError : -2,
        // LIBUSB_ERROR_ACCESS
        SecurityError : -3,
        …
      };
      console.error(error);
      let errorCode = -99; // LIBUSB_ERROR_OTHER
      if (error instanceof DOMException)
      {
        errorCode = ERROR_CODES[error.name] ?? errorCode;
      }
      else if (error instanceof RangeError || error instanceof TypeError)
      {
        errorCode = -2; // LIBUSB_ERROR_INVALID_PARAM
      }
      return {error: errorCode, value: undefined};
    }
  );
  return Emval.toHandle(promise);
});

val em_promise_catch(val &&promise) {
  EM_VAL handle = promise.as_handle();
  handle = em_promise_catch_impl(handle);
  return val::take_ownership(handle);
}

// C++ struct representation for {value, error} object from above
// (performs conversion in the constructor).
struct promise_result {
  libusb_error error;
  val value;

  promise_result(val &&result)
      : error(static_cast<libusb_error>(result["error"].as<int>())),
        value(result["value"]) {}

  // C++ counterpart of the promise helper above that takes a promise, catches
  // its error, converts to a libusb status and returns the whole thing as
  // `promise_result` struct for easier handling.
  static promise_result await(val &&promise) {
    promise = em_promise_catch(std::move(promise));
    return {promise.await()};
  }
};

يمكنني الآن استخدام promise_result::await() على أي Promise يتم عرضها من عمليات WebUSB وفحص حقلَي error وvalue بشكل منفصل.

على سبيل المثال، في حال استرداد val يمثّل USBDevice من libusb_device_handle، يتم استدعاء طريقة open() وانتظار النتيجة وعرض رمز خطأ على شكل رمز حالة libusb على النحو التالي:

int em_open(libusb_device_handle *handle) {
  auto web_usb_device = get_web_usb_device(handle->dev);
  return promise_result::await(web_usb_device.call<val>("open")).error;
}

تعداد الأجهزة

قبل أن أتمكّن من فتح أيّ جهاز، يجب أن يسترد libusb قائمة الأجهزة المتاحة. يجب أن تنفّذ الخلفية هذه العملية باستخدام معالج get_device_list.

تكمن الصعوبة في أنه لا توجد طريقة لتعداد جميع أجهزة USB المتصلة على الويب لأسباب أمنية، على عكس الأنظمة الأساسية الأخرى. وبدلاً من ذلك، ينقسم التدفق إلى جزأين. أولاً، يطلب تطبيق الويب الأجهزة التي تتضمّن خصائص محدَّدة من خلال navigator.usb.requestDevice()، ويختار المستخدم يدويًا الجهاز الذي يريد عرضه أو يرفض طلب الإذن. بعد ذلك، يعرض التطبيق الأجهزة التي سبق أن تمت الموافقة عليها والمتصلة بها من خلال navigator.usb.getDevices().

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

وبدلاً من ذلك، كان عليّ ترك استدعاء navigator.usb.requestDevice() للمطوِّر النهائي وعرض الأجهزة التي تمت الموافقة عليها من navigator.usb.getDevices() فقط:

// Store the global `navigator.usb` once upon initialisation.
thread_local const val web_usb = val::global("navigator")["usb"];

int em_get_device_list(libusb_context *ctx, discovered_devs **devs) {
  // C++ equivalent of `await navigator.usb.getDevices()`.
  // Note: at this point we must already have some devices exposed -
  // caller must have called `await navigator.usb.requestDevice(...)`
  // in response to user interaction before going to LibUSB.
  // Otherwise this list will be empty.
  auto result = promise_result::await(web_usb.call<val>("getDevices"));
  if (result.error) {
    return result.error;
  }
  auto &web_usb_devices = result.value;
  // Iterate over the exposed devices.
  uint8_t devices_num = web_usb_devices["length"].as<uint8_t>();
  for (uint8_t i = 0; i < devices_num; i++) {
    auto web_usb_device = web_usb_devices[i];
    // …
    *devs = discovered_devs_append(*devs, dev);
  }
  return LIBUSB_SUCCESS;
}

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

نقل حلقات الأحداث إلى الويب

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

هناك بعض المشاكل المتعلّقة بهذا النهج على الويب.

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

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

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

لم أكُن أريد إجراء تغييرات مهمة على libusb أو gPhotos2، وقد سبق أن استخدمت Asyncify لدمج Promise، ولذلك هذا هو المسار الذي اخترته. لمحاكاة صيغة حظر من poll()، استخدمت حلقة التكرار كما هو موضَّح أدناه لإثبات المفهوم:

#ifdef __EMSCRIPTEN__
  // TODO: optimize this. Right now it will keep unwinding-rewinding the stack
  // on each short sleep until an event comes or the timeout expires.
  // We should probably create an actual separate thread that does signaling
  // or come up with a custom event mechanism to report events from
  // `usbi_signal_event` and process them here.
  double until_time = emscripten_get_now() + timeout_ms;
  do {
    // Emscripten `poll` ignores timeout param, but pass 0 explicitly just
    // in case.
    num_ready = poll(fds, nfds, 0);
    if (num_ready != 0) break;
    // Yield to the browser event loop to handle events.
    emscripten_sleep(0);
  } while (emscripten_get_now() < until_time);
#else
  num_ready = poll(fds, nfds, timeout_ms);
#endif

وظيفتها:

  1. يتم الاتصال بـ poll() للتأكّد مما إذا كانت الخلفية قد تم الإبلاغ عن أي أحداث. وإذا كان هناك البعض، يتوقف التكرار الحلقي. في حال عدم تنفيذ Emscripten، سيبدأ تنفيذ poll() على الفور مع 0.
  2. الاتصال بالرقم emscripten_sleep(0). تستخدم هذه الدالة Asyncify وsetTimeout() بشكل غير داخلي، وتُستخدَم هنا لإتاحة التحكُّم في حلقة الأحداث الرئيسية للمتصفِّح. ويسمح هذا للمتصفّح بالتعامل مع أي تفاعلات للمستخدم وأحداث الإدخال والإخراج، بما في ذلك WebUSB.
  3. تحقَّق مما إذا كانت المهلة المحدّدة قد انتهت صلاحيتها بعد، وإذا لم تكن منتهية الصلاحية، تابِع حلقة التكرار.

لم يكن هذا الأسلوب صحيحًا لأنّه واصل حفظ حزمة المكالمات بالكامل باستخدام Asyncify حتى في حال عدم توفُّر أحداث USB حتى الآن (في معظم الأوقات)، ولأنّ مدة setTimeout() نفسها كانت 4 ملي ثانية في المتصفِّحات الحديثة. ومع ذلك، نجحت القناة في تصوير بث مباشر يتراوح بين 13 و14 لقطة في الثانية باستخدام كاميرا DSLR لإثبات صحة المعلومات.

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

EM_JS(void, em_libusb_notify, (void), {
  dispatchEvent(new Event("em-libusb"));
});

EM_ASYNC_JS(int, em_libusb_wait, (int timeout), {
  let onEvent, timeoutId;

  try {
    return await new Promise(resolve => {
      onEvent = () => resolve(0);
      addEventListener('em-libusb', onEvent);

      timeoutId = setTimeout(resolve, timeout, -1);
    });
  } finally {
    removeEventListener('em-libusb', onEvent);
    clearTimeout(timeoutId);
  }
});

يتم استخدام الدالة em_libusb_notify() كلما حاول libusb الإبلاغ عن حدث، مثل اكتمال نقل البيانات:

void usbi_signal_event(usbi_event_t *event)
{
  uint64_t dummy = 1;
  ssize_t r;

  r = write(EVENT_WRITE_FD(event), &dummy, sizeof(dummy));
  if (r != sizeof(dummy))
    usbi_warn(NULL, "event write failed");
#ifdef __EMSCRIPTEN__
  em_libusb_notify();
#endif
}

وفي الوقت نفسه، يتم استخدام جزء em_libusb_wait() من أجل "الاستيقاظ" من وضع عدم المزامنة عند تلقّي حدث em-libusb أو انتهاء المهلة:

double until_time = emscripten_get_now() + timeout_ms;
for (;;) {
  // Emscripten `poll` ignores timeout param, but pass 0 explicitly just
  // in case.
  num_ready = poll(fds, nfds, 0);
  if (num_ready != 0) break;
  int timeout = until_time - emscripten_get_now();
  if (timeout <= 0) break;
  int result = em_libusb_wait(timeout);
  if (result != 0) break;
}

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

بناء النظام والاختبار الأول

بعد الانتهاء من استخدام الخلفية، كان عليّ إضافتها إلى "Makefile.am" و"configure.ac". الجزء الوحيد المثير للاهتمام هنا هو تعديل العلامات الخاصة بـ Emscripten:

emscripten)
  AC_SUBST(EXEEXT, [.html])
  # Note: LT_LDFLAGS is not enough here because we need link flags for executable.
  AM_LDFLAGS="${AM_LDFLAGS} --bind -s ASYNCIFY -s ASSERTIONS -s ALLOW_MEMORY_GROWTH -s INVOKE_RUN=0 -s EXPORTED_RUNTIME_METHODS=['callMain']"
  ;;

أولاً، لا تحتوي الملفات القابلة للتنفيذ على أنظمة التشغيل Unix الأساسية على امتدادات ملفات عادةً. ومع ذلك، يُنتج Emscripten مخرجات مختلفة بناءً على الإضافة التي تطلبها. أستخدم AC_SUBST(EXEEXT, …) لتغيير الإضافة القابلة للتنفيذ إلى .html كي تصبح أيّ ملفات قابلة للتنفيذ ضِمن حزمة، مثل الاختبارات والأمثلة، ملف HTML يتضمّن واجهة الأوامر التلقائية التي تهتم بتحميل JavaScript وWebAssembly وإنشاء مثيلهما.

ثانيًا، بما أنّني أستخدم Embind وAsyncify، يجب تفعيل هاتين الميزتَين (--bind -s ASYNCIFY) بالإضافة إلى السماح بنمو الذاكرة الديناميكية (-s ALLOW_MEMORY_GROWTH) من خلال مَعلمات الرابط. للأسف، لا تتوفّر طريقة يمكن للمكتبة من خلالها إبلاغ الرابط بهذه العلامات، لذا على كل تطبيق يستخدم منفذ libusb هذا أن يضيف علامات الرابط نفسها إلى إعدادات تصميمه أيضًا.

أخيرًا، كما ذكرنا سابقًا، يتطلّب WebUSB تعداد الأجهزة بإيماءة مستخدم. تفترض أمثلة واختبارات libusb أن بإمكانها تعداد الأجهزة عند بدء التشغيل، وتتعذّر مع ظهور خطأ بدون تغييرات. بدلاً من ذلك، كان عليّ إيقاف التنفيذ التلقائي (-s INVOKE_RUN=0) وعرض طريقة callMain() اليدوية (-s EXPORTED_RUNTIME_METHODS=...).

وبعد تنفيذ كل ذلك، يمكنني عرض الملفات التي تم إنشاؤها باستخدام خادم ويب ثابت، وإعداد WebUSB، وتشغيل ملفات HTML التنفيذية يدويًا بمساعدة أدوات مطوّري البرامج.

لقطة شاشة تعرض نافذة في Chrome تعرض &quot;أدوات مطوري البرامج&quot; على صفحة &quot;testlibusb&quot; معروضة محليًا. تقيّم وحدة تحكّم أدوات مطوّري البرامج العنصر &quot;navigator.usb.requestDevice({Filters: [] })&quot; ما أدّى إلى ظهور طلب بالأذونات وتطلب من المستخدم حاليًا اختيار جهاز USB يجب مشاركته مع الصفحة. تم اختيار ILCE-6600 (كاميرا Sony) حاليًا.

لقطة شاشة للخطوة التالية تظهر فيها أنّ &quot;أدوات مطوري البرامج&quot; لا تزال مفتوحة بعد اختيار الجهاز، قيّمت وحدة التحكّم التعبير الجديد &quot;Module.callMain([&#39;-v&#39;])&quot; الذي نفَّذ تطبيق &quot;testlibusb&quot; في الوضع المطوَّل. تُظهر المخرجات معلومات تفصيلية مختلفة حول كاميرا USB التي سبق توصيلها: الشركة المصنّعة Sony والمنتج ILCE-6600 والرقم التسلسلي والإعداد وما إلى ذلك.

ولا يبدو ذلك كثيرًا، ولكن عند نقل المكتبات إلى منصة جديدة، من المشوّق الوصول إلى المرحلة التي تُنتج فيها مخرجات صالحة للمرة الأولى!

استخدام المنفذ

كما هو أعلاه، يعتمد المنفذ على بعض ميزات Emscripten التي يجب تفعيلها حاليًا في مرحلة ربط التطبيق. إذا أردت استخدام منفذ libusb هذا في تطبيقك الخاص، إليك ما عليك فعله:

  1. نزِّل أحدث libusb إما كجزء من تصميمك أو أضِفه كوحدة فرعية git في مشروعك.
  2. شغِّل autoreconf -fiv في مجلد libusb.
  3. شغِّل emconfigure ./configure –host=wasm32 –prefix=/some/installation/path لإعداد المشروع للتجميع المتبادل ولضبط مسار تريد وضع العناصر المدمَجة فيه.
  4. قم بتشغيل emmake make install.
  5. وجِّه تطبيقك أو مكتبة ذات مستوى أعلى للبحث عن القائمة المنسدلة ضِمن المسار الذي تم اختياره سابقًا.
  6. أضف العلامات التالية إلى وسيطات روابط تطبيقك: --bind -s ASYNCIFY -s ALLOW_MEMORY_GROWTH.

تخضع المكتبة حاليًا لبعض القيود:

  • لا تتوفّر إمكانية إلغاء عملية النقل. ويشكّل ذلك قيدًا على WebUSB، الذي ينبع بدوره من عدم إلغاء النقل من عدّة منصات في libusb نفسه.
  • ليس هناك دعم النقل المتساوي المتساوي. لن يكون من الصعب إضافتهم باتباع تنفيذ أوضاع النقل الحالية كأمثلة، ولكنه أيضًا وضع نادر إلى حد ما ولم يكن لدي أي أجهزة لاختباره، لذلك تركته في الوقت الحالي كغير متوافق. إذا كان لديك مثل هذه الأجهزة، وتريد المساهمة في المكتبة، نرحّب بخبراء العلاقات العامة.
  • القيود السابقة المذكورة سابقًا على عدّة منصات: تفرض أنظمة التشغيل هذه القيود، ولذلك لا يمكننا فعل أي شيء هنا، باستثناء طلب المستخدم من تجاوز برنامج التشغيل أو الأذونات. ومع ذلك، في حال نقل أجهزة HID أو أجهزة تسلسلية، يمكنك اتّباع مثال libusb ونقل بعض المكتبة الأخرى إلى واجهة برمجة تطبيقات أخرى في Fugu. على سبيل المثال، يمكنك نقل مكتبة C الخاصة بمكتبة hidapi إلى WebHID وتجاوُز هذه المشاكل، المرتبطة بإمكانية وصول USB منخفض المستوى، تمامًا.

الخلاصة

أوضحت في هذه المشاركة كيف يمكن نقل المكتبات منخفضة المستوى مثل libusb إلى الويب من خلال بعض حيل الدمج مع Emscripten وAsyncify وFugu API.

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

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