基于 Linux 和 MiniGUI 的嵌入式系统软件开发指南(六) MiniGUI 提供的非 GUI/GDI 接口...
1 引言
一般而言,GUI 系統的應用程序編程接口主要集中于窗口、消息隊列、圖形設備等相關方面。但因為 GUI 系統在處理系統事件時通常會提供自己的機制,而這些機制往往會和操作系統本身提供的機制不相兼容。比如,MiniGUI 提供了消息循環機制,而應用程序的結構一般是消息驅動的;也就是說,應用程序通過被動接收消息來工作。但很多情況下,應用程序需要主動監視某個系統事件, 比如在 UNIX 操作系統中,可以通過 select 系統調用監聽某個文件描述符上是否有可讀數據。這樣,如何將 MiniGUI 的消息隊列機制和現有操作系統的其他機制融合在一起,就成了一個較為困難的問題。本文將講述幾種解決這一問題的方法。
我們知道,MiniGUI-Lite 采用 UNIX Domain Socket 實現客戶程序和服務器程序之間的交互。應用程序也可以利用這一機制,完成自己的通訊任務――客戶向服務器提交請求,而服務器完成對客戶的請求處理并應答。 一方面,在 MiniGUI-Lite 的服務器程序中,你可以擴展這一機制,注冊自己的請求處理函數,完成定制的請求/響應通訊任務。另一方面,MiniGUI-Lite 當中也提供了若干用來創建和操作 UNIX Domain Socket 的函數,任何 MiniGUI-Lite 的應用程序都可以建立 UNIX Domain Socket,并完成和其他 MiniGUI-Lite 應用程序之間的數據交換。本文將舉例講述如何利用 MiniGUI-Lite 提供的函數完成此類通訊任務。
嵌入式 Linux 系統現在能夠在許多不同架構的硬件平臺上運行,MiniGUI 也能夠在這些硬件平臺上運行。但由于許多硬件平臺具有和其他硬件平臺不同的特性,比如說,常見的 CPU 是 Little Endian 的,而某些 CPU 則是 Big Endian 的。這要求我們在編寫代碼,尤其是文件 I/O 相關代碼時,必須編寫可移植代碼,以便適合具有不同架構的平臺。本文將描述 MiniGUI 為應用程序提供的可移植性函數及其用法。
除了與上述內容相關的函數之外,MiniGUI 還提供了其他一些函數,本文最后部分將描述這些函數的用途和用法,包括配置文件讀寫以及定點數運算。
2 MiniGUI-Lite和 select 系統調用
我們知道,在 MiniGUI-Lite 之上運行的應用程序只有一個消息隊列。應用程序在初始化之后,會建立一個消息循環,然后不停地從這個消息隊列當中獲得消息并處理,直到接收到 MSG_QUIT 消息為止。應用程序的窗口過程在處理消息時,要在處理完消息之后立即返回,以便有機會獲得其他的消息并處理。現在,如果應用程序在處理某個消息時監聽某個 文件描述符而調用 select 系統調用,就有可能會出現問題――因為 select 系統調用可能會長時間阻塞,而由 MiniGUI-Lite 服務器發送給客戶的事件得不到及時處理。這樣,消息驅動的方式和 select 系統調用就難于很好地融合。在 MiniGUI-Threads 中,因為每個線程都有自己相應的消息隊列,而系統消息隊列是由單獨運行的 desktop 線程管理的,所以任何一個應用程序建立的線程都可以長時間阻塞,從而可以調用類似 select 的系統調用。但在 MiniGUI-Lite 當中,如果要監聽某個應用程序自己的文件描述符事件,必須進行恰當的處理,以避免長時間阻塞。
在 MiniGUI-Lite 當中,有幾種解決這一問題的辦法:
由于前兩種解決方法比較簡單,這里我們重點講述的第三種解決辦法。MiniGUI-Lite 為應用程序提供了如下兩個函數及一個宏:
| #define MAX_NR_LISTEN_FD 5 /* Return TRUE if all OK, and FALSE on error. */ BOOL GUIAPI RegisterListenFD (int fd, int type, HWND hwnd, void* context); /* Return TRUE if all OK, and FALSE on error. */ BOOL GUIAPI UnregisterListenFD (int fd); |
- MAX_NR_LISTEN_FD 宏定義了系統能夠監聽的最多文件描述符數,默認定義為 5。
- RegisterListenFD 函數在系統當中注冊一個需要監聽的文件描述符,并指定監聽的事件類型(type 參數,可取 POLLIN、POLLOUT 或者 POLLERR),接收 MSG_FDEVENT 消息的窗口句柄以及一個上下文信息。
- UnregisterListenFD 函數注銷一個被注冊的監聽文件描述符。
在應用程序使用RegisterListenFD 函數注冊了被監聽的文件描述符之后,當指定的事件發生在該文件描述符上時,系統會將 MSG_FDEVENT 消息發送到指定的窗口,應用程序可在窗口過程中接收該消息并處理。MiniGUI 中的 libvcongui 就利用了上述函數監聽來自主控偽終端上的可讀事件,如下面的程序段所示(vcongui/vcongui.c):
| ... /* 注冊主控偽終端偽監聽文件描述符 */ RegisterListenFD (pConInfo->masterPty, POLLIN, hMainWnd, 0); /* 進入消息循環 */ while (!pConInfo->terminate && GetMessage (&Msg, hMainWnd)) { DispatchMessage (&Msg); } /* 注銷監聽文件描述符 */ UnregisterListenFD (pConInfo->masterPty); ... /* 虛擬控制臺的窗口過程 */ static int VCOnGUIMainWinProc (HWND hWnd, int message, WPARAM wParam, LPARAM lParam) { PCONINFO pConInfo; pConInfo = (PCONINFO)GetWindowAdditionalData (hWnd); switch (message) { ... /* 接收到 MSG_FDEVENT 消息,則處理主控偽終端上的輸入數據 */ case MSG_FDEVENT: ReadMasterPty (pConInfo); break; ... } /* 調用默認窗口過程 */ if (pConInfo->DefWinProc) return (*pConInfo->DefWinProc)(hWnd, message, wParam, lParam); else return DefaultMainWinProc (hWnd, message, wParam, lParam); } |
3 MiniGUI-Lite 與進程間通訊
3.1 簡單請求/應答處理
我們知道,MiniGUI-Lite 利用了 UNIX Domain Socket 實現服務器和客戶程序之間的通訊。為了實現客戶和服務器之間的簡單方便的通訊,MiniGUI-Lite 中定義了一種簡單的請求/響應結構。客戶程序通過指定的結構將請求發送到服務器,服務器處理請求并應答。在客戶端,一個請求定義如下 (include/gdi.h):
| typedef struct tagREQUEST { int id; const void* data; size_t len_data; } REQUEST; typedef REQUEST* PREQUEST; |
其中,id 是用來標識請求類型的整型數,data 是發送給該請求的關聯數據,len_data 則是數據的長度。客戶在初始化 REQUEST 結構之后,就可以調用 cli_request 向服務器發送請求,并等待服務器的應答。該函數的原型如下。
| /* send a request to server and wait reply */ int cli_request (PREQUEST request, void* result, int len_rslt); |
服務器程序(即 mginit)會在自己的消息循環當中獲得來自客戶的請求,并進行處理,最終會將處理結果發送給客戶。
在上述這種簡單的客戶/服務器通訊中,客戶和服務器必須就每個請求類型達成一致,也就是說,客戶和服務器必須了解每種類型請求的數據含義并進行恰當的處理。
MiniGUI-Lite 利用上述這種簡單的通訊方法,實現了若干系統級的通訊任務:
- 鼠標光標的管理。鼠標光標是一個全局資源,當客戶需要創建或者銷毀鼠標光標,改變鼠標光標的形狀、位置,顯示或者隱藏鼠標時,就發送請求到服務器,服務器程序完成相應任務并將結果發送給客戶。
- 層及活動客戶管理。當客戶查詢層的信息,新建層,加入某個已有層,或者設置層中的活動客戶時,通過該接口發送請求到服務器。
- 其他一些系統級的任務。比如在新的 GDI 接口中,服務器程序統一管理顯示卡中可能用來建立內存 DC 的顯示內存,當客戶要申請建立在顯示內存中的內存 DC 時,就會發送請求到服務器。
為了讓應用程序也能夠通過這種簡單的方式實現客戶和服務器之間的通訊,服務器程序可以注冊一些定制的請求處理函數,然后客戶就可以向服務器發送這些請求。為此,MiniGUI-Lite 提供了如下接口:
| #define MAX_SYS_REQID 0x0010 #define MAX_REQID 0x0018 /* * Register user defined request handlers for server * Note that user defined request id should larger than MAX_SYS_REQID */ typedef int (* REQ_HANDLER) (int cli, int clifd, void* buff, size_t len); BOOL GUIAPI RegisterRequestHandler (int req_id, REQ_HANDLER your_handler); REQ_HANDLER GUIAPI GetRequestHandler (int req_id); |
服務器可以通過調用RegisterRequestHandler 函數注冊一些請求處理函數。注意請求處理函數的原型由REQ_HANDLER 定義。還要注意系統定義了MAX_SYS_REQID 和 MAX_REQID 這兩個宏。MAX_REQID 是能夠注冊的最大請求 ID 號,而 MAX_SYS_REQID 是系統內部使用的最大的請求 ID 號,也就是說,通過RegisterRequestHandler 注冊的請求 ID 號,必須大于 MAX_SYS_REQID 而小于或等于 MAX_REQID。
作為示例,我們假設服務器替客戶計算兩個整數的和。客戶發送兩個整數給服務器,而服務器將兩個整數的和發送給客戶。下面的程序段在服務器程序中運行,在系統中注冊了一個請求處理函數:
| typedef struct TEST_REQ { int a, b; } TEST_REQ; static int send_reply (int clifd, void* reply, int len) { MSG reply_msg = {HWND_INVALID, 0}; /* 發送一個空消息接口給客戶,以便說明這是一個請求的應答 */ if (sock_write (clifd, &reply_msg, sizeof (MSG)) < 0) return SOCKERR_IO; /* 將結果發送給客戶 */ if (sock_write (clifd, reply, len) < 0) return SOCKERR_IO; return SOCKERR_OK; } static int test_request (int cli, int clifd, void* buff, size_t len) { int ret_value = 0; TEST_REQ* test_req = (TEST_REQ*)buff; ret_value = test_req.a + test_req.b; return send_reply (clifd, &ret_value, sizeof (int)); } ... RegisterRequestHandler (MAX_SYS_REQID + 1, test_request); ... |
而客戶程序可以通過如下的程序段向客戶發送一個請求獲得兩個整數的和:
| REQUEST req; TEST_REQ test_req = {5, 10}; int ret_value; req.id = MAX_SYS_REQID + 1; req.data = &rest_req; req.len_data = sizeof (TEST_REQ); cli_request (&req, &ret_value, sizeof (int)); printf ("the returned value: %d\n", ret_value); /* ret_value 的值應該是 15 */ |
讀者已經看到,通過這種簡單的請求/應答技術,MiniGUI-Lite 客戶程序和服務器程序之間可以建立一種非常方便的進程間通訊機制。但這種技術也有一些缺點,比如受到 MAX_REQID 大小的影響,通訊機制并不是非常靈活,而且請求只能發送給MiniGUI-Lite 的服務器程序(即 mginit)處理等等。
3.2 復雜的 UNIX Domain Socket 封裝
為了解決上述簡單請求/應答機制的不足,MiniGUI-Lite 也提供了經過封裝的 UNIX Domain Socket 處理函數。這些函數的接口原型如下(include/minigui.h):
| /* Used by server to create a listen socket. * Name is the name of listen socket. * Please located the socket in /var/tmp directory. */ /* Returns fd if all OK, -1 on error. */ int serv_listen (const char* name); /* Wait for a client connection to arrive, and accept it. * We also obtain the client's pid and user ID from the pathname * that it must bind before calling us. */ /* returns new fd if all OK, < 0 on error */ int serv_accept (int listenfd, pid_t *pidptr, uid_t *uidptr); /* Used by clients to connect to a server. * Name is the name of the listen socket. * The created socket will located at the directory /var/tmp, * and with name of '/var/tmp/xxxxx-c', where 'xxxxx' is the pid of client. * and 'c' is a character to distinguish diferent projects. * MiniGUI use 'a' as the project character. */ /* Returns fd if all OK, -1 on error. */ int cli_conn (const char* name, char project); #define SOCKERR_IO -1 #define SOCKERR_CLOSED -2 #define SOCKERR_INVARG -3 #define SOCKERR_OK 0 /* UNIX domain socket I/O functions. */ /* Returns SOCKERR_OK if all OK, < 0 on error.*/ int sock_write_t (int fd, const void* buff, int count, unsigned int timeout); int sock_read_t (int fd, void* buff, int count, unsigned int timeout); #define sock_write(fd, buff, count) sock_write_t(fd, buff, count, 0) #define sock_read(fd, buff, count) sock_read_t(fd, buff, count, 0) |
上述函數是 MiniGUI-Lite 用來建立系統內部使用的 UNIX Domain Socket 并進行數據傳遞的函數,是對基本套接字系統調用的封裝。這些函數的功能描述如下:
- serv_listen:服務器調用該函數建立一個監聽套接字,并返回套接字文件描述符。建議將服務器監聽套接字建立在 /var/tmp/ 目錄下。
- serv_accept:服務器調用該函數接受來自客戶的連接請求。
- cli_conn:客戶調用該函數連接到服務器,其中 name 是客戶的監聽套接字。該函數為客戶建立的套接字將保存在 /var/tmp/ 目錄中,并且以 -c 的方式命名,其中 c 是用來區別不同套接字通訊用途的字母,由 project 參數指定。MiniGUI-Lite 內部使用了 'a',所以由應用程序建立的套接字,應該使用除 'a' 之外的字母。
- sock_write_t:在建立并連接之后,客戶和服務器之間就可以使用 sock_write_t 函數和 sock_read_t 函數進行數據交換。sock_write_t 的參數和系統調用 write 類似,但可以傳遞進入一個超時參數,注意該參數以 10ms 為單位,為零時超時設置失效,且超時設置只在 mginit 程序中有效。
- sock_read_t:sock_read_t 的參數和系統調用 read類似,但可以傳遞進入一個超時參數,注意該參數以 10ms 為單位,為零時超時設置失效,且超時設置只在 mginit 程序中有效。
下面的代碼演示了作為服務器的程序如何利用上述函數建立一個監聽套接字:
| #define LISTEN_SOCKET "/var/tmp/mysocket" static int listen_fd; BOOL listen_socket (HWND hwnd) { if ((listen_fd = serv_listen (LISTEN_SOCKET)) < 0) return FALSE; return RegisterListenFD (fd, POLL_IN, hwnd, NULL); } |
當服務器接收到來自客戶的連接請求是,服務器的 hwnd 窗口將接收到 MSG_FDEVENT 消息,這時,服務器可接受該連接請求:
| int MyWndProc (HWND hwnd, int message, WPARAM wParam, LPARAM lParam) { switch (message) { ... case MSG_FDEVENT: if (LOWORD (wParam) == listen_fd) { /* 來自監聽套接字 */ pid_t pid; uid_t uid; int conn_fd; conn_fd = serv_accept (listen_fd, &pid, &uid); if (conn_fd >= 0) { RegisterListenFD (conn_fd, POLL_IN, hwnd, NULL); } } else { /* 來自已連接套接字 */ int fd = LOWORD(wParam); /* 處理來自客戶的數據 */ sock_read_t (fd, ...); sock_write_t (fd, ....); } break; ... } } |
上面的代碼中,服務器將連接得到的新文件描述符也注冊為監聽描述符,因此,在 MSG_FDEVENT 消息的處理中,應該判斷導致 MSG_FDEVENT 消息的文件描述符類型,并做適當的處理。
在客戶端,當需要連接到服務器時,可通過如下代碼:
| int conn_fd; if ((conn_fd = cli_conn (LISTEN_SOCKET, 'b')) >= 0) { /* 向服務器發送請求 */ sock_write_t (fd, ....); /* 獲取來自服務器的處理結果 */ sock_read_t (fd, ....); } |
4 編寫可移植代碼
我們知道,許多嵌入式系統所使用的 CPU 具有和普通臺式機 CPU 完全不同的構造和特點。但有了操作系統和高級語言,可以最大程度上將這些不同隱藏起來。只要利用高級語言編程,編譯器和操作系統能夠幫助程序員解決許多和 CPU 構造及特點相關的問題,從而節省程序開發時間,并提高程序開發效率。然而某些 CPU 特點卻是應用程序開發人員所必須面對的,這其中就有如下幾個需要特別注意的方面:
- 字節順序。一般情況下,我們接觸到的 CPU 在存放多字節的整數數據時,將低位字節存放在低地址單元中,比如常見的 Intel x86 系列 CPU。而某些 CPU 采用相反的字節順序。比如在嵌入式系統中使用較為廣泛的 PowerPC 就將低位字節存放在高地址單元中。前者叫 Little Endian 系統;而后者叫 Big Endian 系統。
- 在某些平臺上的 Linux 內核,可能缺少某些高級系統調用,最常見的就是與虛擬內存機制相關的系統調用。在某些 CPU 上運行的 Linux 操作系統,因為 CPU 能力的限制,無法提供虛擬內存機制,基于虛擬內存實現的某些 IPC 機制就無法正常工作。比如在某些缺少 MMU 單元的 CPU 上,就無法提供 System V IPC 機制中的共享內存。
為了編寫具有最廣泛適應性的可移植代碼,應用程序開發人員必須注意到這些不同,并且根據情況編寫可移植代碼。這里,我們將描述如何在 MiniGUI 應用程序中編寫可移植代碼。
4.1 理解并使用 MiniGUI 的 Endian 讀寫函數
為了解決上述的第一個問題,MiniGUI 提供了若干 Endian 相關的讀寫函數。這些函數可以劃分為如下兩類:
- 用來交換字節序的函數。包括ArchSwapLE16、ArchSwapBE16 等。
- 用來讀寫標準I/O 流的函數。包括MGUI_ReadLE16、MGUI_ReadBE16 等。
前一類用來將某個 16位、32 位或者 64 位整數從某個特定的字節序轉換為系統私有(native)字節序。舉例如下:
| int fd, len_header; ... if (read (fd, &len_header, sizeof (int)) == -1) goto error; #if MGUI_BYTEORDER == MGUI_BIG_ENDIAN len_header = ArchSwap32 (len_header); // 如果是 Big Endian 系統,則轉換字節序 #endif ... |
在上面的程序段中,首先通過 read 系統調用從指定的文件描述符中讀取一個整數值到 len_header 變量中。該文件中保存的整數值是 Little Endian 的,因此如果在 Big Endian 系統上使用這個整數值,就必須進行字節順序交換。這里可以使用 ArchSwapLE32,將 Little Endian 的 32 位整數值轉換為系統私有的字節序。也可以如上述程序段那樣,只對 Big Endian 系統進行字節序轉換,這時,只要利用 ArchSwap32 函數即可。
MiniGUI 提供的用來轉換字節序的函數(或者宏)如下:
- ArchSwapLE16(X) 將指定的以 Little Endian 字節序存放的 16 位整數值轉換為系統私有整數值。如果系統本身是 Little Endian 系統,則該函數不作任何工作,直接返回 X;如果系統本身是 Big Endian 系統,則調用 ArchSwap16 函數交換字節序。
- ArchSwapLE32(X) 將指定的以 Little Endian 字節序存放的 32 位整數值轉換為系統私有整數值。如果系統本身是 Little Endian 系統,則該函數不作任何工作,直接返回 X;如果系統本身是 Big Endian 系統,則調用 ArchSwap32 函數交換字節序。
- ArchSwapBE16(X) 將指定的以 Big Endian 字節序存放的 16 位整數值轉換為系統私有整數值。如果系統本身是 Big Endian 系統,則該函數不作任何工作,直接返回 X;如果系統本身是 Little Endian 系統,則調用 ArchSwap16 函數交換字節序。
- ArchSwapBE32(X) 將指定的以 Big Endian 字節序存放的 32 位整數值轉換為系統私有整數值。如果系統本身是 Big Endian 系統,則該函數不作任何工作,直接返回 X;如果系統本身是 Little Endian 系統,則調用 ArchSwap32 函數交換字節序。
MiniGUI 提供的第二類函數用來從標準 I/O 的文件對象中讀寫 Endian 整數值。如果要讀取的文件是以 Little Endian 字節序存放的,則可以使用 MGUI_ReadLE16 和MGUI_ReadLE32 等函數讀取整數值,這些函數將把讀入的整數值轉換為系統私有字節序,反之使用MGUI_ReadBE16 和MGUI_ReadBE32 函數。如果要寫入的文件是以 Little Endian 字節序存放的,則可以使用 MGUI_WriteLE16 和MGUI_WriteLE32 等函數讀取整數值,這些函數將把要寫入的整數值從系統私有字節序轉換為 Little Endian 字節序,然后寫入文件,反之使用MGUI_WriteBE16 和MGUI_WriteBE32 函數。下面的代碼段說明了上述函數的用法:
| FILE* out; int ount; ... MGUI_WriteLE32 (out, count); // 以 Little Endian 字節序保存 count 到文件中。 ... |
4.2 利用條件編譯編寫可移植代碼
在涉及到可移植性問題的時候,有時我們能夠方便地通過 4.1 中描述的方法進行函數封裝,從而提供具有良好移植性的代碼,但有時我們無法通過函數封裝的方法提供可移植性代碼。這時,恐怕只能使用條件編譯了。下面的代 碼說明了如何使用條件編譯的方法確保程序正常工作(該代碼來自 MiniGUI src/kernel/sharedres.c):
| /* 如果系統不支持共享內存,則定義 _USE_MMAP #undef _USE_MMAP /* #define _USE_MMAP 1 */ void *LoadSharedResource (void) { #ifndef _USE_MMAP key_t shm_key; void *memptr; int shmid; #endif /* 裝載共享資源 */ ... #ifndef _USE_MMAP /* 獲取共享內存對象 */ if ((shm_key = get_shm_key ()) == -1) { goto error; } shmid = shmget (shm_key, mgSizeRes, SHM_PARAM | IPC_CREAT | IPC_EXCL); if (shmid == -1) { goto error; } // Attach to the share memory. memptr = shmat (shmid, 0, 0); if (memptr == (char*)-1) goto error; else { memcpy (memptr, mgSharedRes, mgSizeRes); free (mgSharedRes); } if (shmctl (shmid, IPC_RMID, NULL) < 0) goto error; #endif /* 打開文件 */ if ((lockfd = open (LOCKFILE, O_WRONLY | O_CREAT | O_TRUNC, 0644)) == -1) goto error; #ifdef _USE_MMAP /* 如果使用 mmap,就將共享資源寫入文件 */ if (write (lockfd, mgSharedRes, mgSizeRes) < mgSizeRes) goto error; else { free(mgSharedRes); mgSharedRes = mmap( 0, mgSizeRes, PROT_READ|PROT_WRITE, MAP_SHARED, lockfd, 0); } #else /* 否則將共享內存對象 ID 寫入文件 */ if (write (lockfd, &shmid, sizeof (shmid)) < sizeof (shmid)) goto error; #endif close (lockfd); #ifndef _USE_MMAP mgSharedRes = memptr; SHAREDRES_SHMID = shmid; #endif SHAREDRES_SEMID = semid; return mgSharedRes; error: perror ("LoadSharedResource"); return NULL; } |
上述程序段是 MiniGUI-Lite 服務器程序用來裝載共享資源的。如果系統支持共享內存,則初始化共享內存對象,并將裝載的共享資源關聯到共享內存對象,然后將共享內存對象 ID 寫入文件;如果系統不支持共享內存,則將初始化后的共享資源全部寫入文件。在客戶端,如果支持共享內存,則可以從文件中獲得共享內存對象 ID,并直接關聯到共享內存;如果不支持共享內存,則可以使用 mmap 系統調用,將文件映射到進程的地址空間。客戶端的代碼段如下:
| void* AttachSharedResource (void) { #ifndef _USE_MMAP int shmid; #endif int lockfd; void* memptr; if ((lockfd = open (LOCKFILE, O_RDONLY)) == -1) goto error; #ifdef _USE_MMAP /* 使用 mmap 將共享資源映射到進程地址空間 */ mgSizeRes = lseek (lockfd, 0, SEEK_END ); memptr = mmap( 0, mgSizeRes, PROT_READ, MAP_SHARED, lockfd, 0); #else /* 否則獲取共享內存對象 ID,并關聯該共享內存 */ if (read (lockfd, &shmid, sizeof (shmid)) < sizeof (shmid)) goto error; close (lockfd); memptr = shmat (shmid, 0, SHM_RDONLY); #endif if (memptr == (char*)-1) goto error; return memptr; error: perror ("AttachSharedResource"); return NULL; } |
5 其他
5.1 讀寫配置文件
MiniGUI 的配置文件,即 /etc/MiniGUI.cfg 文件的格式,采用了類似 Windows INI 文件的格式。這種文件格式非常簡單,如下所示:
| [section-name1] key-name1=key-value1 key-name2=key-value2 [section-name2] key-name3=key-value3 key-name4=key-value4 |
這種配置文件中的參數以 section 分組,然后用 key=value 的形式指定參數及其值。應用程序也可以利用這種配置文件格式保存一些配置信息,為此,MiniGUI 提供了如下三個函數(include/minigui.h):
| int GUIAPI GetValueFromEtcFile (const char* pEtcFile, const char* pSection,const char* pKey, char* pValue, int iLen); int GUIAPI GetIntValueFromEtcFile (const char* pEtcFile, const char* pSection,const char* pKey, int* value); int GUIAPI SetValueToEtcFile (const char* pEtcFile, const char* pSection, const char* pKey, char* pValue); |
這三個函數的用途如下:
- GetValueFromEtcFile:從指定的配置文件當中獲取指定的鍵值,鍵值以字符串形式返回。
- GetIntValueFromEtcFile:從指定的配置文件當中獲取指定的整數型鍵值。該函數將獲得的字符串轉換為整數值返回(采用strtol 函數轉換)。
- SetValueToEtcFile:該函數將給定的鍵值保存到指定的配置文件當中,如果配置文件不存在,則將新建配置文件。如果給定的鍵已存在,則將覆蓋舊值。
假定某個配置文件記錄了一些應用程序信息,并具有如下格式:
| [mginit] nr=8 autostart=0 [app0] path=../tools/ name=vcongui layer= tip=Virtual&console&on&MiniGUI icon=res/konsole.gif [app1] path=../bomb/ name=bomb layer= tip=Game&of&Minesweaper icon=res/kmines.gif [app2] path=../controlpanel/ name=controlpanel layer= tip=Control&Panel icon=res/kcmx.gif |
其中的 [mginit] 段記錄了應用程序個數(nr鍵),以及自動啟動的應用程序索引(autostart鍵)。而 [appX] 段記錄了每個應用程序的信息,包括該應用程序的路徑、名稱、圖標等等。下面的代碼演示了如何使用 MiniGU的配置文件函數獲取這些信息(該代碼段來自 mde 演示包中的 mginit 程序):
| #define APP_INFO_FILE "mginit.rc" static BOOL get_app_info (void) { int i; APPITEM* item; /* 獲取應用程序個數信息 */ if (GetIntValueFromEtcFile (APP_INFO_FILE, "mginit", "nr", &app_info.nr_apps) != ETC_OK) return FALSE; if (app_info.nr_apps <= 0) return FALSE; /* 獲取自動啟動的應用程序索引 */ GetIntValueFromEtcFile (APP_INFO_FILE, "mginit", "autostart", &app_info.autostart); if (app_info.autostart >= app_info.nr_apps || app_info.autostart < 0) app_info.autostart = 0; /* 分配應用程序信息結構 */ if ((app_info.app_items = (APPITEM*)calloc (app_info.nr_apps, sizeof (APPITEM))) == NULL) { return FALSE; } /* 獲取每個應用程序的路徑、名稱、圖標等信息 */ item = app_info.app_items; for (i = 0; i < app_info.nr_apps; i++, item++) { char section [10]; sprintf (section, "app%d", i); if (GetValueFromEtcFile (APP_INFO_FILE, section, "path", item->path, PATH_MAX) != ETC_OK) goto error; if (GetValueFromEtcFile (APP_INFO_FILE, section, "name", item->name, NAME_MAX) != ETC_OK) goto error; if (GetValueFromEtcFile (APP_INFO_FILE, section, "layer", item->layer, LEN_LAYER_NAME) != ETC_OK) goto error; if (GetValueFromEtcFile (APP_INFO_FILE, section, "tip", item->tip, TIP_MAX) != ETC_OK) goto error; strsubchr (item->tip, '&', ' '); if (GetValueFromEtcFile (APP_INFO_FILE, section, "icon", item->bmp_path, PATH_MAX + NAME_MAX) != ETC_OK) goto error; if (LoadBitmap (HDC_SCREEN, &item->bmp, item->bmp_path) != ERR_BMP_OK) goto error; item->cdpath = TRUE; } return TRUE; error: free_app_info (); return FALSE; } |
5.2 定點數運算
通常在進行數學運算時,我們采用浮點數表示實數,并利用 頭文件中所聲明的函數進行浮點數運算。我們知道,浮點數運算是一種非常耗時的運算過程。為了減少因為浮點數運算而帶來的額外 CPU 指令,在一些三維圖形庫當中,通常會采用定點數來表示實數,并利用定點數進行運算,這樣,將大大提高三維圖形的運算速度。MiniGUI 也提供了一些定點數運算函數,分為如下幾類:
- 整數、浮點數和定點數之間的轉換。利用 itofix 和 fixtoi 函數可實現整數和定點數之間的相互轉換;利用 ftofix 和 fixtof 函數可實現浮點數和定點數之間的轉換。
- 定點數加、減、乘、除等基本運算。利用 fadd、fsub、fmul、fdiv、fsqrt等函數可實現定點數加、減、乘、除以及平方根運算。
- 定點數的三角運算。利用 fcos、fsin、ftan、facos、fasin 等函數可求給定定點數的余弦、正弦、正切、反余弦、反正弦值。
- 矩陣、向量等運算。矩陣、向量相關運算在三維圖形中非常重要,限于篇幅,本文不會詳細講述這些運算,讀者可參閱MiniGUI 的 include/fixedmath.h 頭文件。
下面的代碼段演示了定點數的用法,該程序段根據給定的三個點(pts[0]、pts[1]、pts[2])畫一個弧線,其中 pts[0] 作為圓心,pts[1] 是圓弧的起點,而 pts[2] 是圓弧終點和圓心連線上的一個點:
| void draw_arc (HDC hdc, POINT* pts) { int sx = pts [0].x, sy = pts [0].y; int dx = pts [1].x - sx, dy = pts [1].y - sy; int r = sqrt (dx * dx * 1.0 + dy * dy * 1.0); double cos_d = dx * 1.0 / r; fixed cos_f = ftofix (cos_d); fixed ang1 = facos (cos_f); int r2; fixed ang2; if (dy > 0) { ang1 = fsub (0, ang1); } dx = pts [2].x - sx; dy = pts [2].y - sy; r2 = sqrt (dx * dx * 1.0 + dy * dy * 1.0); cos_d = dx * 1.0 / r2; cos_f = ftofix (cos_d); ang2 = facos (cos_f); if (dy > 0) { ang2 = fsub (0, ang2); } Arc (hdc, sx, sy, r, ang1, ang2); } |
上述程序的計算非常簡單,步驟如下(該程序段來自 mde 演示程序包中的 painter/painter.c 程序):
6 小結
本文講述了 MiniGUI 為應用程序提供的一些非 GUI/GDI 的接口。這些接口中,某些是為了解決和操作系統的交互而設計的,以便 MiniGUI 應用程序能夠更好地與操作系統提供的機制融合在一起;而某些提供了對 UNIX Domain Socket 良好封裝的接口,可幫助應用程序方便進行進程間通訊或者擴展其功能;其他接口則專注于嵌入式系統的特殊性,為應用程序提供了可移植的文件 I/O 封裝代碼。在這些接口的幫助下,嵌入式系統開發人員可以編寫功能強大而靈活的應用程序。
轉載于:https://www.cnblogs.com/weifuqin530/archive/2009/05/17/1458628.html
總結
以上是生活随笔為你收集整理的基于 Linux 和 MiniGUI 的嵌入式系统软件开发指南(六) MiniGUI 提供的非 GUI/GDI 接口...的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 创业必看:中国八大草根富豪发家史
- 下一篇: 验证码的使用