Szukaj…


Uwagi

Praca z API Win32 przy użyciu C #

Windows udostępnia wiele funkcji w postaci Win32 API. Za pomocą tych interfejsów API można wykonywać bezpośrednie operacje w systemie Windows, co zwiększa wydajność aplikacji. Źródło Kliknij tutaj

Windows udostępnia szeroki zakres API. Aby uzyskać informacje o różnych interfejsach API, możesz sprawdzić witryny takie jak Pinvoke .

Funkcja importu z niezarządzanego DLL C ++

Oto przykład importowania funkcji zdefiniowanej w niezarządzanym DLL C ++. W kodzie źródłowym C ++ dla „myDLL.dll” zdefiniowano add funkcji:

extern "C" __declspec(dllexport) int __stdcall add(int a, int b)
{
    return a + b;
}

Następnie można go włączyć do programu C # w następujący sposób:

class Program
{
    // This line will import the C++ method.
    // The name specified in the DllImport attribute must be the DLL name.
    // The names of parameters are unimportant, but the types must be correct.
    [DllImport("myDLL.dll")]
    private static extern int add(int left, int right);

    static void Main(string[] args)
    {
        //The extern method can be called just as any other C# method.
        Console.WriteLine(add(1, 2));
    }
}

Zobacz Konwencje wywoływania i zmiany nazwy C ++, aby uzyskać wyjaśnienie, dlaczego konieczne jest użycie extern "C" i __stdcall .

Znajdowanie biblioteki dynamicznej

Przy pierwszym wywołaniu metody extern program C # wyszuka i załaduje odpowiednią bibliotekę DLL. Aby uzyskać więcej informacji o tym, gdzie przeszukiwana jest biblioteka DLL i jak wpływać na lokalizacje wyszukiwania, zobacz to pytanie dotyczące przepełnienia stosu .

Prosty kod do prezentacji klasy dla com

using System;
using System.Runtime.InteropServices;
 
namespace ComLibrary
{
    [ComVisible(true)]
    public interface IMainType
    {
        int GetInt();
 
        void StartTime();
 
        int StopTime();
    }
 
    [ComVisible(true)]
    [ClassInterface(ClassInterfaceType.None)]
    public class MainType : IMainType
    {
        private Stopwatch stopWatch;
 
        public int GetInt()
        {
            return 0;
        }
 
        public void StartTime()
        {
            stopWatch= new Stopwatch();
            stopWatch.Start();
        }
 
        public int StopTime()
        {
            return (int)stopWatch.ElapsedMilliseconds;
        }
    }
}

Zmiana nazwy C ++

Kompilatory C ++ kodują dodatkowe informacje w nazwach eksportowanych funkcji, takich jak typy argumentów, aby umożliwić przeciążenie różnymi argumentami. Ten proces nazywa się manglingiem nazw . Powoduje to problemy z importem funkcji w języku C # (i współdziałanie z innymi językami w ogóle), jak nazwa int add(int a, int b) funkcja nie jest już add , to może być ?add@@YAHHH@Z , _add@8 lub cokolwiek innego, w zależności od kompilatora i konwencji wywoływania.

Istnieje kilka sposobów rozwiązania problemu zniekształcania nazw:

  • Eksportowanie funkcji za pomocą extern "C" aby przełączyć na zewnętrzny link C, który używa zmiany nazwy C:

    extern "C" __declspec(dllexport) int __stdcall add(int a, int b)
    
    [DllImport("myDLL.dll")]
    

    Nazwa funkcji nadal będzie zniekształcona ( _add@8 ), ale StdCall C # rozpoznaje StdCall + extern "C" StdCall nazwy extern "C" .

  • Określanie nazw eksportowanych funkcji w pliku definicji modułu myDLL.def :

    EXPORTS
      add
    
    int __stdcall add(int a, int b)
    
    [DllImport("myDLL.dll")]
    

    Nazwa funkcji będzie czysta add w tym przypadku.

  • Importowanie zniekształconej nazwy. Będziesz potrzebować przeglądarki DLL, aby zobaczyć zniekształconą nazwę, a następnie możesz podać ją jawnie:

    __declspec(dllexport) int __stdcall add(int a, int b)
    
    [DllImport("myDLL.dll", EntryPoint = "?add@@YGHHH@Z")]
    

Konwencje zwoływania

Istnieje kilka konwencji wywoływania funkcji, określających, kto (wywołujący lub odbierający) wysyła argumenty ze stosu, sposób przekazywania argumentów i kolejność. C ++ Cdecl korzysta z konwencji wywoływania Cdecl , ale C # oczekuje StdCall , który jest zwykle używany przez Windows API. Musisz zmienić jedną lub drugą:

  • Zmień konwencję wywoływania na StdCall w C ++:

    extern "C" __declspec(dllexport) int __stdcall add(int a, int b)
    
    [DllImport("myDLL.dll")]
    
  • Lub zmień konwencję wywoływania na Cdecl w C #:

    extern "C" __declspec(dllexport) int /*__cdecl*/ add(int a, int b)
    
    [DllImport("myDLL.dll", CallingConvention = CallingConvention.Cdecl)]
    

Jeśli chcesz użyć funkcji z konwencją wywoływania Cdecl i zniekształconą nazwą, kod będzie wyglądał następująco:

__declspec(dllexport) int add(int a, int b)
[DllImport("myDLL.dll", CallingConvention = CallingConvention.Cdecl,
           EntryPoint = "?add@@YAHHH@Z")]
  • thiscall ( __thiscall ) jest używany głównie w funkcjach należących do klasy.

  • Gdy funkcja używa tego wywołania ( __thiscall ), wskaźnik do klasy jest przekazywany jako pierwszy parametr.

Dynamiczne ładowanie i rozładowywanie niezarządzanych bibliotek DLL

Korzystając z atrybutu DllImport , musisz znać poprawną bibliotekę DLL i nazwę metody w czasie kompilacji . Jeśli chcesz być bardziej elastyczny i zdecydować w czasie wykonywania, które biblioteki DLL i metody ładować, możesz użyć metod API Windows LoadLibrary() , GetProcAddress() i FreeLibrary() . Może to być pomocne, jeśli używana biblioteka zależy od warunków środowiska wykonawczego.

Wskaźnik zwrócony przez GetProcAddress() można rzutować na delegata za pomocą Marshal.GetDelegateForFunctionPointer() .

Poniższy przykładowy kod pokazuje to za pomocą myDLL.dll z poprzednich przykładów:

class Program
{
    // import necessary API as shown in other examples
    [DllImport("kernel32.dll", SetLastError = true)]
    public static extern IntPtr LoadLibrary(string lib);
    [DllImport("kernel32.dll", SetLastError = true)]
    public static extern void FreeLibrary(IntPtr module);
    [DllImport("kernel32.dll", SetLastError = true)]
    public static extern IntPtr GetProcAddress(IntPtr module, string proc);

    // declare a delegate with the required signature
    private delegate int AddDelegate(int a, int b);

    private static void Main()
    {
        // load the dll
        IntPtr module = LoadLibrary("myDLL.dll");
        if (module == IntPtr.Zero) // error handling
        {
            Console.WriteLine($"Could not load library: {Marshal.GetLastWin32Error()}");
            return;
        }

        // get a "pointer" to the method
        IntPtr method = GetProcAddress(module, "add");
        if (method == IntPtr.Zero) // error handling
        {
            Console.WriteLine($"Could not load method: {Marshal.GetLastWin32Error()}");
            FreeLibrary(module);  // unload library
            return;
        }
            
        // convert "pointer" to delegate
        AddDelegate add = (AddDelegate)Marshal.GetDelegateForFunctionPointer(method, typeof(AddDelegate));
    
        // use function    
        int result = add(750, 300);
        
        // unload library   
        FreeLibrary(module);
    }
}

Radzenie sobie z błędami Win32

Podczas korzystania z metod interakcji można użyć interfejsu API GetLastError, aby uzyskać dodatkowe informacje na temat wywołań interfejsu API.

DllImport Attribute SetLastError Atrybut

SetLastError = true

Wskazuje, że odbiorca wywoła SetLastError (funkcja API Win32).

SetLastError = false

Wskazuje, że odbiorca nie wywoła SetLastError (funkcja API Win32), dlatego nie otrzymasz informacji o błędzie.

  • Gdy SetLastError nie jest ustawiony, jest ustawiony na false (wartość domyślna).

  • Kod błędu można uzyskać za pomocą metody Marshal.GetLastWin32Error:

Przykład:

[DllImport("kernel32.dll", SetLastError=true)]
public static extern IntPtr OpenMutex(uint access, bool handle, string lpName);

Jeśli spróbujesz otworzyć mutex, który nie istnieje, GetLastError zwróci ERROR_FILE_NOT_FOUND .

var lastErrorCode = Marshal.GetLastWin32Error();

if (lastErrorCode == (uint)ERROR_FILE_NOT_FOUND)
{
    //Deal with error         
}

Kody błędów systemu można znaleźć tutaj:

https://msdn.microsoft.com/en-us/library/windows/desktop/ms681382(v=vs.85).aspx

GetLastError API

Istnieje natywny interfejs API GetLastError, którego można również użyć:

[DllImport("coredll.dll", SetLastError=true)]
static extern Int32 GetLastError();
  • Podczas wywoływania Win32 API z kodu zarządzanego, zawsze musisz użyć Marshal.GetLastWin32Error .

Dlatego:

Między wywołaniem Win32, które ustawia błąd (wywołuje SetLastError), CLR może wywoływać inne wywołania Win32, które również mogą wywoływać SetLastError , zachowanie to może zastąpić wartość błędu. W tym scenariuszu wywołanie GetLastError może spowodować niepoprawny błąd.

Ustawienie SetLastError = true , upewnia się, że CLR pobiera kod błędu, zanim wykona inne wywołania Win32.

Przypięty obiekt

GC (Garbage Collector) jest odpowiedzialny za czyszczenie naszych śmieci.

Podczas gdy GC czyści nasze śmieci, usuwa nieużywane obiekty z zarządzanej sterty, które powodują fragmentację sterty. Po zakończeniu usuwania GC wykonuje kompresję stosu (defragmintację), która obejmuje ruchome obiekty na stercie.

Ponieważ GC nie jest deterministyczne, podczas przekazywania referencji / wskaźnika zarządzanego obiektu do kodu natywnego, GC może uruchomić się w dowolnym momencie, jeśli wystąpi tuż po wywołaniu Inerop, istnieje bardzo duża szansa, że obiekt (który referencja przekazana do natywnego) będzie zostać przeniesionym na zarządzaną stertę - w wyniku tego otrzymujemy nieprawidłowe odwołanie po stronie zarządzanej.

W tym scenariuszu należy przypiąć obiekt przed przekazaniem go do kodu rodzimego.

Przypięty obiekt

Przypięty obiekt to obiekt, który nie może poruszać się za pomocą GC.

Gc Przypięty uchwyt

Możesz utworzyć obiekt pin za pomocą metody Gc.Alloc

GCHandle handle = GCHandle.Alloc(yourObject, GCHandleType.Pinned); 
  • Uzyskanie przypiętego GCHandle do zarządzanego obiektu oznacza określony obiekt jako taki, którego GC nie może przenieść, dopóki nie zwolni uchwytu

Przykład:

[DllImport("kernel32.dll", SetLastError = true)]
public static extern void EnterCriticalSection(IntPtr ptr);

[DllImport("kernel32.dll", SetLastError = true)]
public static extern void LeaveCriticalSection(IntPtr ptr);
       
public void EnterCriticalSection(CRITICAL_SECTION section)
{
    try
    {
        GCHandle handle = GCHandle.Alloc(section, GCHandleType.Pinned); 
        EnterCriticalSection(handle.AddrOfPinnedObject());
        //Do Some Critical Work
        LeaveCriticalSection(handle.AddrOfPinnedObject());
    }
    finaly
    {
        handle.Free()
    }
}

Środki ostrożności

  • Podczas przypinania (szczególnie dużych) obiektu staraj się jak najszybciej zwolnić przypięty GcHandle , ponieważ przerywa on defragmentację sterty.
  • Jeśli zapomnisz uwolnić GcHandle, nic nie będzie. Zrób to w bezpiecznej sekcji kodu (na przykład na końcu)

Czytanie struktur z marszałkiem

Klasa Marshal zawiera funkcję o nazwie PtrToStructure , ta funkcja daje nam możliwość odczytu struktur przez niezarządzany wskaźnik.

Funkcja PtrToStructure ma wiele przeciążeń, ale wszystkie mają tę samą intencję.

Ogólna PtrToStructure :

public static T PtrToStructure<T>(IntPtr ptr);

T - typ konstrukcji.

ptr - Wskaźnik do niezarządzanego bloku pamięci.

Przykład:

NATIVE_STRUCT result = Marshal.PtrToStructure<NATIVE_STRUCT>(ptr);       
  • Jeśli masz do czynienia z obiektami zarządzanymi podczas czytania struktur natywnych, nie zapomnij przypiąć obiektu :)
 T Read<T>(byte[] buffer)
    {
        T result = default(T);
        
        var gch = GCHandle.Alloc(buffer, GCHandleType.Pinned);
    
        try
        {
            result = Marshal.PtrToStructure<T>(gch.AddrOfPinnedObject());
        }
        finally
        {
            gch.Free();
        }
        
        return result;
    }


Modified text is an extract of the original Stack Overflow Documentation
Licencjonowany na podstawie CC BY-SA 3.0
Nie związany z Stack Overflow