本科rice 的cs+
Ruby最酷的功能之一就是可以使用C/C++
定义的应用程序编程接口(API)对其进行扩展。 Ruby提供了C
头ruby.h,它附带了用于创建Ruby类,模块等的大量函数。 除了Ruby提供的标头外,还可以使用其他一些高级抽象来扩展基于本机ruby.h构建的Ruby。本文研究的对象是用于C++
扩展的Ruby接口,即Rice。 。
创建一个Ruby扩展
在进入任何Ruby的C
API或Rice扩展之前,我想清楚地描述创建扩展的标准过程:
- 您有一个或多个
C/C++
源,并在其中使用了一个共享库。 - 如果使用Rice创建扩展,则需要将代码链接到libruby.a和librice.a。
- 将共享库复制到某个文件夹,并将该文件夹作为RUBYLIB环境变量的一部分。
- 在Interactive Ruby(irb)提示/Ruby脚本中使用通常的基于
require
的加载。 如果共享库名为rubytest.so,则只需键入require 'rubytest'
加载共享库。
假设头文件ruby.h位于/usr/lib/ruby/1.8/include中,莱斯头文件位于/ usr / local / include / rice / include中,扩展代码位于文件rubytest.cpp中。 清单1显示了如何编译和加载代码。
清单1.编译和加载Ruby扩展
bash# g++ -c rubytest.cpp –g –Wall -I/usr/lib/ruby/1.8/include \
-I/usr/local/include/rice/include
bash# g++ -shared –o rubytest.so rubytest.o -L/usr/lib/ruby/1.8/lib \
-L/usr/local/lib/rice/lib -lruby –lrice –ldl -lpthread
bash# cp rubytest.so /opt/test
bash# export RUBYLIB=$RUBYLIB:/opt/test
bash# irb
irb> require 'rubytest'
=> true
Hello World程序
现在,您准备使用Rice创建您的第一个Hello World程序。 您可以使用Rice API创建一个名为Test的类,该类的方法hello
显示字符串“ Hello,World!”。 当Ruby解释器加载扩展时,它将调用函数Init_<shared library name>
。 对于清单1中的rubytest
扩展,此调用意味着rubytest.cpp具有定义的Init_rubytest
函数。 Rice使您可以使用API define_class
创建自己的类。 清单2显示了代码。
清单2.使用Rice API创建一个类
#include "rice/Class.hpp"
extern "C"
void Init_rubytest( ) {
Class tmp_ = define_class("Test");
}
在irb中编译并加载清单2中的代码时,应该获得清单3中的输出。
清单3.测试使用Rice创建的类
irb> require ‘rubytest’
=> true
irb> a = Test.new
=> #<Test:0x1084a3928>
irb> a.methods
=> ["inspect", "tap", "clone", "public_methods", "__send__",
"instance_variable_defined?", "equal?", "freeze", …]
请注意,可以使用几种预定义的类方法,例如inspect
。 发生这种情况是因为您定义的Test
类是从Object
类隐式派生的(每个Ruby类都是从Object
派生的;实际上,Ruby中的所有Object
(包括数字)都是一个以Object
为基类的Object
)。
现在,向Test
类添加一个方法。 清单4显示了代码。
清单4.向Test类添加方法
void hello() {
std::cout << "Hello World!";
}
extern "C"
void Init_rubytest() {
Class test_ = define_class("Test")
.define_method("hello", &hello);
}
清单4使用define_method
API向Test
类添加一个方法。 注意define_class
是一个返回Class
类型对象的函数; define_method
是Module_Impl
类的成员函数, Module_Impl
是Class
的基类。 这是Ruby测试,用于验证一切是否正常:
irb> require ‘rubytest’
=> true
irb> Test.new.hello
Hello, World!
=> nil
将参数从Ruby传递到C / C ++代码
现在,您的Hello World程序已启动,尝试将Ruby中的参数传递给hello
函数,并使该函数与标准输出(sdtout)显示相同。 最简单的方法是在hello
函数中添加一个字符串参数:
void hello(std::string args) {
std::cout << args << std::endl;
}
extern "C"
void Init_rubytest() {
Class test_ = define_class("Test")
.define_method("hello", &hello);
}
在Ruby世界中,这是调用hello
函数的方式:
irb> a = Test.new
<Test:0x0145e42112>
irb> a.hello "Hello World in Ruby"
Hello World in Ruby
=> nil
使用Rice最好的事情是,您无需执行任何特定操作即可将Ruby字符串转换为std::string
。
现在,尝试在hello
函数中使用字符串数组,然后检查如何将信息从Ruby传递到C++
代码。 最简单的方法是使用Rice提供的Array
数据类型。 在Rice / Array.hpp标头中定义的使用Rice::Array
类似于使用标准模板库(STL)容器。 常用的STL样式迭代器等定义为Array
接口的一部分。 清单5显示了count
例程,该例程以Rice Array
作为参数。
清单5.显示一个Ruby数组
#include "rice/Array.hpp"
void Array_Print (Array a) {
Array::iterator aI = a.begin();
Array::iterator aE = a.end();
while (aI != aE) {
std::cout << "Array has " << *aI << std::endl;
++aI;
}
}
现在,这里是解决方案的Array_Print
:假设您有一个std::vector<std::string>
作为Array_Print
参数。 这是Ruby引发的错误:
>> t = Test.new
=> #<Test:0x100494688>
>> t.Array_Print ["g", "ggh1", "hh1"]
ArgumentError: Unable to convert Array to std::vector<std::string,
std::allocator<std::string> >
from (irb):3:in `hello'
from (irb):3
但是,借助此处显示的Array_Print
例程,Rice负责从Ruby数组到C++ Array
类型的转换。 这是一个示例运行:
>> t = Test.new
=> #<Test:0x100494688>
>> t.Array_Print ["hello", "world", "ruby"]
Array has hello
Array has world
Array has ruby
=> nil
现在以另一种方式尝试一下,将数组从C++
传递到Ruby世界。 请注意,在Ruby中,数组元素可能不一定是同一类型。 清单6显示了代码。
清单6.将数组从C ++传递给Ruby
#include "rice/String.hpp"
#include "rice/Array.hpp"
using namespace rice;
Array return_array (Array a) {
Array tmp_;
tmp_.push(1);
tmp_.push(2.3);
tmp_.push(String("hello"));
return tmp_;
}
清单6清楚地表明,可以在C++
创建具有不同类型的Ruby数组。 这是Ruby中的测试代码:
>> x = t.return_array
=> [1, 2.3, "hello"]
>> x[0].class
=> Fixnum
>> x[1].class
=> Float
>> x[2].class
=> String
如果我没有灵活性来更改C ++参数列表怎么办?
更为常见的是,您会发现Ruby接口旨在将数据转换为您无法更改其签名的C++
函数。 例如,考虑需要将字符串数组从Ruby传递到C++
。 C++
函数签名如下所示:
void print_array(std::vector<std::string> args)
实际上,您在这里寻找的是某种from_ruby
函数,该函数接收一个Ruby数组并将其转换为std::vector<std::string>
。 这正是莱斯提供的—具有以下签名的from_ruby
函数:
template <typename T>
T from_ruby(Object );
对于需要转换为C++
类型的每种Ruby数据类型,都需要对from_ruby
例程进行模板专用from_ruby
。 例如,如果将Ruby数组传递给上面显示的process函数, 清单7显示了如何定义from_ruby
函数。
清单7.将ruby数组转换为std :: vector <std :: string>
template<>
std::vector<std::string> from_ruby< std::vector<std::string> > (Object o) {
Array a(o);
std::vector<std::string> v;
for(Array::iterator aI = a.begin(); aI != a.end(); ++aI)
v.push_back(((String)*aI).str());
return v;
}
请注意,无需显式调用from_ruby
函数。 当string
数组从Ruby世界作为函数参数传递时, from_ruby
将其转换为std::vector<std::string>
。 但是,清单7中的代码并不完美,并且您已经看到Ruby中的数组可以具有不同的类型。 相反,您已调用((String)*aI).str()
从Rice::String
获得std::string
。 ( str
是Rice::String
的方法:有关更多详细信息,请检查String.hpp。)如果要处理最普通的情况, 清单8显示了代码的外观。
清单8.将ruby数组转换为std :: vector <std :: string>(一般情况)
template<>
std::vector<std::string> from_ruby< std::vector<std::string> > (Object o) {
Array a(o);
std::vector<std::string> v;
for(Array::iterator aI = a.begin(); aI != a.end(); ++aI)
v.push_back(from_ruby<std::string> (*aI));
return v;
}
因为Ruby数组的每个元素也是String
类型的Ruby对象,并且您押注Rice定义了一个from_ruby
方法,用于将该类型转换为std::string
,所以无需执行其他操作。 如果不是这种情况,则需要提供from_ruby
方法进行转换。 以下是莱斯资源中to_from_ruby.ipp中的from_ruby
方法:
template<>
inline std::string from_ruby<std::string>(Rice::Object x) {
return Rice::String(x).str();
}
在Ruby世界中测试此代码。 首先传递所有字符串的数组,如清单9所示 。
清单9.验证from_ruby功能
>> t = Test.new
=> #<Test:0x10e71c5c8>
>> t.print_array ["aa", "bb"]
aa bb
=> nil
>> t.print_array ["aa", "bb", 111]
TypeError: wrong argument type Fixnum (expected String)
from (irb):4:in `print_array'
from (irb):4
如预期的那样,第一次调用print_array
进行得很好。 因为没有from_ruby
方法将Fixnum
转换为std::string
,所以第二次调用导致Ruby解释器抛出TypeError
。 有多种方法可以修复此错误,例如,在Ruby调用期间,仅将字符串作为数组的一部分(例如t.print_array["aa", "bb", 111.to_s]
)或在C++
内部t.print_array["aa", "bb", 111.to_s]
代码,调用Object.to_s
。 to_s
方法是Rice::Object
接口的一部分,并返回Rice::String
,它具有预定义的str
方法,该方法返回std::string
。 清单10使用C++
方法。
清单10.使用Object.to_s填充字符串向量
template<>
std::vector<std::string> from_ruby< std::vector<std::string> > (Object o) {
Array a(o);
std::vector<std::string> v;
for(Array::iterator aI = a.begin(); aI != a.end(); ++aI)
v.push_back(aI->to_s().str());
return v;
}
通常, 清单10中的代码将涉及更多,因为您需要处理用户定义的类的自定义字符串表示形式。
使用C ++创建带有变量的完整类
您已经了解了如何在C++
代码中创建Ruby类和相关函数。 对于更通用的类,您需要一种定义实例变量以及提供initialize
方法的方法。 要设置和获取Ruby对象的实例变量的值,请分别使用Rice::Object::iv_set
和Rice::Object::iv_get
方法。 清单11显示了代码。
清单11.在C ++中定义一个initialize方法
void init(Object self) {
self.iv_set("@intvar", 121);
self.iv_set("@stringvar", String("testing"));
}
Class cTest = define_class("Test").
define_method("initialize", &init);
当使用define_method
API将C++
函数声明为Ruby类方法时,可以选择将C++
函数的第一个参数声明为Object
,并且Ruby会使用对调用实例的引用来填充此Object
。 然后,在Object
上调用iv_set
来设置实例变量。 接口在Ruby世界中的外观如下:
>> require 'rubytest'
=> true
>> t = Test.new
=> #<Test:0x1010fe400 @stringvar="testing", @intvar=121>
同样,要返回实例变量,返回函数需要采用一个Object
,该对象引用Ruby中的对象并在其上调用iv_get
。 清单12显示了一个片段。
清单12.从Ruby对象检索值
void init(Object self) {
self.iv_set("@intvar", 121);
self.iv_set("@stringvar", String("testing"));
}
int getvalue(Object self) {
return self.iv_get("@intvar");
}
Class cTest = define_class("Test").
define_method("initialize", &init).
define_method("getint", &getvalue);
将C ++类转换为Ruby类型
到目前为止,您已经将自由函数(即非类方法)包装为Ruby类方法。 通过使用第一个参数Object
声明C
函数,您已经传递了对Ruby对象的引用。 这种方法有效,但是不足以将C++
类包装为Ruby对象。 要包装C++
类,您仍然使用define_class
方法,除了现在用C++
类类型“模板化”它。 清单13中的代码将C++
类包装为Ruby类型。
清单13.将C ++类包装为Ruby类型
class cppType {
public:
void print(String args) {
std::cout << args.str() << endl;
}
};
Class rb_cTest =
define_class<cppType>("Test")
.define_method("print", &cppType::print);
请注意,如所讨论的, define_class
是模板化的。 但是,这堂课并不是所有人都很好。 当您尝试实例化Test
类型的对象时,这是Ruby解释器的日志:
>> t = Test.new
TypeError: allocator undefined for Test
from (irb):3:in `new'
from (irb):3
刚才发生了什么? 好吧,您需要将构造函数显式绑定到Ruby类型。 (这是Rice的一些怪癖之一。)Rice为您提供了define_constructor
方法来关联C++
类型的构造函数。 您还需要包括标题Constructor.hpp。 请注意,即使您的代码中没有显式构造函数,也必须这样做。 清单14提供了示例代码。
清单14.将C ++构造函数与Ruby类型相关联
#include "rice/Constructor.hpp"
#include "rice/String.hpp"
class cppType {
public:
void print(String args) {
std::cout << args.str() << endl;
}
};
Class rb_cTest =
define_class<cppType>("Test")
.define_constructor(Constructor<cppType>())
.define_method("print", &cppType::print);
也可以使用define_constructor
方法将构造函数与参数列表相关联。 Rice的方法是将参数类型添加到模板列表中。 例如,如果cppType
具有接受整数的构造函数,则必须将define_constructor
调用为define_constructor(Constructor<cppType, int>())
。 需要注意的是:Ruby类型没有多个构造函数。 因此,如果您的C++
类型具有多个构造函数,并且都使用define_constructor
它们关联起来,那么在Ruby世界中,您可以使用源代码中最后一个define_constructor
定义的参数(或不使用参数)实例化该类型。 清单15解释了刚才讨论的所有内容。
清单15.将构造函数与参数关联
class cppType {
public:
cppType(int m) {
std::cout << m << std::endl;
}
cppType(Array a) {
std::cout << a.size() << std::endl;
}
void print(String args) {
std::cout << args.str() << endl;
}
};
Class rb_cTest =
define_class<cppType>("Test")
.define_constructor(Constructor<cppType, int>())
.define_constructor(Constructor<cppType, Array>())
.define_method("print", &cppType::print);
这是Ruby世界的日志。 请注意,最后关联的构造函数是Ruby可以理解的构造函数:
>> t = Test.new 2
TypeError: wrong argument type Fixnum (expected Array)
from (irb):2:in `initialize'
from (irb):2:in `new'
from (irb):2
>> t = Test.new [1, 2]
2
=> #<Test:0x10d52cf48>
定义新的Ruby类型作为模块的一部分
从C++
定义新的Ruby模块归结为对define_module
的调用。 要定义仅在所述模块中可用的类,请使用define_class_under
而不是通常的define_class
方法。 define_class_under
的第一个参数是模块对象。 在清单14中 ,如果要将cppType
定义为名为types
的Ruby模块的一部分, 清单16显示了如何实现。
清单16.将类型声明为模块的一部分
#include "rice/Constructor.hpp"
#include "rice/String.hpp"
class cppType {
public:
void print(String args) {
std::cout << args.str() << endl;
}
};
Module rb_cModule = define_module("Types");
Class rb_cTest =
define_class_under<cppType>(rb_cModule, "Test")
.define_constructor(Constructor<cppType>())
.define_method("print", &cppType::print);
这是您在Ruby中的用法:
>> include Types
=> Object
>> y = Types::Test.new [1, 1, 1]
3
=> #<Types::Test:0x1058efbd8>
请注意,在Ruby中,模块名称和类名称必须以大写字母开头。 如果您将模块类型而不是类型命名,Rice不会出错。
使用C ++代码创建Ruby结构
您可以在Ruby中使用struct
构造来快速创建样板Ruby类。 清单17显示了创建具有三个名为a
, ab
和aab
变量的NewClass
类型的新类的典型Ruby方法。
清单17.使用Ruby Struct创建一个新类
>> NewClass = Struct.new(:a, :ab, :aab)
=> NewClass
>> NewClass.class
=> Class
>> a = NewClass.new
=> #<struct NewClass a=nil, ab=nil, aab=nil>
>> a.a = 1
=> 1
>> a.ab = "test"
=> "test"
>> a.aab = 2.33
=> 2.33
>> a
=> #<struct NewClass a=1, ab="test", aab=2.33>
>> a.a.class
=> Fixnum
>> a.ab.class
=> String
>> a.aab.class
=> Float
要用C++
编写清单17中的等效C++
,您需要使用在标头rice / Struct.hpp中声明的define_struct( )
API。 该API返回Rice::Struct
。 您将关联此struct
创建的Ruby类以及该类将成为其一部分的模块。 这就是initialize
方法的用途。 单独的类成员是使用define_member
函数调用定义的。 请注意,除了尚未将任何C++
类型或函数与之关联之外,您已经创建了一个新的Ruby类型。 这是创建名为NewClass
的类的代码:
#include "rice/Struct.hpp"
…
Module rb1 = define_module("Types");
define_struct().
define_member("a").
define_member("ab").
define_member("aab").
initialize(rb1, "NewClass");
结论
本文涵盖了相当多的基础知识-使用C++
代码创建Ruby对象,将C
样式函数作为Ruby对象方法进行关联,在Ruby和C++
之间转换数据类型,创建实例变量,以及将C++
类包装为Ruby类型。 可以使用ruby.h标头和libruby
来完成所有这些工作,但是您必须做很多样板代码才能使所有末端工作。 赖斯使所有工作变得更加容易。 在此祝您在Ruby世界中用C++
编写新扩展充满乐趣!
翻译自: https://www.ibm.com/developerworks/opensource/library/os-extendruby/index.html
本科rice 的cs+