The ASN1C compiler can parse parameterized type definitions and references as specified in the X.683 standard. These types allow dummy parameters to be declared that will be replaced with actual parameters when the type is referenced. This is similar to templates in C++.
A simple and common example of the use of parameterized types is the declaration of an upper bound on a sized type as follows:
SizedOctetString{INTEGER:ub} ::= OCTET STRING (SIZE (1..ub))
In this definition, ub
would be replaced with an
actual value when the type is referenced. For example, a sized octet
string with an upper bound of 32 would be declared as
follows:
OctetString32 ::= SizedOctetString{32}
The compiler would handle this just as if the original type was declared to be an octet string of size 1 to 32. That is, it will map the type to a go byte slice ([]byte) and do constraint checks before encoding and after decoding to ensure the value is within the specified range.
Another common example of parameterization is the substitution of a given type inside a common container type. For example, security specifications frequently contain a 'signed' parameterized type that allows a digital signature to be applied to other types. An example of this is as follows:
SIGNED { ToBeSigned } ::= SEQUENCE { toBeSigned ToBeSigned, algorithmOID OBJECT IDENTIFIER, paramS Params, signature BIT STRING }
An example of a reference to this definition would be as follows:
SignedName ::= SIGNED { Name }
where Name
would be another type defined elsewhere
within the module.
The compiler performs the substitution to create the proper Go type
for SignedName
:
type SignedName struct { ToBeSigned Name AlgorithmID []uint64 ParamS Params Signature asn1.BitString }
When processing parameterized type definitions, the compiler will first look to see if the parameters are actually used in the final generated code. If not, they will simply be discarded and the parameterized type converted to a normal type reference. For example, when used with information objects, parameterized types are frequently used to pass information object set definitions to impose table constraints on the final type. Since table constraints do not affect the code that is generated by the compiler when table constraint code generation is not enabled, the parameterized type definition is reduced to a normal type definition and references to it are handled in the same way as defined type references. This can lead to a significant reduction in generated code in cases where a parameterized type is referenced over and over again.
For example, consider the following oft-repeated pattern from the UMTS 3GPP specifications:
ProtocolIE-Field {RANAP-PROTOCOL-IES : IEsSetParam} ::= SEQUENCE { id RANAP-PROTOCOL-IES.&id ({IEsSetParam}), criticality RANAP-PROTOCOL-IES.&criticality ({IEsSetParam}{@id}), value RANAP-PROTOCOL-IES.&Value ({IEsSetParam}{@id}) }
In this case, IEsSetParam
refers to an information
object set specification that constrains the values that are allowed to
be passed for any given instance of a type referencing a
ProtocolIE-Field
. The compiler does not add any extra code
to check for these values, so the parameter can be discarded (note
that this is not true if the -tables
compiler option is specified). After processing the Information Object
Class references within the construct (refer to the section on
Information Objects for information
on how this is done), the reduced definition for
ProtocolIE-Field
becomes the
following:
ProtocolIE-Field ::= SEQUENCE { id ProtocolIE-ID, criticality Criticality, value ASN.1 OPEN TYPE }
References to the field are simply replaced with a reference to the
ProtocolID-Field
type.
If -tables is specified, the parameters are used and a new type instance is created in accordance with the rules above.