Chapter 3
Codec Generation

 3.1 Running the CoDec Generator
 3.2 Encoding and Decoding with T3DevLib
  3.2.1 Value framework
  3.2.2 Buffers
  3.2.3 Exceptions
 3.3 Writing Codets (i.e. customising the codec)
  3.3.1 The generated codec
  3.3.2 Helper functions
  3.3.3 Codets
  3.3.4 Adding missing logic
  3.3.5 Additional processing
  3.3.6 Reporting Errors
  3.3.7 Extending class definitions
  3.3.8 Handling Exceptions
 3.4 Defining Primitive Types
  3.4.1 Predefined Macros
  3.4.2 Class definition
  3.4.3 Registering manually defined types
 3.5 Reusing an existing CoDec
  3.5.1 Overview of the problem and solutions
  3.5.2 Calling another CoDec (Master mode)
  3.5.3 Being called by another CoDec (Slave mode)

3.1 Running the CoDec Generator

In order to build the codec, the generator has to be fed with:

It will generate two C++ files, whose default names are gen_classes.h and gen_classes.cpp. The header gen_classes.h must be included by C++ files implementing the codets, so as to perform the compilation successfully.

The type of input files is guessed from their extension. Valid extensions for TTCN-3 files are: .ttcn .3mp and .ttcn3 and for C++ files: .cpp .cc .C .c++ .h .hpp .H and .h++.

During generation codet files are not parsed directly, they are first processed with the C++ preprocesser. Additional include directories and definitions can be provided with -I and -D. Their values should be the same as those used for the C++ compilation.

Usage: t3cdgen [-vh] [-i user_defs.h] [-o output.cpp] [-Dxxx -Iyyy] file.ttcn  
               [files.ttcn files.h files.cpp ...]  
 
Options:  
  -i xxxx.h     header containing user definitions  [default=none]  
  -o xxxx.cpp   output file (also writes xxxx.h)    [default=gen_classes.cpp]  
 
  -Dxx -Ixx     options to be passed to the C preprocessor before extracting  
                the codets  
  -h            print this help and exit  
  -V            print version information and exit  
  -v            verbose output  
 
Input Files:  
  .ttcn .3mp .ttcn3     TTCN-3 files  
  .cpp .cc .C .c++      C++ files containing codets  
  .h .hpp .H .h++       C++ headers containing inline codets

The build dependencies of generated files is shown in Figure 3.1. In this example gen_classes.h and gen_classes.cpp can be generated from the following command:

 
t3cdgen -i user_defs.h  ats_source.ttcn  inline_codets.h  codets.cpp


PIC

Figure 3.1: Dependencies between C++ sources files


3.2 Encoding and Decoding with T3DevLib

3.2.1 Value framework

TTCN-3 variables are represented with an object oriented framework of C++ classes, all derived from the root class t3devlib::Variable. A representation of the framework is showed in Figure 3.2.


PIC

Figure 3.2: T3DevLib Framework for handling TTCN-3 variables


Primitive types are grouped in a subtree (Figure 3.3). The class t3devlib::PrimitiveType provides an abstraction layer for accessing any derived type in a uniform way. Especially it allows manipulating the binary content of the variable (with GetValueBin() and SetValueBin()) without having to worry about its actual type.


PIC

Figure 3.3: Subtree for primitive types


When the CoDec generator is run, it creates a C++ class for representing each TTCN-3 subtype defined by the user in the abstract test suite. These classes are derived from one of the base classes presented above and wear the same name as the TTCN-3 type they are originated from. Generated classes are defined in namespace t3devlib::gen.

For example, the following TTCN-3 subtype definitions...

 
type record   MyRecord { 
  integer     FieldA, 
  integer     FieldB, 
  charstring  FieldC 
} 
 
type record MyRecord2 { 
  integer     FieldA, 
  MyRecord    FieldB 
} 
 
type union MyUnion { 
  octetstring  FieldA, 
  MyRecord     FieldB, 
  MyRecord2    FieldC 
}

...will produce the following C++ classes:

 
namespace t3devlib { 
  namespace gen { 
 
  class MyRecord : public t3devlib::Record 
  { 
    ... 
  }
 
  class MyRecord2 : public t3devlib::Record 
  { 
    ... 
  }
 
  class MyUnion : public t3devlib::Union 
  { 
    ... 
  }
 
  } // namespace gen 
} // namespace t3devlib

Each class is fit with two member functions Encode() and Decode(), that are called by the TCI-CD implementation of T3DevLib. In case of structured types Encode() and Decode() are called in cascade.

** Example 1. encoding a charstring **  
 
-> tciEncode (... , charstring);  
   -> Charstring::Encode();  
 
 
** Example 2. encoding a MyRecord record **  
 
-> tciEncode (... , MyRecord);  
   -> MyRecord::Encode();  
      -> Signed::Encode();  
      -> Signed::Encode();  
      -> Charstring::Encode();  
 
 
** Example 3. decoding a MyRecord2 record **  
 
-> tciDecode (... , MyRecord2);  
   -> MyRecord2::Decode();  
      -> Signed::Decode();  
      -> MyRecord::Decode()  
         -> Signed::Decode();  
         -> Signed::Decode();  
         -> Charstring::Decode();

 

3.2.2 Buffers

Encoding and Decoding operations are performed on buffers implemented by the class t3devlib::Buffer.

A buffer can be viewed as a file with sequential access. It provides Read() and Write() operations, and it has a cursor that moves accordingly and that can be set to and arbitrary position.

The main differences with a regular file is that the buffer is stored in memory and that every operation is bit-oriented (instead of byte-oriented).

The following example shows how buffers work:

 
Buffer      buffer; 
Charstring  cs; 
Unsigned    u8(8); 
 
// write ABC to the buffer 
cs.SetValue (ABC); 
buffer.Write (cs); 
 
cout << buffer;                         //  --> ’414243’O (DBitstring) 
 
// go back to the beginning of the buffer 
buffer.SetPosition (0); 
 
// read 8 bits and store them into u8 
buffer.Read (u8, 8); 
cout << u8;                             //  --> 65 - 0x41 (8 bit unsigned integer) 
 
buffer.Read (u8, 8); 
cout << u8;                             //  --> 66 - 0x42 (8 bit unsigned integer) 
 
buffer.Read (u8, 8); 
cout << u8;                             //  --> 67 - 0x43 (8 bit unsigned integer)

It should be noted that Read() and Write are low-level functions. When implementing codets it will be preferred to use Encode() or Decode() instead, especially because errors will be reported with exceptions.

Here is the same example rewritten with Encode() or Decode():

 
Buffer      buffer; 
Charstring  cs; 
Unsigned    u8(8); 
 
// write ABC to the buffer 
cs.SetValue (ABC); 
cs.Encode (buffer); 
 
cout << buffer;                         //  --> ’414243’O (DBitstring) 
 
// go back to the beginning of the buffer 
buffer.SetPosition (0); 
 
// read 8 bits and store them into u8 
u8.Decode (buffer); 
cout << u8;                             //  --> 65 - 0x41 (8 bit unsigned integer) 
 
u8.Decode (buffer); 
cout << u8;                             //  --> 66 - 0x42 (8 bit unsigned integer) 
 
u8.Decode (buffer); 
cout << u8;                             //  --> 67 - 0x43 (8 bit unsigned integer)

In this example Buffer::Write() is called implicitly by Charstring::Encode(), and Buffer::Read() by Unsigned::Decode(). There is no need to indicate that 8 bits have to be read and to check that there is enough bits left in the buffer, these details are already handled by Unsigned::Decode(). In case of problem an exception is thrown.

Also the class provide functions for arbitrary limiting its apparent size when performing Read() operations, this is very useful when decoding encapsulated or stacked messages.

Technically this is done by setting an end marker with PushEndMarker() and removing it afterwards with PopEndMarker(). Several end markers can be set on the same buffer, they are stored in a stack (Last In First Out). The new marker overrides the previous one until it is removed with PopEndMarker().

Here is an example dealing with end markers:

 
Buffer      buffer; 
Octetstring os; 
Unsigned    u8(8); 
 
// write ’010203...’ to the buffer 
os.SetValueHexa (0102030405060708090A0B0C0D0E0F10); 
os.Encode (buffer); 
 
// go back to the beginning of the buffer 
buffer.SetPosition (0); 
 
cout << buffer.GetPosition() << endl;   //  --> 0 
cout << buffer.GetBitsLeft() << endl;   //  --> 128 
 
// limit the buffer to 40 bits 
buffer.PushEndMarker (40); 
 
cout << buffer;                         //  --> ’0102030405060708090A0B0C0D0E0F10O (DBitstring) 
cout << buffer.GetPosition() << endl;   //  --> 0 
cout << buffer.GetBitsLeft() << endl;   //  --> 40 
 
 
// decode the octetstring from the buffer 
os.Decode (buffer); 
 
cout << buffer.GetPosition() << endl;   //  --> 40 
cout << buffer.GetBitsLeft() << endl;   //  --> 0 
cout << os;                             //  --> ’0102030405’O (Octetstring) 
 
 
// decode the octetstring from the buffer 
os.Decode (buffer); 
 
// this time the buffer seems empty, therefore an empty string is read 
cout << buffer.GetPosition() << endl;   //  --> 40 
cout << buffer.GetBitsLeft() << endl;   //  --> 0 
cout << os;                             //  --> ’’O (Octetstring) 
 
 
// remove the end marker 
buffer.PopEndMarker (); 
cout << buffer.GetPosition() << endl;   //  --> 40 
cout << buffer.GetBitsLeft() << endl;   //  --> 88 
 
 
// set a new end marker 
buffer.PushEndMarker (104); 
cout << buffer.GetPosition() << endl;   //  --> 40 
cout << buffer.GetBitsLeft() << endl;   //  --> 64 
 
// set a new end marker on the top of the previous one 
buffer.PushEndMarker (88); 
cout << buffer.GetPosition() << endl;   //  --> 40 
cout << buffer.GetBitsLeft() << endl;   //  --> 48 
 
 
// decode the octetstring from the buffer 
os.Decode (buffer); 
 
cout << buffer.GetPosition() << endl;   //  --> 88 
cout << buffer.GetBitsLeft() << endl;   //  --> 0 
cout << os;                             //  --> ’060708090A0BO (Octetstring) 
 
 
// remove the second end marker 
buffer.PopEndMarker (); 
cout << buffer.GetPosition() << endl;   //  --> 88 
cout << buffer.GetBitsLeft() << endl;   //  --> 16 
 
 
// decode the octetstring from the buffer 
os.Decode (buffer); 
 
cout << buffer.GetPosition() << endl;   //  --> 104 
cout << buffer.GetBitsLeft() << endl;   //  --> 0 
cout << os;                             //  --> ’0C0DO (Octetstring) 
 
 
// remove the first end marker 
buffer.PopEndMarker (); 
cout << buffer.GetPosition() << endl;   //  --> 104 
cout << buffer.GetBitsLeft() << endl;   //  --> 24 
 
 
// decode the octetstring from the buffer 
os.Decode (buffer); 
 
cout << buffer.GetPosition() << endl;   //  --> 128 
cout << buffer.GetBitsLeft() << endl;   //  --> 0 
cout << os;                             //  --> OEOF10O (Octetstring)

3.2.3 Exceptions

Encoding and decoding errors are reported by various exceptions derived from t3devlib::EncodeError and t3devlib::DecodeError.

They provide a detailed account of the error and can be caught at any level when Encode()/Decode() calls are made in cascade.

The next example shows an exception thrown when there is not enough bits left in the buffer to decode a variable.

 
Buffer      buffer; 
Octetstring os; 
Unsigned    u16(16); 
 
try 
{ 
  // write ’0102030405’ to the buffer 
  os.SetValueHexa (0102030405); 
  os.Encode (buffer); 
 
  // go back to the beginning of the buffer 
  buffer.SetPosition (0); 
 
  // limit the buffer to 3 bytes 
  buffer.PushEndMarker (24); 
 
  // decode the 16-bit integer from the buffer 
  u16.Decode (buffer); 
 
  cout << buffer.GetPosition() << endl;   //  --> 16 
  cout << buffer.GetBitsLeft() << endl;   //  --> 8 
  cout << u16;                            //  --> 258 - 0x102 (16 bit unsigned integer) 
 
 
  // decode the 16-bit integer from the buffer 
  u16.Decode (buffer);                    //  throws an exception 
 
  cout << buffer.GetPosition() << endl;   //  not reached 
  cout << buffer.GetBitsLeft() << endl;   //  not reached 
  cout << u16;                            //  not reached 
} 
catch (DecodeError& ex) 
{ 
  cout << ex;  // --> Error while decoding (unsigned integer) 
               //     unexpected end of stream (16 bits expected / 8 bits present) 
 
 
  // the following variables are unchanged 
  cout << buffer.GetPosition() << endl;   //  --> 16 
  cout << buffer.GetBitsLeft() << endl;   //  --> 8 
  cout << u16;                            //  --> 258 - 0x102 (16 bit unsigned integer) 
}

It is possible to catch exceptions thrown by the generated codec, the procedure is described in Section 3.3.8.

3.3 Writing Codets (i.e. customising the codec)

Generating a working codec automatically from an Abstract Test Suite written in TTCN-3 is an almost impossible task. Source files provide an abstract description of messages, but nothing about their coding. However they still contain lots of informations needed for writing the codec.

The purpose of the CoDec generator is to automatically generate all the parts of the codec that can be guessed from the TTCN-3 sources. The result is a codec that is almost operational. It only needs to be customised by writing what we named codets: small pieces of C++ code that are inserted in the final codec so as to implement the right behaviour.

This section describes the different operation that can be performed by codets.

3.3.1 The generated codec

The current version of the CoDec generator only supports structured type: record, set, record of, set of and union. Other types have to be implemented manually (see Section 3.4).

For each structured type defined in TTCN-3, the CoDec generator produces a class with:

For records, sets and unions only:

The full layout of the classes is described in the API documentation: t3devlib::GeneratedRecord, t3devlib::GeneratedRecordOf, t3devlib::GeneratedUnion.

For example a record defined as follows:

 
module MyModule 
{ 
  type record MyRecord 
  { 
    charstring      FieldA; 
    MyType          FieldB; 
    octetstring     FieldC optional
  } 
}

..will generate this class:

 
namespace t3devlib { 
  namespace gen { 
 
  class MyRecord : public t3devlib::Record 
  { 
  public
    // type information 
    const char* GetTypeName()   const { return MyRecord} 
    const char* GetModuleName() const { return MyModule} 
 
 
    // encoding and decoding functions 
    void Encode (t3devlib::Buffer& buffer) throw (t3devlib::EncodeError); 
    void Decode (t3devlib::Buffer& buffer) throw (t3devlib::DecodeError); 
 
 
    // helper functions: give some hypothesis for the decoder 
    //  - length of the record 
    static void SetHypLength (int length); 
    static int  GetHypLength (); 
 
    //  - length of one field 
    static void SetHypFieldLength (int id, int length); 
    static int  GetHypFieldLength (int id); 
 
    //  - presence of an optional field 
    static void SetHypFieldIsPresent (int id, int is_present); 
    static int  GetHypFieldIsPresent (int id); 
 
 
    // functions for accessing the fields 
    t3devlib::Charstring&  Get_FieldA(); 
    MyType&             Get_FieldB(); 
    t3devlib::Octetstring& Get_FieldC(); 
 
 
    // index of each field 
    static const int id_FieldA = 0; 
    static const int id_FieldB = 1; 
    static const int id_FieldC = 2; 
  }
}}

3.3.2 Helper functions

The purpose of helper functions is to provide the information that the decoder need for deciding:

For each parameter there is a pair of functions: SetHypXxxxxx() and GetHypXxxxxx().

A value of -1 for any parameter means that there is no hypothesis. The meaning of other values is parameter dependant. Be careful with invalid values, they will trigger fatal errors (this behaviour may change in the future).

These functions are declared as static functions. When a hypothesis is set with MyType::SetHypXxxxxx(), it will be used the next time a variable MyType is decoded. Then the parameter is reset to -1 (no hypothesis).

The purpose of each function is described below.

SetHypLength (int bit_length)   [record, set, record of, set of, union]

Set the hypothetical length in bits of the encoded variable. Valid values are -1, 0 or more.

The decoder will check the parameter before decoding the variable.

SetHypFieldLength (int field_id, int bit_length)   [record, set, union]

Set the hypothetical length in bits of an individual field. Valid values are -1, 0 or more.

The decoder will check the parameter before decoding the field.

SetHypFieldIsPresent (int field_id, int bit_length)   [record]

Tell if an optional field (marked with the TTCN-3 keyword “optional”) in a record or a set is assumed to be present or not. Valid values are -1, 0 or 1.

The decoder will check the parameter before decoding the field.

SetHypChosenId (int field_id)   [union]

Tell the decoder which field in a union is assumed to be present. Valid values are -1, 0..N-1 (where N is number of fields in the union).

The decoder will check the parameter before decoding the field.

SetHypSize (int number_of_elements)   [record of, set of]

Set the hypothetical length of a record of or a set of by giving the number of elements. Valid values are -2, -1, 0 or more.

The parameter is checked before decoding each element, thus it is possible to use this parameter even if the size is not known before decoding the first element.

SetHypNextField (int field_id)   [set]

Set the hypothetical id of the next field to be decoded in a set. In such a structure the fields can appear in any order (contrary to a record). The generic codec generated by t3cdgen needs to in which order the fields are encoded. Each time it has to decode a field in the set, it will rely on this hypothesis. Valid values are -2, -1, 0..N-1 (where N is number of fields in the set).

The parameter is checked before decoding each field, thus it is a good choice to call SetHypNextField() from PreEncodeField() to announce which will be the next field to be decoded.

3.3.3 Codets

Codets are pieces of platform code (C++), that are combined with the generated code so as to produce the final codec.

Technically speaking a codet is a member function for a generated class. When the CoDec generator is run, it automatically checks for the presence of codets, includes them in the class definition and make the corresponding calls in Encode() and Decode().

Possible codets definitions for the encoder are:

 
namespace t3devlib { 
  namespace gen { 
 
    void MyVarType::PreEncode (Buffer& buffer) throw (EncodeError) 
    { 
      // called before encoding the variable 
    } 
 
    void MyVarType::PreEncodeField (int field_id, Buffer& buffer) throw (EncodeError) 
    { 
      // called before encoding each field 
    } 
 
    void MyVarType::PostEncodeField (int field_id, Buffer& buffer) throw (EncodeError) 
    { 
      // called after encoding each field 
    } 
 
    void MyVarType::PostEncode (Buffer& buffer) throw (EncodeError) 
    { 
      // called after encoding the variable 
    } 
 
    void MyVarType::PostEncode (Buffer& buffer) throw (EncodeError) 
    { 
      // called after encoding the variable 
    } 
 
    void MyVarType::PostEncode (Buffer& buffer, EncodeError& ex) throw (EncodeError) 
    { 
      // called if an exception was caught while encoding 
    } 
 
  } // namespace gen 
} // namespace t3devlib

and for the decoder:

 
namespace t3devlib { 
  namespace gen { 
 
    void MyVarType::PreDecode (Buffer& buffer) throw (DecodeError) 
    { 
      // called before decoding the variable 
    } 
 
    void MyVarType::PreDecodeField (int field_id, Buffer& buffer) throw (DecodeError) 
    { 
      // called before decoding each field 
    } 
 
    void MyVarType::PostDecodeField (int field_id, Buffer& buffer) throw (DecodeError) 
    { 
      // called after decoding each field 
    } 
 
    void MyVarType::PostDecode (Buffer& buffer) throw (DecodeError) 
    { 
      // called after decoding the variable 
    } 
 
    void MyVarType::PostDecode (Buffer& buffer) throw (DecodeError) 
    { 
      // called after decoding the variable 
    } 
 
    void MyVarType::PostDecode (Buffer& buffer, DecodeError& ex) throw (DecodeError) 
    { 
      // called if an exception was caught while decoding 
    } 
 
  } // namespace gen 
} // namespace t3devlib

The full description of their interaction with codec is given in the API documentation of Encode() and Decode() ( t3devlib::GeneratedRecord, t3devlib::GeneratedSet, t3devlib::GeneratedRecordOf, t3devlib::GeneratedUnion).

The next sections will describe how to write codets.

3.3.4 Adding missing logic

The first purpose of writing codets is to help the decoder in making decisions using the helper functions presented above.

For example an union may be defined as follows in TTCN-3:

 
type union MyUnion 
{ 
  charstring   FieldA, 
  octetstring  FieldB, 
  integer      FieldC 
}

When reaching this variable the decoder does not have any clue about the content of the union, but it has to make a decision.

Let us say that the id of the chosen field is encoded in the first byte of the union: 1 for FieldA, 5 for FieldB and 28 for FieldC.

We can help the decoder by defining the following codet:

 
namespace t3devlib { 
  namespace gen { 
 
  void MyUnion::PreDecode (Buffer& buffer) throw (DecodeError) 
  { 
      // called before decoding the variable 
 
      // instantiate an 8-bit unsigned integer 
      // and read its value from the current position of the buffer 
      Unsigned type (8); 
      type.Decode (buffer); 
 
      // choose the adequate field 
      switch (type.GetValue()) 
      { 
      case 1: 
              SetHypChosenId (id_FieldA); 
              break
      case 5: 
              SetHypChosenId (id_FieldB); 
              break
      case 28: 
              SetHypChosenId (id_FieldC); 
              break
      } 
  } 
 
}}

When decoding the union the decoder will then retrieve the value with GetHypChosenId() and choose the right field. If the message is not well formatted (the type is different from 1, 5 and 28), then no hypothesis is defined and the decoder throws an exception.

When encoding an equivalent codet has to be defined so as to prepend the 8-bit field at the beginning of the union.

 
namespace t3devlib { 
  namespace gen { 
 
  void MyUnion::PreEncode (Buffer& buffer) throw (DecodeError) 
  { 
      // called before encoding the variable 
 
      // instantiate an 8-bit unsigned integer 
      // and fill it with the correct value 
      Unsigned type (8); 
 
      switch (GetChosenId()) 
      { 
      case id_FieldA: 
              type.SetValue (1); 
              break
      case id_FieldB: 
              type.SetValue (5); 
              break
      case id_FieldC: 
              type.SetValue (28); 
              break
      } 
 
      // write its value at the current position of the buffer 
      type.Encode (buffer); 
  } 
}}

3.3.5 Additional processing

Apart from making decisions in the decoder, codets can be used for any processing to be done on the encoded content.

The following example shows how it can be used for computing a checksum.

Let us consider the following record defined in TTCN-3...

 
type integer UInt16 (0 .. 65535); 
 
type record MyMessage 
{ 
  UInt16      FieldA, 
  UInt16      Checksum, 
 
  octetstring FieldB, 
  charstring  FieldC, 
 
  ... 
}

...and a C function, whose purpose is to compute the checksum for this message.

 
unsigned int MyMessage_compute_checksum ( 
               const Buffer& buffer,          // buffer 
               int           message_origin,  // position of the message in the buffer 
               int           message_length); // length of the message in bits

The checksum is computed from the encoded content, so it should calculated in PostEncode(), i.e. after the variable is encoded. Additionally the checksum value needs to be initialised to 0 before.

To perform this computation, several steps have to be followed:

  1. remember the origin of the message in the encoded buffer
  2. initialise the checksum value to zero
  3. encode the record
  4. compute the checksum and write the value in the buffer

This can be done with the following codets:

 
static int my_message_origin; 
 
namespace t3devlib { 
  namespace gen { 
 
  void MyMessage::PreEncode (Buffer& buffer) throw (DecodeError) 
  { 
    // remember the position in the buffer 
    my_message_origin = buffer.GetPosition(); 
 
    // initialise the checksum field to zero 
    Get_Checksum().SetValue(0) 
  } 
 
  void MyMessage::PostEncode (Buffer& buffer) throw (DecodeError) 
  { 
    // remember the current position in the buffer 
    int my_message_end = buffer.GetPosition(); 
 
    // compute the checksum 
    unsigned int checksum =  MyMessage_compute_checksum ( 
                               buffer, 
                               my_message_origin, 
                               my_message_end - my_message_origin); 
 
    // go back to the position of the checksum field 
    buffer.SetPosition (my_message_origin + 16); 
 
    // write the checksum into the buffer 
    Get_Checksum().SetValue (checksum); 
    Get_Checksum().Encode (buffer); 
 
    // return the end of the message 
    buffer.SetPosition (my_message_end); 
  } 
}}

In this example a static variable is used for storing information between successive calls, a cleaner way of doing this is presented later.

3.3.6 Reporting Errors

Errors in the codec are reported with exceptions. There is set of predefined exceptions for reporting generic errors.

If an error is detected inside a codet, it is possible (and even recommended) to report it with DecodeError() or EncodeError().

In the example presented in Section 3.3.4 there is one possible case of error in PreDecode(). When the encoded value representing the variant of the union is unknown, no hypothesis is defined and an exception is thrown afterwards. However this exception will not give an accurate account of the error. It will be limited to the information available to the decoder : “there is no hypothesis”.

One preferred way to report the error is to throw an exception with a relevent message directly from PreDecode(). The codet can be rewritten as follows.

 
namespace t3devlib { 
  namespace gen { 
 
  void MyUnion::PreDecode (Buffer& buffer) throw (DecodeError) 
  { 
      // called before decoding the variable 
 
      // instantiate an 8-bit unsigned integer 
      // and read its value from the current position of the buffer 
      Unsigned type (8); 
      type.Decode (buffer); 
 
      // choose the adequate field 
      switch (type.GetValue()) 
      { 
      case 1: 
              SetHypChosenId (id_FieldA); 
              break
      case 5: 
              SetHypChosenId (id_FieldB); 
              break
      case 28: 
              SetHypChosenId (id_FieldC); 
              break
      default
              // unknown type --> throw an exception 
              DecodeError ex (this); 
              ex.Msg() << Unknown value for deciding MyUnion variant:  
                       << type.GetValue(); 
                       <<  (valid values are: 1, 5 and 28) << endl; 
              throw ex; 
      } 
  } 
 
}}

Then the error message carried by the exception will be:

Error while decoding (MyMessage)  
Unknown value for deciding MyUnion variant: 66 (valid values are: 1, 5 and 28)

Note that to stay consistent with predefined exceptions every exception message should finish with a newline.

3.3.7 Extending class definitions

When writing a set of codets for a class in some cases it may be useful to extend the class with additional attributes or member functions. Since the class definition is written by the codec generator this cannot be done directly.

Anyway the generator provides one way of achieving this. In every generated class definitions it includes a macro named DEFINITIONS_[ClassName]().

By defining this macro in the header provided to t3cdgen with -i (see Section 3.1) it is possible to extend the class definition.

The example presented in Section 3.3.5 uses a static variable for storing a value across different calls. This can be rewritten with a class attribute. Additionally the function for computing the checksum can be defined as a member function.

Hence the new codets will look like:

 
//////////////////////////////////////////////////// 
// Macro definition in the header file given with -i 
 
#define DEFINITIONS_MyMessage()            \ 
  private:                            \ 
    int mMyMessageOrigin;                 \ 
    unsigned int ComputeChecksum (          \ 
                  const Buffer& buffer,      \ 
                  int message_origin,        \ 
                  int message_length) const
 
 
//////////////////////////////////////////////////// 
// Codet definitions (in a seperate file) 
 
namespace t3devlib { 
  namespace gen { 
 
  void MyMessage::PreEncode (Buffer& buffer) throw (DecodeError) 
  { 
    // remember the position in the buffer 
    mMyMessageOrigin = buffer.GetPosition(); 
 
    // initialise the checksum field to zero 
    Get_Checksum().SetValue(0) 
  } 
 
  void MyMessage::PostEncode (Buffer& buffer) throw (DecodeError) 
  { 
    // remember the current position in the buffer 
    int my_message_end = buffer.GetPosition(); 
 
    // compute the checksum 
    unsigned int checksum =  ComputeChecksum ( 
                               buffer, 
                               mMyMessageOrigin, 
                               my_message_end - mMyMessageOrigin); 
 
    // go back to the position of the checksum field 
    buffer.SetPosition (mMyMessageOrigin + 16); 
 
    // write the checksum into the buffer 
    Get_Checksum().SetValue (checksum); 
    Get_Checksum().Encode (buffer); 
 
    // return the end of the message 
    buffer.SetPosition (my_message_end); 
  } 
 
}}

3.3.8 Handling Exceptions

Encoding and decoding errors are reported with exceptions. Unhandled exceptions are automatically processed by T3DevLib before returning from TCI calls tciEncode() and tciDecode(). The error is reported to the test executable, which triggers an error verdict for the current testcase.

This may not be desired behaviours. For some reasons it can be useful to process exceptions:

Exceptions can be caught with overloaded variants of PostEncode() and PostDecode(). These variants take a second parameter that refers to the exception that was caught:

 
namespace t3devlib { 
  namespace gen { 
 
  void MyVarType::PostEncode (Buffer& buffer, EncodeError& ex) throw (EncodeError); 
 
  void MyVarType::PostDecode (Buffer& buffer, DecodeError& ex) throw (DecodeError); 
 
}}

If one of these codets is defined, then the generated function Encode() (respectively Decode()) will contain a try{} catch{} construct for handling exceptions. If no exceptions are caught, then PostEncode(Buffer&) is called normally if defined. Otherwise PostEncode(Buffer&, EncodeError&) is called and the function returns directly without calling PostEncode(Buffer&).

Silently ignoring exceptions can be done as follows:

 
namespace t3devlib { 
  namespace gen { 
 
  void MyMessage::PostDecode (Buffer& buffer, DecodeError& ex) throw (DecodeError) 
  { 
#ifdef DEBUG 
    cerr << Caught exception in MyMessage::PostDecode(): << endl 
         << ex; 
#endif 
 
    // possibly set some ‘‘clean’’ values 
    Get_FieldA().SetValue(0); 
    ... 
  } 
}}

Note that it is highly recommended to keep a statement for displaying the exception on the console, this can prevent strong headaches when debugging the codec.

The next example will show how to implement a fall-back mechanism. The message can be defined in TTCN-3 as follows:

 
// definition of the message that is needed for the test 
type record MyMessageBody 
{ 
  ... 
} 
 
// extra information in the message, that we dont want to implement fully 
// but we would like to have them in the test logs 
type record ExtraInformationBody 
{ 
  ... 
} 
 
// union for representing extra information 
// body is chosen if decoded successfully 
// otherwise raw is chosen 
type union ExtraInformation 
{ 
  ExtraInformationBody body, 
  bitstring raw 
} 
 
// definition of the actual message that is exchanged with the SUT 
type record MyMessage 
{ 
  MyMessageBody    body, 
  ExtraInformation extra 
}

The codets can be implemented as follows:

 
//////////////////////////////////////////////////// 
// Macro definition in the header file given with -i 
 
#define DEFINITIONS_ExtraInformation()      \ 
  private:                            \ 
    int mExtraInformationOrigin; 
 
 
//////////////////////////////////////////////////// 
// Codet definitions (in a separate file) 
 
namespace t3devlib { 
  namespace gen { 
 
  void ExtraInformation::PreDecode (Buffer& buffer) throw (DecodeError) 
  { 
    // try to decode the record 
    SetHypChosenId (id_body); 
 
    // remember the position of the encoded record 
    mExtraInformationOrigin = buffer.GetPosition(); 
  } 
 
  void ExtraInformation::PostDecode (Buffer& buffer, DecodeError& ex) throw (DecodeError) 
  { 
#ifdef DEBUG 
    // this is always useful 
    cerr << Caught exception in ExtraInformation::PostDecode(): << endl 
         << ex 
         <<  -> falling back to bitstring << endl; 
#endif 
 
    // go back to the beginning of the record 
    buffer.SetPosition (mExtraInformationOrigin); 
 
    // decode the raw field 
    // note that Get_raw() implicitly calls ChooseField(id_raw) 
    Get_raw().Decode (buffer); 
  } 
}}

3.4 Defining Primitive Types

The current version of the CoDec generator supports only structured types (record, set, record of, set of and union). Support for primitive types will be added later. For the moment they need to be defined manually.

The procedure is straightforward: one C++ class is defined for each TTCN-3 subtype. This can be done either from scratch or with the help of some C macros provided by T3DevLib.

3.4.1 Predefined Macros

Five macros are provided for easing the definition of common types:

T3DEVLIB_BASIC_DEFINITION(module, type, parent) is used when the default encoding/decoding mechanism implemented in the library is sufficient1 .

 

 
module MyModule { 
  type charstring MyCharStringType; 
  type bitstring MyBitStringType; 
}
 
namespace t3devlib { 
  namespace gen { 
 
  T3DEVLIB_BASIC_DEFINITION(MyModule, MyCharStringType, t3devlib::Charstring) 
  T3DEVLIB_BASIC_DEFINITION(MyModule, MyBitStringType, t3devlib::Bitstring) 
}}

T3DEVLIB_INTEGER_DEFINITION(module, type, parent, size) defines a signed or unsigned big-endian integer.

 
module MyModule { 
  type integer UInt8  (0..255); 
  type integer UInt10 (0..1023); 
  type integer Int8   (-128..127); 
}
 
namespace t3devlib { 
  namespace gen { 
 
  T3DEVLIB_INTEGER_DEFINITION(MyModule, UInt8,  Unsigned,  8) 
  T3DEVLIB_INTEGER_DEFINITION(MyModule, UInt10, Unsigned, 10) 
  T3DEVLIB_INTEGER_DEFINITION(MyModule, Int8,   Signed,    8) 
}}

T3DEVLIB_ASCII_INTEGER_DEFINITION(module, type, parent) defines a signed or unsigned decimal ascii-encoded integer.

 
module MyModule { 
  type integer AsciiInt; 
}
 
namespace t3devlib { 
  namespace gen { 
 
  T3DEVLIB_ASCII_INTEGER_DEFINITION(MyModule, AsciiInt,   Signed) 
}}

T3DEVLIB_FIXED_STRING_DEFINITION(module, type, parent, size) defines a string, whose length is fixed.

 
module MyModule { 
 
  type bitstring Bit10 length (10); 
 
  type octetstring Ipv4AddressType length (4); 
 
}
 
namespace t3devlib { 
  namespace gen { 
 
  T3DEVLIB_FIXED_STRING_DEFINITION(MyModule, Bit10, Bitstring, 10) 
 
  T3DEVLIB_FIXED_STRING_DEFINITION(MyModule, Ipv4AddressType, Octetstring, 32) 
}}

T3DEVLIB_NULL_TERMINATED_CHARSTRING_DEFINITION(module, type) defines a character string, which is terminated with a null character (like C strings). The terminal character ’\0’ is not present in the TTCN-3 string.

 
module MyModule { 
 
  type charstring MyNullTerminatedCharstring; 
 
}
 
namespace t3devlib { 
  namespace gen { 
 
  T3DEVLIB_NULL_TERMINATED_CHARSTRING_DEFINITION(MyModule, MyNullTerminatedCharstring) 
}}

3.4.2 Class definition

In other cases it may be necessary to implement the class manually from scratch.

An example is described in section 6.2 (DNSTester2) for handling IP addresses in the dot notation and using the codec to convert it from/into a 32-bit field.

This type is defined as a charstring in TTCN-3:

 
module DNSTester2 { 
  type charstring IPAddress; 
}

A class named IPAddress is defined. It derives from t3devlib::Charstring. Since the coding rules are not trivial the two member functionsEncode() and Decode() need to be reimplemented. Also GetModuleName() and GetTypeName must be redefined.

 
namespace t3devlib { 
  namespace gen { 
 
  class IPAddress : public t3devlib::Charstring { 
    public
    IPAddress() : Charstring() {} 
 
    const char* GetModuleName() const { return DNSTester2} 
    const char* GetTypeName() const   { return IPAddress} 
 
    void Encode (Buffer& buffer) throw (EncodeError); 
    void Decode (Buffer& buffer) throw (DecodeError); 
  }
}}

3.4.3 Registering manually defined types

In earlier versions user defined types had to be registered manually with Variable::Register(). As of version 0.9.1 the CoDec generator detects them automatically. It is no longer needed to call Variable::Register() (warnings will be issued at runtime if types are registered more than once).

3.5 Reusing an existing CoDec

It is sometimes desirable to have several separate Coding-Decoding (CD) modules in the same test suite, each module being in charge of encoding and decoding a given set of types. This scenario typically happens when parts of the CoDec are already implemented (possibly by another team or for another project). However the TTCN-3 runtime architecture allows only one CoDec module at once, using several modules is conceptually possible but requires some adjustments.

T3DevKit provides two ways of combining the generated CoDec with an existing CoDec, depending on its role in relation with the Test Executable and the other CoDec.

For further information T3DevKit’s package contains an example of test that demonstrate how to mix several CoDecs. It is located in the examples/MixedCoDec directory.

3.5.1 Overview of the problem and solutions

Using several CoDecs together raises two issues that are not addressed by the TCI-CD interface:

Consequently in a static configuration, the following changes are needed.

The CoDec generated with T3DevKit can act as a Master (Figure 3.4) or as a Slave (Figure 3.5). It is even possible to have both roles at the same time an produce an hybrid configuration (Figure 3.6). An hybrid configuration can be useful when a structured type (a record for example) handled by a slave CoDec contains fields whose type are handled by another CoDec (note that such a design should be handled with care to avoid recursive loops).


PIC

Figure 3.4: Generated CoDec acting as the Master (Section 3.5.2)



PIC

Figure 3.5: Generated CoDec acting as a Slave (Section 3.5.3)



PIC

Figure 3.6: An Example of hybrid configuration


3.5.2 Calling another CoDec (Master mode)

To act as a master, the generated needs to know which type is handled by which CoDec. This is done with a C macro named T3DEVLIB_OTHERCODEC_DEFINITION().

T3DEVLIB_OTHERCODEC_DEFINITION(module, type, tci_encoder, tci_decoder) takes four parameters:

The macro will generate an opaque class inherited from OtherCoDec and that will transparently call the adequate CoDec when the Encode() or Decode() method is called.

3.5.3 Being called by another CoDec (Slave mode)

This feature was removed in T3DevKit v0.10