Functions and Methods#
In the Tutorial we touched upon how to wrap C++ functions, static member functions and member functions. Now let’s go into more depth.
Default Arguments#
Going back to our initial C++ class example, lets add additional arguments to the hello()
method, one of which has a default value:
class Test
{
public:
Test();
std::string hello(std::string first, std::string second = "world");
};
Since default parameter values are not available through templates, it is necessary to tell Rice about it using Rice::Arg
:
#include <rice/rice.hpp>
using namespace Rice;
extern "C"
void Init_test()
{
Data_Type<Test> rb_cTest =
define_class<Test>("Test")
.define_constructor(Constructor<Test>())
.define_method("hello",
&Test::hello,
Arg("hello"), Arg("second") = "world"
);
}
The syntax is Arg(nameOfParameter)[ = defaultValue]
. The name of the parameter is not important here (it is for readability), but the value set via operator=
must match the type of the parameter. As such it may be necessary to explicitly cast the default value.
.define_method("hello",
&Test::hello,
Arg("hello"), Arg("second") = (std::string)"world"
);
These Rice::Arg
objects must be in the correct positional order. Thus if the second argument has a default value, then there must be two Arg objects.
Now, Ruby will now know about the default arguments, and this wrapper can be used as expected:
t = Test.new
t.hello("hello")
t.hello("goodnight", "moon")
This also works with Constructors:
.define_constructor(Constructor<SomeClass, int, int>(),
Arg("arg1") = 1, Arg("otherArg") = 12);
Return#
Similarly to the Arg
class, Rice also supports a Return
class that let’s you tell Rice how to handle returned values from C++. This is particularly important in correctly managing memory (see C++ to Ruby).
It is also helpful in dealing with Ruby’s VALUE type which represent Ruby objects. Most of the time Rice will automatically handle VALUE instances, but if a native method takes a VALUE argument or returns a VALUE instance then you have tell Rice about it.
This is because VALUE is a typedef for unsigned long long
- under the hood it is really a pointer to a Ruby object. However, to Rice it is just an integer that needs to be converted to a Ruby numeric value. As a result, if a method takes a VALUE parameter then Rice will convert it to a C++ unsigned long long value instead of passing it through. Similarly, if a method returns a VALUE then Rice will also convert it to a numeric Ruby object as opposed to simply returning it.
To avoid this incorrect conversion, use the setValue()
method on the Arg
and Return
classes. For example:
VALUE some_function(VALUE ary)
{
VALUE new_ary = rb_ary_dup(ary);
rb_ary_push(new_ary, Qtrue);
return new_ary;
}
define_global_function("some_function", &some_function, Arg("ary").setValue(), Return.setValue());
Note that you can mix Arg
and Return
objects in any order. For example this also works:
define_global_function("some_function", &some_function, Return.setValue(), Arg("ary").setValue());
Return Self#
In the case of methods that return self
- meaning they return back the same C++ object that was the receiver of the function call - Rice ensures that the same Ruby object is returned. Returning self is a common pattern in Ruby.
For example:
a = Array.new
a << 1 << 2 << 3 << 4
The above code works because the <<
method returns the Array a
. You can mimic this behavior by the use of lambdas when wrapping C++ classes. For example, Rice wraps std::vector
like this:
define_vector<std::vector<int32_t>>().
define_method("<<", [](std::vector<int32_t>& self, int32_t value) -> std::vector<int32_t>& // <----- DONT MISS THIS
{
self.push_back(value);
return self; // <------ Allows chaining on calls
});
Pay careful attention to the lambda return type of std::vector<int32_t>&
. If the return type is not specified, then by default the lambda will return by value. That will invoke std::vector
’s copy constructor, resulting in two std::vector<int32_t>
instance and two Ruby objects. Not at all what you want.