👉

Ruby でオブジェクトが変更されそうなときに発火するTracePointを定義する

2025/04/03に公開

Ruby でオブジェクトに破壊的変更がされたものを集めて調査をしたくなった。そのために、破壊的変更がされそうなときに発火するTracePointを定義してそれを使うことにした。

Patch

Ruby に次のパッチを当てた。

パッチ全文
diff --git a/error.c b/error.c
index 1a8a4cd430..273f836ad7 100644
--- a/error.c
+++ b/error.c
@@ -1931,6 +1931,23 @@ rb_get_backtrace(VALUE exc)
     return rb_check_backtrace(info);
 }
 
+/** @alias{rb_check_frozen} */
+static inline void
+rb_check_frozen_inline(VALUE obj)
+{
+    if (RB_UNLIKELY(RB_OBJ_FROZEN(obj))) {
+        rb_error_frozen_object(obj);
+    }
+
+    /* ref: internal CHILLED_STRING_P()
+       This is an implementation detail subject to change. */
+    if (RB_UNLIKELY(RB_TYPE_P(obj, T_STRING) && FL_TEST_RAW(obj, RUBY_FL_USER2 | RUBY_FL_USER3))) { // STR_CHILLED
+        rb_str_modify(obj);
+    }
+
+    EXEC_EVENT_HOOK(GET_EC(), RUBY_EVENT_OBJECT_MODIFIED, obj, 0, 0, 0, Qnil);
+}
+
 /*
  *  call-seq:
  *    backtrace_locations -> array or nil
diff --git a/include/ruby/internal/event.h b/include/ruby/internal/event.h
index 1d194ed618..3491c3f813 100644
--- a/include/ruby/internal/event.h
+++ b/include/ruby/internal/event.h
@@ -59,6 +59,7 @@
 #define RUBY_EVENT_FIBER_SWITCH      0x1000 /**< Encountered a `Fiber#yield`. */
 #define RUBY_EVENT_SCRIPT_COMPILED   0x2000 /**< Encountered an `eval`. */
 #define RUBY_EVENT_RESCUE            0x4000 /**< Encountered a `rescue` statement. */
+#define RUBY_EVENT_OBJECT_MODIFIED   0x8000 /**< Encountered an object modification. */
 #define RUBY_EVENT_TRACEPOINT_ALL    0xffff /**< Bitmask of extended events. */
 
 /** @} */
diff --git a/include/ruby/internal/intern/error.h b/include/ruby/internal/intern/error.h
index 1fd9ec2f51..ec6b1d7101 100644
--- a/include/ruby/internal/intern/error.h
+++ b/include/ruby/internal/intern/error.h
@@ -248,25 +248,6 @@ RBIMPL_SYMBOL_EXPORT_END()
  */
 #define rb_check_frozen_internal rb_check_frozen
 
-/** @alias{rb_check_frozen} */
-static inline void
-rb_check_frozen_inline(VALUE obj)
-{
-    if (RB_UNLIKELY(RB_OBJ_FROZEN(obj))) {
-        rb_error_frozen_object(obj);
-    }
-
-    /* ref: internal CHILLED_STRING_P()
-       This is an implementation detail subject to change. */
-    if (RB_UNLIKELY(RB_TYPE_P(obj, T_STRING) && FL_TEST_RAW(obj, RUBY_FL_USER2 | RUBY_FL_USER3))) { // STR_CHILLED
-        rb_str_modify(obj);
-    }
-}
-
-/* rb_check_frozen() is available as a symbol, but have
- * the inline version take priority for native consumers. */
-#define rb_check_frozen rb_check_frozen_inline
-
 /**
  * Ensures that the  passed integer is in  the passed range.  When  you can use
  * rb_scan_args() that is preferred over this one (powerful, descriptive).  But
diff --git a/vm_trace.c b/vm_trace.c
index 81fa6458b7..2e71fd08e5 100644
--- a/vm_trace.c
+++ b/vm_trace.c
@@ -656,6 +656,7 @@ get_event_name(rb_event_flag_t event)
       case RUBY_EVENT_C_CALL:	return "c-call";
       case RUBY_EVENT_C_RETURN:	return "c-return";
       case RUBY_EVENT_RAISE:	return "raise";
+      case RUBY_EVENT_OBJECT_MODIFIED: return "object-modified";
       default:
         return "unknown";
     }
@@ -683,6 +684,7 @@ get_event_id(rb_event_flag_t event)
         C(fiber_switch, FIBER_SWITCH);
         C(script_compiled, SCRIPT_COMPILED);
         C(rescue, RESCUE);
+        C(object_modified, OBJECT_MODIFIED);
 #undef C
       default:
         return 0;
@@ -824,6 +826,7 @@ symbol2event_flag(VALUE v)
     C(fiber_switch, FIBER_SWITCH);
     C(script_compiled, SCRIPT_COMPILED);
     C(rescue, RESCUE);
+    C(object_modified, OBJECT_MODIFIED);
 
     /* joke */
     C(a_call, A_CALL);

簡単な解説。

  • Ruby ではオブジェクトに破壊的変更が加えられる前にrb_check_frozen関数が呼ばれる
    • オブジェクトがfreezeされているかどうかを調べて、freezeされていたらFrozenErrorを投げる関数
    • ということで、ここにイベント発火を仕込んだ。
    • error.hからerror.cに実装を移しているのは、EXEC_EVENT_HOOKマクロを呼び出すため。ヘッダファイルからvm_core.hをincludeしても良かったかも知れない(よくわかってない)
  • event.hに新しいTracePointのイベントを定義
  • vm_trace.cをいくらか修正して、新しく追加したイベントを理解するようにした。

これだけで新しいTracePointのイベントが追加できた。

なお実装はChatGPTに聞いたら7割ぐらいは出てきたので、そこから適当に修正をした。

実行結果

次のようなコードが動くようになる。

trace = TracePoint.new(:object_modified) do |tp|
  p tp
end

trace.enable

[] << 'a'

# => #<TracePoint:object_modified test.rb:7>

あとはこのイベントを使って、好きなように調査をすれば良い。

最後に

一応実用的なプログラムでも動作することは確認済み。
とはいえ雑に書いたものなので、網羅性は保証できないしSEGVしないとも言い切れない。

次のステップとして、この出力結果を使って問題の改善に取り組みたい。

Money Forward Developers

Discussion