序言
译者:这份指南是在阅读 OpenResty® C Coding Style Guide 文章后翻译的。借此机会学习与理解英语、NGINX 编码风格。
OpenResty 在它的 C 语言组件中遵循 NGINX 的编码风格,如 OpenResty 自己的 NGINX 模块和 Lua 库的 C 语言部分。遗憾的是,即使是 NGINX 自己的核心 C 源码也没能严格的遵守与其他代码库相同的编码风格。所以希望准备一份正式指导手册,以避免产生歧义。
贡献给 OpenResty 核心项目的补丁应始终遵循这份指南,不然它将无法通过代码检查,也无法按原样合并。在使用 C 开发自己的模块和库时,OpenResty 和 NGINX 社区始终鼓励遵循这份指南。
命名约定
NGINX 源代码中的命名规则应该始终使用完整名称,包括源码文件名(.c
, .h
)、全局变量、全局函数、C 结构体/联合/枚举、静态变量/函数以及 .h
头文件内的公共宏定义。这一点很重要,因为 C 没有像 C++ 中那样的显式名称空间的概念。使用完整名称有助于调试,避免符号冲突。例如,ngx_http_core_module.c
, ngx_http_finalize_request
, NGX_HTTP_MAIN_CONF
。
在 Lua 库的 C 组件中,我们也应该使用类似 resty_blah_
(如果库名为 lua-resty-blah
)的字符串作为编译单元中的所有 C 符号的前缀。
我们在 C 函数中定义局部变量的时候应该使用短名称。在 NGINX 核心代码中有大量常用的短名称,比如,cl
, ev
, ctx
, v
, p
, q
等等。这些变量一般都是临时的、局部的。根据霍夫曼原则,我们应该在当前上下文环境中使用通用的短名称来避免单行干扰。即使是短名称也应该遵循 NGINX 的约定。不要自己创造新的规则,如果需要,且使用有意义的名称。即使是 p
和 q
,它们也是字符串指针变量的常用名称,在字符串处理上下文环境当中使用。
C 语言结构体和联合名称尽可能的使用单词的完整拼写形式(除非成员名称太长)。例如,NGINX 核心结构体 ngx_http_request_s
当中有像 read_event_handler
, upstream_states
, request_body_in_persistent_file
这样很长的成员名称。
我们应该在 typedef
类型名称时使用 _t
结尾,定义结构体名称时使用 _s
结尾,定义枚举类型时使用 _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 空格缩进。以下是 NGINX 核心的例子:
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
这样的。它们在定义的时候也有一些要求。
它们应总是写在每个 C 函数定义块开始的位置,且不仅仅只在任意代码块开头,除非为了调试或者有其他特殊要求。还有,它们的变量标识符(除 *
前缀字符以外),必须垂直对其。以下是 NGINX 核心的示例:
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];
请注意 value
, 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 函数定义、多行全局/静态变量、结构体/联合/枚举定义必须使用 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;
静态函数声明遵循 C 全局变量声明规则,也使用 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);
即便有些 C 函数声明跨多行也是如此,例如:
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);
前者大多与响应头相关,后者与请求体相关。
类型转换
将 void 指针 (void *
) 赋值给非 void 指针,C 语言是不要求显式类型转换的。NGINX 编码风格中也不要求。例如:
char *
ngx_http_types_slot(ngx_conf_t *cf, ngx_command_t *cmd, void *conf)
{
char *p = conf;
...
}
上面的 conf
变量是个 void 指针,NGINX 核心将它赋值给 char *
类型的局部变量 p
,且没有任何的显式类型转换。
当需要显式类型转换时,请确保类型名称与 *
字符之间有个空格,)
字符之后有个空格符,例如:
*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 在使用 C if
语句的时候会有一些风格要求。
首先,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
语句必须始终使用花括号,即使 "then" 分支只有一条语句。例如:
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 {
是如何放在同一行上的,并且在 } else {
之前有一个空行。
for 语句
for
语句风格与 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 语句章节中的内容类似。
在 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 语句章节中的内容类似。在 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
语句之间加空行。
确保不要省略动态内存分配语句后的 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 核心有时没这么做。
全局/静态变量
在局部变量、顶层静态变量定义和声明时,类型符号和变量标识符之间加至少 2 个空格符(包括前导符 *
)。
以下是些例子:
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
};
运算符
二元运算符
在大多数 C 的二元运算符前后,都要加个空格符,比如:四则运算符、位运算符、关系运算符、逻辑运算符。以下是些例子:
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
语句条件上下文、多个类型相同的变量声明当中使用逗号。而在其他情况下,最好将逗号表达式拆分成独立的语句。
一元运算符
通常在前缀一元运算符的前后是不加空格的。以下是些例子:
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
指令定义主体部分之前有至少 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 源代码树,解决本指南中涉及的许多(但不是全部)风格问题。 它是 OpenResty 核心开发人员必备的,对所有的 NGINX 模块开发人员和 NGINX 核心黑客也有所帮助。 我们一直为此工具添加更多的检查器,我们也欢迎您的贡献。
Clang 静态代码分析器对捕获细微的编码问题也非常有帮助,因此可使用 gcc 的高优化标志来编译所有内容。
现在许多编辑器都有高亮或自动删除行尾空格符、制表符转空格符的功能。在 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
语句的用法感到害怕,这是有点不合理的。
使用 goto
语句向后跳是一件坏事,其他情况下通常没问题,特别是错误处理。
NGINX 要求 goto 标签行前后加空行,如
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
。尽可能遵循此约定。
另外,还建议使用 p != NULL
替代 p
来检查相反的情况,不过在这种情况下简单地用 p
进行检查也没关系。
以下是些例子:
if (addrs != NULL) {
if (name == NULL) {
对 NULL
的检查通常会更清楚地了解这个值的意思,从而有助于提高代码可读性。
作者
这份指南的作者是 OpenResty 的创建者 Yichun Zhang。
反馈与补丁
随时欢迎反馈和补丁!它们应提交至 Yichun Zhang 的电子邮件地址 yichun@openresty.com
。