はじめに
ここまで、パスへのリクエストに対する処理の登録、サーバ起動のからくりについて見てきました。最後に実際にリクエストが来たときにどのような処理が行われるのかについて見ていきましょう。
Sinatra::Base.call(クラスメソッド)
前回も少し説明しましたが、Rackハンドラに渡されているのはApplicationクラスオブジェクトです。Rubyはクラスもオブジェクトなのでcall(env)
というメソッド(クラスなのでクラスメソッド=特異メソッド)さえあればRackハンドラは文句は言いません。
というわけで、読解の起点となるのはこのcallです。
def call(env)
synchronize { prototype.call(env) }
end
prototypeは前回見たようにインスタンスを作っておくメソッドです。というか名前から作っておいたインスタンスをテンプレに処理を行ってそうです。
synchronizeは想像通りなものなので省略。ちなみにデフォルトは排他制御オフのようです。
Sinatra::Base#call(インスタンスメソッド)
prototypeで作られるのはミドルウェアが設定されたアプリケーションオブジェクトをさらにくるんだWrapperオブジェクトですが、起動編で前置きしたようにそこは無視してApplicationオブジェクトのcallインスタンスメソッドに進みます。
# Rack call interface.
def call(env)
dup.call!(env)
end
dupした上でcall!メソッドが呼ばれています。つまり、テンプレをコピーして個々のリクエストを処理しているようです。
call!に進む。invoke { dispatch! }
が処理本体のようです。
なお登録編でもコメントしましたがSinatraでは「!」をinternal的な意味で使っているように思われます。
def call!(env) # :nodoc:
@env = env
@params = IndifferentHash.new
@request = Request.new(env)
@response = Response.new
template_cache.clear if settings.reload_templates
@response['Content-Type'] = nil
invoke { dispatch! }
invoke { error_block!(response.status) } unless @env['sinatra.error']
unless @response['Content-Type']
if Array === body and body[0].respond_to? :content_type
content_type body[0].content_type
else
content_type :html
end
end
@response.finish
end
invokeメソッド
invokeメソッドに進む。
# Run the block with 'throw :halt' support and apply result to the response.
def invoke
res = catch(:halt) { yield }
res = [res] if Integer === res or String === res
if Array === res and Integer === res.first
res = res.dup
status(res.shift)
body(res.pop)
headers(*res)
elsif res.respond_to? :each
body res
end
nil # avoid double setting the same response tuple twice
end
ここでポイントとなるのは次の二点です。
- Sinatraはthrow-catchの仕組みを使っているようだ。なおJavaとかに慣れてる人には誤解を与えると思いますがRubyのthrow-catchは例外処理ではなく大域脱出を行う仕組みです。
- というわけで大域脱出して返された値をもとにレスポンスを設定している様子。ここで使われているstatusやbodyについては後ほど触れます。
dispatch!メソッド
invokeメソッドでyieldされているものは「invokeメソッドにブロックとして渡されたもの」、つまり、dispatch!メソッド呼び出しです。
# Dispatch a request with error handling.
def dispatch!
@params.merge!(@request.params).each { |key, val| @params[key] = force_encoding(val.dup) }
invoke do
static! if settings.static? && (request.get? || request.head?)
filter! :before
route!
end
rescue ::Exception => boom
invoke { handle_exception!(boom) }
ensure
begin
filter! :after unless env['sinatra.static_file']
rescue ::Exception => boom
invoke { handle_exception!(boom) } unless @env['sinatra.error']
end
end
なんとまたinvokeメソッドが呼ばれています。ここがどう動いているのかについては後で詳しく説明するとしてroute!に進みます(static!やfilter!は名前通りのことをやっているので飛ばします)
route!メソッド
登録編で見た「routeクラスメソッド」は登録処理で、「route!インスタンスメソッド」は実際のリクエストの処理というのはなかなか名前付けがいまいちに思うわけですがroute!の定義です。
# Run routes defined on the class and all superclasses.
def route!(base = settings, pass_block = nil)
if routes = base.routes[@request.request_method]
routes.each do |pattern, conditions, block|
returned_pass_block = process_route(pattern, conditions) do |*args|
env['sinatra.route'] = "#{@request.request_method} #{pattern}"
route_eval { block[*args] }
end
# don't wipe out pass_block in superclass
pass_block = returned_pass_block if returned_pass_block
end
end
# Run routes defined in superclass.
if base.superclass.respond_to?(:routes)
return route!(base.superclass, pass_block)
end
route_eval(&pass_block) if pass_block
route_missing
end
今までに比べると少し複雑ですが、雰囲気で読むと以下のようになっていると思われます。
- baseで示されるオブジェクト(デフォルトはsettings)から登録されてる処理の一覧を取り出して、
- process_routeメソッド呼び出してリクエストパスに対応する処理かチェック、
- 対応する処理ならroute_evalが呼び出される。
一つずつちゃんと確かめていきましょう。
settings
まずはsettingsです。「デフォルト値として設定されているsettingsってなんだ?」と思われるかもしれませんが実はメソッドです。Rubyはデフォルト引数としてメソッド呼び出しが書ける言語となっています。
というわけで「settingsメソッド」を探すと以下のようになっています。要するにApplicationクラスオブジェクトですね。登録編で処理が「Applicationクラスの@routes」に登録されていたのを覚えているでしょうか。そこから取り出してマッチングが行われているわけです。
# Access settings defined with Base.set.
def settings
self.class.settings
end
# Access settings defined with Base.set.
def self.settings
self
end
process_route
次はprocess_routeです。
# If the current request matches pattern and conditions, fill params
# with keys and call the given block.
# Revert params afterwards.
#
# Returns pass block.
def process_route(pattern, conditions, block = nil, values = [])
route = @request.path_info
route = '/' if route.empty? and not settings.empty_path_info?
route = route[0..-2] if !settings.strict_paths? && route != '/' && route.end_with?('/')
return unless params = pattern.params(route)
params.delete("ignore") # TODO: better params handling, maybe turn it into "smart" object or detect changes
force_encoding(params)
original, @params = @params, @params.merge(params) if params.any?
regexp_exists = pattern.is_a?(Mustermann::Regular) || (pattern.respond_to?(:patterns) && pattern.patterns.any? {|subpattern| subpattern.is_a?(Mustermann::Regular)} )
if regexp_exists
captures = pattern.match(route).captures.map { |c| URI_INSTANCE.unescape(c) if c }
values += captures
@params[:captures] = force_encoding(captures) unless captures.nil? || captures.empty?
else
values += params.values.flatten
end
catch(:pass) do
conditions.each { |c| throw :pass if c.bind(self).call == false }
block ? block[self, values] : yield(self, values)
end
rescue
@env['sinatra.error.params'] = @params
raise
ensure
@params = original if original
end
これも少し長いですが、http://localhost:4567 にアクセスされたとすると結局実行されるのは以下となります。
def process_route(pattern, conditions, block = nil, values = [])
yield(self, values)
end
route!に戻って、process_routeからyieldされるブロックを改めて見てみましょう。
routes.each do |pattern, conditions, block|
returned_pass_block = process_route(pattern, conditions) do |*args|
env['sinatra.route'] = "#{@request.request_method} #{pattern}"
route_eval { block[*args] }
end
*argsとして渡されるのは、selfとvaluesです。これを登録されているblockに渡す・・・、[]で囲んでいるのがこれは、block([*args])
と解釈されるようです。つまり、*argsと受け取った残余引数を展開し、改めて配列として渡す、意味あるのかな。昔はこう書かないと動かなかったとか?
さて、このblockが何者かというのは登録編の復習になりますが以下となります。
wrapper = block.arity != 0 ?
proc { |a, p| unbound_method.bind(a).call(*p) } :
proc { |a, p| unbound_method.bind(a).call }
つまり、self(Applicationオブジェクト)がunboundなメソッド(オブジェクトに結び付いてないメソッド。ここでは登録編でインスタンスメソッドとして登録されたパスに対するリクエスト処理のブロック)バインドされ、呼び出されています。
route_eval
上で説明した「リクエストに対する処理」はroute_evalにブロックとして渡されるので、route_evalでyieldされないと実際には実行されません。というわけでroute_evalを見てみましょう。
# Run a route block and throw :halt with the result.
def route_eval
throw :halt, yield
end
渡されたブロックがyieldされ、その戻り値(リクエストの処理ブロックからの戻り値)を引数にthrowが行われています。
invoke再訪
route_evalではthrowが行われていました。これは対応する(同じシンボルを指定して待っている)catchまで呼び出しを一気に戻るという仕組みになっています。:haltで待っているのはどこかというとinvokeメソッドです。
# Run the block with 'throw :halt' support and apply result to the response.
def invoke
res = catch(:halt) { yield }
res = [res] if Integer === res or String === res
if Array === res and Integer === res.first
res = res.dup
status(res.shift)
body(res.pop)
headers(*res)
elsif res.respond_to? :each
body res
end
nil # avoid double setting the same response tuple twice
end
ただし、ここまで見てきた中でinvokeは二回呼び出されていました。call!からroute_evalに至るまでの呼び出しは以下のようになっています。
call!
invoke
dispatch!
invoke
route!
process_route
route_eval
この場合、内側のinvokeまで戻るということになります。その後、dipatch!では(登録されていたら)後処理が実行され、外側のinvokeにはnilが返されるので何もしないというようになっているようです。何故このようなややこしいことしているのかについては「歴史的経緯」な気がしますがともかくthrow-catchの大域脱出を使うことで処理を一気に巻き戻すということが行われています。
Sinatra::Helpers
最後に複線しておいたstatusやbodyについて。Sinatraではステータスコードやボディーを戻り値として返す以外に以下のように書くことも可能です。個人的には宣言的に書けるので好きです。
get '/' do
status 200
body 'Hello world!'
end
大体察しはつくと思いますがこのstatusやbodyはApplicationオブジェクトのインスタンスメソッドです。もっとも「DSLとして」Sinatraを使う人にはインスタンスメソッドであるということは意識せずに書けるようになっています。
これらのメソッドはSinatra::Helpersモジュールに定義されており、Baseクラスがインクルードする、リクエストの処理ブロックは先に見たようにApplicationオブジェクトのインスタンスメソッドのため、インクルードされているメソッドが使える(それらを介してインスタンス変数を操作する)というからくりになっています。
# Methods available to routes, before/after filters, and views.
module Helpers
# Set or retrieve the response status code.
def status(value = nil)
response.status = Rack::Utils.status_code(value) if value
response.status
end
処理編まとめ
以上ここまでリクエストが来たときにどのように処理が行われるのかについて見てきました。登録編、起動編に比べると少し複雑でした。
- クラスはオブジェクトなので、呼び出し側の指定を満たすメソッドが定義していれば特異メソッドでも問題ない(呼ばれる側として渡すことができる)
- リクエストが来るとあらかじめ作成しておいた「アプリケーションオブジェクト」を複製し、リクエストに割り当てる。再利用とかを行っていないのは「そこまでパフォーマンスを追求してないから」と思われる。
- Rubyではデフォルト引数にメソッド呼び出しを書くことができる。
- 大域脱出を行うためにthrow-catchが使われている(例外処理ではない)
おわりに
三回にわたってSinatraの中身がどうなっているかを見てきました。「DSLをどう実現してるんだろ?(あと、マイクロフレームワークと呼ばれる類の実装を見てみたい)」という興味から読んでみましたがとてもRubyを使い倒したコードになっていたように思います。明日書くコードの役には立たなそうですが参考になりました(笑)