Win32 API – spojrzenie C# deva [PL]

Czym jest Win32 API?

Win32 API to… API. Hmmm to czym jest API? Z angielskiego rozwiniemy ten skrót jako Application Programing Interface, co oznacza, że jest to zestaw reguł, zasad, których możemy używać, aby móc korzystać z funkcji oferowanych przez dany system. API może być wiele: Android API, Win32 API, X.11 API, Java FX API, JEE API etc. Win32 API to interface programistyczny dostarczany nam przez Microsoft w Windowsie. Możemy go używać całkowicie dowolnie.

Budowa Win32 API

Win 32 API to zespół stałych, funkcji, struktur danych, który jest dostarczany programistom przez Microsoft w raz z systemem MS Windows. Wszystko to jest nam umieszczone bibliotekach .dll i .lib.

Najbardziej znane biblioteki Win32 API to user32.dll i kernel32.dll, ale to nie jedyne z nich, gdyż są jeszcze np. audiodg.dll, gdi32.dll, ntdll.dll i wiele innych. Całe Win32 API znajduje się (rozpatrując uproszczony schemat budowy systemu rodziny NT) w warstwie poniżej warstwy aplikacji Win32 tj. w warstwie wykonawczej tam gdzie zarządca HID(Huma Interface Device), GDI(Graphic Device Interface), zarządca PnP(protokół Plug n Play), IPC(Inter-Process Communication) etc.

Wniosek jest jeden: samo pojęcie API nie dotyczy pliku czy ich zespołu a tego co jest przekazane jako ich zawartość. Widać to też na przykładzie środowiska X.11 dostępnego w Linuxie i używanego przez developerów budujących środowiska graficzne takie jak Unity, KDE, Cinnamon itd.

Ale wracając do tematu Windowsa…

Czy Win32 API ulega zmianom?

Ogółem rzecz ujmując API ulegają zmianom, ale nie są to zmiany drastyczne, zazwyczaj programiści starają sie nie dopuścić do sytuacji, w której wersja poprzednia bardzo mocno różni się od najnowszej, inaczej mogłoby to powodować błędne działanie oprogramowania działajacego w oparciu o dane API lub też całkowity brak możliwości uruchomienia go. Jedyne zmiany jakie są wprowadzane we wszelkich API to dodawanie nowych stałych, funkcji lub też poprawianie tego, co już istnieje, ale bez większej ingerencji w logiczną strukturę całości.

Praktyczny przykład:

W bibliotece user32.dll istnieje funkcja o nazwie “mouse_event”.

Funkcja ta znajduje się w pliku nagłówkowym Winuser.h i jej definicja wygląda następująco:

VOID WINAPI mouse_event(
  _In_ DWORD     dwFlags,
  _In_ DWORD     dx,
  _In_ DWORD     dy,
  _In_ DWORD     dwData,
  _In_ ULONG_PTR dwExtraInfo
);

Omówienie parametrów funkcji:

  • DWORD dwFlags – zmienna przenosząca flagi, które są wartościami ściśle ustalonymi przez Microsoft,
  • DWORD dx – punkt x wydarzenia,
  • DWORD dy – punkt y wydarzenia,
  • DWORD dwData – zawiera informacje zależne od stanu dwFlags tj. co zostało wciśnięte, ile razy itd.
  • ULONG PTR dwExtraInfo – wskaźnik do zmiennej dwExtraInfo, której wartość reprezetuje dodatkowe informacje o zdarzeniu.

Funkcja ta służy do symulowania zdarzeń myszki na całym obszarze ekarnu. Możemy powodować zdarzenia takie jak:

  • kliknięcie lewym,
  • kliknięcie prawym,
  • ruszenie kółkiem myszki
  • i wiele innych…

Dokładna lista znajduje się tutaj.

Jeśli ktoś przeczyta notkę na samej górze strony w linku powyżej, będzie wiedział, że funkcja jest zastąpiona przez inną. Nie została ona porzucona, czyli nie zostanie usunięta z API, jednak nie tylko ze względów kompatybilności; na jej bazie zbudowano nową, lepszą funkcję: SendInput.

Funkcja SendInput znajduje się w bibliotece user32.dll .

Definicja tej funkcji wygląda jak następuje:

UINT WINAPI SendInput(
  _In_ UINT    nInputs,
  _In_ LPINPUT pInputs,
  _In_ int     cbSize
)

Kod wykonuje dokładnie te same operacje co mouse_event z tą różnicą, że dane są podawane w strukturze INPUT, a nie poprzez wartości typu int, to samo tyczy się typów operacji do symulowania: podajemy je w strukturze, czyli w momencie wykorzystwania funkcji w kodzie swojej aplikacji będziemy musieli stworzyć szereg warunków logicznych tak, by działanie programu mogło być ustosunkowane do stanów wprowadzonych na parametr funkcji.

Omówienie parametrów podawanych na wejście:

  • int nInputs – zmienna reprezentująca ilość struktur danych,
  • INPUT pInputs – obiekt struktury INPUT przenoszący informacje o wydarzeniu,
  • int cbSize – rozmiar pojedynczej struktury w byte’ach.

Po więcej informacji technicznych odsyłam do MSDN: SendInput i INPUT.

Cieniutka krawędź…

Należy używać WinAPI z rozwagą, ze względu na to, że tworzenie programów wykorzystujących niektóre jego funkcje(w szczególności te dotyczące procesów innych programów działających równolegle do naszego) może powodować, że antywirusy będą mylić je z prawdziwymi wirusami.

Jak patrzy na to developer C#?

Używanie zasobów dostarczanych przez Win32 API jest możliwe dzięki temu, że Microsoft zaimplementował w bibliotekach frameworka .NET odpowiedni atrybut zwany DllImport. Dzięki niemu developer jest w stanie importować z odpowiedniej biblioteki tzw. kod niezarządzany (ang. unmanaged code; kodem zarządzanym w języku C# określamy kod naszej aplikacji).  Zawsze pod atrybutem DllImport deklarujemy lub definiujemy funkcję, zmienną/stałą, strukturę, którą chcemy importować. Dla biblioteki user32.dll będzie to wyglądać tak:

[DllImport("user32.dll")]

To najprostsza forma użycia atrybutu DllImport. Do niego można zawsze dołączać dodatkowe parametry np. SetLastError(jego wartość na true lub false), EntryPoint czy też CharSet.

Co tak na prawdę wtedy się dzieje?

W języku C++ w momencie dodania include informujemy preprocesor, że ma dołączyć kod zawarty w tym pliku do kodu naszego programu. Jednak w C# nie mamy takiej możliwości, język ten nie operuje na plikach naglówkowych. Jak wspomniałem wyżej, dokonujemy importu biblioteki oraz deklarujemy żądaną stałą/zmienną, funkcję czy strukturę w kodzie swojej aplikacji. W wyniku tego środowisko .NET importuje na czas działania naszego programu to czego żadamy z biblioteki i konwertuje ją w taki sposób, aby była ona dostępna tak jak każda inny inny zasób tego typu. A zatem środowisko .NET umożliwia nam taki import żądanych zasobów, że możemy ich używać, tak jakby były one rodzime dla naszego kodu.

A co z typami danych?

Jak widać, sposób w jaki Microsoft rozwiązał problem importu kodu niezarządzanego z wszelkich bibliotek(DllImport pozwala na importowanie z dowolnych bibliotek) jest bardzo przyjemny i niewymagający. Znacznie bardziej wymagającym elementem jest dobranie typów obecnych w C# do tych znanych z C++.

W Win32 API występują typy jak: LPSTR(8-bitowy string zgodny z ANSI), DWORD(32-bitowy int bez znaku),HWND(int, który jest zaczepem okna) czy też zmienne typów występujących w C# oraz i wskaźniki do nich. Z kolei w C# tak “dzikich” typów nie ma, zatem jak je zamienić? Zazwyczaj wystarczy spojrzeć na opis danego parametru na stronie MSDN dla podanej funkcji/struktury czy stałej, by stwierdzić, że należy wstawić int, long, short, string  czy inny typ lub też zadeklarować obiekt czy zmienną poprzez słowo kluczowe out(coś jak pointer w C++), a może podać referencję używajac słowa kluczowego ref(coś jak referowanie poprzez adres w C++). O ile w samym API jest to bardzo konkretnie powiedziane co należy podać na wejście funkcji lub jakie typy zmiennych użyć budując strukturę danych, o tyle w C# nie ma żadnych konkretnych wskazań jakie to mają być typy. Niestety pomimo tego jest to bardzo istotna rzecz, gdyż wszelkie niedociągnięcia w tym aspekcie mogą się skończyć błędami pamięci(tzw. segmentation faults) lub referencjami do obiektu, który nie został zainicjowany. Uniknąć błędów w doborze typów zmiennych pomoże nam strona: pinvoke.net oraz oczywiście stackoverflow.com.

Przykłady użycia

Powyżej w omówieniu Win32 API użyłem funkcji SendInput jako przykładu. Stąd też posłużę się tymi funkcjami do zaprezentowania najprostszego użycia ich w programie C#.

Kod może wyglądać tak:

namespace SampleProgram{
public class Program{
        public const int MOUSEEVENTF_LEFTDOWN = 2;
        public const int MOUSEEVENTF_LEFTUP = 4;
        public const int MOUSEEVENTF_RIGHTDOWN = 8;
        public const int MOUSEEVENTF_RIGHTUP = 16;
        public const int MOUSEEVENTF_MIDDLEUP = 32;
        public const int MOUSEEVENTF_MIDDLEDOWN = 64;
        public const int INPUT_MOUSE = 0;
    
        struct MOUSEINPUT
        {
            public int dx;
            public int dy;
            public int mouseData;
            public int dwFlags;
            public int time;
            public IntPtr dwExtraInfo;
        }

        struct INPUT
        {
            public uint type;
            public MOUSEINPUT mi;
        };

        [DllImport("user32.dll",SetLastError=true)]
        internal static extern SendInput(int nInputs, ref INPUT pInputs, int cbSize);

        public static void main(string[] args){
            WinApi.Win32.Window.INPUT i = new WinApi.Win32.Window.INPUT();
            //tworzymy obiekt struktury INPUT
            i.type = WinApi.Win32.Window.INPUT_MOUSE;//zdefiniowane typu 
            //zdarzenia
            i.mi.dx = 0;//punkt x
            i.mi.dy = 0;//punkt y
            i.mi.dwFlags = WinApi.Win32.Window.MOUSEEVENTF_LEFTDOWN;
            //nadanie flag, czyli przekazanie funkcji informacji o tym jakie 
            //nastąpiło zdarzenie    
            i.mi.dwExtraInfo = IntPtr.Zero;
            i.mi.mouseData = 0;
            i.mi.time = 0;

           SendInput(1, ref i, Mashal.SizeOf(i));//przeprowadzamy procedurę 
           //marszalizacji, czyli sposobu organizowania danych pomiędzy kodem        
           //zarządzanym i niezarządzanym
       }
    }
 }

Ten przykładowy kod umożliwi nam zasymulowanie kliknięcia lewym przyciskiem myszy w punkcie 0,0, bez żadnych dodatkowych informacji, danych oraz czasu wykonania zdarzenia.

Jak widać, kod jest w miarę prosty, jednak w języku C# wszystkie zasoby, które importujemy, muszą być przez nas albo zadeklarowane w kodzie aplikacji albo nawet zdefiniowane(np. struktura INPUT) dzięki temu środowisko .NET może przenieść reguł← i zasady działania oryginalnych zasobów z Win32 API.

To byłoby na tyle w tym poście.

Dzięki za uwagę i do zobaczenia!

1 thought on “Win32 API – spojrzenie C# deva [PL]”

  1. Pingback: My private look on .NET 5 - IntoIT - From Young Dev's View

Leave a Comment

Your email address will not be published. Required fields are marked *