Field access and method invocations
In the previous blog, we explained what are untyped variables and value holders in e, and how to assign and retrieve values to/from them. In this and the next blogs, we will see how they can be used in conjunction with the Reflection API, to perform operations at run time.
Normally, when you declare fields in your e structs and units, you then procedurally assign values to those fields at some points and retrieve their values at others. When you declare a method, you call it with certain parameters and retrieve its return value for later use. All of this is fine when you deal with a specific field or method, and that is what you need most of the time.
But what if you want to perform some generic operation? For example, you may want--given anye object (of any struct or unit type, which is unknown upfront)--to go over all its numeric fields and print their values. Or, you may want to traverse the whole unit tree, and on every unit whose type has a specific method (given by name), call that method and print its result.
The Reflection API allows us to perform tasks like that in a fairly easy manner. Here are some reflection methods which are helpful for those tasks. Given an instance object, the following two methods allow you to get the reflection representation of the struct or unit type of the object.
- rf_manager.get_struct_of_instance(instance: base_struct): rf_struct
This method returns the like struct of the object, disregarding when subtypes.
- rf_manager.get_exact_subtype_of_instance(instance: base_struct): rf_struct
This method returns the most specific type, including when subtypes, of the object.
For example, for a red packet instance, get_struct_of_instance() will return the reflection representation of type packet, and get_exact_subtype_of_instance() will return the representation of type red packet.
The following methods of rf_field allow, given an instance object of some struct, to set or get the value of the specific field of that object.
- rf_field.set_value(instance: base_struct, value: rf_value_holder);
- rf_field.set_value_unsafe(instance: base_struct, value: untyped);
- rf_field.get_value(instance: base_struct): rf_value_holder;
- rf_field.get_value_unsafe(instance: base_struct): untyped;
The set_value methods take the value passed as parameter, and assign it to the given field of the specified object. The get_value methods retrieve the value of the given field of the specified object and return it. There is a safe and an unsafe version of each method. The safe version uses a value holder, which already contains the type information for the value (as was explained in the previous blog), performs additional checks, and throws a run-time error in case of an inconsistency (for example, if the field does not belong to the struct type of the given instance). The unsafe version (the one with the _unsafe suffix) does not use a value holder and does not perform such checks; in case of an inconsistency, its behavior is undefined and might even cause a crash. Thus, you need to use it with a care. However, the unsafe version is more efficient, and I recommend using it when possible.
Similar to the above rf_field methods, the following methods of rf_method, given an instance object of some struct, allow you to invoke a specific method of that object or to start a TCM.
- rf_method.invoke(instance: base_struct, params: list of rf_value_holder): rf_value_holder;
- rf_method.invoke_unsafe(instance: base_struct, params: list of untyped): untyped;
- rf_method.start_tcm(instance: base_struct, params: list of rf_value_holder);
- rf_method.start_tcm_unsafe(instance: base_struct, params: list of untyped);
The invoke methods call the given method on the specified object and return the value returned from that method. If the given method has parameters, they should be passed as a list in the second parameter; the list size must exactly match the number of parameters the method expects to get. Similarly, the start_tcm methods start the given TCM on the specified object. As with the rf_field methods above, the difference between the safe and unsafe versions of these methods is that the safe one uses value holders and performs additional run-time checks, while the unsafe version is more efficient.
The following short example demonstrates the usage of the above methods. The following method gets an object of an unknown type (declared as any_struct) and a method name. It goes over all fields of the object whose type is int, and calls the method by the given name, passing the field value as parameter. For simplicity, we assume it is known that the method by the given name indeed exists and has one parameter of type int.
extend sys {
print_int_fields(obj: any_struct, meth_name: string) is {
// Keep the reflection representation of the int type itself
var int_type: rf_type = rf_manager.get_type_by_name("int");
// Keep the struct type of the object
var s: rf_struct = rf_manager.get_exact_subtype_of_instance(obj);
// Keep the method which is to be called
var m: rf_method = s.get_method(meth_name);
// Go over fields of the struct
foreach (f) in s.get_fields() do {
// Is this field of type 'int' ?
if f.get_type() == int_type then {
// Retrieve the field value ...
var value: untyped = f.get_value_unsafe(obj);
// ... and pass it to the method
compute m.invoke_unsafe(obj, {value});
};
};
};
};
In the next blog in the series, we will discuss some additional relevant reflection methods, give several tips, and look at some more interesting examples.
Yuri Tsoglin
e Language team, Specman R&D