Programowanie urządzeń mobilnych Laboratorium Dostęp do funkcji niskopoziomowych w.net Compact Framework
Wstęp Technologia.NET Compact Framework dostarcza deweloperom oprogramowania bardzo wiele możliwości w rozwijaniu aplikacji przeznaczonych na urządzenia mobilne. Firma Microsoft, oprócz środowiska programistycznego i licznych narzędzi wspomagających tworzenie kodu, daje także dostęp do wielu bibliotek i gotowych obiektów ułatwiających wykorzystywanie możliwości, jakie dają urządzenia przenośne. Z uwagi na łatwość wykorzystywania i dobrą dokumentację najpopularniejszym językiem tworzenia kodu jest C#, a samo tworzenie aplikacji opiera się na wytwarzaniu tzw. kodu zarządzalnego (ang. managed code), czyli obsługiwanego przez.net Compact Framework CLR (ang. Common Language Runtime), tak jak to zostało przedstawione na Rys. 1. Rozwiązanie to przyśpiesza proces tworzenia samego kodu aplikacji, czyni go bardziej czytelnym, tym samym obniżając koszty całego procesu tworzenia oprogramowania. Rys. 1. Kodowanie natywne a kodowanie a tworzenie kodu zarządzalnego. Czasami jednak zachodzi potrzeba tworzenia bardziej zaawansowanych funkcji dla wymagających użytkowników telefonów komórkowych, w tym przypadku dostęp do niskopoziomowych funkcji Windows Mobile jest bardzo przydatny.
W niniejszym ćwiczeniu pokazany zostanie mechanizm Platform Invocation Service, zwany także P/Invoke lub Pinvoke, który umożliwia wywoływanie z poziomu kodu zarządzalnego funkcji systemowych zawartych w natywnych bibliotekach DLL. Mechanizm P/Invoke Zastosowanie mechanizmu P/Invoke składa się z trzech podstawowych etapów: deklaracji, wywołania, obsługi błędów. DEKLARACJA Na samym początku realizacji mechanizmu wywołania funkcji Win API musimy wiedzieć jaką dokładnie funkcje chcemy wywołać, czyli musimy znać: nazwę biblioteki DLL w której funkcja się znajduje, nazwę samej funkcji (w literaturze anglojęzycznej i dokumentacji możemy napotkać na nazwę entry point ), sposób wywołania (ang. calling convention), zależny głównie od platformy językowej w której piszemy aplikacji (C++, C#, VB). W laboratorium ograniczymy się do wywoływania funkcji systemowych z języka C# za pomocą atrybutu DllImport jak pokazuje fragment kodu poniżej: //using System.Runtime.InteropServices;!! [DllImport("coredll.dll", SetLastError = true)] private static extern bool SHGetSpecialFolderPath( IntPtr hwndowner, StringBuilder lpszpath, int nfolder, int fcreate); W tym przypadku zamieściliśmy deklarację funkcji umożliwiającej pobranie domyślnych ścieżek folderów systemowych w systemie WindowsCE. Oprócz nazwy funkcji
SHGetSpecialFolderPath zamieszczona została także nazwa biblioteki dll, do której się będziemy odwoływać, oraz liczba i typ parametórw wejściowych. Należy także zwrócić uwagę na fakt, iż funkcja została zadeklarowana jako zewnętrzna (extern) i statyczna. Modyfikatory te oznaczają że ciało funkcji jest zaimplementowane poza obrębem deklaracji (extern), a co za tym następuje, funkcja jest dostępna niezależnie od instancji klasy (static). Omawiana funkcja pobiera argument typu cefolders. W praktyce oznacza to, że funkcja wymaga wartości CSIDL (ang. constant special item ID list) wskazującą na jedną że stałych w Windows CE API. Dokładne informację na temat stałych w Windows CE można znaleźć w dokumentacji albo na stronie MSDN (www.msdn.com). Pomocne zatem, może być stworzenie zbioru stałych a podstawie listy CSIDL, która będzie nam definiowała najważniejsze foldery: const int CSIDL_FAVORITES = 0x0006; const int CSIDL_FONTS = 0x0014; const int CSIDL_PERSONAL = 0x0005; const int CSIDL_PROGRAM_FILES = 0x0026; const int CSIDL_PROGRAMS = 0x0002; const int CSIDL_STARTUP = 0x0007; const int CSIDL_WINDOWS = 0x0024; UWAGA!! W niektórych przypadkach może zdarzyć się tak, iż nazwa funkcji w bibliotece DLL będzie kolidowała nam z jednym słów kluczowych C# albo z nazwą innej funkcji. Projektanci platformy.net Compact Framework przewidzieli taką sytuację i stworzyli system nadawaniu aliasów dla deklaracji funkcji natywnych: [DllImport("coredll.dll", EntryPoint="SHGetSpecialFolderPath")] static extern bool GetFolderPath( //reszta deklaracji funkcji WYWOŁANIE Mając prawidłową deklarację funkcji natywnej możemy zaimplementować jej wywołanie. Wywołanie samej funkcji najczęściej implementuje się w ciele specjalnie
przeznaczonej do tego metody typu static tak, aby funkcja natywna mogła być wywołana bez konieczności tworzenia obiektu klasy, w której jej deklaracja została zaimplementowana. Przykładowe wywołanie pokazuje kod poniżej: bool ret = false; StringBuilder resultpath = new StringBuilder(255); try ret = SHGetSpecialFolderPath((IntPtr)0,resultPath,CSIDL_STARTUP,0); catch(exception ex) //HandleCeError(ex, "GetSpecialFolderPath"); return "blad"; if (!ret) Console.Error.WriteLine("API Error"); int errornum = Marshal.GetLastWin32Error(); return resultpath.tostring(); OBSŁUGA BŁĘDÓW W kodzie powyżej pokazano jedynie przykład tego jak można wywołać funkcje natwyną z kodu zarządzalnego oraz przedstawiono prosty przykład obsługi błedów za pomocą wyjątków. W celu dokładniejszego stwierdzenia przyczyny ewentualnie powstałych wyjątków dobrze jest dodatkowo zaimplementować funkcję obsługującą powstałe wyjątki (np. HandleCeError(...)), w której można dokładnie przeanalizować przyczynę nieprawidłowości. Przykładowy kod metody HandleCeError został pokazany poniżej: private static void HandleCeError(Exception ex, string method) if (ex.gettype() == (typeof(notsupportedexception))) throw new WinCeException("Bad arguments or incorrect declaration in " + method, 0, ex); //entry not found if (ex.gettype() == typeof(missingmethodexception)) throw new WinCeException("Entry point not found in " + method, 0, ex); if (ex.gettype() == typeof(winceexception)) throw ex;
//All other exceptions throw new WinCeException("Miscellaneous exception in " + method, 0, ex); PRZEKAZYWANIE DANYCH (ang. MARSHALLING DATA) Istotnym elementem w programowaniu z użyciem kodu zarzadzalnego i natywnego jest przekazywanie danych z i do funkcji natywnych. W przypadku CLR (ang. Common Language Runtime) istnieje szereg reguł i metod definiujących sposób przekształacania i interpretacji danych pomiędzy CLR a Win API. W naszym przypadku ograniczymy się jedynie do najważniejszych postulatów, które powinny wystarczyć aby prawidłowo korzystać z najwązniejszych funkcji Win API. Typy nymeryczne Większość bilbiotek systemu Windows jest napisana z użyciem języka C, a co za tym idzie, większość stałych i zmiennych wykorzystywanaych w funkcjach Win API pochodzi od podstawowych typów języku C bądź ich modyfikacji wynikających ze stosowania definicji typów bądź MACRO. Zatem, w przypadku typów numerczyncych próbójąc przekazać bądź odebrać zmienną z lub do Win API musimu znać odpowiedzi na następujące pytania: czy zmienna jest stało- czy zmienno-przecinkowa? Jeśli zmienna jest stało przecinkowa, czy jest ze znakiem (signed) czy bez znaku (unsigned)? Jeśli zmienna jest stało przecinkowa to na ilu bitach jest zapisywana? Jeśli zmienna jest zmienno-przecinkowa to czy jest typu single- czy doubleprecision? Czasami odpowiedź na poniższe pytania jest oczywista czasami wyamaga zastanowienia. Poniższa tabelka może być pomocna w programowaniu na Win API:
Typy Win API Specyfikacja Typy.NET (CLR) char, INT8, SBYTE, CHAR 8-bit signed integer System.SByte short, short int, INT16, 16-bit signed integer System.Int16 SHORT int, long, long int, INT32, 32-bit signed integer System.Int32 LONG32, BOOLâ, INT int64, INT64, 64-bit signed integer System.Int64 LONGLONG unsigned char, UINT8, 8-bit unsigned integer System.Byte UCHARâ, BYTE unsigned short, UINT16, 16-bit unsigned integer System.UInt16 USHORT, WORD, ATOM, WCHARâ, wchar_t unsigned, unsigned int, 32-bit unsigned integer System.UInt32 UINT32, ULONG32, DWORD32, ULONG, DWORD, UINT unsigned int64, UINT64, 64-bit unsigned integer System.UInt64 DWORDLONG, ULONGLONG float, FLOAT Single-precision floating System.Single point double, long double, DOUBLE Double-precision floating point System.Double Tabela 1. Typu Win API a typy CLR. Więcej informacji odnośnie przekazywania zmiennych pomiędzy CLR a kodem natywnym można znaleźć na stronie MSDN np.: http://msdn.microsoft.com/en-us/magazine/cc164123.aspx#s1 lub http://msdn.microsoft.com/en-us/library/aa446529.aspx ZADANIE 1 Należy zrealizować funkcję odczytu katalogu startowego (CSIDL_WINDOWS_STARTUP) na urządzeniu Windows Mobile ZADANIE 2
Zadanie 2 polega na odczycie pełnej listy kontaktów zapisanej na karcie SIM z użyciem funkcji natywnych i wyświetleniu ich w prostym menu użytkownika. UWAGA!! Istnieją dodatkowe bilbioteki (np. Smart Device Framework) umożliwiające realizację tego zadania, których na tym ćwiczeniu nie należy używać!! Zadanie ma być wykonane z użyciem kodu natywnego tak jak pokazano w opisowej części instrukcji dotyczy to wszystkich zadań na tym ćwiczeniu laboratoryjnym. WSKAZÓWKI Funkcja do odczytu kontaktu z karty SIM (PHONEBOOKENTRY) [DllImport("cellcore.dll")] private static extern IntPtr SimReadPhonebookEntry(IntPtr hsim, uint dwlocation, uint dwindex, ref SIMPHONEBOOKENTRY lpphonebookentry); Struktura simphonebookentry: internal struct SIMPHONEBOOKENTRY public uint cbsize; // @field Size of the structure in bytes public uint dwparams; // @field Indicates valid parameter values [MarshalAs(UnmanagedType.ByValTStr, SizeConst = 256)] public string lpszaddress; // @field The actual phone number public uint dwaddresstype; // @field A SIM_ADDRTYPE_*constant public uint dwnumplan; // @field A SIM_NUMPLAN_*constant [MarshalAs(UnmanagedType.ByValTStr, SizeConst = 256)] string lpsztext; // @field Text assocaited with the entry Niezbędne stałe: private static long SIM_PBSTORAGE_EMERGENCY = (0x00000001) ; // @constdefine Emergency dial list private static long SIM_PBSTORAGE_FIXEDDIALING = (0x00000002) ; // @constdefine SIM fixed dialing list private static long SIM_PBSTORAGE_LASTDIALING = (0x00000004) ; // @constdefine SIM last dialing list private static long SIM_PBSTORAGE_OWNNUMBERS = (0x00000008) ; // @constdefine SIM ownnumbers lists private static long SIM_PBSTORAGE_SIM = (0x00000010); // @constdefine General SIM Storage
private static long SIM_NUMPBSTORAGES = 5; Number of phonebook storages // @constdefine Proponowane wywołanie: res = SimReadPhonebookEntry((IntPtr)hSim, (uint)sim_pbstorage_sim, (uint)1, ref simentry); gdzie 1 oznacza numer kontaktu. Wynik będzie przechowywany w strukturze simentry. Kontakty numeroweane są od 1!!. Funkcja SimReadPhonebookEntry( ) zwraca wartość 0 w przypadku prawidłowego odczytu danych. W zmiennej hsim przechowywany jest adres pamięci umożliwiający zapis i odczyt danych z karty z SIM. Można go uzyskać za pomocą funkcji: [DllImport("cellcore.dll")] private static extern IntPtr SimInitialize(IntPtr dwflags, IntPtr lpfncallback, IntPtr dwparam, out IntPtr lphsim); Należy także pamiętać o zwolnieniu pamięci po wykonaniu operacji na danych SIM za pomocą funkcji: [DllImport("cellcore.dll")] private static extern IntPtr SimDeinitialize(IntPtr hsim); Więcej informacji na temat można znaleźć na stronie MSDN: http://msdn.microsoft.com/en-us/library/aa921503.aspx Uwaga!! Funckje związane z odczytem zawartości danych z karty SIM nie będą działać na emulatorze!! Zadanie 2 i 3 wykonujemy więc na telefonie komórkowym w grupach 2 osobowych (2 osoby na jeden telefon). ZADANIE 3 W zadaniu 3 należy dać użytkownikowi mozliwość wykonania połączenia na wybrany z listy wygenerowanej z zadaniu numer. Realizację wywołania połączenia telefonicznego należy zrealizować także za pomocą funkcji P/Invoke. Wskazówki
Funkcja wywołania połączenia: [DllImport("phone.dll")] private static extern IntPtr PhoneMakeCall(ref PhoneMakeCallInfo ppmci); Struktura przekazywana w argumencie: private struct PhoneMakeCallInfo public IntPtr cbsize; public IntPtr dwflags; public IntPtr pszdestaddress; public IntPtr pszappname; public IntPtr pszcalledparty; public IntPtr pszcomment;