@@ -1,39 +1,37 @@
---
-title: set_trace_func 和骗局
+title: Ruby 里 set_trace_func 歪曲真相的欺骗性行为
layout: post
permalink: /html/set_trace_func.html
-yourls_shorturl:
- - http://miv.im/t05c
-categories:
- - Ruby Sugar
-tags:
- - Ruby
- - 翻译
---
-<div class='et-box et-info'>
- <div class='et-box-content'>
- 本文是对 http://t-a-w.blogspot.jp/2007/04/settracefunc-smoke-and-mirrors.html 的粗略翻译,如有错误请务必指正。
- </div>
-</div>
-
-Ruby 解释器提供了可用于追踪 Ruby 执行的事件钩子。设置钩子用 `set_trace_func(some_proc)`;卸载钩子则用 `set_trace_func(nil)`。
-
-追踪器恐怕是发明以来最有用的调试工具,远比基于单步和断点的调试器有用。
-
-Ruby 追踪 8 种事件:
-
- * `line` – 文件或行改变
- * `class` – 模块或类定义开始
- * `end` – 模块或类定义结束
- * `call` – Ruby 方法调用
- * `return` – Ruby 方法返回
- * `c-call` – C 方法调用
- * `c-return` – C 方法返回
- * `raise` – 异常抛出
-
-有些事件并不会被追踪,例如设置和读取变量(局部、全局、实例或者类变量)。
-
-<pre><code class="lang-ruby">set_trace_func proc { |event, file, line, id, binding, classname|
+**本文是对 http://t-a-w.blogspot.jp/2007/04/settracefunc-smoke-and-mirrors.html 的粗略翻译,如有错误请务必指正。**
+
+Ruby 解释器提供了可用于追踪代码执行的事件钩子。用 `set_trace_func(some_proc)` 可以设置钩子,用 `set_trace_func(nil)` 卸载钩子。这个东西,有时候甚至远比基于单步和断点的调试器有用。
+
+Ruby 会追踪这 8 种事件:
+
+<dl>
+ <dt>line</dt>
+ <dd>文件或行改变</dd>
+ <dt>class</dt>
+ <dd>模块或类定义开始</dd>
+ <dt>end</dt>
+ <dd>模块或类定义结束</dd>
+ <dt>call</dt>
+ <dd>Ruby 方法调用</dd>
+ <dt>return</dt>
+ <dd>Ruby 方法返回</dd>
+ <dt>c-call</dt>
+ <dd>C 方法调用</dd>
+ <dt>c-return</dt>
+ <dd>C 方法返回</dd>
+ <dt>raise</dt>
+ <dd>异常抛出</dd>
+</dl>
+
+并不是所有事件都会被追踪,例如设置和读取变量(局部、全局、实例或者类变量)就不行。
+
+```ruby
+set_trace_func proc { |event, file, line, id, binding, classname|
STDERR.printf "%8s %s:%-2d %10s %8s\n", event, file, line, id, classname
}
@@ -41,9 +39,11 @@ Ruby 追踪 8 种事件:
separator = ", "
$object = "world"
@@excl = "!"
-puts(@welcome + separator + $object + @@excl)</code></pre>
+puts(@welcome + separator + $object + @@excl)
+```
-<pre><code class="lang-bash">$ ./example-1.rb
+```bash
+$ ./example-1.rb
line ./example-1.rb:7 false
line ./example-1.rb:8 false
line ./example-1.rb:9 false
@@ -60,31 +60,37 @@ c-return ./example-1.rb:11 + String
c-return ./example-1.rb:11 write IO
c-call ./example-1.rb:11 write IO
c-return ./example-1.rb:11 write IO
-c-return ./example-1.rb:11 puts Kernel</code></pre>
+c-return ./example-1.rb:11 puts Kernel
+```
例子中,`String#+` 运行了三次,`IO#write` 两次——第二次大概是输出换行符的。
Ruby 中很多东西很神奇并没有被转换为方法调用。这种东西的一种是字符串插值(`... #{ code } ...`),并没有被转换为 `String#+` 或是 `Array#join`,而是直接由解释器特别的处理。
-<pre><code class="lang-ruby">set_trace_func proc { |event, file, line, id, binding, classname|
+```ruby
+set_trace_func proc { |event, file, line, id, binding, classname|
STDERR.printf "%8s %s:%-2d %10s %8s\n", event, file, line, id, classname
}
@welcome = "Hello"
$object = "world"
-puts("#{@welcome}, #{$object}\n")</code></pre>
+puts("#{@welcome}, #{$object}\n")
+```
-<pre><code class="lang-bash">$ ./example-2.rb
+```bash
+$ ./example-2.rb
line ./example-2.rb:7 false
line ./example-2.rb:8 false
line ./example-2.rb:9 false
c-call ./example-2.rb:9 puts Kernel
c-call ./example-2.rb:9 write IO
c-return ./example-2.rb:9 write IO
-c-return ./example-2.rb:9 puts Kernel</code></pre>
+c-return ./example-2.rb:9 puts Kernel
+```
同样,创建类并没有被转化为 `Class#new`,添加方法并没有被转换为 `Module#define_method`。幸运的是 Ruby 提供了特别的钩子让我们需要捕捉这些事件——当类创建时,其父类的 `inherited` 方法会被调用;当一个方法被添加时,Ruby 调用其类的 `method_added`。
-<pre><code class="lang-ruby">set_trace_func proc { |event, file, line, id, binding, classname|
+```ruby
+set_trace_func proc { |event, file, line, id, binding, classname|
STDERR.printf "%8s %s:%-2d %10s %8s\n", event, file, line, id, classname
}
@@ -94,11 +100,13 @@ class Geolocation
@long = long
end
def to_s
- "<#{@lat}, #{@long}>"
+ "<#{@lat}, #{@long}>"
end
-end</code></pre>
+end
+```
-<pre><code class="lang-bash">$ ./example-3.rb
+```bash
+$ ./example-3.rb
line ./example-3.rb:7 false
c-call ./example-3.rb:7 inherited Class
c-return ./example-3.rb:7 inherited Class
@@ -109,11 +117,13 @@ c-return ./example-3.rb:8 method_added Module
line ./example-3.rb:12 false
c-call ./example-3.rb:12 method_added Module
c-return ./example-3.rb:12 method_added Module
- end ./example-3.rb:7 false</code></pre>
+ end ./example-3.rb:7 false
+```
-如果我们想跟踪些有用的东西,是时候改改格式了。如果我们对跟踪执行过程感兴趣,通常来说文件及行号都没什么用,`return/c-return/end` 则应该用缩进代替。同时,相比类,我们更注重的是接收者(\`self)。
+如果我们想跟踪些有用的东西,不如先换一下输出格式。毕竟我们对跟踪执行过程感兴趣,通常来说文件及行号都没什么用,`return/c-return/end` 则应该用缩进代替。同时,相比类,我们更注重的是接收者(self)。
-<pre><code class="lang-ruby">$indent = 0
+```ruby
+$indent = 0
set_trace_func proc { |event, file, line, id, binding, classname|
if event == "line"
# 忽略
@@ -135,39 +145,43 @@ class Geolocation
@long = long
end
def to_s
- "<#{@lat}, #{@long}>"
+ "<#{@lat}, #{@long}>"
end
end
a = Geolocation.new(51.12, 17.03)
puts a
-p a</code></pre>
+p a
+```
-<pre><code class="lang-bash">$ ./example-4.rb >/dev/null
+```
+$ ./example-4.rb > /dev/null
c-call Object.inherited
class Geolocation
c-call Geolocation.method_added
c-call Geolocation.method_added
c-call Geolocation.new
- call <, >.initialize
+ call <, >.initialize
c-call main.puts
- call <51.12, 17.03>.to_s
+ call <51.12, 17.03>.to_s
c-call 51.12.to_s
c-call 17.03.to_s
- c-call #<IO:0xb7c80fc0>.write
- c-call #<IO:0xb7c80fc0>.write
+ c-call #<IO:0xb7c80fc0>.write
+ c-call #<IO:0xb7c80fc0>.write
c-call main.p
- c-call <51.12, 17.03>.inspect
+ c-call <51.12, 17.03>.inspect
c-call 17.03.inspect
c-call 17.03.to_s
c-call 51.12.inspect
c-call 51.12.to_s
- c-call #<IO:0xb7c80fc0>.write
- c-call #<IO:0xb7c80fc0>.write</code></pre>
+ c-call #<IO:0xb7c80fc0>.write
+ c-call #<IO:0xb7c80fc0>.write
+```
-这里的输出又有点小怪癖。在 `Geolocation#initialize` 开始时, `Geolocation#to_s` 就被追踪器调用,而不是在其有意义之后。这种情况下,我们简单的得到了垃圾 `<, >`。但是有些对象,在初始化之前被打印是会产生异常的。
+这里的输出又有点奇怪。在 `Geolocation#initialize` 开始时, `Geolocation#to_s` 就被追踪器调用,而不是在其有意义之后。这种情况下,我们一不小心就得到了垃圾 `<, >`。但是有些对象,在初始化之前被打印是会产生异常的。
-<pre><code class="lang-ruby">require "complex"
+```ruby
+require "complex"
$indent = 0
set_trace_func proc { |event, file, line, id, binding, classname|
@@ -180,7 +194,7 @@ set_trace_func proc { |event, file, line, id, binding, classname|
if event == "class"
STDERR.printf "%*s%s %s\n", $indent, "", event, obj
else
- obj = "<#{obj.class}##{obj.object_id}>" if id == :initialize
+ obj = "<#{obj.class}##{obj.object_id}>" if id == :initialize
STDERR.printf "%*s%s %s.%s\n", $indent, "", event, obj, id
end
$indent += 2 if %w[call c-call class].include?(event)
@@ -189,17 +203,19 @@ set_trace_func proc { |event, file, line, id, binding, classname|
a = Complex.new(11.0, -5.0)
b = Complex.new(2.0, 13.5)
-c = a * b</code></pre>
+c = a * b
+```
-<pre><code class="lang-bash">$ ./example-5.rb
+```bash
+$ ./example-5.rb
c-call Complex.new
- call <Complex#-605829048>.initialize
+ call <Complex#-605829048>.initialize
c-call 11.0.kind_of?
c-call 11.0.kind_of?
c-call -5.0.kind_of?
c-call -5.0.kind_of?
c-call Complex.new
- call <Complex#-605832038>.initialize
+ call <Complex#-605832038>.initialize
c-call 2.0.kind_of?
c-call 2.0.kind_of?
c-call 13.5.kind_of?
@@ -221,33 +237,36 @@ call 11.0-5.0i.*
call 138.5.real
c-call 0.+
c-call Complex.new
- call <Complex#-605842188>.initialize
+ call <Complex#-605842188>.initialize
c-call 89.5.kind_of?
c-call 89.5.kind_of?
c-call 138.5.kind_of?
- c-call 138.5.kind_of?</code></pre>
+ c-call 138.5.kind_of?
+```
-许多难以找到错误的,最终变成了琐碎的自定义追踪器、grep 或是一些一行 Ruby 脚本来美化事实。
+很多平常难以发现的错误,自从有了 set_trace_func、grep 和一些处理日志的单行 Ruby 脚本的帮助,就变得微不足道了……
-## 骗局 {#-}
+## 歪曲真相的欺骗性行为
`set_trace_func` 不仅仅对调试有用,也适合做各种有趣的事情。`Binding.of_caller` 是用 `set_trace_func` 实现的,虽然已经不能用了。[magic/help][1] 用了 `set_trace_func` 在 `irb` 中提供方便的帮助:
- irb> help { STDERR.write }
- --------------------------------------------------------------- IO#write
- ios.write(string) => integer
- ------------------------------------------------------------------------
- Writes the given string to _ios_. The stream must be opened for
- writing. If the argument is not a string, it will be converted to a
- string using +to_s+. Returns the number of bytes written.
-
- count = $stdout.write( "This is a test\n" )
- puts "That was #{count} bytes of data"
-
- _produces:_
-
- This is a test
- That was 15 bytes of data
+```
+irb> help { STDERR.write }
+--------------------------------------------------------------- IO#write
+ ios.write(string) => integer
+------------------------------------------------------------------------
+ Writes the given string to _ios_. The stream must be opened for
+ writing. If the argument is not a string, it will be converted to a
+ string using +to_s+. Returns the number of bytes written.
+
+ count = $stdout.write( "This is a test\n" )
+ puts "That was #{count} bytes of data"
+
+ _produces:_
+
+ This is a test
+ That was 15 bytes of data
+```
`magic/help` 通过设置一个 `set_trace_func` 钩子工作,执行传送过来的块,然后一遇到感兴趣的事件就停止执行。这意味着 `help { rm_rf "/" }` 完全是安全的。
@@ -257,36 +276,42 @@ call 11.0-5.0i.*
要描述这个问题,我们来跟踪一下 C 方法 `STDERR.write`:
- c-call #<IO:0xb7c98fa8>.write
- c-call ArgumentError.new
- c-call #<ArgumentError: ArgumentError>.initialize
- c-return #<ArgumentError: wrong number of arguments (0 for 1)>.initialize
- c-return ArgumentError.new
- c-call #<ArgumentError: wrong number of arguments (0 for 1)>.backtrace
- c-return #<ArgumentError: wrong number of arguments (0 for 1)>.backtrace
- c-call #<ArgumentError: wrong number of arguments (0 for 1)>.set_backtrace
- c-return #<ArgumentError: wrong number of arguments (0 for 1)>.set_backtrace
- raise #<IO:0xb7c98fa8>.write
- c-return #<IO:0xb7c98fa8>.write
+```bash
+c-call #<IO:0xb7c98fa8>.write
+ c-call ArgumentError.new
+ c-call #<ArgumentError: ArgumentError>.initialize
+ c-return #<ArgumentError: wrong number of arguments (0 for 1)>.initialize
+ c-return ArgumentError.new
+ c-call #<ArgumentError: wrong number of arguments (0 for 1)>.backtrace
+ c-return #<ArgumentError: wrong number of arguments (0 for 1)>.backtrace
+ c-call #<ArgumentError: wrong number of arguments (0 for 1)>.set_backtrace
+ c-return #<ArgumentError: wrong number of arguments (0 for 1)>.set_backtrace
+ raise #<IO:0xb7c98fa8>.write
+c-return #<IO:0xb7c98fa8>.write
+```
与此同时我们跟踪一下 Ruby 方法 `FileUtils.remove_entry`:
- # "call FileUtils.remove_entry" 永远不会产生!
- c-call ArgumentError.new
- c-call #<ArgumentError: ArgumentError>.initialize
- c-return #<ArgumentError: wrong number of arguments (0 for 1)>.initialize
- c-return ArgumentError.new
- c-call #<ArgumentError: wrong number of arguments (0 for 1)>.backtrace
- c-return #<ArgumentError: wrong number of arguments (0 for 1)>.backtrace
- c-call #<ArgumentError: wrong number of arguments (0 for 1)>.set_backtrace
- c-return #<ArgumentError: wrong number of arguments (0 for 1)>.set_backtrace
- raise FileUtils.remove_entry
- # "return FileUtils.remove_entry" 确实产生了。
- return FileUtils.remove_entry
+```bash
+# "call FileUtils.remove_entry" 永远不会产生!
+ c-call ArgumentError.new
+ c-call #<ArgumentError: ArgumentError>.initialize
+ c-return #<ArgumentError: wrong number of arguments (0 for 1)>.initialize
+ c-return ArgumentError.new
+ c-call #<ArgumentError: wrong number of arguments (0 for 1)>.backtrace
+ c-return #<ArgumentError: wrong number of arguments (0 for 1)>.backtrace
+ c-call #<ArgumentError: wrong number of arguments (0 for 1)>.set_backtrace
+ c-return #<ArgumentError: wrong number of arguments (0 for 1)>.set_backtrace
+ raise FileUtils.remove_entry
+# "return FileUtils.remove_entry" 确实产生了。
+return FileUtils.remove_entry
+```
两种情况下,参数都是在调用方法之前被检查的。`IO#write` 是定义为一个参数的方法:
-<pre><code class="lang-c">rb_define_method(rb_cIO, "write", io_write, 1);</code></pre>
+```c
+rb_define_method(rb_cIO, "write", io_write, 1);
+```
新版的 `magic/help` 也能处理这个情况了——如果首事件是 `c-call` 到 `ArgumentError.new`,`magic/help` 就等到首个 `return` 事件(如果有)而不是立刻放弃执行传来的块。快去[下载新版][2],享受神奇的帮助吧。