Bagikan melalui


Memanggil Fungsi Asli dari Kode Terkelola

Runtime bahasa umum menyediakan Platform Invocation Services, atau PInvoke, yang memungkinkan kode terkelola untuk memanggil fungsi gaya C di pustaka (DLL) asli yang ditautkan secara dinamis. Marshaling data yang sama digunakan untuk interoperabilitas COM dengan runtime dan untuk mekanisme "It Just Works," atau IJW.

Untuk informasi selengkapnya, lihat:

Sampel di bagian ini hanya menggambarkan bagaimana PInvoke dapat digunakan. PInvoke dapat menyederhanakan marshaling data yang disesuaikan karena Anda memberikan informasi marshaling secara deklaratif dalam atribut alih-alih menulis kode marshaling prosedural.

Catatan

Pustaka marshaling menyediakan cara alternatif untuk marshal data antara lingkungan asli dan terkelola dengan cara yang dioptimalkan. Lihat Gambaran Umum Marshaling di C++ untuk informasi selengkapnya tentang pustaka marshaling. Pustaka marshaling hanya dapat digunakan untuk data, dan bukan untuk fungsi.

PInvoke dan Atribut DllImport

Contoh berikut menunjukkan penggunaan PInvoke dalam program Visual C++. Fungsi asli didefinisikan dalam msvcrt.dll. Atribut DllImport digunakan untuk deklarasi put.

// 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);
}

Sampel berikut setara dengan sampel sebelumnya, tetapi menggunakan 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);
}

Keuntungan IJW

  • Tidak perlu menulis DLLImport deklarasi atribut untuk API yang tidak dikelola yang digunakan program. Cukup sertakan file header dan tautan dengan pustaka impor.

  • Mekanisme IJW sedikit lebih cepat (misalnya, stub IJW tidak perlu memeriksa kebutuhan untuk menyematkan atau menyalin item data karena itu dilakukan secara eksplisit oleh pengembang).

  • Ini dengan jelas menggambarkan masalah performa. Dalam hal ini, fakta bahwa Anda menerjemahkan dari string Unicode ke string ANSI dan bahwa Anda memiliki alokasi dan dealokasi memori penjawab. Dalam hal ini, pengembang yang menulis kode menggunakan IJW akan menyadari bahwa panggilan _putws dan penggunaan PtrToStringChars akan lebih baik untuk performa.

  • Jika Anda memanggil banyak API yang tidak dikelola menggunakan data yang sama, melakukan marshal sekali dan melewati salinan marshal jauh lebih efisien daripada melakukan marshaling ulang setiap saat.

Kerugian IJW

  • Marshaling harus ditentukan secara eksplisit dalam kode alih-alih oleh atribut (yang sering memiliki default yang sesuai).

  • Kode marshaling sebaris, di mana lebih invasif dalam alur logika aplikasi.

  • Karena API marshaling eksplisit mengembalikan IntPtr jenis untuk portabilitas 32-bit ke 64-bit, Anda harus menggunakan panggilan tambahan ToPointer .

Metode khusus yang diekspos oleh C++ adalah metode yang lebih efisien dan eksplisit, dengan biaya beberapa kompleksitas tambahan.

Jika aplikasi menggunakan jenis data yang terutama tidak dikelola atau jika memanggil API yang lebih tidak dikelola daripada API .NET Framework, kami sarankan Anda menggunakan fitur IJW. Untuk memanggil API yang tidak dikelola sesekali dalam aplikasi yang sebagian besar dikelola, pilihannya lebih halus.

PInvoke dengan API Windows

PInvoke nyaman untuk memanggil fungsi di Windows.

Dalam contoh ini, program Visual C++ melakukan interoperaksi dengan fungsi MessageBox yang merupakan bagian dari API Win32.

// 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);
}

Output adalah kotak pesan yang memiliki judul PInvoke Test dan berisi teks Halo Dunia!.

Informasi marshaling juga digunakan oleh PInvoke untuk mencari fungsi di DLL. Dalam user32.dll sebenarnya tidak ada fungsi MessageBox, tetapi CharSet=CharSet::Ansi memungkinkan PInvoke untuk menggunakan MessageBoxA, versi ANSI, bukan MessageBoxW, yang merupakan versi Unicode. Secara umum, kami sarankan Anda menggunakan versi Unicode dari API yang tidak dikelola karena menghilangkan overhead terjemahan dari format Unicode asli objek string .NET Framework ke ANSI.

Kapan Tidak Menggunakan PInvoke

Menggunakan PInvoke tidak sesuai untuk semua fungsi gaya C di DLL. Misalnya, ada fungsi MakeSpecial dalam mylib.dll dinyatakan sebagai berikut:

char * MakeSpecial(char * pszString);

Jika kita menggunakan PInvoke dalam aplikasi Visual C++, kita mungkin menulis sesuatu yang mirip dengan yang berikut ini:

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

Kesulitan di sini adalah bahwa kita tidak dapat menghapus memori untuk string yang tidak dikelola yang dikembalikan oleh MakeSpecial. Fungsi lain yang dipanggil melalui PInvoke mengembalikan pointer ke buffer internal yang tidak harus dibatalkan alokasinya oleh pengguna. Dalam hal ini, menggunakan fitur IJW adalah pilihan yang jelas.

Batasan PInvoke

Anda tidak dapat mengembalikan penunjuk persis yang sama dari fungsi asli yang Anda ambil sebagai parameter. Jika fungsi asli mengembalikan penunjuk yang telah dirusak oleh PInvoke, kerusakan memori dan pengecualian mungkin terjadi.

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

Sampel berikut menunjukkan masalah ini, dan meskipun program mungkin tampaknya memberikan output yang benar, output berasal dari memori yang telah dibebaskan.

// 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);
}

Argumen Marshaling

Dengan PInvoke, tidak ada marshaling yang diperlukan antara jenis primitif terkelola dan C++ asli dengan bentuk yang sama. Misalnya, tidak ada marshaling yang diperlukan antara Int32 dan int, atau antara Double dan double.

Namun, Anda harus jenis marshal yang tidak memiliki bentuk yang sama. Ini termasuk jenis karakter, string, dan struct. Tabel berikut ini memperlihatkan pemetaan yang digunakan oleh marshaler untuk berbagai jenis:

wtypes.h Visual C++ Visual C++ dengan /clr Runtime bahasa umum
MENANGANI kosong* kosong* IntPtr, UIntPtr
BYTE char yang tidak bertanda char yang tidak bertanda Byte
PENDEK pendek pendek Int16
WORD short tidak bertanda short tidak bertanda UInt16
INT int int Int32
UINT int tidak bertanda int tidak bertanda UInt32
LONG long long Int32
BOOL long bool Boolean
DWORD panjang tidak ditandatangani panjang tidak ditandatangani UInt32
ULONG panjang tidak ditandatangani panjang tidak ditandatangani UInt32
CHAR char char Char
LPSTR Char* String ^ [in], StringBuilder ^ [in, out] String ^ [in], StringBuilder ^ [in, out]
LPCSTR karakter const * Tali^ String
LPWSTR wchar_t * String ^ [in], StringBuilder ^ [in, out] String ^ [in], StringBuilder ^ [in, out]
LPCWSTR const wchar_t* Tali^ String
FLOAT float float Tunggal
DOUBLE ganda ganda Laju

Marshaler secara otomatis menyematkan memori yang dialokasikan pada tumpukan runtime jika alamatnya diteruskan ke fungsi yang tidak dikelola. Penyematan mencegah pengumpul sampah memindahkan blok memori yang dialokasikan selama pemadatan.

Dalam contoh yang ditunjukkan sebelumnya dalam topik ini, parameter CharSet dllImport menentukan bagaimana String terkelola harus di-marshal; dalam hal ini, mereka harus dinakhuskan ke string ANSI untuk sisi asli.

Anda dapat menentukan informasi marshaling untuk argumen individual dari fungsi asli dengan menggunakan atribut MarshalAs. Ada beberapa pilihan untuk marsekal argumen String *: BStr, ANSIBStr, TBStr, LPStr, LPWStr, dan LPTStr. Defaultnya adalah LPStr.

Dalam contoh ini, string dinamai sebagai string karakter Unicode byte ganda, LPWStr. Outputnya adalah huruf pertama dari Halo Dunia! karena byte kedua dari string marshaled null, dan menempatkan menafsirkan ini sebagai penanda akhir string.

// 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);
}

Atribut MarshalAs ada di namespace Layanan System::Runtime::InteropServices. Atribut dapat digunakan dengan jenis data lain seperti array.

Seperti disebutkan sebelumnya dalam topik, pustaka marshaling menyediakan metode baru yang dioptimalkan untuk marshaling data antara lingkungan asli dan terkelola. Untuk informasi selengkapnya, lihat Gambaran Umum Marshaling di C++.

Pertimbangan Performa

PInvoke memiliki overhead antara 10 dan 30 x86 instruksi per panggilan. Selain biaya tetap ini, marshaling menciptakan overhead tambahan. Tidak ada biaya marshaling antara jenis blittable yang memiliki representasi yang sama dalam kode terkelola dan tidak terkelola. Misalnya, tidak ada biaya untuk diterjemahkan antara int dan Int32.

Untuk performa yang lebih baik, memiliki lebih sedikit panggilan PInvoke yang melakukan marshal sebanyak mungkin data, alih-alih lebih banyak panggilan yang marshal lebih sedikit data per panggilan.

Lihat juga

Interoperabilitas Native dan .NET