Rails 关于查看 Rails 动态生成的函数源码的问题

ery · 2012年02月27日 · 最后由 hhuai 回复于 2012年02月29日 · 5371 次阅读

我的目的之一

想知道 Rails 动态生成的函数的源码是什么。 比如在路由中定义以下代码:

resources :posts

Rails 会动态生成一些函数, 比如posts_path这个函数。

我目前的解决方案

我发现 Method Class 提供了一下些方法,但是就是没有函数的源码。 后来我发现了一个 gem 叫做 ParseTree 可以实现以下效果:

class PostsController
  def new
    method = PostsController.instance_method(:posts_path)
    puts method.to_ruby
  end
end

log 输出如下:

proc { |*args|
  options = hash_for_posts_path(args.extract_options!)
  if args.any? then
    options[:_positional_args] = args
    options[:_positional_keys] = [:format]
  end
  url_for(options)
}

我的问题

目前 ParseTree 不支持 Ruby 1.9。 有没有同学,研究过这类问题,请给点建议。 有没有比 ParseTree 更好的解决方案?

我的主要目的是

通过查看动态生成的函数的源码,更好的理解函数的使用。

关于动态生成函数,开始挺快乐,后来很痛苦

Rails 中有很多函数都是动态生成的, 如果我没记错的话,@ashchan在一次关于元编程的演讲中说, rails 内部对元编程的使用达到了奇淫技巧,令人发指的程度。

动态生成函数,有很多好处,不用我多说, 但同时也给我们带来了很多麻烦,

起初,我们使用 url_path 等 rails 动态生成的函数的时候, 感觉很快乐,都是动态生成的,觉得 rails 很酷, 但后来,可能因为某次调用失败,于是你开始想知道, 这个函数到底该如何使用,这个函数的源码是什么,他到底做了什么的时候,你就会很痛苦。 因为,我们无法简单地,快速地,直接地,查看这些动态生成的函数的源码,

  • 第一 代码阅读工具失效 我们无法通过 ctags 之类的工具,阅读动态生成的代码。

  • 第二 源码极其难懂 即使,你通过Method#source_location方法, 找到了,动态生成的函数的定义处,想彻底的理解那些 maigc code,也是非常的痛苦。

  • 第三 没有官方文档 动态生成的函数,通常也有没有官方文档。 没有固定的函数名,怎么写文档啊。 没有文档和源码,你无法知道你的用法是否正确,只能 run 一下试试。

无法简单直接的阅读和理解,动态生成的函数的源码,让我们感觉很不爽。

有同感,同问。

其实也不难追踪啊。你想看 posts_path 是怎么定义的,那就去翻 ActionDispatch::Routing::Mapper::Resources源码

至于出错时的调试,我觉得 stack trace 提供的信息已经足够定位出错的地方了……

@ery 要不你试一下pry,它有个edit-method,能直接编辑方法,不知对动态生成的是否有效。在上班,晚上回家我试试。

是的,我当时是这么说的。原因有很多,其中两条是:

  1. 找/读源码会困难的多
  2. 大部分动态生成的方法没有相应的 respond_to? 实现,导致与 Ruby 的自然风格不符(你查接口它说没有某方法,但调用该方法却可以成功)。

#4 楼 @ashchan

respoind_to? 是个比较容易被忽视的问题,我是看 Meteprogramming Ruby 的时候才意识到。ActiveRecord 貌似有 override 掉 respond_to?

#3 楼 @camel 实验结果:


[2] pry(main)> class Test
[2] pry(main)*   %w(method_a method_b method_c).each do |name|
[2] pry(main)*     define_method name do
[2] pry(main)*       puts "I'm in method #{name}"
[2] pry(main)*     end  
[2] pry(main)*   end  
[2] pry(main)* end  
=> ["method_a", "method_b", "method_c"]
[3] pry(main)> Test.new.respond_to? :method_a
=> true
[4] pry(main)> Test.new.respond_to? :method_b
=> true
[5] pry(main)> Test.new.respond_to? :method_c
=> true
[6] pry(main)> show-source Test.new.meth
Test.new.method           Test.new.method_c         Test.new.methods          
Test.new.method_a         Test.new.method_defined?  Test.new.meths            
Test.new.method_b         Test.new.method_missing   Test.new.meths=           
[6] pry(main)> show-source Test.new.method_a

From: (pry) @ line 3:
Number of lines: 3
Owner: Test
Visibility: public

define_method name do
  puts "I'm in method #{name}"
end
[7] pry(main)> 

#5 楼 @iwinux 应该吧。放到当时,rails 代码中有大量 eval,这种是最麻烦的:)

debug 一跟踪就知道了,动态性增加了看代码的难度。

在使用 eval 时,通常 ruby 里会这样写

class_eval <<-RUBY_EVAL, __FILE__, __LINE__ + 1

那莫出错与 debugger 都可以看到 代码及位置

#9 楼 @hysios 是哇 像 rails 这样的项目 直接看源码和看出错的错误信息都很容易追踪的

#6 楼 @iwinux 这个 pry 取到的方法没什么意义呀 元编程最 magic 的是 self、scope 和 definee。没有这些,只看到一个方法是很难的,除非代码非常简单。

#3 楼 @camel 谢谢你的建议, 不过,使用edit-method ApplicationController#posts_path将定位到以下位置: 和Method#source_location的效果是一样的, 估计pryedit-method就是用Method#source_location实现的。

~/.rvm/gems/ruby-1.9.3-p0@ab/gems/actionpack-3.2.1/lib/action_dispatch/routing

182  # Create a url helper allowing ordered parameters to be associated
183  # with corresponding dynamic segments, so you can do:
184  #
185  #   foo_url(bar, baz, bang)
186  #
187  # Instead of:
188  #
189  #   foo_url(:bar => bar, :baz => baz, :bang => bang)
190  #
191  # Also allow options hash, so you can do:
192  #
193  #   foo_url(bar, baz, bang, :sort_by => 'baz')
194  #
195  def define_url_helper(route, name, kind, options)
196    selector = url_helper_name(name, kind)
197    hash_access_method = hash_access_name(name, kind)
198              
199    @module.module_eval <<-END_EVAL, __FILE__, __LINE__ + 1
200      remove_possible_method :#{selector}
201      def #{selector}(*args)
202        url_for(#{hash_access_method}(*args))
203      end
204    END_EVAL
205    helpers << selector
206  end

这种效果实现了定位到有关代码,但是没有展示出动态代码的函数内容, 没有达到类似与Passtree的效果。

#2 楼 @iwinux 通过阅读 ActionDispatch::Routing::Mapper::Resources 源码,的确可以知道 posts_path 是怎么定义的。但是时间成本颇高,而且这个解决方案不具备通用性,比如通过Mapper::Resources 源码你依然不知道 Customers.find_by_name 的函数体是如何实现的。

我的主要目的是 通过查看动态生成的函数的源码,更好的理解函数的使用。

这里所说的函数的源码,不是指生成动态函数的源码,而是指被动态生成的函数的源码,比如posts_path的源码如下:

proc { |*args|
  options = hash_for_posts_path(args.extract_options!)
  if args.any? then
    options[:_positional_args] = args
    options[:_positional_keys] = [:format]
  end
  url_for(options)
}

#12 楼 @ery 以下是我的瞎扯淡 理论上我觉得是可以实现的,如果从编译后生成的中间字节码,逆向过来应该是可以还原到不管你是不是动态生成,或是哪里 monkey patch 过了的 ruby 代码。 不知道我说清楚没有,就是运行期间动态调试,逆向过来。

设想 ruby-debug 提供一个功能,在当前的 frame 或 context show-method "".contact 之类,这个功能会真正的 call 一下 contact 函数,然后取到 contact 的函数入口,把这段字节码取出来,逆向回来。

需要 登录 后方可回复, 如果你还没有账号请 注册新账号