Libevent原始碼分析-----通用型別和函式
Libevent定義了一系列的可移植的相容型別和函式。這使得在各個系統上都有一致的效果,Libevent一般都會在相容通用型別和函式的前面加上ev或evutil字首。
在實現上,Libevent都是使用條件編譯+巨集定義的方式。使用這種方式,同一個巨集名字,可以使得在不同的系統上, 編譯時得到不同的值。這種方式在跨平臺程式設計中,經常使用到。此外,對於Libevent的相容型別,如果所在系統已經有對應功能的型別,那麼Libevent將直接將ev_XXX巨集的值定義為該型別。如果所在系統沒有對應的型別,那麼就會選擇一個比較合理的型別作為巨集值。
相容型別:
定長位寬型別:
因為,C/C++中,整型int的位寬(多少bit)是沒有限定的,而在有的時候卻需要一個確定的長度。在C99標準中有一個stdint.h標頭檔案定義了一些確定位寬的整型,如int64_t、int32_t。但Libevent考慮到可能有的環境並沒有支援這個標頭檔案,所以自己就定義了自己的一套確切位寬的整型。
在util.h檔案的一開始,就定義了一些通用的位寬確定的整數型別。如下:
#ifdef _EVENT_HAVE_UINT64_T #define ev_uint64_t uint64_t #define ev_int64_t int64_t #elif defined(WIN32) #define ev_uint64_t unsigned __int64 #define ev_int64_t signed __int64 #elif _EVENT_SIZEOF_LONG_LONG == 8 #define ev_uint64_t unsigned long long #define ev_int64_t long long #elif _EVENT_SIZEOF_LONG == 8 #define ev_uint64_t unsigned long #define ev_int64_t long #elif defined(_EVENT_IN_DOXYGEN) #define ev_uint64_t ... #define ev_int64_t ... #else #error "No way to define ev_uint64_t" #endif
從程式碼中可以看到,它最先考慮當前環境是否已經定義了64位寬的整型,如果有的話,就直接使用。然後再考慮是否在Windows系統、是否定義了_EVENT_SIZEOF_LONG_LONG,並且值為8 ……
正如《event-config.h指明所在系統的環境》博文所說的,像_EVENT_HAVE_UINT64_T、_EVENT_SIZEOF_LONG_LONG這些巨集定義都是在Libevent檢測系統環境時定義的。
Libevent定義了一系列位寬的整型,如下圖:
其最值是直接計算出來的,如下:
#define EV_UINT64_MAX ((((ev_uint64_t)0xffffffffUL)<< 32) | 0xffffffffUL)
#define EV_INT64_MAX ((((ev_int64_t) 0x7fffffffL) << 32) |0xffffffffL)
#define EV_INT64_MIN ((-EV_INT64_MAX) - 1)
#define EV_UINT32_MAX((ev_uint32_t)0xffffffffUL)
#define EV_INT32_MAX ((ev_int32_t) 0x7fffffffL)
#define EV_INT32_MIN ((-EV_INT32_MAX) - 1)
#define EV_UINT16_MAX((ev_uint16_t)0xffffUL)
#define EV_INT16_MAX ((ev_int16_t) 0x7fffL)
#define EV_INT16_MIN ((-EV_INT16_MAX) - 1)
#define EV_UINT8_MAX 255
#define EV_INT8_MAX 127
#define EV_INT8_MIN ((-EV_INT8_MAX) - 1)
EV_UINT64_MAX是需要使用位操作才能得到的。因為對於UL(unsigned long)型別說,是可移植的最大值了。因為對於32位的OS來說,long型別位寬是32位的,64位的OS,long是64位的。對於0xffffffffUL這個只有32位的字面值來說保證了可移植性。接著把其強制轉換成ev_uint64_t型別,此時就有了64位寬,無論是在32位的系統還是64位的系統。然後再利用位操作達到目的。
有符號型別size_t:
Libevent定義了ev_ssize_t作為有符號size_t的相容型別。因為在遵循POSIX標準的系統中,將有符號的size_t定義為ssize_t,而Windows系統則定義為SSIZE_T。其的具體實現是,在util.h檔案中如下定義:
#ifdef _EVENT_ssize_t
#define ev_ssize_t _EVENT_ssize_t
#else
#define ev_ssize_t ssize_t
#endif
然後在Windows系統的event-config.h檔案中,則有下面的定義
#define _EVENT_ssize_t SSIZE_T
同樣,Libevent也給ev_size_t和ev_ssize_t定義了範圍:
#if _EVENT_SIZEOF_SIZE_T == 8
#define EV_SIZE_MAX EV_UINT64_MAX
#define EV_SSIZE_MAX EV_INT64_MAX
#elif _EVENT_SIZEOF_SIZE_T == 4
#define EV_SIZE_MAX EV_UINT32_MAX
#define EV_SSIZE_MAX EV_INT32_MAX
#elif defined(_EVENT_IN_DOXYGEN)
#define EV_SIZE_MAX ...
#define EV_SSIZE_MAX ...
#else
#error "No way to defineSIZE_MAX"
#endif
#define EV_SSIZE_MIN((-EV_SSIZE_MAX) - 1)
偏移型別:
Libevent定義了ev_off_t作為相容的偏移型別。其實現也很簡單。
#ifdef WIN32
#define ev_off_t ev_int64_t
#else
#define ev_off_t off_t
#endif
socket型別:
按照Libevent的說法,除了Windows系統外,其他OS的套接字型別大多數都是int型別的。而在Windows系統中,為SOCKET型別,實際為intptr_t型別。所以Libevent的實現也很簡單:
#ifdef WIN32
#define evutil_socket_t intptr_t
#else
#define evutil_socket_t int
#endif
socklen_t型別:
在Berkeley套接字中,有一些函式的引數型別是socklen_t型別,你不能傳一個int或者size_t過去。但在Windows系統中,又沒有這樣的一個型別。比如bind函式。在使用Berkeley套接字的系統上,該函式的第三個引數為socklen_t,而在Windows系統上,該引數的型別只是簡單的int。為此,Libevent定義了一個相容的ev_socklen_t型別。其實現為:
#ifdef WIN32
#define ev_socklen_t int
#elif defined(_EVENT_socklen_t)
#define ev_socklen_t _EVENT_socklen_t
#else
#define ev_socklen_t socklen_t
#endif
通用可以在event-config.h檔案中找到_EVENT_socklen_t的定義,要在沒有定義socklen_t系統的event-config.h檔案中才能找到該定義。
/* Define to unsigned int if you donthave it */
#define _EVENT_socklen_t unsigned int
指標型別:
intptr_t是一個很重要的型別,特別是在64位系統中。如果你要對兩個指標進行運算,最好是先將這兩個指標轉換成intptr_t型別,然後才進行運算。因為在一些64位系統中,int還是32位,而指標型別為64位,所以兩個指標相減,其結果對於32位的int來說,可能會溢位。
為了相容,Libevent定義了相容的intptr_t型別。
#ifdef _EVENT_HAVE_UINTPTR_T
#define ev_uintptr_t uintptr_t
#define ev_intptr_t intptr_t
#elif _EVENT_SIZEOF_VOID_P <= 4
#define ev_uintptr_t ev_uint32_t
#define ev_intptr_t ev_int32_t
#elif _EVENT_SIZEOF_VOID_P <= 8
#define ev_uintptr_t ev_uint64_t
#define ev_intptr_t ev_int64_t
#elif defined(_EVENT_IN_DOXYGEN)
#define ev_uintptr_t ...
#define ev_intptr_t ...
#else
#error "No way to defineev_uintptr_t"
#endif
從程式碼中可以看到,如果系統本身有intptr_t型別的話,那麼Libevent直接使用之,如果沒有,那麼就選擇一個完全能放得下指標的型別。
在event-config.h中,_EVENT_SIZEOF_VOID_P被定義成sizeof(void* ),即一個指標型別的位元組數。一般來說,在32位系統中,為4位元組; 在64位系統中,為8位元組。在Windows版本的event-config.h檔案中,定義如下:
/* The size of `void *', as computed by sizeof. */
#ifdef _WIN64
#define _EVENT_SIZEOF_VOID_P 8
#else
#define _EVENT_SIZEOF_VOID_P 4
#endif
在我的Linux(32位),直接定義為:
/* The size of `void *', as computed bysizeof. */
#define _EVENT_SIZEOF_VOID_P 4
讀者可以谷歌一下"intptr_t"和" LP32 ILP32 LP64 LLP64 ILP64"。
相容函式:
時間函式:
在《Libevent時間管理》一文中,列出了一些基本的時間操作函式。這裡就不重複了。在Libevent還定義了一個evutil_gettimeofday函式,那篇文章並沒有展開講。
該函式作為一個相容函式可以在各個平臺上獲取系統時間。在非Windows平臺上,可以直接使用函式gettimeofday。Windows則通過_ftime函式獲取系統時間,然後轉換。實現如下:
//util.h檔案
#ifdef _EVENT_HAVE_GETTIMEOFDAY
#define evutil_gettimeofday(tv, tz) gettimeofday((tv), (tz))
#else
struct timezone;
int evutil_gettimeofday(struct timeval *tv, struct timezone *tz);
#endif
//evutil.c檔案
#ifndef _EVENT_HAVE_GETTIMEOFDAY
/* No gettimeofday; this muse be windows. */
int
evutil_gettimeofday(struct timeval *tv, struct timezone *tz)
{
struct _timeb tb;
if (tv == NULL)
return -1;
_ftime(&tb);
tv->tv_sec = (long)tb.time;
tv->tv_usec = ((int)tb.millitm) * 1000;
return 0;
}
#endif
socket API函式:
由於遵循POSIX標準的OS有“一切皆檔案”的思想,所以在處理socket 的時候比較簡單。而在Windows平臺,處理socket就沒這麼簡單。而且Windows也沒有很好地相容Berkeley socket API。為了統一相容,Libevent定義了一些通用的函式。
Libevent定義了通用函式有下面這些:
int evutil_make_socket_nonblocking(evutil_socket_t sock);
int evutil_make_listen_socket_reuseable(evutil_socket_t sock);
int evutil_make_socket_closeonexec(evutil_socket_t sock);
int evutil_closesocket(evutil_socket_t sock);
int evutil_socketpair(int d, int type, int protocol, evutil_socket_t sv[2]);
EVUTIL_SOCKET_ERROR();//巨集定義
EVUTIL_SET_SOCKET_ERROR(errcode);//巨集定義
上面的函式中,除了evutil_socketpair其他的都沒有什麼好講的。因為都是一些Linux和Windows系統程式設計的基礎內容。
在遵循POSIX的系統已經定義了socketpair,所以直接使用即可。但在Windows中並沒有定義socketpair。Libevent的處理方法是:使用普通的socket,在函式內部建立一個伺服器socket和客戶端socket,並讓客戶端連線上伺服器。其中,IP地址使用環路地址(ipv4中就是那個127.0.0.1,ipv6是::1),埠號則由核心自動選定。實現起來還是有點麻煩的。因為具體的實現就是一個簡單的C/S模式程式碼,這裡就不貼程式碼了。
Libevent還定義了另外三個socket相關的通用操作函式。
const char *evutil_inet_ntop(int af, const void *src, char *dst, size_t len);
int evutil_inet_pton(int af, const char *src, void *dst);
int evutil_parse_sockaddr_port(const char *str, struct sockaddr *out, int *outlen);
inet_ntop主要的一個功能是,它可以用於ipv6。對於ipv4,有inet_aton和inet_ntoa。這兩個函式在POSIX和Windows中都是有的,不需要Libevent做什麼工作。但對於inet_ntop和inet_pton,Windows中並沒有提供(POSIX提供了)。 Libevent也是可以用於ipv6的。所以Libevent就定義了兩個通用的函式。在實現上,Libevent也是自己寫程式碼將之轉換。這裡也不貼程式碼了。
evutil_parse_sockaddr_port函式是用來解析一個字串的。字串的格式是,IP:port。比如,8.8.8.8:53。這個函式的作用就是將字串所表示的ip和埠進行解析,並存放到引數out所指向的結構體上。之前,我們需要手動將一個ip和埠賦值給sockaddr_in 結構體上。現在有這個函式,這工作不用我們做了。第三個引數是一個 值-結果 引數。即呼叫函式時,它的值是第二個引數out指向空間的大小。函式返回後,它的值指明該函式寫了多少位元組在out上。具體的實現這裡也不說了。
結構體偏移量:
這個函式的功能主要是求結構體成員在結構體中的偏移量。定義如下:
#ifdef offsetof
#define evutil_offsetof(type, field) offsetof(type, field)
#else
#define evutil_offsetof(type, field) ((off_t)(&((type*)0)->field))
#endif
其中,type表示結構體名稱,field表示成員名稱。可以看到,Libevent還是優先使用所在系統本身提供的offsetof函式。Libevent自己實現的版本也是很巧妙的。它用(type*)0來讓編譯器認為有個結構體,它的起始地址為0。這樣,編譯器給field所在的地址就是編譯器給field安排的偏移量。
這個求偏移量的功能是Libevent是很有用的。不過,Libevent不是直接使用這個巨集evutil_offsetof。而是使用巨集EVUTIL_UPCAST。
//util-internal.h檔案
#define EVUTIL_UPCAST(ptr, type, field) \
((type *)(((char*)(ptr))- evutil_offsetof(type, field)))
這個巨集EVUTIL_UPCAST的作用是通過成員變數的地址獲取其所在的結構體變數地址。比如有下面的結構體
struct Parent
{
struct Children ch;
struct event ev;
};
假如變數child是struct Children型別指標,並且它是struct Parent結構體的成員。而且知道了child的地址,現在想獲取child所在結構體的struct Parent的地址。此時就可以用EVUTIL_UPCAST巨集了。如下使用就能轉換了。
struct Parent *par = EVUTIL_UPCAST(child, struct Parent, ch);
//展開巨集後,如下
struct Parent *par = ((struct Parent *)(((char*)(child)) - evutil_offsetof(struct Parent, ch)));
其中,並不需要ch為struct Parent的第一個成員變數。
EVUTIL_UPCAST巨集的工作原理也是挺簡單的,成員變數的地址減去其本身相對於所在結構體的偏移量就是所在結構體的起始地址了,再將這個地址強制轉換成即可。
參考:
本文來自 luotuo44 的CSDN 部落格 ,全文地址請點選:https://blog.csdn.net/luotuo44/article/details/38780157?utm_source=copy