SISS CONCEPTS 0. What's It All About SISS (or "SISS is a SIP Stack" unless I'm stumbling over a better backronym) is an implementation of the Session Initiation Protocol version 2.0 as described in RFC 3261 and a couple of related things. SISS aims at providing a clean yet slender implementation of the protocol in ANSI-C. Our main goals are correctness and stability; we will put these before other things such as performance. This file explains some of the concepts on which SISS is based. There is no particular order. I just wrote some things down when it appeared to become necessary to me. 1. Parts The SIP protocol actually is just one of a hand-full of protocols for internet telephony. SISS will implement the most important of them in order to allow you to built relatively complete telephony applications. Since you may not necessarly need all of them, we have split the whole beast into a number of smaller libraries. Eventually, there will be at least the following parts: syss The system layer. Implements a event driven file and network access layer and most of the things that differ on the various platforms. In theory you should get away with only porting this library. siss The actual SIP stack. Note that the name of the project as a whole is written with capital letters while the stack as part of the project is written with small letters. rirr An RTP stack. fiff An implementation of T.38 fax over IP. For now we have a large part of syss (though some things are still missing -- they will be added when need arises) and bits and pieces of siss. Current plans are to postpone rirr and fiff for a while and to build a SIP proxy after siss is relatively stable. Maybe we will call it pipp. (Who would have thought that we actually get a Farscape reference in there.) 2. Principles 2.1. Objects The below text often refers to objects. Since there now is a lot of programmers around that grew up with OOP it is worth mentioning that this does not refer to objects in the OO sense but rather more to mere ADTs. SISS is not object oriented. 2.2. Function Prototypes Within all of SISS we use a standard way of defining functions. This mostly regards the return value. Functions can have one of two basic return values: an int or a pointer to an object. The former is the standard return value and has meaning as in POSIX, ie., 0 means success, -1 means failure and a value greater than 0 means partly success (which depends on the context of the function and is in most cases not used). The latter is used when objects are created or searched for. Here, a return value of NULL means failure or not found, depending on the context. Thus, most function can return with a failure. You always have to check for this. An error code is transmitted by means of the errno global variable. In particular almost all functions may return ENOMEM when they run out of memory. Other values for errno are usually stated in the function's documentation. Things are different for callbacks. Their return value is usually void because there is no standard way to handle errors within a callback. Thus, a callback has to take care of error handling all by itself. However, some callbacks have to report whether they processed a certain piece of data. These do return an int the meaning of which is documented with the function. 2.3. Documentation Speaking of documentation. There currently is only few. Generally, all public functions should have a comment block before there definition (ie., in the source file, not in the header) explaining their use, arguments and return values in a format similar to the manual pages. Eventually, we will have some tool that looks at the publich headers and the source files and auto-magically creates manual pages from the comment blocks. In other words, there is no doxygen. This is a feature. There currently are some functions that do not have a comment block, especially in the parts we currently work on. Consider this a bug. Also, if the comment block does not fit the function it describes, this is a bug as well. This is the reason why we have the block in the sources and not in the headers. This way you can easily check whether the comment actually describes the function. 2.4. Comments Closely related to documentation is commenting the source code. If you have a look at it you will see very few comments. This is not a lazyness on my part but done on purpose. Granted, there should be more comment blocks introducing the purpose of static functions. However, usually the function's name is chosen such that its purpose is quite clear from its name. Having functions that do not do what you would expect from their names is clearly a Bad Thing and should not happen (consider it a bug). Secondly, there a virtually no comments inside of functions. This again is done purposely. The reasoning behind this is that if you need to explain what you are doing you're code is overly complicated and thus probably broken. This is also true for comments that are headlines for various parts of a function. In this case these parts are to be put into functions of there own and merely function calls are left. This--together with the Functions Do What They Are Called principle--greatly benefits the readability of code by keeping functions short and avoiding deeply nested conditional or looping statements. This, of course, doesn't mean there are no comments at all. Sometimes it is necessary to do slightly evil things (this _is_ C, after all). In such cases the things are marked as such. Also, there are /* XXX */ comments which mark code that could do with a bit of improvement or where things need to be checked. 3. syss The system layer provides basic infrastructure for the other parts. The most important are memory management, string handling and the event driven file and network access. Additionally, there are a couple of extra services such as DNS querying. For now, we keep syss relativly small, ie., we define proper interfaces but keep their implementation as minimalistic as possible. This is simply due to the fact that you cannot see these things work during hacking which is a bit frustrating. So we rather do this later when we can see and measure the outcome of doing things properly. 3.1. Memory Management The principles of memory management are stolen from the Apache httpd. All memory is allocated from pools. All important objects provide such pools which exist for the entire lifetime of the object. When the object is destroyed all memory from its pool is freed. Thus we have what one could call a poor man's garbage collector. The advantage over a real garbage collector is that it isn't remotely as complicated. The drawback, of course, is that you may crash your application if you carelessly allocate memory from the wrong pool. Pools can be subordinate to other pools. In this case they are called subpools. If the pool they belong to is destroyed they and all their memory is destroyed as well. However, you still can destroy them manually. Note that the whole memory managment is currently quite primitive. Have a look in syss/mem.c and you'll see what I mean. Eventually, we will have our own memory manager which will be faster than the malloc(3) we currently use due to the fact that we don't need free and thus don't have to take care of fragmentation and such. But for now, performance isn't really an issue so malloc(3) will do. 3.2. String Handling Handling strings properly is one of the major difficulties when doing programming in C. For this reason we do not use NUL-terminated strings but have our very own string object struct str. It combines a pointer into memory with a size_t that indicates the string's length. Functions are provided in syss/str.h to do all the usual things with struct strs instead of NUL-terminated char pointers. Together with the pool based memory management this way of string handling provides for much easier parsing and building of string based messages (such are used for instance with SIP). For parsing you just keep a struct str that points to the rest of the message and, whenever you successfully have parsed out something move it ahead beyond the end of what you parsed. Likewise, when building a message you pass along a struct str with the rest of the allocated memory. The various steps put their content at the beginning of the string and move it ahead behind the end of what they added. If you use the functions provided in syss/str.h you can be sure that you will never run over the end of your string thus omiting buffer overflow bugs and such. 3.3. The Main System Loop All file and network access is event driven. This means that, first you acquire a pointer to a struct sys_loop, usually by calling sys_create_loop(), then create all sorts of file and network objects and register all sorts of event handlers with those objects and finally hand control over to the system layer by calling sys_loop_run(). At its lowest level, syss only knowns of two kinds of events: timer and file descriptor events. Timers are pretty easy. You give a wake-up time and a function to either sys_register_timer() or sys_register_timeout() and the function is called once the timer is triggered. The function is called only once. Thus, if you need a periodical timer your function has to re-register itself as last thing. File descriptors have an object of their own: struct sys_fd. You create one using sys_create_fd(). Then you can assign callback functions for reading, writing, or exceptions. Once the file descriptor is ready for reading, writing, or has an exceptional state pending, the functions will be called. This is really just a glorified select(2) interface. 3.4. Network Access For access to network file descriptors aka sockets we have a much more sophisticated interface. This differs a little bit from what you have learned about sockets and such, so a few introductory words may be in order. Basically, we use semi-connection oriented access. This means that to send or receive data you always have to create a semi-connection between exactly two endpoints, one local and one remote. With stream sockets real connections are used. With datagram sockets, we have an abstraction layer that hands incoming datagrams to the right semi-connection. These are called links and are handled by struct sys_net_link objects. For this to work, you have to be able to accept new links. For this purpose we use the concept of a listener known from stream sockets provided by an struct sys_net_listener object. All network endpoints are identified through a struct sys_net_address object which can hold all sorts of network addresses, currently we provide IPv4 and IPv6 addresses which consist of an actual address plus a port number. Additionally, it identifies the transport to use, ie., TCP, UDP, TCP over TLS, or SCTP. This is all information that is necessary to create a sys_net_link or sys_net_listener. To create a sys_net_link you use sys_create_net_link() and provide the sys_loop this link should belong to and two net addresses, one local and one remote. To obtain a link through a listener you first create a listener using sys_create_net_listener() and then provide it with an accept callback via sys_register_net_listener_accept(). This callback is called whenever there is a new connection ready to be accepted. To actually accept it, you call sys_net_listener_accept() which will do all the hard work and will then give you a shiny new link ready to read from and write to. Which is what you really want to do: Reading and writing. Because of the connection abstraction for connection-less sockets you can't just read and write. Instead, syss does it for you. Writing is relatively simple: You call sys_net_link_write() with the link to write on, the data to write and a callback function to call when writing is done. The callback has a status parameter which informs you whether writing succeeded. Reading is a bit more difficult. Whenever there is data to be read from a socket, syss reads it, puts it into struct sys_net_frame objects and queues them. It then informs the user of a link through a callback which said user has previously registered with sys_register_net_link_read(). Within this callback function you can aquire the list of queued frames by calling sys_net_link_read(). You can keep the frames as long as you want, which is necessary because with stream sockets a message may arrive in two or more frames so you may have to wait for further data. Once you are done with processing, you give the frames back to the link by calling sys_net_link_read_done(). You don't have to give all frames back nor do they have to be in the same order you received them. Just link all the frames you don't need anymore together via the snf_next field and hand them to sys_net_link_read_done(). 3.5. Resolver Another thing you may need to do is domain name service (DNS) queries. You could do this yourself using the resolver library but this would mean synchronous queries plus you would have to parse all the resource records yourself. Instead, syss provides a resolver which works asynchronously and parses the resource records necessary for SISS. It also provides a query cache which may come in handy for applications that frequently have to resolve names such as SIP proxies. NOTE: Currently, the resolver actually works synchronously and doesn't have a cache. This is one of the points where I didn't like to do more behind-the-scenes stuff then actually necessary. Using the resolver is quite simple. First, you create yourself a resolver object using sys_create_resolver(). You can then ask question using sys_query_resolver(). You provide a callback which is informed when the query is done and either has succeeded or failed. If it did succeed, you will get a struct sys_resolve_answer object which contains the name of your question plus lists of the various useful resource records. It also may contain a pointer to yet another sys_resolve_answer which will be provided in case the query turned up some additional sections. Say you make an SRV query. In this case most nameservers are kind enough to also provide the A and AAAA resource records for the hosts in the SRV resource records through additional sections. These sections then are given to you through the pointers in sra_next. Don't modify any of the data in the answer you get. Doing so you may corrupt the cache or make other nasty things happen. If you are done with the answer, you hand it back to the resolver with sys_resolver_answer_done(). 4. siss The actual SIP stack is called siss (in small letters). It is implemented closely along RFC 3261 and the other relevant documents. 4.1. Basic Message Parsing 4.2. Transport Layer