Dynamic Memory Management

The ASN1C run-time uses specialized dynamic memory functions to improve the performance of the encoder/decoder. It is imperative to understand how these functions work in order to avoid memory problems in compiled applications. ASN1C also provides the capability to plug-in a different memory management scheme at two different levels: the high level API called by the generated code and the low level API that provides the core memory managment functionality.

The ASN1C Default Memory Manager

The default ASN1C run-time memory manager uses an algorithm called the nibble-allocation algorithm. Large blocks of memory are allocated up front and then split up to provide memory for smaller allocation requests. This reduces the number of calls required to the C malloc and free functions. These functions are very expensive in terms of performance.

The large blocks of memory are tracked through the ASN.1 context block (OSCTXT) structure. For C, this means that an initialized context block is required for all memory allocations and deallocations. All allocations are done using this block as an argument to routines such as rtxMemAlloc. All memory can be released at once when a user is done with a structure containing dynamic memory items by calling rtxMemFree. Other functions are available for doing other dynamic memory operations as well. See the C/C++ Run-time Reference Manual for details on these.

High Level Memory Management API

The high-level memory management API consists of C macros and functions called in gemerated code and/or in application programs to allocate and free memory within the ASN1C run-time.

At the top level are a set of macro definitions that begin with the prefix rtxMem. These are mapped to a set of similar functions that begin with the prefix rtxMemHeap. A table showing this basic mapping is as follows:

Macro Function Description
rtxMemAlloc
rtxMemHeapAlloc
Allocate memory
rtxMemAllocZ
rtxMemHeapAllocZ
Allocate and zero memory
rtxMemRealloc
rtxMemHeapRealloc
Reallocate memory
rtxMemFree
rtxMemHeapFreeAll
Free all memory in context
rtxMemFreePtr
rtxMemHeapFreePtr
Free a specific memory block

See the ASN1C C/C++ Common Runtime Reference Manual for further details on these functions and macros.

It is possible to replace the high-level memory allocation functions with functions that implement a custom memory management scheme. This is done by implementing some (or all) of the C rtxMemHeap functions defined in the following interface (note: a default implementation is shown that replaces the ASN1C memory manager with direct calls to the standard C run-time memory management functions):

   #include <stdlib.h>
   #include "rtxMemory.h"

   /* Create a memory heap */
   int rtxMemHeapCreate (void** ppvMemHeap) {
      return 0;
   }

   /* Allocate memory */
   void* rtxMemHeapAlloc (void** ppvMemHeap, int nbytes) {
      return malloc (nbytes);
   }

   /* Allocate and zero memory */
   void* rtxMemHeapAllocZ (void** ppvMemHeap, int nbytes) {
      void* ptr = malloc (nbytes);
      if (0 != ptr) memset (ptr, 0, nbytes);
      return ptr;
   }

   /* Free memory pointer */
   void rtxMemHeapFreePtr (void** ppvMemHeap, void* mem_p) {
      free (mem_p);
   }

   /* Reallocate memory */
   void* rtxMemHeapRealloc (void** ppvMemHeap, void* mem_p, int nbytes_) {
      return realloc (mem_p, nbytes_);
   }

   /* Clears heap memory (frees all memory, reset all heap's variables) */
   void rtxMemHeapFreeAll (void** ppvMemHeap) {
      /* should remove all allocated memory. there is no analog in standard memory
         management. */
   }

   /* Frees all memory and heap structure as well (if was allocated) */
   void rtxMemHeapRelease (void** ppvMemHeap) {
      /* should free all memory allocated + free memory heap object if exists */
   }

In most cases it is only necessary to implement the following functions: rtxMemHeapAlloc, rtxMemHeapAllocZ, rtxMemHeapFreePtr and rtxMemHeapRealloc. Note that there is no analog in standard memory management for ASN1C’s rtxMemFree macro (i.e. the rtxMemHeapFreeAll function). A user would be responsible for freeing all items in a generated ASN1C structure individually if standard memory management is used.

The rtxMemHeapCreate and rtxMemHeapRelease functions are specialized functions used when a special heap is to be used for allocation (for example, a static block within an embedded system). In this case, rtxMemHeapCreate must set the ppvMemHeap argument to point at the block of memory to be used. This will then be passed in to all of the other memory management functions for their use through the OSCTXT structure. The rtxMemHeapRelease function can then be used to dispose of this memory when it is no longer needed.

To add these definitions to an application program, compile the C source file (it can have any name) and link the resulting object file (.OBJ or .O) in with the application.

Built-in Compact Memory Management

A built-in version of the simple memory management API described above (i.e with direct calls to malloc, free, etc.) is available for users who have the source code version of the run-time. The only difference in this API with what is described above is that tracking of allocated memory is done through the context. This makes it possible to provide an implementation of the rtxMemHeapFreeAll function as described above. This memory management scheme is slower than the default manager (i.e. nibble-based), but has a smaller code footprint.

This form of memory management is enabled by defining the _MEMCOMPACT C compile time setting. This can be done by either adding -D_MEMCOMPACT to the C compiler command-line arguments, or by uncommenting this item at the beginning of the rtxMemory.h header file:

/*
 * Uncomment this definition before building the C or C++ run-time 
 * libraries to enable compact memory management.  This will have a 
 * smaller code footprint than the standard memory management; however, 
 * the performance may not be as good.
 */
/*#define _MEMCOMPACT*/

Low Level Memory Management API

It is possible to replace the core memory management functions used by the ASN1C run-time memory manager. This has the advantage of preserving the existing management scheme but with the use of different core functions. Using different core functions may be necessary on some systems that do not have the standard C run-time functions malloc, free, and realloc, or when extra functionality is desired.

To replace the core functions, the following run-time library function would be used:

   void rtxMemSetAllocFuncs (OSMallocFunc malloc_func,
      OSReallocFunc realloc_func, OSFreeFunc free_func);

The malloc, realloc, and free functions must have the same prototype as the standard C functions. Some systems do not have a realloc-like function. In this case, realloc_func may be set to NULL. This will cause the malloc_func/free_func pair to be used to do reallocations.

This function must be called before the context initialization function (rtInitContext) because context initialization requires low level memory management facilities be in place in order to do its work.

Note that this function makes use of static global memory to hold the function definitions. This type of memory is not available in all run-time environments (most notably Symbian). In this case, an alternative function is provided for setting the memory functions. This function is rtxInitContextExt which must be called in place of the standard context initialization function (rtInitContext). In this case, there is a bit more work required to initialize a context because the ASN.1 subcontext must be manually initialized. This is an example of the code required to do this:

   int stat = rtxInitContextExt (pctxt, malloc_func, realloc_func, free_func);
   if (0 == stat) {
      /* Add ASN.1 error codes to global table */
      rtErrASN1Init ();

      /* Init ASN.1 info block */
      stat = rtCtxtInitASN1Info (pctxt);
   }

Memory management can also be tuned by setting the default memory heap block size. The way memory management works is that a large block of memory is allocated up front on the first memory management call. This block is then subdivided on subsequent calls until the memory is used up. A new block is then started. The default value is 4K (4096) bytes. The value can be set lower for space constrained systems and higher to improve performance in systems that have sufficient memory resources. To set the block size, the following run-time function should be used:

   void rtxMemSetDefBlkSize (OSUINT32 blkSize);

This function must be called prior to context initialization.

C++ Memory Management

In the case of C++, the ownership of memory is handled by the control class and message buffer objects. These classes share a context structure and use reference counting to manage the allocation and release of the context block. When a message buffer object is created, a context block structure is created as well. When this object is then passed into a control class constructor, its reference count is incremented. Then when either the control class object or message buffer object are deleted or go out of scope, the count is decremented. When the count goes to zero (i.e. when both the message buffer object and control class object go away) the context structure is released.

What this means to the user is that a control class or message buffer object must be kept in scope when using a data structure associated with that class. A common mistake is to try and pass a data variable out of a method and use it after the control and message buffer objects go out of scope. For example, consider the following code fragment:

   ASN1T_<type>* func2 () {
      ASN1T_<type>* p = new ASN1T_<type> ();
      ASN1BERDecodeBuffer decbuf;
      ASN1C_<type> cc (decbuf, *p);

      cc.Decode();

      // After return, cc and decbuf go out of scope; therefore
      // all memory allocated within struct p is released..

      return p;
      }

   void func1 () {
      ASN1T_<type>* pType = func2 ();

      // pType is not usable at this point because dynamic memory
      // has been released..
   }

As can be seen from this example, once func2 exits, all memory that was allocated by the decode function will be released. Therefore, any items that require dynamic memory within the data variable will be in an undefined state.

An exception to this rule occurs when the type of the message being decoded is a Protocol Data Unit (PDU). These are the main message types in a specification. The ASN1C compiler designates types that are not referenced by any other types as PDU types. This behavior can be overridden by using the -pdu command line argument or <isPDU> configuration file element.

The significance of PDU types is that generated classes for these types are derived from the ASN1TPDU base class. This class holds a reference to a context object. The context object is set by Decode and copy methods. Thus, even if control class and message buffer objects go out of scope, the memory will not be freed until the destructor of an ASN1TPDU inherited class is called. The example above will work correctly without any modifications in this case.

Another way to keep data is to make a copy of the decoded object before it goes out of scope. A method called newCopy is also generated in the control class for these types which can be used to create a copy of the decoded object. This copy of the object will persist after the control class and message buffer objects are deleted. The returned object can be deleted using the standard C++ delete operator when it is no longer needed.

Returning to the example above, it can be made to work if the type being decoded is a PDU type by doing the following:

   ASN1T_<type>* func2 () {
      ASN1T_<type> msgdata;
      ASN1BERDecodeBuffer decbuf;
      ASN1C_<type> cc (decbuf, msgdata);

      cc.Decode();

      // Use newCopy to return a copy of the decoded item..

      return cc.newCopy();
   }