编写可移植代码
Sofia-SIP的大部分代码都以可移植性方式编写。所有核心模块都采用ANSI C 89规范编写,偶尔加上一些ANSI C 99规范特性。如果存在一些特定平台相关的代码,将它们隔离在一个独立的C文件中,并对外提供包装接口。
SU模块处理针对操作系统特定功能的抽象工作。例如,内存管理、sockets网络通信、多线程和时间相关函数。
ANSI C 99 features
以下两项ANSI C 99特性在Sofia-SIP库中被使用:
- 整型类型
- va_copy()和snprintf()函数
以下ANSI C 99特性不应当在Sofia-SIP库中被使用:
- 在函数的中间定义变量(即,总是在函数的开始处定义变量)。
整型类型
正如我们所知,原生类型存储大小依赖硬件、操作系统和编译器。这就意味着在实践中,int或者long的大小不一定是32位。因此,你必须确认你希望存储在int中的值可以适用不同平台上的int类型。作为一个经验法则,如果整型值会超过8位,你应当使用预定义好大小的类型。
即便你知道了第一段阐述的内容,然而使用原生整型类型也不是不可以。使用原生数据类型的理由是性能。整型类型总是会以最快的方式被存储。
在类型的大小方面永远不要假定任何事情。应当总是使用sizeof操作符来获得大小。
C 99标准定义了如下一些固定长度的数据类型:
- int64_t
- uint64_t
- int32_t
- uint32_t
- int16_t
- uint16_t
- int8_t
- uint8_t
为了使用上述这些类型,必须包含<sofia-sip/su_types.h>头文件,这个头文件的作用是包含正确的文件。如果su头文件没包含的话,必须在使用这些类型的文件内加入下述的代码片断:
#if HAVE_STDINT_H #include <stdint.h> #elif HAVE_INTTYPES_H #include <inttypes.h> #else #error Define HAVE_STDINT_H as 1 if you have <stdint.h>, \ or HAVE_INTTYPES_H if you have <inttypes.h> #endif
引申阅读:http://www.cnblogs.com/zyl910/archive/2012/08/08/c99int.html。
字节序
不同平台的本机字节序是有差异的。如果只是针对本机处理完全可以不用考虑字节序问题。一旦开始编写任何与网络有关的代码,就必须开始考虑这点了。
如果希望转换字节序,很简单只需调用下面所列的函数即可:
htonl()函数将无符号整型从本机字节序转换成网络字节序。
htons()函数将无符号短整型从本机字节序转换成网络字节序。
ntohl()函数将无符号整型从网络字节序转换成本机字节序。
ntohs()函数将无符号短整型从网络字节序转换成本机字节序。
如果需要调用这些函数,应当包含<netinet/in.h>或<sofia-sip/su.h>头文件。
压缩结构体
通常,编译器会调整结构体以便它们可以被最快的速度访问。这意味着结构体内的字段大多数都位于32位整数地址处。如果需要节约内存开销,必须采用压缩结构体方案。
为了告诉编译器只需要确定大小位数空间来存储变量需要用到位字段概念。编译器可能或者不压缩位字段。
struct foo { unsigned bar:5; unsigned foo:2; unsigned :0; int something; }
如果编译器决定压缩这个结构体,bar和foo两个字段会占用头7个位,something字段从下一个32位整数地址处开始。
当采用压缩结构体方案会引发一个问题:在ARM硬件系统上,通常不可能访问一个不是32位整数地址的32位字段。因此,在这个示例中出现了一个填充字段。看上去很容易,但记住:在某些ARM系统的gcc编译器上初始化这个结构体会失败(可以咨询Kai Vehmanen获得更多这方面的细节)。
强制采用压缩结构体的方式是使用预处理器指令:#pragma(pack)。但特定编译器才支持这个指令,因此如果想编写真正的可移植性代码就不能使用它。即便如此,仍然在Sofia-SIP中部分地方使用了这个指令。唯一的替代方法就是编写使用位操作符从32位字段中取出特定位的函数。但这不是很容易且易出错。
如果试图将一个char类型的数组转换成一个int32_t的值,同样的对齐问题也会出现。在ARM平台上只能从32位整数地址处访问int32_t类型的值。
结论是当使用位字段和结构体压缩切忌它们可能引发的陷阱。如果你不是真的需要它们(例如,在解析二进制协议时),不要使用这些技术。
文件和目录结构
一个Sofia-SIP库内的模块可以定义为libsofia-sip-ua目录下的一个子目录,这个目录下包含一个<模块名称>.doc文件。
在你打算开发一个新模块前,请通知Sofia-SIP开发团队以便为你建立起基本的模块框架。
一个模块目录内容的概览:
- <模块名称>.docs文件
模块的主文档文件。更多详情参考http://sofia-sip.sourceforge.net/refdocs/docguide.html页面内的Module documentation in <module>.docs部分。 - 图片子目录
目录中包含模块文档所需的图片文件。图片文档格式有GIF(html使用)或者EPS(latex使用)。如果使用其它程序创建了新的图片,它们也应该存放在这里。
(注意,更老一点的模块可能包含“iamges”子目录而不是“pictures”。) - Makefile.am文件
参考下面的dealing with GNU Autotools部分。 - (可选)模块以及模块测试用源码文件。可能的话这些源码文件也有可能放置在子目录下。
编写面向对象代码
C语言本身并不提供任何关于面向对象特性。但使用C语言以面向对象方式来开发也是可行的。Sofia代码在完全使用C语言的前提下也应用了很多面向对象特性。
数据隐藏
数据隐藏是让两个模块间分隔清晰的一种实践方法(Data hiding is a practice that makes separation between two modules very clear.)。模块外的代码无法直接访问模块内的数据,只能通过模块提供的方法达到访问数据的目的。数据隐藏也让定义两个对象间的协议变得更简单,所有协议均通过函数调用来实现。
C语言中如何实现数据隐藏?最简单的回答是只在头文件中申明数据结构但不定义它们。在<sofia-sip/msg.h>头文件中有一个针对结构体struct msg_s的typedef msg_t类型,但实际的结构体是在msg_internal.h头文件中定义。msg模块外的代码无法直接访问msg_s结构体,只能通过<sofia-sip/msg.h>头文件中提供的函数来访问。msg的实现使得改变结构体内部非常自由,只需保持函数接口不变即可。
接口
抽象接口是在Sofia库中使用的另一项面向对象实践。在<sofia-sip/msg_types.h>头文件中定义的解析器头(Parser headers)是抽象接口的一个很好的示例。消息头类型msg_header_t使用两个结构体来定义:struct msg_common_s(msg_common_t)和struct msg_hclass_s(msg_hclass_t)。
在msg_hclass_t结构体中使用虚函数表方式来实现抽象接口。这有点类似C++中实现抽象类和虚函数的方式。对每个头的实现来说(For implemenation of each header),用负责编码、解码和操作头结构体的函数来初始化函数表。与C++不同,对象(msg_hclass_t)的类用实际数据结构体来表示,结构体可以包括头特定数据,例如头名称。
msg_hclass_t sip_contact_class[] = {{ /* hc_hash: */ sip_contact_hash, /* hc_parse: */ sip_contact_d, /* hc_print: */ sip_contact_e, /* hc_dxtra: */ sip_contact_dup_xtra, /* hc_dup_one: */ sip_contact_dup_one, /* hc_update: */ sip_contact_update, /* hc_name: */ "Contact", /* hc_len: */ sizeof("Contact") - 1, /* hc_short: */ "m", /* hc_size: */ MSG_ALIGN(sizeof(sip_contact_t), sizeof(void*)), /* hc_params: */ offsetof(sip_contact_t, m_params), /* hc_kind: */ msg_kind_append, /* hc_critical: */ 0 }};
继承和后代对象
继承在Sofia库中作为有限使用的面向对象实践出现。继承的通常例子就是su_home_t的使用。很多对象继承自su_home_t,这意味着继承对象可以使用很多来自<su_alloc.h>基于home的内存管理函数。
从这个角度看,继承意味着一个指向继承对象的指针可以强制转换成指向基类对象的指针。或者说,继承类的内存空间最前部必须是基类对象:
struct derived { struct base base[1]; int extra; char *data; };
有下面三种方式执行这种转换:
struct base *base1 = (struct base *)derived; struct base *base2 = &derived->base; struct base *base3 = derived->base;
第三种方式可以正确工作,因为base是只有一个元素的数组。
模板
Sofia库中用宏实现了一些模板类型。他们包括哈希表,在<sofia-sip/htable.h>头文件中。这些宏可以用来定义哈希表类型和各种类型的访问函数和红黑树等,在<sofia-sip/rbtree.h>头文件中定义。
内存管理
为一个给定任务分配大量的内存块时基于home的内存管理将非常高效。分配将通过home内存来执行,home内存保留着每个分配内存块的地址。当home内存被释放时,它也将释放那些它跟踪的内存块。这大大简化了应用代码逻辑,不再需要维护每块分配的内存空间,不需要保留他们的地址也不再需要一个个单独释放。
可以参考<sofia-sip/su_alloc.h>头文件和内存管理指南memory managment tutorial 等内容获得更多关于内存管理服务方面的信息。
上下文数据的内存管理
home内存使用的一个典型例子是在如下所示的context结构体内的第一个成员属性是home内存结构体(su_home_t)。
/* context info structure */ struct context { su_home_t ctx_home[1]; /* memory home */ other_t *ctx_other_stuff; /* example of memory areas */ ... }; /* context pointer */ struct context *ctx; /* Allocate memory for context structure and initialize memory home */ ctx = su_home_clone(NULL, sizeof (struct context)); /* Allocate memory and register it with memory home */ ctx->ctx_other_stuff = su_zalloc(ctx->ctx_home, sizeof(other_t)); ... processing and allocating more memory ... /* Release registered memory areas, home, and context structure */ su_home_zap(ctx->ctx_home);
混合分配
另一个基于home的内存管理、让程序员工作更简单的场景是这样的:当子函数申请了很多次内存分配,当子函数失败时所有的分配必须得到释放,当子函数成功时所有的分配必须交给上层的内存管理控制。
/* example sub-procedure. top_home is upper-level memory home */ int sub_procedure( su_home_t *top_home, ... ) { su_home_t temphome[1] = { SU_HOME_INIT(temphome) }; ... allocations and other processing ... /* was processing successfull ? */ if (success) { /* ok -> move registered allocated memory to upper level memory home */ su_home_move( top_home, temphome ); } /* destroy temporary memory home (and registered allocations) */ /* Note than in case processing was succesfull the memory */ /* registrations were already moved to upper level home. */ su_home_deinit(temphome); /* return ok/not-ok */ return success; }
测试代码
看看<sofia-sip/tstdef.h>头文件获得如何利用Sofia提供的宏来编写模块测试。
这里有一些你应当测试些什么的思路:
- “冒烟测试”
观察模块编译,链接和执行。 - 模块API函数应当这么测试:
- 合法参数
- 不合法的参数
- 100%全代码覆盖
(即便只有一行代码没有测试到,你也不知道这行代码是否能够正确执行)
对被选中的部分代码也应当瞄准100%的分支全路径覆盖。
但是因为各种可能的原因实践中是不可能做到代码全覆盖测试(因此实践中能达到80%就可以了)。 - 创建检查代码中假设和窍门的测试。
例如,如果依赖某项编译器特性,那么就创建一个没有这个特性的编译器会失败的测试。
运行模块测试
Automake被用来构建Sofia SIP,它内建对单元测试的支持。为了让你的模块能自动运行测试,只需在模块的Makefile.am文件中加上如下内容(当然还得编写测试程序):
TESTS = test_foo test_bar check_PROGRAMS = test_foo test_bar test_foo_SOURCES = foo.c foo.h test_foo_LDADD = -L. -lmy test_bar_SOURCES = bar.c bar.h test_foo_LDADD = -L. -lmy
每个测试程序应当在成功时返回0错误时返回非0。当你执行“make check”时my_test_foo和my_test_bar会被构建和运行。Make将打印出测试如何运行的详细信息。因为是从构建系统中运行,所以测试程序必须是非交互性的(没有提问回答)以及不依赖于不在版本控制系统中的文件。
Sofia SIP的顶级makefile包含一个递归检查,因此你只需这么做“cd sofia-sip ; make check"通过一个单独的指令来运行所有的测试。
使用GNU Autotools
Sofia-SIP构建系统基于GNU工具:automake,autoconf和libtool。这个工具集已经成为编译Linux软件事实上的标准。所以有很多公开的有关这些工具的文档,以及很多很多使用范例。
在developer.gnome.org这有一篇关于这些工具的非常好的入门教材。Autobook提供更多有关autoconf和automake的详细内容。同样,GNU make手册也是一个非常好的参考。
autogen.sh
在最高层目录下有一个名为autogen.sh的shell脚本文件。这个脚本调用了一个名为autoreconf的便捷工具,这个工具可为你生成配置脚本。它同样可以修复mode bits: the mode bits are not stored indarcs version control system.(这句啥意思?)
$ sh autogen.sh $ ./configure ... with your configure options ...
configure.ac
configure.ac(老版本的autoconf使用configure.in文件)文件包含autoconf所需的主要配置信息。configure.ac文件包括构建目标模块所需的所有外部库、非标准语言和编译器特性的检查。
这个文件由模块的开发人员负责创建。
m4 files
Sofia-SIP自身的autoconf宏存储在项目顶级目录下的m4目录下。这些宏和其他configure.ac使用的宏都将被一个名为aclocal的工具程序拷贝到模块特定的aclocal.m4文件内。
如果你需要改变这些文件,请联系Sofia-SIP开发团队。
aclocal.m4
aclocal.m4文件内包括将要在文件configure.ac中使用的宏定义。
这个文件由aclocal命令生成。
Makefile.am
Makefile.am文件是你用来定义什么程序和库应当被构建以及由哪些源文件来创建他们的地方。当你运行automake,他会创建Makefile.in文件。
这个文件由模块的开发人员创建。
configure
当执行配置脚本时,执行configure.ac文件中定义的所有检查,并且用对应的xxx文件替换所有的xxx.in文件。使用在配置处理过程中发现的值替换在*.in源文件中所有的@FOO@变量。例如,Makefile.in文件中的变量@srcdir@用源代码目录路径替换(这在主代码目录树外编译时非常有用)。
这个文件由autoconf命令生成。
config.status
这个脚本存储最后一次提供给配置命令的参数。如果需要,你可以使用命令 "./config.status -r"或"./config.status --recheck"再次执行最后一次配置脚本(使用最后一次提供的参数)。
这个文件由配置脚本生成。
config.cache
这个文件包括各种配置脚本执行的各项检查的结果。一旦配置脚本执行失败,你可以试着删除这个文件然后再次执行配置脚本。
这个文件由配置脚本生成。
Makefile
Makefile包括真正如何构建目标库和程序的规则。make程序使用它。当你运行autoconf命令
Makefile脱胎自Makefile.in。确保"make dist"和"make install"能够正确运行。
这个文件由config.status和配置脚本生成。
config.h
这个文件包括各种可配置发布的C语言定义。
这个文件由config.status和配置脚本生成。
sofia-sip/su_configure.h
这个文件包括描述Sofia SIP UA库如何配置的C语言定义。
这个文件由config.status和配置脚本生成。