BLOG | DOCUMENTATION | TRAC

Home --> Documentations --> Porting Guide

Porting SIP and Media Stack to Embedded Systems


Last Update: $Date: 2007-01-20 04:23:13 +0000 (Sat, 20 Jan 2007) $ Print Friendly Page


 

PJLIB, PJLIB-UTIL, PJSIP and PJMEDIA libraries (or will be called just PJ libraries) have been designed specificly to be very portable and have very small footprint, to make it ideal to be used on embedded or even deeply embedded system development.

This article describes the challenges faced when porting PJ software to these "non-standard" platforms and presents some strategies to maximize the chance of successful porting efforts.

 

Table of Contents

Porting Considerations

This section describes components that need to be considered when porting PJ libraries to new platform, especially the embedded or small-footprint systems.

  

Target CPU

For this porting purpose, the target CPU determines the characteristic of the basic data types (more specifically the width and endianness) and the availability of floating point co-processors. 

Instruction Set

This normally is not something that we should worry about since all libraries are written on C language (there is no assembly instructions at all in the libraries).

Data Types

PJLIB encapsulates all integral data types into the corresponding pj_* data types in <pj/types.h>. In addition, 64bit integral data types are defined in the appropriate compiler specific header file (e.g. <pj/compat/cc_gcc.h> for GCC, <pj/compat/cc_msvc.h>for MSVC).

Currently the data type mapping is defined below.

PJ data types Description Availability Mapped to..
pj_int8_t, pj_uint8_t 8-bit signed/unsigned integer always defined char, unsigned char
pj_int16_t, pj_uint16_t 16-bit signed/unsigned integer always defined short, unsigned short
pj_int32_t, pj_uint32_t 32-bit signed/unsigned integer always defined int, unsigned int
pj_int64_t, pj_uint64_t 64-bit signed/unsigned integer only when PJ_HAS_INT64 is defined (defined by compiler specific header file). long long/int64_t, unsigned long long/uint64_t
pj_ssize_t, pj_size_t At least 32 bit, may be 64 bit. always defined long, size_t
pj_bool_t Boolean always defined int
pj_char_t Native character type for given platform. always defined char or wchar_t, depending on the value of PJ_NATIVE_STRING_IS_UNICODE.
(no PJ data type is defined) Floating point - float
(no PJ data type is defined) Double precision floating point - double

The above mappings should work on most (if not all) 32-bit or 64-bit CPU and across different types of compilers, however these may not be accurate for 8-bit or 16-bit CPUs (the int/unsigned int data type may be less than 32-bit wide), therefore developer MUST review and possibly fix these mappings before attempting to compile any codes.

Endianness

The PJLIB macros PJ_IS_LITTLE_ENDIAN and PJ_IS_BIG_ENDIAN MUST be declared correctly according to the endianness of the target CPU. These macros are declared by processor/machine specific header file <pj/compat/m_*.h>.

Floating-Point Availability

Floating point availability is controlled by PJ_HAS_FLOATING_POINT macro, which by default is set to 1 in <pj/config.h>. This can be overriden by declaring PJ_HAS_FLOATING_POINT to zero in your <pj/config_site.h>.

PJLIB, PJLIB-UTIL, and PJSIP does not need any floating point support. Even when floating point is disabled, everything should work correctly. However, the PJ_HAS_FLOATING_POINT macro still MUST be set accordingly in order for some fallback code to get activated.

However, the situation is different in PJMEDIA. Some PJMEDIA components (such as tone generator, RTCP calculation, Speex codec, and AEC) do have fallback algorithm implemented in fixed point, which get activated when PJ_HAS_FLOATING_POINT macro is set to zero. However, some other components do not have the alternative fixed point implementation, thus the floating point based code will get used regardless of the floating point setting (examples of such components are the resampling, PLC, and iLBC codec).

  

Compiler and Build System

The use of particular compiler will need to consider the following issues.

Compiler Specific Features

PJ currently supports GCC, MS Visual Studio, MS Embedded VC, Intel C compiler (using gcc defines), and MetroWerks C compiler (works in progress in Symbian port). To support new compiler, a new <pj/compat/cc_*.h> needs to be created, and the <pj/config.h> file needs to be modified to include this new compiler specific header file whenever the use of such compiler is detected.

PJ does not use any compiler specific constructs, so supporting a new compiler should be straightforward.

64bit Integral Data Type Availability

A 64bit integer/unsigned integer data types are used quite heavily throughout the libraries for representing large integers. The availability of such data types are controlled by PJ_HAS_INT64 macro.

On PJLIB and PJSIP libraries, normally the use of 64bit data types ar optional; when PJ_HAS_INT64 macro is not declared, or when its value is zero, the 64bit data type will be replaced by multiple 32bit integers.

However, PJMEDIA uses 64bit data type as the frame timestamp (to avoid overflow), and this currently does not have the replacement 32bit algorithms. Many PJMEDIA components use the 64bit operations against frame timestamp, without providing alternative 32bit operations.

Most modern compilers such as gcc should have support for 64bit integral data types; however please check this when porting PJ software using other compilers.

Integrating into the Build System

If the existing PJ Makefile based build system is to be used, then ideally the compiler should support GCC syntax for specifying options (for example "/I" to specify include search path). If this is not possible, when a new "cc-*.mak" file need to be created in $PJ-ROOT/build/ directory, to tell the Makefile build system how to invoke various options to the compiler (note: the use of compiler other than gcc family for the Makefile has not been tested for some time, so few things may be broken).

Alternatively, you may create a new build system altogether. For example, if the compiler comes with its own build system (an IDE, for example), then probably it's better to create new project files for building the libraries instead of using PJ build system. This is the approach that we use when porting PJ to Windows Mobile and Symbian.

When creating a new build system, please prefer to put the build specific files (such as project files) under each build/ directory of each project, or in a sub-directory under build/ directory in each project, if at all possible. Also take care to create another subdirectory under the project directory, to place the object files, so that it is easy to add Subversion rule to ignore these directories containing output files from being included in the source control.

However, creating a new build system comes at a cost, that is it would need to be maintained to keep it in sync with changes in main PJ libraries. So please weight the convenience of having a build system supported by/integrated with the target against the cost of maintaining such build system when deciding whether choose this route.

Availability of <stdarg.h>

Variable number of argument functionality is needed by only one place, i.e. the logging functions declared in <pj/log.h> header file. In most cases, <stdarg.h> header file should be available, since this is standard ANSI C feature. But should this is not available, then probably we can get away with disabling PJLIB logging functionality by defining PJ_LOG_MAX_LEVEL macro to zero in your <pj/config_site.h>, but definitely this practice is highly discouraged since having logging is crucial when debugging the libraries!

 

Standard LIBC Features

Although higher level PJ libraries (such as PJSIP and PJMEDIA) can work without LIBC (this has been proven in the past with the porting to Linux kernel mode module target), in most cases this means that either PJ will abstract LIBC or the similar LIBC functionalities need to be implemented in PJLIB.

The LIBC header availabilities should be indicated by the corresponding PJ_HAS_*_H macro in the appropriate OS specific <pj/compat/os_*.h> header file (for example, PJ_HAS_STRING_H indicates the availability of standard <string.h> header file). 

When some header files are missing, sometimes it is still possible to recover the compilation if the same functionality is defined by some other header files (for example, the standard string manipulation functions are declared by <string.h>, but some systems such as WinCE declare them in <stdlib.h>). In other cases, when the functionality is really not available, then the appropriate replacement implementation need to be provided (for example, isblank() is implemented in <pj/compat/ctype.h> for targets that lack it).

Some of the most "problematic" LIBC features will be described below.

setjmp() and longjmp() 

Both setjmp() and longjmp() functions are needed to get the exception framework working. The PJLIB exception framework is a TRY/CATCH mechanism for C programs, and is normally used in scanners/parsers to raise syntax error exception and in PJLIB Memory Pool to raise memory allocation failure exception.

This functionality is mandatory, and if setjmp()/longjmp() are not available, they MUST be implemented in <pj/compat/setjmp.h>.

malloc()

Malloc is the default memory allocation backend for PJLIB Memory Pool, but this can be changed by creating a new memory pool policy (similar to pj/pool_policy_malloc.c) and specify this policy when creating the pool factory.

 

OS Features

This definitely would be the hardest part of the porting efforts. Some of the challenges to be expected are describesd below.

OS Dependent Features

OS dependent features needed by PJ are collected together in <pj/os.h>, which are documented in PJLIB OS Dependent documentation. PJLIB provides the implementation for Win32 and Posix family OSes, so if the new OS supports one of these OS API, probably we don't need to create a new os_*.c implementation. If this is not the case, then a new os_*.c MUST be created.

Note that depending on the selected porting strategy (described in next chapter), it may be possible to skip creating a full os_*.c implementation for the new OS, and instead just create a dummy implementation just to allow the library to link.

Threading

PJ software works with or without threads, as the libraries do not create any threads on their own. But when threads are needed, naturally all synchronization functions must be implemented for the new OS.

It is perfectly possible to NOT use threads, for example, to run the application on interrupt context, or to use some kind of cooperative multitasking mechanism where a task does not relinguish it's execution time until it has finished with the task. With PJSIP and PJMEDIA, it is also perfectly possible to use only single thread (the main thread) to run everything (including polling for the RTP/RTCP packets), provided that the sound device abstraction supports this. For these cases, we don't need to implement all the PJLIB OS features.

Thread Specific Data

The libraries need to store some data that is specific (and private) for each thread. PJLIB provides this functionality by abstracting the thread local storage (TLS) or thread specific storage API that is system dependent into a uniform thread local storage API.

Even when multithreading is NOT used, thread local storage API must still be implemented in PJLIB. The TLS is used for example by the PJLIB exception framework, since an exception should only be cascaded to the appropriate handler on the same thread only.

In other cases, thread local storage or thread specific storage API may not be available in the target OS. In these cases, application developer would need to implement this functionality via a homegrown mechanism.

Synchronization Objects

PJLIB provides abstractions for synchronization objects such as semaphore, simple mutex, recursive mutex, and read-write mutex. All of these objects are used by the upper layer libraries or application.

The Operating System normally would provide these functionalities in the OS API. But even when it's not available, normally we can emulate most mutex functionalities using semaphore object.

More specificly, PJLIB provides a simple and elegant implementation of read-write mutex for OS that doesn't provide this functionality (such as Win32).

Unicode Support

PJ can work in both ANSI and UNICODE systems, and in fact, with PJLIB, it is possible to create a source files that would build correctly in both ANSI and UNICODE variant of the same OS family (such as between Win32 and Windows CE).

The UNICODE support is indicated by PJ_NATIVE_STRING_IS_UNICODE macro, which MUST be defined in the appropriate <pj/compat/os_*.h> file. Some UNICODE utility functions/macros are declared in <pj/unicode.h>.

PJLIB itself (and all other PJ libraries such as PJLIB-UTIL, PJSIP, and PJMEDIA) internally uses ANSI representation for all purposes, and only converts strings to Unicode when they interract with the Unicode operating systems.

 

Networking Features

PJLIB defines rich abstractions for networking features. These abstractions and the consideration when porting will be described below.

Socket API

PJLIB Socket API provides abstraction for various socket backends using BSD like API, and the API is declared in <pj/sock.h>, and implementation is provided in pj/sock_bsd.c for BSD socket API. The abstraction has been designed to allow very different socket API backend to be used; for example, efforts are currently under way to implement socket API for Symbian OS directly on top of RSocket interface (instead of using the BSD abstraction).

The PJLIB socket API is needed by the default SIP and media transports. However this may not be needed if application completely rewrites these SIP and media transports.

Address Resolution API

PJLIB provides address resolution API abstraction (pj_gethostbyname() and pj_gethostip() functions) in <pj/addr_resolv.h>, and implementation for BSD socket backend is provided in pj/addr_resolv_sock.c.

The address resolution functions are needed for two purposes:

  • for finding the API address of local host when creating the SIP and media transports,
  • for SIP server resolution procedure, if RFC 3263 server resolution is not used.
If application works strictly with IP addresses only (no hostnames), then the address resolution API may not be needed.

Socket select() Abstraction

PJLIB provides abstraction for BSD select() system call in <pj/sock_select.h>, and the implementation in pj/sock_select.c. The select() abstraction is only used by select IOQUEUE (see below), so if select IOQUEUE is not used, the select() abstraction is not needed.

IOQUEUE

PJLIB IOQUEUE (I/O Queue) implements Proactor Pattern for demultiplexing network events. The IOQUEUE can be implemented with multiple backends, such as Windows NT IO Completion Port, Linux epoll, or PJLIB's select() abstraction.

The IOQUEUE is mainly used to poll all sockets for incoming packets, and standard PJSIP and PJMEDIA transports make use of IOQUEUE. Because of this, it is possible to have all sockets in the application (SIP sockets and RTP/RTCP sockets) register to one ioqueue, then poll this ioqueue from a single place (and possibly with a single thread).

However, for platforms that doesn't have socket demultiplexing API (such as selec()), it is possible to NOT implement IOQUEUE altogether, by implementing a custom SIP and media transport.

 

Porting Strategies

This section describes the strategies to port PJ libraries into a new platform. There are basically two approaches to porting PJ software into new platform.

The "traditional" way is to completely port PJLIB for the new platform. Once PJLIB has been ported successfully,  all other libraries (PJLIB-UTIL, PJSIP, and PJMEDIA) should run on the new platform without modifications, since they depend only to PJLIB.

The second approach is only to partially port PJLIB, but some parts of PJSIP and PJMEDIA will need to be modified.

Both approaches will be described below.

 

Fully Porting PJLIB

The "traditional" path to porting PJ software is to port the whole PJLIB to the new platform. Since all other libraries and applications only depend to PJLIB, these upper layer libraries/applications would most likely be able to run without changes on the new platform once PJLIB porting is complete.

To fully port PJLIB, developer need to provide all the PJLIB features described above. In addition, PJLIB provides quite comprehensive tests (pjlib-test application) to test PJLIB functionalities for the new platform. These test should catch more than 90% of problems that may be had for the new platform, so when pjlib-test reports that everything is okay, there is a very good chance that the porting effort has been successful.

This porting path is recommended for platforms that provide "modern" APIs such as Posix or Windows.  If the target platform provides Posix abstraction layer, then this path is recommended, since PJLIB supports Posix API. If this is not available (or desired), then some efforts would be required to rewrite/build some features that are pretty "advanced", such as threading, thread local storage, and network event demultiplexing API (select() and the like).

Since not all Operating Systems are able to provide such features, this porting effort may not be feasible. In this case, the second approach below may be used to port PJ software into such platforms.

 

Partially Porting PJLIB

Many Operating Systems, especially deeply embedded OSes, do not have "advanced" features such as multithreading, thread local storage, network event demultiplexing, and the likes. Or in other cases, these features may not be desired, for design requirements reason or size consideration. For these cases, fully porting PJLIB may not be the best option.

This section attempts to describe alternative way to port PJ software into such platforms. However, readers please be aware that this effort is still being tested, so the procedures described here may not be accurate at all.

 

Design Considerations

This kind of port requires specific application design considerations, which will be described below.

Threading Strategy: No Threading!

To simplify the porting, we would need to disable threading. By disabling threading, we then could skip implementing the bulk OS specific codes in pj/os_*.c (things like threads and synchronization objects). Disabling threading in this case does not mean that we should not use multitasking at all, but rather to make sure that no task would be interrupted unless the task is ready to yield when it thinks that it is safe to do so. This concept is commonly called cooperative multitasking.

Create Active Transports

The transport interfaces in both SIP and media stack was designed around the idea that transports should be active (by "active", it means that the stack should not need to poll these transports to get events from the network). But "under the hood", actually these transports would register their sockets to an IOQUEUE, and application or SIP endpoint (pjsip_endpoint) would need to regularly poll the ioqueue to retrieve the network events.

In other words, current framework requires full implementation of IOQUEUE and PJLIB socket abstraction API, which may not be available in the target platform. If IOQUEUE and socket API porting is not desired, then developer would need to rewrite these SIP and media transports to NOT use IOQUEUE and PJLIB socket. The SIP and media transports API is quite straightforward; the interface would just need to register a callback to be called when the stack need to send packet, and the transport need to call some function in the stack whenever a packet has arrived.

The transport object itself then may be written in whatever API provided by the OS. It may even be called from some interrupt that is triggered when incoming packets arrived.

By implementing the transports this way, there would not be any need to implement PJLIB's IOQUEUE, select abstraction, and socket abstraction API.

 Implement PJLIB Dummy Functions

With this approach, many PJLIB functionalities would not need to be implemented. However, those functions may still have to be defined (otherwise link error will occur), so dummy implementations need to be created.

 

More Information

This type of porting is still under investigation, thus there's not much details can be revealed at the moment. More will be written when there's more information available.

If you want to try this path, please contact me to discuss things in more details. Thanks.

 

Related Pages

The PJLIB Porting Guide in PJLIB's manual also provides some useful info about PJLIB build system and porting PJLIB.