activeadmin読んだ
activeadmin/activeadminを初めて使うことになったので、どういう仕組みになっているのか調べてみた。
TL;DR
rails g active_admin:install
を実行するとlib/generators/active_admin/install_generator.rbが実行され、ActiveAdmin.routes(self)
がconfig/routes.rbに追加される。- app/admin/以下にあるResource定義ファイル内で実行される
ActiveAdmin.register
では、ActiveAdmin::Resource
インスタンスが生成され、動的にResourceごとのcontrollerが生成される。それらはすべてActiveAdmin::ResourceController
を継承している。 - config/routes.rbに追加された
ActiveAdmin.routes(self)
は内部でapp/admin/以下のファイルをロードし(このタイミングで上述のActiveAdmin.register
が実行される)、ActiveAdmin::Resource
インスタンスから動的にroutingが定義される。
長いので、以下ActiveAdmin::
をAA::
と略記する。
Generator
Active Adminをセットアップするにはまずrails g active_admin:install
を実行する。
このとき、lib/generators/active_admin/install_generator.rbに定義されたRails::Generators::Base
のサブクラスにあるpublicメソッドが上から順番に実行される。Railsはlib/generators/**/*_generator.rbにマッチするファイルに定義されたRails::Generators::Base
のサブクラスをRails Generatorとして実行することができる。
# lib/generators/active_admin/install_generator.rb module ActiveAdmin module Generators class InstallGenerator < ActiveRecord::Generators::Base # ... def setup_routes if options[:user] inject_into_file "config/routes.rb", "\n ActiveAdmin.routes(self)", after: /devise_for .*, ActiveAdmin::Devise\.config/ else route "ActiveAdmin.routes(self)" end end # ... end end end
いくつかメソッドが定義されている中でsetup_routes
を見ると、config/routes.rbにActiveAdmin.route(self)
を追記しているようだ。self
はRails.application.routes.draw do ... end
のブロック内でのself
なのでActionDispatch::Routing::Mapper
インスタンスを表している。
Register a resource
Generatorでファイルの追加と変更を行ったあとは、管理画面で管理するResourceを作成する。例えば、rails g active_admin:resource Post
を実行すると以下のようなapp/admin/post.rbが生成される。
# app/admin/post.rb ActiveAdmin.register Post do end
このブロックの中にviewやcontrollerの設定を追加していくのだけど、まずActiveAdmin.register
の定義を調べる。
# lib/active_admin.rb module ActiveAdmin class << self # ... def application @application ||= ::ActiveAdmin::Application.new end # ... delegate :register, to: :application # ... end end
ActiveSupportが拡張したメソッドdelegate
によって、ActiveAdmin.register
の処理は実際にはAA::Application#register
が行っている。
# lib/active_admin/application.rb def register(resource, options = {}, &block) ns = options.fetch(:namespace){ default_namespace } namespace(ns).register resource, options, &block end
options[:namespace]
がなければdefault_namespace
つまり:admin
がns
に入る。#namespace
はnamespaces[ns]
があればそれを返し、なければAA::Namespace
インスタンスを初期化しnamespaces
に追加した上で返す。よって、AA::Namespace#register
が処理が渡っている。
# lib/active_admin/namespace.rb def register(resource_class, options = {}, &block) config = find_or_build_resource(resource_class, options) register_resource_controller(config) parse_registration_block(config, resource_class, &block) if block_given? reset_menu! ActiveAdmin::Event.dispatch ActiveAdmin::Resource::RegisterEvent, config config end
find_or_build_resource
はAA::Resource
インスタンスを返す。#register_resource_controller
は以下のように定義されており、Resource
インスタンスから動的にAA::ResourceController
を継承するResourceごとのcontrollerを定義している。
# lib/active_admin/namespace.rb def register_resource_controller eval "class ::#{config.controller_name} < ActiveAdmin::ResourceController; end" config.controller.active_admin_config = config end
parse_registration_block
は上述の例のapp/admin/post.rbでActiveAdmin.register
に渡されていたブロックを評価する部分だと思う。ブロックの中身を独自のDSLとして評価してカスタマイズを行っていると思う。
Routing
Generatorによってconfig/routes.rbに追加されたActiveAdmin.routes
の定義を調べる。
# lib/active_admin.rb module ActiveAdmin # ... def application @application ||= ::ActiveAdmin::Application.new end # ... delegate :routes, to: :application # ... end
delegate
はActiveSupportが拡張しているメソッドで、メソッドの呼び出しをto
で指定したオブジェクトに委譲する。なので、ActiveAdmin.routes
は実際にはAA::Application#routes
を指している。
# lib/active_admin/application.rb def routes(rails_router) load! router.apply(rails_router) end
load!
はapp/admin/**/*.rbをKernel.load
する。このとき上述したapp/admin/post.rbのようなResource定義ファイルがロードされる。そして、ActiveAdmin.register
が実行され各Resourceのcontrollerが定義される。
router
はRouter
インスタンスなので、Router#apply
を調べる。
# lib/active_admin/router.rb def apply(router) define_root_routes router define_resource_routes router end
まずdefine_root_routes
は以下のように定義されている。
# lib/active_admin/router.rb def define_root_routes(router) router.instance_exec @application.namespaces.values do |namespaces| namespaces.each do |namespace| if namespace.root? root namespace.root_to_options.merge(to: namespace.root_to) else namespace namespace.name do root namespace.root_to_options.merge(to: namespace.root_to) end end end end end
このrouter
はActionDispatch::Routing::Mapper
であり、@application.namespaces.values
はAA::Namespace
インスタンスの配列だ。
ActiveAdmin.register
に特にoptionを指定しない場合、namespace.root?
はtrue
となる。namespace.root_to_options
とnamespace.root_to
がどこで定義されているのか不明。。。なんだけど、AA::Application
内にこれらが定義されており、root_to_options
は{}
でroot_to
は"dashboard#index"
となっている。どのようにしてAA::Namespace
にそれらが定義されるのか不明ではあるが、おそらくこれらの値が使われるのだと思う。よって、結局このメソッドはroot to: "dashboard#index"
としているだけだ。
# lib/active_admin/router.rb def define_resource_routes(router) router.instance_exec @application.namespaces, self do |namespaces, aa_router| resources = namespaces.value.flat_map{ |n| n.resources.values } resources.each do |config| routes = aa_router.resource_routes(config) # ... instance_exec &routes end end end
config
は先述したAA::Resource
インスタンスだ。aa_router
はAA::Router
インスタンスなのでAA::Router#resource_routes
を見る。
# lib/active_admin/router.rb def resource_routes(config) Prox.new do build_route = proc{ |verbs, *args| [*verbs].each{ |verb| send verb, *args } } build_action = proc{ |action| build_route.call(action.http_verb, action.name) } case config when ::ActiveAdmin::Resource resources config.resource_name.route_key, only: config.defined_actions do member do config.member_actions.each &build_action end collection do config.collection_actions.each &build_action post :batch_action if config.batch_action_enabled? end end when ::ActiveAdmin::Page # ... else # ... end end end
このメソッドで返されるProcオブジェクトはActionDispatch::Routing::Mapper
インスタンスのコンテキストでinstance_exec
されるため、要するにこのProcオブジェクト内の処理がそのままconfig/routes.rb内のroutingの設定となる。Resourceインスタンスの情報から動的にroutingを組み立てているようだ。
所感
軽く触ってみたけど、Resource定義ファイルに独自DSLでviewを書いていくのが非常にカスタマイズが大変だし覚えることが多そうだな、あまり筋がよくなさそうという印象を受けた。
で、調べてみた結果、Resource定義ファイルから動的にcontrollerとかroutingとかを定義していて、それらをカスタマイズするのに独自DSLを使うという構図になっていることが分かった。管理画面って、ビジネスサイドの要求によってどんどんカスタマイズが必要になるので、カスタマイズに独自のDSLを覚えなくてはいけないとか、場合によってはカスタマイズできないみたいな状況は大きな問題だと思う。だから、動的にいろいろ生成する方針は管理画面の実装には適していないのではないかと思った。なんでこれがこんなに支持されているのかよくわからない。
RailsのReloaderの仕組み
Resqueのworker上で実行されるコードが古いまま更新されないというような問題があって、意味もわからず書いたコードでなんとかその場を収めたんだけど、気持ち悪いのでRailsでいかにしてコードが更新されるのか調べてみることにした。
TL;DR
未定義の定数が参照されると、ActiveSupport::ModuleConstMissing
によって拡張されたModule#const_missing
が呼ばれる。命名規則に基いて定数名からファイル名が推測され、autoload_paths
に存在するファイルを見つける。そのファイルがロード済みであればそこで終了。ロード済みでなければKernel.load
でロードし、ロードされた定数は配列で管理される。
development環境では、Railsのmiddleware stackにActionDispatch::Reloader
というmiddlewareがあり、リクエストごとにファイルの最終更新日時を確認し、変更されていればActiveSupport::Dependencies.clear
を呼ぶ。これによって、ロード済みのファイルが空っぽになり、ロードされた定数もすべて削除される(=未定義状態に戻る)。なので、ファイルが変更されるたびにconst_missing
から始まる一連のフローが起こり、Kernel.load
によって最新のコードがロードされるようになっている。
以下は、上述の結論に至るまでのソースコードリーディングのメモです。分かりにくいかも。
ActionDispatch::Reloader
ActionDispatch::ReloaderはRackミドルウェアなので、#call
の中で初期化時に受け取った他のRackミドルウェアの#call
を呼んでいる。その前後でリロードに関する処理を実行しているはずだ。
def call(env) @validated = @condition.call prepare! response = @app.call(env) response[2] = ::Rack::BodyProxy.new(response[2]) { cleanup! } response rescue Exception cleanup! raise end def prepare! run_callbacks :prepare if validated? end def cleanup! run_callbacks :cleanup if validated? ensure @validated = true end
次のRackミドルウェアが処理を行う前後でprepare!
とcleanup!
を呼んでいる。その中身はrun_callbacks
を呼んでいる。これはActiveSupport::Callbacks
で定義されているメソッドで、set_callbacks
で登録されたcallbackを実行する。なので、:prepare
と:cleanup
というイベントに対してどこかで登録されたcallbackがされている。このcallbackの登録を行うメソッドもActionDispatch::Reloader
に含まれている。
def self.to_prepare(*args, &block) # ... set_callbacks(:prepare, *args, &block) end def self.to_cleanup(*args, &block) # ... set_callbacks(:cleanup, *args, &block) end
この2つのメソッドを使ってcallbackの登録が行われている。これらを呼び出している箇所を探すと、Rails::Application::Finisher
で呼ばれていることがわかる。
Rails::Application::Finisher
initializer :set_clear_dependencies_hook, group: :all do callback = lambda do ActiveSupport::DescendantsTracker.clear ActiveSupport::Dependencies.clear end if config.reload_classes_only_on_change reloader = config.file_watcher.new(*watchable_args, &callback) self.reloaders << reloader ActionDispatch::Reloader.to_prepare(prepend: true) do reloader.execute end else ActionDispatch::Reloader.to_cleanup(&callback) end end
initializer
はRails::Initializable
で定義されているメソッドでRailsの初期化時に実行される処理を登録することができる。つまり、Railsの初期化時に:prepare
または:cleanup
のcallbackを登録しているということになる。
reload_classes_only_on_change
という設定はデフォルトでtrue
になっていて、依存するファイルが変更されたときだけクラスを再読み込みするかどうかを制御する。file_watcher
はデフォルトではActiveSupport::FileUpdateChecker
を指している。つまり、デフォルトでは、:prepare
のときにActiveSupport::FileUpdateChecker#execute
が実行されるように設定されていることになる。
ActiveSupport::FileUpdateChecker
は初期化時に渡されたファイルを配列として受け取り、またそれらが更新されたときに実行されるブロックを受け取る。#execute
はファイルが更新されているかどうかに関わらずブロックを実行する。ここで実行されるのは、以下のブロックとなる。
callback = lambda do ActiveSupport::DescendantsTracker.clear ActiveSupport::Dependencies.clear end
ここまでをまとめると、リクエストを受けるごとに上の2つのメソッドが実行されコードのリロードが行われるということになる。
ActiveSupport::DescendantsTracker
def clear if defined? ActiveSupport::Dependencies @@direct_descendants.each do |klass, descendants| if ActiveSupport::Dependencies.autoloaded?(klass) @@direct_descendants.delete(klass) else descendants.reject! { |v| ActiveSupport::Dependencies.autoloaded?(v) } end end else @@direct_descendants.clear end end
@@direct_descendants
の中身を消去しているようだ。これはHashであり、中身がキーがクラスで、値がそのクラスを継承したサブクラスの配列となっている。Class#inherited
をoverrideしており、ActiveSupport::DescendantsTracker
をextend
しているクラスを継承したタイミングで@@direct_descendants
に追加される。ActiveSupport::DescendantsTracker
は例えばActiveRecord::Base
でextend
されているため、ActiveRecord::Base
のサブクラス、つまり通常のModelクラスはActiveRecord::Base.descendants
から取得できる。これを利用しているのが先述したActiveSupport::Callbacks
で、callbackをサブクラスから親クラスへ辿っていくときに利用されている。
ActiveSupport::Dependencies
def clear log_call loaded.clear loading.clear remove_unloadable_constants! end
loaded
とloading
はクラス変数@@loaded
および@@loading
へのアクセサでmattr_accessor
によって定義されている。そして、これらのクラス変数の実体はSet
オブジェクトだ。
次にloaded
とloading
にいつ何が追加されるのか調べると、ActiveSupport::Dependencies.require_or_load
というメソッドで呼ばれている。
def require_or_load(file_name, const_path = nil) # ... file_name = $` if file_name =~ /\.rb\z/ expanded = File.expanded_path(file_name) return if loaded.include?(expanded) loaded << expanded loading << expanded begin if load? # ... load_args = ["#{file_name}.rb"] load_args << const_path unless const_path.nil? if !warnings_on_first_load or history.include?(expanded) result = load_file(*load_args) else enable_warnings { result = load_file(*load_args) } end else # ... end rescue Exception loaded.delete expanded raise ensure loading.pop end history << expanded result end
loaded
とloading
に追加されているのはおそらくロードするファイルの絶対パスと思われる。そして、一連のロードが完了したらloading
からは削除されるようだ。loading
は読み込み中の再読み込みを防ぐために一時的に利用される変数らしい。一方、loaded
は既にロード済みかどうかをチェックして、ロード済みであればrequire_or_load
を中断させるために使われているようだ。実際のロードの処理はload_file
で行われるようだ。ActiveSupport::Dependencies.clear
によってloaded
が空になると、require_or_load
内で再度load_file
を実行することになる。
def load_file(path, const_paths = loadable_constants_for_path(path)) # ... const_paths = [const_paths].compact unless const_paths.is_a? Array parent_paths = const_paths.collect { |const_path| const_path[/.*(?=::)/] || ::Object } result = nil newly_defined_paths = new_constants_in(*parent_paths) do result = Kernel.load path end autoloaded_constants.concat newly_defined_paths unless load_once_path?(path) autoloaded_constants.uniq! # ... result end
const_paths
はapp/models/user.rb
とapp/controllers/users_controller.rb
いうファイルがあれば"User"
と"UsersController"
という表す文字列を含む配列となる。parent_paths
はconst_paths
の中で"Admin::UsersController"
のようなネストするものと"::Object"
を抽出した配列となる。new_constants_in
は渡したブロックを実行し、その中で新たにロードされた定数を返す。なので、Kernel.load
がとりあえず実行されるようだ。
話を少し戻してrequire_or_load
はどこで呼ばれているかを調べると、load_missing_constant
で呼ばれており、さらにこのメソッドはActiveSupport::ModuleConstMissing#const_missing
で呼ばれている。
module ModuleConstMissing # ... def const_missing(const_name) from_mod = anonymous ? guess_for_anonymous(const_name) : self Dependencies.load_missing_constant(from_mod, const_name) end end
そしてこのmoduleは以下のようにしてModule
クラスにinclude
されるため、デフォルトのconst_missing
の挙動をoverrideすることになる。
module ActiveSupport module Dependencies # ... def hook! Object.class_eval { include Loadable } Module.class_eval { include ModuleConstMissing } Exception.class_eval { include Blamable } end # ... end end ActiveSupport::Dependencies.hook!
つまり、定義されていない定数を参照する→const_missing
→load_missing_constant
→require_or_load
→load_file
という順番で呼ばれることになる。
ここで、ActiveSupport::Dependencies.clear
の定義に戻る。
def clear log_call loaded.clear loading.clear remove_unloadable_constants! end
見ていなかったremove_unloadable_constants!
について見ていく。
def remove_unloadable_constants! autoloaded_constants.each { |const| remove_constant const } autoloaded_constants.clear Reference.clear! explicitly_unloadable_constants.each { |const| remove_constant const } end
autoloaded_constants
は上述のload_file
でロードされた定数を含む配列だ。remove_constant
はその名の通り定数を削除するメソッドで内部でModule#remove_const
を呼んでいる。
RubyのWebSocketサーバー「pingpong」を作った
最近、「Working with TCP Sockets」って本を読んだ。Rubyでソケットと戯れつつ、7つくらいのWebサーバーのアーキテクチャを概観できるいい本だった。で、その中にイベント駆動モデルの実装とかノンブロッキングIOの実装について紹介されてて面白かったので、練習がてらWebSocketサーバーを作ることにした。
PingPong
https://github.com/naoty/pingpong
卓球ハウスっぽい名前にした。数日で作ったので、他のクライアントへpush通知を行うことしかできない。たぶん大きいデータも送れない気がする。
WebSocketサーバーの実装とは
まずはRFC 6455のサーバーに関する部分を読んだ。最低限必要な部分をRubyで実装していった。例えば、以下のコードはHandshake(websocket接続の確立)の際にサーバーがクライアントに返すレスポンスヘッダーを作っている。
def response_headers [ ["Upgrade", "websocket"], ["Connection", "Upgrade"], ["Sec-WebSocket-Accept", signature] ] end def signature value = @header["Sec-WebSocket-Key"] + "258EAFA5-E914-47DA-95CA-C5AB0DC85B11" hash = Digest::SHA1.digest(value) Base64.strict_encode64(hash) end
ご覧のとおりハードコーディングがたくさん出てくる。RFCを読むと、このヘッダーにはこの値を入れなさいって書いてあることが多い。なので、それぞれの値の意味はわからないけどとりあえずRFCに従ってハードコーディングしている。signature
というメソッドはあるヘッダーの値をRFCで以下のように定められた形式で生成している。(余談だけど、ここでBase64.encode64
を使って小1時間ハマった。これは改行コードを入れるためここでは使えない。)
A |Sec-WebSocket-Accept| header field. The value of this header field is constructed by concatenating /key/, defined above in step 4 in Section 4.2.2, with the string "258EAFA5-E914-47DA-95CA-C5AB0DC85B11", taking the SHA-1 hash of this concatenated value to obtain a 20-byte value and base64-encoding (see Section 4 of [RFC4648]) this 20-byte hash.
イベント駆動モデルとノンブロッキングIO
push通知はイベント駆動モデルというアーキテクチャを使って実装した。イベント駆動モデルはマルチプロセスやマルチスレッドとは違ってシングルスレッドで多数のリクエストを並行処理する。具体的には、websocket接続の確立に成功したソケットを配列に入れておき、ループ内でそれらのソケットにread/writeしていく。このとき、read/writeがブロッキングしてしまうとすべての処理がそこで止まってしまうので、read/writeの前にselect(2)
等を使ってread/write可能なソケットだけ選択してread/writeを行う。これがノンブロッキングIOだと思う(だよね…?)。
実際のコードは以下の通り。
def start @sockets = {} @message_queue = [] loop do to_read = @sockets.values << @server to_write = @sockets.values readables, writables, _ = IO.select(to_read, to_write) readables.each do |socket| if socket == @server establish_connection else begin request = socket.read_nonblock(CHUNK_SIZE) message = Frame::Request.new(request).message # the message may be passed to a web application. @message_queue << Message.new(socket.fileno, message) rescue EOFError @sockets.delete(socket.fileno) end end end message = @message_queue.shift next if message.nil? || message.empty? writables.each do |socket| if socket.fileno != message.from data = Frame::Response.new(message.body).data socket.write_nonblock(data) end end end end
感想
WebSocket、イベント駆動モデル、ノンブロッキングIO…という言葉はよく耳にしてきたけど理解したとは言えなかった。実際にWebSocketサーバーを書いてみると、コードに基づいて何が行われているのか正確に理解することができた。push通知も何やら凄そうな響きがするけど、実際に実装してみると特に難しいことはしていなかった。また、websocketの弱点と言われている、CPUヘビーな処理がなぜ弱点なのかも合点がいった。シングルスレッドで処理しているので、例えばレンダリングのような重い処理がひとつでも走ると、全体に悪影響が出るということだと理解した。
mrb_valueについて調べてみた
昨日の続き。
mrubyのソースコードを読むと、mrb_value
という構造体がよく出てくるのでソースコードを追いかけて使い方を調べてみた。参照しているコミット番号は昨日と同じく「9663a7」です。
mrb_valueの定義
// include/mruby/value.h:40 typdef struct mrb_value { union { mrb_float f; void *p; mrb_int i; mrb_sym sym; } value; enum mrb_vtype tt; } mrb_value;
mrb_value
構造体は値とその値のデータ型をもつ。enum mrb_vtype
にはMRB_TT_FIXNUM
とかMRB_TT_STRING
などが入る。value
とtt
は適切な組み合わせにする必要があるはず。
mrb_value
はMRB_NAN_BOXING
が定義されているかどうかでその定義が変わるんだけど、MRB_NAN_BOXING
はmrbconf.hでコメントアウトされていたので、mrb_value
のデフォルトの定義は上のようになる。
// include/mrbconf.h:23 /* represent mrb_value in boxed double; conflict with MRB_USE_FLOAT */ //#define MRB_NAN_BOXING
どういうときにこれを使うのかはまだよくわかってない。
mrb_valueとデータ型の変換
int型、char型などとmrb_value
を変換する方法も調べた。まず、変換する関数によく使われているmrb_value
構造体に値をセットするマクロがある。
// include/mruby/value.h:53 #define MRB_SET_VALUE(o, ttt, attr, v) do {\ (o).tt = ttt;\ (o).attr = v;\ } while (0)
これを使って変換する関数が実装されているっぽい。とりあえず見つけたのは以下の通り。
int
-> mrb_value
// include/mruby/value.h:205 static inline mrb_value mrb_fixnum_value(mrb_int i) { mrb_value v; MRB_SET_VALUE(v, MRB_TT_FIXNUM, value.i, i); return v; }
mrb_value
-> int
// include/mruby/value.h:145 #define mrb_fixnum(o) (o).value.i
float
-> mrb_value
// include/mruby/value.h:58 static inline mrb_value mrb_float_value(mrb_float f) { mrb_value v; MRB_SET_VALUE(v, MRB_TT_FLOAT, value.f, f); return v; }
mrb_value
-> float
// include/mruby/value.h:51 #define mrb_float(o) (o).value.f
char[]
-> mrb_value
// src/string.c:670 char * mrb_string_value_ptr(mrb_state *mrb, mrb_value ptr) { mrb_value str = mrb_str_to_str(mrb, ptr); return RSTRING_PTR(str); }
char[]
-> mrb_value
// src/string.c:232 mrb_value mrb_str_new(mrb_state *mrb, const char *p, size_t len) { struct RString *s; s = str_new(mrb, p, len); return mrb_obj_value(s); }
mrubyで定義したクラスとメソッドをCから呼び出す
mrubyで書いた方がいいところはmrubyで書いてそうじゃないところはCで書く、という開発をするには、Cで定義した関数をRubyから実行させたり、逆にRubyで定義したクラスやメソッドをCから呼び出せるようにする必要があると思った。前者のような実装はmrbgemsを読めばたくさんある一方で、後者の実装は調べたけどあんまりなかった。そこで、先日「Head First C」でCの初歩を学んだことだし、mrubyのソースコードを読みながら後者の「mrubyで定義したクラスとメソッドをCから呼び出す」実装を試行錯誤してみた。
試行錯誤してみてとりあえず動いたというだけで、正しいやり方じゃないかもしれないので、コメントか@naoty_k宛にメッセージをいただけるとありがたいです。また、参照しているmrubyのコミット番号は「9663a7」です。
Rubyのクラスとメソッドを用意
適当にPersonクラスとメソッド2つを用意する。あとでこれらをCから呼び出す。
// person.rb class Person attr_accessor :name, :age def initialize(name, age) @name = name @age = age end def greeting "Hello, my name is #{name}, #{age} years old." end end
mrbcでコンパイル
RubyのファイルをCからロードするにはいくつか方法があるようだけど、今回はmrbcで*.mrb形式にコンパイルしてCからロードするようにする。
$ ls person.rb $ mrbc person.rb $ ls person.mrb person.rb
Cから定義したクラスとメソッドを呼び出す
CからRubyで定義したPerson
インスタンスを生成してgreeting
メソッドの結果を標準出力に表示してみる。
// greeting.c #include <stdio.h> #include <mruby.h> #include <mruby/string.h> int main() { mrb_state* mrb = mrb_open(); // mrubyファイルをロードする FILE *fd = fopen("person.mrb", "r"); mrb_load_irep_file(mrb, fd); // クラスオブジェクトを取得する struct RClass *person = mrb_class_obj_get(mrb, "Person"); // 引数をmrb_valueに変換する mrb_value person_value = mrb_obj_value(person); mrb_value name_value = mrb_str_new(mrb, "naoty", 5); mrb_value age_value = mrb_fixnum_value(25); // Person#newを呼び出す mrb_value naoty = mrb_funcall(mrb, person_value, "new", 2, name_value, age_value); // Person#greetingを呼び出す mrb_value greeting_value = mrb_funcall(mrb, naoty, "greeting", 0); // 返り値をchar*に変換して出力する char *greeting = mrb_string_value_ptr(mrb, greeting_value); printf("%s\n", greeting); mrb_close(mrb); return 0; }
- *.mrb形式のファイルをロードするには
mrb_load_irep_file()
を実行する。 - 次にクラスを取得するには
mrb_class_obj_get()
を実行し、メソッドを呼び出すにはmrb_funcall()
を実行する。 mrb_funcall()
には、第2引数にメソッドのレシーバ、第3引数にメソッド名、第4引数にメソッドの引数の数、第5引数以降にはメソッドの引数を渡す。第2引数と第5引数以降はint
やchar*
などをそのまま渡すことはできなくて、mrb_value
という構造体に変換する必要がある。変換するための関数については長くなりそうなので、別の記事にしようと思う。mrb_funcall()
の返り値もmrb_value
構造体なので、標準出力をするためにchar*
に変換する。
Cをコンパイルして実行
Cのソースコードをmrubyのヘッダーファイルやスタティックライブラリと一緒にコンパイルする。僕の環境だと以下のコマンドでコンパイルできた。
$ gcc -I ~/mruby/include greeting.c ~/mruby/build/host/lib/libmruby.a -lm -o greeting $ ./greeting Hello, my name is naoty, 25 years old.
greeting.cはperson.mrbに依存し、person.mrbはperson.rbに依存しているので、一連のビルドはMakefileかRakefileで自動化したほうがいいと思う。
// Rakefile require "rake/clean" CC = "gcc" MRBC = "mrbc" CLEAN.include("person.mrb") CLOBBER.include("greeting") task default: "greeting" file "greeting" => ["greeting.c", "person.mrb"] do |t| sh "#{CC} -I ~/mruby/include #{t.prerequisites[0]} ~/mruby/build/host/lib/libmruby.a -lm -o #{t.name}" end file "person.mrb" => ["person.rb"] do |t| sh "#{MRBC} #{t.prerequisites[0]}" end
$ rake $ ./greeting
参考
@naoty_k クラスの取り出しはmrb_class_obj_get()、メソッドの呼び出しはmrb_funcall()を使ってください。funcallには派生形あり。
— Yukihiro Matsumotoさん (@yukihiro_matz) 2013年4月30日
Railsに組み込むgemを作るためのTips
params_inquirerというgemを作りました。何ができるかと言うと、文で説明するのがなかなか難しいので、下のコードを見てください。
# users_controller.rb def index if params[:status].accepted? # params[:status] == 'accepted' と同じ @users = User.accepted elsif params[:status].rejected? # params[:status] == 'rejected' と同じ @users = User.rejected else @users = User.all end end
params_inquirerを使うと上のaccepted?
のようなメソッドがparams
に対して呼べるようになります。すでにrubygemsで公開してるので、ちょっと試してみたい場合は、irbで試してもらうこともできます。
$ gem install params_inquirer $ irb irb > require 'params_inquirer' irb > params = ParamsInquirer::Parameters.new({ name: 'naoty' }) irb > params[:name].naoty? => true
params
の中身を文字列で比較するのがなんとなくダサいと感じていたので、作ってみました。あとは、Railsの中身について勉強してみたかったというのもあります。
Railsに組み込みgemを作るにあたって知っておいた方がいいポイントについてまとめてみます。
Bundlerでgemのひな形を作る
gemを作るとき、まず最初にBundlerを使ってgemのひな形を作ります。
$ gem install bundler $ bundle gem params_inquirer
これでgemのひな形ができます。作ったgemをローカル環境にインストールしたりrubygems.orgにリリースするためのRaketaskもここに含まれるので、かなり便利です。
Bundlerを使ったgemの開発についてはこの記事を参考にしました。
Railtie
Railtieは、Railsを起動するときにgemのコードをActionController::Base
にinclude
させるために使いました。これによって、自分のgemをRailsアプリケーションに組み込むことができます。
下のコードでは、Railsプロセスが起動するときにinitializer
ブロック内の処理が実行されて、自分で作ったParamsInquirer::ActionController::Base
がActionController::Base
にinclude
されるようになります。
# lib/params_inquirer/railtie.rb require 'params_inquirer/action_controller/base' module ParamsInquier class Railtie < ::Rails::Railtie initializer 'Initialize params_inquirer' do ::ActionController::Base.send :include, ParamsInquirer::ActionController::Base end end end
ただ、このファイルがRails起動時にrequire
されている必要があります。
インストールされたgemをrequire
するときlib/<gem_name>.rb
がrequire
されます。このgemであればlib/params_inquirer.rb
です。なので、ここでrailtieをrequire
しておく必要があります。
# lib/params_inquirer.rb if defined?(Rails) require 'lib/params_inquirer/railtie' else require 'lib/params_inquirer/parameters' end
require 'params_inquirer'
が実行されるとこのファイルが実行されます。もしRailsアプリケーション内であればrailtieをrequire
し、最初に見せたirbのような場合は必要なファイルだけrequire
するようにしています。
以上のようすることで、Rails起動時にrailtieをrequire
しrailtieから自分で作ったコードをRailsアプリケーション内にinclude
させることができました。
ActiveSupport::Concern
ここからは実際に使ったというよりは、actionpackやactivesupportなどのgemを読んでいくときに必要になったtipsです。
include
したモジュールを使ってクラスメソッドをmixinしたい場合、下のようにModule#.included
をオーバーライドしその中で内部モジュールをextend
するテクニックが定石みたいです。
module M def self.included(base) # extendによってクラスメソッドとしてmixinされる base.extend ClassMethods scope :disabled, where(disabled: true) end # クラスメソッドを定義する内部モジュール module ClassMethods ... end end
上のようなコードはActiveSupport::Concern
を使うと下のように書けます。
module M extend ActiveSupport::Concern included do scope :disabled, where(disabled: true) end module ClassMethods ... end end
一見すると、ClassMethods
モジュールがextend
されていないように見えますが、内部的にClassMethods
という名前のモジュールがextend
されます。「設定より規約」に従ってるんだと思います。
これを知らないと、クラスメソッドがextend
されていることに気づきにくいかもしれないです。また、ActiveSupport::Concern
はいろんなところに頻出するので、知っておいた方がいいと思いました。
ActiveSupport::Autoload
ActiveSupport::Autoload#autoload
はModule#autoload
の拡張で、Module#autoload
は必要なファイルを必要なタイミングでrequire
するメソッドです。
autoload(:Hoge, 'hoge') # 'hoge.rb'はこの時点ではrequireされていない p Hoge # ここで'hoge.rb'がrequireされる
ActiveSupport::Autoload#autoload
は、「Hoge
はhoge.rbにあるはず」という「設定より規約」に従って、Module#autoload
の第2引数を省略できるメソッドなので、上のコードは下のように書けます。
extend ActiveSupport::Autoload autoload :Hoge p Hoge # ここで'hoge.rb'がrequireされる
これもファイル名が省略されているということを知らないと、どのファイルをrequire
しているか見えづらいと思います。
最後に
あまりまとまらなくてすごい量になってしまいました。簡単なgemを作るのに知っておくべきことがいろいろあって大変でした。間違っていることがあれば修正しますので、コメントいただけると助かります。また、params_inquirerもまだ未完成なので、pull requestも待ってます。
最近のテスト事情
むかしに比べると、かなりテストが書けるようになってきたし、TDDもだんだん慣れてきた。最近テスト書いてて便利だと思ったことについてメモっておく。
スタブを使ってbefore_filterをスキップする
describe 'GET index' do context 'ログインしている場合' do before(:each) do controller.any_instances.stub(:authenticate_user).and_return(true) end it 'hogehogeなfugafugaを取得する' do get index, params assigns[:fugafuga].should be_hogehoge end end end
ログイン判定のような、リクエストをはじく処理をbefore_filter
で実装することがよくあるけど、そういうコントローラーをテストする場合、スタブが便利だということにようやく気づいた。スタブによって、メソッドの中身をごまかして好きな値を返すようにできる。だから、before_filter
をスキップしたい場合は、とにかくスタブしてtrue
を返すようにしとけばいい。skip_before_filter
でもスキップすることはできるけど、僕はスタブを使う方が好み。
FactoryGirlを使いこなす
FactoryGirl.define do factory :user do # 連番を使えばuniquenessのバリデーションにかからなくなる sequence(:name) {|n| "user #{n}" } sequence(:email) {|n| "user#{n}@example.com" } age { rand(18..30) } after(:build) do |user| # 余計なデータを作るコールバックがあればスキップできる User.skip_callback(:after, :create, :create_data) end # ネストしたfactoryで上書きできる factory :naoty do name 'naoty' email 'naoty@example.com' age 18 end # traitで属性のグループに名前をつけられる trait :resigned do resigned_at { Time.now } end end end user = FactoryGirl.create(:user) p user.name #=> "user 1" naoty = FactoryGirl.create(:naoty) p naoty.name #=> "naoty" resigned_user = FactoryGirl.create(:user, :resigned) p resigned_user.name #=> "user 2" p resigned_user.resigned_at #=> "2013-01-19 00:43:59 +0900"
factory_girl
はテスト用のデータを簡単につくるためのgem。似たようなgemは他にもあるけど、こういうgemを使うと、テストデータを作るロジックとテストコードを分離できる。なので、いろんなテストで使われるテストデータを重複なく簡単に作ることができる。
FactoryGirlでテストデータを作成するときに、よくひっかかるのがバリデーションやafter_save
などのコールバック内の余計な処理だと思う。こういう鬱陶しい処理は、FactoryGirlのコールバックを使ってスキップしてる。
trait
は特殊なデータを作る場合にすごく役に立つ。上記の例のような「退会ユーザー」をテストに使いたいときなど、特殊なデータの属性をひとまとめにしてFactoryGirl.create(:user, :resigned)
のように簡単に作成できる。
changeマッチャが便利
describe '#resign' do let(:user) { FactoryGirl.create(:user) } it 'resigned_atを更新する' do lambda { user.resign }.should change(user, :resigned?).from(false).to(true) end end
モデルの更新系のメソッドをテストするとき、change
マッチャが非常に便利。上の例で言うと、user.resigned?
の結果がlambda内の処理を実行した前後でfalse
からtrue
に変わることをテストしている。