Udostępnij za pośrednictwem


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 partial class Program
{
    // Import user32.dll (containing the function we need) and define
    // the method corresponding to the native function.
    [LibraryImport("user32.dll", StringMarshalling = StringMarshalling.Utf16, SetLastError = true)]
    private static partial 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 przedstawia dyrektywę using dla System.Runtime.InteropServices przestrzeni nazw, która zawiera wszystkie potrzebne elementy.
  • Wiersz 8 wprowadza LibraryImportAttribute atrybut . Ten atrybut informuje środowisko uruchomieniowe, że powinno załadować niezarządzany plik binarny. Przekazany ciąg jest niezarządzanym plikiem binarnym zawierającym funkcję docelową. Ponadto określa kodowanie, które ma być używane 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.GetLastPInvokeError().
  • Wiersz 9 jest cruxem pracy P/Invoke. Definiuje metodę zarządzaną, która ma dokładnie taki sam podpis jak niezarządzany. Deklaracja używa atrybutu LibraryImport i słowa kluczowego partial , aby poinformować rozszerzenie kompilatora o wygenerowaniu kodu w celu wywołania do niezarządzanej biblioteki.
    • W wygenerowany kod i przed platformą .NET 7 DllImport jest używany. Ta deklaracja używa słowa kluczowego extern , aby wskazać środowisku uruchomieniowemu, że jest to metoda zewnętrzna i że podczas wywoływania środowisko uruchomieniowe powinno znaleźć je w niezarządzanym pliku binarnym określonym w atrybucie DllImport .

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 LibraryImport 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 partial class Program
    {
        // Import the libSystem shared library and define the method
        // corresponding to the native function.
        [LibraryImport("libSystem.dylib")]
        private static partial 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 partial class Program
    {
        // Import the libc shared library and define the method
        // corresponding to the native function.
        [LibraryImport("libc.so.6")]
        private static partial 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 partial 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.
        [LibraryImport("user32.dll")]
        private static partial 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 partial 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.
        [LibraryImport("libc.so.6", StringMarshalling = StringMarshalling.Utf16)]
        private static partial 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 LibraryImport , ponieważ system macOS utrzymuje libc się w innym miejscu.

using System;
using System.Runtime.InteropServices;

namespace PInvokeSamples
{
    public static partial 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.
        [LibraryImport("libSystem.dylib", StringMarshalling = StringMarshalling.Utf16)]
        private static partial 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