At this point you may be thinking something on the line of “hey, since variables and functions are also members of the struct, I should be able to access them the same way than fields, right?”.
So you will want to do:
(poke) var p = Packet 12#B (poke) p.real_size (poke) p.corrected_crc
But sorry, this won’t work.
To understand why, think about the struct building process we sketched above. The mapper and constructor functions are derived/compiled from the struct type. You can imagine them to have prototypes like:
Packet_mapper (IOspace, offset) -> Packet value Packet_constructor (template) -> Packet value
You can also picture the fields, variables and functions in the struct type specification as being defined inside the bodies of Packet_mapper and Packet_constructor, as their contents get mapped/constructed. For example, let’s see what the mapper does:
Packet_mapper: . Map a byte, put it in a local `magic'. . Map a byte, put it in a local `size'. . Calculate the real size, put it in a local `real_size'. . Map an array of real_size bytes, put it in a local `payload'. . Map an array of real_size bytes, put it in a local `control'. . Compile a function, put it in a local `corrected_crc'. . map a byte, call the function in the local `corrected_crc', complain if the values are not the same, otherwise put the mapped byte in a local `crc'. . Build a struct value with the values from the locals `magic', `size', `payload', `control' and `crc', and return it.
The pseudo-code for the constructor would be almost identical. Just replace "map a byte" with “construct a byte”.
So you see, both the values for the mapped fields and the values for the variables and functions defined inside the struct type end as locals of the mapping process, but only the values of the fields are actually put in the struct value that is returned in the last step.
This is where methods come in the picture. A method looks very similar to a function, but it is not quite the same thing. Let me show you an example:
load supercrc; type Packet = struct { byte magic = 0xab; byte size; var real_size = (size == 0xff ? 0 : size); byte[real_size] payload; byte[real_size] control; fun corrected_crc = int: { try return calculate_crc (payload, control); catch if E_div_by_zero { return 0; } } int crc = corrected_crc; method c_crc = int: { return corrected_crc; } };
We have added a method c_crc
to our Packet struct type, that
just returns the corrected superCRC (patented, TM) of a packet. This
can be invoked using dot-notation, once a Packet value is
mapped/constructed:
(poke) var p = Packet 12#B (poke) p.c_crc 0xdeadbeef
Now, the important bit here is that the method returns the corrected
crc of a Packet
. That’s it, it actually operates on a
Packet value. This Packet value gets implicitly passed as an argument
whenever a method is invoked.
We can visualize this with the following “pseudo Poke”:
method c_crc = (Packet SELF) int: { return SELF.corrected_crc; }
Fortunately, poke takes care to recognize when you are referring to fields of this implicit struct value, and does The Right Thing(TM) for you. This includes calling other methods:
method foo = void: { ... } method bar = void: { [...] foo; }
The corresponding “pseudo-poke” being:
method bar = (Packet SELF) void: { [...] SELF.foo (); }
It is also possible to define methods that modify the contents of struct fields, no problem:
var packet_special = 0xff; type Packet = struct { byte magic = 0xab; byte size; [...] method set_size = (byte s) void: { if (s == 0) size = packet_special; else size = s; } };
This is what is commonly known as a setter. Note, incidentally,
how a method can also use regular variables. The Poke compiler knows
when to generate a store in a normal variable such as
packet_special
, and when to generate a set to a SELF
field.
Given the different nature of the variables, functions and methods, there are a couple of restrictions:
This will be rejected by the compiler:
type Foo = struct { int field; fun wrong = void: { field = 10; } };
Remember the construction/mapping process. When a function accesses a
field of the struct type like in the example above, it is not doing
one of these pseudo SELF.field = 10
. Instead, it is simply
updating the value of the local created in this step in Foo_mapper:
Foo_mapper: . Map an int, put it in a local `field'. . [...]
Setting that local would impact the mapping of the subsequent fields
if they refer to field
(for example, in their constraint
expression) but it wouldn’t actually alter the value of the field
field
in the struct value that is created and returned from the
mapper!
This is very confusing, so we just disallow this with a compiler error “invalid assignment to struct field”, for your own sanity 8-)
How could they be? The field constraint expressions, the initialization expressions of variables, and the functions defined in struct types are all executed as part of the mapper/constructor and, at that time, there is no struct value yet to pass to the method.
If you try to do this, the compiler will greet you with an “invalid reference to struct method” message.
Something to keep in mind about methods is that they can destroy the integrity of the data stored in a struct. Consider for example the following struct type:
type Packet = struct { byte magic = 0xab; byte size : size <= 4096; [...] method set_size = (byte s) void: { if (s == 0) size = packet_special; else size = s; } };
Observe how this new version of Packet
has an additional
constraint that specifies size
should not exceed 4096.
However, when the method set_size
is executed the constraints
are not checked again. This is useful at times, but can also lead to
unintended data corruption.
A solution for this problem is to make methods aware of the restrictions. Like in this case:
type Packet = struct { var MAXSIZE = 4096; byte magic = 0xab; byte size : size <= MAXSIZE; [...] method set_size = (byte s) void: { if (s == 0) size = packet_special; else if (s > MAXSIZE) raise E_inval; else size = s; } };
Note how we use a variable MAXSIZE
in order to avoid
hard-coding 4096 twice in the struct definition.
Occasionally it is useful to access to additional properties of a
containing struct or union in a method, other than its fields. For
this purpose it is possible to refer explicitly to the struct using
the SELF
keyword. Note that the value referred has type
any
.
For example, this is how we could print the name of the current alternative in a pretty-printer for an union:
type Foo = union int<32> { int<32> One == 1; int<32> Two == 2; method _print = void: { printf ("%s (%v)", SELF'ename (0), SELF'elem (0)); } };