当前位置: 首页 > 工具软件 > Sofia-SIP > 使用案例 >

Sofia-SIP模块开发指南

公孙宏畅
2023-12-01

源文出处http://sofia-sip.sourceforge.net/refdocs/programming.html

编程指南

编写可移植代码

Sofia-SIP软件代码大部分都是可移植的。所有核心模块都是(至少应该是)遵循ANSI C 89规范的,也用到了一些ANSI C 99特性。如果有平台依赖的部分,都被集中分离到独立的C文件中,并封装接口,与软件其他部分隔离。

SU模块处理OS特定功能的抽象,比如内存管理、socket通信、线程和时间函数。

ANSI C 99 特性

Sofia-SIP软件中用到了以下的ANSI C 99特性:

·   Integer types

·   functions va_copy() and snprintf()

·   整数类型

·   va_copy()snprintf()

Sofia-SIP软件中不得使用以下ANSI C 99特性:

·   definition of a variable in the middle offunction code. (so always define your variables in the beginning of the block)

·   在函数体中间定义变量。(因此,请把变量定义在代码块的开头部分)

 

整数类型

如你所知,本机存储大小的长度取决于硬件、OS和编译器。这意味着实践中,intlong的长度不一定是32位。因此,您需要确保您打算存储在int中的值,实际上可以在不同平台上适用于int。根据经验,如果整数值超过8位,就应该使用具有定义长度的类型。

 

不过,如果你记住上面说的话,那么使用本地整数类型也无妨。根源是本地数据类型往往会有最好的性能表现。Int型总是存储最快的类型(通常也是最大长度)

永远不要假定任何数据类型的长度。坚持使用sizeof()操作符获取长度。

 

Ç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 youhave <stdint.h>, \

 or HAVE_INTTYPES_H if you have<inttypes.h>

#endif

 

字节序

不同平台的主机字节序是不同的。当您只做本地处理时,不需要关心字节顺序。一旦您开始编写网络收发数据的代码,那你就必须考虑(字节序)

 

如果需要转换字节序,很简单,调用下列函数之一就可以了:

函数htonl()会把无符号长整型从主机字节序转换为网络字节序。

函数htons()会把无符号短整型从主机字节序转换为网络字节序。

函数ntohl()会把无符号长整从网络字节序转换为主机字节序。

函数ntohs()会把无符号短整型从网络字节序转换为主机字节序。

 

要调用这些函数,您需要引用头文件<netinet/in.h> <sofia-sip/su.h>

 

压缩结构体

默认情况下,编译器通常会调整结构体布局以提高访问速度。这意味着结构体内的字段会以32位对齐。如果您需要节省内存,可以使用结构体压缩。

 

为了告诉编译器你只需用要特定数量的位来存储一个变量,您可以使用位域。编译器或许会压缩位域,或许不会。

struct foo {

  unsigned bar:5;

  unsigned foo:2;

  unsigned :0;

  int      something;

}

如果编译器决定压缩这个结构,这段代码就会生成一个结构,在前七位中有barfoo,然后从下一个32位边界开始存储结构体的其他字段。

使用位域压缩会有一个问题:在ARM平台下,通常不能访问不是以32位边界开始的32位数据。因此,实例结构中的:0填充字段看起来很方便,但使用要小心,这个字段的初始化在一些ARM编译器上会失败。(详情请咨询Kai Vehmanen)

有一种方式可以强制数据结构压缩使用预处理指令#pragma(pack)。这个指令是编译器相关的,因此,如果你打算写真正的可移植代码,那么最好不要用。尽管我们已经在Sofia-SIP的部分代码中使用了它。唯一的选择是编写函数,通过位操作从32位字段获取所需的位,这不太方便,但是也不容易出错。

    一个char缓存强制转换为int32_t,也会有同样的内存对齐问题。ARM架构下,您只能从32位边界开始读取int32_t,要特别小心。

    使用位域和结构体压缩时,要小心这些陷阱。如果您不需要使用它们(比如说解析二进制协议),尽量不要使用。

 

文件和目录结构

Sofia-SIP库模块可以定义为libsofia-sip-ua目录层次结构下的一个子目录,其中包含一个<modulename> .docs文件(<modulename>当然是模块的实际名称)。

 

如果您想开始开发新模块,请联系Sofia-SIP开发团队,以便他们可以帮您设置基本模块。

 

模块目录的内容概述:

·    <modulename>.docs 
模块相关的主要文档文件。详细信息请参考Module documentation in <module>.docs

  • pictures 子目录
    包含模块文档所需的任何图片/图像。 使用的文件格式是GIF(用于html页面)和EPS(用于latex)。 如果使用某些程序(例如MS Visio)创建图片,则原始文件也必须存储在此处。
    (Note that old modules may have "images" subdirectory instead of "pictures")
    (注意:旧的模块子目录名可能是"images",而不是"pictures")

·   Makefile.am文件
请参处理GNU Autotools一章。

  • (可选)模块和模块测试的源码文件。 如果需要,源代码文件也可以位于子目录中。

 

编写面向对象代码

虽然C语言本身没有提供任何面向对象的特性,但也是可以用C语言进行面向对象编程。Sofia是用C语言实现的,使用了很多面向对象的技术。

数据隐藏

数据隐藏是把两个模块彻底分离的一种方法。模块外部的代码不能直接访问模块内部的数据,只能通过模块提供的接口函数达到目的(访问数据)。数据隐藏同时让我们在两个对象间定义协议变得更容易所有通信都通过函数调用完成。

那么怎么用C语言实现数据隐藏呢?最简单的答案就是在头文件中声明数据结构体,但不要在头文件中定义。在<sofia-sip/msg.h>头文件中,我们用typedef struct msg_s结构体声明了一种名为msg_t 的数据结构,但是它的实际定义内容是放在"msg_internal.h"文件里的。msg模块之外的程序不能直接访问 struct msg_s 结构体,但它们可以通过msg模块在<sofia-sip/msg.h>接口中提供的方法函数访问msg_t对象。msg模块的实现方可以自由地改变结构体内部的布局,只需要保证函数接口的稳定(对外接口不要频繁变更)

 

接口

抽象接口是Sofia采用的另一种面向对象实践。在<sofia-sip/msg_types.h>中定义的解析头域相关实现,是抽象接口的一个良好实例。消息头域 msg_header_t的类型定义,使用了两个C结构体:struct msg_common_s (msg_common_t)struct msg_hclass_s(msg_hclass_t)

    抽象接口是由 msg_hclass_t结构里的虚函数表实现的。有点像C++典型实现的抽象类和虚函数。对于每个头域的实现,需要初始化函数表以实现相应的功能,如解码、编码、操作头域等等。和C++不同的是:对象类(msg_hclass_t)是用一个真实的数据结构描述的,其中包含了头域的特有数据,比如头域的名字。

msg_hclass_tsip_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>中的的各种基础内存管理功能(函数)

在这种情况下,继承意味着可以将指向派生对象的指针可以转换为指向基础对象的指针。换句话说,派生对象必须在其内存区域的开始处放一个基础对象:

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;

 

第三种方法之所以可用,是因为基础对象被处理为一元数组。

 

模板

Sofia库里有一些模板是用宏实现。其中包括<sofia-sip/htable.h>里定义的hash表,它可用于为不同对象定义hash表和访问器;此外还有在<sofia-sip/rbtree.h>定义的红黑树。

 

内存管理

当为特定任务分配大量内存块时,基于主页的内存管理非常有用。分配是通过内存主页完成的,内存主页保留了每个分配的内存块的引用。当内存主页被释放后,将同时释放它所引用的所有内存块。这简化了应用程序逻辑,因为应用程序代码不需要跟踪分配的内存并分别释放每个分配的块。

更多内存管理服务相关的内容,请参考<sofia-sip/su_alloc.h>的文档和memory managment tutorial 

 

上下文数据的内存管理

内存主页用法的的一个典型实例是:在对象中保存一个内存页结构(su_home_t),作为操作上下文信息结构的一部分。

  /* context infostructure */

  struct context {

    su_home_t  ctx_home[1];      /* memory home */

   other_t   *ctx_other_stuff;  /* example of memory areas */

   ...

  };

 

  /* context pointer*/

  struct context *ctx;

 

  /* Allocatememory for context structure and initialize memory home */

 ctx = su_home_clone(NULL, sizeof (struct context));

 

  /* Allocatememory and register it with memory home */

 ctx->ctx_other_stuff = su_zalloc(ctx->ctx_home, sizeof(other_t));

 

 ... processing and allocating more memory ...

 

  /* Releaseregistered memory areas, home, and context structure */

 su_home_zap(ctx->ctx_home);

 

组合分配内存

     主页内存管理让程序员生活变得更轻松的另一个地方是:子程序进行多次的内存分配,一旦子程序执行失败,子程序所分配的所有内存都要被释放;如果子程序执行成功,子程序所分配的内存必须由上层内存管理控制的场景。

  /* examplesub-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 processingsuccessfull ? */

    if (success) {

     /* ok -> move registered allocated memory to upper level memory home */

     su_home_move( top_home, temphome);

   }

    /* destroytemporary memory home (and registered allocations) */

    /* Note than incase processing was succesfull the memory    */

    /* registrationswere already moved to upper level home.     */

    su_home_deinit(temphome);

 

    /* returnok/not-ok */

    return success;

  }

 

测试代码

有关如何使用Sofia提供的宏编写模块测试的示例,请参阅<sofia-sip/tstdef.h>

这里列出您应该测试的几个点:

  • 冒烟测试
    See that the module compiles, links and executes.
    模块编译,链接并执行。

·   模块API函数需要测试:

有效参数

无效参数

  • 100%路径覆盖目标
    (如果有一行代码未经测试,就不参判断程序是否工作。)
    对于代码的选定部分,您还是应该把目标定为100%分支/路径覆盖。
    但无论如何都要合理,因为在实践中完全覆盖几乎不可能实现(所以在实践中80%是可以的)。
     
  • 创建测试来检查假设和/或手段。
    例如:如果代码依赖于某个编译器功能。用没有相应功能编译的测试结果将会失败。
     

运行模块测试

Sofia SIP是用Automake构建的,Automake内置了单元测试的支持。要为你的模块添加一个自动运行的测试程序,只需要在模块的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


    每个测试程序,都应该在成功时返回零,失败时返回非零错误代码。现在,当你执行"make check"命令时, my_test_foo  my_test_bar 将会被编译和运行。Make将打印测试结果的摘要。由于这些测试是由编译系统运行的,所以测试必须是非交互式的(不要提任何问题),并且不要依赖任何不在版本控制系统中的文件。

Sofia SIP的顶层makefile包含一个名为check的递归目标,因此,你可以用一条命令执行所有的测试程序:"cd sofia-sip; make check"

 

处理GNU Autotools

Sofia-SIP的编译体系是基于GNU auto工具建立的,包括automakeautoconflibtool。该工具集已成为构建Linux软件的事实上的方式。正因为如此,有很多这些工具相关的公开文档,当然还有很多很多的例子。

可以从 developer.gnome.org找到这些工具集的一份很好的说明文档。Autobook 提供了autoconfautomake的更多详细信息。 GNUmake manual也是一个很好的信息源。

autogen.sh

在顶层目录下,有一个名为autogen.shshell脚本。它调用一个叫autoreconf 的便利工具,为我们生成configure脚本。它还有固定的模式位,模式位并未存储在 darcs版本控制系统中。

$ sh autogen.sh

$ ./configure ... with your configureoptions ...

configure.ac

configure.ac文件(旧版本autoconf曾用configure.in)包含了autoconf的主要配置。 configure.ac的内容包含一系列检查宏,检查构建目标模块所需的所有外部库,非标准语言和编译器功能。

m4 files

Sofia-SIP自身的autoconf宏保存在顶层目录下,一个叫m4的文件中。这些宏,连同configure.ac使用到的的所有其它宏,都被一个叫aclocal的工具拷贝到一个模块特定的aclocal.m4文件中。

Contact Sofia-SIPdevelopment team, if you need changes to these files.

如果需要修改这些文件的内容,请联系Sofia-SIP开发团队。

aclocal.m4

aclocal.m4文件包含configure.ac中使用的autoconf宏的定义。这个文件由aclocal生成。

Makefile.am

Makefile.am文件里定义了需要编译的程序和库,还有编译这些目标所需要的源文件。当你执行automake命令后,它创建了Makefile.in文件。

这个文件由模块的开发者提供。

configure

运行configure脚本时,它执行configure.ac中所定义的所有检查动作,并把所有的xxx.in文件转换成相应的xxx文件,所有.in文件中的@FOO@描述的变量,也由configuration处理的结果值替换。例如:Makefile.in中的@srcdir@变量,变成了Makefile 中的源码实际路径(在主源代码树之外编译时格外有用)。

这个脚本由autoconf命令生成。

config.status

这个脚本保存了上一次传递给configre命令的参数表。如果需要,你可以通过以下命令返回上一次给定的configure脚本(通过给定的参数)"./config.status -r" 或者 "./config.status --recheck".

这个文件是由configure脚本生成的。

config.cache

这个文件保存了configure执行过程中各种检查的结果值。在configure执行失败时,你可以删除这个文件,并重新执行configure用于检查。

这个文件由configure脚本生成。

Makefile

Makefile 文件包含包含了编译目标库和程序的实际规则。它是供make工具使用的。Makefile 文件以Makefile.in文件为模板生成的,而Makefile.in文件是autoconf 命令生成的。请确保"make dist""make install"这两个目标能正常工作。

这个文件是由config.statusconfigure脚本生成的。

config.h

这个文件包含了C语言定义的各种可配置的编译开关。

这个文件是由config.statusconfigure脚本生成的。

 

sofia-sip/su_configure.h

这个文件包含C语言定义的Sofia SIP UA库的配置。

这个文件是由config.statusconfigure脚本生成的。

 类似资料: