Wywołanie platformy (P/Invoke)

P/Invoke to technologia umożliwiająca dostęp do struktur, wywołań zwrotnych i funkcji w bibliotekach niezarządzanych z poziomu kodu zarządzanego. Większość interfejsu API P/Invoke znajduje się w dwóch przestrzeniach nazw: System i System.Runtime.InteropServices. Dzięki tym dwóm przestrzeniom nazw możesz opisać, jak chcesz komunikować się ze składnikiem natywnym.

Zacznijmy od najbardziej typowego przykładu, który wywołuje funkcje niezarządzane w kodzie zarządzanym. Pokażmy pole komunikatu z poziomu aplikacji wiersza polecenia:

using System;
using System.Runtime.InteropServices;

public class Program
{
    // Import user32.dll (containing the function we need) and define
    // the method corresponding to the native function.
    [DllImport("user32.dll", CharSet = CharSet.Unicode, SetLastError = true)]
    private static extern int MessageBox(IntPtr hWnd, string lpText, string lpCaption, uint uType);

    public static void Main(string[] args)
    {
        // Invoke the function as a regular managed method.
        MessageBox(IntPtr.Zero, "Command-line message box", "Attention!", 0);
    }
}

Poprzedni przykład jest prosty, ale pokazuje, co jest potrzebne do wywoływania funkcji niezarządzanych z kodu zarządzanego. Przyjrzyjmy się przykładowi:

  • Wiersz 2 zawiera instrukcję using dla System.Runtime.InteropServices przestrzeni nazw, która zawiera wszystkie potrzebne elementy.
  • Wiersz 8 wprowadza DllImport atrybut . Ten atrybut informuje środowisko uruchomieniowe, że powinno załadować niezarządzaną bibliotekę DLL. Przekazany ciąg jest biblioteką DLL, w których znajduje się nasza funkcja docelowa. Ponadto określa, który zestaw znaków ma być używany do marshalingu ciągów. Na koniec określa, że ta funkcja wywołuje metodę SetLastError i że środowisko uruchomieniowe powinno przechwycić ten kod błędu, aby użytkownik mógł go pobrać za pomocą polecenia Marshal.GetLastWin32Error().
  • Wiersz 9 jest cruxem pracy P/Invoke. Definiuje metodę zarządzaną, która ma dokładnie taki sam podpis jak niezarządzany. Deklaracja zawiera nowe słowo kluczowe, które można zauważyć, extern, który informuje środowisko uruchomieniowe, że jest to metoda zewnętrzna i że po wywołaniu go środowisko uruchomieniowe powinno znaleźć go w dll określonym w DllImport atrybucie.

Pozostała część przykładu polega na wywołaniu metody tak, jak w przypadku każdej innej metody zarządzanej.

Przykład jest podobny dla systemu macOS. Nazwa biblioteki w atrybucie DllImport musi ulec zmianie, ponieważ system macOS ma inny schemat nazewnictwa bibliotek dynamicznych. W poniższym przykładzie użyto getpid(2) funkcji , aby uzyskać identyfikator procesu aplikacji i wyświetlić ją w konsoli:

using System;
using System.Runtime.InteropServices;

namespace PInvokeSamples
{
    public static class Program
    {
        // Import the libSystem shared library and define the method
        // corresponding to the native function.
        [DllImport("libSystem.dylib")]
        private static extern int getpid();

        public static void Main(string[] args)
        {
            // Invoke the function and get the process ID.
            int pid = getpid();
            Console.WriteLine(pid);
        }
    }
}

Jest również podobny w systemie Linux. Nazwa funkcji jest taka sama, ponieważ getpid(2) jest standardowym wywołaniem systemu POSIX .

using System;
using System.Runtime.InteropServices;

namespace PInvokeSamples
{
    public static class Program
    {
        // Import the libc shared library and define the method
        // corresponding to the native function.
        [DllImport("libc.so.6")]
        private static extern int getpid();

        public static void Main(string[] args)
        {
            // Invoke the function and get the process ID.
            int pid = getpid();
            Console.WriteLine(pid);
        }
    }
}

Wywoływanie kodu zarządzanego z niezarządzanego kodu

Środowisko uruchomieniowe umożliwia komunikację w obu kierunkach, umożliwiając wywołanie z powrotem do kodu zarządzanego z funkcji natywnych przy użyciu wskaźników funkcji. Najbliżej wskaźnika funkcji w kodzie zarządzanym jest delegat, dlatego służy do zezwalania na wywołania zwrotne z kodu natywnego do kodu zarządzanego.

Sposób korzystania z tej funkcji jest podobny do opisanego wcześniej procesu natywnego zarządzanego. Dla danego wywołania zwrotnego należy zdefiniować delegata, który pasuje do podpisu i przekazać go do metody zewnętrznej. Środowisko uruchomieniowe zajmie się wszystkimi innymi elementami.

using System;
using System.Runtime.InteropServices;

namespace ConsoleApplication1
{
    public static class Program
    {
        // Define a delegate that corresponds to the unmanaged function.
        private delegate bool EnumWC(IntPtr hwnd, IntPtr lParam);

        // Import user32.dll (containing the function we need) and define
        // the method corresponding to the native function.
        [DllImport("user32.dll")]
        private static extern int EnumWindows(EnumWC lpEnumFunc, IntPtr lParam);

        // Define the implementation of the delegate; here, we simply output the window handle.
        private static bool OutputWindow(IntPtr hwnd, IntPtr lParam)
        {
            Console.WriteLine(hwnd.ToInt64());
            return true;
        }

        public static void Main(string[] args)
        {
            // Invoke the method; note the delegate as a first parameter.
            EnumWindows(OutputWindow, IntPtr.Zero);
        }
    }
}

Przed przejściem przez przykład warto przejrzeć podpisy funkcji niezarządzanych, z których należy pracować. Funkcja, która ma zostać wywołana w celu wyliczenia wszystkich okien, ma następujący podpis: BOOL EnumWindows (WNDENUMPROC lpEnumFunc, LPARAM lParam);

Pierwszy parametr to wywołanie zwrotne. Powiedział callback ma następujący podpis: BOOL CALLBACK EnumWindowsProc (HWND hwnd, LPARAM lParam);

Teraz przyjrzyjmy się przykładowi:

  • Wiersz 9 w przykładzie definiuje delegata pasujący do podpisu wywołania zwrotnego z niezarządzanego kodu. Zwróć uwagę, że typy LPARAM i HWND są reprezentowane przy użyciu IntPtr w kodzie zarządzanym.
  • Wiersze #13 i #14 wprowadzają EnumWindows funkcję z biblioteki user32.dll.
  • Wiersze nr 17–20 implementują delegata. W tym prostym przykładzie chcemy po prostu wyświetlić uchwyt do konsoli.
  • Na koniec, w wierszu 24, metoda zewnętrzna jest wywoływana i przekazywana do delegata.

Poniżej przedstawiono przykłady systemów Linux i macOS. W tym przypadku używamy ftw funkcji , która znajduje się w libcbibliotece języka C. Ta funkcja służy do przechodzenia przez hierarchie katalogów i przyjmuje wskaźnik do funkcji jako jeden z jego parametrów. Ta funkcja ma następujący podpis: int (*fn) (const char *fpath, const struct stat *sb, int typeflag).

using System;
using System.Runtime.InteropServices;

namespace PInvokeSamples
{
    public static class Program
    {
        // Define a delegate that has the same signature as the native function.
        private delegate int DirClbk(string fName, ref Stat stat, int typeFlag);

        // Import the libc and define the method to represent the native function.
        [DllImport("libc.so.6")]
        private static extern int ftw(string dirpath, DirClbk cl, int descriptors);

        // Implement the above DirClbk delegate;
        // this one just prints out the filename that is passed to it.
        private static int DisplayEntry(string fName, ref Stat stat, int typeFlag)
        {
            Console.WriteLine(fName);
            return 0;
        }

        public static void Main(string[] args)
        {
            // Call the native function.
            // Note the second parameter which represents the delegate (callback).
            ftw(".", DisplayEntry, 10);
        }
    }

    // The native callback takes a pointer to a struct. This type
    // represents that struct in managed code.
    [StructLayout(LayoutKind.Sequential)]
    public struct Stat
    {
        public uint DeviceID;
        public uint InodeNumber;
        public uint Mode;
        public uint HardLinks;
        public uint UserID;
        public uint GroupID;
        public uint SpecialDeviceID;
        public ulong Size;
        public ulong BlockSize;
        public uint Blocks;
        public long TimeLastAccess;
        public long TimeLastModification;
        public long TimeLastStatusChange;
    }
}

Przykład systemu macOS używa tej samej funkcji, a jedyną różnicą jest argument atrybutu DllImport , ponieważ system macOS utrzymuje libc się w innym miejscu.

using System;
using System.Runtime.InteropServices;

namespace PInvokeSamples
{
    public static class Program
    {
        // Define a delegate that has the same signature as the native function.
        private delegate int DirClbk(string fName, ref Stat stat, int typeFlag);

        // Import the libc and define the method to represent the native function.
        [DllImport("libSystem.dylib")]
        private static extern int ftw(string dirpath, DirClbk cl, int descriptors);

        // Implement the above DirClbk delegate;
        // this one just prints out the filename that is passed to it.
        private static int DisplayEntry(string fName, ref Stat stat, int typeFlag)
        {
            Console.WriteLine(fName);
            return 0;
        }

        public static void Main(string[] args)
        {
            // Call the native function.
            // Note the second parameter which represents the delegate (callback).
            ftw(".", DisplayEntry, 10);
        }
    }

    // The native callback takes a pointer to a struct. This type
    // represents that struct in managed code.
    [StructLayout(LayoutKind.Sequential)]
    public struct Stat
    {
        public uint DeviceID;
        public uint InodeNumber;
        public uint Mode;
        public uint HardLinks;
        public uint UserID;
        public uint GroupID;
        public uint SpecialDeviceID;
        public ulong Size;
        public ulong BlockSize;
        public uint Blocks;
        public long TimeLastAccess;
        public long TimeLastModification;
        public long TimeLastStatusChange;
    }
}

Oba poprzednie przykłady zależą od parametrów, a w obu przypadkach parametry są podane jako typy zarządzane. Środowisko uruchomieniowe wykonuje "właściwą rzecz" i przetwarza je w odpowiedniki po drugiej stronie. Dowiedz się, jak typy są ułożone do kodu natywnego na naszej stronie na stronie Typ marshalling.

Więcej zasobów