2009-06-02
目次
りんご味Ruby 第48回 藤本 尚邦
引き続き、Ruby+RubyCocoaを使って「CoreGraphicsによるPDFの描画」を記述するための簡易言語(DSL)を作ります。今回で完成します。
■ CGPDFContext.file の実装
目標とする簡易言語のプログラム例(第42回または45回参照)のうち、まだ実装していないのは、クラスメソッド CGPDFContext.file のみとなりました。このメソッドは、ブロックで渡された描画コードを実行して、指定されたファイル名(パス名)にPDFファイルとして出力するというものです。さくっと実装してしまいましょう (注: 第45回では CGPDFContext.file の部分を PDFDrawer.fileとしていましたが、説明をシンプルにするために変更しています):
class CGPDFContext
def self.file(path, rect, &block)
url = OSX::NSURL.fileURLWithPath(path)
rect = OSX::NSRect.new(*rect)
context_ref = CGPDFContextCreateWithURL(url, rect, nil)
pdf = self.new(context_ref)
block.call(pdf)
end
end
◇ (補足) RubyCocoaとMacRubyのメモリ管理について
前々回(46回)に実装した CGPDFContext#page が、beginPage と endPage の組み合わせを1つにまとめるためのメソッドであるのに対して、CGPDFContext.file は、描画コンテキストリソースの確保と解放のためのAPIの組みである CGPDFContextCreateWithURL と CGRelease をひとつにまとめるという位置づけに近いかもしれません。
実際には、RubyCocoaでは、CGPDFContextCreateWithURL を使って確保した描画コンテキストは自動的に解放されるので、CGRelease(あるいはCFRelease)を明示的に呼び出す必要はありません。もう少し細かく説明すると、描画コンテキストを持つRuby オブジェクト(上記のコードの場合context_refが指すオブジェクト)が、Rubyのオブジェクトシステムによってガベージコレクションされるタイミングで、描画コンテキストは自動的に解放されます。ほっといても自動的に解放されるので、むしろ明示的に呼んではいけません。
このあたりは、明示的に呼ぶ必要がある(現時点での)MacRubyと少し違うところなので注意が必要です。この違いの背景を少し説明します。
RubyCocoaは、ガベージコレクション無効のObjective-Cを前提に、Objective-Cの参照カウンタ方式によるメモリ管理の規約を、RubyのガベージコレクタをベースにしてRubyCocoa自体が自動的に実行するように実装されています。その流れで、CoreFoundationフレームワークのようなC言語によるAPIに対応すべく拡張されたときにも、同様の自動処理がなされるように実装されました。
MacRubyは、ガベージコレクションが有効なObjective-Cを前提に実装されています。MacRubyでは、RubyオブジェクトもObjective-Cオブジェクトなので、Objective-Cのガベージコレクタによって自動的にメモリ管理されます。
RubyCocoaの場合と違って、ガベージコレクタの管理の外にある参照ポインタに関して、MacRuby自体ではとくに何もしていません(MacRubyを利用したライブラリとして実装することは可能でしょう)。したがって、CGPDFContext.fileのようなコードをMacRubyで実装する場合には、CGRelease(もしくはCFRelease)を明示的に使う必要があります。
■ 完成!
以上で、ようやく、Ruby+RubyCocoaを使った「CoreGraphicsによるPDFの描画」のための簡易言語ライブラリが完成しました。簡易言語という言い方はちょっと大げさな気もするたった52行のプログラム(cg_context.rb 本文末尾を参照)です。このcg_context.rb を使った簡単なサンプルとして、1984年から現在までのAppleの株価のグラフを描画するプログラムを書いてみたので試してみてください:
------------------------ aapl_chart.rb
# -*- coding:utf-8 -*-
require 'cg_context'
require 'open-uri'
url = 'http://ichart.finance.yahoo.com/table.csv?' +
's=AAPL&a=08&b=7&c=1984&d=04&e=29&f=2009&g=m&ignore=.csv'
csv = open(url){ |io| io.read }
prices = csv.split(" n")[1..-1].
map{ |row| row.split(',').last.to_f }.reverse
width, height = 600, 400
CGPDFContext.file("aapl.pdf", [0, 0, width, height]) do |pdf|
pdf.page do
scale_x = width / prices.size
scale_y = height / (prices.max - prices.min)
pdf.scaleCTM(scale_x, scale_y)
pdf.translateCTM(0, -prices.min)
pdf.setRGBFillColor(0.0, 0.0, 1.0, 1.0)
points = [ CGPointMake(prices.size-1, 0), CGPointMake(0,0) ]
prices.each_with_index { |v,i| points << CGPointMake(i, v) }
pdf.beginPath
pdf.addLines(points, points.size)
pdf.fillPath
end
pdf.flush
end
---------
このプログラムは、Yahoo! Finance から Apple の株価の履歴をcsv形式でダウンロードして、簡単な折れ線グラフとして描画しています。
CSV形式でダウンロードして株価の配列にしてしまうコードを、3行で書けてしまうあたりがRubyマジックといったところでしょうか? このようなグラフは、PDFよりもむしろ画像ファイルにした方が使い勝手がいいような気がしますね。cg_context.rb を少し改造するだけで PNG画像なども生成できるはずですので、興味のある方は挑戦してみてください。
■ PDF描画簡易言語ライブラリ完成版
------------------------------- cg_context.rb --
# -*- coding: utf-8 ; indent-tabs-mode:nil -*-
require 'osx/cocoa'
include OSX
class CGContext
# context_type 宣言構文の定義:
# コンテキストの種類(PDFなど)を宣言するとCG(種類)Context関数のイン
# スタンスメソッドを生成する
class << self
private
def methodnize(original_name, name)
method_name = name[0].chr.downcase + name[1..-1]
define_method(method_name) do |*args|
OSX.__send__(original_name, @context, *args)
end
end
def context_type(type)
pattern = / ACG#{type}Context(.*) Z/
OSX.methods.each do |func_name|
matched = pattern.match(func_name)
methodnize(func_name, matched[1]) if matched
end
end
end
# デフォルトの context_type 宣言
context_type ''
# CGContextクラスのインスタンスオブジェクトの初期化
def initialize(context)
@context = context
end
end
class CGPDFContext < CGContext
context_type :PDF
def page(page_info={}, &block)
beginPage(page_info)
block.call(self)
ensure
endPage
end
def self.file(path, rect, &block)
url = OSX::NSURL.fileURLWithPath(path)
rect = OSX::NSRect.new(*rect)
context_ref = OSX::CGPDFContextCreateWithURL(url, rect, nil)
pdf = self.new(context_ref)
block.call(pdf)
end
end
# ------------------------------ cg_context.rb --
藤本裕之のプログラミング夜話 #161
突然だがみなさんは、インフレ……近頃は「豚インフル」とかいう略語が流行って来てるんで混同するかもしれんが「インフレーション」のほうね、と聞くとまず何を連想されるだろうか。昨今の不景気はデフレのせいなんだからってんで、政治家とか経済評論家とかテレビキャスターとか一応テレビキャスターになったということにされてるお笑いタレントとかが一時「インフレターゲット論」というのを振り回してたのをご記憶の方も多かろう。
インフレってのはデフレの逆だからモノの値段が上がることである。これ、ものは言いようでね。「モノの値段が上がる」ってのと「お金の価値が下がる」っての、同じことを言ってるのに文字通り「聞こえ」が違う。
ポール・クルーグマンあたりが書いてるようなことばっかり読んでると「そうだインフレを目指さなければ」と思うかも知れないが、スプーンという渾名で知られるブルースマン、ジミー・ウィザースプーンの「Money's Getting Cheaper」という歌には「♪墓堀人まで組合作りやがって死ぬのも高くつく」なんて文句もある。
インフレもデフレも要は需要と供給のバランスの崩れであって自由経済の宿命みたいなものやないの。インフレターゲット論者の言う「適度なインフレ」なんてものが政策で実現できるもんだったら最初から不況だのデフレだの起こさないで済むんぢゃないかと思うが、どうも経済学という学問の世界ではそういうことは言ってはいけない決まりになっているらしい。
あ、このバカなにをトチ狂って付け焼き刃の経済学なんか講釈はじめたのかって? そりゃもちろん前回最後に提示した設問「ソフトウェア技術者はなぜ『高度で専門的な知識や経験を要する』職業ではなくなってしまったの?」に対する答えを出すためである。そのために上のインフレ/デフレ論議を覚えてて欲しいのね。
いっすか。まず最初に「真実」をおさえておくべきだ。真実とはこれね。「ソフトウェア技術者は昔も今も『高度で専門的な知識や経験を要する』職業である」。これは身びいきでなく、そうでしょ? 少なくともオレは、自分の名刺に「プログラマ」と印刷することに関して「ある水準を保ってる」ことを旨としているし、これを読んでる多くの技術者もオレと同じように考えると思う。
次に事実を指摘する。上の真実にも関わらず、なぜワレワレが「ソフトウェア技術者は『高度で専門的な知識や経験を要する』職業ではなくなってしまった」ようだと感じるかというと、それは「ソフトウェア技術者が『高度で専門的な知識や経験を要する』職業にふさわしい扱われかた」をしていないから、もっとハッキリ言えば「安くコキ使われている」からである。
そこで問題は、この真実と事実のギャップがどーして生じたのかである。まず最初に検討してみたい仮説は、これである。
仮説1:ソフトウェア技術者の待遇が悪いのは受給のバランス、つまり供給が過剰だからである。「高度で専門的な知識や経験」も数が増えれば安くなるのが当然やんけ。
どっすか? みなさん、タケナカ元大臣とか、カツマカズヨとかが言いそうな言葉だけど、これにガッテンできますか? この仮説、オレたち当のソフトウェア技術者にしてみると到底首肯しかねるシロモノだが、オレが見るところいわゆる「企業経営者」という肩書きを持つヒトたちの少なくとも50%は、この仮説を聞いて「正しい」と答えるような気がする。でっかい企業に勤めてるヒトは機会をとらえて社長とかに聞いてみて欲しいが(できないか)、なんてのかな、そういうヒトが好んで読む「日経ビジネス」とか「週刊ダイヤモンド」とかって、だいたいこういう認識だと思うのだ。
でもこの仮説には大きな誤謬が潜んでいる。次回はそこをつっついてみる。
(以下次回 2009_05_29)
高橋真人の「プログラミング指南」第159回
皆さんこんにちは、高橋真人です。
今回は、Cocoaのフレームワークに備わっている自動解放プール(Autorelease Pool)という仕組みについてのお話です。ちなみに、この「プール」という言葉ですが、最近は「資金をプールしておく」などという使い方も日常的に耳にしますのでお分かりと思いますが、ここでは「貯めておく場所」という意味で使われています。
さて、いきなりですが、以下のような関数を考えてみます。
NSString *sjis2string(const char *sjis)
{
NSString *str = [[NSString alloc] initWithBytes:sjis
length:strlen(sjis) encoding:NSShiftJISStringEncoding];
return str;
}
これは、通常のC文字列として与えられたシフトJISの文字列をNSStringに変換して返す関数です。
使い方としては、以下のような感じになります。
int main (int argc, const char * argv[])
{
NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init];
const char hello[] = "\x82\xb1\x82\xf1\x82\xc9\x82\xbf\x82\xcd\x81\x49";
NSString *s = sjis2string(hello);
NSLog(@"%@", s);
[s release]; // 注:後半の例では、不要になります
[pool release];
return 0;
}
文字列helloに入っているのは、シフトJISコードで「こんにちは!」となるバイト並びです。
それをsjis2string()に渡すと、NSStringのオブジェクトとして返ってきます。sという変数で受け、NSLog()で表示してから解放しています。
つまりこのコードでは、sjis2string関数を呼び出した側で関数が返したオブジェクトをreleaseしています。確かに、sjis2string()の上記の実装では、allocをしているのにreleaseがありませんから、これは正しい解放です。
ですが、このように「呼び出す側に必ず解放することを要求する」ような関数の仕様はあまりよいとは言えません。できるならば、sjis2string関数の内部でallocとreleaseのバランスを完結させたいところです。
そこで登場するのが、Autoreleaseという仕組みです。Cocoaのフレームワークには、自動解放プールという、任意のオブジェクトを登録できるメモリ管理用のオブジェクトを作って利用することができます。
上記のmain関数の中にも例があるように、
NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init];
という要領で作成します。こうしておいて、あとは任意の場所でこの自動解放プールを使ってメモリ解放をしたいオブジェクトに対して、releaseの代わりにautoreleaseというメッセージを送るだけです。(これによって何が起こるかは、以下で説明します)
上記の関数をこれを利用して書き換えてみると、以下のようになります。
NSString *sjis2string(const char *sjis)
{
NSString *str = [[NSString alloc] initWithBytes:sjis length:strlen(sjis) encoding:NSShiftJISStringEncoding];
return [str autorelease];
}
または、NSStringのオブジェクトを作る場所でメッセージを送っても構いません。
NSString *sjis2string(const char *sjis)
{
NSString *str = [[[NSString alloc] initWithBytes:sjis length:strlen(sjis) encoding:NSShiftJISStringEncoding] autorelease];
return str;
}
さて、このように任意のオブジェクトに対してautoreleaseメッセージを送ると、そのオブジェクトが自動解放プールに登録されることになります。
自動解放プールのオブジェクトは、自身が解放される際に、登録されているオブジェクトに対してそれぞれreleaseメッセージを送ります。前述のmain関数の例で言うと、関数の終わりの部分の[pool release]によって、自動解放プールが開放される時に、一緒にNSStringのオブジェクトも後始末されるというわけです。
sjis2string()が返してくるNSStringのオブジェクトは、関数から戻ってきた時点ではまだ解放されていませんから、上記例のNSLog()に渡してちゃんと出力ができますし、その後の[s release];がなくても(むしろ、あるとまずいのですが)、[pool release];のところできちんと解放されるわけです。
ところで、自動解放プールにオブジェクトを登録する際に、明示的に登録先となる自動解放プールのオブジェクトを指定しないところに、違和感を覚える方もおいでかもしれません。「一体、いまautoreleaseメッセージを投げたこのオブジェクトは、『どの』自動解放プールオブジェクトに登録されるのだろうか?」と。
もしくは、「プログラム全体の中で自動解放プールのオブジェクトは一つしか存在しないのかな?」と疑問をお持ちになる方もいらっしゃるかもしれません。
自動解放プールのオブジェクトは、プログラムの中でいくらでも好きなだけ作ることが可能です。オブジェクト自体も軽量なので、あまり深く考えずにポンポン作ってしまっても、問題になることはあまりないでしょう。
実際、Cocoaのフレームワーク自体も、内部でいくつもの自動解放プールオブジェクトを作ります。
複数のオブジェクトを作った場合、autoreleaseメッセージは最も直近に作られた自動解放プールオブジェクトに対しての登録を意味します。
さて、こうして自動解放プールのことを知り、それなりに使えるようになってくると、あるとき「あれ、このオブジェクトはいつ解放されるんだろう?」と疑問を感じることがあります。「ちゃんと必要な間、このオブジェクトは解放されずに残っているんだろうか」という不安を私自身も持ったことがあります。
そんな時、先輩やドキュメントは「メモリが解放されてしまうと困る場合は、retainを呼びなさい」とアドバイスしてくれています。(この辺、リファレンスカウントという考え方が絡みますので、次回に説明します)
結論から言いますと、一つの関数やメソッド、または自分が制御している一連の処理の間は、自動解放プールが勝手に解放されることはありません。(自分で作った自動解放プールを解放する場合は、もちろん別です)
Cocoaの一般的なプログラムでは、イベントループというのが常に回っていてユーザーの操作などを監視しています。このイベントループの繰り返しごとにも自動解放プールが使われています。つまり、ループの1回が回り始める先頭で新しい自動解放プールオブジェクトが作られ、繰り返しの終わりのところでオブジェクトが解放されるのです。
通常の処理、例えばユーザーのボタンクリックなどによって起動されるメソッドは、そのメソッドの末尾まで実行されるまでの間はよそから割り込まれることはなく、その間の主導権はずっと保持したままですから、知らないうちに自動解放プールが働くことはありません。(マルチスレッドが絡むと、この限りではない)
そもそも、イベントの処理は複数のループにまたがって行われるのではなく、繰り返しの1単位の中で完結するものですから、処理の途中で気付かないうちに自分の使っているオブジェクトが解放されてしまうことはありませんから、安心してautoreleaseを使ってください。
そうは言うものの、プログラム側の事情で、複数のイベントループをまたいで存在する必要のあるオブジェクトというのもありますよね。そういう場合はどうしたらいいのでしょう? こういったオブジェクトには自動解放プールを使ってはいけないのでしょうか?
もちろん、そんなことはありません。そのためにCocoaのフレームワークにはretainカウントというものが備わっています。次回はそれについて説明します。
◇MOSAからのお知らせと編集後記は割愛します◇