استدعاء Win32 API بأمان من C# ── دليل P/Invoke العملي (DllImport / LibraryImport / CsWin32)
· آخر تحديث: · غو كومورا · P/Invoke, DllImport, LibraryImport, CsWin32, C#, .NET, Win32, SafeHandle, التكامل مع الشيفرة الأصليّة, تطوير Windows, الاستشارات التقنية
سجل التعديلات (2 تحديثات، آخر تحديث 2 Sep، 2026)
سجل بالتغييرات التي أُجريت على هذا المقال. وحيثما حُفظت نسخة سابقة، تبقى متاحة للقراءة عبر رابط دائم يحمل معرّف DOI.
- أُعيدَت الترجمة العربية كترجمة كاملة عن النص الياباني الأصلي، وأُضيفَت خريطة المعرفة.
- أعيدت الترجمة كترجمة كاملة عن النص الياباني الأصلي. كانت النسخة العربية السابقة مختصراً يسقط أبواباً وجداول ورسوم Mermaid وتعليقات الأشكال وFAQ. أُعيدت هذه العناصر وفق الأصل الياباني، والادّعاءات التقنية مطابقة للنسخة اليابانية.
- النشر الأول
الاستشهاد بهذا المقال(DOI: 10.5281/zenodo.21621622)
هذا المقال محفوظ على Zenodo. يرد أدناه معرّف DOI الذي يشير دائمًا إلى أحدث نسخة، ومعرّف DOI المثبَّت على النسخة التي تقرؤها.
غو كومورا (2026). استدعاء Win32 API بأمان من C# ── دليل P/Invoke العملي (DllImport / LibraryImport / CsWin32). شركة كومورا سوفت ذ.م.م.. https://doi.org/10.5281/zenodo.21621622 https://comcomponent.com/ar/blog/pinvoke-safe-guide/
- DOI (أحدث نسخة)
- 10.5281/zenodo.21621622
- DOI (هذه النسخة)
- 10.5281/zenodo.22241003
في هذه المدوّنة كتبنا سابقاً عدّة مقالات حول التشغيل التبادليّ الأصليّ، مثل الفصل بين غلاف C++/CLI وP/Invoke، وطريقة استدعاء C# Native AOT DLL من C/C++، وجسر COM لاستدعاء 64bit DLL من تطبيق 32bit، وآليّة تحليل أسماء DLL في Windows. لكن لم يكن هناك بعد مقال يجمع P/Invoke نفسها التي تشكّل الأساس لكلّ ذلك.
يتمتّع P/Invoke بسهولة «يمكن استدعاء دالّة DLL عبر تصريح extern»، لكنّه في الوقت نفسه تقنيّة يقع فيها المرء مرّة واحدة على الأقلّ في مشكلة ما، سواء في ترحيل السلاسل، أو عمر المقبض، أو الحصول على رمز الخطأ، أو تخطيط البنى. في هذه المقالة نرتّب النقاط الواجب مراعاتها في العمل الفعليّ، ومحورها LibraryImport الذي يشكّل القيمة الافتراضيّة ابتداءً من .NET 7.
المصطلحات المستخدمة في هذه المقالة
نرتّب أوّلاً المصطلحات التي نستخدمها لاحقاً دون شرح متكرّر.
| المصطلح | المعنى |
|---|---|
| P/Invoke | Platform Invoke. آليّة .NET لاستدعاء دوالّ DLL غير مُدارة من شيفرة مُدارة |
| الترحيل (marshalling) | تحويل تمثيل الأنواع عند الحدّ بين المُدار وغير المُدار. يشمل تمرير السلاسل والبنى والمصفوفات والـ delegate |
| IL stub | شيفرة وسيطة يولّدها وقت التشغيل عند استدعاء DllImport، وتتضمّن معالجة الترحيل. تُترجم بـ JIT ثمّ تُستخدم في الاستدعاء الفعليّ1 |
| Native AOT | طريقة نشر تُترجم تطبيق .NET مسبقاً إلى شيفرة أصليّة عند الإصدار. لا يمكن استخدام آليّات تولّد شيفرة وقت التشغيل، لذا لا تتوافق جيّداً مع أسلوب IL stub1 |
| التقليم (trimming) | ميزة تحذف الشيفرة غير المستخدمة عند الإصدار فتصغّر حجم النشر. لا يمكن تتبّع الشيفرة المولَّدة ديناميّاً وقت التشغيل1 |
| النوع blittable | نوع تمثيله البتّيّ واحد في المُدار وغير المُدار، فيُمرَّر كما هو بلا تحويل. التفصيل في الفصل 72 |
1. الخلاصة أوّلاً
سيطول الحديث، لذا نذكر أوّلاً أربع نقاط يكثر فيها الحادث.
- من .NET 7 فما بعد اجعل
LibraryImportالافتراضيّ لاDllImport. يولّد شيفرة الترحيل وقت الترجمة، فيتوافق مع Native AOT والتقليم، ويزول تكلفة توليد IL stub وقت التشغيل، ويمكن تنفيذ الشيفرة المولَّدة خطوة بخطوة في المصحّح. المحلّلSYSLIB1054يدلّك على مواضع إعادة الكتابة منDllImport.13 - احتفظ بالمقبض في صنف مشتقّ من
SafeHandleلا فيIntPtrخام. هذه الممارسة الأساسيّة للتشغيل التبادليّ الأصليّ في .NET تمنع التحرير المبكّر للمقبض بفعل GC، والتحرير المزدوج، و«هجوم إعادة التدوير».45 - صرِّح بـ
StringMarshallingللسلاسل وتجنّبStringBuilder. ترحيلStringBuilderيرافقه دائماً نسخ إلى مخزن أصليّ، وهو غير كفء ويسهل فيه الخطأ في الإنهاء.6 - إن وضعت
SetLastError = true، اقرأMarshal.GetLastPInvokeError()فور الاستدعاء. يلزم التقاط الرمز قبل أن تطمسه شيفرة مُدارة أخرى.7
اجتماع هذه الأربع في موضع واحد ليس مصادفة. تصريح P/Invoke يعد، في تلك الأسطر القليلة، النوع والسلسلة وعمر الذاكرة والمقبض وطريقة استقبال الخطأ عند عبور الحدّ دفعة واحدة.
flowchart TB
subgraph MG["الجانب المُدار (.NET)"]
CODE["شيفرة C# المستدعية"]
OBJ["كائنات يحرّكها GC<br/>· مصفوفة blittable ← ثبّت (pin) ومرّر كما هي<br/>· سلسلة وغيرها غير blittable ← حوّل وانسخ<br/>· delegate ← أبق العمر حيّاً أثناء الاستدعاء"]
SH["SafeHandle<br/>يحمل عمر المقبض الأصليّ"]
end
subgraph BD["الحدّ (المرحّل)"]
SIG["تصريح DllImport / LibraryImport<br/>= عقد الحدّ هو ما كُتب هنا كلّه"]
CONV["تحويل الأنواع وضبط اتّفاقية الاستدعاء"]
TMP["مخزن مؤقّت للوجهة المحوَّلة"]
end
subgraph NT["الجانب الأصليّ (Win32 API / DLL بلغة C للشركة)"]
FN["دالّة مصدَّرة"]
NRES["ذاكرة ومقابض أصليّة"]
end
CODE --> SIG --> CONV --> FN --> NRES
OBJ -.->|"طريقة التمرير تتغيّر حسب طبيعة القيمة"| CONV
CONV --> TMP
TMP -.->|"يمرَّر المنسوخ"| FN
NRES -.->|"من يحرّر لا يُقرأ من التصريح<br/>يُقرَّر من مواصفات API"| SH
الشكل 1: أسطر التصريح القليلة تعدّ «النوع والسلسلة والعمر والخطأ» دفعة واحدة. طريقة التمرير تتغيّر حسب طبيعة القيمة (تثبيت، أو نسخ، أو إبقاء العمر فقط)، وأيّ خلل يظهر عرضاً «يسقط أحياناً».
الباقي من نوع «تطؤه إن لم تعرفه، ويكفيه بضعة أسطر إن عرفته». نضع الخلاصة في جدول، والتفصيل في الفصل المعنيّ.
| النقطة | الخلاصة | التفصيل |
|---|---|---|
| توقيع Win32 API | لا تكتبه بيدك، دَع CsWin32 يولّده. يكفي سرد أسماء الدوالّ في NativeMethods.txt فتخرج التوقيعات والثوابت والبنى من بيانات Win32 الرسميّة8 |
الفصل 3 |
| تخطيط البنية | اجعل LayoutKind.Sequential افتراضيّاً، وراعِ ما إذا صرّحت بـ Pack. Pack = 0 (الافتراضيّ) ليس «بلا حدّ أعلى» بل «حجم التعبئة الافتراضيّ للمنصّة»، وطريقة تحديد القيمة تختلف عن افتراضيّ /Zp في C++. وقد يختلف أيضاً بين .NET Framework و.NET 5+9 |
الفصل 7 |
| الـ callback (delegate) | أدِر العمر حتّى ينتهي الجانب الأصليّ من الاستخدام فلا يجمعه GC. احتفظ بحقل static أو استخدم GC.KeepAlive، وإن أمكن ففضّل UnmanagedCallersOnly10 |
الفصل 8 |
| 32bit/64bit | استقبل أنواع المؤشّرات بـ IntPtr/nint. لا تتعايش DLL بعدد بت مختلف في العملية نفسها، وهذا المتطلّب لا يُحلّ بـ P/Invoke |
الفصل 9 |
| بدائل P/Invoke | واجهة C صريحة ← P/Invoke؛ أصناف C++ وملكيّة واستثناءات ← C++/CLI؛ عبور حدود العملية ← COM. تقسيم عمل لا تنافس | الفصل 10 |
في المخطّط، يشير الخطّ المتّصل إلى علاقة قائمة دائماً، ويشير الخطّ المتقطّع إلى علاقة مشروطة (شروط قيامها مذكورة في شرح كلّ علاقة في الصفحة التفصيليّة). القائمة الكاملة للعلاقات (المجموع 21، مع الأدلّة ودرجة اليقين) وتعريفات المفاهيم الرئيسة مجمّعة في صفحة تفاصيل خريطة المعرفة (باليابانية). البيانات: JSON-LD / Turtle
2. DllImport مقابل LibraryImport ── أيّهما نستخدم
DllImport آليّة قديمة يولّد فيها وقت التشغيل IL stub للترحيل ثمّ يترجمه JIT قبل الاستدعاء. وبما أنّ التوليد وقت التشغيل، فهو لا يتوافق جيّداً مع تكوينات تترجم التجميعة مسبقاً مثل Native AOT والتقليم، وتكلفة التوليد نفسها ليست صفراً.1
LibraryImport مولّد مصدر أُضيف في .NET 7، يولّد شيفرة الترحيل وقت الترجمة لدوالّ partial. الشيفرة المولَّدة موجودة كمصدر C#، فيمكن تنفيذها خطوة بخطوة في المصحّح، وتُكتشف أخطاء التوقيع مبكّراً كأخطاء بناء.1
using System.Runtime.InteropServices;
internal static partial class NativeMethods
{
[LibraryImport("nativelib", EntryPoint = "to_lower", StringMarshalling = StringMarshalling.Utf16)]
internal static partial string ToLower(string str);
}
لقيمة الإرجاع string هذه افتراض يسهل إغفاله. المرحّل، بعد نسخ السلسلة التي يشير إليها المؤشّر المعاد، يحاول دائماً تحرير تلك الذاكرة. على Windows يُستخدم CoTaskMemFree، فإن كان الجانب الأصليّ قد خصّص ذلك المؤشّر بغير CoTaskMemAlloc (مخزن ثابت، malloc، new[]، وهي تنفيذات غير نادرة في واجهات C)، حرّر المرحّل الذاكرة بمخصِّص خاطئ، فيفسد الكومة أو ينهار.11 ما لم تنصّ ترويسة الطرف أو وثائقه صراحة على تخصيص متوافق مع CoTaskMemAlloc، استقبل الإرجاع بـ IntPtr لا string، واستدعِ دالّة التحرير المقابلة (أو الإجراء الذي يطلبه الطرف) بنفسك. تخصيص المخزن في جانب الاستدعاء وتمريره (مصفوفة أحرف بديلاً عن StringBuilder المذكور، أو نمط مخزن [Out] لاحقاً) يتجنّب أصلاً غموض الملكيّة من هذا النوع.
الفروق الرئيسة عن DllImport كالتالي.12
- أُلغي
CharSetواستُبدل بـStringMarshalling(Utf16/Utf8/ مخصّص). أُلغي ANSI وصار UTF-8 خياراً من الدرجة الأولى. - استُبدل
CallingConventionبـUnmanagedCallConvAttribute. - لا مقابل لـ
ExactSpellingوPreserveSig. يُحدَّد اسم نقطة الدخول دائماً بالتهجئة الدقيقة، وتحويل قيمة الإرجاع يجري دائماً كما هو. - يلزم جعل الصنف والدالّة المستدعاة كليهما
partial، ويلزم المشروعAllowUnsafeBlocks.
ما زال DllImport لازماً عندما تستخدم إعداداً لا يدعمه LibraryImport (بعض تحديدات MarshalAs مثلاً). المحلّل يخبرك بخطأ إن حاولت إعداداً غير مدعوم، لذا المسار الواقعيّ أن تكتب LibraryImport أوّلاً، فإن رُفض عدت إلى DllImport.12
3. CsWin32 ── خيار عدم كتابة التوقيعات يدويّاً
إن صرّحت بـ DllImport/LibraryImport يدوياً لكلّ Win32 API، تراكمت مخاطر الخطأ في نوع المعامل وقيمة الثابت وترتيب حقول البنية. CsWin32 (Microsoft.Windows.CsWin32) مولّد مصدر يولّد تلقائيّاً توقيع الدالّة المطلوبة والثوابت والبنى المرتبطة من بيانات Win32 الرسميّة.8
الاستخدام بسيط: أضف حزمة NuGet إلى المشروع، واسرد أسماء الدوالّ في ملفّ نصّيّ باسم NativeMethods.txt.
GetDpiForWindow
SetWindowPos
CreateFileW
CloseHandle
في كلّ سطر من NativeMethods.txt يمكن كتابة اسم الدالّة أو النوع أو الثابت أو مساحة الأسماء أو الوحدة، والبادئة - تعني الاستبعاد.13
عند البناء تُولَّد توقيعات P/Invoke لهذه الدوالّ (قيمة الإرجاع والمعاملات وتحديد SetLastError). انتبه إلى أنّ التوليد الافتراضيّ مبنيّ على DllImport التقليديّ. إن كنت تستشرف Native AOT والتقليم، ضع NativeMethods.json في جذر المشروع وعطّل allowMarshaling فينتقل التوليد إلى شيفرة لا تعتمد على مرحّل وقت التشغيل.13 يكفي هذان السطران في الملفّ.
{
"$schema": "https://aka.ms/CsWin32.schema.json",
"allowMarshaling": false
}
سطر $schema ليس إلزاميّاً، لكن كتابته تفعّل الإكمال والشرح والتحقّق في كثير من محرّرات JSON، ويمكن تتبّع قائمة الإعدادات المتاحة منه.13
يخرج HANDLE كنوع مشتقّ مناسب من SafeHandle، والسلاسل بـ CharSet/StringMarshalling الصحيح، فلا يمكن أصلاً إدخال أخطاء شائعة في الكتابة اليدويّة مثل خلط CharSet أو ترتيب حقول البنية.
كما كتبنا في المواضع التي ينفع فيها غلاف C++/CLI، DLL المعقّد الذي تتداخل فيه أصناف C++ والملكيّة والاستثناءات ينفع معه غلاف رفيع، لكن إن كان الطرف Win32 API صريحاً (أو DLL بواجهة C مماثلة)، فتوليد التوقيع بـ CsWin32 أقصر المسارات وأقلها حوادث. لا يُستخدم CsWin32 مع DLL خاصّ بالشركة، لكن يمكن عندئذ اتّخاذ أسلوب الشيفرة المولَّدة قالباً.
4. مطبّات ترحيل السلاسل
مترجمات C# وVB وF# تسند افتراضيّاً CharSet.None لتصريح P/Invoke الذي لا يصرّح بـ CharSet. سلوك CharSet.None الفعليّ كـ CharSet.Ansi، فيُرحَّل على Windows بغير Unicode (صفحة رموز محلّيّة). إن افترض Win32 API المستدعى الإصدار Unicode (لاحقة W)، أدّى الاستدعاء بهذا الافتراضيّ إلى تشويه الأحرف أو سقوط أحرف متعدّدة البايت.14
في LibraryImport الشكل الأساسيّ التصريح بـ StringMarshalling.Utf16. وبما أنّ خيار ANSI نفسه أُلغي، يصعب بنيويّاً حادث عصر DllImport الشائع: «تركتُ الافتراضيّ فصار ANSI دون قصد».12
مطبّ آخر هو معامل StringBuilder. يُستخدم كثيراً مع واجهات «الجانب الأصليّ يكتب مخزن سلسلة ويعيده»، لكن ترحيل StringBuilder يولّد دائماً نسخاً إلى مخزن أصليّ، ثمّ يجري ToString() تخصيصاً آخر. إن كان المخزن [Out] (الافتراضيّ)، تراكمت تخصيصات عدّة في كلّ استدعاء. إضافة إلى ذلك يسهل الخلل إن عاد المخزن بلا إنهاء NUL، أو بسلسلة مزدوجة الإنهاء. في الاستدعاءات المتكرّرة أثبت مصفوفة أحرف من ArrayPool<char>.6
معامل [Out] string تحديد يجب تجنّبه أيضاً. إن كانت السلسلة interned فقد يزعزع وقت التشغيل.6
5. إدارة عمر المقبض ── سبب استخدام SafeHandle
الاحتفاظ بمورد أصليّ مثل مقبض ملفّ أو مفتاح سجلّ أو مقبض جهاز في IntPtr خام تصميم يجب تجنّبه في التشغيل التبادليّ الأصليّ لـ .NET. الأسباب ثلاثة.4
- تحرير مبكّر للمقبض بفعل GC. إن حمل صنف ينفّذ finalizer المقبض في حقل
IntPtr، قد يجمع GC الكائن أثناء استدعاء P/Invoke فيغلق المقبض. - هجوم إعادة تدوير المقبض. تعيد Windows استخدام قيم المقابض بفاعليّة. إن واصلت استخدام
IntPtrقديم بعد أن أُعيد تخصيص القيمة لمورد آخر، تعاملت مع مورد غير ذي صلة حادثاً خطيراً. - تسريب بسبب استثناء غير متزامن. إن وقع انقطاع غير متزامن مثل إيقاف الخيط بين الحصول على المقبض وتخزينه في الحقل، قد يتسرّب المقبض.
SafeHandle صنف مجرّد صُمِّم لحلّ هذه. يرث CriticalFinalizerObject، فيُضمن تنفيذ التحرير حتّى عند إنهاء غير طبيعيّ لـ AppDomain. يزيد استدعاء P/Invoke عدّاد مراجع المقبض وينقصه تلقائيّاً، فلا يُعاد تدوير المقبض أثناء الاستدعاء.4
عند الكتابة الذاتيّة ارث مثلاً SafeHandleZeroOrMinusOneIsInvalid في مساحة الأسماء Microsoft.Win32.SafeHandles، وتجاوز ReleaseHandle(). تعمل ReleaseHandle() في منطقة تنفيذ مقيّدة تفترض «ألا تفشل»، لذا الثابت أن تقتصر على استدعاء API تحرير بسيط بلا منطق معقّد. لا حاجة إلى كتابة finalizer بنفسك (بل ينبغي تجنّبه).5
6. معالجة الأخطاء ── SetLastError وGetLastPInvokeError
كثير من Win32 API يضبط عند الفشل رمز خطأ محلّيّاً للخيط بـ SetLastError، ويقرأه المستدعي بـ GetLastError. لمعالجة ذلك في P/Invoke اضبط DllImportAttribute.SetLastError (وخاصّيّة بالاسم نفسه في LibraryImport) على true.15
[LibraryImport("kernel32", EntryPoint = "SetCurrentDirectoryW", StringMarshalling = StringMarshalling.Utf16, SetLastError = true)]
[return: MarshalAs(UnmanagedType.Bool)]
internal static partial bool SetCurrentDirectoryW(string path);
نقطتان هنا.
- اقرأ رمز الخطأ فور الاستدعاء. في .NET (عدا .NET Framework) تُمسح معلومات الخطأ في كلّ استدعاء P/Invoke بـ
SetLastError = true، ويُحفظ ناتج ذلك الاستدعاء وحده. إن حشرت إخراج سجلّ أو استدعاء API آخر طُمس الرمز، فالتقط القيمة في موضع اكتشاف الفشل.15 - استخدم
Marshal.GetLastPInvokeError()لاMarshal.GetLastWin32Error(). من .NET 6 فما بعد هما وظيفيّاً واحد، لكنّ الثاني اسم أحدث يعكس نيّة عبر المنصّات ويُوصى به.7
if (!SetCurrentDirectoryW(path))
{
int error = Marshal.GetLastPInvokeError();
throw new Win32Exception(error);
}
7. ترحيل البنى ── الأنواع blittable وStructLayout
الأنواع التي تمثيلها البتّيّ واحد في .NET والشيفرة الأصليّة تُسمّى «blittable»، وتُمرَّر كما هي بلا تحويل فتكون سريعة. الأنواع الأساسيّة مثل byte وint وlong، والبنى ذات التخطيط الثابت المؤلّفة من أنواع قيمة blittable فقط، تدخل هنا. للبنية blittable استخدام sizeof() في C# أسرع من Marshal.SizeOf<T>(). في المقابل bool ليس blittable (BOOL الأصليّ 4 بايتات وbool في C/C++ بايت واحد)، فإن استخدمته دون وعي صنعت خطأ يُسقط نصف قيمة الإرجاع.2
يُضبط تخطيط البنية بـ StructLayoutAttribute. الافتراضيّ LayoutKind.Sequential (الترتيب حسب التصريح)، ولا تستخدم LayoutKind.Explicit إلا عندما تريد التصريح بمواضع الحقول كما في union.9
الحقل Pack يسهل إغفاله. بحسب الوثائق الرسميّة قواعد الوضع درجتان.9
- محاذاة النوع كلّه = الأصغر بين «حجم أكبر حقل» و«قيمة
Packالمحدَّدة» - حدّ وضع كلّ حقل = الأصغر بين «حجمه هو» و«محاذاة النوع»
أي إن صرّحت بـ Pack صغير (2 أو 4 مثلاً) عمل حدّاً أعلى للمحاذاة مثل #pragma pack(N) في C++. المشكلة في الافتراضيّ Pack = 0.
7.1 تقابل «المعماريّة × القيمة الافتراضيّة»
Pack = 0 لا يعني «بلا حدّ أعلى». بحسب الوثائق، 0 يشير إلى «حجم التعبئة الافتراضيّ للمنصّة الحاليّة».9 أي الحدّ الأعلى موجود، وأنت لم تقرّر قيمته بنفسك. وليس «كافتراضيّ /Zp في C++» أيضاً. كلاهما حدّ أعلى لحجم التعبئة، لكن طريقة تحديد القيمة تختلف، فنقل رقم من أحدهما إلى الآخر يفسد الحساب.
| نظام المعالجة | مضمون الافتراضيّ | x86 | x64 | ARM / ARM64 | ARM64EC |
|---|---|---|---|---|---|
Pack = 0 في C# (الافتراضيّ) |
محاذاة النوع كلّه = الأصغر بين «حجم أكبر حقل» و«حجم التعبئة الافتراضيّ للمنصّة»9 | كما في اليسار | كما في اليسار | كما في اليسار | كما في اليسار |
/Zp في C++ (الحدّ الأعلى لمحاذاة أعضاء البنية)16 |
يُوضع العضو على الأصغر بين «حجمه» و«حدّ N بايت» | 8 بايتات | 16 بايتاً | 8 بايتات | 16 بايتاً |
الملاحظة العملية: لا تقرأ هذا الافتراضيّ «بلا حدّ أعلى» فتحسب الإزاحة يدوياً. خصوصاً في بنية تحوي حقلاً محاذاته الطبيعيّة كبيرة، يعمل الحدّ الأعلى حتّى بالافتراضيّ فيفسد الحساب. إن حاولت عندئذ التصريح بـ Pack تخميناً لرتق الحساب، ثبّت الاختلاف عن الجانب الأصليّ، وهذه أخطر حالة في P/Invoke. إن لم تكن واثقاً من الإزاحة، قِس بـ Marshal.SizeOf وMarshal.OffsetOf في القسم 7.2 ثمّ قابل ترويسة الجانب الأصليّ.
إضافة إلى ذلك قد يتغيّر التخطيط الافتراضيّ في جانب C# حسب إصدار وقت التشغيل. الوثائق تحمل مثالاً لبنية تتضمّن decimal يصير حجمها بالتعبئة الافتراضيّة 28 بايتاً في .NET Framework و32 بايتاً في .NET 5+ لاختلاف تكوين الحقول الداخليّ.9
| محور المقارنة | ما يتغيّر |
|---|---|
| عملية 32bit / عملية 64bit | عرض حقول المؤشّر (الفصل 9). ويتبعه حجم البنية كلّها |
| .NET Framework / .NET 5+ | الحجم الافتراضيّ لبنية تتضمّن بعض الأنواع (مثال decimal أعلاه)9 |
| جانب C# / جانب C++ | قاعدة المحاذاة الافتراضيّة نفسها (الجدول أعلاه) |
الخلاصة هنا: لا تفترض «الافتراضيّ فلا بدّ أن يتطابق». إن غيّرت ترويسة الجانب الأصليّ حجم التعبئة صراحة بـ #pragma pack، أو كان الطرف DLL يطلب محاذاة تتجاوز 8 بايتات، فصرِّح بـ Pack في جانب C# أو تحقّق من إزاحات الحقول بالطريقة في القسم التالي قبل الاستخدام. إن أهملت ذلك انزاحت إزاحات الحقول وفسدت البيانات بصمت.
في المقابل، مع API صريح يستخدم ترويسات Windows SDK كما هي وحقول كلّها أنواع أساسيّة 8 بايتات أو أقلّ، نادراً ما تقع مشكلة عملية إن تركت المحاذاة الافتراضيّة دون لمس Pack.
// ネイティブ側のヘッダーが pack(4) を明示している場合の例
[StructLayout(LayoutKind.Sequential, Pack = 4)]
internal struct DeviceInfo
{
public int DeviceId;
public uint Flags;
public long Timestamp;
}
7.2 التحقّق الفعليّ من التخطيط ── Marshal.OffsetOf
تطابق التخطيط أوثق بالقياس من العدّ على الورق. تطبيق الطرفيّة التالي يخرج حجم البنية وإزاحة كلّ حقل كما هما. استخدمه أداة: ألصق البنية المراد فحصها ونفّذ، ثمّ قابل ترويسة الجانب الأصليّ.
// استبدل بهذا Program.cs لمشروع أُنشئ بـ dotnet new console (.NET 8 / C# 12)
using System.Runtime.InteropServices;
Console.WriteLine($"معمارية العملية: {RuntimeInformation.ProcessArchitecture}");
Console.WriteLine($"IntPtr.Size : {IntPtr.Size} بايت");
Console.WriteLine($"Marshal.SizeOf : {Marshal.SizeOf<DeviceInfo>()} بايت");
Console.WriteLine("--- إزاحات الحقول ---");
foreach (var field in typeof(DeviceInfo).GetFields())
{
IntPtr offset = Marshal.OffsetOf<DeviceInfo>(field.Name);
Console.WriteLine($"{field.Name,-12} : {offset}");
}
// الصق هنا البنية المراد التحقق منها.
// انتبه: يجب وضعها بعد عبارات المستوى الأعلى
[StructLayout(LayoutKind.Sequential, Pack = 4)]
internal struct DeviceInfo
{
public int DeviceId;
public uint Flags;
public long Timestamp;
}
Marshal.OffsetOf<T>(string fieldName) يعيد إزاحة البايت من بداية الحقل عند الترحيل كغير مُدار. في الجانب الأصليّ أخرج القيمة نفسها بماكرو offsetof وsizeof في C/C++ وقارن.
// 比較用。ネイティブ側のヘッダー(DeviceInfoの定義を含むもの)をインクルードする
#include <stdio.h>
#include <stddef.h>
#include "device.h"
int main(void)
{
printf("sizeof(DeviceInfo) = %zu\n", sizeof(DeviceInfo));
printf("offsetof(DeviceId) = %zu\n", offsetof(DeviceInfo, DeviceId));
printf("offsetof(Flags) = %zu\n", offsetof(DeviceInfo, Flags));
printf("offsetof(Timestamp) = %zu\n", offsetof(DeviceInfo, Timestamp));
return 0;
}
إن تطابقت القيم في كلّ الحقول، فالتخطيط متوافق على تلك المنصّة. إن انزاح واحد، فخطأ في تحديد Pack أو في نوع الحقول أو ترتيبها. إن نشرت 32bit و64bit كليهما، أجرِ هذا الفحص بعددَي البتّ. أن يتطابق أحدهما فقط هو المظهر النموذجيّ لهذا العطل.
8. إدارة عمر الـ callback (delegate)
ليس نادراً تمرير callback إلى API أصليّ بمعنى «عند الاكتمال استدعِ هذه الدالّة». في الشيفرة المُدارة يؤدّي delegate هذا الدور، لكن هنا مطبّ خاصّ بـ GC. حتّى إن حصلت على مؤشّر دالّة من الـ delegate بـ Marshal.GetFunctionPointerForDelegate، لا يتتبّع GC العلاقة بين مؤشّر الدالّة والـ delegate. إن جُمع الـ delegate بينما الجانب الأصليّ ما زال يستخدم مؤشّر الدالّة، انهار الأمر.10
مطبّ آخر يسهل إغفاله هو اتّفاقية الاستدعاء. عند تمرير delegate كمؤشّر دالّة إلى الأصليّ عبر P/Invoke، تُستخدم افتراضيّاً «اتّفاقية الاستدعاء الافتراضيّة للمنصّة»، وإن أردت التطابق صراحة فأضف UnmanagedFunctionPointerAttribute إلى نوع الـ delegate.17 على x64/ARM/ARM64 الاتّفاقية عمليّاً واحدة فلا ضرر غالباً إن لم تنتبه، لكن على Windows x86 (32bit) تختلف Stdcall (افتراضيّ Win32 API) عن Cdecl (شائعة في مكتبات C ذات أصل Unix)، فإن استخدمت ترويسة الطرف Cdecl وبقيت على الافتراضيّ فسد المكدّس.17
// 呼び出し規約を明示する。x86ビルドで相手がCdeclを使っている場合はここが必須
[UnmanagedFunctionPointer(CallingConvention.Cdecl)]
private delegate void MyCallback(int code);
private static readonly MyCallback s_callback = OnNativeEvent; // static保持で寿命を確定させる
// [UnmanagedFunctionPointer]はコールバックが「呼ばれる」ときの規約であり、
// この呼び出し自体(RegisterCallbackというP/Invoke)の規約は別物。
// LibraryImportの既定はプラットフォーム既定(Windowsではstdcall相当)なので、
// 相手がCdeclのC DLLならこちらにも明示が必要
[LibraryImport("nativelib")]
[UnmanagedCallConv(CallConvs = new[] { typeof(CallConvCdecl) })]
internal static partial void RegisterCallback(MyCallback callback);
private static void OnNativeEvent(int code)
{
// ...
}
// 呼び出し側
RegisterCallback(s_callback);
GC.KeepAlive(s_callback); // 直後にスコープを外れる可能性がある変数を明示的に生かす
إن احتفظت بحقل static لم يُجمع طوال عمر التطبيق. وإن تيقّنت أنّ الجانب الأصليّ لا يستخدم الـ callback إلا أثناء استدعاء واحد (يتخلّص من مؤشّر الدالّة عند عودة الـ callback)، يمكن إطالة العمر بمتغيّر محلّيّ + GC.KeepAlive كتابة أخفّ.
أفضل الممارسات الرسميّة توصي، إن أمكن، باستخدام دالّة ساكنة عليها UnmanagedCallersOnlyAttribute ومؤشّر دالّة (delegate*<...>) بدل نوع Delegate. الكلفة أقلّ من ترحيل الـ delegate، والتوافق مع Native AOT أعلى.10
9. اختلافات 32bit/64bit
بكتابة توقيع P/Invoke واحد يُستخدم مسار الشيفرة نفسه من عملية 32bit ومن عملية 64bit وقت التشغيل. ما يسهل أن يصير مشكلة هنا أنّ عرض النوع في الجانب الأصليّ يتبع عدد بت العملية.
- أنواع المؤشّرات مثل
HANDLEوHWNDوLPARAM4 بايتات في عملية 32bit و8 بايتات في عملية 64bit. في جانب .NET الاستقبال الصحيح بـIntPtr/UIntPtr(أوnint/nuint)، والاستقبال بـint/longثابت الحجم يجعل الشيفرة تعمل على 32bit أو 64bit فقط.6 - إن تضمّنت البنية حقول مؤشّر كهذه، تغيّر حجم البنية كلّها حسب عدد البتّ. مع اختلاف افتراضيّ
Packحسب المعماريّة في الفصل 7، اختبر على افتراض أنّ نفس تعريف البنية قد يتغيّر تخطيطه الثنائيّ بين بناء 32bit وبناء 64bit. - متطلّب «أريد من تطبيق 32bit قائم استخدام وظيفة DLL لا تعمل إلا بـ 64bit» نفسه لا يُحلّ بـ P/Invoke (لا تتعايش DLL بعدد بت مختلف في العملية نفسها). عندئذ افصل العملية وابنِ جسراً بـ COM أو أنبوب مسمّى. المثال في «دراسة حالة جسر COM لاستدعاء 64bit DLL من تطبيق 32bit».
- أن لا تُوجد DLL أصلاً أو يُحمَّل إصدار غير مقصود ليس حديث P/Invoke بل حديث محمّل Windows. رتّبنا ترتيب البحث وسلوك SxS في «آليّة تحليل أسماء DLL في Windows»، فارجع إليه عند تحقيق سبب
DllNotFoundException.
10. جدول القرار ── P/Invoke مقابل غلاف C++/CLI مقابل التشغيل التبادليّ عبر COM
وسائل استدعاء الشيفرة الأصليّة من C# ليست P/Invoke وحدها. إن كان الطرف DLL معقّداً بأصناف C++ وملكيّة واستثناءات نفع غلاف C++/CLI، وإن عبرت حدود العملية (جسر 32/64bit، الاستخدام من VBA ولغات أخرى) صار COM خياراً.
| المحور | P/Invoke (LibraryImport) | غلاف C++/CLI | التشغيل التبادليّ عبر COM |
|---|---|---|---|
| يناسبه | واجهة C صريحة (محورها البنى والأنواع الأوّليّة) | DLL تتداخل فيه أصناف C++ والملكيّة والاستثناءات وأنواع std:: |
طرف يعبر العملية، أو لغات أخرى مثل VBA |
| تكلفة التنفيذ | منخفضة إلى متوسّطة (تعريف التوقيع فقط) | متوسّطة (طبقة غلاف إضافيّة) | مرتفعة (تصميم الواجهة وتسجيل السجلّ) |
| أمان الأنواع | متوسّط (يدويّاً قد لا يظهر خطأ التوقيع إلا وقت التشغيل. يتحسّن بـ CsWin32) | مرتفع (تعامل مع أنواع C++ كما هي) | متوسّط (يضمنه IDL/مكتبة الأنواع) |
| توافق AOT/التقليم | ممتاز (إن كان LibraryImport) | محدود (C++/CLI لا يدعم Native AOT) | محدود |
| معالجة الاستثناءات | ضعيف (حكم ذاتيّ بقيمة الإرجاع أو HRESULT) | ممتاز (يمكن تحويل استثناء C++ إلى استثناء .NET) | جيّد (يُحوَّل HRESULT إلى استثناء COM) |
| عبور حدود العملية | ضعيف (داخل العملية نفسها فقط) | ضعيف (داخل العملية نفسها فقط) | ممتاز (خادم خارج العملية ممكن) |
| سهولة التصحيح | جيّد (LibraryImport يمكن تنفيذ الشيفرة المولَّدة خطوة بخطوة) | جيّد (تصحيح الأصليّ والمُدار كليهما في VS) | محدود (صعب تتبّع عدّاد المراجع والتسجيل) |
| تكلفة التعلّم | منخفضة | متوسّطة إلى مرتفعة (صيغة C++/CLI) | مرتفعة (مواضعات COM عموماً) |
«إن كان الطرف Win32 API بدوالّ C، أو DLL بلغة C صريح للشركة» فـ P/Invoke (ومع CsWin32 إن أمكن)، «إن كان الطرف أصناف C++ وتريد تبادلاً طبيعيّاً يشمل الملكيّة والاستثناءات» فغلاف C++/CLI (التفصيل في «استدعاء native DLL من C#: غلاف C++/CLI مقابل P/Invoke»)، «إن عبرت العملية أصلاً أو أردت الاستخدام من VBA» فـ COM — بهذا الترتيب لا تحتار. إن رسمت هذا الترتيب تبيّن أنّ الحكم ينقسم عند السؤالين الأوّلين فقط.
flowchart TD
Q0{"ما اتّجاه الاستدعاء؟"}
Q0 -->|"أريد استدعاء معالجة C# من C/C++"| QR{"ما شكل .NET<br/>المستدعى؟"}
QR -->|"أريد callback<br/>إلى .NET العامل"| CB["مرّر delegate أو مؤشّر دالّة<br/>(الفصل 8) يكفي وقت التشغيل العاديّ"]
QR -->|"أريد التصدير<br/>كـ DLL أصليّ"| AOT["Native AOT + UnmanagedCallersOnly<br/>(ليس P/Invoke)"]
Q0 -->|"أريد استدعاء الأصليّ من C#"| QC{"هل الطرف ينشر COM<br/>بالفعل؟"}
QC -->|"ينشر (In-proc / Out-of-proc)"| COM["التشغيل التبادليّ عبر COM"]
QC -->|"لا"| Q1{"هل يكتمل داخل العملية نفسها؟"}
Q1 -->|"يعبر العملية"| QI{"هل يطلب الطرف COM<br/>(استخدام من VBA مثلاً)؟"}
QI -->|"يطلب"| COM2["أعدّ بنفسك خادم COM<br/>خارج العملية"]
QI -->|"لا يطلب"| IPC["ابنِ جسراً بـ IPC قائم<br/>كأنبوب مسمّى أو مقبس أو RPC (الفصل 9)"]
Q1 -->|"تكفي العملية نفسها"| Q2{"شكل واجهة الطرف"}
Q2 -->|"محورها دوالّ C وبنى"| PI["P/Invoke (LibraryImport،<br/>ومع Win32 API فـ CsWin32)"]
Q2 -->|"أصناف C++ وملكيّة واستثناءات"| CLI["غلاف C++/CLI<br/>(Native AOT غير متاح)"]
الشكل 2: الثلاثة تقسيم عمل لا تنافس. إن كان الطرف ينشر COM بالفعل فـ COM مدخل صريح حتّى داخل العملية نفسها، وإن كان الفصل مجرّد فصل عمليات فـ IPC القائم يكفي دون COM.
الاتّجاه العكسيّ (أريد استدعاء معالجة C# من C/C++) ينقسم أيضاً إلى اثنين. إن أردت فقط callback من الجانب الأصليّ إلى عملية .NET تعمل بالفعل، يكفي تمرير delegate أو مؤشّر دالّة UnmanagedCallersOnly في الفصل 8، ويعمل بوقت التشغيل العاديّ. إن أردت تصدير معالجة C# كـ DLL أصليّ مستقلّ يحمّله طرف لا يعرف .NET، فالتكوين نشر Native AOT. للأخير راجع «طريقة استدعاء C# Native AOT DLL من C/C++».
11. مثال تنفيذ ── التعامل مع المقابض ومعالجة الأخطاء عبر LibraryImport
مثال يجمع ما سبق. نغلّف OpenDevice / CloseDevice / ReadDeviceData التي ينشرها SDK وهميّ لجهاز استشعار device.dll، مع إدارة المقبض بـ SafeHandle، وترحيل وقت الترجمة بـ LibraryImport، ومعالجة الأخطاء بـ SetLastError + GetLastPInvokeError.
أوّلاً صنف مشتقّ من SafeHandle يحمل المقبض الأصليّ.
using Microsoft.Win32.SafeHandles;
// device.dll のハンドルをラップする。GCの寿命とは独立して、
// ハンドルの二重解放・リサイクル攻撃・早期解放を防ぐ
internal sealed class DeviceSafeHandle : SafeHandleZeroOrMinusOneIsInvalid
{
// OpenDevice の戻り値として使われるため、パラメーターなしコンストラクターが必要
public DeviceSafeHandle() : base(ownsHandle: true)
{
}
protected override bool ReleaseHandle()
// ReleaseHandle内は「失敗しない」ことが前提の制約実行領域。
// 単純なネイティブ解放呼び出し1つに留める
=> DeviceNativeMethods.CloseDevice(handle);
}
ثمّ تصريح P/Invoke. صرِّح بالسلاسل بـ StringMarshalling.Utf16، وأضف SetLastError = true لكلّ استدعاء قد يفشل.
using System.Runtime.InteropServices;
internal static partial class DeviceNativeMethods
{
private const string DeviceDll = "device.dll";
// ハンドルを戻り値にすると、呼び出しに成功した瞬間からSafeHandleが
// 寿命を追跡してくれる。失敗時はIsInvalidがtrueのハンドルが返る
[LibraryImport(DeviceDll, EntryPoint = "OpenDevice",
StringMarshalling = StringMarshalling.Utf16, SetLastError = true)]
internal static partial DeviceSafeHandle OpenDevice(string devicePath);
// SafeHandleのReleaseHandleから直接呼ぶための内部API。
// handle は解放専用なので生のIntPtrで受ける
[LibraryImport(DeviceDll, EntryPoint = "CloseDevice", SetLastError = true)]
[return: MarshalAs(UnmanagedType.Bool)]
internal static partial bool CloseDevice(IntPtr handle);
// buffer は呼び出し元が確保済みの配列。byte[]はブリッタブルなためピン留めされ、
// ネイティブ側の書き込みは同じメモリに対して行われる。[Out]を明示すること自体は
// 必須ではないが、意図を自己文書化するために付けておく
[LibraryImport(DeviceDll, EntryPoint = "ReadDeviceData", SetLastError = true)]
[return: MarshalAs(UnmanagedType.Bool)]
internal static partial bool ReadDeviceData(
DeviceSafeHandle handle,
[Out] byte[] buffer,
int bufferLength,
out int bytesRead);
}
أخيراً غلاف رفيع لجانب الاستخدام. يُلتقط رمز الخطأ فور اكتشاف الفشل ويُغلَّف في Win32Exception إلى المستدعي.
using System.ComponentModel;
using System.Runtime.InteropServices;
public sealed class DeviceConnection : IDisposable
{
private readonly DeviceSafeHandle _handle;
private DeviceConnection(DeviceSafeHandle handle) => _handle = handle;
public static DeviceConnection Open(string devicePath)
{
DeviceSafeHandle handle = DeviceNativeMethods.OpenDevice(devicePath);
if (handle.IsInvalid)
{
// التقطه فور الفشل، قبل أن تطمسه استدعاءات API أخرى
int error = Marshal.GetLastPInvokeError();
handle.Dispose();
throw new IOException(
$"تعذّر فتح الجهاز: {devicePath} (Win32 error {error})",
new Win32Exception(error));
}
return new DeviceConnection(handle);
}
public byte[] Read(int maxBytes)
{
var buffer = new byte[maxBytes];
if (!DeviceNativeMethods.ReadDeviceData(_handle, buffer, buffer.Length, out int bytesRead))
{
int error = Marshal.GetLastPInvokeError();
throw new IOException($"فشل قراءة الجهاز (Win32 error {error})",
new Win32Exception(error));
}
return bytesRead == buffer.Length ? buffer : buffer[..bytesRead];
}
// يكفي استدعاء SafeHandle.Dispose ولا تكتب finalizer
public void Dispose() => _handle.Dispose();
}
يكفي جانب استخدام DeviceConnection أن يحيطه بـ using، دون القلق من نسيان تحرير المقبض. مبدأ أين يُكتشف ماذا وكيف يُحوَّل في هذا التشكيل هو نفسه توزيع المسؤوليّات حسب الطبقة في «أين يُوضع catch والسجلّ في معالجة الاستثناءات». النقطة أن تترجم رموز خطأ الطبقة الأصليّة إلى استثناءات عند حدّ P/Invoke، وتُعامل فوق ذلك كاستثناءات .NET عاديّة.
12. الخلاصة
P/Invoke تقنيّة يقع فيها المرء مرّة على الأقلّ في مشكلة، في ترحيل السلاسل أو عمر المقبض أو توقيت التقاط رمز الخطأ أو تخطيط البنية، خلف سهولة «صرِّح بدالّة DLL فتستدعيها». من .NET 7 فما بعد اجعل LibraryImport الافتراضيّ، وإن أمكن دَع CsWin32 يولّد التوقيع نفسه. صرِّح بالسلاسل بـ StringMarshalling وتجنّب StringBuilder. احتفظ بالمقابض في SafeHandle. إن استخدمت SetLastError فالتقط رمز الخطأ فور الاستدعاء. راعِ أنّ افتراضيّ Pack في البنية يختلف حسب المعماريّة. أدِر عمر الـ callback صراحة — النقاط في هذه المقالة كلّها من نوع «يكفيها بضعة أسطر إن عرفتها، وتتحوّل إلى عطل لا يُعاد إلا في الإنتاج إن جهلتها».
أما الحكم بين المضيّ بـ P/Invoke أو التحوّل إلى غلاف C++/CLI أو COM فيُحدَّد بمدى «طابع C» لـ DLL الطرف، وبضرورة عبور حدود العملية. الاستشارة حول استدعاء أصل أصليّ قائم من C#، أو العكس استدعاء أصل C# من شيفرة أصليّة، غالباً لا يتّضح فيها التشكيل الأمثل إلا بالنظر إلى ملفّات الترويسة وبنية DLL الفعليّة، فلا تتردّدوا عند الحيرة.
مقالات ذات صلة
- استدعاء native DLL من C#: غلاف C++/CLI مقابل P/Invoke
- طريقة استدعاء C# Native AOT DLL من C/C++
- كيفية استدعاء DLL بنظام 64 بت من تطبيق 32 بت - دراسة حالة عملية لجسر COM
- آليّة تحليل أسماء DLL في Windows - ترتيب البحث وSxS
مجالات الاستشارة ذات الصلة
تتعامل شركة كومورا سوفت ذ.م.م. مع تصميم الحدّ بين C# وDLL الأصليّة / Win32 API، وتطوير مكوّنات COM والتحقيق فيها، والاستشارة التقنيّة في مشاريع ترحيل تربط الأصول الأصليّة القائمة بـ .NET.
روابط مرجعية
-
Microsoft Learn, Source generation for platform invokes. حول توليد الترحيل وقت الترجمة عبر LibraryImportAttribute، والفرق عن توليد IL stub وقت التشغيل الخاصّ بـ DllImport، والتوافق مع Native AOT/التقليم. ↩ ↩2 ↩3 ↩4 ↩5 ↩6
-
Microsoft Learn, Native interoperability best practices - Blittable types. حول تعريف الأنواع blittable، ومطبّ أنّ bool ليس blittable، وميزة استخدام sizeof() مع البنى blittable. ↩ ↩2
-
Microsoft Learn, SYSLIB diagnostics for p/invoke source generation. حول قائمة معرّفات التشخيص بما فيها المحلّل SYSLIB1054 الذي يحثّ على إعادة الكتابة من DllImport إلى LibraryImport. ↩
-
Microsoft Learn, SafeHandle Class. حول آليّة منع SafeHandle للتحرير المبكّر للمقبض وهجوم إعادة التدوير، وضمان التحرير المؤكَّد عبر CriticalFinalizerObject. ↩ ↩2 ↩3
-
Microsoft Learn, Native interoperability best practices - General guidance. حول التوجيه باستخدام SafeHandle لإدارة عمر الموارد غير المُدارة وتجنّب استخدام finalizer. ↩ ↩2
-
Microsoft Learn, Native interoperability best practices. حول عدم كفاءة ترحيل StringBuilder لاستلزامه نسخ المخزن الأصليّ دائماً، وضرورة تجنّب معامل [Out] string، واستخدام SafeHandle وتجنّب finalizer. ↩ ↩2 ↩3 ↩4
-
Microsoft Learn, Marshal.GetLastPInvokeError Method. حول طريقة الحصول على رمز الخطأ فور استدعاء P/Invoke المضبوط بـ SetLastError=true، وكونه موصى به أكثر من GetLastWin32Error ابتداءً من .NET 6. ↩ ↩2
-
Microsoft Learn, Build a C# .NET app with WinUI 3 and Win32 interop. حول إدخال C#/Win32 P/Invoke Source Generator (Microsoft.Windows.CsWin32)، وإجراء توليد التوقيعات بسرد أسماء الدوالّ في NativeMethods.txt. ↩ ↩2
-
Microsoft Learn, StructLayoutAttribute.Pack Field. حول معنى القيمة الافتراضيّة 0 لـ Pack («حجم التعبئة الافتراضيّ للمنصّة الحاليّة») وقواعد حساب محاذاة الحقول. ↩ ↩2 ↩3 ↩4 ↩5 ↩6 ↩7
-
Microsoft Learn, Native interoperability best practices - Prevent delegate collection with GC.KeepAlive. حول عدم تتبّع GC للعلاقة بين مؤشّر الدالّة المستحصل عبر GetFunctionPointerForDelegate والـ delegate، وإطالة العمر عبر GC.KeepAlive، والتوصية باستخدام UnmanagedCallersOnly. ↩ ↩2 ↩3
-
Microsoft Learn, Default Marshalling Behavior - Memory management with the interop marshaller. حول محاولة الترحيل الدائمة لتحرير الذاكرة التي خصّصتها الشيفرة غير المُدارة، وضرورة استخدام IntPtr والتحرير اليدويّ للذاكرة المخصَّصة بغير CoTaskMemAlloc لأنّ Windows تستخدم CoTaskMemFree. ↩
-
Microsoft Learn, Source generation for platform invokes - Differences from DllImport. حول استبدال CharSet بـ StringMarshalling، واستخدام UnmanagedCallConvAttribute بدل CallingConvention، وعدم وجود مقابل لـ ExactSpelling/PreserveSig. ↩ ↩2 ↩3
-
وثائق CsWin32 الرسميّة, Getting Started. حول إمكان كتابة اسم الدالّة أو النوع أو الثابت أو مساحة الأسماء أو الوحدة في كلّ سطر من NativeMethods.txt (البادئة
-استبعاد)، وتغيير الإعداد بـ NativeMethods.json في جذر المشروع، وأنّ تعطيل allowMarshaling يجعل التوليد لا يعتمد على مرحّل وقت التشغيل، وأنّ تحديد$schemaإلى https://aka.ms/CsWin32.schema.json يفعّل الإكمال والشرح والتحقّق في محرّر JSON وقائمة عناصر الإعداد هناك. ↩ ↩2 ↩3 -
Microsoft Learn, Charsets and marshalling. حول إسناد مترجمات C# وVisual Basic وF# افتراضيّاً CharSet.None عند عدم تحديد CharSet صراحة، وكون CharSet.None بنفس سلوك CharSet.Ansi (الترحيل بغير Unicode). ↩
-
Microsoft Learn, DllImportAttribute.SetLastError Field. حول سلوك ضبط SetLastError على true في .NET (مسح معلومات الخطأ عند كلّ استدعاء). ↩ ↩2
-
Microsoft Learn, /Zp (Struct Member Alignment). حول كون القيمة الافتراضيّة لمحاذاة أعضاء البنية في مترجم C++ حدّ 8 بايتات على x86/ARM/ARM64، وحدّ 16 بايتاً على x64/ARM64EC. ↩
-
Microsoft Learn, Unmanaged calling conventions. حول اختلاف اتّفاقية الاستدعاء الافتراضيّة بين Stdcall وCdecl في Windows x86، وكونها واحدة عمليّاً في x64/ARM/ARM64، وإمكان تحديد اتّفاقية الاستدعاء صراحة عبر UnmanagedFunctionPointerAttribute. ↩ ↩2
مقالات ذات صلة
أحدث المقالات التي تشترك في نفس الوسوم. عمّق فهمك بمواضيع مرتبطة.
هل تعمل تطبيقات الأعمال على Windows بإصدار Arm؟ ── واقع محاكاة x64 (Prism) ومكتبات DLL وCOM الأصليّة
نجيب المطوّرين ومسؤولي الأنظمة عن سؤال «هل تعمل تطبيقات الأعمال على Windows بإصدار Arm؟». نستعرض آليّة محاكاة x64 (Prism)، والطبقات التي ...
عندما يُعامَل تطبيق Windows الذي طوّرته شركتك بوصفه فيروساً ── التعامل مع الكشف الخاطئ في Microsoft Defender والتعايش مع أثره على الأداء
نرتّب الإجراء الرسميّ عند كشف Microsoft Defender تطبيق Windows الذي طوّرته شركتك خطأً. نشرح آليّة برامج مكافحة الفيروسات الحديثة، والإبلا...
السكون والإسبات وModern Standby والتطبيقات طويلة التشغيل ── منع «التوقّف في منتصف الليل» بالتصميم
نرتّب أسباب وصول تطبيقات Windows طويلة التشغيل إلى حالة «توقّفت عندما نظرت صباحاً»، انطلاقاً من الفرق بين سكون S3 والإسبات وModern Standb...
MAX_PATH ومطبّات المسارات وأسماء الملفّات في Windows ── حدّ 260 حرفاً، الأسماء المحجوزة، النقطة الأخيرة، حساسيّة الأحرف
ننظّم مشكلات حدود المسارات وأسماء الملفّات، وهي سبب شائع لظهور «الملفّ غير موجود». نشرح تفاصيل MAX_PATH=260 حرفاً، وتفعيل المسارات الطويل...
مطبّات محرّك الشبكة ومسار UNC ── التعامل العملي مع خادم الملفّات (المجلّد المشترك) في تطبيقات الأعمال
نرتّب الأعطال الشائعة عند الكتابة إلى مجلّد مشترك أو مراقبته من تطبيق أعمال. نشرح سبب عدم ظهور حرف القرص (Z:) من الخدمة، والصلاحيّات المط...
أين يتصل هذا الموضوع
ترتبط هذه المقالة بشكل طبيعي بصفحات الخدمات التالية.
تطوير تطبيقات ويندوز
ندعم تطوير برامج ويندوز للأعمال، وتكامل الأجهزة، وأدوات التواصل.
الأسئلة الشائعة
أسئلة شائعة حول موضوع هذه المقالة.
- هل ينبغي استخدام DllImport أم LibraryImport؟
- ابتداءً من .NET 7 فما بعده، اجعل LibraryImport الخيار الافتراضيّ. بينما ينشئ DllImport في وقت التشغيل IL stub لأغراض الترحيل، ينشئ LibraryImport شيفرة الترحيل عبر مولّد مصدر في وقت الترجمة، ما يجعله متوافقاً مع Native AOT والتقليم، ويتيح تنفيذ الشيفرة المولَّدة خطوة بخطوة في المصحّح. المحلّل SYSLIB1054 يشير إلى المواضع التي ينبغي إعادة كتابتها من DllImport. لا يُعاد استخدام DllImport إلا عند الاعتماد على إعدادات لا يدعمها LibraryImport (كبعض تحديدات MarshalAs).
- لماذا يكون الاحتفاظ بالمقبض عبر IntPtr في P/Invoke خطراً؟
- بسبب وجود ثلاث مشكلات. أوّلاً، قد يحدث تنافس يؤدّي إلى تحرير مبكّر، إذ يستطيع GC تجميع الكائن وإغلاق المقبض أثناء تنفيذ استدعاء P/Invoke. ثانياً، تعيد Windows استخدام قيم المقابض بفاعليّة، ما يفضي إلى هجوم إعادة تدوير يؤدّي إلى التعامل مع موارد غير ذات صلة عبر قيمة مقبض كان يُفترض إغلاقها. ثالثاً، قد يحدث تسرّب في المقبض بسبب استثناءات غير متزامنة. استخدام صنف مشتقّ من SafeHandle يمنع هذه المشكلات عبر الإدارة التلقائيّة لعدّاد المراجع وضمان التحرير المؤكَّد.
- هل توجد طريقة لتجنّب كتابة توقيعات Win32 API يدويّاً؟
- يمكن استخدام مولّد المصدر CsWin32 (Microsoft.Windows.CsWin32). بعد إضافة حزمة NuGet، يكفي سرد أسماء الدوالّ المطلوبة في ملفّ نصّيّ باسم NativeMethods.txt، فيولّد التوقيعات والثوابت والبنى تلقائيّاً من بيانات Win32 الرسميّة. يظهر HANDLE كنوع مشتقّ من SafeHandle مناسب، ما يمنع أخطاء مثل الخلط في CharSet أو ترتيب الحقول الشائعة عند الكتابة اليدويّة. القيمة الافتراضيّة هي التوليد المبنيّ على DllImport، لذا إن كنت تستهدف Native AOT فحدّد allowMarshaling: false في NativeMethods.json.
- كيف نفرّق الاستخدام بين P/Invoke وغلاف C++/CLI وCOM؟
- يُحدَّد ذلك حسب طبيعة الـ DLL المقابل ووجود حدود عملية أم لا. إن كان واجهة C بسيطة (محورها البنى والأنواع الأوّليّة)، فـ P/Invoke هو الأقلّ تكلفة، ومع Win32 API يكون استخدام CsWin32 معه فعّالاً. إن كان DLL معقّداً تتداخل فيه أصناف C++ والملكيّة والاستثناءات وأنواع std::، فإدراج غلاف C++/CLI واحد هو الحلّ. أمّا عند تجاوز حدود العملية كجسر 32bit/64bit، أو عند الاستخدام من لغات أخرى مثل VBA، فـ COM خيار مطروح. لا يمكن لـ DLL بعددَي بت مختلفَين التعايش في العملية نفسها، وهذا المتطلّب لا يمكن حلّه عبر P/Invoke.
الملف الشخصي للمؤلف
صفحة الملف الشخصي لمؤلف المقالة.
غو كومورا
مؤسّس شركة كومورا سوفت ذ.م.م.
يركّز على تطوير برامج ويندوز، والاستشارات التقنية، والتحقيق في الأخطاء، ويتميّز في المشاريع التي تبقى فيها الأصول القديمة ناشطة، وفي تشخيص الأعطال التي يصعب تحديد سببها.