Types in Ruby
- Basics
- Loading and Accessing Types
- Converting between the C++ definitions and more Ruby-ish types
- Extending the Rock Types
You will be using Ruby to interact with a running system (via Syskit) and post-process log files. It's important to understand how the types that are defined and exchanged from the C++ side end up being manipulated in Ruby.
Basics
The mapping from C++ to Ruby is mostly as one would expect: one can create, read
or modify a struct by setting or reading its fields. One can access an array or
std::vector
as one would access a Ruby array and so on.
For instance, an instance of the Time type we already used as example:
namespace base {
struct Time
{
uint64_t microseconds;
static Time fromMilliseconds(uint64_t ms);
Time operator +(Time const& other);
};
}
would be accessed with:
obj.microseconds # current value of 'microseconds' as a Ruby integer
obj.microseconds = 20 # set the value of 'microseconds' to 20
A more complex struct such as:
namespace base {
struct Timestamps {
std::vector<base::Time> stamps;
};
}
would be accessed with
obj.stamps[0].microseconds
obj.stamps << new_time
Enums are represented as Ruby symbols, so
namespace base {
enum Result {
OK, FAILED
};
struct S {
Result status;
};
}
would be manipulated with
obj.status # => :OK
obj.status = :FAILED
Loading and Accessing Types
To get access to registered types within Syskit, one needs to call
import_types_from
at toplevel with the oroGen project that defines the type:
import_types_from 'base'
From their on, all the types that the base
typekit define are made available
under the Types
object. For instance, the base::Time
type is available
as Types.base.Time
. Containers based on std::vector
are mapped using
functions, e.g. Types.std.vector(Types.base.Time)
for
/std/vector</base/Time>
.
New objects can thus be created Types.base.Time.new
. New objects - except
enums - are left uninitialized. Enums are initialized to the first valid value
in their definition. Call #zero!
to zero-initialize an object.
The fields of a struct can be initialized on construction: Types.base.Time.new(microseconds: 0)
.
Converting between the C++ definitions and more Ruby-ish types
As it is, the Rock type system is optimized for C++. The types can have a proper API, accessors, initialization … These parts of the types are available in C++ but are "lost in translation" when passed to Ruby.
However, Ruby also has a rich ecosystem of built-in types and external
libraries, that sometimes match what the C++ types provide. For instance,
Rock's existing base::Time
type has an equivalent in the Ruby Time
class.
To ease the use of Rock on the Ruby side, the framework provides a way to
convert to and from pure-Ruby types. Rock's own base/types
package defines
such conversions. The main (but no only) conversions are used to handle Eigen
types in Ruby (using built-in Eigen bindings), and the Time
conversion that
we just described.
If one defines a conversion to ruby with:
Typelib.convert_to_ruby '/base/Time', Time do |value|
microseconds = value.microseconds
seconds = microseconds / 1_000_000
Time.at(seconds, microseconds % 1_000_000)
end
Then the framework will automatically convert /base/Time
values into Ruby's
Time using the given block. Note that the Ruby type is optional in this case
(whatever's returned by the block will be considered "the converted type")
Important the conversions must be defined before the type is loaded
Where to define these ? One-shot conversions can be defined straight into
your system (ruby script or Syskit app). For conversions that are too widespread
for that, consider installing a typelib_plugin.rb
file under a folder that is resolved
by RUBYLIB
(e.g. mylib/typelib_plugin.rb
), most likely a Ruby package.
The inverse conversion may also be provided
Typelib.convert_to_ruby Time, '/base/Time' do |value, type|
type.new(
microseconds: value.tv_sec * 1_000_000 + value.tv_usec)
end
Reminder if you don't understand the /base/Time
syntax, we've covered that
when we talked about the type system's naming scheme.
Extending the Rock Types
An alternative to the conversions mechanism is to extend the types with new methods, and/or initializers.
To define methods on the type class itself, one uses
Typelib.specialize_model
. The following would for instance allow to create a
/base/Angle
initialized with NaN by doing Types.base.Angle.Invalid
Typelib.specialize_model '/base/Angle' do
def Invalid
new(rad: position: Float::NAN)
end
end
To define methods on the values themselves, one uses Typelib.specialize
.
Typelib.specialize '/base/Angle' do
def to_degrees
rad * 180 / Math::PI
end
end
It is possible to define an initializer this way:
Typelib.specialize '/base/Angle' do
def initialize
self.rad = Float::NAN
end
end
Important the specializations must be defined before the type is loaded
Where to define these ? One-shot conversions can be defined straight into
your system (ruby script or Syskit app). For conversions that are too widespread
for that, consider installing a typelib_plugin.rb
file under a folder that is resolved
by RUBYLIB
(e.g. mylib/typelib_plugin.rb
). This would either be a plain Ruby package
or a file installed by a C++ package within the Ruby search path. Both methods are
described in more details in the Creating Functionality section.