PER encodings are designed to be compact: they are schema informed and bit packed, which permits significant compression across the wire. Unfortunately, it also eliminates the regular structure of BER encodings, which makes it nearly impossible to improve throughput by skipping past unneeded elements. Moreover, the code footprint for many specifications is quite large, making it difficult to deploy protocol stacks on constrained devices.
This is a problem we've thought about along with our customers, and we have provided a few different features in ASN1C as a result. The ASN1C Compiler Configuration File is instrumental in helping to achieve these optimizations. Following is a deep dive on the various configuration options that can be used.
A Sample Specification
For the following examples, we'll use a toy specification to illustrate the various configuration file features. Where a feature is language specific, we'll note this in parentheses.
Test DEFINITIONS ::= BEGIN
S ::= SEQUENCE { s UTF8String }
PDU ::= SEQUENCE { s S OPTIONAL, ... }
END
ASN1C generates code in different ways depending on the language selected. By default, C/C++ code generation results in a single header file for each module that contains class or structure definitions for the types defined in the module as well as function prototypes and methods for encoding, decoding, structure initialization, and so on. By contrast, Java and C# produce a single file for each type defined in a module that contains relevant methods for encoding, decoding, and initialization. (The C# code generation can be adjusted to produce a different number of files, but by default it behaves like Java code generation.)
For the given specification, then, we find the following files generated for each language:
- C/C++ • Test.c, Test.h, TestDec.c, TestEnc.c
- Java • PDU.java, S.java
- C# • PDU.cs, S.cs
The following sections will describe how some of the configuration file directives can be used to reduce both the number of generated functions and files.
<notUsed/>
The <notUsed/> predicate can be applied to elements in a CHOICE as well as OPTIONAL elements in a SEQUENCE or SET. In the context of our sample specification, we would produce an XML file that looks like this:
<asn1config> <module name="Test"> <production name="PDU"> <element name="s"> <notUsed/> </element> </production> </module> </asn1config>
The s element in the PDU type is the only optional element.
<notUsed/> changes the element's type from S to NULL, which is not represented in PER encodings. The expectation in this case is that the optional element is never encoded, so the optional bit in the preamble is always turned off. In the event that a PER encoding is specified manually by the user—see below—the behavior is to encode the specified value.
This produces the following changes in generated C code.
First, the data structure for the PDU type is changed:
Second, the encoding and decoding structures are changed (only decoding is shown here; the changes for encoding are analogous):
Despite these changes, the type definitions for S are still generated to allow users to encode and decode the type if needed. (Java and C# users will note that the class files are still generated, even if the element is marked as not used.)
<exclude types="[type1, type2, ...]">
If we want to eliminate the type definitions entirely, we can use the <exclude> configuration element. In simple specifications, this is relatively straightforward since we can easily trim out undesired types, but ASN1C will still generate code for types that are referenced (i.e., used) after the configuration file is processed whether they are specifically excluded or not.
Having said this, we can adjust the configuration file easily to exclude a particular type from the module:
<asn1config> <module name="Test"> <production name="PDU"> <element name="s"> <notUsed/> </element> </production> <exclude types="S"/> </module> </asn1config>
In this case, the generated C code no longer contains a definition for the S type at all:
(In the other languages, no files are generated for the corresponding types.)
Alongside the <exclude> element is an analogous <include> element that can be used to build an API from the ground up, rather than by exclusion. This is often more practical, since types are not excluded unless all of the references to that type are unused.
<perEncoding> (C/C++ only)
There remain, however, occasions when some repeated or known content will be encoded in the stream. For example, a protocol stack may be required encode valid content for an outside consumer even though it does not really care about a particular field.
In this case, the <perEncoding> element can be set to alter the C/C++ encoding and decoding logic: instead of making a call to the generated encoding and decoding function, the type will be encoded or decoded inline as a sequence of octets. Suppose, for example, that the s element was always equal to Jones. The PER encoding will include a length for the string (it is unconstrained in the specification) and the value, but the generated encoder and decoder applications will ignore it entirely, just as though the <notUsed/> predicate were used.
In this case, the PER encoding is 0x054a6f6e6573: the length is five bytes (0x05) and the string is Jones. There are therefore 48 bits in the encoding; the configuration file expects to have a number of bits provided so as to avoid encoding unneeded bits in a bit-packed message. To specify the encoding, the configuration file looks like this:
<asn1config> <module name="Test"> <production name="PDU"> <element name="s"> <perEncoding numbits="48">054a64636573</perEncoding> </element> <production> <exclude types="S"/> </module> </asn1config>
In this case, the resulting encoding logic will differ from the usual result like this:
The resulting decoding logic will differ like this:
When generating a sample writer and reader application, we see the following during execution:
$ bin/writer The following record will be encoded: data { s = } Hex dump of encoded record: 40 05 4a 6f 6e 65 73 @.Jones Binary dump: data.PDU extension marker bit offset: 0 bit count: 1 next bit offset: 1 0------- -------- -------- -------- -------- ---- data.PDU sPresent bit offset: 1 bit count: 1 next bit offset: 0 -1------ -------- -------- -------- -------- ----
The hex dump indicates that we have encoded the string "Jones," but the content is ignored by the writer. While it's possible to set the string value in the data structure, it is never referenced by the encoding logic.
$ bin/reader Dump of decoded bit fields: data.PDU extension marker bit offset: 0 bit count: 1 next bit offset: 1 0------- -------- -------- -------- -------- ---- data.PDU optional bits bit offset: 1 bit count: 1 next bit offset: 0 -1------ -------- -------- -------- -------- ---- Data { s = }
"Jones" has been transparently overlooked by the reader, and no value has been stored in the data structure.
Generating Code for Selected PDUs
Finally, it may be possible to strip down a generated API by explicitly selecting PDUs to support. Our toy example is a bit too simple to demonstrate, so instead we can use E-UTRA LTE RRC (i.e., TS 36.331). ASN1C identifies PDUs in any specification by determining whether they are referenced by other types. This does not always correlate, however, to genuine PDU data types—or to those that a particular protocol stack really needs to support.
In RRC, for example, ASN1C identifies the following PDUs: BCCH-BCH-Message, BCCH-DL-SCH-Message, MCCH-Message, PCCH-Message, DL-CCCH-Message, DL-DCCH-Message, UL-CCCH-Message, UL-DCCH-Message, HandoverCommand, HandoverPreparationInformation, and UERadioAccessCapabilityInformation. Supposing that we only needed to support the RRC messages rather than the inter-node messages, ASN1C can provide some code savings in one of two ways: by using a configuration file, or by selective compilation on the command line.
The configuration file would look like this:
<asn1config> <module name="EUTRA-InterNodeDefinitions"> <noPDU/> </module> </asn1config>
Another means to achieve roughly the same equivalent is to use the <include> directive, as in this elided configuration file:
<asn1config> <module name="EUTRA-RRC-Definitions"> <include types="BCCH-BCH-Message,BCCH-DL-SCH-Message,..."> </module> </asn1config>
All of the desired PDU types need to be specified; ASN1C will automatically include all of the dependent types for those specified and exclude the rest.
This differs from using –depends on the command line, which excludes only types from imported modules while retaining all of the types in the main module (in this case, EUTRA-RRC-Definitions). The behavior is otherwise similar, and could be achieved using the following invocation of ASN1C:
asn1c EUTRA-RRC-Definitions.asn -c -per -pdu BCCH-BCH-Message \ -pdu BCCH-DL-SCH-Message -pdu MCCH-Message -pdu PCCH-Message \ -pdu DL-CCCH-Message -pdu DL-DCCH-Message -pdu UL-CCCH-Message \ -pdu UL-DCCH-Message -depends
The final option, –depends, causes ASN1C to import only those type definitions that are needed to provide complete support for the selected PDU types. This has the effect of shrinking the generated code footprint by automatically eliminating unnecessary types from imported modules.
Concluding Remarks
These are only a few of the many ways that ASN1C's output can be customized to help shrink both message size and the generated code foot print. Users are encouraged to look through the Compiler Configuration File documentation for further information. As always, please contact us if you have any questions. We'll be glad to help.