使用C++来编写Ruby程序扩展的教程
Ruby最酷的功能之一就是使用C/C++定义的应用程序编程接口(API)扩展它。Ruby提供了C头文件ruby.h,它随附提供了许多功能,可使用这些功能创建Ruby类、模块和更多内容。除了头文件,Ruby还提供了其他几个高层抽象来扩展基于本地ruby.h构建的Ruby,本文要介绍的是RubyInterfaceforC++Extensions或Rice。
创建Ruby扩展
在进行任何Ruby的CAPI或Rice扩展前,我想明确地介绍一下创建扩展的标准过程:
- 您具有一个或多个C/C++源代码,可使用它们构建共享库。
- 如果您使用Rice创建扩展,则需要将代码链接到libruby.a和librice.a。
- 将共享库复制到同一文件夹,并将该文件夹作为RUBYLIB环境变量的一部分。
- 在InteractiveRuby(irb)prompt/ruby脚本中使用常见的基于require的加载。如果共享库名为rubytest.so,只需键入require'rubytest'即可加载共享库。
假设头文件ruby.h位于/usr/lib/ruby/1.8/include中,Rice头文件位于/usr/local/include/rice/include中,并且扩展代码位于文件rubytest.cpp中。清单1显示了如何编译和加载代码。
清单1.编译和加载Ruby扩展
bash#g++-crubytest.cpp–g–Wall-I/usr/lib/ruby/1.8/include\ -I/usr/local/include/rice/include bash#g++-shared–orubytest.sorubytest.o-L/usr/lib/ruby/1.8/lib\ -L/usr/local/lib/rice/lib-lruby–lrice–ldl-lpthread bash#cprubytest.so/opt/test bash#exportRUBYLIB=$RUBYLIB:/opt/test bash#irb irb>require'rubytest' =>true
HelloWorld程序
现在,您已经准备好使用Rice创建自己的首个HelloWorld程序。您使用名为Test的RiceAPI和名为hello的方法创建了一个类,用它来显示字符串"Hello,World!"。当Ruby解释器加载扩展时,会调用函数Init_<sharedlibraryname>。对于清单1的rubytest扩展,此调用意味着rubytest.cpp已定义了函数Init_rubytest。Rice支持您使用APIdefine_class创建自己的类。清单2显示了相关代码。
清单2.使用RiceAPI创建类
#include"rice/Class.hpp" extern"C" voidInit_rubytest(){ Classtmp_=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的对象)。
现在,为Test类添加一个方法。清单4显示了相关代码。
清单4.为Test类添加方法
voidhello(){ std::cout<<"HelloWorld!"; } extern"C" voidInit_rubytest(){ Classtest_=define_class("Test") .define_method("hello",&hello); }
清单4使用define_methodAPI为Test类添加方法。注意,define_class是返回一个类型为Class的对象的函数;define_method是Module_Impl类的成员函数,该类是Class的基类。下面是Ruby测试,验证所有内容是否都运行良好:
irb>require‘rubytest' =>true irb>Test.new.hello Hello,World! =>nil
将参数从Ruby传递到C/C++代码
现在,HelloWorld程序已正常运行,尝试将参数从Ruby传递到hello函数,并让函数显示与标准输出(sdtout)相同的输出。最简单的方法是为hello函数添加一个字符串参数:
voidhello(std::stringargs){ std::cout<<args<<std::endl; } extern"C" voidInit_rubytest(){ Classtest_=define_class("Test") .define_method("hello",&hello); }
在Ruby环境中,以下是调用hello函数的方式:
irb>a=Test.new <Test:0x0145e42112> irb>a.hello"HelloWorldinRuby" HelloWorldinRuby =>nil
使用Rice最出色的一点是,无需进行任何特殊操作将Ruby字符串转换为std::string。
现在,尝试在hello函数中使用字符串数组,然后检查如何将信息从Ruby传递到C++代码。最简单的方式是使用Rice提供的Array数据类型。在头文件rice/Array.hpp中定义Rice::Array,使用Rice::Array的方式类似于使用StandardTemplateLibrary(STL)容器。还要将常见的STL样式迭代器等内容定义为Array接口的一部分。清单5显示了count例程,该例程使用RiceArray作为参数。
清单5.显示Ruby数组
#include"rice/Array.hpp" voidArray_Print(Arraya){ Array::iteratoraI=a.begin(); Array::iteratoraE=a.end(); while(aI!=aE){ std::cout<<"Arrayhas"<<*aI<<std::endl; ++aI; } }
现在,下面是此解决方案的魅力所在:假设您拥有std::vector<std::string>作为Array_Print参数。下面是Ruby抛出的错误:
>>t=Test.new =>#<Test:0x100494688> >>t.Array_Print["g","ggh1","hh1"] ArgumentError:UnabletoconvertArraytostd::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"] Arrayhashello Arrayhasworld Arrayhasruby =>nil
现在,尝试相反的过程,将C++的数组传递到Ruby环境。请注意,在Ruby中,数组元素不一定是同一类型的。清单6显示了相关代码。
清单6.将数组从C++传递到Ruby
#include"rice/String.hpp" #include"rice/Array.hpp" usingnamespacerice; Arrayreturn_array(Arraya){ Arraytmp_; tmp_.push(1); tmp_.push(2.3); tmp_.push(String("hello")); returntmp_; }
清单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++函数签名如下所示:
voidprint_array(std::vector<std::string>args)
实际上,您在这里寻找的是某种from_ruby函数,Ruby数组使用该函数并将它转换为std::vector<std::string>。这正是Rice提供的内容,具有下列签名的from_ruby函数:
template<typenameT> Tfrom_ruby(Object);
对于需要转换为C++类型的每种Ruby数据类型,需要针对模板详细说明from_ruby例程。例如,如果将Ruby数组传递到上述处理函数,清单7显示了应如何定义from_ruby函数。
清单7.将ruby数组转换为std::vector<std::string>
template<> std::vector<std::string>from_ruby<std::vector<std::string>>(Objecto){ Arraya(o); std::vector<std::string>v; for(Array::iteratoraI=a.begin();aI!=a.end();++aI) v.push_back(((String)*aI).str()); returnv; }
请注意,不需要显式地调用from_ruby函数。当从Ruby环境传递作为函数参数的string数组时,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>>(Objecto){ Arraya(o); std::vector<std::string>v; for(Array::iteratoraI=a.begin();aI!=a.end();++aI) v.push_back(from_ruby<std::string>(*aI)); returnv; }
由于Ruby数组的每个元素仍然是类型为String的Ruby对象,因此可以假设Rice已定义了from_ruby方法,将此类型转换为std::string,不需要进行其他操作。如果情况并非如此,则需要为此转换提供from_ruby方法。下面是Rice资源中to_from_ruby.ipp的from_ruby方法:
template<> inlinestd::stringfrom_ruby<std::string>(Rice::Objectx){ returnRice::String(x).str(); }
在Ruby环境中测试此代码。首先传递所有字符串的数组,如清单9所示。
清单9.验证from_ruby功能
>>t=Test.new =>#<Test:0x10e71c5c8> >>t.print_array["aa","bb"] aabb =>nil >>t.print_array["aa","bb",111] TypeError:wrongargumenttypeFixnum(expectedString) 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++代码中,调用Object.to_s。to_s方法是Rice::Object接口的一部分,它会返回Rice::String,它还有一个返回std::string的预定义str方法。清单10使用了C++方法。
清单10.使用Object.to_s填充字符串向量
template<> std::vector<std::string>from_ruby<std::vector<std::string>>(Objecto){ Arraya(o); std::vector<std::string>v; for(Array::iteratoraI=a.begin();aI!=a.end();++aI) v.push_back(aI->to_s().str()); returnv; }
通常,清单10中的代码更为重要,因为您需要处理用户定义的类的自定义字符串表示。
使用C++创建一个具有变量的完整类
您已经了解了在C++代码内如何创建Ruby类和相关函数。对于更通用的类,需要一种定义实例变量的方法,并提供一个initialize方法。要设置并获得Ruby对象实例变量的值,可以使用Rice::Object::iv_set和Rice::Object::iv_get方法。清单11显示了相关的代码。
清单11.在C++中定义initialize方法
voidinit(Objectself){ self.iv_set("@intvar",121); self.iv_set("@stringvar",String("testing")); } ClasscTest=define_class("Test"). define_method("initialize",&init);
使用define_methodAPI将C++函数声明为Ruby类方法时,可选择将C++函数的第一个参数声明为Object,并且Ruby会使用调用实例的引用来填充此Object。然后,在Object上调用iv_set来设置实例变量。下面是接口在Ruby环境中的外观:
>>require'rubytest' =>true >>t=Test.new =>#<Test:0x1010fe400@stringvar="testing",@intvar=121>
同样地,要返回实例变量,返回的函数需要接收在Ruby中引用对象的Object,并对它调用iv_get。清单12显示了相关的代码片段。
清单12.从Ruby对象检索值
voidinit(Objectself){ self.iv_set("@intvar",121); self.iv_set("@stringvar",String("testing")); } intgetvalue(Objectself){ returnself.iv_get("@intvar"); } ClasscTest=define_class("Test"). define_method("initialize",&init). define_method("getint",&getvalue);
将C++类转换为Ruby类型
迄今为止,您已经将免费的函数(非类方法)包装为Ruby类方法。您已经将引用传递给Ruby对象,方法是使用第一个参数Object声明C函数。这种方法有用,但是在将C++类包装为Ruby对象时,这种方法不够好用。要包装C++类,仍需要使用define_class方法,除非现在您使用C++类类型对它进行了“模板化”。清单13中的代码将C++类包装为Ruby类型。
清单13.将C++类包装为Ruby类型
classcppType{ public: voidprint(Stringargs){ std::cout<<args.str()<<endl; } }; Classrb_cTest= define_class<cppType>("Test") .define_method("print",&cppType::print);
注意,如前所述,对define_class进行了模板化。尽管这种方法并不是适合所有此类。下面是您试图实例化类型Test的对象时,Ruby解释器的记录:
>>t=Test.new TypeError:allocatorundefinedforTest 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" classcppType{ public: voidprint(Stringargs){ std::cout<<args.str()<<endl; } }; Classrb_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.将构造函数与参数关联起来
classcppType{ public: cppType(intm){ std::cout<<m<<std::endl; } cppType(Arraya){ std::cout<<a.size()<<std::endl; } voidprint(Stringargs){ std::cout<<args.str()<<endl; } }; Classrb_cTest= define_class<cppType>("Test") .define_constructor(Constructor<cppType,int>()) .define_constructor(Constructor<cppType,Array>()) .define_method("print",&cppType::print);
下面是来自Ruby环境的记录。注意,最后关联的构造函数是Ruby理解的构造函数:
>>t=Test.new2 TypeError:wrongargumenttypeFixnum(expectedArray) 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" classcppType{ public: voidprint(Stringargs){ std::cout<<args.str()<<endl; } }; Modulerb_cModule=define_module("Types"); Classrb_cTest= define_class_under<cppType>(rb_cModule,"Test") .define_constructor(Constructor<cppType>()) .define_method("print",&cppType::print);
下面是在Ruby中使用相同声明的方法:
>>includeTypes =>Object >>y=Types::Test.new[1,1,1] 3 =>#<Types::Test:0x1058efbd8>
注意,在Ruby中,模块名称和类名称必须以大写字母开头。如果您将模块命名为types而不是Types,Rice不会出错。
使用C++代码创建Ruby结构
您在Ruby中使用struct构造函数来快速创建样本Ruby类。清单17显示了使用名为a、ab和aab的三个变量创建类型NewClass的新类的方法。
清单17.使用RubyStruct创建新类
>>NewClass=Struct.new(:a,:ab,:aab) =>NewClass >>NewClass.class =>Class >>a=NewClass.new =>#<structNewClassa=nil,ab=nil,aab=nil> >>a.a=1 =>1 >>a.ab="test" =>"test" >>a.aab=2.33 =>2.33 >>a =>#<structNewClassa=1,ab="test",aab=2.33> >>a.a.class =>Fixnum >>a.ab.class =>String >>a.aab.class =>Float
要在C++中进行清单17的等效编码,您需要使用头文件rice/Struct.hpp中声明的define_struct()API。此API返回Rice::Struct。您将此struct创建的Ruby类与该类所属的模块关联起来。这是initialize方法的目的。使用define_member函数调用定义各个类成员。注意,您已经创建了一个新的Ruby类型,可惜您没有将任何C++类型或函数与它关联起来。下面是创建名为NewClass的类的方法:
#include"rice/Struct.hpp" … Modulerb1=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实现所有这些操作,但是您需要编写大量样板代码来结束所有操作。Rice使这些工作变得更加简单。在这里,祝您使用C++针对Ruby环境编写新扩展愉快!world!