استدعاء الدالات الأصلية من التعليمات البرمجية المُدارة

وقت تشغيل اللغة العامة يوفر خدمات استدعاء للنظام الأساسي أو PInvoke تمكّن التعليمات البرمجية المُدارة من استدعاء دالات نمط C في مكتبات الارتباط الحيوي الأصلية (DLLs). تنظيم و إرسال البيانات نفسه يُستخدم لإمكانية التشغيل التفاعلى لـ COM مع وقت التشغيل، ومن أجل آلية "بالكاد يعمل" أو IJW ،.

لمزيد من المعلومات، راجع:

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

ملاحظة

توفر مكتبة التنظيم و الإرسال طريقة بديلة لتنظيم و إرسال البيانات بين البيئة الأصلية و المدارة بطريقة محسّنة. لمزيد من المعلومات حول مكتبة التنظيم و الإرسال، راجع نظرة عامة حول التعبئة في C++‎. مكتبة التنظيم و الإرسال قابلة لإعادة الاستخدام للبيانات فقط، و وليس للدالات.

سمة PInvoke و DllImport

يظهر المثال التالي استخدام PInvoke في برنامج Visual C++ . يضع دالة الأصلية هو في msvcrt.dll. سمة DllImport هو المستخدمة للتعريف puts.

// platform_invocation_services.cpp
// compile with: /clr
using namespace System;
using namespace System::Runtime::InteropServices;

[DllImport("msvcrt", CharSet=CharSet::Ansi)]
extern "C" int puts(String ^);

int main() {
   String ^ pStr = "Hello World!";
   puts(pStr);
}

يكافئ النموذج التالي النموذج السابق ولكن يستخدم IJW.

// platform_invocation_services_2.cpp
// compile with: /clr
using namespace System;
using namespace System::Runtime::InteropServices;

#include <stdio.h>

int main() {
   String ^ pStr = "Hello World!";
   char* pChars = (char*)Marshal::StringToHGlobalAnsi(pStr).ToPointer(); 
   puts(pChars);
   
   Marshal::FreeHGlobal((IntPtr)pChars);
}

فوائد IJW

  • ليس هناك حاجة لكتابة تعريفات سمة DLLImportللـ API الغير المدارة التى يستخدمها البرنامج. فقط قم بتضمين ملف الرأس و اربط بمكتبة الاستيراد.

  • آلية IJW أسرع بقليل (على سبيل المثال، كعوب الروتين IJW لا تحتاج إلى التحقق من الحاجة إلى تثبيت أو نسخ عناصر البيانات لأن هذا يتم صراحة بواسطة المطور).

  • توضح مشكلات الأداء. في هذه الحالة،حقيقة انك تقوم بالترجمة من سلسلة Unicode إلى سلسلة ANSI و أن عندك تخصيص و إلغاء تخصيص ذاكرة حاضر. في هذه الحالة، سيكتشف المطور الذى يكتب التعليمات البرمجية باستخدام IJW أن استدعاء_putwsواستخدامPtrToStringChars سيكون أفضل للأداء.

  • فى حالة استدعاء العديد من APIs الغير المُدارة باستخدام نفس البيانات، يكون تنظيمها و إرسالها مرة واحدة و تمرير النسخة التى تم تنظيمها و إرسالها أكثر فاعلية من إعادة التنظيم و الإرسال كل مرة.

مساوئ IJW

  • التنظيم و الإرسال يجب أن يكون محدداً بشكل صريح في التعليمات البرمجية بدلاً من أن يكون بواسطة السمات (التي غالبًا ما يكون لها افتراضيات مناسبة).

  • التعليمات البرمجية الخاصة بالتنظيم و الإرسال تكون مضمنة، حيث تكون اكثر اجتياحية في تدفق منطق التطبيق.

  • لأن التنظيم و الإرسال الصريح لـ APIs يقوم بإرجاع أنواع IntPtr لإمكانية نقل 32-بت إلى 64-بت, يجب عليك استخدام استدعاءات ToPointer إضافية.

الأسلوب المحدد الذى تم عرضه بواسطة C++ أكثر كفاءة مع كونه أسلوباً صريحاً و يتكلف بعض التعقيد الإضافى.

إذا كان التطبيق يستخدم أنواع البيانات غير المُدارة بشكل رئيسي أو إذا كان يقوم باستدعاء APIs غير مدارة أكثر من .NET Framework APIs ، فإننا ننصح باستخدام ميزة IJW. لاستدعاء API غير مدار نادراً في تطبيق مدار غالبًا، يكون هذا الاختيار أكثر دقة.

PInvoke مع Windows APIs

PInvoke يكون ملائماً لاستدعاء الدالات في Windows.

في هذا المثال، يتفاعل برنامج Visual C++ مع دالة MessageBox التى هى جزء من Win32 API.

// platform_invocation_services_4.cpp
// compile with: /clr /c
using namespace System;
using namespace System::Runtime::InteropServices;
typedef void* HWND;
[DllImport("user32", CharSet=CharSet::Ansi)]
extern "C" int MessageBox(HWND hWnd, String ^ pText, String ^ pCaption, unsigned int uType);

int main() {
   String ^ pText = "Hello World! ";
   String ^ pCaption = "PInvoke Test";
   MessageBox(0, pText, pCaption, 0);
}

الإخراج هو مربع رسالة له العنوان "PInvoke Test" و يحتوي النص " Hello World!".

يتم أيضاً استخدام معلومات التنظيم و الإرسال بواسطة PInvoke للبحث عن الدالات في DLL. في user32.dll لا يوجد في الواقع دالة MessageBox ولكن CharSet=CharSet::Ansi تمكّن PInvoke من استخدام MessageBoxA الإصدار ANSI، بدلاً من MessageBoxW الذى هو إصدار Unicode. بشكل عام، نوصيك باستخدام إصدارات Unicode لـ API غير المدارة لأنه ذلك يزيل التحميل الزائد للترجمة من تنسيق Unicode الأصلي لكائنات سلسلة .NET Framework إلى ANSI.

عندما لا تستخدم PInvoke

استخدام PInvoke غير مناسب لكافة دالات نمط C في DLL. على سبيل المثال، افترض أن هناك دالة MakeSpecial في mylib.dll تم التصريح بها كما يلي:

char * MakeSpecial(char * pszString);

إذا كنا تستخدم PInvoke في تطبيق Visual C++، فإننا قد نقوم بكتابة شيء مشابه لما يلي:

[DllImport("mylib")]

extern "C" String * MakeSpecial([MarshalAs(UnmanagedType::LPStr)] String ^);

الصعوبة هنا هى انه لا يمكن حذف الذاكرة الخاصة بسلسلة غير مدارة يتم إرجاعها من قبل MakeSpecial. الدالات الأخرى المستدعاة خلال PInvoke تقوم بإرجاع مؤشر إلى المخزن المؤقت الداخلي الذى لا يحتاج أن يزال تخصيصه من قبل المستخدم. في هذه الحالة، استخدام ميزة IJW هو الاختيار الواضح.

قيود PInvoke

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

__declspec(dllexport)
char* fstringA(char* param) {
   return param;
}

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

// platform_invocation_services_5.cpp
// compile with: /clr /c
using namespace System;
using namespace System::Runtime::InteropServices;
#include <limits.h>

ref struct MyPInvokeWrap {
public:
   [ DllImport("user32.dll", EntryPoint = "CharLower", CharSet = CharSet::Ansi) ]
   static String^ CharLower([In, Out] String ^);
};

int main() {
   String ^ strout = "AabCc";
   Console::WriteLine(strout);
   strout = MyPInvokeWrap::CharLower(strout);
   Console::WriteLine(strout);
}

وسيطات التنظيم و الإرسال

مع PInvoke ، التنظيم و الإرسال غير مطلوب بين الأنواع المدارة و أنواع C++ الأصلية مع نفس النموذج. على سبيل المثال، التنظيم و الإرسال غير مطلوب بين Int32 Int أو بين مزدوج و مزدوج.

و لكن، يجب تنظيم و إرسال الأنواع التي لها نفس النموذج. يتضمن هذا الحرف و السلسلة و أنواع البنية (struct). يعرض الجدول التالي التعيينات المستخدمة من قبل المنظم لأنواع متعددة :

wtypes.h

Visual C++‎

Visual C++ مع /clr

وقت تشغيل اللغة العامة

مؤشر (HANDLE)

void*

void*

IntPtr UIntPtr

بايت

unsigned char (حرف بدون إشارة)

unsigned char (حرف بدون إشارة)

Byte (بايت)

short

قصير

قصير

Int16

WORD

unsigned short

unsigned short

UInt16

عدد صحيح (INT)

عدد صحيح

عدد صحيح

Int32

UINT

unsigned int

unsigned int

UInt32

طويل (LONG)

طويل

طويل

Int32

BOOL

طويل

bool

قيمة منطقية

DWORD

unsigned long

unsigned long

UInt32

ULong

unsigned long

unsigned long

UInt32

Char

حرف

حرف

حرف

lpcstr

Char

String ^ [in], StringBuilder ^ [in, out]

String ^ [in], StringBuilder ^ [in, out]

lpcstr

const char *

String

سلسة نصية

lpwstr

wchar_t *

String ^ [in], StringBuilder ^ [in, out]

String ^ [in], StringBuilder ^ [in, out]

lpcwstr

const wchar_t *

String

سلسة نصية

FLOAT

حر

حر

مفرد

Double

مزدوج

مزدوج

مزدوج

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

في المثال الموضح مسبقاً في هذا الموضوع , تحدد المعلمة CharSet لـ DllImport كيفية تنظيم و إرسال سلاسل مدارة; في هذه الحالة، يجب أن يتم تنظيمهم و إرسالهم إلى سلاسل ANSI للجانب الأصلي.

يمكنك تحديد معلومات تنظيم و إرسال الوسائط الفردية للدالات الأصلية باستخدام السمة MarshalAs. هناك عدة اختيارات لتنظيم و إرسال String * argument : BStr ANSIBStr ، TBStr ، LPStr ، LPWStr و LPTStr. الافتراضى هو LPStr.

في هذا المثال، يتم تنظيم و إرسال السلسلة كسلسلة أحرف Unicode مزدوجة-البايت، LPWStr. الإخراج هو الحرف أول من "شبكة مرحبا"! ونظرا لأن البايت الثاني من السلسلة منظماً فارغة، و puts يفسر هذا كعلامة إنهاء السلسلة.

// platform_invocation_services_3.cpp
// compile with: /clr
using namespace System;
using namespace System::Runtime::InteropServices;

[DllImport("msvcrt", EntryPoint="puts")]
extern "C" int puts([MarshalAs(UnmanagedType::LPWStr)] String ^);

int main() {
   String ^ pStr = "Hello World!";
   puts(pStr);
}

السمة MarshalAs فى مساحة اسم System::Runtime::InteropServices. يمكن استخدام السمة مع أنواع البيانات الأخرى مثل الصفائف.

كما ذكر في الموضوع سابقاً، توفر مكتبة التنظيم و الإرسال أسلوباً جديداً محسناً لتنظيم و إرسال البيانات بين البيئة الأصلية و المدارة. لمزيد من المعلومات، راجع نظرة عامة حول التعبئة في C++‎.

اعتبارات الأداء

PInvoke له حمولة زائدة ما بين 10 و 30 تعليمة x 86 لكل استدعاء. بالإضافة إلى التكلفة الثابتة هذه ، التنظيم و الإرسال ينشئ حملاً إضافياً. لا توجد تكلفة تنظيم و إرسال بين الأنواع المشتركة التي لها نفس التمثيل في التعليمات البرمجية المدارة و الغير مدارة. على سبيل المثال،لا توجد تكلفة فى الترجمة بين Int و Int32.

للحصول على أداء أفضل، ليكن عندك أقل ما يمكن من استدعاءات PInvoke التى تنظم و ترسل أكبر قدر ممكن من البيانات، بدلاً من استدعاءات أكثر تنظم و ترسل بيانات أقل فى كل استدعاء.

راجع أيضًا:

موارد أخرى

إمكانية التشغيل التفاعلي الأصلي و الـ .NET