3.3 Csound 7: New Parser, New Possibilities
3.3.3 User-Defined Types: Structs
User-Defined Types (also called structs in Csound) are a feature that allows users to define their own data types using Csound Orchestra language code.
This provides extensibility within the language for the user to create new kinds of data and signals for processing.
Motivation
In Csound6, the new type system (described in Section 3.2.1) provided concrete type definitions. For core developers, it clarified both definition and internal use of type-related code. For third party developers, it made data types extensible, providing a systematic way to define and introduce new types.
For Csound7, the ability to define types is extended out to users and implemented as structs. The goal of this is provide users with the same ability to create their own signal representations that developers received in Csound6. This permits new kinds of research to be done by users.
Design
In Csound 7, user-defined types are called structs. They are based on C’s concept and implementation of structures, which are defined using the struct keyword. Kernighan and Ritchie define structures in C as:
a collection of one or more variables, possibly of different types, grouped together under a single name for convenient handling. (Structures are called “records” in some languages, notably Pascal.) Structures help to organise complicated data, particularly in large programs, because they permit a group of related variables to be treated as a unit instead of as separate entities.9
Like C, Csound structs are defined in terms of other existing types. They may use natively-defined types (such as a-, k-, and i-types), arrays, as well as other defined struct types. Structs in Csound can also be used with arrays, such that one can define an array of structs.
struct TypeName varName1 , varName2 [, varName3 ...]
Listing 3.32: Csound struct syntax
Listing 3.32 shows the syntax for defining structs in Csound Orchestra code. The user defines a struct type by using the struct keyword, followed by the name of the type, then a comma-separated list of members of the structure. As in other areas of Csound, struct member variables are typed using the same implicit or explicit type rules as other variables. Listing 3.33 presents a sample definition of a complex number type using explicitly-typed variables.
struct ComplexNumber real :k, imaginary :k
Listing 3.33: Csound struct example: ComplexNumber
Using Structs Defining a struct registers the data type with Csound’s type definition table. Once registered, users can write code that creates instances of the struct, read from and write values to the struct, as well as use the data type as arguments in UDOs. Listing 3.34 shows these various facets in use. (Note that the opcode definition here uses the new-style UDO syntax, discussed in Section 3.3.4.)
struct Rectangular x:i, y:i struct Polar R:i, t:i
opcode to_polar ( num : Rectangular ):( Polar ) ipolarR = sqrt ( num .x ^ 2 + num .y ^ 2)
ipolart = taninv2 ( num .y, num .x) * (360 / (2 * $M_PI )) retVal : Polar init ipolarR , ipolart
xout retVal endop
instr 1
r: Rectangular init 1.0 , 0.5 polar : Polar = to_polar (r)
print polar .R ; 1.118 print polar .t ; 26.565 endin
Listing 3.34: Csound struct usage example
In the example, the to_polar UDO takes in a single argument called num of type Rectangular. A variable called retVal of type Polar is initialised using values that are calculated using the x and y members of the passed in num. The syntax to access member values of a struct variable follows the same syntax as C, using the variable’s name, followed by a period, followed by a member name. In the example, num.x and num.y are used to read the x and y member values from the num variable.
Implementation
Implementing structs in Csound required modifications to the parser, semantic analyser, compiler, and engine runtime. Also, new type system related code was required, which will be described below. For the parser, new rules
(shown in Listing 3.35) were added to Csound’s grammar for processing struct definitions and member access. The struct_expr rule was also added to rules for input and output arguments (not shown).
struct_definition : STRUCT_TOKEN identifier struct_arg_list ;
struct_arg_list : struct_arg_list ',' struct_arg | struct_arg
;
struct_arg : identifier
| typed_identifier | array_identifier ;
struct_expr : struct_expr '.' identifier | identifier '.' identifier ;
Listing 3.35: Struct-related grammar rules
Next, the analyser was modified for processing struct definitions. When a definition is found, a new CS_TYPE is generated and registered with the type system, and a new init opcode OENTRY is synthesised and registered. Once the type is registered, it can be understood by the rest of the system and used for variable declarations, UDOs, etc. The init opcode is what allows users to create new instances of the struct.
For the generated CS_TYPE, the members in the struct definition are parsed and added to the type definition’s members field as a list of CS_VARIABLEs. The variables define both the name and the type of the member.
Other type-related functions for CS_TYPE, copyValue and createVari- able, are written in a generic way. They receive both the CS_TYPE and the memory allocated for the variable. When structs are copied or newly created, these functions reference the member variables from the CS_TYPE to perform their processing with the variable memory.
typedef struct csstructvar { CS_VAR_MEM ** members ; } CS_STRUCT_VAR ;
Listing 3.36: C data structure for Csound struct variables
Listing 3.36 shows the C data structure used for Csound struct variables. When a new Csound struct variable is created, the size to allocate for the variable will be calculated from the sizes of the members defined in the CS_TYPE. This uses the CS_VAR_MEM data structure so that the CS_TYPE for each of the members precedes its variable data, which allows RTTI to function with struct member data.
For struct member access, they are treated specially. For the analyser, the information from the struct member’s type is used for semantic verification. In the compiler, the member access is converted into a special address notation. At runtime, when an instrument instance is created, the notation is used to find the location in memory for the specified member. The process starts at base struct’s address, then navigates using the notation to find the specific member’s address using the members field from CS_STRUCT_VAR. The result is that when struct members are used with opcodes, the data pointer set for the opcode is the direct location for the member. This implementation adds a small cost at initialisation time to find the correct data address; however, this
adds no additional opcode calls at runtime to retrieve or set values within the struct.
Summary
Structs provide a user-extensible way to introduce new data types into Csound. The syntax for definition and usage were modeled upon C’s syntax and semantics for structures. The implementation provides efficient runtime characteristics as no additional opcode calls were necessary. The result is that users can now do new kinds of research and musical work with Csound that requires new data types.