読者です 読者をやめる 読者になる 読者になる

Sinatraのソースコードを読む(1)

Rubyの軽量フレームワークSinatraソースコードがイケテルらしいので、がんばって読む事にしました。

Sinatraって?

こいつ

バージョン

こちらのブログが大変参考になりそうです。こちらのブログではSinatra v.1.3.0.dを対象にしているようなので、以降、それに合わせます。Rubyは自分が今使っている1.9.2で。

とりあえずsinatra動かしてみる

本家チュートリアルのGetting Startedの部分だけやってみます。さくっと動きます

githubからgit cloneし、v1.3.0.dにチェックアウト

# ターミナルで
$ git clone https://github.com/sinatra/sinatra.git
$ cd sinatra
$ git checkout v1.3.0.d

ソースコード読む準備。手元にソースコードを持ってきて、値をダンプしながら読み進めた方が効率的なので、git cloneしてきます。その後sinatraディレクトリに移動し、v1.3.0.dにチェックアウトします。

cloneしたsinatraを動かす

# hoge.rb
require './sinatra'

get '/' do
'hello'
end

libディレクトリ直下にhoge.rbみたいなファイルを作ります。それをターミナルでruby hoge.rb。するとwebrickの4567ポートが立ち上がるので、ブラウザでlocalhost:4567にアクセスすると、helloと表示されます。

ソースコードリーディング

本題のソースコードリーディング。さきほどのhoge.rbで実行したメソッドgetを対象に

  • メソッドgetはどこで定義されているのか?
  • メソッドgetはどのような内容を実行しているのか?

を調べます


sinatra / lib / sinatra.rb

libdir = File.dirname(__FILE__)
$LOAD_PATH.unshift(libdir) unless $LOAD_PATH.include?(libdir)

require 'sinatra/base'
require 'sinatra/main'

enable :inline_templates

まずは起点となるsinatra.rb。ロードパスを追加した上で、sinatra/baseとsinatra/mainをrequire。


sinatra / lib / sinatra / base.rb

require 'thread'
require 'time'
require 'uri'
require 'sinatra/rack'
require 'sinatra/showexceptions'
require 'tilt'

module Sinatra
  ...1000行以上
end

requireされたbase.rb。rackなどをrequireしている。その後Sinatra名前空間配下に色々なクラスやモジュールを定義してる。


sinatra / lib / sinatra / main.rb

require 'sinatra/base'

module Sinatra
  class Application < Base
    ...
  end

  at_exit { Application.run! if $!.nil? && Application.run? }
end

# ポイント
include Sinatra::Delegator

お次はmain.rb。module Sinatraとclass Applicationはbase.rbでも定義されていたので、再オープンして定義を追加しているということになる。

参考:http://d.hatena.ne.jp/kitokitoki/20120627/p2

ポイントはinclude Sinatra::Delegator。トップレベルでモジュールをインクルードするということは、モジュール内で定義されたメソッドをトップレベルでレシーバを明示せず実行できるようになることを意味する。つまり、Sinatra::Delegatorをトップレベルでインクルードすれば、Sinatra::Delegatorで定義されたメソッドをトップレベルでレシーバを明示せず(get '/'のような形で)実行できるようになる。

このことから、Sinatra::Delegatorにgetなどのメソッドが定義されていると推測できる。


https://github.com/sinatra/sinatra/blob/v1.3.0.d/lib/sinatra/base.rb#L1482

  module Delegator #:nodoc:
    def self.delegate(*methods)
      methods.each do |method_name|
        # 動的にメソッドを定義
        define_method(method_name) do |*args, &block|
          # メソッドgetを実行した時、if respond_to? method_nameはfalseなので一旦無視
          return super(*args, &block) if respond_to? method_name
          # Delegator.targetをレシーバにしてmethod_nameを実行
          # Delegator.targetは、メソッドgetを実行した時はSinatra::Application
          Delegator.target.send(method_name, *args, &block)
        end
        private method_name
      end
    end

    # def self.delegateを呼び出し
    delegate :get, :patch, :put, :post, :delete, :head, :options, :template, :layout,
             :before, :after, :error, :not_found, :configure, :set, :mime_type,
             :enable, :disable, :use, :development?, :test?, :production?,
             :helpers, :settings

    class << self
      attr_accessor :target
    end

    self.target = Application
  end

Sinatra::Delegatorはbase.rbで定義されている。で、この中ではdelegate :get, :patch,....でself.delegateを呼び出している。このメソッドは:getなどのシンボルを配列でうけとり、1つ1つのシンボルに対して、define_methodを実行している。

define_methodはrubyメタプログラミングでよく使うメソッドで、動的にメソッドを定義するもの。def get, def patch...と定義していっていると思えばいい。

参考: http://rurema.clear-code.com/1.9.2/method/Module/i/define_method.html

ではメソッドの内容はどうか。まず謎めいたreturn super(*args, &block) if respond_to? method_nameの部分であるが、メソッドgetを実行した際にはif respond_to? method_nameの値がfalseとなるので、一旦無視する。

次にDelegator.target.send(method_name, *args, &block)というコード。sendというのは、オブジェクトをレシーバにして第1引数に与えられたメソッド名を実行するもの。

参考: http://rurema.clear-code.com/1.9.2/method/Object/i/__send__.html

で、レシーバのDelegator.targetだが、メソッドgetを実行した際にダンプしてみると、Sinatra::Applicationであることがわかる。つまり、Sinatra::Application内のdef getが実行されるということになる。なので、Sinatra::Application内でdef getが定義されている箇所を探す。


https://github.com/sinatra/sinatra/blob/v1.3.0.d/lib/sinatra/base.rb#L1462

  class Application < Base
    set :logging, Proc.new { ! test? }
    set :method_override, true
    set :run, Proc.new { ! test? }
    set :session_secret, Proc.new { super() unless development? }

    def self.register(*extensions, &block) #:nodoc:
      added_methods = extensions.map {|m| m.public_instance_methods }.flatten
      Delegator.delegate(*added_methods)
      super(*extensions, &block)
    end
  end

ここにdef getはない

https://github.com/sinatra/sinatra/blob/v1.3.0.d/lib/sinatra/main.rb

  class Application < Base

    # we assume that the first file that requires 'sinatra' is the
    # app_file. all other path related options are calculated based
    # on this path by default.
    set :app_file, caller_files.first || $0

    set :run, Proc.new { $0 == app_file }

    if run? && ARGV.any?
      require 'optparse'
      OptionParser.new { |op|
        op.on('-x')        {       set :lock, true }
        op.on('-e env')    { |val| set :environment, val.to_sym }
        op.on('-s server') { |val| set :server, val }
        op.on('-p port')   { |val| set :port, val.to_i }
        op.on('-o addr')   { |val| set :bind, val }
      }.parse!(ARGV.dup)
    end
  end

こちらにも無い。。。ということは、スーパークラスであるBaseに書かれているのか


https://github.com/sinatra/sinatra/blob/v1.3.0.d/lib/sinatra/base.rb#L1108

      # Defining a `GET` handler also automatically defines
      # a `HEAD` handler.
      def get(path, opts={}, &block)
        conditions = @conditions.dup
        route('GET', path, opts, &block)

        @conditions = conditions
        route('HEAD', path, opts, &block)
      end

あった。パスとオプションとブロックを引数にして、routeメソッドなどを実行している。routeメソッドなどの中身についてはさらに読み込んで行く必要がありそうなので、今日のところは一旦ここまでにしよう。

ここまでに分かった事

メソッドgetはどこで定義されているのか?
  • base.rbのmodule Delegatorで、define_methodにより動的に定義されている。
  • が、定義内容はbase.rbのSinatra::Base内のdef getに定義されている。
  • module Delegatorをmain.rbがトップレベルでインクルードしているのでget '/' のようにトップレベルでレシーバ無しでメソッドを呼び出せる。
メソッドgetは呼び出されるとどのような処理をするのか?
  • パスとオプションとブロックを引数にして、routeメソッドなどを実行している。
  • 詳細は次の機会にみていく。

続く...