顯示具有 ruby_binding 標籤的文章。 顯示所有文章
顯示具有 ruby_binding 標籤的文章。 顯示所有文章

2/19/2008

Rice - Ruby Interface for C++ Extensions

Rice - Ruby Interface for C++ Extensions

一年半前(天啊,有這麼早嗎?)曾經介紹過 swig, 一個專門產生各種介於 C/C++ 與其他語言 interface 的產生器。他是利用一個自訂的表示法,藉由讀取該表示法,產生出各種不同真正的 binding 程式。其中也有內建不少 STL 的 binding,所以想用 STL 的東西並不見得需要自己寫,只要叫他內部的東西出來即可。

swig 很厲害,不過他有個麻煩。雖然說 DSL 的威力強大,但是對於想要快速上手而言,其實有時候反而會是種阻礙。另一方面,我 swig 手冊翻了翻,要把 C++ port 到 ruby 很容易,但反之不亦然,要在 C++ 中使用 ruby object, 就不是那麼方便。(雖然也許這樣做是奇怪了點...)

所以後來我試了 Rice.
http://rice.rubyforge.org/
rice 的官方手冊也說了,rice 並不是要來取代 swig 的。swig 有他的不足,而 rice 大抵上又是模仿 boost.python 而作成的,所以兩者並不同。他自己也做過不少東西是同時使用 swig 和 rice. 不過也不能說 rice 是 ruby 版的 boost.python. 因為他的目的也不是完全模仿 boost.python.

anyway, 之所以會想試用,是出自於找不太到良好的 C++ yaml parser. 我有看到兩個 C 版本的實作,但是真不好意思啊,個人實在不太喜歡純 C 的東西...。所以想說如果可以把 ruby 的 yaml 搬過來就太好了。以下就是測試結果:

> sudo gem install rice

理論上這樣安裝是最方便的。要在 C++ 裡執行 ruby, 一樣會需要 ruby.h. 一般來說,他會在 lib/ruby/1.8/your_architecture/ 裡面。在我的電腦上,他是:/opt/local/lib/ruby/1.8/i686-darwin9.1.0/

而 rice 呢,則是在 lib/ruby/gems/1.8/gems/rice-x.y.z/, librice.a 則是在 lib/ruby/gems/1.8/gems/rice-x.y.z/rice/ 下。

所以在我的電腦裡,g++ options 是這樣下:

-I/opt/local/lib/ruby/1.8/i686-darwin9.1.0/ -I/opt/local/lib/ruby/gems/1.8/gems/rice-1.0.1/ -L/opt/local/lib/ruby/gems/1.8/gems/rice-1.0.1/rice/ -lrice -lruby -std=c++98 -Wall -w

主程式大概是長這樣:

int main(){
using Rice::Hash;
using Rice::protect;

ruby_init(); // 使用 ruby 前一定要呼叫
// 設定 load path, 否則 load path 會是 []
rb_eval_string("$LOAD_PATH << '/opt/local/lib/ruby/1.8'");
rb_eval_string("$LOAD_PATH << '/opt/local/lib/ruby/1.8/i686-darwin9.1.0'");

// require yaml 進來。當然也可以用上面的方式 require. 不過之所以會這樣寫,
// 是因為我不知道 load path 要怎麼直接從 rb_ function 中設定?
// 否則我是覺得能用 rb_ 去跑盡量用,evil eval 不是叫假的...
rb_require("yaml");

// protect 我猜是把所有的錯誤都轉成 rice 本身的 exception.
// Hash, 則是 ruby 的 hash 在 C++ 裡的 wrapper,
// 所以我是把 YAML 的讀取結果存入這份 C++ Hash 中。
Hash h(protect(rb_eval_string, "YAML.load(File.read('database.yml'))"));

// 這邊,我要做的事只是展現如何使用這份 hash.
Extractor e;
e.extract(h);
std::cout << std::endl;
}


雖然我覺得 doxygen 生出來的東西常常很難閱讀,不過 rice 的 doxygen 文件還算不錯,有什麼東西都很清楚。就算不夠清楚,也能去直接看他的原始檔。他原始檔的東西並不多,稍微翻一下,有什麼疑惑我想都可以解決。根據我 C++ 的經驗,rice 這份程式也算是寫得非常漂亮的了,應該滿有參考價值。

我的 extractor 是把整個 hash 都走過一次,如果不需要這麼複雜的操作,其實也可以很單純地這樣呼叫:

std::cout << static_cast<Hash>(h[String("development")])[String("adapter")];

這樣會輸出:

sqlite1

這樣實在是有點囉唆沒錯,不過我想這可以靠擴充 rice 解決。他有個 from_ruby 和 to_ruby 的 template, 擴充那個東西,好像就能把很多東西從 explicit 法轉成 implicit 法。不過我暫時懶得去做那麼多研究,這應該都是小問題。不過 down cast 就比較麻煩了。他有個 get method, 好像是能做一些 down cast, 但我測試都會有 runtime error, 大概是用法不對吧。有興趣的人歡迎去研究看看要怎麼做。

extractor 我想就不解釋了,就只是單純把抓出的 yaml 再輸出回 yaml. 其實那都已經差不多單純是 C++ 的問題了。僅列出程式碼與附註的一些註解:(不過我沒測試過比較複雜的 yaml, 我想一定會有問題,當作業自己試著改好吧 :p)

#include <ruby.h>
#include <rice/Hash.hpp>
#include <rice/Array.hpp>
#include <iostream>

using Rice::Object;
using Rice::Class;
using Rice::Hash;
using Rice::Array;
using Rice::String;

// 排版算空格用的
std::string spacer(int depth){
std::string result;
for(int i=0; i<depth; ++i)
result += " ";
return result;
}

class Extractor{
public:
// 他 class 判斷法有點麻煩,所以我先把這三個 class instance cache 起來
Extractor(): hash_class_(Hash().class_of()),
array_class_(Array().class_of()),
string_class_(String().class_of())
{}
void extract(Object const& obj, int depth = 0) const{
if(obj.is_instance_of(hash_class_))
extract_hash(obj, depth);
else if(obj.is_instance_of(array_class_))
extract_array(obj, depth);
else if(obj.is_instance_of(string_class_))
std::cout << spacer(depth) << obj << "\n";
else // 這表示他是 Fixnum or Float?
std::cout << spacer(depth) << obj << "\n";
}
private:
void extract_array(Array const& obj, int depth) const{
for(Array::const_iterator i=obj.begin(), iend=obj.end(); i!=iend; ++i)
extract(*i, depth);
}
void extract_hash(Hash const& obj, int depth) const{
for(Hash::const_iterator i=obj.begin(), iend=obj.end(); i!=iend; ++i){
std::cout << spacer(depth) << i->key << ":";

// i->value 結果會是 ruby 上 C 的 VALUE, 所以要 cast 成 Rice::Object
if(static_cast<Object>(i->value).is_instance_of(hash_class_))
std::cout << "\n", extract(i->value, depth+1);
else // 單純的值
std::cout << " ", extract(i->value, 0);
}
}
private:
Class hash_class_, array_class_, string_class_;
};

3/18/2007

SWIG/Ruby

以下文章最早發表於 2006.08.06, ptt Ruby 板。由於這裡的版面跟 BBS 有極大的差異,所以稍微調整了一下排版。不過程式碼的部份可能很難調整到方便閱讀,這點也就請見諒了。

http://www.swig.org/

SWIG is an interface compiler that connects programs written in C and C++ with scripting languages such as Perl, Python, Ruby, and Tcl.

也就是,讓 C/C++ 與 scripting language 溝通的介面產生器。目前支援 13 種程式語言(含非 scripting language):

* Allegro Common Lisp
* C#
* Chicken
* Guile
* Java
* Modula-3
* Mzscheme
* OCAML
* Perl
* PHP
* Python
* Ruby
* Tcl

之前我測了一些 Ruby 與 C++ 的溝通方式,以下將簡單介紹一下測試的方法。首先先看到使用方式,我想這個應該是最重要的部份,畢竟如果使用不便,那其他的也不用再多說什麼了…。




以下這個程式是一個簡單的 type wrapper,(傳值版)由 template parameter 指定型別,data() 當 getter, data(type) 當 setter. 另外在 c'tor 和 d'tor 中插入 IO 來追蹤物件的生成與摧毀:
// in Wrapper.h

#include <iostream>

template <class Data>
class Wrapper{
public:
Wrapper(Data new_data): data_(new_data){
std::cout << "C++: Wrapper is created, which is '"
<< data_ << "' .\n";
}
~Wrapper(){
std::cout << "C++: Wrapper is decayed, which is '"
<< data_ << "' .\n";
}
Data data() const{ return data_; }
void data(Data new_data){ data_ = new_data; }
private:
Data data_;
};

有了這個 .h 檔的介面後(這裡同時也包含實作),接下來我們需要的是一個由 swig 產生的膠水,將 C++ 介面與 Ruby 介面混合,暫時命名為:Wrapper.i, 詳細做法等一下我們再來看。總之最終我們將產生一個動態連結檔,也就是一個 Ruby 的 module, 叫 Wrapper. 不過需要一提的是,由於 Ruby 本身不支援 template, 所以我們在 Wrapper.i 中必須明確指出我們需要什麼型別,否則 Ruby 會無法使用。(更明確來講,純 template 根本無法 compile)

所以在 Wrapper.i 中,裡面有一行是這個:

%template(Integer) Wrapper<int>;

這句話的意思是,將 Wrapper<int> 這個型別取作 Integer. 於是 Ruby module 做好後,可以使用 Integer 這個型別。




雖然我中間還有很多測試,不過直接看目前的最後結果吧。寫一個 Ruby 程式,內容如下:
#!/usr/bin/ruby
# in test.rb

require 'Wrapper' # 讀入剛剛做好的 Wrapper module

class Test < Wrapper::Integer # 繼承 C++ 寫好的 Wrapper<int>

def initialize(new_data) # c'tor 其實可以省略,因為可以使用
super(new_data) # C++ 中寫好的 Wrapper
end # 這裡只是展示 super 是可以用的

def plus(num)
data(data + num)
data
end

end

# 正式測試開始

fat = Test.new(123) # 建構出 Test 物件,傳入 123 當引數
puts fat.plus(7) # output: 130

fat.data(456) # 設值為 456
puts fat.plus(4) # output: 460

ok, 可是原本插入 C++ 的 c'tor 中的 IO 呢?整個程式的輸出結果是這樣:

C++: Wrapper is created, which is '123' .
130
460
C++: Wrapper is decayed, which is '460' .

也就是說,C++ 的 c'tor 與 d'tor 有被確實喚起。這樣說不定可以替 Ruby 實作出 d'tor...不過這是題外話。

另外,剛剛程式中的 fat.data 與 fat.data(456) 確實是喚起正確的 getter 與 setter, 可是 Ruby 沒有 overload? 我沒有實際去看,但我猜他內部大概是這樣實作的:
def data(*args)
case args.size
when 0 then return self.data_a
when 1 then return self.data_b(args[0])
end
end

所以如果你想要重新定義其中一個 overloaded method, 做不到…可能必須找到他實際的名字才有辦法。




那麼我們再來看到由 C++ 呼喚 Ruby... 否則不就太寂寞了嗎?基本上 Ruby 本身就提供了良好與 C 溝通的機制,照用不就好了?是這樣說沒錯,可是說真的我很討厭 C 的介面…囧rz
常常會要你記下一堆有的沒的,感覺很討厭。所以我覺得需要一個 C++ 的 wrapper, 一個可以輕鬆呼叫 Ruby 的方式。後來我找到這個:

http://www.sourcepole.com/sources/software/c++ruby/

不過這個東西實在是寫得不好,沒有處理 finalize 的部份。不知道還有沒有其他類似的 wrapper 可以用,所以我就暫時用這個來改。改寫他 singleton 的實作,還有記得在 d'tor 中呼叫 ruby_finalize();

最後的結果是,我可以這樣寫:
#include "rubyeval.h" // 就是上面抓來的那個
#include <ruby.h> // embedded ruby 必要的東西,去 ruby-lang 就可以抓

int main(){

RubyEval& ruby = RubyEval::instance();
ruby.eval("require 'Wrapper'"); // 直接用字串執行 Ruby
// 這邊是含入剛剛做好的 C++ => Ruby mod
// 當然這東西要先 compile 好

ruby.eval("fat = Wrapper::Integer.new(123)"); // 直接建立物件
std::cout << NUM2INT(ruby.eval("fat.data")) << std::endl; // 輸出 123

ruby.eval("fat.data(456);"); // 設值 456
std::cout << NUM2INT(ruby.eval("fat.data")) << std::endl; // 輸出 456

ruby.run_file("test.rb"); // 直接執行 ruby 程式
}

那個 test.rb, 就是上面寫好拿來測試 Ruby 呼叫 C++ 的程式。於是整個輸出結果就是:

C++: Wrapper is created, which is '123' .
123
456
C++: Wrapper is created, which is '123' .
130
460
C++: Wrapper is decayed, which is '460' .
C++: Wrapper is decayed, which is '456' .

第一行是從 C++ 建構的 Integer; 123, 456 則是 cout 輸出的。第四行的 123 則是 test.rb 產生 class Test < Wrapper::Integer 那個。130, 460 則是 Ruby 的 puts 產生出來的。

最後兩個 decayed 則是 Ruby 的 gc 正確在程式結束時摧毀物件輸出的順序剛好跟 c'tor 反過來,一切正常。




最後就來看怎麼實現這個的。Wrapper.i 是這樣寫的:

%module Wrapper

%{
#include "Wrapper.h"
%}

%include "Wrapper.h"
%template(Integer) Wrapper<int>;

這些語法請參考 SWIG 的網站,那邊都有詳細說明。甚至是 C++ 的多重繼承,在 Ruby 中也能使用。當然多少可能會有點限制,但似乎可以實現一定的功能。

下指令:
swig -c++ -ruby Wrapper.i
這樣就可以以 Wrapper.i 這個介面檔實作出由 Ruby 溝通 C++ 的程式。那個程式會叫做 Wrapper_wrap.cxx, 也就是 YOUR_NAME_wrap.cxx

再來就是將所有的程式打包成一個動態連結檔了,所有的檔案是:Wrapper.h (包含實作), Wrapper_wrap.cxx (SWIG 的膠水)。方便的做法是使用 Ruby 的 lib, 叫 mkmf
寫一個 Ruby 程式叫 mkmf.rb, 內容是:

require 'mkmf'
$libs = append_library($libs, "stdc++")
create_makefile(ARGV[0])

這邊是由於我個人方便,所以這樣寫的。第一行含入 mkmf, 第二行是因為我用到 C++ 標準函數庫(cout),所以必須連結 stdc++ 才行(我的系統是 GCC, VC++ 的話我不清楚)接著由 create_makefile 產生出我要的 makefile, 名稱由 cmd line 輸入。這邊我是輸入 Wrapper.(btw, 有人知道 stdc++ 可否動態連結嗎?)

接著就可以由 make 將 Wrapper.h 和 Wrapper_wrap.cxx 合併做出 Wrapper.so(我想 VC++ 系統應該會做出 Wrapper.dll 之類的)




最後要提的是,整個程式的執行環境。由於我是在 cygwin 下作業的,所以獨立執行這些程式需要的是:

cygwin1.dll 1.78 MB
cygruby18.dll 703 KB
cygcrypt-0.dll 6.5 KB

我想如果在 windows 下由 VC++ compile 的話,應該就只會需要 Ruby interpreter 的 dll 檔(這裡是 cygruby18.dll)。不過我就沒有做這一步的測試了,留給讀者當作練習吧 XD

(忽然心血來潮所以整理了這些東西)

2006.08.06 godfat 真常

延伸閱讀

3/16/2007

why scripting?

why scripting?

簡單一句話就是:「為了增進生產力」。不過這樣回答的話,就會衍生出另外一個問題:
「為什麼要增進生產力?」當然不是這個問題,這種問題根本沒有回答的必要。
這個問題是:「如何增進生產力?」

我們知道有一些狀況下,必須不斷修改程式,不管是因為現階段不知道最好的解決方式
是什麼,或是現況真的是會不斷改變。總而言之,我們必須「嘗試」而後「修改」。
這種時候,我們可以用需要 compile 而後 link 的程式語言(或是工作環境)嗎?

這篇影片道出了一些 "why": Better Web App Development
可以不用看沒關係,這邊會稍微提一下大概內容。簡單地說,GUI 的部份是最明顯的。
因為我們不知道我們的 user 會比較想要哪一種操作模式,甚至,連他們自己也不清楚。
這種事說來很不可思議,但實際上就是如此。很多事情是很難預測的。或是該說,
要正確地預測事情的成本,遠遠高於直接試試看,再從中獲取經驗,進而修改以符合
我們的希望。所以我們需要的是「能夠快速修改、變化」的程式語言(或是工作環境)。

scripting 就是為此而誕生的。他們的立意和 system programming 有著根本上的不同,
所以無須拿此二者來比較。這一篇並不是想探討此二者間的異同,所以不會再對此著墨。
這裡直接假設兩者同等重要,以此討論兩者之間該如何取得平衡。

*

事實上,越是前端的程式,越是需要此種「變化」的特性。因為 system programming
是面對機器,而機器可不會跟你鬧脾氣。(如果不明原因的當機不算的話…)
但是 scripting 通常是面對人,而人可是可以陰晴不定,今天好好的明天就跟你翻臉,
或是嘴巴說不要,身體倒是挺誠實的,諸如此類的問題可多著。
為了「適應」人的這種「特性」,我們也需要一個擅長「變化」的東西,
以此銜接 system, application, 與人類。

除此之外,大家也都知道 scripting 的執行效率一定遠遠不如 binary program,
在大部份的情況下,這不是個問題。因為現在的電腦已經快到一種很恐怖的境界了,
真的非常需要執行效能的狀況,其實很少。更何況大家都知道 80-20(1) 法則,效能的
重要性只會隨著時間越來越減少,但永遠不會消失,因為人的慾望是無窮的…。

*

舉遊戲程式為例。一般比較大型的遊戲程式,大抵上都會切成三個部份。第一個是
廣為人知的 engine, 這邊的執行效能是非常 critical 的。第二部份是遊戲主程式,
這部份一般而言,效能也是非常重要的,所以還是會以 C++ 來完成。但是有些東西,
卻不得不引入 scripting, 儘管執行效能可能會是個問題,但還是不得不引入。
這就是第三個部份,掌控一些最細微的遊戲設定,也是跟「人」最有關係的部份。

為何這部份需要引入 scripting? 最簡單的例子就是平衡度設定。Dragon 是否太強?
Drawf 是否太弱?Golem 需不需要再多加個 Trample 的能力?Ranger 的 favored enemy
是否該換成 Goblin? 諸如此類,不是一開始就能確定,你一定得「玩一玩」才有可能
知道的麻煩事。甚至,必須讓成千成萬個玩家去玩過,才會知道的,很隱晦的事。
在複雜系統之下,這種碰過才知道的事,是非常多的。但是我們可不能稍微修改一下,
重新 compile, link, 泡杯咖啡。接著測試兩分鐘,啊,不行,這個數字應該再減少一個
單位。重複上面發生的事。這樣別說效率很差了,耐心都被磨光了。有些時候,士氣其實
是遠遠重要於產出的程式,像這種極度消磨耐心的事,再怎麼樣都是不能出現的。

引入 scripting 的好處就在這裡,我們可以輕易地修改程式,接著直接重新啟動遊戲
即可。更有甚者,直接叫你的遊戲重讀 script 檔即可,連重新啟動遊戲都不必。
不過就像我上面說的,基本上遊戲是個幾乎每一處都很 critical 的程式,所以
scripting 也要越快越好。最常被選用的就是 Lua, 因為他真的很快。不過啊,個人是
不太喜歡使用 Lua, 因為他和 C++ 間的連接不是那麼地良好,怎麼說,有點詭異…。

第二個最常被使用的,可能是 Python. 這是個相當不錯的程式語言,boost 也有一整份
Python 的 binding library. 不過我個人還是比較偏好 Ruby 一點,Ruby 是目前為止
我看過我覺得最好的 scripting language, 原因很多,但主要可能是因為他對物件導向
的支援非常完善,而物件導向在遊戲裡,是個幾乎可以說不能沒有的東西。光 GUI 就
非常仰賴物件導向了,更何況 GUI 只是遊戲的一部分。這部份可以很複雜,也可以很
單純。但不管怎麼樣,物件導向都太重要了。

*

所以事實上,我是很希望遊戲程式可以引入 Ruby, 雖然 Ruby 的執行效能比較差,
但應該好用的東西就應該好用到底,應該跑得快的東西就應該快到底,我是這樣認為的。
Python 被用很多,但我相信 Ruby 也會越來越重要。最近也確實如雨後春筍一般,
四處都出現其應用,不是嗎? :p

boost 沒有提供 Ruby binding 是有點可惜,我想這是因為 Ruby 的熱門是最近幾年的
事。不過呢,SWIG 還是有提供 Ruby 的 binding! 而且也是相當完整的支援。
何不就來試試 C++ 透過 SWIG 與 Ruby 溝通的方式呢? :p
欲知詳情,請待下回分曉。

2007.03.16 godfat 真常

p.s. 為何遊戲一定要用 C++? 這…不在本篇討論範圍裡 :o

(1) 一個程式只有 20% 的部份會佔去 80% 的執行效率。或是各種類似說法。