【譯】OpenResty C 編碼風格指南
OpenResty在C語言模塊遵守NGINX的編碼風格,比如OpenResty本身的NGINX插件模塊或者OpenResty的Lua庫中C部分。然而,即便是NGINX自身的C語言核心代碼風格並沒有與基礎代碼保持一致。所以,本文擬定一份指南性的文檔以便消除各種歧義。
提交給OpenResty核心項目的補丁需要遵守此指南,否則不會通過評審進程,更不用說歸並在一起。OpenRest和NGINX社區都鼓勵開發者在使用C語言開發自身模塊或庫時遵守此指南。
命名規範
對於NIGINX相關的C代碼,源碼文件名字(包括.c
和.h
文件)、全局變量、全局函數、C語言中的結構/聯合體/枚舉的名字、編譯單元作用域的靜態變量和函數塊,還有在頭文件中公開定義的宏,以上這些都是全量命名。比如
ngx_http_core_module.c
ngx_http_finalize_request
和 NGX_HTTP_MAIN_CONF
這是十分重要的,是因為在C語言中並不像C++中有顯示的名字空間。使用全量性名字可以避免名字沖突,而且有助於調試。在Lua庫中的C組件,對於所有在相關C編譯單元中頂層C標簽中我們同樣使用前綴,比如resty_blah_
(如果庫的名稱是lua-resty-blah
)。
在C函數中聲明局部變量使用的短名字。在NGINX核心組件中廣泛使用短變量名字是:cl
、ev
、p
和 q
等等。這些變量名通常是生命周期比較短並且是有限的作用域。根據Huffman原則,在當前上下文中對於常用的地方我們使用短變量名可以避免行噪音。但短變量名需要遵守NGINX的風格。不要自我發明除非必要,但必須使用含義明確的名字。對於p
q
,它們經常用在處理字符串處理上下文中的字符串指針變量名。
一旦需要C結構體和聯合體,全寫形式命名之,除非成員的名字很長。比如,在NGINX中 struct ngx_http_request_s
,有很長的成員變量名字: read_event_handler
、upstream_states
、
和 request_body_in_persistent_file
等。
對於typedef
指向的結構體的變量名以_t
作為後綴,結構體名以_s
作為後綴;對於typedef
指向枚舉的變量名以_e
作為後綴。在函數作用域中定義的局部變量可以不必遵守此約定。下面是NGINX核心組件的代碼:
typedef struct ngx_connection_s ngx_connection_t; typedef struct { WSAOVERLAPPED ovlp; ngx_event_t *event; int error; } ngx_event_ovlp_t; struct ngx_chain_s { ngx_buf_t *buf; ngx_chain_t *next; }; typedef enum { ngx_pop3_start = 0, ngx_pop3_user, ... ngx_pop3_auth_external } ngx_pop3_state_e;
縮進
NGINX中使用空格作為縮進,而不是使用制表符。通常我們使用4個空格,除了某些地方有對齊要求或者在特定的類中需要(我們會在後面詳細討論這些示例)。總之,合理的縮進你的代碼。
每行限定80個字符
所有的源碼行必須控制在80個字符內(盡管在NGINX中有些地方保持是78個,但建議定死80個字符)。不同場景下,對於縮進在連續行中有不同的縮進規則。我我們將在下面詳細討論之。
每行結尾無空白符
代碼行尾不應有任何空格、制表符,甚至空行。很多編輯器會在用戶設置下對空白字符自動高亮或者截斷。合理地配置你的編輯器或者IDE。
函數聲明
將頭文件或者.c
文件頭部的C函數聲明(不是定義!)盡量放在一行。下面是來自NGINX中的示例:
ngx_int_t ngx_http_send_special(ngx_http_request_t *r, ngx_uint_t flags);
若一行太長,超過了80個字符,可以使用4個空格符將其分隔多行。比如:
ngx_int_t ngx_http_filter_finalize_request(ngx_http_request_t *r,
ngx_module_t *m, ngx_int_t error);
若返回類型是指針類型,需要*
之前有個空格而不是其後,如下所示:
char *ngx_http_types_slot(ngx_conf_t *cf, ngx_command_t *cmd, void *conf);
註意函數定義與聲明是不同的風格, 更多可以參考 函數定義部分 。
函數定義
C函數定義編碼風格與聲明是不同的(參看函數聲明部分)。返回類型獨占一行,第二行是函數的名稱和參數列表;{
獨占第三行。下面是NGINX中的代碼示例:
ngx_int_t
ngx_http_compile_complex_value(ngx_http_compile_complex_value_t *ccv)
{
...
}
註意一點的是:在 (
前後都沒有空格,並且在前三行無任何縮進。
如果參數列表很長,比如超過了80個字符,以不同的行分隔之,每一行前縮進4個空格。示例:
ngx_int_t
ngx_http_complex_value(ngx_http_request_t *r, ngx_http_complex_value_t *val,
ngx_str_t *value)
{
...
}
若返回的類型是指針類型,在*
之前有一個空格,如下所示:
static char *
ngx_http_core_pool_size(ngx_conf_t *cf, void *post, void *data)
{
...
}
局部變量
在命名規範部分,要求局部變量使用短名字,比如ev
、clcf
等。通常它們放在每個函數定義個開始處,而不是在任何代碼塊的開始處,除非有助於天使或者其他特殊的需求。同樣,函數內的標誌符(除了*
前綴),必須垂直對齊。比如:
ngx_str_t *value;
ngx_uint_t i;
ngx_regex_elt_t *re;
ngx_regex_compile_t rc;
u_char errstr[NGX_MAX_CONF_ERRSTR];
註意標誌符i
、re
、rc
和 errstr
是如何垂直對齊,但*
前綴不包括在內。
可能會出現超長的局部變量名,若是一同對齊,會導致代碼超級難看。可以將長局部變量名與其他局部變量之間插入空行分隔開來。此時,兩組標誌符,沒有必要垂直對齊。示例如下:
static char *
ngx_http_core_open_file_cache(ngx_conf_t *cf, ngx_command_t *cmd, void
*conf)
{
ngx_http_core_loc_conf_t *clcf = conf;
time_t inactive;
ngx_str_t *value, s;
ngx_int_t max;
ngx_uint_t i;
...
}
可以看出clcf
定義與其他局部變量是分開的,其他的局部變量仍舊垂直對齊的。
在C函數中,局部變量後面與真正執行的代碼之間使用空行分隔的。比如:
u_char * ngx_cdecl
ngx_sprintf(u_char *buf, const char *fmt, ...)
{
u_char *p;
va_list args;
va_start(args, fmt);
p = ngx_vslprintf(buf, (void *) -1, fmt, args);
va_end(args);
return p;
}
正好有一空行在局部變量定義之後。
空行的使用
連續的C函數定義、多行全局/靜態變量定義和struct/union/enum
定義必須使用2個空行分隔。下面是連續的C函數定義:
void
foo(void)
{
/* ... */
}
int
bar(...)
{
/* ... */
}
下面是多個連續的靜態變量定義的例子:
static ngx_conf_bitmask_t ngx_http_core_keepalive_disable[] = {
...
{ ngx_null_string, 0 }
};
static ngx_path_init_t ngx_http_client_temp_path = {
ngx_string(NGX_HTTP_CLIENT_TEMP_PATH), { 0, 0, 0 }
};
僅有一行變量定義必須組織在一起,比如:
static ngx_str_t ngx_http_gzip_no_cache = ngx_string("no-cache");
static ngx_str_t ngx_http_gzip_no_store = ngx_string("no-store");
static ngx_str_t ngx_http_gzip_private = ngx_string("private");
下面是多個結構體的定義:
struct ngx_http_log_ctx_s {
ngx_connection_t *connection;
ngx_http_request_t *request;
ngx_http_request_t *current_request;
};
struct ngx_http_chunked_s {
ngx_uint_t state;
off_t size;
off_t length;
};
typedef struct {
ngx_uint_t http_version;
ngx_uint_t code;
ngx_uint_t count;
u_char *start;
u_char *end;
} ngx_http_status_t;
所有的都是以2個空行分隔的。
若不同類型的頂層對象定義也是需要用2行空行分隔,比如:
#if (NGX_HTTP_DEGRADATION)
ngx_uint_t ngx_http_degraded(ngx_http_request_t *);
#endif
extern ngx_module_t ngx_http_module;
在全局變量聲明後面使用2行空行分隔靜態函數聲明。
多個C函數聲明不比適用兩行空行彼此分隔,如下所示:
ngx_int_t ngx_http_discard_request_body(ngx_http_request_t *r);
void ngx_http_discarded_request_body_handler(ngx_http_request_t *r);
void ngx_http_block_reading(ngx_http_request_t *r);
void ngx_http_test_reading(ngx_http_request_t *r);
甚至在跨越多行的情況下也是如此。比如:
char *ngx_http_merge_types(ngx_conf_t *cf, ngx_array_t **keys,
ngx_hash_t *types_hash, ngx_array_t **prev_keys,
ngx_hash_t *prev_types_hash, ngx_str_t *default_types);
ngx_int_t ngx_http_set_default_types(ngx_conf_t *cf, ngx_array_t **types,
ngx_str_t *default_type);
有時候,我們需要根據語意將函數聲明分組,然後使用2行空行分隔,這樣有助於代碼可讀性,比如:
ngx_int_t ngx_http_send_header(ngx_http_request_t *r);
ngx_int_t ngx_http_special_response_handler(ngx_http_request_t *r,
ngx_int_t error);
ngx_int_t ngx_http_filter_finalize_request(ngx_http_request_t *r,
ngx_module_t *m, ngx_int_t error);
void ngx_http_clean_header(ngx_http_request_t *r);
ngx_int_t ngx_http_discard_request_body(ngx_http_request_t *r);
void ngx_http_discarded_request_body_handler(ngx_http_request_t *r);
void ngx_http_block_reading(ngx_http_request_t *r);
void ngx_http_test_reading(ngx_http_request_t *r);
前面一組基本上是響應頭部的函數聲明,後面一組是請求體相關的函數聲明。
類型轉換
在C語言中將void
指針(void *
)賦值給非void
指針並沒有要求顯示轉換。NGINX中的編碼風格也是如此。比如:
char *
ngx_http_types_slot(ngx_conf_t *cf, ngx_command_t *cmd, void *conf)
{
char *p = conf;
...
}
示例中conf
變量是void
指針類型,將其賦值給一個char *
局部變量,並沒有使用任何顯示類型轉換。
確實需要顯示類型轉換的時候,確保在目標指針類型名字的第一個*
前加一個空格
,同樣在)
後面加入一個空格。比如:
*types = (void *) -1;
示例中在*)
之前有一個空格,同樣在)
後面也有一個。此規則同樣適用於將一個值進行類型轉換。比如:
if ((size_t) (last - buf) < len) {
...
}
或者多個連續的類型轉換:
aio->aiocb.aio_data = (uint64_t) (uintptr_t) ev;
註意(uint64_t)
與 (uintptr_t)
之間的空格,還有在(uintptr_t)
之後的空格。
If
語句
NGINX 適用的if
語句與C語言中的編碼風格是一樣的。
首先,在 if
後面必須有一個空格;再者()
與{
之間有空格。如下:
if (a > 3) {
...
}
可以看出,在 if
後面有一個空格;在 {
之前有一個空格。然而在(
右邊和)
左邊是沒有空格。此外,
{
與if
關鍵字在同一行,除非此行超過了80個字符。若是則,我們需要將其分隔多行,{
獨立成行。
如下面的例子:
if (ngx_http_set_default_types(cf, prev_keys, default_types)
!= NGX_OK)
{
return NGX_CONF_ERROR;
}
註意 != OK
與if
語句的條件部分對齊(不包括 (
)。
若邏輯操作符在很長的條件語句中,需要確保連接邏輯操作符在後續行開頭處,並且縮進需體現出條件的嵌套結構。如下:
if (file->use_event
|| (file->event == NULL
&& (of->uniq == 0 || of->uniq == file->uniq)
&& now - file->created < of->valid
#if (NGX_HAVE_OPENAT)
&& of->disable_symlinks == file->disable_symlinks
&& of->disable_symlinks_from == file->disable_symlinks_from
#endif
))
{
...
}
我們可以忽略中間的宏指令,它們並不是if
語句本身的編碼風格。
若在if
語句塊後有其他的語句,通常會在其後空一行。比如:
if (rc != NGX_OK && (of->err == 0 || !of->errors)) {
goto failed;
}
if (of->is_dir) {
...
}
註意空行是如何分離if
語句塊的,或者後面跟著其他的語句:
if (file->is_dir) {
/*
* chances that directory became file are very small
* so test_dir flag allows to use a single syscall
* in ngx_file_info() instead of three syscalls
*/
of->test_dir = 1;
}
of->fd = file->fd;
of->uniq = file->uniq;
類似的,常常會在if
語句前有一空行。比如:
rc = ngx_open_and_stat_file(name, of, pool->log);
if (rc != NGX_OK && (of->err == 0 || !of->errors)) {
goto failed;
}
在代碼塊之間使用空白符,會讓代碼不那麽臃腫。同樣也適用於while
、for
等語句。
盡管只有單條件的If
語句,也需要使用 {}
,比如:
if (file->is_dir || file->err) {
goto update;
}
在例子中絕對不能省略{}
,盡管標準C語言是允許這麽做的。
else
語句
當if
語句有else
分支的時,相關的語句也必須使用{}
包裹。而且} else {
之前有一空行。如下所示:
if (of->disable_symlinks == NGX_DISABLE_SYMLINKS_NOTOWNER
&& !(create & (NGX_FILE_CREATE_OR_OPEN|NGX_FILE_TRUNCATE)))
{
fd = ngx_openat_file_owner(at_fd, p, mode, create, access, log);
} else {
fd = ngx_openat_file(at_fd, p, mode|NGX_FILE_NOFOLLOW, create, access);
}
註意} else {
在同一行,並且在其上方有一空行。
For
語句
for
語句的編碼風格在很多地方與If
語句部分描述的if
語句相似。在for
關鍵字之後和{
之前有空格。另外,語句塊需要用{}
包裹。再者就是在for
的條件部分中的;
後面有空格。下面的例子展示上述的要求:
for (i = 0; i < size; i++) {
...
}
一個特殊情況是無限循環,在NGINX中通常是如下編碼的:
for ( ;; ) {
...
}
或者for
語句條件部分包括,
:
for (i = 0, n = 2; n < cf->args->nelts; i++, n++) {
...
}
或者單獨忽略循環條件:
for (p = pool, n = pool->d.next; /* void */; p = n, n = n->d.next) {
...
}
While
語句
while
語句的編碼風格在很多地方與If
語句部分描述的if
語句相似。在while
關鍵字之後和{
之前有空格。另外,語句塊需要用{}
包裹。如下:
while (log->next) {
if (new_log->log_level > log->next->log_level) {
new_log->next = log->next;
log->next = new_log;
return;
}
log = log->next;
}
Do-while
語句與之類似:
do {
p = h2c->state.handler(h2c, p, end);
if (p == NULL) {
return;
}
} while (p != end);
註意在do
和{
之間存在一個空格,在while
的前後也是存在的。
Switch
語句
switch
語句的編碼風格在很多地方與If
語句部分描述的if
語句相似。在switch
關鍵字之後和{
之前有空格。另外,語句塊需要用{}
包裹。如下:
switch (unit) {
case 'K':
case 'k':
len--;
max = NGX_MAX_SIZE_T_VALUE / 1024;
scale = 1024;
break;
case 'M':
case 'm':
len--;
max = NGX_MAX_SIZE_T_VALUE / (1024 * 1024);
scale = 1024 * 1024;
break;
default:
max = NGX_MAX_SIZE_T_VALUE;
scale = 1;
}
註意case
標簽與switch
關鍵字是垂直對齊的。
有時,會在第一個case
標簽前加入一空行,如下:
switch (c->log_error) {
case NGX_ERROR_IGNORE_EINVAL:
case NGX_ERROR_IGNORE_ECONNRESET:
case NGX_ERROR_INFO:
level = NGX_LOG_INFO;
break;
default:
level = NGX_LOG_ERR;
}
分配內存的錯誤處理
NGINX中有一個很好習慣就是檢查動態內存申請的錯誤。任何地方都如下所示處理:
sa = ngx_palloc(cf->pool, socklen);
if (sa == NULL) {
return NULL;
}
上面的兩條語句使用很頻繁,通常不會在申請語句和if
語句之間加入空行。
確保你從來不會遺漏檢查類似動態申請內存。
函數調用
C函數調用不需要在參數列表的(
和)
前後加入空格,如下所示:
sa = ngx_palloc(cf->pool, socklen);
若函數調用超過了80個字符,需要將參數列表獨立成行,子行中的參數需要與首行參數垂直對齊。如下:
buf->pos = ngx_slprintf(buf->start, buf->end, "MEMLOG %uz %V:%ui%N",
size, &cf->conf_file->file.name,
cf->conf_file->line);
宏
宏定義要求在#define
後有一個空格,而在定義體前面需至少2個空格。如下:
#define F(x, y, z) ((z) ^ ((x) & ((y) ^ (z))))
有時候定義體前需要更多的空格,出於多個相關宏定義之間對齊。如下:
#define NGX_RESOLVE_A 1
#define NGX_RESOLVE_CNAME 5
#define NGX_RESOLVE_PTR 12
#define NGX_RESOLVE_MX 15
#define NGX_RESOLVE_TXT 16
#define NGX_RESOLVE_AAAA 28
#define NGX_RESOLVE_SRV 33
#define NGX_RESOLVE_DNAME 39
#define NGX_RESOLVE_FORMERR 1
#define NGX_RESOLVE_SERVFAIL 2
對於展開多行的宏定義,需要對齊續行符\
。如下:
#define ngx_conf_init_value(conf, default)
if (conf == NGX_CONF_UNSET) { conf = default; }
我們建議將\
放在第78列,盡管NGINX前後不一致。
全局或靜態變量
對於局部變量、頂部的靜態變量的定義和聲明,需要在類型描述符與變量名之間加入至少兩個空格(包括多個前導*
修飾)
下面是一些示例:
ngx_uint_t ngx_http_max_module;
ngx_http_output_header_filter_pt ngx_http_top_header_filter;
ngx_http_output_body_filter_pt ngx_http_top_body_filter;
ngx_http_request_body_filter_pt ngx_http_top_request_body_filter;
同樣地對於帶有初始化的變量定義也是如此,如下:
ngx_str_t ngx_http_html_default_types[] = {
ngx_string("text/html"),
ngx_null_string
};
操作符
二元操作符
很多二元操作符前後需要一個空格。比如,算術運算符、位運算符、關系運算符合邏輯運算符。如下:
yday = days - (365 * year + year / 4 - year / 100 + year / 400);
還有,
if (*p >= ‘0‘ && *p <= ‘9‘) {
對於結構體/聯合體成員操作符 ->
和 .
,在其前後都沒有空格。比如:
ls = cycle->listening.elts;
對於,
操作符,在其後面需要有一個空格,而不是前面:
for (p = pool, n = pool->d.next; /* void */; p = n, n = n->d.next) {
NGINX中通常避免使用,
操作符,除非在 for
語句中聲明多個同樣類型的變量。在其他情況,最好將,
表達式
分成多條語句。
一元操作符
通常C中得一元操作符前後都不會放入任何空格。如下:
for (p = salt; *p && *p != '$' && p < last; p++) { /* void */ }
#define SET(n) (*(uint32_t *) &p[n * 4])
註意,在*
和&
前後都沒放入任何空白符(在第二例子中&
的前面加入了空格,是因為使用了類型轉換,更多地可以參考[類型轉換][#類型轉換]部分)。
對於後綴操作符同樣適用:
for (value = 0; n--; line++) {
三元操作符
三元操作符要求在其前後使用空白符,如同二元操作符一樣。比如:
node = (rc < 0) ? node->left : node->right;
從上面的例子可以看出,當三元操作中的條件部分是一個表達式時,可以加入()
,盡管不是必須的。
結構體/聯合體/枚舉 定義
結構體、聯合體和枚舉定義的風格是相似的,都需要將標誌符垂直對齊,與在局部變量中說明的局部變量對齊類似。我們列舉來自
NGINX核心代碼的例子來說明:
typedef struct {
ngx_uint_t http_version;
ngx_uint_t code;
ngx_uint_t count;
u_char *start;
u_char *end;
} ngx_http_status_t;
與局部變量的定義一樣,我們同樣需要使用空行分開組字段,如下所示:
struct ngx_http_request_s {
uint32_t signature; /* "HTTP" */
ngx_connection_t *connection;
void **ctx;
void **main_conf;
void **srv_conf;
void **loc_conf;
ngx_http_event_handler_pt read_event_handler;
ngx_http_event_handler_pt write_event_handler;
...
};
在此例中,每一組成員變量都是垂直對齊的,但不要求所有的組都一同對齊(盡管我們可以這麽做,如上述例子一般)。
聯合體也是類似的:
typedef union epoll_data {
void *ptr;
int fd;
uint32_t u32;
uint64_t u64;
} epoll_data_t
下面是枚舉類型:
typedef enum {
NGX_HTTP_INITING_REQUEST_STATE = 0,
NGX_HTTP_READING_REQUEST_STATE,
NGX_HTTP_PROCESS_REQUEST_STATE,
NGX_HTTP_CONNECT_UPSTREAM_STATE,
NGX_HTTP_WRITING_UPSTREAM_STATE,
NGX_HTTP_READING_UPSTREAM_STATE,
NGX_HTTP_WRITING_REQUEST_STATE,
NGX_HTTP_LINGERING_CLOSE_STATE,
NGX_HTTP_KEEPALIVE_STATE
} ngx_http_state_e;
Typedef
定義
類似於宏,typedef
定義同樣需要在定義體的前面至少有2個空格,(通常就2個空格)
typedef u_int aio_context_t;
在將typedef
定義分組的時候可以使用多於2個空格將其垂直對齊,這樣可以代碼更為美觀。
typedef struct ngx_module_s ngx_module_t;
typedef struct ngx_conf_s ngx_conf_t;
typedef struct ngx_cycle_s ngx_cycle_t;
typedef struct ngx_pool_s ngx_pool_t;
typedef struct ngx_chain_s ngx_chain_t;
typedef struct ngx_log_s ngx_log_t;
typedef struct ngx_open_file_s ngx_open_file_t;
工具
OpenResty團隊維護ngx-releng工具。它靜態地掃描當前C文件,校驗本文檔所覆蓋的風格,但不全部。`ngx-releng是OpenResty核心開發的必選工具,也有助於
NGINX模塊開發者或者NGINX核心開發者。我們會持續增加更多地校驗功能,同時我們也期待您的加入。
clang靜態代碼分析器對於獲取代碼隱匿問題是一個好幫手,你可以開啟優化配置標誌以編譯一切。
很多編輯器提供了高亮後者自動截除行尾的空白符,也可以將制表符擴展成空格。比如,在vim
中我們可以將下面的
配置放入到 ~/.vimrc
,可以標亮任何行尾空白符:
highlight WhiteSpaceEOL ctermbg=darkgreen guibg=lightgreen
match WhiteSpaceEOL /\s$/
autocmd WinEnter * match WhiteSpaceEOL /\s$/
或者設置一組簡便的屬性:
set expandtab
set shiftwidth=4
set softtabstop=4
set tabstop=4
Goto
語句和代碼標簽
NGINX中使用goto
語句進行錯誤處理是十分明智,這是對臭名昭著的goto
語句使用比較好的場景。許多非資深的C語言程序員對於goto
語句使用存在恐慌,這其實是不公平的。(PS:Dijkstra大神的Go To Statement Considered Harmful,然而在《代碼大全》一書中:
90%情況下用goto都是錯的,但少數情況goto確實有效(比如在錯誤處理中的多重判斷,又如兩個條件判斷和一個else子句的情況),是解決問 題的合理辦法。這時候用goto無妨,但應該加註釋說明理由。
)。使用goto
語句往回跳轉是十分糟糕的,其他是可以的,特別是錯誤處理。NGINX要求代碼標簽包裹在空行中,比如:
p = ngx_pnalloc(pool, len);
if (p == NULL) {
goto failed;
}
...
i++;
}
freeaddrinfo(res);
return NGX_OK;
failed:
freeaddrinfo(res);
return NGX_ERROR;
校驗空指針
在NGINX中,常常使用 p == NULL
,而不是 !p
檢查一個指針的值是否為NULL
。只要校驗指針是否為NULL
的地方就遵守此約定。同理,推薦使用 p != NULL
而不是 p
檢驗指針的值是非NULL
,但是有時候使用 p
也是可以的。
下面是一些例子:
if (addrs != NULL) {
if (name == NULL) {
檢驗 NULL
通常清楚的表明了檢查值屬性,並且提高代碼的可讀性。
作者
本指南由OpenResty的創建者章亦春撰寫。
反饋與建議
有任何反饋或者建議都可以發送郵件章亦春。
【譯】OpenResty C 編碼風格指南