How to expose C++ functions to Ruby Scripts
As we want to expose much functionality to the script side, we also want to be able to call C++ functions from Ruby. In order to do this, we intercept unknown function calls on the Ruby side. The parameters are converted on the C++ side. There we know the name of the function to call, the object to call them on, and the parameters. But, how do we actually call the correct C++ function. The answer lies in the class object. The class object will contain the necessary meta-data to reroute the function call to the correct C++ function. The class object defines the interface of the class to the script side. This is done in the same file as the class definition was performed (the additional CPP file). Let's go back to our Simple class from earlier HowTos:
#include <zeitgeist/leaf.h> class Simple : public zeitgeist::Leaf { public: Simple(); virtual ~Simple(); void DoSomething(); void PrintString(const std::string& s); void PrintInt(int i); void PrintFloat(float f); void PrintBool(bool b); }; DECLARE_CLASS(Simple);
Now, let's say we want to expose the DoSomething() method. simple_c.cpp would look like this:
#include "simple.h" using namespace zeitgeist; FUNCTION(doSomething) { if (in.size() == 0) { Simple *simple = static_cast<Simple*>(obj); simple->DoSomething(); } } void CLASS(Simple)::DefineClass() { DEFINE_BASECLASS(zeitgeist/Leaf); DEFINE_FUNCTION(doSomething); }
Yes, you guessed right ... more helper macros. Every function is declared using the FUNCTION()-macro. As a parameter it takes the name of the function. I like to always have Ruby-side functions start with a lower-case letter and C++-side functions with a capital letter. The function macro just declares a function, which takes two parameters: obj and in. obj is the object we are calling the function on (basically, the 'this' or 'self' pointer). in is an STL vector of input parameters. The vector holds boost::any types, which have to be cast back. Within the function, we check if the number of parameters match (in our case 0 parameters). We cast the obj pointer to the correct type and call the appropriate function. In DefineClass() we also have to define the function using the DEFINE_FUNCTION() macro. After this has been done (and the class object is registered with the Core), we can execute the following script-code:
mySimpleObj = new ('Simple', 'test');
mySimpleObj.doSomething();
This would then call the C++ member function DoSomething(). Now, how about passing some parameters. Let's marshall the PrintInt() function:
#include "simple.h" using namespace zeitgeist; using namespace boost; FUNCTION(printInt) { if (in.size() == 1) { Simple *simple = static_cast<Simple*>(obj); simple->PrintInt(any_cast<int>(in[0])); } }
The above function obviously also would need to be defined in DefineClass(). Now, we see that any_cast is used to retrieve the type (!) of the parameter stored in the vector 'in' at position 0 (the first parameter from left to right maps to position 0). It should be noted, that this way of marshalling is not safe, meaning that exceptions will occur if a wrong parameter has been issued script-side. So, please be careful when doing this ... and take care to pass correct parameters on the script side. The problem is, that Ruby is dynamically typed, whereas C++ is statically typed. Also, no types are coerced automatically. So, if you cast to an int and somebody uses a float in the script, this will likely cause an error!
Another note is that in the current implementation NO return parameters can be passed to the script side. This is unfortunate, but would have created yet another (major) source of typing errors. One is dangerous enough. Also, scripts should be kept linear to make them easier to read.