Ruby 最酷的功能之一就是使用 C/C++ 定義的應(yīng)用程序編程接口 (API) 擴(kuò)展它。Ruby 提供了 C 頭文件 ruby.h,它隨附提供了許多功能,可使用這些功能創(chuàng)建 Ruby 類、模塊和更多內(nèi)容。除了頭文件,Ruby 還提供了其他幾個(gè)高層抽象來擴(kuò)展基于本地 ruby.h 構(gòu)建的 Ruby,本文要介紹的是 Ruby Interface for C++ Extensions 或 Rice。
創(chuàng)建 Ruby 擴(kuò)展
在進(jìn)行任何 Ruby 的 C API 或 Rice 擴(kuò)展前,我想明確地介紹一下創(chuàng)建擴(kuò)展的標(biāo)準(zhǔn)過程:
- 您具有一個(gè)或多個(gè) C/C++ 源代碼,可使用它們構(gòu)建共享庫。
- 如果您使用 Rice 創(chuàng)建擴(kuò)展,則需要將代碼鏈接到 libruby.a 和 librice.a。
- 將共享庫復(fù)制到同一文件夾,并將該文件夾作為 RUBYLIB 環(huán)境變量的一部分。
- 在 Interactive Ruby (irb) prompt/ruby 腳本中使用常見的基于 require 的加載。如果共享庫名為 rubytest.so,只需鍵入 require 'rubytest' 即可加載共享庫。
假設(shè)頭文件 ruby.h 位于 /usr/lib/ruby/1.8/include 中,Rice 頭文件位于 /usr/local/include/rice/include 中,并且擴(kuò)展代碼位于文件 rubytest.cpp 中。 清單 1 顯示了如何編譯和加載代碼。
清單 1. 編譯和加載 Ruby 擴(kuò)展
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 程序
現(xiàn)在,您已經(jīng)準(zhǔn)備好使用 Rice 創(chuàng)建自己的首個(gè) Hello World 程序。您使用名為 Test 的 Rice API 和名為 hello 的方法創(chuàng)建了一個(gè)類,用它來顯示字符串 "Hello, World!"。當(dāng) Ruby 解釋器加載擴(kuò)展時(shí),會(huì)調(diào)用函數(shù) Init_shared library name>。對(duì)于 清單 1 的 rubytest 擴(kuò)展,此調(diào)用意味著 rubytest.cpp 已定義了函數(shù) Init_rubytest。Rice 支持您使用 API define_class 創(chuàng)建自己的類。清單 2 顯示了相關(guān)代碼。
清單 2. 使用 Rice API 創(chuàng)建類
#include "rice/Class.hpp"
extern "C"
void Init_rubytest( ) {
Class tmp_ = define_class("Test");
}
當(dāng)您在 irb 中編譯和加載清單 2 的代碼時(shí),應(yīng)得到 清單 3 所示的輸出。
清單 3. 測試使用 Rice 創(chuàng)建的類
irb> require ‘rubytest'
=> true
irb> a = Test.new
=> #Test:0x1084a3928>
irb> a.methods
=> ["inspect", "tap", "clone", "public_methods", "__send__",
"instance_variable_defined?", "equal?", "freeze", …]
注意,有幾個(gè)預(yù)定義的類方法可供使用,比如 inspect。出現(xiàn)這種情況是因?yàn)?,定義的 Test 類隱式地衍生自 Object 類(每個(gè) Ruby 類都衍生自 Object;實(shí)際上,Ruby 中的所有內(nèi)容(包括數(shù)字)都是基類為 Object 的對(duì)象)。
現(xiàn)在,為 Test 類添加一個(gè)方法。清單 4 顯示了相關(guān)代碼。
清單 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 是返回一個(gè)類型為 Class 的對(duì)象的函數(shù);define_method 是 Module_Impl 類的成員函數(shù),該類是 Class 的基類。下面是 Ruby 測試,驗(yàn)證所有內(nèi)容是否都運(yùn)行良好:
irb> require ‘rubytest'
=> true
irb> Test.new.hello
Hello, World!
=> nil
將參數(shù)從 Ruby 傳遞到 C/C++ 代碼
現(xiàn)在,Hello World 程序已正常運(yùn)行,嘗試將參數(shù)從 Ruby 傳遞到 hello 函數(shù),并讓函數(shù)顯示與標(biāo)準(zhǔn)輸出 (sdtout) 相同的輸出。最簡單的方法是為 hello 函數(shù)添加一個(gè)字符串參數(shù):
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 環(huán)境中,以下是調(diào)用 hello 函數(shù)的方式:
irb> a = Test.new
Test:0x0145e42112>
irb> a.hello "Hello World in Ruby"
Hello World in Ruby
=> nil
使用 Rice 最出色的一點(diǎn)是,無需進(jìn)行任何特殊操作將 Ruby 字符串轉(zhuǎn)換為 std::string。
現(xiàn)在,嘗試在 hello 函數(shù)中使用字符串?dāng)?shù)組,然后檢查如何將信息從 Ruby 傳遞到 C++ 代碼。最簡單的方式是使用 Rice 提供的 Array 數(shù)據(jù)類型。在頭文件 rice/Array.hpp 中定義 Rice::Array,使用 Rice::Array 的方式類似于使用 Standard Template Library (STL) 容器。還要將常見的 STL 樣式迭代器等內(nèi)容定義為 Array 接口的一部分。清單 5 顯示了 count 例程,該例程使用 Rice Array 作為參數(shù)。
清單 5. 顯示 Ruby 數(shù)組
#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;
}
}
現(xiàn)在,下面是此解決方案的魅力所在:假設(shè)您擁有 std::vectorstd::string> 作為 Array_Print 參數(shù)。下面是 Ruby 拋出的錯(cuò)誤:
>> t = Test.new
=> #Test:0x100494688>
>> t.Array_Print ["g", "ggh1", "hh1"]
ArgumentError: Unable to convert Array to std::vectorstd::string,
std::allocatorstd::string> >
from (irb):3:in `hello'
from (irb):3
但是,使用此處顯示的 Array_Print 例程,Rice 負(fù)責(zé)執(zhí)行從 Ruby 數(shù)組到 C++ Array 類型的轉(zhuǎn)換。下面是樣例輸出:
>> t = Test.new
=> #Test:0x100494688>
>> t.Array_Print ["hello", "world", "ruby"]
Array has hello
Array has world
Array has ruby
=> nil
現(xiàn)在,嘗試相反的過程,將 C++ 的數(shù)組傳遞到 Ruby 環(huán)境。請(qǐng)注意,在 Ruby 中,數(shù)組元素不一定是同一類型的。清單 6 顯示了相關(guān)代碼。
清單 6. 將數(shù)組從 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++ 中創(chuàng)建具有不同類型的 Ruby 數(shù)組。下面是 Ruby 中的測試代碼:
>> x = t.return_array
=> [1, 2.3, "hello"]
>> x[0].class
=> Fixnum
>> x[1].class
=> Float
>> x[2].class
=> String
如果我沒有更改 C++ 參數(shù)列表的靈活性,會(huì)怎么樣?
更常見的情況是具有這樣的靈活性,您將發(fā)現(xiàn) Ruby 接口旨在將數(shù)據(jù)轉(zhuǎn)換為 C++ 函數(shù),該函數(shù)的簽名無法更改。例如,考慮需要將字符串?dāng)?shù)組從 Ruby 傳遞到 C++ 的情形。C++ 函數(shù)簽名如下所示:
void print_array(std::vectorstd::string> args)
實(shí)際上,您在這里尋找的是某種 from_ruby 函數(shù),Ruby 數(shù)組使用該函數(shù)并將它轉(zhuǎn)換為 std::vectorstd::string>。這正是 Rice 提供的內(nèi)容,具有下列簽名的 from_ruby 函數(shù):
template typename T>
T from_ruby(Object );
對(duì)于需要轉(zhuǎn)換為 C++ 類型的每種 Ruby 數(shù)據(jù)類型,需要針對(duì)模板詳細(xì)說明 from_ruby 例程。例如,如果將 Ruby 數(shù)組傳遞到上述處理函數(shù),清單 7 顯示了應(yīng)如何定義 from_ruby 函數(shù)。
清單 7. 將 ruby 數(shù)組轉(zhuǎn)換為 std::vectorstd::string>
template>
std::vectorstd::string> from_ruby std::vectorstd::string> > (Object o) {
Array a(o);
std::vectorstd::string> v;
for(Array::iterator aI = a.begin(); aI != a.end(); ++aI)
v.push_back(((String)*aI).str());
return v;
}
請(qǐng)注意,不需要顯式地調(diào)用 from_ruby 函數(shù)。當(dāng)從 Ruby 環(huán)境傳遞作為函數(shù)參數(shù)的 string 數(shù)組時(shí),from_ruby 將它轉(zhuǎn)換為 std::vectorstd::string>。清單 7 中的代碼并不完美,但是您已經(jīng)看到,Ruby 中的數(shù)組具有不同類型。相反,您調(diào)用了 ((String)*aI).str(),以便從 Rice::String 獲得 std::string。(str 是 Rice::String 的一種方法:查看 String.hpp 以了解有關(guān)的更多信息。)如果您處理的是最常見的情形,清單 8 顯示了相關(guān)的代碼。
清單 8. 將 ruby 數(shù)組轉(zhuǎn)換為 std::vectorstd::string>(通用情況)
template>
std::vectorstd::string> from_ruby std::vectorstd::string> > (Object o) {
Array a(o);
std::vectorstd::string> v;
for(Array::iterator aI = a.begin(); aI != a.end(); ++aI)
v.push_back(from_rubystd::string> (*aI));
return v;
}
由于 Ruby 數(shù)組的每個(gè)元素仍然是類型為 String 的 Ruby 對(duì)象,因此可以假設(shè) Rice 已定義了 from_ruby 方法,將此類型轉(zhuǎn)換為 std::string,不需要進(jìn)行其他操作。如果情況并非如此,則需要為此轉(zhuǎn)換提供 from_ruby 方法。下面是 Rice 資源中 to_from_ruby.ipp 的 from_ruby 方法:
template>
inline std::string from_rubystd::string>(Rice::Object x) {
return Rice::String(x).str();
}
在 Ruby 環(huán)境中測試此代碼。首先傳遞所有字符串的數(shù)組,如 清單 9 所示。
清單 9. 驗(yàn)證 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
和預(yù)期一樣,首次調(diào)用 print_array 運(yùn)行正常。由于沒有 from_ruby 方法來將 Fixnum 轉(zhuǎn)換為 std::string,因此第二次調(diào)用時(shí),會(huì)導(dǎo)致 Ruby 解釋器拋出 TypeError。有幾種修復(fù)此錯(cuò)誤的方法:例如,在 Ruby 調(diào)用期間,僅將字符串作為數(shù)組的一部分(比如 t.print_array["aa", "bb", 111.to_s])來傳遞,或者是在 C++ 代碼中,調(diào)用 Object.to_s。to_s 方法是 Rice::Object 接口的一部分,它會(huì)返回 Rice::String,它還有一個(gè)返回 std::string 的預(yù)定義 str 方法。清單 10 使用了 C++ 方法。
清單 10. 使用 Object.to_s 填充字符串向量
template>
std::vectorstd::string> from_ruby std::vectorstd::string> > (Object o) {
Array a(o);
std::vectorstd::string> v;
for(Array::iterator aI = a.begin(); aI != a.end(); ++aI)
v.push_back(aI->to_s().str());
return v;
}
通常,清單 10 中的代碼更為重要,因?yàn)槟枰幚碛脩舳x的類的自定義字符串表示。
使用 C++ 創(chuàng)建一個(gè)具有變量的完整類
您已經(jīng)了解了在 C++ 代碼內(nèi)如何創(chuàng)建 Ruby 類和相關(guān)函數(shù)。對(duì)于更通用的類,需要一種定義實(shí)例變量的方法,并提供一個(gè) initialize 方法。要設(shè)置并獲得 Ruby 對(duì)象實(shí)例變量的值,可以使用 Rice::Object::iv_set 和 Rice::Object::iv_get 方法。清單 11 顯示了相關(guān)的代碼。
清單 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++ 函數(shù)聲明為 Ruby 類方法時(shí),可選擇將 C++ 函數(shù)的第一個(gè)參數(shù)聲明為 Object,并且 Ruby 會(huì)使用調(diào)用實(shí)例的引用來填充此 Object。然后,在 Object 上調(diào)用 iv_set 來設(shè)置實(shí)例變量。下面是接口在 Ruby 環(huán)境中的外觀:
>> require 'rubytest'
=> true
>> t = Test.new
=> #Test:0x1010fe400 @stringvar="testing", @intvar=121>
同樣地,要返回實(shí)例變量,返回的函數(shù)需要接收在 Ruby 中引用對(duì)象的 Object,并對(duì)它調(diào)用 iv_get。清單 12 顯示了相關(guān)的代碼片段。
清單 12. 從 Ruby 對(duì)象檢索值
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++ 類轉(zhuǎn)換為 Ruby 類型
迄今為止,您已經(jīng)將免費(fèi)的函數(shù)(非類方法)包裝為 Ruby 類方法。您已經(jīng)將引用傳遞給 Ruby 對(duì)象,方法是使用第一個(gè)參數(shù) Object 聲明 C 函數(shù)。這種方法有用,但是在將 C++ 類包裝為 Ruby 對(duì)象時(shí),這種方法不夠好用。要包裝 C++ 類,仍需要使用 define_class 方法,除非現(xiàn)在您使用 C++ 類類型對(duì)它進(jìn)行了 “模板化” 。清單 13 中的代碼將 C++ 類包裝為 Ruby 類型。
清單 13. 將 C++ 類包裝為 Ruby 類型
class cppType {
public:
void print(String args) {
std::cout args.str() endl;
}
};
Class rb_cTest =
define_classcppType>("Test")
.define_method("print", cppType::print);
注意,如前所述,對(duì) define_class 進(jìn)行了模板化。盡管這種方法并不是適合所有此類。下面是您試圖實(shí)例化類型 Test 的對(duì)象時(shí),Ruby 解釋器的記錄:
>> t = Test.new
TypeError: allocator undefined for Test
from (irb):3:in `new'
from (irb):3
剛剛發(fā)生了什么事?您需要將構(gòu)造函數(shù)顯式地綁定到 Ruby 類型。(這是 Rice 的怪異之處之一。)Rice 為您提供了 define_constructor 方法來關(guān)聯(lián) C++ 類型的構(gòu)造函數(shù)。您還需要包含頭文件 Constructor.hpp。注意,即使在您的代碼中沒有顯式構(gòu)造函數(shù),您也必須這樣做。清單 14 提供了示例代碼。
清單 14. 將 C++ 構(gòu)造函數(shù)與 Ruby 類型關(guān)聯(lián)起來
#include "rice/Constructor.hpp"
#include "rice/String.hpp"
class cppType {
public:
void print(String args) {
std::cout args.str() endl;
}
};
Class rb_cTest =
define_classcppType>("Test")
.define_constructor(ConstructorcppType>())
.define_method("print", cppType::print);
還可以將構(gòu)造函數(shù)與使用 define_constructor 方法的參數(shù)列表關(guān)聯(lián)起來。Rice 進(jìn)行此操作的方法是為模板列表添加參數(shù)類型。例如,如果 cppType 有一個(gè)接收整數(shù)的構(gòu)造函數(shù),那么您必須將 define_constructor 作為 define_constructor(ConstructorcppType, int>()) 進(jìn)行調(diào)用。關(guān)于此處的一條警告:Ruby 類型沒有多個(gè)構(gòu)造函數(shù)。因此,如果您有具有多個(gè)構(gòu)造函數(shù)的 C++ 類型,并使用 define_constructor 將它們關(guān)聯(lián)起來,那么從 Ruby 環(huán)境的角度講,您可以像源代碼最后一個(gè) define_constructor 定義的那樣,初始化具有(或沒有)參數(shù)的類型。清單 15 解釋了剛剛討論的所有內(nèi)容。
清單 15. 將構(gòu)造函數(shù)與參數(shù)關(guān)聯(lián)起來
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_classcppType>("Test")
.define_constructor(ConstructorcppType, int>())
.define_constructor(ConstructorcppType, Array>())
.define_method("print", cppType::print);
下面是來自 Ruby 環(huán)境的記錄。注意,最后關(guān)聯(lián)的構(gòu)造函數(shù)是 Ruby 理解的構(gòu)造函數(shù):
>> 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 模塊可歸結(jié)為調(diào)用 define_module。要定義僅作為所述模塊一部分的類,請(qǐng)使用 define_class_under 而不是常用的 define_class 方法。define_class_under 的第一個(gè)參數(shù)是模塊對(duì)象。根據(jù) 清單 14,如果您打算將 cppType 定義為名為 types 的 Ruby 模塊的一部分,清單 16 顯示了如何進(jìn)行此操作。
清單 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_undercppType>(rb_cModule, "Test")
.define_constructor(ConstructorcppType>())
.define_method("print", cppType::print);
下面是在 Ruby 中使用相同聲明的方法:
>> include Types
=> Object
>> y = Types::Test.new [1, 1, 1]
3
=> #Types::Test:0x1058efbd8>
注意,在 Ruby 中,模塊名稱和類名稱必須以大寫字母開頭。如果您將模塊命名為 types 而不是 Types,Rice 不會(huì)出錯(cuò)。
使用 C++ 代碼創(chuàng)建 Ruby 結(jié)構(gòu)
您在 Ruby 中使用 struct 構(gòu)造函數(shù)來快速創(chuàng)建樣本 Ruby 類。清單 17 顯示了使用名為 a、ab 和 aab 的三個(gè)變量創(chuàng)建類型 NewClass 的新類的方法。
清單 17. 使用 Ruby Struct 創(chuàng)建新類
>> 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++ 中進(jìn)行 清單 17 的等效編碼,您需要使用頭文件 rice/Struct.hpp 中聲明的 define_struct( ) API。此 API 返回 Rice::Struct。您將此 struct 創(chuàng)建的 Ruby 類與該類所屬的模塊關(guān)聯(lián)起來。這是 initialize 方法的目的。使用 define_member 函數(shù)調(diào)用定義各個(gè)類成員。注意,您已經(jīng)創(chuàng)建了一個(gè)新的 Ruby 類型,可惜您沒有將任何 C++ 類型或函數(shù)與它關(guān)聯(lián)起來。下面是創(chuàng)建名為 NewClass 的類的方法:
#include "rice/Struct.hpp"
…
Module rb1 = define_module("Types");
define_struct().
define_member("a").
define_member("ab").
define_member("aab").
initialize(rb1, "NewClass");
結(jié)束語
本文介紹了一些背景知識(shí):使用 C++ 代碼創(chuàng)建 Ruby 對(duì)象,將 C 樣式的函數(shù)作為 Ruby 對(duì)象方法進(jìn)行關(guān)聯(lián),在 Ruby 和 C++ 之間轉(zhuǎn)換數(shù)據(jù)類型,創(chuàng)建實(shí)例變量,以及將 C++ 類包裝為 Ruby 類型。您可以使用 ruby.h 頭文件和 libruby 實(shí)現(xiàn)所有這些操作,但是您需要編寫大量樣板代碼來結(jié)束所有操作。Rice 使這些工作變得更加簡單。在這里,祝您使用 C++ 針對(duì) Ruby 環(huán)境編寫新擴(kuò)展愉快! world!
您可能感興趣的文章:- Ruby on Rails下的圖像處理入門教程
- 優(yōu)化Ruby代碼使程序運(yùn)行速度提高的例子
- 詳解Ruby中范圍的概念