backtracie 0.1.0 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 14ea35393497d5cb266920ef4da5b21b116ef1c4c25624da3c52fcd89dddcd7d
4
- data.tar.gz: 4d954ba9c6527707fcec413b320f6a80fa11a42f19743a94ed79a6d42871b95e
3
+ metadata.gz: b8ef71cf1cfd9ca005e0ecb0fe7434a9c8121bc46fc70c34209c74ecad1c705c
4
+ data.tar.gz: 723d3c66fb919432481428609a8316243e160805ff4142757f9ec65cf254f2d8
5
5
  SHA512:
6
- metadata.gz: 072ac1862f8211ba37358f5d20dbc9ff3ffa176559d50d176d8d05f5e3940b9e18865e4a358b170915afa63980f3811deef6313400219048ce9587259e869a98
7
- data.tar.gz: 13db93efa6360ef1edb00277d7730fd837ba4aca67ca8a1bc8f61ecf8369d35968263630f5cbe458309452726d26ee03b57eab3e927987245b5b3b5f7c11f032
6
+ metadata.gz: ceffbd048f44b6563298f301bde8dd498c1ea92be244e90a42390f4d387d58ef3837d936afbd4c63376ebfa2a20984989a2f8e1426ab9cccc30fc8c844c78a68
7
+ data.tar.gz: cc6e34b726934f01ae92bf089954bace7063619ce8e3d71b1d1eea610d153f10641257399388ef70bcd7730b1aea89039d4f7e6e2b960f2b09e937fd4d802969
data/.editorconfig CHANGED
@@ -9,6 +9,10 @@ end_of_line = lf
9
9
  insert_final_newline = true
10
10
  trim_trailing_whitespace = true
11
11
 
12
+ [*.h]
13
+ indent_style = space
14
+ indent_size = 2
15
+
12
16
  [*.c]
13
17
  indent_style = space
14
18
  indent_size = 2
data/README.adoc CHANGED
@@ -4,8 +4,18 @@
4
4
  :toclevels: 4
5
5
  :toc-title:
6
6
 
7
+ A Ruby gem for beautiful backtraces.
8
+
9
+ Aims to offer alternatives to `Thread#backtrace`, `Thread#backtrace_locations`, `Kernel#caller` and `Kernel#caller_locations` with similar (or better!?) performance, but with better metadata (such as class names).
10
+
11
+ WARNING: **⚠️ This gem is currently an exploration ground for the use of low-level techniques to get at internal Ruby VM structures.
12
+ Due to the way it works, bugs can easily cause your whole Ruby VM to crash;
13
+ Please take this into account if you plan on using this gem.
14
+ Still, contributions and bug reports are mega welcome, and I hope to one day be able to remove this warning ⚠️.**
15
+
7
16
  image:https://img.shields.io/badge/Contributor%20Covenant-2.0-4baaaa.svg["Contributor Covenant", link="CODE_OF_CONDUCT.adoc"]
8
17
  image:https://badge.fury.io/rb/backtracie.svg["Gem Version", link="https://badge.fury.io/rb/backtracie"]
18
+ image:https://github.com/ivoanjo/backtracie/actions/workflows/test.yml/badge.svg["CI Status", link="https://github.com/ivoanjo/backtracie/actions/workflows/test.yml"]
9
19
 
10
20
  [discrete]
11
21
  == Contents
@@ -39,7 +49,75 @@ This gem is versioned according to http://semver.org/spec/v2.0.0.html[Semantic V
39
49
 
40
50
  == Usage
41
51
 
42
- TODO
52
+ Currently, `backtracie` exposes two APIs:
53
+
54
+ * `Backtracie.backtrace_locations(thread)`: Returns an array representing the backtrace of the given `thread`. Similar to `Thread#backtrace_locations`.
55
+ * `Backtracie.caller_locations`: Returns an array representing the backtrace of the current thread, starting from the caller of the current method. Similar to `Kernel#caller_locations`.
56
+
57
+ These methods, inspired by their original Ruby counterparts, return arrays of `Backtracie::Location` items. These items return A LOT more information than Ruby usually exposes:
58
+
59
+ [source,ruby]
60
+ ----
61
+ $ bundle exec pry
62
+ [1] pry(main)> require 'backtracie'
63
+
64
+ [2] pry(main)> class Example
65
+ [2] pry(main)* def foo
66
+ [2] pry(main)* bar
67
+ [2] pry(main)* end
68
+ [2] pry(main)* def bar
69
+ [2] pry(main)* Backtracie.caller_locations.first
70
+ [2] pry(main)* end
71
+ [2] pry(main)* end
72
+
73
+ [3] pry(main)> Example.new.foo
74
+ => #<Backtracie::Location:0x0000561b236c9208
75
+ @absolute_path="(pry)",
76
+ @base_label="foo",
77
+ @debug=
78
+ {:ruby_frame?=>true,
79
+ :should_use_iseq=>false,
80
+ :should_use_cfunc_name=>false,
81
+ :vm_method_type=>0,
82
+ :line_number=>3,
83
+ :called_id=>:foo,
84
+ :defined_class_refinement?=>false,
85
+ :self_class=>Example,
86
+ :real_class=>Example,
87
+ :self=>#<Example:0x0000561b236f3a08>,
88
+ :self_class_singleton?=>false,
89
+ :iseq_is_block?=>false,
90
+ :iseq_is_eval?=>false,
91
+ :cfunc_name=>nil,
92
+ :iseq=>
93
+ {:path=>"(pry)",
94
+ :absolute_path=>"(pry)",
95
+ :label=>"foo",
96
+ :base_label=>"foo",
97
+ :full_label=>"foo",
98
+ :first_lineno=>2,
99
+ :classpath=>nil,
100
+ :singleton_method_p=>false,
101
+ :method_name=>"foo",
102
+ :qualified_method_name=>"foo"},
103
+ :callable_method_entry=>
104
+ {:path=>"(pry)",
105
+ :absolute_path=>"(pry)",
106
+ :label=>"foo",
107
+ :base_label=>"foo",
108
+ :full_label=>"Example#foo",
109
+ :first_lineno=>2,
110
+ :classpath=>"Example",
111
+ :singleton_method_p=>false,
112
+ :method_name=>"foo",
113
+ :qualified_method_name=>"Example#foo"}},
114
+ @label="foo",
115
+ @lineno=3,
116
+ @path="(pry)",
117
+ @qualified_method_name="Example#foo">
118
+ ----
119
+
120
+ This information can be used to to create much richer stack traces than the ones exposed by Ruby, including details such as class and module names, if methods are singletons, etc.
43
121
 
44
122
  == Development
45
123
 
@@ -49,13 +127,16 @@ To open a console with the gem loaded, run `bundle console`.
49
127
 
50
128
  To install this gem onto your local machine, run `bundle exec rake install`. To release a new version, update the version number in `version.rb`, and then run `bundle exec rake release`, which will create a git tag for the version, push git commits and tags, and push the `.gem` file to https://rubygems.org[rubygems.org].
51
129
 
130
+ To test on specific Ruby versions you can use docker. E.g. to test on Ruby 2.6, use `docker-compose run ruby-2.6`.
131
+ To test on all rubies using docker, you can use `bundle exec rake test-all`.
132
+
52
133
  == Feedback and success stories
53
134
 
54
135
  Your feedback is welcome!
55
136
 
56
137
  == Contributing
57
138
 
58
- Bug reports and pull requests are welcome on GitLab at https://github.com/ivoanjo/backtracie.
139
+ Bug reports and pull requests are welcome at https://github.com/ivoanjo/backtracie.
59
140
 
60
141
  This project is intended to be a safe, welcoming space for collaboration, and contributors are expected to adhere to the http://contributor-covenant.org[Contributor Covenant] code of conduct.
61
142
 
@@ -64,3 +145,27 @@ Maintained with ❤️ by https://ivoanjo.me/[Ivo Anjo].
64
145
  == Code of Conduct
65
146
 
66
147
  Everyone interacting in the backtracie project’s codebases, issue trackers, chat rooms and mailing lists is expected to follow the link:CODE_OF_CONDUCT.adoc[code of conduct].
148
+
149
+ == Interesting Links
150
+
151
+ Here's some gems that are doing similar things to `backtracie`:
152
+
153
+ * https://github.com/tmm1/stackprof: A sampling call-stack profiler for Ruby
154
+ * https://github.com/ko1/pretty_backtrace: Pretty your exception backtrace
155
+ * https://github.com/Shopify/stack_frames: This library allows backtraces to be captured and accessed without object allocations by leveraging MRI's profile frames API
156
+
157
+ Other interesting links on this matter:
158
+
159
+ * https://github.com/ruby/ruby/pull/3299: vm_backtrace.c: let rb_profile_frames show cfunc frames
160
+ * https://github.com/ruby/ruby/pull/2713: Fix use of the rb_profile_frames start parameter
161
+ * https://github.com/rake-compiler/rake-compiler: Provide a standard and simplified way to build and package Ruby C and Java extensions using Rake as glue.
162
+ * https://github.com/ko1/rubyhackchallenge: "Ruby Hack Challenge" (RHC) is a short guide to hack MRI (Matz Ruby Interpreter) internals
163
+ * https://docs.ruby-lang.org/en/3.0.0/doc/extension_rdoc.html: Creating Extension Libraries for Ruby
164
+ * https://ruby-hacking-guide.github.io/: Ruby Hacking Guide
165
+ ** This is one of the most through and deep guides out there to the MRI internals. Very detailed and in depth, but outdated.
166
+ * http://patshaughnessy.net/ruby-under-a-microscope: Book with a really good introduction to MRI internals.
167
+
168
+ My blog posts on better backtraces:
169
+
170
+ * https://ivoanjo.me/blog/2020/07/05/ruby-experiment-include-class-names-in-backtraces/: ruby experiment: include class names in backtraces
171
+ * https://ivoanjo.me/blog/2020/07/19/better-backtraces-in-ruby-using-tracepoint/: better backtraces in ruby using tracepoint
data/backtracie.gemspec CHANGED
@@ -32,12 +32,21 @@ Gem::Specification.new do |spec|
32
32
  spec.description = "Ruby gem for beautiful backtraces"
33
33
  spec.homepage = "https://github.com/ivoanjo/backtracie"
34
34
  spec.license = "LGPL-3.0+"
35
- spec.required_ruby_version = ">= 3.0.0"
35
+ spec.required_ruby_version = ">= 2.3.0"
36
36
 
37
37
  # Specify which files should be added to the gem when it is released.
38
38
  # The `git ls-files -z` loads the files in the RubyGem that have been added into git.
39
39
  spec.files = Dir.chdir(File.expand_path(__dir__)) do
40
- `git ls-files -z`.split("\x0").reject { |f| f.match(%r{\A(?:test|spec|features)/}) }
40
+ `git ls-files -z`.split("\x0")
41
+ .reject { |f| f.match(%r{\A(?:test|spec|features|[.]github)/}) }
42
+ .reject { |f|
43
+ ["gems.rb", ".whitesource", ".ruby-version", ".gitignore", ".rspec", ".standard.yml",
44
+ "DEVELOPMENT_NOTES.adoc", "Rakefile", "docker-compose.yml", "bin/console"].include?(f)
45
+ }
41
46
  end
42
47
  spec.require_paths = ["lib", "ext"]
48
+ spec.extensions = ["ext/backtracie_native_extension/extconf.rb"]
49
+
50
+ # Enables support for Ruby 2.5 and below. Not used at all in the others.
51
+ spec.add_dependency "debase-ruby_core_source", "~> 0.10", ">= 0.10.12"
43
52
  end
@@ -16,44 +16,52 @@
16
16
  // You should have received a copy of the GNU Lesser General Public License
17
17
  // along with backtracie. If not, see <http://www.gnu.org/licenses/>.
18
18
 
19
+ #include <stdbool.h>
20
+
19
21
  #include "ruby/ruby.h"
20
22
  #include "ruby/debug.h"
21
23
 
22
24
  #include "extconf.h"
23
25
 
24
- #include "ruby_3.0.0.h"
25
-
26
- // Constants
26
+ #include "ruby_shards.h"
27
27
 
28
28
  #define MAX_STACK_DEPTH 2000 // FIXME: Need to handle when this is not enough
29
29
 
30
- // Globals
30
+ #define VALUE_COUNT(array) (sizeof(array) / sizeof(VALUE))
31
+ #define SAFE_NAVIGATION(function, maybe_nil) ((maybe_nil) != Qnil ? function(maybe_nil) : Qnil)
31
32
 
33
+ static VALUE main_object_instance = Qnil;
34
+ static ID ensure_object_is_thread_id;
35
+ static ID to_s_id;
32
36
  static VALUE backtracie_module = Qnil;
33
37
  static VALUE backtracie_location_class = Qnil;
34
38
 
35
- // Function headers
36
-
37
39
  static VALUE primitive_caller_locations(VALUE self);
38
40
  static VALUE primitive_backtrace_locations(VALUE self, VALUE thread);
39
- static VALUE caller_locations(VALUE self, VALUE thread, int ignored_stack_top_frames);
40
- inline static VALUE new_location(VALUE absolute_path, VALUE base_label, VALUE label, VALUE lineno, VALUE path, VALUE debug);
41
- static bool is_ruby_frame(VALUE ruby_frame);
42
- static VALUE ruby_frame_to_location(VALUE frame, VALUE last_ruby_line, VALUE correct_label);
43
- static VALUE cfunc_frame_to_location(VALUE frame, VALUE last_ruby_frame, VALUE last_ruby_line);
44
- static VALUE debug_frame(VALUE frame, VALUE type);
45
-
46
- // Macros
47
-
48
- #define VALUE_COUNT(array) (sizeof(array) / sizeof(VALUE))
41
+ static VALUE collect_backtrace_locations(VALUE self, VALUE thread, int ignored_stack_top_frames);
42
+ inline static VALUE new_location(VALUE absolute_path, VALUE base_label, VALUE label, VALUE lineno, VALUE path, VALUE qualified_method_name, VALUE debug);
43
+ static VALUE ruby_frame_to_location(raw_location *the_location);
44
+ static VALUE qualified_method_name_for_location(raw_location *the_location);
45
+ static VALUE cfunc_frame_to_location(raw_location *the_location, raw_location *last_ruby_location);
46
+ static VALUE frame_from_location(raw_location *the_location);
47
+ static VALUE qualified_method_name_for_block(raw_location *the_location);
48
+ static VALUE qualified_method_name_from_self(raw_location *the_location);
49
+ static bool is_self_class_singleton(raw_location *the_location);
50
+ static bool is_defined_class_a_refinement(raw_location *the_location);
51
+ static VALUE debug_raw_location(raw_location *the_location);
52
+ static VALUE debug_frame(VALUE frame);
53
+ static inline VALUE to_boolean(bool value);
49
54
 
50
55
  void Init_backtracie_native_extension(void) {
56
+ main_object_instance = rb_funcall(rb_const_get(rb_cObject, rb_intern("TOPLEVEL_BINDING")), rb_intern("eval"), 1, rb_str_new2("self"));
57
+ ensure_object_is_thread_id = rb_intern("ensure_object_is_thread");
58
+ to_s_id = rb_intern("to_s");
59
+
51
60
  backtracie_module = rb_const_get(rb_cObject, rb_intern("Backtracie"));
52
61
  rb_global_variable(&backtracie_module);
53
62
 
54
63
  rb_define_module_function(backtracie_module, "backtrace_locations", primitive_backtrace_locations, 1);
55
64
 
56
- // We need to keep a reference to Backtracie::Locations around, to create new instances
57
65
  backtracie_location_class = rb_const_get(backtracie_module, rb_intern("Location"));
58
66
  rb_global_variable(&backtracie_location_class);
59
67
 
@@ -63,47 +71,34 @@ void Init_backtracie_native_extension(void) {
63
71
  }
64
72
 
65
73
  // Get array of Backtracie::Locations for a given thread; if thread is nil, returns for the current thread
66
- static VALUE caller_locations(VALUE self, VALUE thread, int ignored_stack_top_frames) {
74
+ static VALUE collect_backtrace_locations(VALUE self, VALUE thread, int ignored_stack_top_frames) {
67
75
  int stack_depth = 0;
68
- VALUE frames[MAX_STACK_DEPTH];
69
- VALUE correct_labels[MAX_STACK_DEPTH];
70
- int lines[MAX_STACK_DEPTH];
76
+ raw_location raw_locations[MAX_STACK_DEPTH];
71
77
 
72
78
  if (thread == Qnil) {
73
79
  // Get for own thread
74
- stack_depth = modified_rb_profile_frames(0, MAX_STACK_DEPTH, frames, correct_labels, lines);
80
+ stack_depth = backtracie_rb_profile_frames(MAX_STACK_DEPTH, raw_locations);
75
81
  } else {
76
- stack_depth = modified_rb_profile_frames_for_thread(thread, 0, MAX_STACK_DEPTH, frames, correct_labels, lines);
82
+ stack_depth = backtracie_rb_profile_frames_for_thread(thread, MAX_STACK_DEPTH, raw_locations);
83
+ if (stack_depth == 0 && !backtracie_is_thread_alive(thread)) return Qnil;
77
84
  }
78
85
 
79
- // Ignore the last frame -- seems to be an uninteresting VM frame. MRI itself seems to ignore the last frame in
80
- // the implementation of backtrace_collect()
81
- int ignored_stack_bottom_frames = 1;
82
-
83
- stack_depth -= ignored_stack_bottom_frames;
84
-
85
86
  VALUE locations = rb_ary_new_capa(stack_depth - ignored_stack_top_frames);
86
87
 
87
88
  // MRI does not give us the path or line number for frames implemented using C code. The convention in
88
89
  // Kernel#caller_locations is to instead use the path and line number of the last Ruby frame seen.
89
90
  // Thus, we keep that frame here to able to replicate that behavior.
90
91
  // (This is why we also iterate the frames array backwards below -- so that it's easier to keep the last_ruby_frame)
91
- VALUE last_ruby_frame = Qnil;
92
- VALUE last_ruby_line = Qnil;
92
+ raw_location *last_ruby_location = 0;
93
93
 
94
94
  for (int i = stack_depth - 1; i >= ignored_stack_top_frames; i--) {
95
- VALUE frame = frames[i];
96
- int line = lines[i];
97
-
98
95
  VALUE location = Qnil;
99
96
 
100
- if (is_ruby_frame(frame)) {
101
- last_ruby_frame = frame;
102
- last_ruby_line = INT2FIX(line);
103
-
104
- location = ruby_frame_to_location(frame, last_ruby_line, correct_labels[i]);
97
+ if (raw_locations[i].is_ruby_frame) {
98
+ last_ruby_location = &raw_locations[i];
99
+ location = ruby_frame_to_location(&raw_locations[i]);
105
100
  } else {
106
- location = cfunc_frame_to_location(frame, last_ruby_frame, last_ruby_line);
101
+ location = cfunc_frame_to_location(&raw_locations[i], last_ruby_location);
107
102
  }
108
103
 
109
104
  rb_ary_store(locations, i - ignored_stack_top_frames, location);
@@ -119,67 +114,232 @@ static VALUE primitive_caller_locations(VALUE self) {
119
114
  // * the frame from the caller itself (since we're replicating the semantics of Kernel#caller_locations)
120
115
  int ignored_stack_top_frames = 3;
121
116
 
122
- return caller_locations(self, Qnil, ignored_stack_top_frames);
117
+ return collect_backtrace_locations(self, Qnil, ignored_stack_top_frames);
123
118
  }
124
119
 
125
120
  static VALUE primitive_backtrace_locations(VALUE self, VALUE thread) {
126
- rb_funcall(backtracie_module, rb_intern("ensure_object_is_thread"), 1, thread);
121
+ rb_funcall(backtracie_module, ensure_object_is_thread_id, 1, thread);
127
122
 
128
123
  int ignored_stack_top_frames = 0;
129
124
 
130
- return caller_locations(self, thread, ignored_stack_top_frames);
125
+ return collect_backtrace_locations(self, thread, ignored_stack_top_frames);
131
126
  }
132
127
 
133
- inline static VALUE new_location(VALUE absolute_path, VALUE base_label, VALUE label, VALUE lineno, VALUE path, VALUE debug) {
134
- VALUE arguments[] = { absolute_path, base_label, label, lineno, path, debug };
128
+ inline static VALUE new_location(
129
+ VALUE absolute_path,
130
+ VALUE base_label,
131
+ VALUE label,
132
+ VALUE lineno,
133
+ VALUE path,
134
+ VALUE qualified_method_name,
135
+ VALUE debug
136
+ ) {
137
+ VALUE arguments[] = { absolute_path, base_label, label, lineno, path, qualified_method_name, debug };
135
138
  return rb_class_new_instance(VALUE_COUNT(arguments), arguments, backtracie_location_class);
136
139
  }
137
140
 
138
- static bool is_ruby_frame(VALUE frame) {
139
- VALUE absolute_path = rb_profile_frame_absolute_path(frame);
141
+ static VALUE ruby_frame_to_location(raw_location *the_location) {
142
+ VALUE frame = frame_from_location(the_location);
140
143
 
141
- return (rb_profile_frame_path(frame) != Qnil || absolute_path != Qnil) &&
142
- (rb_funcall(absolute_path, rb_intern("=="), 1, rb_str_new2("<cfunc>")) == Qfalse);
143
- }
144
-
145
- static VALUE ruby_frame_to_location(VALUE frame, VALUE last_ruby_line, VALUE correct_label) {
146
144
  return new_location(
147
145
  rb_profile_frame_absolute_path(frame),
148
146
  rb_profile_frame_base_label(frame),
149
- rb_profile_frame_label(correct_label),
150
- last_ruby_line,
147
+ rb_profile_frame_label(the_location->iseq),
148
+ INT2FIX(the_location->line_number),
151
149
  rb_profile_frame_path(frame),
152
- debug_frame(frame, rb_str_new2("ruby_frame"))
150
+ qualified_method_name_for_location(the_location),
151
+ debug_raw_location(the_location)
153
152
  );
154
153
  }
155
154
 
156
- static VALUE cfunc_frame_to_location(VALUE frame, VALUE last_ruby_frame, VALUE last_ruby_line) {
157
- VALUE method_name = rb_profile_frame_method_name(frame); // Replaces label and base_label in cfuncs
155
+ static VALUE qualified_method_name_for_location(raw_location *the_location) {
156
+ VALUE frame = frame_from_location(the_location);
157
+ VALUE defined_class = backtracie_defined_class(the_location);
158
+ VALUE qualified_method_name = Qnil;
159
+
160
+ if (is_defined_class_a_refinement(the_location)) {
161
+ qualified_method_name = backtracie_refinement_name(the_location);
162
+ rb_str_concat(qualified_method_name, rb_str_new2("#"));
163
+ rb_str_concat(qualified_method_name, rb_profile_frame_label(frame));
164
+ } else if (is_self_class_singleton(the_location)) {
165
+ qualified_method_name = qualified_method_name_from_self(the_location);
166
+ } else if (backtracie_iseq_is_block(the_location) || backtracie_iseq_is_eval(the_location)) {
167
+ qualified_method_name = qualified_method_name_for_block(the_location);
168
+ } else if (defined_class != Qnil && rb_mod_name(defined_class) == Qnil) {
169
+ // Instance of an anonymous class. Let's find it a name
170
+ VALUE superclass = defined_class;
171
+ VALUE superclass_name = Qnil;
172
+ do {
173
+ superclass = RCLASS_SUPER(superclass);
174
+ superclass_name = rb_mod_name(superclass);
175
+ } while (superclass_name == Qnil);
176
+
177
+ qualified_method_name = rb_str_new2("");
178
+ rb_str_concat(qualified_method_name, superclass_name);
179
+ rb_str_concat(qualified_method_name, rb_str_new2("$anonymous#"));
180
+ rb_str_concat(qualified_method_name, rb_profile_frame_base_label(frame_from_location(the_location)));
181
+ } else {
182
+ qualified_method_name = backtracie_rb_profile_frame_qualified_method_name(frame);
183
+
184
+ if (qualified_method_name == Qnil) {
185
+ qualified_method_name = qualified_method_name_from_self(the_location);
186
+ }
187
+ }
188
+
189
+ if (backtracie_iseq_is_block(the_location) || backtracie_iseq_is_eval(the_location)) {
190
+ rb_str_concat(qualified_method_name, rb_str_new2("{block}"));
191
+ }
192
+
193
+ return qualified_method_name;
194
+ }
195
+
196
+ static VALUE cfunc_frame_to_location(raw_location *the_location, raw_location *last_ruby_location) {
197
+ VALUE last_ruby_frame =
198
+ last_ruby_location != 0 ? frame_from_location(last_ruby_location) : Qnil;
199
+
200
+ // Replaces label and base_label in cfuncs
201
+ VALUE method_name = backtracie_rb_profile_frame_method_name(the_location->callable_method_entry);
158
202
 
159
203
  return new_location(
160
- last_ruby_frame != Qnil ? rb_profile_frame_absolute_path(last_ruby_frame) : Qnil,
204
+ SAFE_NAVIGATION(rb_profile_frame_absolute_path, last_ruby_frame),
161
205
  method_name,
162
206
  method_name,
163
- last_ruby_line,
164
- last_ruby_frame != Qnil ? rb_profile_frame_path(last_ruby_frame) : Qnil,
165
- debug_frame(frame, rb_str_new2("cfunc_frame"))
207
+ last_ruby_location != 0 ? INT2FIX(last_ruby_location->line_number) : Qnil,
208
+ SAFE_NAVIGATION(rb_profile_frame_path, last_ruby_frame),
209
+ backtracie_rb_profile_frame_qualified_method_name(the_location->callable_method_entry),
210
+ debug_raw_location(the_location)
166
211
  );
167
212
  }
168
213
 
169
- // Used to dump all the things we get from the rb_profile_frames API, for debugging
170
- static VALUE debug_frame(VALUE frame, VALUE type) {
214
+ static VALUE frame_from_location(raw_location *the_location) {
215
+ return \
216
+ the_location->should_use_iseq ||
217
+ // This one is somewhat weird, but the regular MRI Ruby APIs seem to pick the iseq for evals as well
218
+ backtracie_iseq_is_eval(the_location) ?
219
+ the_location->iseq : the_location->callable_method_entry;
220
+ }
221
+
222
+ static VALUE qualified_method_name_for_block(raw_location *the_location) {
223
+ VALUE class_name = backtracie_rb_profile_frame_classpath(the_location->callable_method_entry);
224
+ VALUE method_name = backtracie_called_id(the_location);
225
+ VALUE is_singleton_method = rb_profile_frame_singleton_method_p(the_location->iseq);
226
+
227
+ VALUE name = rb_str_new2("");
228
+ rb_str_concat(name, class_name);
229
+ rb_str_concat(name, is_singleton_method ? rb_str_new2(".") : rb_str_new2("#"));
230
+ rb_str_concat(name, rb_sym2str(method_name));
231
+
232
+ return name;
233
+ }
234
+
235
+ static VALUE qualified_method_name_from_self(raw_location *the_location) {
236
+ if (the_location->self == Qnil) return Qnil;
237
+
238
+ VALUE self_class = rb_class_of(the_location->self);
239
+ bool is_self_class_singleton = FL_TEST(self_class, FL_SINGLETON);
240
+
241
+ VALUE name = rb_str_new2("");
242
+ if (is_self_class_singleton) {
243
+ if (the_location->self == main_object_instance) {
244
+ rb_str_concat(name, rb_str_new2("Object$<main>#"));
245
+ } else {
246
+ // Crawl up the hierarchy to find a real class
247
+ VALUE the_class = rb_class_real(self_class);
248
+
249
+ // This is similar to BetterBacktraceHelper's self_class_module_or_class?
250
+ // If the real class of this object is the actual class Class or the class Module,
251
+ // it means that this is a method being directly called on a given Class/Module,
252
+ // e.g. Kernel.puts or BasicObject.name. In this case, Ruby already sets the name
253
+ // correctly, so we just delegate.
254
+ if (the_class == rb_cModule || the_class == rb_cClass) {
255
+ // Is the class/module is anonymous?
256
+ if (rb_mod_name(the_location->self) == Qnil) {
257
+ rb_str_concat(name, rb_funcall(the_class, to_s_id, 0));
258
+ rb_str_concat(name, rb_str_new2("$singleton."));
259
+ } else {
260
+ return SAFE_NAVIGATION(backtracie_rb_profile_frame_qualified_method_name, the_location->callable_method_entry);
261
+ }
262
+ } else {
263
+ rb_str_concat(name, rb_funcall(the_class, to_s_id, 0));
264
+ rb_str_concat(name, rb_str_new2("$singleton#"));
265
+ }
266
+ }
267
+ } else {
268
+ // Not very sure if this branch of the if is ever reached, and if it would be for a instance or static call, so
269
+ // let's just have these defaults and revisit as needed
270
+ rb_str_concat(name, rb_funcall(self_class, to_s_id, 0));
271
+ rb_str_concat(name, rb_str_new2("#"));
272
+ }
273
+
274
+ if (backtracie_iseq_is_block(the_location) || backtracie_iseq_is_eval(the_location)) {
275
+ // Nothing to do, {block} will be appended in qualified_method_name_for_location which called us
276
+ } else {
277
+ rb_str_concat(name, rb_profile_frame_base_label(frame_from_location(the_location)));
278
+ }
279
+
280
+ return name;
281
+ }
282
+
283
+ static bool is_self_class_singleton(raw_location *the_location) {
284
+ return the_location->self != Qnil && FL_TEST(rb_class_of(the_location->self), FL_SINGLETON);
285
+ }
286
+
287
+ static bool is_defined_class_a_refinement(raw_location *the_location) {
288
+ VALUE defined_class = backtracie_defined_class(the_location);
289
+
290
+ return defined_class != Qnil && FL_TEST(rb_class_of(defined_class), RMODULE_IS_REFINEMENT);
291
+ }
292
+
293
+ static VALUE debug_raw_location(raw_location *the_location) {
294
+ VALUE self_class = SAFE_NAVIGATION(rb_class_of, the_location->self);
295
+
171
296
  VALUE arguments[] = {
172
- rb_profile_frame_path(frame),
173
- rb_profile_frame_absolute_path(frame),
174
- rb_profile_frame_label(frame),
175
- rb_profile_frame_base_label(frame),
176
- rb_profile_frame_full_label(frame),
177
- rb_profile_frame_first_lineno(frame),
178
- rb_profile_frame_classpath(frame),
179
- rb_profile_frame_singleton_method_p(frame),
180
- rb_profile_frame_method_name(frame),
181
- rb_profile_frame_qualified_method_name(frame),
182
- type
297
+ ID2SYM(rb_intern("ruby_frame?")) , /* => */ to_boolean(the_location->is_ruby_frame),
298
+ ID2SYM(rb_intern("should_use_iseq")), /* => */ to_boolean(the_location->should_use_iseq),
299
+ ID2SYM(rb_intern("vm_method_type")), /* => */ INT2FIX(the_location->vm_method_type),
300
+ ID2SYM(rb_intern("line_number")), /* => */ INT2FIX(the_location->line_number),
301
+ ID2SYM(rb_intern("called_id")), /* => */ backtracie_called_id(the_location),
302
+ // TODO: On Ruby < 3.0, running be pry -e 'require "backtracie"; Backtracie.caller_locations' with this being
303
+ // exposed causes a VM segfault inside the pretty printing code
304
+ // ID2SYM(rb_intern("defined_class")), /* => */ backtracie_defined_class(the_location),
305
+ ID2SYM(rb_intern("defined_class_refinement?")), /* => */ to_boolean(is_defined_class_a_refinement(the_location)),
306
+ ID2SYM(rb_intern("self_class")), /* => */ self_class,
307
+ ID2SYM(rb_intern("real_class")), /* => */ SAFE_NAVIGATION(rb_class_real, self_class),
308
+ ID2SYM(rb_intern("self")), /* => */ the_location->self,
309
+ ID2SYM(rb_intern("self_class_singleton?")), /* => */ to_boolean(is_self_class_singleton(the_location)),
310
+ ID2SYM(rb_intern("iseq_is_block?")), /* => */ to_boolean(backtracie_iseq_is_block(the_location)),
311
+ ID2SYM(rb_intern("iseq_is_eval?")), /* => */ to_boolean(backtracie_iseq_is_eval(the_location)),
312
+ ID2SYM(rb_intern("iseq")), /* => */ debug_frame(the_location->iseq),
313
+ ID2SYM(rb_intern("callable_method_entry")), /* => */ debug_frame(the_location->callable_method_entry),
314
+ ID2SYM(rb_intern("original_id")), /* => */ the_location->original_id
183
315
  };
184
- return rb_ary_new_from_values(VALUE_COUNT(arguments), arguments);
316
+
317
+ VALUE debug_hash = rb_hash_new();
318
+ for (long unsigned int i = 0; i < VALUE_COUNT(arguments); i += 2) rb_hash_aset(debug_hash, arguments[i], arguments[i+1]);
319
+ return debug_hash;
320
+ }
321
+
322
+ static VALUE debug_frame(VALUE frame) {
323
+ if (frame == Qnil) return Qnil;
324
+
325
+ VALUE arguments[] = {
326
+ ID2SYM(rb_intern("path")), /* => */ rb_profile_frame_path(frame),
327
+ ID2SYM(rb_intern("absolute_path")), /* => */ rb_profile_frame_absolute_path(frame),
328
+ ID2SYM(rb_intern("label")), /* => */ rb_profile_frame_label(frame),
329
+ ID2SYM(rb_intern("base_label")), /* => */ rb_profile_frame_base_label(frame),
330
+ ID2SYM(rb_intern("full_label")), /* => */ rb_profile_frame_full_label(frame),
331
+ ID2SYM(rb_intern("first_lineno")), /* => */ rb_profile_frame_first_lineno(frame),
332
+ ID2SYM(rb_intern("classpath")), /* => */ backtracie_rb_profile_frame_classpath(frame),
333
+ ID2SYM(rb_intern("singleton_method_p")), /* => */ rb_profile_frame_singleton_method_p(frame),
334
+ ID2SYM(rb_intern("method_name")), /* => */ rb_profile_frame_method_name(frame),
335
+ ID2SYM(rb_intern("qualified_method_name")), /* => */ backtracie_rb_profile_frame_qualified_method_name(frame)
336
+ };
337
+
338
+ VALUE debug_hash = rb_hash_new();
339
+ for (long unsigned int i = 0; i < VALUE_COUNT(arguments); i += 2) rb_hash_aset(debug_hash, arguments[i], arguments[i+1]);
340
+ return debug_hash;
341
+ }
342
+
343
+ static inline VALUE to_boolean(bool value) {
344
+ return value ? Qtrue : Qfalse;
185
345
  }