Ruby 最酷的功能之一就是使用 C/C++ 定義的API (API) 擴充它。Ruby 提供了 C 標頭檔 ruby.h,它隨附提供了許多功能,可使用這些功能建立 Ruby 類、模組和更多內容。除了標頭檔,Ruby 還提供了其他幾個高層抽象來擴充基於本地 ruby.h 構建的 Ruby,本文要介紹的是 Ruby Interface for C++ Extensions 或 Rice。
建立 Ruby 擴充
在進行任何 Ruby 的 C API 或 Rice 擴充前,我想明確地介紹一下建立擴充的標準流程:
- 您具有一個或多個 C/C++ 原始碼,可使用它們構建共用庫。
- 如果您使用 Rice 建立擴充,則需要將代碼連結到 libruby.a 和 librice.a。
- 將共用庫複製到同一檔案夾,並將該檔案夾作為 RUBYLIB 環境變數的一部分。
- 在 Interactive Ruby (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++ -c rubytest.cpp –g –Wall -I/usr/lib/ruby/1.8/include \ -I/usr/local/include/rice/includebash# g++ -shared –o rubytest.so rubytest.o -L/usr/lib/ruby/1.8/lib \ -L/usr/local/lib/rice/lib -lruby –lrice –ldl -lpthreadbash# cp rubytest.so /opt/testbash# export RUBYLIB=$RUBYLIB:/opt/testbash# irbirb> require 'rubytest'=> true
Hello World 程式
現在,您已經準備好使用 Rice 建立自己的首個 Hello World 程式。您使用名為 Test 的 Rice API 和名為 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'=> trueirb> 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 類添加方法
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 類的成員函數,該類是 Class 的基類。下面是 Ruby 測試,驗證所有內容是否都運行良好:
irb> require ‘rubytest'=> trueirb> Test.new.helloHello, 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,使用 Rice::Array 的方式類似於使用 Standard Template Library (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; } }
現在,下面是此解決方案的魅力所在:假設您擁有 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 helloArray has worldArray 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>。這正是 Rice 提供的內容,具有下列簽名的 from_ruby 函數:
template <typename T>T from_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> > (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 函數。當從 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> > (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 方法。下面是 Rice 資源中 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++ 代碼中,調用 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> > (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>
同樣地,要返回執行個體變數,返回的函數需要接收在 Ruby 中引用對象的 Object,並對它調用 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 類方法。您已經將引用傳遞給 Ruby 對象,方法是使用第一個參數 Object 聲明 C 函數。這種方法有用,但是在將 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.newTypeError: 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 2TypeError: 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 中,模組名稱和類名稱必須以大寫字母開頭。如果您將模組命名為 types 而不是 Types,Rice 不會出錯。
使用 C++ 代碼建立 Ruby 結構
您在 Ruby 中使用 struct 建構函式來快速建立樣本 Ruby 類。清單 17 顯示了使用名為 a、ab 和 aab 的三個變數建立類型 NewClass 的新類的方法。
清單 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 的等效編碼,您需要使用標頭檔 rice/Struct.hpp 中聲明的 define_struct( ) API。此 API 返回 Rice::Struct。您將此 struct 建立的 Ruby 類與該類所屬的模組關聯起來。這是 initialize 方法的目的。使用 define_member 函數調用定義各個類成員。注意,您已經建立了一個新的 Ruby 類型,可惜您沒有將任何 C++ 類型或函數與它關聯起來。下面是建立名為 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 實現所有這些操作,但是您需要編寫大量樣板代碼來結束所有操作。Rice 使這些工作變得更加簡單。在這裡,祝您使用 C++ 針對 Ruby 環境編寫新擴充愉快! world!