---
title: "ربط libVLC مباشرةً من Swift 6"
description: "احتاج تطبيق IPTV إلى «صورة داخل صورة»، وخطُّ VLCKit الذي كان يقوم عليه لا يملكها. ملاحظات في امتلاك محرّك C بدل استهلاكه — عزلُ المؤشّرات، وأعمارٌ لا يفحصها شيء، ومجموعةُ رقعٍ مرتّبة على شيفرة VLC نفسها، وما كلّفه ذلك."
author: "Omar Albeik"
date: 2026-08-03
type: article
topics: [swift, open-source, software, engineering]
language: ar
reading_time_minutes: 24
canonical_url: https://omaralbeik.com/ar/blog/binding-libvlc-directly-from-swift-6
translation_url: https://omaralbeik.com/en/blog/binding-libvlc-directly-from-swift-6
source_url: https://omaralbeik.com/ar/blog/binding-libvlc-directly-from-swift-6.md
---

# ربط libVLC مباشرةً من Swift 6

[«تلفاز»](https://tilfaz.app) تطبيق IPTV، تجده في متجر التطبيقات، ويعمل على
‏iPhone و‏iPad و‏Mac و‏Apple TV. و‏IPTV تعني ما يقرّر المزوّد أن يرسله إليك:
‏MPEG-TS فوق UDP، وبياناتِ HLS تتقادم بين مقطعٍ وآخر، وحاوياتِ MKV، وترجماتِ
‏SSA تحمل تنسيقها معها، وصوتًا بترميزاتٍ لم توفّر آبل لها فاكَّ ترميزٍ قطّ.

وإطار AVFoundation ممتاز في الصيغ التي تدعمها آبل. أمّا القول إنّ «AVFoundation
لا يصلح لـ IPTV» فدعوى يصحّحها لك خلال ساعةٍ واحدة رجلٌ أصدر تطبيق HLS لا غبار
عليه، ولم يجد يومًا سببًا لأن يُطعمه شيئًا سواه. وإليك المسألة في أصغر صورها.

خُذ ملفَّ MP4 يحمل فيديو H.264 وصوت AAC، وأعد تغليفه إلى Matroska بـ `-c copy`.
تخرج المسارات الأوّليّة متطابقةً بايتًا ببايت — وقد تحقّقتُ من بصماتها. يُفَكّ
ترميز MP4، أمّا MKV فيردّ `-11828`، *صيغة الوسيط هذه غير مدعومة*. إذ لا فاكَّ
تغليفٍ لـ Matroska أصلًا، فلا يُسأل البتّة عمّا في الداخل.

<Figure
  caption="اختُبرت بـ AVAssetReader على عيّناتٍ مفكوكة الترميز فعلًا لا على isPlayable، على macOS 26 وعلى محاكي iOS 26. وعشرة ملفّات، لأنّ النمط لا يتبيّن إلا إذا رأيتها مجتمعةً."
>
  <AVFoundationWall locale="ar" />
</Figure>

والرفض هو النصف الرخيص: العطب الصريح عطبٌ تستطيع التعامل معه. أمّا الصفوف
الثلاثة الأخيرة فهي الباهظة. يُفتح HEVC داخل تيّار نقلٍ على macOS، ويُبلغ أنّ
`isPlayable == true`، ولا يرفع خطأً، ثمّ يناولك أصلًا بلا مسار فيديو — و‏HEVC
داخل تيّار نقلٍ أمرٌ مألوفٌ في IPTV. فلا يرى المشاهد رسالة «صيغة غير مدعومة»، بل
يرى قناةً تبثّ صوتًا فوق مستطيلٍ أسود، فيخبرك أنّ تطبيقك معطوب، وهو محقّ.

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

## كنتُ أستعمل VLCKit أصلًا

فكان لا بدّ أن يكون المحرّك [libVLC](https://www.videolan.org/vlc/libvlc.html) —
نواةَ التشغيل لدى ‏VideoLAN، وهي واجهة C نفسها التي يقوم عليها VLC. والغلاف
المعروف لمنصّات آبل هو [VLCKit](https://code.videolan.org/videolan/VLCKit)، وهو
أيضًا من صنع ‏VideoLAN، مكتوبٌ بـ Objective-C. وكنتُ أستعمله أصلًا.

و‏VLCKit أثرٌ أمينٌ للعقد الذي كُتب فيه، وأقولها على وجهها: بروتوكولُ المفوَّض،
وأسماءُ الإشعارات، و`id drawable` الذي تُسنِد إليه عرضًا — هكذا كانت تُبنى أُطر
الوسائط في 2013، وكثيرٌ ممّا تعتمد عليه اليوم بُني على المنوال نفسه ويعمل.

ولم أغادر VLCKit بسبب الأسلوب، بل بسبب «صورة داخل صورة»، والسببُ أهونُ من أن
يُعدّ حجّةً في التصميم: خطُّ VLCKit 3 لا يملكها. لا نوعَ لها، ولا بروتوكول، ولا
وجودَ لـ `VLCDrawable.h` في ذلك الخطّ أصلًا — فالميزة إنّما تصل في VLCKit 4،
و‏VLCKit 4 في طور ألفا منذ أيلول 2022، وتُوزَّع من مسار تنزيلٍ فيه كلمة
`unstable`. فلم يكن الخيار إذًا بين Swift حديثة و‏Objective-C تفي بالغرض، بل كان:
إمّا أن أعتمد في الإنتاج على نسخةٍ اختباريّةٍ عمرها أربع سنوات لأنال ميزةً
واحدة، وإمّا أن أربط واجهة C التي تحتها بنفسي. وكلُّ ما تلا ذلك القرار — الربطُ
المباشر بـ C، وقواعدُ الأعمار، وأكثرُ الرقع الواردة في هذه المقالة — منه ينبع.

<Figure
  caption="مقارنةً بـ VLCKit 4.0.0-a22، لا بالخطّ الثالث ولا بملفّ README الخاص به، وهو بائتٌ حتى في فرعه الافتراضيّ. قُسمت الصفوف قسمين ولم تُحسب نقاطًا: ما أتقدّم فيه أنا في اللغة، وما تتقدّم فيه هي في الثنائيّ الصادر."
>
  <VLCKitMatrix locale="ar" />
</Figure>

وأمران لولاهما لكان ذلك الجدول غير أمين. أوّلهما أنّ VLCKit 4 لم تقف مكانها:
أضافت visionOS و‏watchOS، وأعادت كتابة إدارة الأحداث، ونالت تنقّلًا زمنيًّا دون
الثانية، وحازت دعم Swift Package Manager قبل أسبوعين من كتابتي هذه السطور.
والمقارنةُ بملفّ README الخاصّ بالخطّ الثالث — وهو ما يعرضه مستودعها عليك إلى
اليوم — مقارنةٌ بخصمٍ من قشّ.

وثانيهما أنّ المحرّك غير الصادر محرّكي أنا لا محرّكهم: فأنا على libVLC 4، ولم
يصدر منه إصدارٌ مستقرٌّ قطّ، بينما تُبقي VLCKit خطًّا على libVLC 3 الصادر وما
تزال تُصلح فيه. فإن كنتَ تحتاج محرّكًا قد صدر فعلًا، ففي هذه المقارنة خيارٌ
واحدٌ بالضبط، وليس خياري.

وثمّة [بلاغٌ مفتوحٌ موسومٌ
بـ Blocker](https://code.videolan.org/videolan/VLCKit/-/issues/759) على متتبّع
‏VideoLAN، سجّله في تموز غيري، يصف مدخلًا ثانيًا على مشغّلٍ مُعادِ الاستعمال لا
يعيد ربط خرج الفيديو أبدًا: صوتٌ يعمل خلف صورةٍ سوداء دائمة، على ‏tvOS، على
MPEG-TS حيٍّ من لوحة IPTV. وهذا تبديلُ قنوات. وهذا هو الحِمل بعينه.

## المحاولة التي سبقت المحاولة

في كانون الأول 2025 نشرتُ [`harflabs/VLC`](https://github.com/harflabs/VLC).
أربعةُ إيداعاتٍ في مساءٍ واحد، ثم لا شيء بعدها أبدًا.

ولم تكن حزمةَ libVLC. أخذت الأُطر الثلاثة المكتوبة بـ Objective-C التي توفّرها
‏VLCKit 3.7.0، إطارًا لكلّ منصّة، وأعادت نشرها أهدافًا ثنائيّةً في SPM خلف
مظلّةٍ مشروطةٍ بالمنصّة — إذ لا `Package.swift` لخطّ VLCKit 3 أصلًا، وأردتُ أن
أكتب عنوانًا في بيان الحزمة كما يفعل سائر الناس.

وهي مؤرشفةٌ اليوم، والسببُ ظاهرٌ في بيانها نفسه: يُعلن `swift-tools-version:
6.0` ثم يغلّف إطارًا لا تحمل ترويساته العامّة سمةَ تزامنٍ واحدة. وتبيّن أنّ
التحزيم هو النصف السهل: جعلتُ VLCKit قابلةً للاستحضار، ولم أغيّر شيئًا في أنّ
تطبيق SwiftUI بـ Swift 6 صار يملك نموذجَ كائناتٍ بلا سماتِ تزامن، بمفوَّضٍ واحد.

وبدأ [SwiftVLC](https://github.com/harflabs/SwiftVLC) في شباط، وبلغ 1.0 في تموز.
وبقيّةُ هذه المقالة تُبيّن أين ذهبت تلك الأشهر الأربعة والنصف.

## اربط C مباشرةً، واقبل الفاتورة

كان القرار الأول أن أنتقل من C إلى Swift بلا Objective-C في المنتصف.

وقد ساق الجدولُ أعلاه تلك الحجّة، وما لا يستطيع أن يُظهره أنّ الرسم كلَّه يصير
عرضًا واحدًا:

```swift
struct PlayerView: View {
  @State private var player = Player()

  var body: some View {
    VideoView(player)
      .onAppear { try? player.play(url: streamURL) }
  }
}
```

يسلّم `VideoView` إلى libVLC كائن `NSView`/`UIView` عبر `set_nsobject` فيرسم
‏VLC فيه مباشرة. لا طبقةَ تبنيها أنت، ولا `MTKView`، ولا `AVPlayerLayer`.

أمّا الفاتورةُ فهي أنّك صرتَ تملك كلَّ قواعد أعمار C بنفسك، بلا طبقةٍ على هيئة
ARC تختبئ خلفها. وكلُّ كائنٍ في libVLC يتبع النمط ذاته — `init` يخصّص، و`deinit`
يحرّر، وعمرُ كائن Swift يملك عمرَ مؤشّر C — وهذا الجزء آليّ. أمّا الإخفاقات
المثيرة فتقع كلّها عند المفاصل.

## طريقتان لنقل مؤشّر

لا يطابق `OpaquePointer` ولا `UnsafeMutableRawPointer` بروتوكول `Sendable`. وليس
ذلك سهوًا، ولا نتيجةً للعزل المناطقيّ: بل تُعلن المكتبة القياسيّة هذه المطابقة
*غير متاحة*، عن قصد، لأنّ المترجم لا يستطيع أن يستدلّ على ما يشير إليه المؤشّر.
والمناطق إنّما تُنقذك أحيانًا بأن تسمح بنقل قيمةٍ منفصلة بين نطاقات العزل، وهي
لا تنفع هنا، لأنّ الالتقاط داخل مُغلَّفٍ هاربٍ موسومٍ بـ `@Sendable` ليس نقلًا،
وتحريرُ كائن C خارج الخيط الرئيسي هو ذلك الالتقاط بعينه.

ويسمح SwiftVLC بمخرجين اثنين. فللالتقاط داخل مُغلَّفٍ واحد، ربطٌ محليّ ينسحب من
الفحص:

```swift
nonisolated(unsafe) let p = pointer
DispatchQueue.global(qos: .utility).async {
  libvlc_media_player_release(p)
}
```

فالمؤشّر قابلٌ للنقل بداهةً ويبقى صالحًا طوال النطاق المحيط، وهذه هي الحجّة
كلّها. أمّا المؤشّرات التي تُقرأ وتُكتب من خيوطٍ عدّة عبر الزمن، فـ`Mutex` حول
بنية حالة، والبنية موسومةٌ بـ `@unchecked Sendable`، والقفلُ — لا المترجم — هو
من يؤدّي العمل الفعليّ.

وثمّة طريقةٌ ثالثة تحظرها وثيقةُ البنية: تهريبُ المؤشّر عبر `Int(bitPattern:)`
والعودةُ به. إنّها تُترجَم، وتُسكِت التحذير، وتُتلف الشيئين اللذين جعلا التحذير
جديرًا بالوجود: النوعَ والقصد. وبعد ستّة أشهر لن يستطيع قارئُ تلك الدالّة أن
يميّز مؤشّرًا حيًّا من عددٍ صحيح، ويكون المترجم قد أُقنع بالكفّ عن المساعدة.

وتخرق الشيفرةُ قاعدتَها هذه في موضعٍ واحدٍ بالضبط: مسارٌ للتأهيل يخزّن عنوان
المشغّل بوصفه `UInt` ثمّ يعيد تحويله إلى مؤشّرٍ مباشرةً، وتعليقُ الحقل نفسه يسوق
العذر المحظور — *مخزَّنٌ بتّاتٍ لأنّ `OpaquePointer` ليس Sendable*. وقد تركتُه
على حاله. فالقاعدةُ ذاتُ الاستثناء الواحد الذي تستطيع الإشارة إليه، والقاعدةُ
التي كفّ الناس عن إنفاذها، تتشابهان تمامًا من الخارج، ولهذا وحده وُجدت هذه
الفقرة.

## أعمارٌ لا يفحصها شيء

تعبر الأحداثُ ثلاثةَ نطاقات عزل: نداءاتُ C التي تنطلق على خيوط libVLC نفسها، ثم
مذيعٌ متعدّد المستهلكين، ثم خصائصُ `@Observable` على المُمثِّل الرئيسي.

<Figure
  lockDirection
  caption="ليس للمذيع خيطٌ يخصّه: إنّما يعمل على الخيط الذي صادف أن ناداه منه libVLC. وينال كلُّ مشتركٍ تدفّقًا خاصًّا به، لا حصّةً من تدفّقٍ مشترك."
>
  <EventPath locale="ar" />
</Figure>

تلتقط `Broadcaster.broadcast` المشتركين تحت القفل، ثم تشغّل مرشّحاتهم وتسلّم
القيمة *خارجه*. سطرٌ واحد.

ويسوء أمران منفصلان إن سلّمتَ من الداخل. أوّلهما أنّ مرشّح المشترك شيفرةُ
مستخدم، فإن مسّت تلك الشيفرةُ المذيعَ من جديد — باشتراكٍ، أو بسؤالٍ عن خلوّه، أو
ببثّ — فوقعت في استعصاءٍ مع قفلٍ لا يقبل إعادة الحيازة؛ وهي ما تنتظرُ لا تزال
حائزةً قفلًا يقف عليه خيطُ أحداث libVLC نفسه. وثانيهما أنّ `onTermination` تعمل
في الحال على الخيط الذي ألغى المهمّة المستهلِكة، وتستدعي `unsubscribe` التي تطلب
ذلك القفل بعينه. فإلغاءٌ يقع في وسط البثّ يصير خيطًا واحدًا يطلب قفلًا هو حائزُه
أصلًا.

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

ويحمل النوعُ نفسه طريقتين للإغلاق: تُغلق `finishAll()` المشتركين الحاليين وتسمح
بالاشتراك من جديد، أمّا `terminate()` فتغلقهم وتجعل كلَّ استدعاء `subscribe`
*لاحق* يعيد تدفّقًا منتهيًا فورًا. ووُجدت الثانية لأنّ بعض المذيعين يُبلَغ إليهم
عبر خاصّيةٍ محسوبة تبني اشتراكًا جديدًا مع كلّ وصول، فوجب أن ينال المشتركُ الذي
يصل بعد زوال المنتِج تدفّقًا منتهيًا، لا تدفّقًا حيًّا لن يُطعمه أحدٌ أبدًا.
وبدونها لا يكون العطب انهيارًا، بل `await` لا يعود أبدًا، وهذا أعسر في التشخيص
بكثير.

كذلك وجب أن يصير النداءُ الواحد تدفّقين، لأنّ libVLC يجمع فيه بين نوعين من
الأحداث، ولا يجوز أن يُسقَط بأمانٍ إلا أحدُهما.

<Figure
  caption="الفصلُ بنيويٌّ لا إعدادٌ يُضبط: لا تستطيع دَفقةٌ من أحداث التوقيت أن تُزيح حدث تحكّم، لأنّ أحداث التوقيت لا تدخل مخزن التحكّم أصلًا. والفصل لا يحلّ مسألة الدمج، ولا يدّعي ذلك."
>
  <EventLanes locale="ar" />
</Figure>

ويبدو الهدم، إذا وُصف بالكلام، كأنّه قائمةٌ من أربعة أشياء بترتيبٍ ثابت. وليس
بقائمة، بل هو مجموعةُ قيودٍ يصادف أنّ ذلك الترتيب يفي بها، وأنفعُ طريقةٍ لإمساكه
أن تسأل: *ما الذي لا يزال يقرأ هذا؟*

<Figure
  lockDirection
  caption="بدّل عقدتين وسيظلّ كلّ شيءٍ يُترجَم. وحزمةُ الاختبارات التي قد تضبطك لا تعمل إلا تحت أدوات الفحص، وعلى جدولٍ زمنيّ."
>
  <DeinitOrder locale="ar" />
</Figure>

والقيود الثلاثة كلّها تعيش في تعليقٍ واحدٍ فوق دالّةٍ واحدة، وهو وحده ما
يُنفِذها.

## المؤشّر ليس هويّة

هذه هي الفكرة التي كنتُ أودّ لو أُخبرتُ بها قبل أن أبدأ، وهي التي بلغتُها
أخيرًا.

يستطيع `Player` أن يستبدل مشغّل libVLC القابع تحته بينما يبقى كائن Swift
وتدفّقات أحداثه على قيد الحياة. وحين يقع ذلك تصل النداءاتُ الجاريةُ لا محالة،
وستصل واصفةً شيئًا لم يعد موجودًا. والبديهةُ أن تقارن المؤشّرات، والبديهةُ
خاطئة، والمصدر يقول السبب في سطر: *عناوينُ المؤشّرات الأصليّة ليست هويّات — فقد
يعيد المُخصِّص عنوانًا متقاعدًا لمقبضٍ لاحق*.

ولذلك تنال كلُّ طبقةٍ يمكن أن تُستبدل تحت مستهلكٍ حيّ عدّادًا تصاعديًّا، ويحمل
كلُّ حدثٍ القيمَ التي وُلد بها.

<Figure
  lockDirection
  caption="خمسةُ عدّادات، مستقلّةٌ غيرُ متداخلة: قد تتغيّر جلسةُ الوسيط دون أن يتغيّر المقبض. ويصير الحدث قابلًا للرفض ما إن يتقدّم أيُّ عنصرٍ من عناصره."
>
  <GenerationAxes locale="ar" />
</Figure>

ولم أصمّم هذا سلفًا، بل تراكم صنفَ عللٍ بعد صنف؛ ولمّا دقّقتُ المكتبة بعد 1.0
كانت أكبرُ مجموعةٍ من الملاحظات صورةً من صور *هذا النداء يتحدّث عن الشيء
السابق*. ولو ابتدأتُ من جديد لمددتُ يدي إليه في اليوم الأول، وأحسب ذلك صحيحًا في
حقّ كلّ من يربط مكتبة C تعمّر كائناتُها بعد النداءات التي أنشأتها.

## حيث يكفّ الغلاف عن كونه غلافًا

والحقيقةُ الحاكمة أنّ libVLC ينسخ مؤشّرات نداءات ذاكرة الفيديو لديك وسياقَها
المبهم حين *يُفتح* خرجُ فيديو. ومسحُ متغيّرات النداء على مشغّل الوسائط بعد ذلك
لا يُبطل نسخةً يحوزها ذلك الخرج أصلًا. فالهدم البديهيّ — امسح النداءات ثمّ حرّر
السياق — استخدامٌ بعد التحرير ينتظر خرجًا لم يفرغ منك بعد.

والعلاج يربط كلَّ سياقٍ محتجَز بمؤشّر `libvlc_media_player_t` واحدٍ بعينه،
فتستطيع المتحكّمات المتعاقبة على المقبض الأصليّ نفسه أن تتسلّم ذرّيًّا، بينما لا
يستطيع خرجٌ متداخل أن يمسّ أبعادَ خرجٍ آخر ولا مجمّعَه ولا حالةَ تنظيفه. وإحالةُ
السياق إلى التقاعد توقف عملَ العرض الجديد فورًا، لكنّه لا يُحرَّر إلا بعد أن
يعود آخرُ تحريرٍ أصليٍّ محسوبٍ لذلك المقبض وتنضب كلُّ النداءات الجارية. ولا
تُعامَل مهلةٌ زمنيّة ولا ملاحظةٌ عابرةٌ لخرج الفيديو دليلًا على الأمان. فمع
نداءات C، عبارةُ «الغالب أنّه انتهى الآن» ليست عمرًا.

ثمّ إنّ مسارات العرض أربعة، وأظهرُ ما تفترق فيه هو مَن يملك الكائن في نهايتها.

<Figure
  caption="أربعة مسارات، والفرق بينها مَن يملك الكائن في آخرها: على iOS يملك libVLC متحكّم «صورة داخل صورة» ثم يعيده، وفي المسار المباشر تملك SwiftVLC الطبقة."
>
  <PiPPaths locale="ar" />
</Figure>

وذلك الصفُّ الأخير هو القرار الذي أنا أقلُّ ارتياحًا له وأكثرُ ثقةً بصوابه. فعلى
macOS تقتطع مرآةُ عازل العيّنات العامّة عند حجم طبقةٍ 1:1 بدل أن تتحجّم داخل
لوحة «صورة داخل صورة»، في إصدارات macOS التي أدعمها — أي أنّ الصورة ببساطة خطأٌ
على الشاشة. والمسار الذي يعمل إطارٌ خاصّ: تحميل `PIPViewController` من
`PIP.framework` في زمن التشغيل، ونقلُ عرض الرسم الحقيقيّ لـ VLC إليه.

وليس في شيءٍ من هذا ما يعني أنّ VideoLAN تركت «صورة داخل صورة» دون حلّ.
فـ‏libVLC يوفّر متحكّمًا لها، وكلا الغلافين يستعمله. غير أنّه مقصورٌ على iOS
و‏tvOS في ملفّات بناء VLC نفسها، وملفُّ المصدر يقول ذلك في ترويسته، والصنفُ غائبٌ
ببساطة عن شريحة macOS من الثنائيّ الذي أُصدره. فلا مسارَ أصليًّا على macOS في
المحرّك لأستعمله.

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

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

ثمّ يتسرّب التجريدُ في الاتّجاه الآخر. فاستخراجُ الهندسة التي احتجتُها من مسار
‏vmem اقتضى إضافةَ مداخلَ إلى libVLC لا يملكها libVLC — أي إصدارَ محرّكٍ وسّعتُ
سطحَه العامّ بلغة C، ثمّ التفاوضَ مع أرشيفاتٍ بُنيت قبل أن أخترعه.

<Figure
  caption="دالّةُ الإصدار هي المصافحة، والتعاريفُ الضعيفة في وصلة Swift هي ما يُبقي أرشيفًا غير مرقوعٍ قابلًا للربط. ترويسةٌ مُثبَّتة في قائمة تضمين VLC نفسها، ورموزٌ مُصدَّرة في ملفّ رموزها، وتأكيدٌ ساكن يثبّت موضع كلّ حقل."
>
  <EngineABI locale="ar" />
</Figure>

## حين تكفّ عن قراءة الترويسات

تكفّ في لحظةٍ ما عن قراءة ترويسات محرّك C وتبدأ بقراءة شيفرته، وبعد ذلك بقليل
تبدأ بتغييرها.

<Figure
  caption="حُسبت بتحليل كلّ فرقٍ لا بقراءة عنوانه، فتقع الرقعة في كلّ منطقةٍ تمسّها ويتجاوز مجموعُ الأعمدة عددَ الرقع. وصفُّ الملفّات مقسومٌ لأنّ عدَّ الملفّات المعدَّلة وحدها يُخفي الملفّات المُنشأة."
>
  <PatchAnatomy locale="ar" />
</Figure>

شُحن الإصدار 1.0 بستّ رقع، ونزلت التسع عشرة الباقية بعده باثني عشر يومًا، في
دفقةٍ واحدةٍ امتدّت ثمانية أيّام — وهو تقاربٌ أو تباعد بحسب حسن ظنّك. وأحسبه
تقاربًا: فالإصدار 1.0 هو النقطة التي كففتُ عندها عن الالتفاف حول المحرّك وبدأتُ
بإصلاحه.

وبعضها نقلٌ لإصلاحاتٍ رسميّةٍ وصلت بعد تثبيتي. وأوضحها سلسلةُ مؤقّت المشغّل: على
المراجعة المثبَّتة يستكمل المؤقّت الاستقراء بعد نقطة الإيقاف، فيظلّ مشغّلٌ
متوقّفٌ يبلّغ عن زمنٍ يتقدّم. ويظهر ذلك مباشرةً في `Player.currentTime`، ومن
القراءة نفسها يُضبط مرجعُ زمن «صورة داخل صورة»، فيكون العَرَضُ المرئيّ فيديو
متوقّفًا وشريطَ تقدّمٍ يزحف. وتحمل تلك الرقعة معها ثلاثَ إعاداتِ هيكلةٍ رسميّة
تعتمد عليها، حرفيًّا، بدل تكييف الإصلاحين الحقيقيّين يدويًّا على الشكل الأقدم —
وإحدى الثلاث هي التي تحوّل رايةَ الاستقراء إلى التعداد الذي يضيف إليه الإصلاحُ
الحقيقيّ حالةً. فإعادةُ أساسٍ يمكنك اشتقاقها من جديد خيرٌ من إعادة كتابةٍ عليك
التحقّق منها من جديد.

وبعضها لي. منها تحريرٌ مزدوج في فاكِّ التغليف التكيّفيّ في VLC.

تحرّر `ISegment::toChunk()` مصدرَ القطعة مرّتين على مسار فشل `prepareChunk()`:
مرّةً صراحةً عبر `recycleSource()`، ومرّةً حين تحذف القطعة، لأنّ
`~AbstractChunk()` تحرّر المصدر الذي تملكه القطعة. ولقِطَع المقاطع لا تكون
إعادةُ التدوير قابلةً للتخزين فتمضي مباشرةً إلى `delete`، فيتوزّع التحرير الثاني
عبر جدول دوالٍّ افتراضيّة فوق ذاكرةٍ محرَّرة.

وقد نسخ إيداعٌ من سطرٍ واحدٍ في 2022 نداءَ إعادة التدوير من الفرع الذي تحته
مباشرةً إلى الفرع الذي فوقه، وإعادةُ التدوير في الفرع الأسفل صوابٌ تمامًا: إذ لم
تُرجع `createChunk()` شيئًا هناك، فلا قطعةَ توجد لتملك المصدر. أمّا في الفرع
الذي فوقه فالقطعةُ موجودة، فصارت النسخةُ تحريرًا ثانيًا. نسخ أحدُهم سطرًا صحيحًا
إلى فرعٍ لا يصلح فيه. وهو باقٍ في فرع VLC الرئيسيّ إلى اليوم، وثمّة [بلاغٌ
مفتوح](https://code.videolan.org/videolan/vlc/-/work_items/29845) على متتبّع
VideoLAN يصف `EXC_BAD_ACCESS` متقطّعًا على iOS ‏arm64 بعد نحو عشر دقائق من تشغيل
HLS، بمكدّسٍ ينتهي عند الهادم.

والدقائق العشر ليست مصادفة. فـ`prepareChunk()` لا تعمل إلا للمقاطع المشفّرة، ولا
تفشل إلا حين يتعذّر الحصول على المفتاح. والمفاتيحُ محفوظةٌ في ذاكرةٍ مؤقّتة.
فتضرب العلّةُ عند تدوير المفاتيح أو بعد إخلاء تلك الذاكرة، لا عند البدء — وهذا
بعينه طابعُ *يعمل عشر دقائق ثمّ ينهار* في البلاغ.

مقاطعُ مشفّرةٌ وحدها، وتعذُّرُ مفتاح AES-128 وحده، وشبكةٌ متذبذبة، وجهازٌ
حقيقيّ، وعشرُ دقائق. هذه العلّة لن تُسقط حزمةَ اختباراتك. ولم تُسقط حزمتي أنا
كذلك، وأودّ أن أكون دقيقًا في ترتيب ما جرى: أصابها غيري أوّلًا، على إيداع
المحرّك نفسه الذي أثبّته، وسجّلها في المشروع الأصل. وأنا وجدتُ الخلل بقراءة
مخطّط الملكيّة، ثمّ أعدتُ إنتاجه على نحوٍ حتميّ. ورقعتي تضيف اختبارَ انحدارٍ إلى
حزمة VLC التكيّفيّة نفسها، يعدّ التحريرات بدل أن يحرّر، فيُسقط التحريرُ المزدوج
تأكيدًا بدل أن يقع في سلوكٍ غير معرَّف.

## كتبتُ العروض قبل أن أكتب المكتبة

بنيتُ تطبيقات العرض أوّلًا — واحدًا لكلّ منصّةٍ أُصدر عليها، يستوفي كلَّ ميزةٍ
عزمتُ على إصدارها، على ملفّ Matroska ذي الستّين ثانية — وبنيتُ حزمة الاختبارات
إلى جانبها. وكُتب الاثنان كأنّ المكتبة التي تحتهما تعمل أصلًا. ولم تكن موجودة
بعد. الموجودُ كان بيانًا قابلًا للتنفيذ لمعنى «أن تعمل»: شاشةٌ تشغّل بثًّا يحمل
ثلاثة مسارات صوت وتتيح لك التنقّل بينها، ونافذةُ «صورة داخل صورة» تنجو من تنقّلٍ
زمنيّ، وقائمةُ تشغيلٍ تتقدّم دون أن تُسقط حدثًا في الطريق.

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

والذي تغيّر هو ما صارت ساعاتي تذهب إليه. كفّت عن الذهاب إلى التنفيذ وصارت تذهب
إلى التوصيف — إلى قول ما يعنيه «الصواب» بدقّةٍ تكفي ليقرّر غيري بلوغَه من عدمه.
وإلى مسائل ذلك المستودع ذهب العمل، ولا شيء منه في العنوان أبدًا؛ وإنّما معايير
القبول تحته هي التي أخذت الوقت: عشرةُ آلاف استبدالِ وسيطٍ نشط تحت أدوات الفحص،
مع بياناتٍ وصفيّةٍ متأخّرةٍ ونداءاتِ هدم. واختبارُ تحمُّلٍ ممتدّ للتنقّل الزمنيّ
على جهازٍ حقيقيّ، مع الاحتفاظ بمنشأ التخصيص. وصفٌّ واحدٌ يجمع واجهتَي «صورة داخل
صورة» الخلفيّتين كلتيهما، كي لا تستطيع واجهةٌ ناجحة أن تخفي أخرى فاشلة. وكتابةُ
ذلك كتابةً جيّدة أعسرُ من كتابة الإصلاح، وهي الجزءُ الذي لا يُفوَّض.

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

ولذلك فكلُّ ما بنيتُه بعد ذلك إنّما وُجد ليجعل ذلك الفحص رخيصًا. فكلُّ ما يحمل
ثقلًا يحمل في متن إيداعه ملاحظةَ «أحمر-أخضر» تسجّل أنّ الإصلاح نُقض وأنّ
الاختبار شوهد وهو يفشل. وفحصُ تغطيةٍ يبلّغ أيَّ الرقع *لا* يحويها الثنائيّ قيدَ
الاختبار، لأنّ التكامل المستمرّ يربط محرّكًا صادرًا، والرقعةُ في الشجرة ليست
رقعةً في المُخرَج. وبوّابةُ الإصدار تُفصح عن حقيقتها: *هذه لا تشغّل الاختبارات —
بل يشغّلها إنسان، على عتاد. وإنّما تفرض أن تكون النتائج موجودة، وأن يكون كلُّ
صفٍّ مطلوبٍ قد أُجري ونجح، وأنّها تصف هذا المُخرَج لا مُخرَجًا أسبق.*

وقد استقرّ الحدُّ في الموضع نفسه كلَّ مرّة. فالكثرةُ والصبر يُفوَّضان تفويضًا
تامًّا، أمّا الملكيّة فلا. ترتيبُ الأقفال، واللحظةُ التي يموت فيها كائن C فعلًا،
وما تتوقّعه AVKit منك ممّا لم يكتبه أحدٌ قطّ — في هذه كنتُ أتلقّى شيئًا معقولًا
وخاطئًا، وأحبُّ الأجوبة الخاطئة إليها مهلةٌ زمنيّة، والجوابُ الحقيقيّ قاعدةٌ في
مَن يملك ماذا. والمهلةُ تنجح، ثمّ تفشل بعد شهر، على شبكة شخصٍ آخر. وقراءةُ شيفرة
VLC حتى تُفهم الآليّة هي ما لم أستطع تسليمه، وهي أيضًا ما أنتج الرقع.

وقد لزم النصفان كلاهما، والتحريرُ المزدوج هو الدليل — في الاتّجاه الذي لم
أتوقّعه. فالحلقةُ لم تجده. وجده بلاغُ انهيارٍ من شخصٍ آخر، على الإيداع الذي
أثبّته، وإغلاقُه اقتضى الجلوس إلى شيفرة VLC حتى يستقيم توزيعُ جدول الدوالّ
الافتراضيّة. أمّا ما كسبته الحلقة فكلُّ ما تلا ذلك: إعادةُ الإنتاج الحتميّة،
وتشغيلُ أدوات الفحص، وأربعُ مئة مرورٍ أظهرت أنّ الإصلاح صامد. التشخيصُ كان لي،
وأمّا البرهانُ فلا.

## ما كلّفه ذلك

أربعةُ أشهرٍ ونصف حتى 1.0، والفاتورة ما تزال تصل.

ومجموعةُ الرقع أكبرُ بندٍ فيها، وهي التزامٌ لا إنجاز. فسلسلةُ رقعٍ مرتّبة على
مراجعةٍ مثبَّتة تعني إعادةَ أساسٍ كلّما تحرّك التثبيت، والتثبيتُ لا يضمن لك
عالمًا مستقرًّا أصلًا: إحدى تلك الرقع موجودةٌ لا لشيء إلا أنّ إصدارًا أحدث من
‏Autoconf صار يناول مترجمَ C رايةَ معيارٍ غير التي يناولها مترجمَ Objective-C،
فعجز libtool عن استنتاج الوسم، فماتت كلُّ شرائح آبل في `test/`، بعد أن كان
libvlccore و‏libvlc قد رُبطا كلاهما. وهي المصادر نفسها التي بُنيت قبل ذلك
بأسبوعين.

ويُوزَّع المحرّك أرشيفًا ساكنًا، وذلك القرارُ الواحد في التحزيم يُحسب عليّ
مرّتين. مرّةً بوصفه مشكلة ترخيص: فـ‏libVLC تحت LGPL 2.1، والربطُ الساكن يدفع
المستهلك نحو البند الذي يتوقّع منك توزيع ملفّات الكائنات ليتمكّن مستخدموك من
إعادة الربط، وتطبيقُ متجر التطبيقات لا يستطيع ذلك. وثمّة مسألةٌ مفتوحة تطلب منّي
إصدارَ xcframework ديناميكيّ بدلًا من ذلك، سجّلها شخصٌ لا يعمل على هذا وكان
كريمًا في صياغتها — *الترخيصُ وحده هو ما يدفع في الاتّجاه الآخر* — ولم أجب عنها.
فالربطُ الديناميكيّ سيُنتج ثنائيًّا أكبر، ولم أحسم بعد ما تساويه تلك المقايضة.

ومرّةً بوصفه مشكلةً هندسيّة، لأنّ الأرشيف الساكن لا هويّة له عند التحميل.

<Figure
  caption="لا شيء هنا يفشل في الربط، وهذا ما يجعله خطرًا. فإذا ربطت صورتان كلتاهما الأرشيف، أنتجتا زمنَي تشغيلٍ كاملين لـ libVLC في عمليّةٍ واحدة، والشيء الوحيد الذي يكشف ذلك عدُّ الصور المحمَّلة التي تُعرّف رمزًا بعينه."
>
  <SingleCopy locale="ar" />
</Figure>

وبقيّةُ الدفتر باختصار. مسارُ «صورة داخل صورة» الذي يرسم على وجهه الصحيح على
macOS لا يمكن أن يدخل متجر التطبيقات، أي أنّ أفضل نسخةٍ من ميزةٍ بنيتُها هي
نسخةٌ لا أستطيع إصدارها. ويُوزَّع المحرّك مجرّدًا من رموز التنقيح وبلا dSYM،
فالانهيار داخل ‏libVLC يعطي كلَّ من يستعمله اسمَ دالّةٍ بلا رقم سطر. وصار تأهيلُ
الأجهزة الفعليّة مصفوفةً من ثلاثةٍ وخمسين صفًّا، وعُدّةَ اختبارٍ ملفُّ اختبارها
وحده أطولُ من أيّ شيءٍ كتبتُه في المكتبة — لأنّ ما ينكسر هنا ينكسر على العتاد،
وما سواه لا يُقبل دليلًا.

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

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

وليس لديّ مجموعٌ أعطيكه. فـ«صورة داخل صورة» على iOS ما تزال تُطفئ نفسها عند
تنقّلٍ زمنيٍّ عاديّ — والعلّةُ مفتوحةٌ على المتتبّع وأنا أكتب هذا، بإصلاحٍ
مدموجٍ لم يُثبَت بعد على العتاد، سجّلها شخصٌ لم ألقه قطّ. وهي الميزةُ التي
غادرتُ VLCKit لأنالها. بضعٌ وعشرون رقعةً في محرّك غيري، والشيءُ الذي بدأ هذا
كلَّه هو الشيءُ الذي ما يزال مفتوحًا.
