التوافق الخلفي لواجهات DLL وCOM ── جدول قرار لتحديد أيّ تغيير يكسر جهة الاستدعاء

· آخر تحديث: · · COM, DLL, .NET, C#, C++, التوافق الخلفي, ترقيم الإصدارات, تقنيات قديمة, إعادة استخدام الأصول القائمة, جدول القرار

سجل التعديلات (2 تحديثات، آخر تحديث 2 Sep، 2026)

سجل بالتغييرات التي أُجريت على هذا المقال. وحيثما حُفظت نسخة سابقة، تبقى متاحة للقراءة عبر رابط دائم يحمل معرّف DOI.

أُعيدَت الترجمة العربية كترجمة كاملة عن النص الياباني الأصلي، وأُضيفَت خريطة المعرفة.
أعيدت الترجمة كترجمة كاملة عن النص الياباني الأصلي. كانت النسخة العربية السابقة مختصراً يسقط أبواباً وجداول ورسوم Mermaid وتعليقات الأشكال وFAQ. أُعيدت هذه العناصر وفق الأصل الياباني، والادّعاءات التقنية مطابقة للنسخة اليابانية.
النشر الأول
الاستشهاد بهذا المقال(DOI: 10.5281/zenodo.21621734)

هذا المقال محفوظ على Zenodo. يرد أدناه معرّف DOI الذي يشير دائمًا إلى أحدث نسخة، ومعرّف DOI المثبَّت على النسخة التي تقرؤها.

غو كومورا (2026). التوافق الخلفي لواجهات DLL وCOM ── جدول قرار لتحديد أيّ تغيير يكسر جهة الاستدعاء. شركة كومورا سوفت ذ.م.م.. https://doi.org/10.5281/zenodo.21621734 https://comcomponent.com/ar/blog/dll-com-interface-backward-compatibility/

DOI (أحدث نسخة)
10.5281/zenodo.21621734
DOI (هذه النسخة)
10.5281/zenodo.22241055

«هل يكفي استبدال DLL لهذا الإصلاح؟ أم يجب إعادة بناء جهة الاستدعاء أيضاً؟» ── عندما تصون DLL مشتركاً أو مكوّن COM تستدعيه عدّة تطبيقات، ستجد نفسك تجيب عن هذا السؤال في كلّ إصدار. وإذا أخطأت في الإجابة، فقد يتوقّف ملفّ EXE قديم يعمل لدى العميل عن الإقلاع، أو الأسوأ: يستمرّ في الإقلاع لكن تتغيّر نتائج الحساب بصمت.

المعضلة أنّ هذا الحكم يميل إلى أن يُجرى بإحساس «يبدو خطراً نوعاً ما». في الواقع، أيّ تغيير يكسر التوافق يمكن الحكم عليه آليّاً تقريباً. لـ DLL الأصليّ قواعد التصدير واتّفاقيّة الاستدعاء، ولـ COM قاعدة صارمة منصوصة «الواجهة ثابتة»،1 ولـ .NET قائمة قواعد تغيير التوافق التي تستخدمها Microsoft نفسها في تطوير مكتبات .NET.2

في هذه المدوّنة شرحنا أساس COM في «ما هي COM / ActiveX / OCX»، وفكر التصميم في «ما هو COM - لماذا لا يزال تصميم Windows COM يبدو جميلاً حتى اليوم». في هذا المقال نرتّب لكلٍّ من DLL وCOM وتجميعات .NET «أيّ تغيير يكسر جهة الاستدعاء» في شكل جدول قرار، وحتّى الإجراء عندما لا بدّ من الكسر.

المصطلحات المستخدمة في هذا المقال

لقراءة جدول القرار تلزم عدّة مصطلحات على مستوى الثنائيّ. التفاصيل في كلّ فصل، لكن تثبيتها بسطر لكلٍّ أوّلاً يسهّل المتابعة.

المصطلح المعنى بسطر
ABI (Application Binary Interface) الاتّفاق الذي تلتزم به الثنائيّات المترجَمة فيما بينها. طريقة تمرير الوسائط، وتخطيط الذاكرة للبنية، وتسمية الرموز: عقد على مستوى لغة الآلة لا الشيفرة المصدر
اتّفاقيّة الاستدعاء (calling convention) جزء من ABI. الاتّفاق على تمرير الوسائط في السجلات أم المكدّس وبأيّ ترتيب، وعلى من ينظّف الوسائط المكدَّسة: المستدعي أم المستدعى (__cdecl / __stdcall ونحوهما)
vtable (جدول الدوالّ الافتراضيّة) جدول صفّ مؤشّرات الدوالّ بترتيب ثابت. هو كيان واجهة COM، وتستدعي جهة الاستدعاء الطريقة المقصودة بـ الموضع «أيّ فتحة هي»
الرقم التسلسليّ للتصدير (ordinal) رقم يُعطى لكلّ دالّة في جدول تصدير DLL. تستطيع جهة الاستدعاء الاستيراد بهذا الرقم بدل اسم الدالّة
IID / CLSID / ProgID على الترتيب: معرّف واجهة COM (العقد)، ومعرّف صنف التنفيذ، والاسم المستعار المقروء للبشر المقابل لـ CLSID (الفقرة 4.1)
الاسم القويّ (strong name) آليّة تمييز تجميعة .NET تمييزاً فريداً بـ «الاسم + الإصدار + الثقافة + رمز المفتاح العامّ» والتوقيع
إعادة توجيه الربط (binding redirect) إعداد ملفّ تكوين في .NET Framework يُعيد قراءة إصدار التجميعة الذي تطلبه جهة الاستدعاء إلى إصدار آخر يُحمَّل فعليّاً

1. الخلاصة أوّلاً

  • للتوافق ثلاث طبقات: التوافق الثنائيّ (يعمل بلا إعادة بناء)، والتوافق المصدريّ (يعمل إن أُعيد البناء)، والتوافق السلوكيّ (السلوك لا يتغيّر). ليس «بلا إعادة بناء = آمن»، ويلزم الحكم بما فيه التوافق السلوكيّ.3
  • الخطّ الأساس لـ DLL الأصليّ: «إضافة تصدير آمنة، وتغيير تصدير قائم أو حذفه مدمِّر». توقيع الدالّة واتّفاقيّة الاستدعاء وتخطيط البنية هي العقد الثنائيّ نفسه.
  • واجهة COM ثابتة (immutable) بعد النشر. إضافة طريقة أو حذفها أو إعادة ترتيبها بعد النشر مخالفة للمواصفة، والتغيير يُضاف كـ واجهة جديدة بـ IID جديد (IFoo ← IFoo2).41
  • عملاء VB6/VBA يحرقون الموضع على vtable بالربط المبكّر، فهم جهة الاستدعاء الأكثر انكساراً عند تغيير تخطيط الواجهة.
  • في .NET «ما الذي يُعدّ مدمِّراً في الواجهة العامّة» منشور كـ قواعد تغيير التوافق من Microsoft، ولا يُصنَّف حذف طريقة أو تغيير توقيع فقط، بل حتّى الافتراض (إضافة virtual) وتغيير اسم المعامل تغييرات مدمِّرة.2
  • الترقيم الدلاليّ للإصدارات اتّفاقيّة «ارفع الرئيس إن كان مدمِّراً»، لكنّه لا يعمل إلّا بعد إعلان تعريف ما الذي يُعدّ تغييراً مدمِّراً.5 يمكن استخدام جدول قرار هذا المقال كذاك التعريف.
  • عندما لا بدّ من كسر التوافق، السير توفير متوازٍ للقديم والجديد ← فترة إهمال ← جرد جهة الاستدعاء ← إلغاء. المبدأ ألّا تُستبدل فجأة.

في المخطّط، يشير الخطّ المتّصل إلى علاقة قائمة دائماً، ويشير الخطّ المتقطّع إلى علاقة مشروطة (شروط قيامها مذكورة في شرح كلّ علاقة في الصفحة التفصيليّة). القائمة الكاملة للعلاقات (المجموع 27، مع الأدلّة ودرجة اليقين) وتعريفات المفاهيم الرئيسة مجمّعة في صفحة تفاصيل خريطة المعرفة (باليابانية). البيانات: JSON-LD / Turtle

2. طبقات التوافق الثلاث ── من ينكسر، ومتى؟

ما يُقال بكلمة «توافق خلفيّ» ينقسم في الواقع إلى ثلاث طبقات. وثائق .NET الرسميّة أيضاً تصنّف التغيير المدمِّر من منظور التوافق المصدريّ والثنائيّ والسلوكيّ.3

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

هذه الطبقات الثلاث يسهل الإمساك بعلاقتها إن فُكّر فيها متداخلة.

[التوافق السلوكيّ] السلوك لا يتغيّر            ← الأبعد خارجاً
  └─ [التوافق المصدريّ] يعمل إن أُعيد البناء
       └─ [التوافق الثنائيّ] يعمل بلا إعادة بناء   ← الأعمق داخلاً

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

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

3. جدول قرار توافق DLL الأصليّ (C/C++)

توافق DLL الأصليّ يُحسَم بجدول التصدير واتّفاقيّة الاستدعاء وتخطيط الذاكرة. كيف يُبحث عن DLL ويُحمَّل شرحناه في «آليّة حلّ أسماء DLL في Windows»، أمّا التوافق بعد نجاح التحميل فيُحكَم بالجدول التالي.

محتوى التغيير التوافق الثنائيّ ملاحظات
إضافة دالّة مُصدَّرة لا ينكسر أكثر وسائل التوسيع أماناً. لكن إن اعتُمد على الأرقام التسلسليّة الضمنيّة في ملفّ .def، فقد تُعاد ترقيم الأرقام القائمة بحسب موضع الإضافة، فإن وُجد عميل ربط بالأرقام ثبّت الأرقام القائمة صراحة وأضِف في النهاية
حذف أو إعادة تسمية دالّة مُصدَّرة ينكسر يفشل حلّ الاستيراد، ويخطئ عند التحميل أو GetProcAddress
تغيير توقيع دالّة قائمة (إضافة وسيط أو حذفه أو تغيير نوعه، وتغيير نوع قيمة الإرجاع) ينكسر يختلف تسليم المكدّس والسجلات. قيمة الإرجاع كذلك: إن غُيّرت من عدد صحيح (RAX) إلى فاصلة عائمة (XMM0) تقرأ جهة الاستدعاء قمامة بـ ABI القديم. قد لا يصير خطأ ويجري بغير ضابط
تغيير اتّفاقيّة الاستدعاء (__cdecl__stdcall) ينكسر (32-bit) في x86 تتبادل مسؤوليّة تنظيف المكدّس فيؤدّي إلى تدمير المكدّس. في x64 اتّفاقيّة الاستدعاء واحدة وهذه التحديدات تُتجاهل عمليّاً، فهذا الصفّ حديث DLL بـ 32-bit
تغيير الرقم التسلسليّ للتصدير (ordinal) ينكسر بشروط جهة الاستدعاء التي تربط بالرقم تستدعي دالّة أخرى. الربط بالاسم فقط لا أثر
إضافة عضو إلى بنية تحجزها جهة الاستدعاء ينكسر جهة الاستدعاء القديمة تحجز صغيراً وتمرّر (يمكن التخفيف بعُرف cbSize المذكور لاحقاً)
تغيير الحزم والمحاذاة لبنية منشورة (#pragma pack، /Zp، تغيير سلسلة الأدوات) ينكسر حتّى بلا لمس أيّ عضو تتغيّر إزاحات الأعضاء القائمة والحجم الكلّيّ. cbSize أيضاً لا ينقذ انزياح الموضع، فثبّت الحزم صراحة في الترويسة المنشورة
تغيير داخليّ لبنية يحجزها DLL ويحرّرها وحده لا ينكسر إن أُظهر للخارج مؤشّر (مقبض) فقط يمكن تغيير الداخل بحريّة
تغيير معنى قيمة الإرجاع أو رمز الخطأ لا ينكسر (لكن ينكسر التوافق السلوكيّ) الربط ينجح والسلوك يتغيّر، النمط الأبطأ اكتشافاً
إضافة عضو بيانات أو دالّة افتراضيّة عند تصدير صنف C++ مباشرة ينكسر يتغيّر حجم الكائن أو تخطيط vtable. إضافة دالّة عضو غير افتراضيّة وحدها لا تغيّر التخطيط ولا تكسر العملاء القائمين مباشرة، لكن تصدير صنف C++ مباشرة أصلاً بلا توافق بين المترجمين، ولحظة اضطرارك لهذا الحكم في كلّ مرّة ABI هشّ

هذا الجدول لاتّخاذ «ماذا نغيّر من الآن»، لكن في الميدان يكثر الاتّجاه المعكوس: «هذا العَرَض لدى العميل، أيّ صفّ تغيير سببه». نقابل كيف يظهر كلّ صفّ أعلاه فعليّاً.

العَرَض في الميدان الصفّ الذي يُشتبَه به
عند الإقلاع يظهر حوار من نوع «تعذّر العثور على نقطة دخول الإجراء»، والتطبيق لا يقوم أصلاً. NTSTATUS هو 0xC0000139 (STATUS_ENTRYPOINT_NOT_FOUND، النصّ الأصليّ “The procedure entry point %hs could not be located in the dynamic link library %hs.”)6 حذف أو إعادة تسمية دالّة مُصدَّرة. في C++، الحالة التي تغيّر فيها التوقيع فتغيّر تزيين الاسم (الاسم المُشوَّه) وصارت «اسماً آخر» أيضاً العَرَض نفسه
GetProcAddress يُرجع NULL، والتطبيق يُخرج رسالة خطأ خاصّة به كذلك. في جهة استدعاء تحميل مؤجَّل أو تحميل ديناميكيّ يظهر بهذا الشكل
DLL نفسه لا يُوجد فلا يقلع. NTSTATUS هو 0xC0000135 (STATUS_DLL_NOT_FOUND)6 ليست توافقاً بل مشكلة نشر وترتيب بحث. قبل النظر في جدول القرار اشتبه بمسار البحث عن DLL
يسقط فور العودة من الدالّة. في بناء التنقيح (/RTCs أو /RTC1) يمسك فحص وقت التشغيل تدمير مؤشّر المكدّس تغيير اتّفاقيّة الاستدعاء (32-bit). Microsoft أيضاً تنصّ على أنّ تدمير مؤشّر المكدّس قد يحدث بعدم تطابق اتّفاقيّة الاستدعاء7. في بناء الإصدار لا يُكتشَف، ويسقط في موضع مختلف تماماً
لا خطأ، لكن قيم أعضاء معيّنة في البنية فقط تفسد. نادراً تجاوز مصدّ إضافة عضو إلى البنية، أو تغيير الحزم والمحاذاة. إن لم يُدخل عُرف cbSize (الفقرة 3.1) يصعب التضييق
في جهة استدعاء .NET يظهر MissingMethodException حذف عضو أو إعادة تسميته أو تغيير توقيعه في جهة .NET (الفصل 5)
لا خطأ أصلاً، لكن تغيّرت أرقام التقارير أو التجميع تغيير معنى قيمة الإرجاع أو رمز الخطأ. حالة انكسر فيها التوافق السلوكيّ وحده، والأبطأ اكتشافاً

إرشاد التصميم المستمدّ من هذا الجدول لم يتغيّر منذ قديم. ضيّق الحدود على ABI بلغة C (دالّات extern “C” وبنى بسيطة)، ووسّع بإضافة دوالّ. حتّى عند صنع DLL أصليّ من C# الأمر نفسه، ووجه التصدير الذي عالجناه في «كيف نستدعي DLL من C# Native AOT من C/C++» يُدار وفق هذا الجدول.

3.1 عُرف cbSize ── حكمة Win32 لجعل البنية قابلة للتوسيع

المعالجة الكلاسيكيّة لـ «إضافة عضو إلى البنية تنكسر» هي عُرف Win32 حمل حقل حجم في صدر البنية. تضع جهة الاستدعاء في cbSize حجم البنية الذي كانت تعرفه وقت الترجمة وتمرّره، وينظر DLL إلى ذلك الحجم ليميّز «هذا المستدعي يعرف أيّ جيل من البنية».

typedef struct KS_CONFIG {
    DWORD cbSize;      // 呼び出し側が sizeof(KS_CONFIG) を設定する
    DWORD dwMode;
    DWORD dwTimeout;
    // 将来のメンバは必ず末尾に追加する
} KS_CONFIG;

// DLL側: cbSize で世代を判別し、古い呼び出し側には既定値で振る舞う
if (pConfig->cbSize >= FIELD_OFFSET(KS_CONFIG, dwTimeout) + sizeof(DWORD)) {
    timeout = pConfig->dwTimeout;   // 新しい呼び出し側
} else {
    timeout = DEFAULT_TIMEOUT;      // 古い呼び出し側
}

سبب استخدام FIELD_OFFSET(KS_CONFIG, dwTimeout) + sizeof(DWORD) لا sizeof(KS_CONFIG) في الحكم هو الحكم «جيل يوجد فيه حتّى dwTimeout» بوحدة العضو الأخير. إن حُكم بـ sizeof، لحظة إضافة عضو آخر في النهاية لاحقاً يصير الشرط أشدّ، فيسقط إلى معاملة قديمة حتّى جهة استدعاء «تعرف dwTimeout ولا تعرف العضو الجديد». إن حُكم بهذا الشكل لكلّ عضو، لا يلزم إعادة كتابة الأحكام القائمة عند كلّ توسيع.

فعلاً، بنية NOTIFYICONDATA في Windows API تُدار أجيالها بهذا الأسلوب تحديداً، ومنصوص رسميّاً أنّ القيمة المضبوطة في cbSize تحفظ التوافق مع Shell32.dll القديم.8 إن وُضع cbSize في البنية المنشورة لـ DLL ذاتيّ من الإصدار الأوّل، ينتقل التوسيع اللاحق من «تغيير مدمِّر» إلى «جهة الأمان في جدول القرار». لكن إضافة الأعضاء حتماً في النهاية، وتغيير نوع الأعضاء القائمة أو ترتيبها ما زال محظوراً. وأمر آخر: في بنية تُستخدم للإخراج تزداد مسؤوليّة جهة DLL. الكتابة والتهيئة حتماً ضمن نطاق cbSize المستلَم. الكتابة بلا شرط بمقدار sizeof الجديد تتجاوز المصدّ الصغير الذي حجزته جهة استدعاء قديمة، فيُحدث DLL نفسه التدمير الذي كان العُرف سيمنعه.

4. قاعدة واجهة COM الصارمة ── بعد النشر يُحظَر التغيير

COM هي التقنية التي أعطت أوضح جواب لهذه المشكلة. في مواصفة COM تتبع الواجهة القواعد التالية.

  • للواجهة IID فريد (معرّف الواجهة).1
  • الواجهة ثابتة (immutable). بعد إنشائها ونشرها لا يجوز تغيير أيّ جزء من التعريف.1
  • إضافة طريقة أو حذفها أو تغيير الدلالة لا تعني «إصداراً جديداً للواجهة القديمة» بل صنع واجهة جديدة بـ IID آخر.4

لماذا هذه الصرامة؟ لأنّ كيان واجهة COM تخطيط ثنائيّ هو vtable (جدول مؤشّرات الدوالّ). عملاء C++ وVB6 يحرقون وقت الترجمة الموضع «الفتحة الثالثة هي GetName». إن أُدرجت طريقة بعد النشر، يستدعي العميل القديم طريقة أخرى بلا أيّ خطأ. لذلك ألغى COM عمليّة «التغيير» نفسها من المواصفة، وجهّز بدلها إجراء التوسيع التالي.

// v1: 公開済み。もう一切変更しない
[object, uuid(1111....)]
interface ICalc : IUnknown {
    HRESULT Add([in] long a, [in] long b, [out, retval] long* result);
};

// v2: 新しいIIDを持つ新インターフェース。ICalcを継承して拡張
[object, uuid(2222....)]
interface ICalc2 : ICalc {
    HRESULT AddChecked([in] long a, [in] long b, [out, retval] long* result);
};

IDL أعلاه مثال تصويريّ اختُصر فيه uuid إلى 1111....، ولا يُترجم كما هو. في الواقع تكتب GUID كاملاً مولَّداً بأداة صنع GUID المضمَّنة في Visual Studio (guidgen) أو أمر uuidgen. استخدام GUID نفسه لواجهتين يجعل تمييز العقد متعذّراً، لذلك عند كلّ زيادة واجهة تُولَّد واحدة جديدة حتماً.

صنف التنفيذ (coclass) ينفّذ ICalc وICalc2 كليهما، فيستخدم العميل القديم ICalc كالمعتاد، ويطلب العميل الجديد ICalc2 عبر QueryInterface. نظريّة ترقيم الإصدارات الرسميّة لـ RPC/COM أيضاً ترتّب: «واجهة جديدة ترث القديمة تعادل ترقية ثانويّة، وإن غُيّرت طريقة قائمة أو نوع فواجهة جديدة تماماً بلا وراثة (تعادل ترقية رئيسة)».9 ما يقيم هذا الأسلوب هو أنّ جهة الاستدعاء تستطيع عبر QueryInterface التحقّق بأمان وقت التشغيل من حالة الدعم. جمال هذا التصميم في الفكر عالجناه في «ما هو COM».

4.1 تقسيم أدوار CLSID وProgID وIID

عند التفكير في ترقيم إصدارات COM، فكّر بفصل أدوار ثلاثة أنواع من المعرّفات.10

  • IID معرّف الواجهة (العقد). إن تغيّر العقد يصير IID جديداً حتماً.
  • CLSID معرّف صنف التنفيذ. استبدال التنفيذ مع بقاء CLSID نفسه حرّ ما دمت تحفظ عقد الواجهات المنشورة.
  • ProgID اسم مستعار مقروء للبشر (KomuraSoft.Calc.1) لسحب المقابلة إلى CLSID من السجلّ. العُرف إيراد ProgID يحمل رقم إصدار، وProgID مستقلّ عن الإصدار يشير دائماً إلى الأحدث (KomuraSoft.Calc)، والأخير يُقابَل بالأحدث عبر CurVer.10

أي أنّ «ترقية إصدار التنفيذ» حديث عالم CLSID وProgID، و«تغيير العقد» حديث عالم IID، فلا تُخلَط. خيار تجنّب تسجيل السجلّ نفسه عالجناه في «ما هو Reg-Free COM».

4.2 لماذا ينكسر عملاء VB6/VBA خصوصاً بسهولة

عند استخدام مكوّن COM من VB6 أو VBA بإعداد مرجع (ربط مبكّر)، تُقرأ مكتبة الأنواع وقت الترجمة ويُحلّ الاستدعاء. الربط المبكّر شكل موصى به يعمل فيه IntelliSense وفحص الأنواع، والتنفيذ أيضاً أسرع،11 لكن الثمن ارتباط قويّ بتخطيط مكتبة الأنواع. إن تغيّر vtable الواجهة فبالطبع، وحتّى إن تغيّر التعريف على مكتبة الأنواع فقط يظهر بشكل «فتحت المشروع فإذا المرجع مكسور» أو «وقت التشغيل خطأ 430/438».

لذلك، في مكوّن جهة استدعائه VB6/VBA/ماكرو Excel، يلزم حفظ قاعدة ثبات الواجهة بأشدّ صرامة. لمكتبة الأنواع أيضاً إصدار (major.minor)، ويُرفع ويُدار عند زيادة العقد. توليد مكتبة الأنواع عند النشر بأنواع من جهة .NET إلى VBA مشروح في «استخدام DLL لـ .NET 8 من VBA بأنواع صريحة: نشر COM وTLB بـ dscom». وبالمقابل، عملاء الربط المتأخّر الذين يستخدمون CreateObject فقط يحلّون بالاسم فيقوَون على تغيير التخطيط، لكنّهم يتأثّرون بتغيير معنى الطريقة (التوافق السلوكيّ) بالقدر نفسه.

5. توافق تجميعات .NET ── الحكم آليّاً بالقواعد الرسميّة

في .NET منشورة «قواعد التغيير لأجل التوافق» التي تستخدمها Microsoft في تطوير مكتبات .NET نفسها، وتُصنَّف التغييرات إلى مسموح (✔️) ومحظور (❌) ويحتاج حكماً (❓).2 منصوص أنّها تُعتمَد كما هي كمعيار حكم لمكتبة شركتك، فنقتطف الصفوف الرئيسة.

التغيير على الواجهة العامّة الحكم تكملة
إضافة طريقة أو نوع أو عضو ✔️ آمن من حيث المبدأ لكن إضافة تغيّر حلّ تحميل زائد قائم تحتاج انتباهاً. إضافة حقل مثيل إلى struct منشور استثناء، لأنّ الحجم والتخطيط يتغيّران فيكسران التشغيل البينيّ والمستخدِمين غير الآمنين
حذف أو إعادة تسمية نوع أو عضو عامّ ❌ مدمِّر ينكسر وقت التشغيل بـ MissingMethodException ونحوه
تغيير التوقيع (إضافة وسيط أو حذفه أو ترتيبه أو نوعه، ونوع قيمة الإرجاع) ❌ مدمِّر يكسر الثنائيّ والمصدريّ كليهما
تغيير اسم المعامل ❌ مدمِّر يكسر الوسائط المسمّاة في C# والربط المتأخّر في VB. سهل الإغفال
إضافة virtual إلى عضو ❌ مدمِّر فخّ نموذجيّ يبدو «إضافة إذن آمن». قد يحدث اختلاف في IL الاستدعاء (call/callvirt)
حذف virtual، وجعل عضو افتراضيّ abstract ❌ مدمِّر ينكسر تجاوز الأصناف المشتقّة
إضافة عضو مجرّد إلى نوع عامّ غير sealed ❌ مدمِّر الأصناف المشتقّة القائمة لا تملك تنفيذاً
جعل النوع sealed ❌ مدمِّر الأصناف المشتقّة القائمة تصير غير قابلة للترجمة
إضافة عضو إلى واجهة ❓ يحتاج حكماً بإرفاق تنفيذ افتراضيّ (DIM) قد تزيد الأعضاء دون كسر أصناف التنفيذ القائمة، لكن الشروط كثيرة (أدناه)
تغيير قيمة ثابت أو قيمة تعداد، وإعادة تسمية عضو تعداد أو حذفه ❌ مدمِّر القيمة تُضمَّن في جهة الاستدعاء وقت الترجمة
التغيير إلى رمي استثناء أكثر اشتقاقاً ✔️ مسموح لأنّ catch القائم يواصل العمل
رمي استثناء من نوع جديد في مسار شيفرة قائم ❌ مدمِّر الرمي عند قيم معامل جديدة فقط جائز

الوحيد ❓ (يحتاج حكماً) في هذا الجدول هو «إضافة عضو إلى واجهة»، وهو أيضاً الصفّ الأكثر حيرة في العمل. بإرفاق تنفيذ افتراضيّ (DIM: Default Interface Members، أعضاء الواجهة الافتراضيّة) يمكن زيادة الأعضاء دون إحداث نقص تنفيذ في أصناف التنفيذ القائمة، لكن الشروط التي توردها القواعد الرسميّة كالتالي.2

  • يرتفع الحدّ الأدنى لمتطلّب جهة الاستخدام إلى .NET Core 3.0 / C# 8.0. أُدخل DIM في هذا الإصدار، فلحظة إضافة تنفيذ افتراضيّ يُترَك المستخدِمون الذين يستخدمون زمن تشغيل أدنى. .NET Framework خارج النطاق، لذلك في مكتبة يبقى فيها ولو عميل واحد على .NET Framework لا تُستخدم وسيلة التخفيف هذه.
  • توجد لغات لا تدعم DIM. .NET يُستخدم من لغات عدّة، فلا يُعتمَد على التنفيذ الافتراضيّ في واجهة منفَّذة من غير C#.
  • توجد مواضع لا يستطيع فيها زمن التشغيل حسم أيّ تنفيذ افتراضيّ يُستدعى. في تكوين تتداخل فيه واجهات عدّة قد يصير حلّ التنفيذ الافتراضيّ غامضاً.
  • من C# 13 فصاعداً، إضافة عضو مثيل افتراضيّ إلى واجهة ينفّذها ref struct تغيير مدمِّر مصدريّاً. ref struct لا يُعبَّأ ولا يُحوَّل إلى نوع واجهة، فلا يستطيع الرجوع إلى التنفيذ الافتراضيّ، ويلزم تنفيذ أعضاء المثيل صراحة حتماً.

وبالمقابل، إضافة عضو ساكن غير abstract وغير virtual مسموحة.2 عندما «تريد إضافة عضو لكنّ .NET Framework باقٍ لدى المستخدِمين»، الأمان أوّلاً ألّا تلمس الواجهة، وتدرس إضافة واجهة جديدة بفكر COM نفسه في الفصل 4، أو البديل بدالّة توسيع.

ليس ببساطة «ثبات الواجهة» في COM، لكن الفكر نفسه. الواجهة العامّة عقد، والإضافة إلى العقد جائزة، وتغيير العقد القائم غير جائز. وكون تغييرات تبدو آمنة مثل الدالّة الافتراضيّة واسم المعامل مصنَّفة في جهة المدمِّر هو سبب الحكم بالجدول لا بالإحساس.

5.1 الاسم القويّ وأرقام الإصدار الثلاثة

لتجميعة .NET أرقام إصدار عدّة، وأدوارها تختلف.12

  • AssemblyVersion: رقم الإصدار الوحيد الذي تستخدمه بيئة التشغيل لتمييز التجميعة وتحميلها. في التجميعة ذات الاسم القويّ، يشترط CLR في .NET Framework تطابقاً دقيقاً، لذلك عند كلّ رفع يلزم إعادة توجيه الربط في جهة الاستدعاء (.NET / .NET Core يقبل الإصدار الأعلى تلقائيّاً). تقترح الإرشادات الرسميّة، لتقليل إعادة التوجيه، عكس رقم الإصدار الرئيس فقط في AssemblyVersion.
  • FileVersion (AssemblyFileVersion): يظهر في خصائص مستكشف الملفّات فقط، ولا يؤثّر في سلوك بيئة التشغيل. يُوصى به كموضع لملء رقم بناء CI.
  • InformationalVersion: سلسلة حرّة موجَّهة للبشر. تسجّل إصدار الحزمة بصيغة semver أو hash التزام المصدر.

أي أنّ الشكل العمليّ السهل التعامل هو تكوين ثلاثيّ: «إعلان التوافق بإصدار الحزمة/المنتج (semver)، وAssemblyVersion للرئيس فقط، وتتبع البناء بـ FileVersion».

6. كيف يُوضَع رقم الإصدار ── semver لا يعمل إلّا بوجود «تعريف»

نقاط الترقيم الدلاليّ للإصدارات (semver) تُكتب بثلاثة أسطر. ارفع MAJOR عند تغيير بلا توافق، وMINOR عند إضافة وظيفة متوافقة خلفيّاً، وPATCH عند إصلاح خلل متوافق خلفيّاً.5

ما يسهل إغفاله أنّ أوّل طلب في مواصفة semver هو «البرمجيّة التي تستخدم semver يجب أن تعلن واجهة عامّة».5 إن لم تُعلَن ما هي الواجهة العامّة، لا يوجد معيار حكم «تغيير بلا توافق»، فيُحسَم رفع الرئيس بمزاج المسؤول. كثير من المواقع التي لا يعمل فيها semver لا تُغفل وضع الرقم بل هذا الإعلان.

التشغيل الواقعيّ لـ DLL موزَّع داخليّاً يصير الشكل التالي.

  1. أعلن نطاق الواجهة العامّة ── في DLL أصليّ الدوالّ المُصدَّرة والترويسة المنشورة، وفي COM الـ IDL / مكتبة الأنواع، وفي .NET الأنواع والأعضاء العامّة. انصص «ما عدا ذلك تنفيذ داخليّ يتغيّر بلا إعلان».
  2. اعتمد تعريف التغيير المدمِّر ── ضع جدولي قرار الفصلين 3 و5 في هذا المقال، وقواعد تغيير .NET،2 في المستودع كـ «تعريف الشركة».
  3. أتمت الحكم ── في .NET يمكن فحص التوافق الثنائيّ مع الإصدار السابق آليّاً بأدوات Package Validation / ApiCompat.13 يمكن إقصاء «ربّما بخير» في المراجعة.
  4. اجعل في ملاحظات الإصدار خانة توافق ── انصص في كلّ مرّة إحدى ثلاث قيم: «بلا إعادة بناء / يُوصى بإعادة البناء / يوجد تغيير مدمِّر». آليّة لإخراج جواب «هل يكفي الاستبدال؟» في المقدّمة كتابةً قبل أن يُسأل.

7. الإجراء عندما لا بدّ من كسر التوافق

عندما يصير التغيير المحكوم «مدمِّراً» في جدول القرار لا بدّ منه، لا تُستبدل بل تُقدَّم تقديماً متوازياً.

  1. وفّر القديم والجديد معاً ── في COM أضِف IFoo2 وأبقِ IFoo (الفصل 4). في DLL أصليّ أضِف دالّة جديدة (FooEx) أو أبقِ DLL جديداً باسم آخر معاً. في .NET أخرج كحزمة جديدة رُفع إصدارها الرئيس، وواصل الإصدار الرئيس القديم بإصلاح الخلل فقط.
  2. ضع فترة إهمال ── في .NET يمكن إخراج تحذير وقت الترجمة بصفة [Obsolete]. في الأصليّ / COM أعلن في تعليق الترويسة وملاحظات الإصدار، وانصص تاريخ الإلغاء المقرَّر. الجوهر قطع تاريخ لا «سيُحذف يوماً».
  3. اجرد جهة الاستدعاء ── من بحث المصدر داخل الشركة، وسجلّ توزيع المثبّت، وفي COM حالة المراجع في السجلّ، اجعل قائمة «من ما زال يستدعي الواجهة القديمة». إن وُجد هنا ثنائيّ يتعذّر إعادة بنائه (أداة متقاعد، تطبيق شركة أخرى)، مدّد عمر الواجهة القديمة بذلك القدر أو ابنِ جسراً بغلاف.
  4. احذف الواجهة القديمة ── احذف بعد التأكيد في الجرد على صفر جهة استدعاء، وارفع الإصدار الرئيس.

هذا الإجراء يكلّف. ولذلك بالمفارقة، تصميم الواجهة صغيرة واعياً بجدول القرار عند أوّل نشر (إن لم تُنشَر لا ينشأ واجب التوافق) يصير أكبر معالجة للتوافق.

8. الخلاصة

  • فكّر في التوافق بـ ثلاث طبقات: ثنائيّ ومصدريّ وسلوكيّ. قد ينكسر التوافق السلوكيّ ولو بلا إعادة بناء.3
  • DLL الأصليّ «الإضافة آمنة، وتغيير تصدير قائم أو توقيع أو تخطيط بنية مدمِّر». احمل cbSize في البنية واصنع فسحة توسيع.8
  • واجهة COM ثابتة بعد النشر. أضِف التغيير كواجهة جديدة بـ IID جديد (IFoo2) وميّز بـ QueryInterface.149 احفظ بصرامة أشدّ خصوصاً عند وجود عملاء VB6/VBA بالربط المبكّر.
  • .NET يُحكَم آليّاً بـ قواعد تغيير التوافق الرسميّة. انتبه إلى تصنيف «تغييرات تبدو آمنة» مثل الافتراض وتغيير اسم المعامل وجعل sealed في جهة المدمِّر.2
  • التكوين الواقعيّ AssemblyVersion للرئيس فقط، وتتبع البناء بـ FileVersion، وإعلان التوافق بـ semver.12
  • semver لا يعمل إلّا بعد إعلان تعريف الواجهة العامّة والتغيير المدمِّر.5 اعتمد جدول القرار كتعريف، وافحص تلقائيّاً بـ Package Validation ونحوه.13
  • عند الكسر توفير متوازٍ ← فترة إهمال ← جرد ← حذف. عدم الاستبدال فجأة يحمي EXE القديم لدى العميل.

مقالات ذات صلة

مجالات الاستشارة ذات الصلة

تتولّى شركة كومورا سوفت ذ.م.م. تصميم توافق DLL ومكوّنات COM ومكتبات .NET التي تشير إليها منظومات أخرى، وجرد الواجهة العامّة وتهيئة سياسة ترقيم الإصدارات، وتصميم وتنفيذ توسيع لا يكسر العملاء القائمين (أسلوب IFoo2 والتوفير المتوازيّ).

روابط مرجعية

  1. Microsoft Learn, Interface Design Rules. حول وجوب أن تحمل الواجهة التي ينفّذها كائن COM معرّف IID فريداً، وأنّه بعد الإنشاء والنشر لا يجوز تغيير أيّ جزء من التعريف (ثابتة).  2 3 4 5

  2. Microsoft Learn, Change rules for compatibility (.NET). حول تصنيف تغيير واجهة .NET إلى مسموح ومحظور ويحتاج حكماً، وأنّ حذف نوع أو عضو عامّ أو إعادة تسميته وتغيير التوقيع وتغيير اسم المعامل وإضافة virtual أو حذفه وجعل sealed وتغيير قيمة ثابت أو تعداد محظور (مدمِّر)، وأنّ إضافة عضو إلى واجهة يحتاج حكماً، وأنّ مطوّر المكتبة يستطيع استخدامها كمعيار تقييم لمكتبته.  2 3 4 5 6 7

  3. Microsoft Learn, Breaking changes (.NET library guidance). حول تصنيف التغيير المدمِّر إلى كسر مصدريّ وكسر سلوكيّ وكسر ثنائيّ، وأنّ الكسر الثنائيّ يفشل وقت التشغيل بـ MissingMethodException ونحوه في تجميعة تُرجمت مقابل الإصدار القديم.  2 3

  4. Microsoft Learn, Interface Pointers and Interfaces. حول أنّ واجهة COM ثابتة، وأنّ إضافة طريقة أو حذفها أو تغيير الدلالة يعني صنع واجهة جديدة لا إصداراً جديداً للواجهة القديمة، وأنّ IID يعرّف العقد تعريفاً فريداً.  2 3

  5. semver.org, Semantic Versioning 2.0.0. حول رفع MAJOR عند تغيير واجهة بلا توافق، وMINOR عند إضافة وظيفة متوافقة خلفيّاً، وPATCH عند إصلاح خلل متوافق خلفيّاً، وأنّ البرمجيّة التي تستخدم semver يجب أن تعلن واجهة عامّة، وأنّ التغيير غير المتوافق خلفيّاً على الواجهة العامّة يلزم رفع إصدار MAJOR حتماً.  2 3 4

  6. Microsoft Learn, MS-ERREF 2.3.1 NTSTATUS Values. حول أنّ STATUS_ENTRYPOINT_NOT_FOUND (0xC0000139) هو “The procedure entry point %hs could not be located in the dynamic link library %hs.”، وأنّ STATUS_DLL_NOT_FOUND (0xC0000135) هو “This application has failed to start because %hs was not found.”.  2

  7. Microsoft Learn, /RTC (Run-time error checks). حول أنّ /RTCs/RTC1) يتحقّق من مؤشّر المكدّس ويكشف تدمير مؤشّر المكدّس، وأنّ ذلك التدمير قد يحدث بعدم تطابق اتّفاقيّة الاستدعاء (مثل استدعاء دالّة يصدّرها DLL بـ __stdcall عبر مؤشّر دالّة __cdecl)، وأنّ /RTC لا يُستخدم في بناء الإصدار (المحسَّن). 

  8. Microsoft Learn, NOTIFYICONDATAW structure (shellapi.h). حول ضبط حجم البنية في العضو cbSize، وأنّ البنية توسّعت مع الأجيال، وأنّ ضبط قيمة مناسبة في cbSize يمكّن الاستخدام مع حفظ التوافق مع إصدارات Shell32.dll القديمة.  2

  9. Microsoft Learn, The Versioning Theory for RPC and COM. حول أنّ أفضل توسيع للوظائف في COM صنع واجهة جديدة، وأنّ واجهة جديدة ترث القديمة تعادل إصداراً ثانويّاً، وأنّ تغيير طريقة قائمة أو نوع يحتاج واجهة جديدة تماماً بلا وراثة، وأنّ حالة الدعم تُتحقَّق بـ QueryInterface.  2

  10. Microsoft Learn, COM Registry Keys. حول أنّ CLSID هو GUID يميّز صنف COM، وأنّ ProgID يقابل سلسلة مقروءة للبشر بـ CLSID دون ضمان الفرادة، وأنّ ProgID المستقلّ عن الإصدار يُقابَل بصنف أحدث إصدار عبر CurVer، وأنّ مفتاح Interface يسجّل IID.  2

  11. Microsoft Learn, OLE programmatic identifiers, late binding, and early binding (Project). حول التوصية بالربط المبكّر بإعداد مرجع في VBA، وأنّ الربط المتأخّر (CreateObject/ProgID) لا تُرى فيه الأعضاء وقت كتابة الشيفرة وأداء التنفيذ أدنى، وأنّ الربط المبكّر يحتاج إعداد مرجع إلى مكتبة كائنات الهدف. 

  12. Microsoft Learn, Versioning (.NET library guidance). حول استخدام AssemblyVersion لتحميل بيئة التشغيل واشتراط تطابق دقيق في .NET Framework مع الاسم القويّ، واقتراح تضمين رقم الإصدار الرئيس فقط في AssemblyVersion، وأنّ FileVersion للعرض في Windows ولا يؤثّر في سلوك وقت التشغيل، وأنّ InformationalVersion لتسجيل معلومات إصدار إضافيّة، والتوصية باستخدام semver 2.0.0 لإصدار حزمة NuGet.  2

  13. Microsoft Learn, NuGet package compatibility rules. حول وجوب تجنّب التغيير المدمِّر ثنائيّاً، وإمكان كشف التوافق مع إصدار خطّ الأساس تلقائيّاً بأدوات Package Validation وApiCompat، وعدم خفض AssemblyVersion بين الإصدارات.  2

أحدث المقالات التي تشترك في نفس الوسوم. عمّق فهمك بمواضيع مرتبطة.

ترتبط هذه المقالة بشكل طبيعي بصفحات الخدمات التالية.

الأسئلة الشائعة

أسئلة شائعة حول موضوع هذه المقالة.

إذا اكتفيت بإضافة دالّة إلى DLL، فهل يستغني ذلك عن إعادة بناء جهة الاستدعاء؟
من حيث المبدأ، إذا اكتفيت بإضافة دالّة مُصدَّرة (export)، يستمرّ عمل جهة الاستدعاء القائمة دون تغيير. فما دمت لم تغيّر اسم الدالّة الحالية أو توقيعها (signature) أو اتّفاقيّة الاستدعاء (calling convention) أو الرقم التسلسليّ للتصدير (export ordinal)، يبقى حلّ الاستيراد يعمل كما كان. لكن إذا أضفت عضواً إلى بنية تحجزها جهة الاستدعاء وتمرّرها، أو غيّرت معنى قيمة الإرجاع أو رمز الخطأ لدالّة قائمة، فقد يستمرّ التشغيل دون إعادة بناء لكنّ التوافق السلوكيّ ينكسر. القاعدة الأساسيّة: «إضافة دالّة آمنة، وتغيير توقيع دالّة قائمة مدمِّر».
لماذا لا يجوز إضافة طريقة إلى واجهة COM لاحقاً؟
لأنّ القاعدة في مواصفة COM هي أنّ واجهة COM بعد نشرها تصبح غير قابلة للتغيير (immutable). فالواجهة عقد بشأن تخطيط ثنائيّ يتمثّل في vtable (مصفوفة من مؤشّرات الدوالّ)، وإدراج طريقة أو حذفها أو إعادة ترتيبها يجعل الملفّات الثنائيّة القديمة تستدعي طريقة مختلفة عند الموضع الذي ثُبِّت وقت الترجمة. الإضافة في النهاية لا تغيّر مواضع الفتحات الحالية، لكنّها تخلق مشكلة أخرى: قد يمسك عميل جديد بمكوّن قديم على أنّه التنفيذ «الذي يُفترَض أن يكون قد أُضيف إليه»، فيستدعي فتحة غير موجودة أصلاً؛ لذلك تظلّ الإضافة على نفس IID غير مسموحة. عندما تريد زيادة الوظائف، تضيف واجهة جديدة بـ IID جديد (IFoo2) وتبقي على IFoo كما هي. وتستطيع جهة الاستدعاء عبر QueryInterface أن تحدّد بأمان أيّاً من القديم والجديد مدعوم.
كيف يُستخدم كلّ من AssemblyVersion وFileVersion وInformationalVersion في .NET؟
AssemblyVersion هو رقم الإصدار الوحيد الذي تستخدمه بيئة التشغيل للتعرّف على التجميعة وتحميلها. وفي التجميعات ذات الاسم القويّ، يشترط .NET Framework تطابقاً دقيقاً، ما يعني أنّ رفع هذا الرقم يستلزم في كلّ مرّة إعادة توجيه الربط (binding redirect). لهذا تقترح الإرشادات الرسميّة أن يُعكَس فيه رقم الإصدار الرئيس فقط. أمّا FileVersion فلا يظهر إلّا في خصائص مستكشف الملفّات ولا يؤثّر في سلوك بيئة التشغيل، ويناسب تسجيل رقم بناء CI مثلاً. وInformationalVersion نصّ حرّ موجَّه للبشر، يُسجَّل فيه رقم إصدار بصيغة semver أو hash الالتزام.
هل يحلّ اعتماد الترقيم الدلاليّ للإصدارات (semver) مشكلات التوافق؟
لا يحلّ semver وحده هذه المشكلة. فـ semver اتّفاقيّة تقول «ارفع رقم الإصدار الرئيس إذا أجريت تغييراً غير متوافق خلفيّاً»، لكنّه يفترض مسبقاً اشتراط إعلان «ما هي الواجهة العامّة، وما الذي يُعدّ تغييراً مدمِّراً». وبدون هذا التعريف، فإنّ مجرّد وضع رقم إصدار يجعل الحكم يتفاوت من شخص لآخر ولا يعمل فعليّاً. بالنسبة لـ DLL أصليّة يمكن اعتماد معيار مثل جدول القرار في هذا المقال، وبالنسبة لـ .NET يمكن اعتماد قواعد التغيير الخاصّة بالتوافق من Microsoft، بوصفها «تعريف الشركة الخاصّ للتغيير المدمِّر»، ودمجها في إجراءات الإصدار؛ عندها فقط يصبح semver ذا معنى.

الملف الشخصي للمؤلف

صفحة الملف الشخصي لمؤلف المقالة.

غو كومورا

مؤسّس شركة كومورا سوفت ذ.م.م.

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

العودة إلى المدونة